淺析ELF轉(zhuǎn)二進(jìn)制允許把 Binary 文件加載到任意位置
背景簡介
有一天,某位同學(xué)在討論群聊起來:
除了直接把 C 語言程序編譯成 ELF 運行以外,是否可以轉(zhuǎn)成二進(jìn)制,然后通過第三方程序加載到內(nèi)存后再運行。
帶著這樣的問題,我們寫了四篇文章,這是其二。
上篇 介紹了如何把 ELF 文件轉(zhuǎn)成二進(jìn)制文件,并作為一個新的 Section 加入到另外一個程序中執(zhí)行。
這個代碼包括兩個段,一個 text 段,一個 data 段,默認(rèn)鏈接完以后,text 中是通過絕對地址訪問 data 的,ELF 轉(zhuǎn)成 Binary 后,這個地址也寫死在 ELF 中,如果要作為新的 Seciton 加入到另外一個程序,那么鏈接時必須確保 Binary 文件的加載地址跟之前的 ELF 加載地址一致,否則數(shù)據(jù)存放的位置就偏移了,訪問不到,所以上篇文章用了一個客制化的 ld script,在里頭把 Binary Seciton 的加載地址(運行時地址)寫死的。
讓數(shù)據(jù)地址與加載地址無關(guān)
本篇來討論一個有意思的話題,那就是,是否可以把這個絕對地址給去掉,只要把這個 Binary 插入到新程序的 Text 中,不關(guān)心加載地址,也能運行?
想法是這樣:data 應(yīng)該跟 text 關(guān)聯(lián)起來,也就是說,用相對 .text 的地址,因為 Binary 里頭的 .rodata 是跟在 .text 后面,在文件中的相對位置其實是固定的,是否可以在運行時用一個偏移來訪問呢?也就是在運行過程中,獲取到 .text 中的某個位置,然后通過距離來訪問這個數(shù)據(jù)?
在運行時獲取 eip
由于加載地址是任意的,用 .text 中的符號也不行,因為在鏈接時也一樣是寫死的(用動態(tài)鏈接又把問題復(fù)雜度提升了),所以,唯一可能的辦法是 eip,即程序地址計數(shù)器。
但是 eip 是沒有辦法直接通過寄存器獲取的,得通過一定技巧來,下面這個函數(shù)就可以:
eip2ecx: movl (%esp), %ecx ret
這個函數(shù)能夠把 eip 放到 ecx 中。
原理很簡單,那就是調(diào)用它的 call 指令會把 next eip 放到 stack,并跳到 eip2ecx。所以 stack 頂部就是 eip。這里也可以直接用 pop %ecx 。
所以這條指令能夠拿到 .here 的地址,并且存放在 ecx 中:
call eip2ecx .here: ... .section .rodata .LC0: .string "Hello World\xa\x0"
通過 eip 與數(shù)據(jù)偏移計算數(shù)據(jù)地址
然后接下來,由于匯編器能夠算出 .here 離 .LC0(數(shù)據(jù)段起始位置): .LC0 - .here ,對匯編器而言,這個差值就是一個立即數(shù)。如果在 ecx 上加上(addl)這個差值,是不是就是數(shù)據(jù)在運行時的位置?
我們在 .here 放上下面這條指令:
call eip2ecx .here: addl $(.LC0 - .here), %ecx ... .section .rodata .LC0: .string "Hello World\xa\x0"
同樣能夠拿到數(shù)據(jù)的地址,等同于:
movl $.LC0, %ecx # ecx = $.LC0, the addr of string
下面幾個綜合一起回顧:
- addl 這條指令的位置正好是運行時的 next eip (call 指令的下一條)
- .here 在匯編時確定,指向 next eip
- .LC0 也是匯編時確定,指向數(shù)據(jù)開始位置
- .LC0 - .here 剛好是 addl 這條指令跟數(shù)據(jù)段的距離/差值
- call eip2ecx 返回以后,ecx 中存了 eip
- addl 這條指令把 ecx 加上差值,剛好讓 ecx 指向了數(shù)據(jù)在內(nèi)存中的位置
完整代碼如下:
# hello.s # # as --32 -o hello.o hello.s # ld -melf_i386 -o hello hello.o # objcopy -O binary hello hello.bin # .text .global _start _start: xorl %eax, %eax movb $4, %al # eax = 4, sys_write(fd, addr, len) xorl %ebx, %ebx incl %ebx # ebx = 1, standard output call eip2ecx .here: addl $(.LC0 - .here), %ecx # ecx = $.LC0, the addr of string # equals to: movl $.LC0, %ecx xorl %edx, %edx movb $13, %dl # edx = 13, the length of .string int $0x80 xorl %eax, %eax movl %eax, %ebx # ebx = 0 incl %eax # eax = 1, sys_exit int $0x80 eip2ecx: movl (%esp), %ecx ret .section .rodata .LC0: .string "Hello World\xa\x0"
鏈接腳本簡化
這個生成的 hello.bin 鏈接到 run-bin,就不需要寫死加載地址了,隨便放,而且不需要調(diào)整 run-bin 本身的加載地址,所以 ld.script 的改動可以非常簡單:
$ git diff ld.script ld.script.new diff --git a/ld.script b/ld.script.new index 91f8c5c..e14b586 100644 --- a/ld.script +++ b/ld.script.new @@ -60,6 +60,11 @@ SECTIONS /* .gnu.warning sections are handled specially by elf32.em. */ *(.gnu.warning) } + .bin : + { + bin_entry = .; + *(.bin) + } .fini : { KEEP (*(SORT_NONE(.fini)))
直接用內(nèi)聯(lián)匯編嵌入二進(jìn)制文件
在這個基礎(chǔ)上,可以做一個簡化,直接用 .pushsection 和 .incbin 指令把 hello.bin 插入到 run-bin 即可,無需額外修改鏈接腳本:
$ cat run-bin.c #include <stdio.h> asm (".pushsection .text, \"ax\" \n" ".globl bin_entry \n" "bin_entry: \n" ".incbin \"./hello.bin\" \n" ".popsection" ); extern void bin_entry(void); int main(int argc, char *argv[]) { bin_entry(); return 0; }
這個內(nèi)聯(lián)匯編的效果跟上面的鏈接腳本完全等價。
把數(shù)據(jù)直接嵌入代碼中
進(jìn)一步簡化匯編代碼把 eip2ecx 函數(shù)去掉:
# hello.s # # as --32 -o hello.o hello.s # ld -melf_i386 -o hello hello.o # objcopy -O binary hello hello.bin # .text .global _start _start: xorl %eax, %eax movb $4, %al # eax = 4, sys_write(fd, addr, len) xorl %ebx, %ebx incl %ebx # ebx = 1, standard output call eip2ecx eip2ecx: pop %ecx addl $(.LC0 - eip2ecx), %ecx # ecx = $.LC0, the addr of string # equals to: movl $.LC0, %ecx xorl %edx, %edx movb $13, %dl # edx = 13, the length of .string int $0x80 xorl %eax, %eax movl %eax, %ebx # ebx = 0 incl %eax # eax = 1, sys_exit int $0x80 .LC0: .string "Hello World\xa\x0"
再進(jìn)一步,直接把數(shù)據(jù)搬到 next eip 所在位置:
# hello.s # # as --32 -o hello.o hello.s # ld -melf_i386 -o hello hello.o # objcopy -O binary hello.o hello # .text .global _start _start: xorl %eax, %eax movb $4, %al # eax = 4, sys_write(fd, addr, len) xorl %ebx, %ebx incl %ebx # ebx = 1, standard output call next # push eip; jmp next .LC0: .string "Hello World\xa\x0" next: pop %ecx # ecx = $.LC0, the addr of string # eip is just the addr of string, `call` helped us xorl %edx, %edx movb $13, %dl # edx = 13, the length of .string int $0x80 xorl %eax, %eax movl %eax, %ebx # ebx = 0 incl %eax # eax = 1, sys_exit int $0x80
小結(jié)
本文通過 eip + 偏移地址 實現(xiàn)了運行時計算數(shù)據(jù)地址,不再需要把 Binary 文件裝載到固定的位置。
另外,也討論到了如何用 .pushsection/.popsection 替代 ld script 來添加新的 Section,還討論了如何把數(shù)據(jù)直接嵌入到代碼中。
以上所述是小編給大家介紹的ELF轉(zhuǎn)二進(jìn)制允許把 Binary 文件加載到任意位置,希望對大家有所幫助!
相關(guān)文章
VScode配置匯編語言環(huán)境的實現(xiàn)步驟
本文主要介紹了VScode配置匯編語言環(huán)境的實現(xiàn)步驟,文中通過圖文的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-03-03匯編語言80x86系統(tǒng)通用數(shù)據(jù)傳送指令詳解
這篇文章主要為大家介紹了匯編語言80x86系統(tǒng)通用的數(shù)據(jù)傳送指令詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11os_object_release Crash 排查記錄分析
這篇文章主要為大家介紹了os_object_release Crash 排查記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11