--- tags: pwn --- # (study) - off-by-one error これまでに解いた問題: https://hackmd.io/@Xornet/BkemeSAhU ## これは何? 第2週目も前半が終わり、後半に差し掛かろうとしていますがそろそろ新しい攻撃方法を学びたいと思ってoff-by-one脆弱性(特にnull文字が漏れる奴)に入門することにしました。労働で時間があまり取れなくて自明問題でお茶を濁すか座学まとめをするかで後者を取りました。明日は問題を解きます ## outline off-by-one errorはHeapに限った用語では無い。終端文字の処理ミスやループの回数設定ミスで1文字だけ境界を破って書き込まれてしまう事の総称である。 Heap問題ではその中でも特にヌル文字が誤って余分に書き込まれてしまうoff-by-one-null errorが扱われる。 これは次のチャンクのサイズ部分にヌル文字が入ってしまうことでPREV_INUSEフラグを潰すことを利用する。 これによってoff-by-oneを引き起こしたチャンクは干渉出来る(show, editが可能)にも関わらず、既にfreeされた扱いになり真下のチャンクのfree時に結合されてUnsorted Binへ放り込まれる。適切にprev_sizeを設定しfd, bkのチェックも通過するようにしておけば無事に放り込まれるのでUnsorted Binの中にあるチャンクの一部に干渉することが出来る。 ここで適切なサイズでmallocするとUnsorted Binから切り出しが行われるので切り出されなかったチャンクがUnsorted Binに直接繋がることになりlibc leakが出来る。 また、再度確保させることでDouble Freeも狙える。 ## PREV_INUSEを潰す 隣接するチャンク`p1(0x20), p2(0x100)`を考える。具体的には`AAA...`と`BBB...`が入っている。 ``` <p1> : 41 41 41 41 41 41 41 41 | 41 41 41 41 41 41 41 41 <p1 + 0x10>: 41 41 41 41 41 41 41 00 | 01 01 00 00 00 00 00 00 <p2> : 42 42 42 42 42 42 42 42 | ... ``` ここでstrcpy時に終端文字も付与されることを忘れていて`p1`への`a`のnバイトの書き込みがn+1バイトの書き込みになってしまったとする。ここで特にn=0x18とすると、この時のメモリレイアウトは次のようになる。 ``` <p1> : 61 61 61 61 61 61 61 61 | 61 61 61 61 61 61 61 61 <p1 + 0x10>: 61 61 61 61 61 61 61 61 | 00 01 00 00 00 00 00 00 <p2> : 42 42 42 42 42 42 42 42 | ... ``` 終端文字が付与されたことで`p2`のサイズが0x101から0x100になった。チャンクのサイズは0x10の倍数なので特に大きさに影響はない。ただし`p2`のPREV_INUSEフラグが立っていたのが消えてしまった。 `p2`のPREV_INUSEフラグが消えたということは`p1`は既にfreeされていると判断される。このフラグが使われるのは`p2`がfreeされた時であり、真上のチャンクが使われていないならメモリのデフラグメンテーションの為にチャンクを結合してUnsorted Binへ放り込まれる(上記メモリレイアウトではprev_size部分がイカれているので出来ない、次で詳解する)。 ## チャンク結合 以下では既に対応するサイズのtcacheが満杯でfastbinかそのサイズに収まらなければUnsorted Binに送られるという状況を仮定する(libc 2.26より前ならtcacheが未実装なので関係ない)。 ところでUnsorted Binへ放り込む際のチェックは結構厳しい。 `PREV_INUSE`が立っているのなら結合は起こらないので下のチャンクが整っていれば良いのはこれまでのUnsorted Binに関する問題等で述べた通り。 今回は`PREV_INUSE`が立っていないので真上のチャンクとの結合が発生する、つまり真上のチャンクの状況を整えなくてはならない。 見られるのは`prev_size`部分、つまり`p - 0x16`に入っている数字である。これは前のチャンクのサイズなのでそのサイズが適当かどうかをチェックする。 と言っても実際にチャンクヘッダと合っているかどうかではなくその部分のfd, bkが正当かどうかを確かめる。これが正しくなければfree時に落ちる。 というわけでまず上記メモリレイアウトの改善部分はprev_sizeである。チャンクのサイズギリギリまで書き込みが出来るので当然prev_sizeも編集することが出来る(`strcpy`の場合はヌル文字で止まるので一度大きい値を書き込んでから値を小さくしていきヌル文字で埋めていく)。 当たり前だが`p1`がfreeされていると"偽装"するため`p1`はfreeされていない。したがって`fd`, `bk`メンバまで偽装するのは難しい(main_arenaの位置がバレてるなら別だがそれならそれでlibc leakまで出来ているのでこの攻撃の意味が薄れてしまう)。というわけで既にfreeされているチャンクを利用する。 先程のメモリレイアウトは`p1`, `p2`の2つだけだったがここで`p1`の上に`p0`というチャンクがあったとする。サイズは0x90とする。 そしてこの`p0`は既にfreeされてUnsorted Binに繋がれているとする、ということは正当な`fd`, `bk`メンバを有している。ここで通常の書き込みとoff-by-oneを利用して次のようなチャンクを構成する。 ``` <p0> : <main_arena->top> | <main_arena->top> <p0 + 0x10>: ... <p1> : 61 61 61 61 61 61 61 61 | 61 61 61 61 61 61 61 61 <p1 + 0x10>: b0 00 00 00 00 00 00 00 | 00 01 00 00 00 00 00 00 <p2> : 42 42 42 42 42 42 42 42 | ... ``` `p2`のPREV_INUSEが立っておらず、prev_sizeは0xb0である。そしてなんと`p0`と`p1`の合計サイズは0x20 + 0x90 = 0xb0である。 ということはこの状態で`p2`をfreeすると`p1`を通り越して`p0`が正当な`fd`, `bk`を有しているかが確認される、もちろん`p0`は正しくfreeされたチャンクなのでこのチェックも通過して`p0`(`p1`を内包する)と`p2`が結合されてUnsorted Binに送られる。 ところで`p1`は結局freeされていないのでポインタが利用出来るなら未だに使用(show, edit, delete)出来る。ということは次に示すUnsorted Bin内チャンクの切り出しによって色々悪いことが出来る。 ## Unsorted Bin切り出し Unsorted Bin内に繋がれているチャンクは何らかのmallocが発生した時、そのサイズがチャンクより大きければSmall BinかLarge Binに繋がれるがそうでは無かった場合、切り出しを行ってから残りはUnsorted Binに繋がれる。 上記メモリレイアウトでは`p0`のサイズが0x90だったので`malloc(0x80)`等でこのサイズのチャンクを切り出すと`p0`の分だけ切り出されることになる。すると切り出し後のチャンクの新しい先頭は`p1`に一致する。`p1`はまだ使えるので実質的にUAFの形になる。切り出された残りのチャンクはUnsorted Binに繋がれていたのでここを読むと例によって`main_arena->top`に対応するアドレスが判明する。 それ以外にもこの後にもう一度mallocすると`p1`と同じチャンクを指す別のポインタが得られる。このポインタも`p1`もどちらも生きているのでDouble Freeが出来る。libc-2.27ならtcache poisoningで任意アドレス書き換えが出来る。 ## 参考文献 * [Malleus CTF Pwn](https://bookwalker.jp/de9809fe60-ad5a-4bb7-87d9-92061b55c81f/): kusano CTFで有名なkusano_kさんの書いたPwn解説の薄い本、実は私のPwn入門もこの本からでした。 * [[CB16] House of Einherjar :GLIBC上の新たなヒープ活用テクニック by 松隈大樹 ](https://www.slideshare.net/codeblue_jp/cb16-matsukuma-ja-68459648): heapのアドレスリークによってfd, bkを自身のアドレスにする例