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

