4.6 x64dbg 内存扫描与查壳实现

LyScript 插件中默认提供了多种内存特征扫描函数,每一种扫描函数用法各不相同,在使用扫描函数时应首先搞清楚不同函数之间的差异,本章内容将分别详细介绍每一种内存扫描函数是如何灵活运用,并实现一种内存查壳脚本,可快速定位目标程序加了什么壳以及寻找被加壳程序的入口点。

计算机中的壳定义

加壳通常指对可执行文件或者动态链接库等二进制文件进行加密或压缩等处理,以使得这些文件难以被反汇编或破解。通常情况下,加壳会增加二进制文件的大小,并在程序运行时增加一定的开销。加壳技术通常被用于保护软件的知识产权和防止软件被盗版。通过加壳,软件开发者可以使得软件更难被破解和复制,从而保护自己的商业利益。

计算机中查壳的原理

软件查壳的实现原理可以分为静态分析和动态分析两种方式。静态分析是指在不运行被加壳程序的情况下,通过对程序的二进制代码进行解析,识别出程序是否被加壳,以及加壳的种类和方法。动态分析是指通过运行被加壳程序,并观察程序运行时的行为,识别程序是否被加壳,以及加壳的种类和方法。

静态分析的实现原理通常包括以下几个步骤:

  • 提取被分析程序的二进制代码;
  • 识别程序的文件格式,并解析文件头等元数据信息;
  • 根据文件格式,从程序代码中提取节区、导入表、导出表等重要信息;
  • 分析程序的代码段,并检查代码中是否存在加壳相关的特征,如代码的执行流程、加密算法等;
  • 根据分析结果,判断程序是否被加壳,以及加壳的种类和方法。

静态分析的优点是分析速度快,不需要运行程序,可以有效地识别出程序中的加壳。但是它也有一些缺点,例如无法识别动态加载的加壳、易受代码混淆和反调试等技术的影响。

动态分析的实现原理通常包括以下几个步骤:

  • 启动被分析程序,并在程序运行期间捕捉程序的行为;
  • 跟踪程序的执行流程,并分析程序的内存、寄存器、堆栈等状态信息;
  • 检查程序的内存中是否存在加壳相关的特征,如解密函数、加壳程序等;
  • 根据分析结果,判断程序是否被加壳,以及加壳的种类和方法。

动态分析的优点是可以识别动态加载的加壳,并且可以有效地避免代码混淆和反调试等技术的影响。但是它也有一些缺点,例如需要运行被分析程序,因此可能会影响程序的性能和稳定性,并且可能会被加壳程序的反调试技术所绕过。

本例中将采用scan_memory_all()函数对特定内存进行动态扫描,该函数用来扫描当前进程内EIP所指向位置处整个内存段中符合条件的特征,如果找到了则返回一个列表,如果没有找到则返回False,该函数与scan_memory_one()函数原理是一致的,唯一的不同是all以列表形式返回所有匹配到的行,one则只返回匹配到的第一条记录,这两个函数都支持??模糊匹配。

如果载入一个程序,默认停留在系统领空,则调用该函数你所能得到的特征记录只能是系统领空当前dll内的特征集。

例如在程序被载入后,其EIP指针默认会停留在ntdll.dll模块上,需要注意的是,函数scan_memory_one用于扫描并返回第一段符合条件的内存,函数scan_memory_all则用于扫描并输出当前所有符合条件的内存地址;

>>> from LyScript32 import MyDebug
>>>
>>> dbg = MyDebug()
>>> conn = dbg.connect()
>>>
>>> ref_one = dbg.scan_memory_one("55 8b ec")
>>> ref_one
1995793090
>>>
>>> ref_all = dbg.scan_memory_all("55 8b ec")
>>> ref_all
[1995793090, 1995793298, 1995793455, 1995793799, 1995794214]

而有时,我们需要指定扫描某个模块,例如扫描进程内的msvcr120.dll模块里面的特征,则此时读者可调用scan_memory_any()函数,该函数无需切换到模块入口处即可实现扫描特定模块内的特征,不过该函数只能返回找到的第一条记录,且需要传入扫描起始位置以及扫描长度,不过得到这些参数并不难。

from LyScript32 import MyDebug

if __name__ == "__main__":
    dbg = MyDebug()
    conn = dbg.connect()

    # 得到进程模块
    local_module = dbg.get_all_module()[0]

    # 得到模块参数
    module_base = local_module.get("base")
    module_size = local_module.get("size")
    print("基地址: {} 长度: {} 结束地址: {}".format(hex(module_base),hex(module_size),hex(module_base+module_size)))

    # 扫描内存
    ref = dbg.scan_memory_any(module_base,module_size,"55 8b ec ??")
    if ref != False:
        print("找到内存: {}".format(hex(ref)))
    dbg.close()

运行如上所示的代码片段,则默认会扫描进程内第一个模块也就是主程序模块内的,特征55 8b ec ??指令集,当找到后会输出如下图所示的提示信息;

如上代码中的内存扫描方法如果能被读者理解,那么查壳这个功能就变得很简单了,市面上的查壳软件PEID等基本都是采用特征码定位的方式,所以我们想要实现查壳以及检测编译器特征可以采用特征码扫描法,只需要将上述代码更改一下,即可实现查壳功能。

from LyScript32 import MyDebug

# 查壳功能
def Scan(dbg, string):
    # 得到进程模块
    local_module = dbg.get_all_module()[0]

    # 得到模块参数
    module_base = local_module.get("base")
    module_size = local_module.get("size")
    # print("基地址: {} 长度: {} 结束地址: {}".format(hex(module_base),hex(module_size),hex(module_base+module_size)))

    # 扫描内存
    ref = dbg.scan_memory_any(module_base,module_size,string)
    if ref != False:
        return True
    return False

if __name__ == "__main__":
    dbg = MyDebug()
    conn = dbg.connect()

    # 存储特征码
    signs = [
        {"key": "Microsoft Visual C++ 2013", "value": "e8 ?? ?? ?? ?? e9 ?? ?? ?? ?? 55 8b ec"},
        {"key": "UPX 3.96w", "value": "60 be ?? ?? ?? ?? 8d be 00 90 ff ff 57"}
    ]

    for index in signs:
        check = Scan(dbg, index.get("value"))
        if check == True:
            print("编译特征: {}".format(index.get("key")))

    dbg.close()

当运行上述代码片段,则将会寻找特定指令集,如果找到则说明采用的是特定的加壳程序或特定编译器编译生成的,输出效果图如下所示;

通过运用scan_memory_one()函数,读者何以很容易的实现对特定内存区域的寻找,以32为代码为例,通过get_register函数获取到当前EIP的内存地址,并通过read_memory_word读入前四个字节的数据,当前四个字节的数据为0xBE60此时说明是UPX壳的开头,通过scan_memory_one("83 EC ?? E9")匹配特征码,当找到后顺势在此处set_breakpoint(patternAddr)下一个断点,程序通过不断地运行,最终即可定位到程序真正的入口地址;

from LyScript32 import MyDebug

if __name__ == "__main__":
    # 初始化
    dbg = MyDebug()

    # 连接到调试器
    connect_flag = dbg.connect()
    print("连接状态: {}".format(connect_flag))

    # 检测套接字是否还在
    ref = dbg.is_connect()
    print("是否在连接: ", ref)

    is_64 = False

    # 判断是否时64位数
    if is_64 == False:
        currentIP = dbg.get_register("eip")

        if dbg.read_memory_word(currentIP) != int(0xBE60):
            print("[-] 可能不是UPX")
            dbg.close()

        patternAddr = dbg.scan_memory_one("83 EC ?? E9")
        print("匹配到的地址: {}".format(hex(patternAddr)))

        dbg.set_breakpoint(patternAddr)
        dbg.set_debug("Run")
        dbg.set_debug("Wait")
        dbg.delete_breakpoint(patternAddr)

        dbg.set_debug("StepOver")
        dbg.set_debug("StepOver")
        print("[+] 程序OEP = 0x{:x}".format(dbg.get_register("eip")))

    else:
        currentIP = dbg.get_register("rip")

        if dbg.read_memory_dword(currentIP) != int(0x55575653):
            print("[-] 可能不是UPX")
            dbg.close()

        patternAddr = dbg.scan_memory_one("48 83 EC ?? E9")
        print("匹配到的地址: {}".format(hex(patternAddr)))

        dbg.set_breakpoint(patternAddr)
        dbg.set_debug("Run")
        dbg.set_debug("Wait")
        dbg.delete_breakpoint(patternAddr)

        dbg.set_debug("StepOver")
        dbg.set_debug("StepOver")
        print("[+] 程序OEP = 0x{:x}".format(dbg.get_register("eip")))

    dbg.close()

运行上述代码片段,则会看到如下图所示的输出效果,并以自动跳转到了程序的真正入口位置0x40153e,此时用户只需要脱壳转存即可;

posted @ 2023-07-09 16:17  lyshark  阅读(732)  评论(0编辑  收藏  举报

loading... | loading...
博客园 - 开发者的网上家园