## 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。大功告成,各位不妨测试 着写写不同壳的解压代码,相信不久之后你会学到更多。
- 序
- 1 搭建开发环境
- 1.1 操作系统准备
- 1.2 获取和安装 Python2.5
- 1.3 配置 Eclipse 和 PyDev
- 2 调试器设计
- 2.1 通用 CPU 寄存器
- 2.2 栈
- 2.3 调试事件
- 2.4 断点
- 3 自己动手写一个 windows 调试器
- 3.2 获得 CPU 寄存器状态
- 3.3 实现调试事件处理
- 3.4 全能的断点
- 4 PyDBG---纯 PYTHON 调试器
- 4.1 扩展断点处理
- 4.2 处理访问违例
- 4.3 进程快照
- 5 IMMUNITY----最好的调试器
- 5.1 安装 Immunity 调试器
- 5.2 Immunity Debugger 101
- 5.3 Exploit 开发
- 5.4 搞定反调试机制
- 6 HOOKING
- 6.1 用 PyDbg 实现 Soft Hooking
- 6.2 Hard Hooking
- 7 Dll 和代码注入
- 7.1 创建远线程
- 7.2 邪恶的代码
- 8 FUZZING
- 8.1 Bug 的分类
- 8.2 File Fuzzer
- 8.3 改进你的 Fuzzer
- 9 SULLEY
- 9.1 安装 Sulley
- 9.2 Sulley primitives
- 9.3 猎杀 WarFTPD
- 10 Fuzzing Windows 驱动
- 10.1 驱动通信
- 10.2 用 Immunity fuzzing 驱动
- 10.4 构建 Driver Fuzzer
- 11 IDAPYTHON --- IDA 脚本
- 11.1 安装 IDAPython
- 11.2 IDAPython 函数
- 11.3 脚本例子
- 12 PyEmu
- 12.1 安装 PyEmu
- 12.2 PyEmu 一览
- 12.3 IDAPyEmu