12.2 PyEmu 一览
PyEmu 被划分成三个重要的系统:PyCPU,PyMemory 和 PyEmu。与我们交互最多的 就是 PyEmu 类,它再和 PyCPU 和 PyMemoey 交互完成底层的仿真工作。当我们测试驱动 PyEmu 执行一个指令的时候,它就调用 PyCPU 完成真正的指令操作。PyCPU 在进行指令操 作的时候,把需要的内存操作告诉 PyEmu,由 PyEmu 继续调用 PyMemory 辅助完成整个指 令的操作,最后由 PyEmu 将指令的结果返回给调用者。
接下来,让我们简短的了解下各个子系统和他们的使用方法,以便更好的明白伟大的 PyEmu 替我们完成了什么,同时大家也能对实际应用有个初略的了解。
12.2.1 PyCPU
PyCPU 类是 PyEmu 的核心,它模拟成和真实的 CPU 一样。在仿真的过程中,它负责 执行指令。当 PyCPU 处理一个指令的时候,会先检索指令指针 ( 由负责静态分析的 IDA Pro/PEPyEmu 或者负责动态调试的 PyDbg 获取),然后将指令传递给 pydasm,由后者解码成 操作码和操作对象。PyCPU 提供的独立解码指令的能力使得 PyEmu 的跨平台变成了可能。 每个 PyEmu 接收到的指令,都有一个相对应内部函数。举个例子,如果将指令 CMP EAX ,1 传给 PyCPU,接着 PyCPU 就会调用 PyCPU CMP()函数执行真正的操作,并从内存中 检索必要的值,之后设置 CPU 的标志位,告诉程序这次比较的结果。有兴趣的各位都可以 看看 PyCPU.py,所有的 PyEmu 支持的指令处理函数都在这里,通过研究它们可以明白 CPU 是如何完成那些神秘的底层操作的。别担心代码的可读性, Cody 在这上面可没少花功夫。
12.2.2 PyMemory
PyMemor 负责加载和储存执行指令的必要数据。同时也可以对可执行程序的代码和数 据块进行映射,以便在仿真器中访问。在将借完两个主要类之后,让我们看看核心类 PyEmu,以及相关的类方法。
12.2.3 PyEmu
PyEmu 负责驱动整个仿真器的运作。PyEmu 类本身被设计的非常轻便和灵活,使得开 发者能够很块的开发出强大的仿真器脚本,而不用关心底层操作。这一切都由 PyEmu 提供的帮助函数实现,使用它们能让我们的这个逆向工作变得更简单,无论是操作执行流程,改变寄存器值还是更新内存等等。下面就来卓一介绍它们。
12.2.4 执行操作
PyEmu 的执行过程由一个函数控制,execute()。原型如下:
execute( steps=1, start=0x0, end=0x0 )
总共三个参数,如果一个都没有提供,就从 PyEmu 当前的地址开始执行。这个地址也 许是 PyDbg 的 EIP 寄存器指向的位置,也许是 PEPyEmu 加载的可执行程序的入口地址,也 许是 IDA Pro 光标所处的位置。start 为开始执行的地址,steps 为执行的指令数量,end 为结 束的地址。
12.2.5 内存和寄存器操作
修改和检索寄存器与内存的值在逆向的过程中特别重要。 PyEmu 将它们分成了 4 类: 内存,栈变量(stack variables),栈参数(stack arguments),寄存器。内存操作由 get_memory() 和 set_memory()完成。
get_memory( address, size ) set_memory( address, value, size=0 )
get_memory()函数接收 2 个参数:address 为要查询的地址,size 为要获得数据的大小。 set_memoey()负责写入数据,address 为写入的地址,value 为写入的值,size 为写入数据的 大小。
另外两类基于栈操作的函数也差不多,主要负责栈框架中函数参数和本地变量的检索和 修改。
set_stack_argument( offset, value, name="" )
get_stack_argument( offset=0x0, name="" )
set_stack_variable( offset, value, name="" )
get_stack_variable( offset=0x0, name="" )
set_stack_argument()的 offset 相对与 ESP,用于对传入函数的参数进行改变。在操作的过程中可以提供可以可选的名字。get_stack_argument()通过 offset 指定的相对于 ESP 的位移 获得参数值,或者通过指定的 name(前提是在 set_stack_argument 中提供了)获得。使用方式 如下:
set_stack_argument( 0x8, 0x12345678, name="arg_0" ) get_stack_argument( 0x8 )
get_stack_argument( "arg_0" )
set_stack_variable()和 get_stack_variable()的操作也类似除了 offset 是相对于 EBP(如果允 许的话)以外,因为它们负责操作函数的局部变量。
12.2.6 处理函数
处理函数提供了一种非常强大且灵活的回调结构,用于 观察,设置或者修改程序的特定部分。PyEmu 中有 8 个主要处理函数: register 处理函数, library 处理函数, exception 处理函数, instruction 处理函数, opcode 处理函数, memory 处理 函数, high-level memory 处理函数还有 program counter 处理函数。让我们快速的了解下每一 个函数,之后我们马上要在用到它们。
12.2.6.1 Register 处理函数
Register Handlers 寄存器处理函数,用于监视任何寄存器的改变。只要有寄存器的遭到 修改就将触发 Register Handlers。安装方式如下:
set_register_handler( register, register_handler_function )
set_register_handler( "eax ", eax_register_handler )
安装好之后,就需要定义处理函数了,原型如下:
def register_handler_function( emu, register, value, type ):
当处理函数被调用的时候,所有的参数都又 PyEmu 传入,第一个参数就是 PyEmu 实例 首,接着是寄存器名,以及寄存器的值,type 告诉我们这次操作是读还是写。时间久了你就 会发现用这种方式观察寄存器是有多么强大且方便,如果需要你还能在处理函数里改变它 们。
12.2.6.2 ### Library 处理函数
Library handle 库处理函数,能让我们捕捉所有的外部库调用,在它们被调用进程序之前就截获它们,这样就能很方便的修改外部库函数的调用方式以及返回值。安装方式如下:
set_library_handler( function, library_handler_function )
set_library_handler( "CreateProcessA", create_process_handler )
set_library_handler("LoadLibraryA", loadlibrary)
库处理函数的原型如下:
def library_handler_function( emu, library, address ):
第一个参数就是 PyEmu 的实例。library 为我们想要监视的函数,或者库,第三个是函 数被映射在内存中的地址。
12.2.6.3 Exception 处理函数
Exception Handlers 异常处理函数和第二章介绍的"处理函数相似"。PyEmu 仿真器中的 异常会触发 Exception Handlers 的调用。当前 PyEmu 支持通用保护错误,也就是说我们能够 处理在模拟器中的任何内存访问违例。安装方式如下:
set_exception_handler( "GP", gp_exception_handler )
Exception 处理函数原型如下:
def gp_exception_handler( emu, exception, address ):
同样,第一个参数是 PyEmu 实例,exception 为异常代码,address 为异常发生的地址。
12.2.6.4 Instruction 处理函数
Instruction Handlers 指令处理函数,很强大,因为它能捕捉任何特定的指令。就像 Cody 在 BlackHat 说展示的那样,你能够通过安装一个 CMP 指令的处理函数,来监视整个程序流 程的分支判断,并控制它们。
set_instruction_handler( instruction, instruction_handler )
set_instruction_handler( "cmp", cmp_instruction_handler )
Instruction 处理函数原型如下:
def cmp_instruction_handler( emu, instruction, op1, op2, op3 ):
第一个参数照旧是 PyEmu 实例,instruction 则为被执行的指令,另外三个都是可能的运 算对象。
12.2.6.5 Opcode 处理函数
Opcode handlers 操作码处理函数和指令处理函数非常相似,任何一个特定的操作码被执 行的时候,都会调用 Opcode handlers。这样我们对代码的控制就变得更精确了。每一个指令 都有可能有不同的操作码这依赖于它们的运算对象,例如,PUSH EAX 时操作码是 0x50, 而 PUSH 0x70 时操作码是 0x6A,合起来整个指令的操作码就是 0x6A70,如下所示:
50 PUSH EAX
6A 70 PUSH 0x70
它们的安装方法很简单:
set_opcode_handler( opcode, opcode_handler )
set_opcode_handler( 0x50, my_push_eax_handler )
set_opcode_handler( 0x6A70, my_push_70_handler )
第一个参数只要简单的设置成我们需要捕捉的操作码,第二个参数就是处理函数了。捕捉的范围不限于单个字节,而可以是多这个字节,就想第二个例子一样。处理函数原型如下:
def opcode_handler( emu, opcode, op1, op2, op3 ):
第一个 PyEmu 实例,后面不再累赘。opcode 是捕捉到的操作码,剩下的三个就是指令 可能使用到的计算对象。
12.2.6.6 Memory 处理函数
Memory handlers 内存处理函数用于跟踪特定地址的数据访问。它能让我们很方便的跟 踪缓冲区中感兴趣的数据以及全局变量的改变过程。安装过程如下:
set_memory_handler( address, memory_handler )
set_memory_handler( 0x12345678, my_memory_handler )
address 简单传入我们想要观察的内存地址, my_memory_handler 就是我们的处理函 数。函数原型如下:
def memory_handler( emu, address, value, size, type )
第二个参数 address 为发生内存访问的地址,value 是被读取或者写入的数据,size 是数 据的大小,type 告诉我们这次操作读还是写。
12.2.6.7 High-Level Memory 处理函数
High-Level Memory Handlers 高级内存处理函数,很高级很强大。通过安装它们,我们 就能监视这个内存快(包括栈和堆)的读写。这样就能全面的控制内存的访问,是不是很邪恶。安装方式如下:
set_memory_write_handler( memory_write_handler )
set_memory_read_handler( memory_read_handler )
set_memory_access_handler( memory_access_handler )
set_stack_write_handler( stack_write_handler )
set_stack_read_handler( stack_read_handler )
set_stack_access_handler( stack_access_handler )
set_heap_write_handler( heap_write_handler )
set_heap_read_handler( heap_read_handler )
set_heap_access_handler( heap_access_handler )
所有的这些安装函数只要简单的提供一个处理函数就可以了,任何内存的变动都会通知 我们。处理函数的原型如下:
def memory_write_handler( emu, address ):
def memory_read_handler( emu, address ):
def memory_access_handler( emu, address, type ):
memory_write_handler 和 memory_read_handler 只是简单的接收 PyEmu 实例和发生读写 的地址。第三个 access handler 多了一个 type 用于说明这次不做到的是读数据还是些数据。 栈和堆的处理函数和上面的一样,不做解说。
12.2.6.8 Program Counter 处理函数
The program counter handler 程序计数器处理函数,将在程序执行到特定地址的时候触 发。安装过程如下:
set_pc_handler( address, pc_handler )
set_pc_handler( 0x12345678, 12345678_pc_handler )
address 为我们将要监视的地址,一旦 CPU 执行到这就会触发我们的处理函数。处理 函数的原型如下:
def pc_handler( emu, address ):
第二个参数 address 为被捕捉到的地址。
现在我们已经讲解完了,PyEmu 的基础知识。是时候将它们用于实际工作中了。接下 来会进行两个实验。第一个使用 IDAPyEmu 在 IDA Pro 模拟一个简单的函数调用。第二个 实验使用 PEPyEmu 解压一个被 UPX 压缩过的(伟大的开源压缩程序)二进制文件。