找回密码
 加入
搜索
查看: 3066|回复: 5

[原创] AutoIt 機械碼的製作與調用(二)

  [复制链接]
发表于 2012-11-1 08:59:38 | 显示全部楼层 |阅读模式
本帖最后由 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 中可調用的機械碼」這樣一個看似複雜的問題,看完我這兩篇教程大概就可以完成十之六七了,最後剩下一些比較旁枝未節的問題,就留待下次吧。再次提醒,我介紹我用的工具和做法,當然各位可以完全照我的方式做,但最重要的還是要把原理弄懂,說不定用自己熟悉工具來完成還事半功倍再加創新呢!而有什麼成果或研究心得時,也別吝嗇和大家分享喔。

评分

参与人数 2威望 +5 金钱 +90 贡献 +6 收起 理由
bakefish + 10 非常感谢!
afan + 5 + 80 + 6 好教程,辛苦了

查看全部评分

发表于 2012-11-1 12:45:24 | 显示全部楼层
繁体的看着有点累
发表于 2012-11-2 09:23:52 | 显示全部楼层
楼主好人,欢迎常来!
谢谢你的帖子
发表于 2012-11-2 22:16:18 | 显示全部楼层
不是很懂,存下来好好学习一下!
发表于 2012-11-3 19:57:30 | 显示全部楼层
这太繁琐了,用ahk的mcode吧
发表于 2012-11-4 20:26:21 | 显示全部楼层
只能说 太nb

不过一般 懂 C 和 asm 的 人 应该 都不会用 autoit了吧
您需要登录后才可以回帖 登录 | 加入

本版积分规则

QQ|手机版|小黑屋|AUTOIT CN ( 鲁ICP备19019924号-1 )谷歌 百度

GMT+8, 2024-11-23 17:53 , Processed in 0.085348 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表