淺析ELF轉(zhuǎn)二進(jìn)制允許把 Binary 文件加載到任意位置
背景簡介
有一天,某位同學(xué)在討論群聊起來:
除了直接把 C 語言程序編譯成 ELF 運(yùn)行以外,是否可以轉(zhuǎn)成二進(jìn)制,然后通過第三方程序加載到內(nèi)存后再運(yùn)行。
帶著這樣的問題,我們寫了四篇文章,這是其二。
上篇 介紹了如何把 ELF 文件轉(zhuǎn)成二進(jìn)制文件,并作為一個(gè)新的 Section 加入到另外一個(gè)程序中執(zhí)行。
這個(gè)代碼包括兩個(gè)段,一個(gè) text 段,一個(gè) data 段,默認(rèn)鏈接完以后,text 中是通過絕對(duì)地址訪問 data 的,ELF 轉(zhuǎn)成 Binary 后,這個(gè)地址也寫死在 ELF 中,如果要作為新的 Seciton 加入到另外一個(gè)程序,那么鏈接時(shí)必須確保 Binary 文件的加載地址跟之前的 ELF 加載地址一致,否則數(shù)據(jù)存放的位置就偏移了,訪問不到,所以上篇文章用了一個(gè)客制化的 ld script,在里頭把 Binary Seciton 的加載地址(運(yùn)行時(shí)地址)寫死的。
讓數(shù)據(jù)地址與加載地址無關(guān)
本篇來討論一個(gè)有意思的話題,那就是,是否可以把這個(gè)絕對(duì)地址給去掉,只要把這個(gè) Binary 插入到新程序的 Text 中,不關(guān)心加載地址,也能運(yùn)行?
想法是這樣:data 應(yīng)該跟 text 關(guān)聯(lián)起來,也就是說,用相對(duì) .text 的地址,因?yàn)?Binary 里頭的 .rodata 是跟在 .text 后面,在文件中的相對(duì)位置其實(shí)是固定的,是否可以在運(yùn)行時(shí)用一個(gè)偏移來訪問呢?也就是在運(yùn)行過程中,獲取到 .text 中的某個(gè)位置,然后通過距離來訪問這個(gè)數(shù)據(jù)?
在運(yùn)行時(shí)獲取 eip
由于加載地址是任意的,用 .text 中的符號(hào)也不行,因?yàn)樵阪溄訒r(shí)也一樣是寫死的(用動(dòng)態(tài)鏈接又把問題復(fù)雜度提升了),所以,唯一可能的辦法是 eip,即程序地址計(jì)數(shù)器。
但是 eip 是沒有辦法直接通過寄存器獲取的,得通過一定技巧來,下面這個(gè)函數(shù)就可以:
eip2ecx: movl (%esp), %ecx ret
這個(gè)函數(shù)能夠把 eip 放到 ecx 中。
原理很簡單,那就是調(diào)用它的 call 指令會(huì)把 next eip 放到 stack,并跳到 eip2ecx。所以 stack 頂部就是 eip。這里也可以直接用 pop %ecx 。
所以這條指令能夠拿到 .here 的地址,并且存放在 ecx 中:
call eip2ecx .here: ... .section .rodata .LC0: .string "Hello World\xa\x0"
通過 eip 與數(shù)據(jù)偏移計(jì)算數(shù)據(jù)地址
然后接下來,由于匯編器能夠算出 .here 離 .LC0(數(shù)據(jù)段起始位置): .LC0 - .here ,對(duì)匯編器而言,這個(gè)差值就是一個(gè)立即數(shù)。如果在 ecx 上加上(addl)這個(gè)差值,是不是就是數(shù)據(jù)在運(yùn)行時(shí)的位置?
我們在 .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
下面幾個(gè)綜合一起回顧:
- addl 這條指令的位置正好是運(yùn)行時(shí)的 next eip (call 指令的下一條)
- .here 在匯編時(shí)確定,指向 next eip
- .LC0 也是匯編時(shí)確定,指向數(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"
鏈接腳本簡化
這個(gè)生成的 hello.bin 鏈接到 run-bin,就不需要寫死加載地址了,隨便放,而且不需要調(diào)整 run-bin 本身的加載地址,所以 ld.script 的改動(dòng)可以非常簡單:
$ 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)制文件
在這個(gè)基礎(chǔ)上,可以做一個(gè)簡化,直接用 .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; }
這個(gè)內(nèi)聯(lián)匯編的效果跟上面的鏈接腳本完全等價(jià)。
把數(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 + 偏移地址 實(shí)現(xiàn)了運(yùn)行時(shí)計(jì)算數(shù)據(jù)地址,不再需要把 Binary 文件裝載到固定的位置。
另外,也討論到了如何用 .pushsection/.popsection 替代 ld script 來添加新的 Section,還討論了如何把數(shù)據(jù)直接嵌入到代碼中。
以上所述是小編給大家介紹的ELF轉(zhuǎn)二進(jìn)制允許把 Binary 文件加載到任意位置,希望對(duì)大家有所幫助!
相關(guān)文章
VScode配置匯編語言環(huán)境的實(shí)現(xiàn)步驟
本文主要介紹了VScode配置匯編語言環(huán)境的實(shí)現(xiàn)步驟,文中通過圖文的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-03-03匯編中的數(shù)組分配和指針的實(shí)現(xiàn)代碼
這篇文章主要介紹了匯編中的數(shù)組分配和指針的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01匯編語言80x86系統(tǒng)通用數(shù)據(jù)傳送指令詳解
這篇文章主要為大家介紹了匯編語言80x86系統(tǒng)通用的數(shù)據(jù)傳送指令詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11匯編語言軟件延時(shí)1s的實(shí)現(xiàn)方法
這篇文章主要介紹了匯編語言軟件延時(shí)1s的實(shí)現(xiàn)方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-01-01os_object_release Crash 排查記錄分析
這篇文章主要為大家介紹了os_object_release Crash 排查記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11