淺析ELF轉二進制允許把 Binary 文件加載到任意位置
背景簡介
有一天,某位同學在討論群聊起來:
除了直接把 C 語言程序編譯成 ELF 運行以外,是否可以轉成二進制,然后通過第三方程序加載到內存后再運行。
帶著這樣的問題,我們寫了四篇文章,這是其二。
上篇 介紹了如何把 ELF 文件轉成二進制文件,并作為一個新的 Section 加入到另外一個程序中執(zhí)行。
這個代碼包括兩個段,一個 text 段,一個 data 段,默認鏈接完以后,text 中是通過絕對地址訪問 data 的,ELF 轉成 Binary 后,這個地址也寫死在 ELF 中,如果要作為新的 Seciton 加入到另外一個程序,那么鏈接時必須確保 Binary 文件的加載地址跟之前的 ELF 加載地址一致,否則數據存放的位置就偏移了,訪問不到,所以上篇文章用了一個客制化的 ld script,在里頭把 Binary Seciton 的加載地址(運行時地址)寫死的。
讓數據地址與加載地址無關
本篇來討論一個有意思的話題,那就是,是否可以把這個絕對地址給去掉,只要把這個 Binary 插入到新程序的 Text 中,不關心加載地址,也能運行?
想法是這樣:data 應該跟 text 關聯起來,也就是說,用相對 .text 的地址,因為 Binary 里頭的 .rodata 是跟在 .text 后面,在文件中的相對位置其實是固定的,是否可以在運行時用一個偏移來訪問呢?也就是在運行過程中,獲取到 .text 中的某個位置,然后通過距離來訪問這個數據?
在運行時獲取 eip
由于加載地址是任意的,用 .text 中的符號也不行,因為在鏈接時也一樣是寫死的(用動態(tài)鏈接又把問題復雜度提升了),所以,唯一可能的辦法是 eip,即程序地址計數器。
但是 eip 是沒有辦法直接通過寄存器獲取的,得通過一定技巧來,下面這個函數就可以:
eip2ecx: movl (%esp), %ecx ret
這個函數能夠把 eip 放到 ecx 中。
原理很簡單,那就是調用它的 call 指令會把 next eip 放到 stack,并跳到 eip2ecx。所以 stack 頂部就是 eip。這里也可以直接用 pop %ecx 。
所以這條指令能夠拿到 .here 的地址,并且存放在 ecx 中:
call eip2ecx .here: ... .section .rodata .LC0: .string "Hello World\xa\x0"
通過 eip 與數據偏移計算數據地址
然后接下來,由于匯編器能夠算出 .here 離 .LC0(數據段起始位置): .LC0 - .here ,對匯編器而言,這個差值就是一個立即數。如果在 ecx 上加上(addl)這個差值,是不是就是數據在運行時的位置?
我們在 .here 放上下面這條指令:
call eip2ecx .here: addl $(.LC0 - .here), %ecx ... .section .rodata .LC0: .string "Hello World\xa\x0"
同樣能夠拿到數據的地址,等同于:
movl $.LC0, %ecx # ecx = $.LC0, the addr of string
下面幾個綜合一起回顧:
- addl 這條指令的位置正好是運行時的 next eip (call 指令的下一條)
- .here 在匯編時確定,指向 next eip
- .LC0 也是匯編時確定,指向數據開始位置
- .LC0 - .here 剛好是 addl 這條指令跟數據段的距離/差值
- call eip2ecx 返回以后,ecx 中存了 eip
- addl 這條指令把 ecx 加上差值,剛好讓 ecx 指向了數據在內存中的位置
完整代碼如下:
# 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,就不需要寫死加載地址了,隨便放,而且不需要調整 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)))
直接用內聯匯編嵌入二進制文件
在這個基礎上,可以做一個簡化,直接用 .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; }
這個內聯匯編的效果跟上面的鏈接腳本完全等價。
把數據直接嵌入代碼中
進一步簡化匯編代碼把 eip2ecx 函數去掉:
# 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"
再進一步,直接把數據搬到 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
小結
本文通過 eip + 偏移地址 實現了運行時計算數據地址,不再需要把 Binary 文件裝載到固定的位置。
另外,也討論到了如何用 .pushsection/.popsection 替代 ld script 來添加新的 Section,還討論了如何把數據直接嵌入到代碼中。
以上所述是小編給大家介紹的ELF轉二進制允許把 Binary 文件加載到任意位置,希望對大家有所幫助!
相關文章
os_object_release Crash 排查記錄分析
這篇文章主要為大家介紹了os_object_release Crash 排查記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11