## 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