💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 12.3 IDAPyEmu 我们的第一个例子就是在 IDA Pro 分析程序的时候,使用 PyEmu 仿真一次简单的函数 调用。这次实验的程序就是 addnum.exe,主要功能就是从命令行中接收两个参数,然后相 加,再输出结果,代码使用 C++ 编写,可从 [http://www.nostarch.com/ghpython.htm](http://www.nostarch.com/ghpython.htm) 下载。 ``` /*addnum.cpp*/ #include <stdlib.h> #include <stdio.h> #include <windows.h> int add_number( int num1, int num2 ) { int sum; sum = num1 + num2; return sum; } int main(int argc, char* argv[]) { int num1, num2; int return_value; if( argc < 2 ) { printf("You need to enter two numbers to add.\n"); printf("addnum.exe num1 num2\n"); return 0; } num1 = atoi(argv[1]); num2 = atoi(argv[2]); return_value = add_number( num1, num2 ); printf("Sum of %d + %d = %d",num1, num2, return_value ); return 0; } ``` 程序将命令行传入的参数转换成整数,然后调用add_number函数相加。我们将 add_number 函数作为我们的仿真对象,因为它够简单而且结果也很容易验证,作为我们使 用 PyEmu 的起点是个不二选择。 在深入 PyEmu 使用之前,让我们看看 add_number 的反汇编代码。 ``` var_4= dword ptr -4 # sum variable arg_0= dword ptr 8 # int num1 arg_4= dword ptr 0Ch # int num2 push ebp mov ebp, esp push ecx mov eax, [ebp+arg_0] add eax, [ebp+arg_4] mov [ebp+var_4], eax mov eax, [ebp+var_4] mov esp, ebp pop ebp retn ``` Listing 12-1: add_number 的反汇编代码 var_4,arg_0,arg_4 分别是参数在栈中的位置,从 C++的反汇编代码中可以清楚的看 出,整个函数的执行流程,和参数的调用关系。我们将使用 PyEmu 仿真整个函数,也就是 上面列出的汇编代码,同时设置 arg_0 和 arg_4 为我们需要的任何数,最后 retn 返回的时候, 捕获 EAX 的值,也就是函数的返回值。虽然仿真的函数似乎过于简单,不过整个仿真过程 就是一切函数仿真的基础,一通百通。 ### 12.3.1 函数仿真 开始脚本编写,第一步确认 PyEmu 的路径设置正确。 ``` #addnum_function_call.py import sys sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import * ``` 设置好库路径之后,就要开始函数仿真部分的编写了。首先将我们逆向的程序的,代码 块和数据块映射到仿真器中,以便仿真器仿真运行。因为我们会使用 IDAPython 加载这些块,对相关函数不熟悉的同学,请翻到第十一章,认真阅读。 ``` #addnum_function_call.py ... emu = IDAPyEmu() # Load the binary's code segment code_start = SegByName(".text") code_end = SegEnd( code_start ) while code_start <= code_end: emu.set_memory( code_start, GetOriginalByte(code_start), size=1 ) code_start += 1 print "[*] Finished loading code section into memory." # Load the binary's data segment data_start = SegByName(".data") data_end = SegEnd( data_start ) while data_start <= data_end: emu.set_memory( data_start, GetOriginalByte(data_start), size=1 ) data_start += 1 print "[*] Finished loading data section into memory." ``` 使用任何仿真器方法之前都必须实例化一个 IDAPyEmu 对象。接着将代码块和数据块 加载进 PyEmu 的内存,名副其实的依葫芦画瓢喔。使用 IDAPython 的 SegByName()函数找 出块首,SegEnd()找出块尾。然后一个一个字节的将这些块中的数据拷贝到 PyEmu 的内存 中。代码和数据块都加载完成后,就要设置栈参数了,这些参数可以任意设置,最后再安装 一个 retn 指令处理函数。 ``` #addnum_function_call.py ... # Set EIP to start executing at the function head emu.set_register("EIP", 0x00401000) # Set up the ret handler emu.set_mnemonic_handler("ret", ret_handler) # Set the function parameters for the call emu.set_stack_argument(0x8, 0x00000001, name="arg_0") emu.set_stack_argument(0xc, 0x00000002, name="arg_4") # There are 10 instructions in this function emu.execute( steps = 10 ) print "[*] Finished function emulation run." ``` 首先将 EIP 指向到函数头,0x00401000,PyEmu 仿真器将从这里开始执行指令。接着, 在函数的 retn 指令上设置 助记符(mnemonic)或者指令处理函数(set_instruction_handler)。第 三步,设置栈参数以供函数调用。在这里设置成 0x00000001 和 0x00000002。最后让 PyEmu 执行完成整个函数 10 行代码。完整的代码如下。 ``` #addnum_function_call.py import sys sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import * def ret_handler(emu, address): num1 = emu.get_stack_argument("arg_0") num2 = emu.get_stack_argument("arg_4") sum = emu.get_register("EAX") print "[*] Function took: %d, %d and the result is %d." % (num1, n return True emu = IDAPyEmu() # Load the binary's code segment code_start = SegByName(".text") code_end = SegEnd( code_start ) while code_start <= code_end: emu.set_memory( code_start, GetOriginalByte(code_start), size=1 ) code_start += 1 print "[*] Finished loading code section into memory." # Load the binary's data segment data_start = SegByName(".data") data_end = SegEnd( data_start ) while data_start <= data_end: emu.set_memory( data_start, GetOriginalByte(data_start), size=1 ) data_start += 1 print "[*] Finished loading data section into memory." # Set EIP to start executing at the function head emu.set_register("EIP", 0x00401000) # Set up the ret handler emu.set_mnemonic_handler("ret", ret_handler) # Set the function parameters for the call emu.set_stack_argument(0x8, 0x00000001, name="arg_0") emu.set_stack_argument(0xc, 0x00000002, name="arg_4") # There are 10 instructions in this function emu.execute( steps = 10 ) print "[*] Finished function emulation run." ``` ret 指令处理函数简单的设置成检索出栈参数和 EAX 的值,最后再将它们打印出来。 用 IDA 加载 addnum.exe,然后将 PyEmu 脚本当作 IDAPython 文件调用。输出结果将如下: ``` [*] Finished loading code section into memory. [*] Finished loading data section into memory. [*] Function took 1, 2 and the result is 3. [*] Finished function emulation run. ``` Listing 12-2: IDAPyEmu 仿真函数的输出 很好很简单!整个过程很成功,栈参数和返回值都从捕获,说明函数仿真成功了。作为进一步的练习,各位可以加载不同的文件,随机的选择一个函数进行仿真,然后监视相关数 据的调用或者任何感兴趣的东西。某一天,当你遇到一个上千行的函数的时候,相信这种方 法能帮你从无数的分支,循环还有可怕的指针中拯救出来,它们节省的不仅仅是事件,更是 你的信心。接下来让我们用 PEPyEmu 库解压一个被压缩文件。 ### 12.3.2 PEPyEmu PEPyEmu 类用于可执行文件的静态分析(不需要 IDA Pro)。整个处理过程就是将磁盘 上的可执行文件映射到内存中,然后使用 pydasm 进行指令解码。下面的试验中,我们将通 过仿真器运行一个压缩过的可执行文件,然后把解压出来的原始文件转存到硬盘上。这次使 用的压缩软件就是 UPX(Ultimate Packer for Executables),一款伟大的开源压缩软件,同时也 是使用最广的压缩软件,用于最大程度的压缩可执行文件,同样也能被病毒软件用来迷惑分 析者。在使用自定义 PyEmu 脚本( Cody Pierce 提供 )对程序进行解压之前,让我们看看压缩 程序是怎么工作的。 ### 12.3.3 压缩程序 压缩程序由来已久。最早在我们使用 1.44 软盘的时候,压缩程序就用来尽可能的减少 程序大小(想当初我们的软盘上可是有上千号文件),随着事件的流逝,这项技术也渐渐成为 病毒开发中的一个主要部分,用来迷惑分析者。一个典型的压缩程序会将目标程序的代码段 和数据段进行压缩,然后将入口点替换成解压的代码。当程序执行的时候,解压代码就会将 原始代码加压进内存,然后跳到原始入口点 OEP(original entry point ),开始正常运行程序。 在我们分析调试任何压缩过的程序之前,也都必须解压它们。这时候你会想到用调试器完成 这项任务(因为各种丰富的脚本),不过现在的病毒一般都缴入反调试代码,用调试器进行 解压变得越来越困难。那怎么办呢?用仿真器。因为我们并没有附加到正在执行的程序,而 是将压缩过的代码拷贝到仿真器中运行,然后等待它自动解压完成,接着再把解压出来的原 始程序,转储到硬盘上。以后就能够正常的分析调试它们了。 这次我们选择 UPX 压缩 calc.exe。然后用 PyEmu 解压它,最后 dump 出来。记得这种 方法同样适用于别的压缩程序,万变不离其宗。 1. ### UPX UPX 是自由的,是开源的,是跨平台的(Linux Windows....)。提供不同的压缩级别,和 许多附加的选项,用于完成各种不同的压缩任务。我们使用默认的压缩方案,都让你可随意 的测试。 从 [http://upx.sourceforge.net](http://upx.sourceforge.net) 下载 UPX。 解压到 C 盘,官方没有提供图形界面,所以我们必须从命令行操作。打开 CMD,改 变当前目录到 C:\upx303w(也就是 UPX 解压的目录),输入以下命令: ``` C:\upx303w>upx -o c:\calc_upx.exe C:\Windows\system32\calc.exe Ultimate Packer for eXecutables Copyright (C) 1996 - 2008 UPX 3.03w Markus Oberhumer, Laszlo Molnar & John Reiser Apr 27th 2008 File size Ratio Format Name -------------------- ------ ----------- ----------- 114688 -> 56832 49.55% win32/pe calc_upx.exe Packed 1 file. C:\upx303w> ``` 成功的压缩了 Windows 的计算器,并且转储到了 C 盘下。 -o 为输出标志,指定输出文件名。接下来,终于到了 PEPyEmu 出马了。 2. 使用 PEPyEmu 解压 UPX UPX 压缩可执行程序的方法很简单明了:重写程序的入口点,指向解压代码,同时添 加两个而外的块,UPX0 和 UPX1。使用 Immunity 加载压缩程序,检查内存布局(ALT-M),将会看到如下相似的输出: ``` Address Size Owner Section Contains Access Initial Access 00100000 00001000 calc_upx PE Header R RWE 01001000 00019000 calc_upx UPX0 RWE RWE 0101A000 00007000 calc_upx UPX1 code RWE RWE 01021000 00007000 calc_upx .rsrc data,imports RW RWE resources ``` Listing 12-3: UPX 压缩之后的程序的内存布局. UPX1 显示为代码块,其中包含了主要的解压代码。代码经过 UPX1 的解压之后,就跳 出 UPX1 块,到达真正的可执行代码块,开始执行程序。我们要做的就是让仿真器运行解压 代码,同时不断的检测 EIP 和 JMP,当发现有 JMP 指令使得 EIP 的范围超出 UPX1 段的时 候,说明将到跳转到原始代码段了。 接下来开始代码的编写,这次我们只使用独立的 PEPyEmu 模块。 ``` #upx_unpacker.py from ctypes import * # You must set your path to pyemu sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import PEPyEmu # Commandline arguments exename = sys.argv[1] outputfile = sys.argv[2] # Instantiate our emulator object emu = PEPyEmu() if exename: # Load the binary into PyEmu if not emu.load(exename): print "[!] Problem loading %s" % exename sys.exit(2) else: print "[!] Blank filename specified" sys.exit(3) # Set our library handlers emu.set_library_handler("LoadLibraryA", loadlibrary) emu.set_library_handler("GetProcAddress", getprocaddress) emu.set_library_handler("VirtualProtect", virtualprotect) # Set a breakpoint at the real entry point to dump binary emu.set_mnemonic_handler( "jmp", jmp_handler ) # Execute starting from the header entry point emu.execute( start=emu.entry_point ) ``` 第 一 步 将 压 缩 文 件 加 载 进 PyEmu 。 第 二 部 , 在 LoadLibraryA, GetProcAddress, VirtualProtect 三个函数上设置库处理函数。这些函数都将在解压代码中调用,这些操作必须 我们自己在仿真器中完成。第三步,在解压程序执行完成准备跳到 OEP 的时候,我们将进 行相关的操作,这个任务就有 JMP 指令处理函数完成。最后告诉仿真器,从压缩程序头部 开始执行代码。 ``` #upx_unpacker.py from ctypes import * # You must set your path to pyemu sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import PEPyEmu ''' HMODULE WINAPI LoadLibrary( in LPCTSTR lpFileName ); ''' def loadlibrary(name, address): # Retrieve the DLL name dllname = emu.get_memory_string(emu.get_memory(emu.get_register("ESP"))) # Make a real call to LoadLibrary and return the handle dllhandle = windll.kernel32.LoadLibraryA(dllname) emu.set_register("EAX", dllhandle) # Reset the stack and return from the handler return_address = emu.get_memory(emu.get_register("ESP")) emu.set_register("ESP", emu.get_register("ESP") + 8) emu.set_register("EIP", return_address) return True ''' FARPROC WINAPI GetProcAddress( in HMODULE hModule, in LPCSTR lpProcName ); ''' def getprocaddress(name, address): # Get both arguments, which are a handle and the procedure name handle = emu.get_memory(emu.get_register("ESP") + 4) proc_name = emu.get_memory(emu.get_register("ESP") + 8) # lpProcName can be a name or ordinal, if top word is null it's an ordinal # lpProcName 的高 16 位是 null 的时候,它就是序列号(也就是个地址),否者就是名字 if (proc_name >> 16): procname = emu.get_memory_string(emu.get_memory(emu.get_register("ESP") + 8)) else: procname = arg2 #这 arg2 不知道从何而来,应该是 procname = proc_name # Add the procedure to the emulator emu.os.add_library(handle, procname) import_address = emu.os.get_library_address(procname) # Return the import address emu.set_register("EAX", import_address) # Reset the stack and return from our handler return_address = emu.get_memory(emu.get_register("ESP")) emu.set_register("ESP", emu.get_register("ESP") + 8) #这里应该是 r("ESP") + 8,因为有两个参数需要平衡 emu.set_register("EIP", return_address) return True ''' BOOL WINAPI VirtualProtect( in LPVOID lpAddress, in SIZE_T dwSize, in DWORD flNewProtect, out PDWORD lpflOldProtect ); ''' def virtualprotect(name, address): # Just return TRUE emu.set_register("EAX", 1) # Reset the stack and return from our handler return_address = emu.get_memory(emu.get_register("ESP")) emu.set_register("ESP", emu.get_register("ESP") + 16) emu.set_register("EIP", return_address) return True # When the unpacking routine is finished, handle the JMP to the OEP def jmp_handler(emu, mnemonic, eip, op1, op2, op3): # The UPX1 section if eip < emu.sections["UPX1"]["base"]: print "[*] We are jumping out of the unpacking routine." print "[*] OEP = 0x%08x" % eip # Dump the unpacked binary to disk dump_unpacked(emu) # We can stop emulating now emu.emulating = False return True ``` LoadLibrary 处理函数从栈中捕捉到调用的 DLL 的名字,然后使用 ctypes 库函数进 行真正的 LoadLibraryA 调用,这个函数由 kernel32.dll 导出。调用成功返回后,将句柄传递 给 EAX 寄存器,重新调整仿真器栈,最后重处理函数返回。同样, GetProcAddress 处理函 数 从 栈 中 接 收 两 个 参 数(arg2) , 然 后 在 仿 真 器 中 进 行 真 实 的 调 用(emu.os.add_library 和 emu.os.get_library_address) , 这 个 函 数 也 由 kernel32.dll 导 出 ( 当 然 也 可 以 使 用 windll.kernel32.GetProcAddress) 。 之 后 把 地 址 存 储 到 EAX , 调 整 栈 ( 这 里 原 作 者 使 用 emu.set_register("ESP", emu.get_register("ESP") + 8),不过由于是两个参数,应该是+12),返 回。第三个 VirtualProtect 处理函数,只是简单的返回一个 True 值,接着就是一样的栈处理 和从函数中返回。之所以这样做,是因为我们不需要真正的保护内存中的某个页面;我们值 需要确保在仿真器中的 VirtualProtect 调用都返回真。最后的 JMP 指令处理函数做了一个简 单的确认,看是否要跳出解压代码段,如果跳出,就调用 dump_unpacked 将代码转储到硬 盘上。之后告诉仿真器停止工作,解压工作完成了。 下面就是 dump_unpacked 代码。 ``` #upx_unpacker.py ... def dump_unpacked(emu): global outputfile fh = open(outputfile, 'wb') print "[*] Dumping UPX0 Section" base = emu.sections["UPX0"]["base"] length = emu.sections["UPX0"]["vsize"] print "[*] Base: 0x%08x Vsize: %08x"% (base, length) for x in range(length): fh.write("%c" % emu.get_memory(base + x, 1)) print "[*] Dumping UPX1 Section" base = emu.sections["UPX1"]["base"] length = emu.sections["UPX1"]["vsize"] print "[*] Base: 0x%08x Vsize: %08x" % (base, length) for x in range(length): fh.write("%c" % emu.get_memory(base + x, 1)) print "[*] Finished." ``` 我们只需要简单的将 UPX0 和 UPX1 两个段的代码写入文件。一旦文件 dump 成功,就能够想正常程序一样分析调试它们了。在命令行中使用我们的解压脚本看看: ``` C:\>C:\Python25\python.exe upx_unpacker.py C:\calc_upx.exe calc_clean.exe [*] We are jumping out of the unpacking routine. [*] OEP = 0x01012475 [*] Dumping UPX0 Section [*] Base: 0x01001000 Vsize: 00019000 [*] Dumping UPX1 Section [*] Base: 0x0101a000 Vsize: 00007000 [*] Finished. C:\> ``` Listing 12-4:upx_unpacker.py 的命令行输出 现在我们有了一个和未加密的 calc.exe 一样的 calc_clean.exe。大功告成,各位不妨测试 着写写不同壳的解压代码,相信不久之后你会学到更多。