我在很久以前學習組合語言時 (雖然很久之前就學, 不過並不表示我有學好, 事實上, 它還讓我產生不少挫折感), 就有一個疑問?
dos 執行檔和其他 os 執行檔有什麼分別, 為什麼不能拿來互相執行? 因為都是 intel cpu, machine code 應該都一樣。
ex:
nop 這指令, 在 dos, os/2, linux, win 3.1, win 95, winnt, winxp 不都是 0x90 嗎?為什麼不能在每個 os 平台執行這個執行檔。
別急 ... 我知道你可能有幾個答案, 但這可不是簡單就能回答的問題。這次介紹的範例程式就是要來突破這樣的限制, 不過不是
載入/執行執行檔 (pe, elf), 而是 object file (coff, elf format), 我要在 linux 上載入/執行 windows coff object file。能載入 objet file 應該也能載入 pe file, 這我就沒實作過了, load/relocate/run object file 的練習對我來說已經足夠, 我已經滿足, 希望有人看到我這篇後, 實作出載入 pe 的版本。而為什麼我這麼說呢?因為要是不行, 那 wine 是怎麼做到的, 所以一定有辦法, 而難度我覺得比本範例要難上不少。
在自行 parse elf object file 之後, 我有了上述的想法, 因為原理都是一樣的, 只要能從 coff object file 找到一樣的資訊, 應該可以搞定, 能完成的話一定很酷。
流程大概是這樣:
- load elf/coff object to memory.
- do the relocation.
- call the object hello function, hello will call func, func will call printf.
- back to linux shell prompt.
所需要的知識:
要看懂這個範例程式, 上述資料一定要看懂, 這個程式是我集合上面兩本的內容發展而來。
coff 可以參考:
我研究 machine code 就是為了要做 dynamic linker/loader, 因為要把 address 的部份找出來, 然後修改它; 而事實上, 事情比我想的更簡單, assembler 已經做好最難的苦工, 把要修改的位址, 該位址要填上的新值都已經紀錄在 object file, 程式只要去讀出來就好了, 不過研讀 machine code 還是沒有白費, 至少能看懂修改了什麼部份, 只是沒那麼重要就是。
來看看要載入的 object source code, 目的很簡單, 就是要去執行 hello() 然後返回呼叫端。
這次的工具除了 linux 上的開發工具還要準備 windows 上的開發工具 vc2010, 用來編譯出 coff object file, 你可能需要兩台電腦或是使用 vmware 這樣的虛擬機器。
env:
vc2010
windows xp
cl /c hello.c 產生 hello.obj (要載入的就是這個 object file)
程式最主要就是在 linux 載入由 windows vc2010 產生的 hello.obj, 先來分析這個 object file。
我用上的工具有:
dumpbin (vc2010)
objdump (mingw)
hexdump
GNU toolchain 我比較熟悉, 可惜 readelf 這個最有力的工具沒有 coff 版本, 使用 dumpbin 代替。
要怎麼開始呢?先來找出要 relocation 的地方吧, 看看 table 1 和 table 3 所標示的紅色部份。
總共有五處, 我有提到, 要是能看懂機械碼, 把這五處看出來會比較輕鬆, 若是看後面的組合語言部份, 應該也勉強可看出來, 這裡需要知道 c function call 轉成組合語言後的規則, 若不懂, 這裡可能會有點難理解 (這你得先克服, 我沒打算說明這部份, 請參考組合語言書籍和 c 連結的部分)。再來就是找出這五個地方的位址, 把 relocate 後的值填上。
說來簡單, 要怎麼找呢?從 coff object file 本身就可以得到這五個地方的位址。
relocate _func:
找出 .text section 的 offset (因為 _func 在 .text section), table 1 L92 得知, .text offset 是 0x165
L117 0x39
0x165+0x39 = 0x19e
Table 2 0x19e 將 00000000 (4 bytes) 填上正確的值就完成了。
那正確的值是多少呢?
_func 這個 symbol 在記憶體上的位置是多少, 就是那個值了。所以先找到 _func 在檔案中的 offset, 在加上整個 hello.obj 被載入到記憶體的位置就搞定。
talbe 1 L133 可以查到 _func 在 SEC4 (L87 得到 .text, offset 0x165), symbol 那欄是 0, 所以 0x165 + 0 = 0x165 就是 _func 所在檔案 offset。Table 2 藍色部份就是 _func。
搞定了嗎?還沒, e8 是 call 指令, 感覺填上要位址就是要 call 的位址, 但是 call 指令並不是這麼直覺, 它是這樣的:
假設要 call 0x100 的位址, 實際上的指令是 (0x100, 0x110, 0x115 為記憶體位址)
0x100
...
...
0x110 call ???
0x115 nop
??? 是 0x100 - 0x115 =
-0x15 -> call
-0x15 才會去 call 0x100 的位址。
所以這個值得要用 _func - cal 下一個指令的位址 (ref TABLE 0x1a2) -> 0x165 - 0x1a2 = 0xffffffc3 (十進位 -61), 填上就對了。
來看看 _hello 的 offset:
Talbe 1 L133 可以查到 _func 在 SEC4 (L87 得到 .text, offset 0x165), symbol 那欄是 0x30, 所以 0x165 + 0 = 0x190 就是 _hello 所在檔案 offset。Table 2 綠色部份就是 _hello。
relocate _i:
步驟都一樣, 從 Table 1 L192 找出 _i 在 SEC3, SEC3 offset (0x153)+ _i SYMOBL 值是 4 = 0x157。
那個地方要改成 0x157 呢?
table 1 L113 (0xe), L116 (0x34)
relocate $SG2641: 這是 printf 傳入的字串, hello.c L10 or L12, 道理一樣, 自己試試看。
relocate _printf, 這個最簡單了, printf 已經被我使用 static link 連結到主程式中, 怎麼得到他的位址?
printf_addr = &printf;
出乎意料之外的簡單吧, 想辦法把這位址放在 call printf 這邊就對了, 和 _func 一樣的作法。而這個就是 windows coff 可以正常在 linux 環境執行的關鍵, 把 windows printf 的位址換成 linux printf 即可, 秘密說穿了, 很簡單吧!
最後提一下這個語法, 很嚇人吧!
(*(void(*)())(hello_addr + text_offset + hello_val) )();
把 hello() 的位址當成 function pointer 來執行, 如果你看不懂, 就先背起來, 搞懂那一堆 relocation value 應該已經昏頭了。
除了使用 vs 2010 compile 這個 hello.o, 我還測試了 cygwin gcc, mingw gcc, 本程式都可以正常載入並執行。
table x 是程式執行結果, 也歡迎自己 git clone 把程式 compile 起來試試。
mingw objdump 2.23.1
cygwin objdump 2.22.52
source code:
https://github.com/descent/progs/tree/master/load_obj
ref:
Code Injection into Running Linux Application 中文版
Code Injection into Running Linux Application
Dynamic Test Runner
It's a PE! No, It's an ELF!
沒有留言:
張貼留言
使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。
我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。