本帖最后由 ward 于 2012-11-1 09:13 编辑
各位好久不見了,上次的教程「AutoIt 機械碼的製作與調用(一)」還能理解嗎?工作繁忙了一段時間,終於有空寫教程二了。
在開始研究教程二之前,請確定你已經看過並看懂了教程一,不然有些觀念會接不下去喔!如果各位有看過教程一但一時想不起來也沒關係,我這邊簡單總結一下,把 C 源碼轉換成可在 AutoIt 中調用的機械碼的步驟為:
- 先把 C 源碼編譯成 OBJ 檔,再用 ObjConv 反組譯為 ASM 檔
- 修改 ASM 檔以通過 FASM 組譯器,並組譯出 BIN 二進位檔
- 把 BIN 檔轉成 HEX 字串,並在 AutoIt 中撰寫代碼調用
上次主要是介紹這三個步驟的完整流程,所以拿很簡單的 C 範例來實作。如果各位有心試了別的 C 源碼(真有人試過嗎?),就會發現不是所有事情都那麼簡單。明明都照著步驟來做,怎麼有的可以有的不行呢?所以接下來的教程要開始講解這整個過程中可能會遇到的問題和解決方式。也因為這些方式都牽扯到 C 源碼或 ASM 的修改,所以當然也需要一些 C 和 ASM 的基本知識。但請放心,都只用到最基礎的部份,而且我會講的很簡單加上範例。真的看不懂的話,可能就要先加強基本功囉!
好,廢話不多說,先付上這次教程的範例:一個簡單的 CRC32 函式。CRC32 的計算原理和用途在此並不重要,不懂的請自己查囉。我們專注的只是機械碼提取的技巧。C 源碼如下:unsigned long table[256] = {0};
unsigned long crc32(const void *buf, unsigned long bufLen, unsigned long crc32, unsigned long poly)
{
unsigned long crc;
unsigned char *byteBuf;
int i, j;
for(i = 0; i < 256; i++)
{
crc = i;
for (j = 8; j > 0; j--)
{
if (crc & 1)
crc = (crc >> 1) ^ poly;
else
crc >>= 1;
}
table[i] = crc;
}
byteBuf = (unsigned char*) buf;
for(i = 0; i < bufLen; i++)
crc32 = (crc32 >> 8) ^ table[(crc32 ^ byteBuf[i]) & 0xFF];
return crc32 ^ 0xFFFFFFFF;
}
用 GCC 編譯成功並用 ObjConv 轉出 ASM 檔後,依上次提到的要領做修改,結果如下:use32
_crc32:
push ebp ; 0000 _ 55
xor ecx, ecx ; 0001 _ 31. C9
mov ebp, esp ; 0003 _ 89. E5
push edi ; 0005 _ 57
mov eax, dword [ebp+10H] ; 0006 _ 8B. 45, 10
push esi ; 0009 _ 56
mov edi, dword [ebp+14H] ; 000A _ 8B. 7D, 14
mov esi, dword [ebp+8H] ; 000D _ 8B. 75, 08
push ebx ; 0010 _ 53
?_001: mov edx, ecx ; 0011 _ 89. CA
mov ebx, 8 ; 0013 _ BB, 00000008
?_002: test dl, 01H ; 0018 _ F6. C2, 01
jz ?_003 ; 001B _ 74, 06
shr edx, 1 ; 001D _ D1. EA
xor edx, edi ; 001F _ 31. FA
jmp ?_004 ; 0021 _ EB, 02
?_003: shr edx, 1 ; 0023 _ D1. EA
?_004: dec ebx ; 0025 _ 4B
jnz ?_002 ; 0026 _ 75, F0
mov dword [_table+ecx*4], edx ; 0028 _ 89. 14 8D, 00000000(d)
inc ecx ; 002F _ 41
cmp ecx, 256 ; 0030 _ 81. F9, 00000100
jnz ?_001 ; 0036 _ 75, D9
xor edx, edx ; 0038 _ 31. D2
jmp ?_006 ; 003A _ EB, 15
?_005: mov ecx, eax ; 003C _ 89. C1
xor al, byte [esi+edx] ; 003E _ 32. 04 16
inc edx ; 0041 _ 42
shr ecx, 8 ; 0042 _ C1. E9, 08
movzx eax, al ; 0045 _ 0F B6. C0
mov eax, dword [_table+eax*4] ; 0048 _ 8B. 04 85, 00000000(d)
xor eax, ecx ; 004F _ 31. C8
?_006: cmp edx, dword [ebp+0CH] ; 0051 _ 3B. 55, 0C
jnz ?_005 ; 0054 _ 75, E6
not eax ; 0056 _ F7. D0
pop ebx ; 0058 _ 5B
pop esi ; 0059 _ 5E
pop edi ; 005A _ 5F
pop ebp ; 005B _ 5D
ret ; 005C _ C3
_table: ; dword
rd 256 ; 0000
請注意,除了拿掉 global、SECTION、nop 什麼的,因為我是用 FASM 來組譯,所以還要把 NASM 用的語法 resd 改成 FASM 的語法 rd。還有,因為只有一個函式,所以在 AutoIt 中直接用 DllCallAddress 呼叫此段機械碼的開頭位址即可,不再使用教程一中提到的標籤與跳轉方法。
上面的代碼組譯成功後,就剩下 AutoIt 的部份了。我採取比較簡單的寫法,把教程一中的初始化步驟也省略了。這樣一來每次呼叫函式時都要重新申請內存並填入機械碼,所以這不是最佳化的寫法。不過我們的目的只是確定機械碼可用,所以還是採取簡單的寫法就好。請注意最後保留的 rd 256 = 1024 bytes 我直接在申請內存時加上去,避免 HEX 後面一大串的 0。#Include <Memory.au3>
Func _CRC32($Data, $Initial = -1, $Polynomial = 0xEDB88320)
Local $Opcode = Binary('0x5531C989E5578B4510568B7D148B75085389CABB08000000F6C2017406D1EA31FAEB02D1EA4B75F089148D5D0000004181F90001000075D931D2EB1589C132041642C1E9080FB6C08B04855D00000031C83B550C75E6F7D05B5E5F5DC3')
Local $CodeBufferPtr = _MemVirtualAlloc(0, BinaryLen($Opcode) + 1024, $MEM_COMMIT, $PAGE_EXECUTE_READWRITE)
Local $CodeBuffer = DllStructCreate("byte[" & BinaryLen($Opcode) & "]", $CodeBufferPtr)
DllStructSetData($CodeBuffer, 1, $Opcode)
$Data = Binary($Data)
Local $BufferLen = BinaryLen($Data)
Local $Buffer = DllStructCreate("byte[" & $BufferLen & "]")
DllStructSetData($Buffer, 1, $Data)
Local $Ret = DllCallAddress("uint:cdecl", $CodeBufferPtr, "ptr", DllStructGetPtr($Buffer), "uint", $BufferLen, "uint", $Initial, "uint", $Polynomial)
_MemVirtualFree($CodeBufferPtr, 0, $MEM_RELEASE)
Return $Ret[0]
EndFunc
ConsoleWrite(Hex(_CRC32("The quick brown fox jumps over the lazy dog")))
好了完工,看來一切步驟和教程一沒什麼兩樣,趕快來執行看看。如果正確的話應該要出現 "The quick brown fox jumps over the lazy dog" 的 CRC32 Checksum(414FA339)。但真正執行的結果卻是......整個 AutoIt 當掉!杯具了~來人啊,問題在哪裡?
其實我一開始的 C 代碼就留下了小陷阱,故意把計算過程中用到的 table 變數宣告成靜態的全域變數,程度稍好的人應該很快就可以看出問題出在這裡。簡單來說,一般含有「靜態全域變數」的 C 代碼會編譯出「直接定址」的機械碼,比如說上述範例 ASM 裡的 mov dword [_table+ecx*4], edx 這行,其中的 _table 值應該要是一個指定的內存位址才對(就是最後保留的 rd 256 的位址)。但注意看這行後面代表的機械碼竟然只是 0x00000000,而實際上對照在 BIN 裡的數值則是 0x0000005D,總之就不是內存位址。不懂為什麼會是 0x5D 嗎,其實它是一個「偏移位址」,因為機械碼在戴入內存時,每次的真正位址都不一樣,所以一般都只記錄一個偏移位址,當 Windows 的 DLL 載入時,DLL 載入器會按照偏移位址計算真正的內存位址,再取代掉原本的 0x5D,之後機械碼才能正確執行,這也就是俗話說的「重定位」過程。而我們的 AutoIt 代碼並沒有做相應的重定位計算,當然就會杯具啊。
上面講的重定位問題,是大部份的 C 源碼要轉成機械碼時都會遇到的問題。而根據情況的不同,通常的解決方式有三種,以下就讓我一一介紹:
一、修改 C 源碼
最簡單也是最直覺的解決方式,就是想辦法讓 C 語言編譯器不要產生「直接定址」的機械碼。具體做法是把「靜態全域變數」改為「區域變數」,也就是變數不要放內存,都放到堆疊上去。舉例來說,此範例可修改如下:/* unsigned long table[256] = {0}; */
unsigned long crc32(const void *buf, unsigned long bufLen, unsigned long crc32, unsigned long poly)
{
unsigned long table[256]; /* add this line */
unsigned long crc;
unsigned char *byteBuf;
int i, j;
...
}
如此一來,編譯的結果會出現 sub esp, 1024,也就是在堆疊中保留空間存放 table 變數,且不再出現定址存取的指令。試著重新提取機械碼,修改範例中 $Opcode 的 HEX 再試一次,應該就會運行出正確的結果了。如果對 C 語言不熟悉,不確定編譯後的結果為何,可能就要多方嘗試,看看如何修改才能產生不使用固定位址的機械碼。當然也要注意,不要一不小心改動源碼後讓程式產生錯誤的結果。
二、修改 ASM 代碼
第一種方法雖然方便,但有些情況下並不適用。比如說有初值的 table 放到堆疊就不適當,或是內存需求很大時,會編譯出要呼叫外部函式來調整堆疊的代碼,而處理外部函式反而更麻煩。所以另一種方式是,修改 ASM 代碼來解決定址呼叫的問題。修改的方式很多,我慣用的技巧是利用 shellcode 的概念。shellcode 是一段放到內存中的任意位址都能正確執行的機械碼,也是駭客取得系統控制權的常用手法。不過別擔心,此處不會用到太難的觀念。首先,在 _table 前面加上下列代碼(由原始範例開始修改):_pop_ebx:
pop ebx
ret
_load_table_ebx:
call _pop_ebx
_table: ; dword
rd 256 ; 0000
然後把原本存取 _table 的部份也做相應的修改 ; mov dword [_table+ecx*4], edx
push ebx
call _load_table_ebx
mov dword [ebx+ecx*4], edx
pop ebx
...
; mov eax, dword [_table+eax*4]
push ebx
call _load_table_ebx
mov eax, dword [ebx+eax*4]
pop ebx
這樣有看懂嗎? _load_table_ebx 函式利用 call 和 pop 的技巧取得執行時期 _table 的內存位址並存入 ebx,再用 ebx 來取代原本用到 _table 的地方。經過這樣的修改,機械碼就可以放心地在任意位址執行了。當然觀念上很簡單,但實作的時候如果要最佳化,可能還要多費一些心思,比如避免在迴圈中頻繁的 push、pop,或是仔細選擇回傳位址的暫存器等。一般而言,在少量的 C 源碼要轉換為機械碼時,此種方式是最實用的。
三、修改 AutoIt 代碼(自行重定位)
只要適當的併用上述兩種方法,就可以解決所有重定位問題了。但上述兩種修改技巧,多多少少會影響機械碼的最佳化,雖然造成的差異微乎其微,不過這裡還是介紹第三種方式,也就是修改 AutoIt 代碼來模擬 Windows 載入器所做的重定位。
其實也沒有很難,先從 ASM 或用十六進位編輯器查看 BIN,推敲出機械碼中兩處使用 _table 的位址,分別是 0x2B 和 0x4B 處。然後在機械碼執行前適當的修改這兩處的值就可以了,具體的 AutoIt 代碼如下(由原始範例開始修改): Local $TablePtr1 = DllStructCreate("ptr", $CodeBufferPtr + 0x2B)
Local $TablePtr2 = DllStructCreate("ptr", $CodeBufferPtr + 0x4B)
DllStructSetData($TablePtr1, 1, DllStructGetData($TablePtr1, 1) + $CodeBufferPtr)
DllStructSetData($TablePtr2, 1, DllStructGetData($TablePtr2, 1) + $CodeBufferPtr)
這一小段代碼做的事很簡單,就是把偏移位址(如上述的 0x5D)加上機械碼的載入位址 $CodeBufferPtr 之後放回。雖然初步看來這似乎不是一個好辦法,因為要自行計算出 0x2B 和 0x4B 這兩個位址,而且若是 ASM 代碼有更動就又要重新計算。但其實我認為這種方式才是最妙的,因為 0x2B 和 0x4B 這兩個位址是可以用程式自動計算的。所以我們可以寫一個重定位表產生器,並於機械碼函式初始化時呼叫自製的重定位函式,根據重定位表來執行重定位。只要相關的工作都預先設計好,這種方式反而是最快的。
要怎麼產生重定位表呢?解析 OBJ 檔是其中一個方式,但未免小題大作。我推薦的方式是用 ASM 的 ORG 假指令產生兩個偏移位址不同的 BIN 檔,再比對兩個檔的相異處即可得到簡單的重定位表,然後在 AutoIt 代碼中用上述的原理自製重定位函式。聽起來複雜,但只要上述的原理都弄懂了實作不難,而且重定位表的產生和套用都是 AutoIt 二進位操作的好練習,就留著給各位當習題吧。某些 C 源碼,用前面兩種方式去改可能要改數百個地方,這時用自動化產生的重定位表來完成所有工作,你就會感覺到 AutoIt 的美好!
好了,這次的教程大概就到這裡吧,主要就是講解機械碼的重定位問題和解決方式。其實「把 C 源碼轉成 AutoIt 中可調用的機械碼」這樣一個看似複雜的問題,看完我這兩篇教程大概就可以完成十之六七了,最後剩下一些比較旁枝未節的問題,就留待下次吧。再次提醒,我介紹我用的工具和做法,當然各位可以完全照我的方式做,但最重要的還是要把原理弄懂,說不定用自己熟悉工具來完成還事半功倍再加創新呢!而有什麼成果或研究心得時,也別吝嗇和大家分享喔。 |