导言
这类题型还是我复现CISCN_2019_西南的PWN1的时候遇见的,算是涨知识了
前置知识
我们都知道,在程序中最先调用的不是main,也不是__libc_start_main,而是_start,我们来看一下再x86下的_start
1 | .text:08048420 public _start |
我们可以看到,在_start结束的时候会调用__libc_start_main,而我们再看__libc_start_main的函数
1 | // attributes: thunk |
可以看到,包含有main,init,fini,既然传进去了这些参数,那必然有他们的用处,main和init就不用多说了,fini是做什么的呢?我们得跟进看一看。

可以看到,__libc_start_main的返回地址就是__libc_csu_fini,证明它是在__libc_start_main在结束后就会调用__libc_csu_fini,要是我们能对它进行一些修改,那说不定就能做一些“坏事”。我们来看看跟它相关的东西。
我们可以在fini_array段找到与__libc_csu_fini相关的东西,是数组
1 | .fini_array:0804979C ; ELF Termination Function Table |
在这个数组里存放着一些函数的指针,并且在进入__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函数,我们先修改一下
1 | fini_array = 0x804979C |
OK,现在我们再看看

可以看到已经成功修改了,因为在这里边会将指针遍历并调用,所以我们会再次执行main函数
至于为什么是倒序写fini_array,就跟我上面说的fini_array的下标是从大到小开始执行的,所以越往后越先执行
既然我们能再一次调用main函数,那么我们就可以在第一次main函数执行的时候把printf@got指向system@plt,在第二次main函数执行时,就可以直接往scanf里传/bin/sh\x00,从而达到getshell的效果,跟后门一样。
Payload
1 | from pwn import * |
后日谈
这玩意在x64的情况其实跟x86差不多(特指格式化字符串),就是多了寄存器传参,步骤会变得更加繁琐一点。这个技巧我觉得更适用于无计可施的时候,在其它攻击方法能用的时候,都不会挑这个来用,只有想不到别的办法的时候,它的价值才体现出来
当然,fini_array的作用远不止于此,它还可以和更多的攻击方式利用起来
比如
Loop链
1 | fini_array[0] = __libc_csu_fini |
因为是从大到小开始执行,所以它的执行流程是这样的
_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链布置完后,要想跳出循环
1 | fini_array[0]修改为leave_ret |
修改完后就能getshell了
至于为什么没有例题…是因为我还没有遇到,如果有我后续会补上来的