环境准备
c程序 test.c :
编译:
选项 -O0
表示不进行任何优化, -fno-stack-protector
表示无canary保护。
基础知识
C语言调用机制使用了栈数据结构,先进后出。同时栈是由高地址向低地址增长。
在64位环境下,函数的调用所需要的参数是优先通过寄存器来进行的。寄存器的顺序如下:rdi,rsi,rdx,rcx,r8,r9。当一个函数有大于6个整形参数,则超出的部分会通过栈来传递,这个情况少见。
64位环境下,还有几个特殊的指针(寄存器):rip、rbp和rsp。其中rip是指令指针,cpu会把rip指向的内容当作指令执行。rbp指向当前栈帧的底部。rsp指向当前栈帧的顶部。
ROP
由于64位下,是优先通过寄存器来传参,所以不能像在32位环境下直接去布置栈上的数据来exp。这时需要用到ROP技术(Retrun-oriented Programmming),从可执行文件或者库中提取部分代码片段来进行恶意利用。
比如我们想要传入一个参数,那这个参数需要被布置到寄存器rdi中,这时我们可以寻找诸如pop rdi;ret
的代码片段,从而在执行完pop rdi
后把栈上布置好的数据存放到寄存器rdi中后能够再次控制程序执行流(ret)
常见的寻找ROP的工具有很多,这里使用ROPgadget。以前面的例子为例。
分析
我们的测试程序是
有非常明显的栈溢出漏洞,read从标准输入流(0)读取0x100放到name里面,之后write从name中读取长度为0x100的字节输出到屏幕(1)。我们可以通过输入,从而去覆盖func()的返回地址,从而劫持控制流。
为能找到溢出点,可以使用pattern.py来测试。
接着利用gdb,在运行(r)后输入上述生成的字符串,此时gdb发生段错误。因为是在64位环境下,指针无法到达高地址,即不能超过0x00007fffffffffff,所以不能直接利用查看$eip的方法。但因为ret
指令,相当于pop rsp
,所以只要看一下rsp
的值,就知道跳转的地址,从而知道溢出点。
所以,溢出点是88个字节。
提供libc
环境准备
用ldd命令可以看到pwn程序运行时使用的libc.so。
将/lib/x86_64-linux-gnu/libc.so.6
拷贝到当前目录下。
思路
- 提供了libc.so,可以计算出read函数与system函数和sh字符串的偏移量。
- 利用write函数泄露出read函数的地址,从而计算system函数和sh字符串的真实地址。
- 调用system函数,并传入参数,即sh字符串。
exp:
接下去根据exp进行一下详细的讲解。
offset偏移由pattern.py和gdb计算得出。read与system地址和sh地址由提供的libc,结合pwntools得到。
|
|
两个地址,由ROPgadget得到,用于参数的传递。
|
|
利用write函数泄露read函数地址。'a'*offset
后到溢出点return跳到执行pop rdi
,此时栈顶的数据为1,pop完后1被保存在寄存器rdi中,作为write的第一个参数。之后返回(ret)跳转到执行pop rsi
,此时栈顶的数据为read函数的plt表地址,pop完后,其地址被保存在rsi中,作为write的第二个参数。接下去要执行pop r15,没有什么用,所以我们随便写一个p64(1)进去。再接下去就是ret(返回)跳转到func函数,以便进行下一次利用。为什么我们没有设置write的第三个参数呢??? 见下文。
|
|
前面发送payload后,先执行了正常的write函数流程,注意源程序中的write的第三个参数是0x100,所以需要先p.recv(0x100),之后由于func的返回地址被覆盖了,程序流程会进入我们设置好的rop中。在rop链中,我们修改了write的前两个参数,此时write的函数调用如下:write(1,read_plt_addr,0x100)。我们的目的是获得read函数的地址,在64位环境下为8个字节,所以只需要截取write输出的前8个字节,即read_addr = u64(p.recv(8))。
|
|
获取了read函数地址后,就可以计算system()函数和字符串sh的地址了
|
|
第一次rop结束后,我们让它ret到func函数,接下来构造新的rop。同样利用'a'*offset
先溢出到return,通过pop rdi
,将sh字符串的地址保存到寄存器rdi中,作为system()函数的参数。之后是ret,直接返回到system()函数的地址,从而成功getshell()
附上完整的exp:
不提供libc
若题目没有提供libc的话,需要利用pwntool中的DynELF来泄露地址。但有些地方需要注意,因为DynELF会一直循环地去泄露地址,所以栈可能会有不可控的情况。根据《借助DynELF实现无libc的漏洞利用小结》,可以在函数地址泄露完后,调用_start函数以恢复栈。但我这里测试时,如果在泄露完成后再恢复就没办法pwn成功,我就直接把对_start的调用放到了leak函数里,每泄露一次就恢复一次栈。
思路
- 利用DynELF泄露出system的地址,
- 利用read函数向可写数据段(比如.bss段)写入字符串“/bin/sh”
- 调用system,getshell。
exp
下面也是根据exp具体讲解。
|
|
注意这里,我们已经没有用到libc.so了,手里有的只有pwn这个程序。
|
|
bss_addr是我们准备写入字符串的bss段地址。start_addr用于恢复栈。pop_rdi_ret_addr,pop_rsi_pop_r15_addr由ROPgadget得到。
|
|
利用rop技术,结合write()函数,泄露出地址后又回到func函数体中。注意泄露的地址是8个字节,所以address = p.recv(8)
,而write的第三个参数(0x100)我们没法改变,所以需要用p.recv()接收剩下的字符。接下去就是回到func函数后,会去调用_start函数,恢复栈。完了后再次进入func(),进行下一次泄露。
|
|
利用DynELF泄露出system和read函数的地址。read函数的地址,其实可以直接获得,即注释中的read_addr = elf.plt['read']
|
|
这一部分,调用read函数,向bss段写入/bin/sh
字符串,之后再回到func函数体中。通过pop_rdi_ret_addr
构造了read的第一个参数是0,通过pop_rsi_pop_r15_addr
构造了read的第二个参数为bss_addr
。接下去的那个p64(1)只是为了执行pop_r15
没有其他用处。因此在调用read()时是这样的:read(0,bss_addr,0x100)
。
|
|
这一部分,调用system(),利用pop_rdi_ret_addr
提供参数/bin/sh
附上完整exp