12.3 IDAPyEmu
12.3 IDAPyEmu
我们的第一个例子就是在 IDA Pro 分析程序的时候,使用 PyEmu 仿真一次简单的函数 调用。这次实验的程序就是 addnum.exe,主要功能就是从命令行中接收两个参数,然后相 加,再输出结果,代码使用 C++ 编写,可从 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 出来。记得这种 方法同样适用于别的压缩程序,万变不离其宗。
UPX
UPX 是自由的,是开源的,是跨平台的(Linux Windows....)。提供不同的压缩级别,和 许多附加的选项,用于完成各种不同的压缩任务。我们使用默认的压缩方案,都让你可随意 的测试。
从 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 出马了。
- 使用 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。大功告成,各位不妨测试 着写写不同壳的解压代码,相信不久之后你会学到更多。