newbieからバイナリアンへ

newbieからバイナリアンへ

人参の秘めたる甘さに気づいた大学生日記

【pwn 32.0】glibc2.32 Safe-Linking とその Bypass の概観

 

 

 

 

 

0: 参考

【A】Safe-Linking設計者ブログ

research.checkpoint.com

 

【B】Safe-Linking Bypass の提案

https://www.researchinnovations.com/post/bypassing-the-upcoming-safe-linking-mitigation

 

【C-1】House of io の提案

House of Ioawaraucom.wordpress.com

 

 【C-2】 House of io Remastered

https://awaraucom.wordpress.com/2020/07/19/house-of-io-remastered/ 

 

1: イントロ

こんにちは、ニートです。

この夏もまた、glibcの新しいバージョン(2.32)のリリース日が近づいてきました。

今回のアップデートでは、malloc/freeに Safe Linking というものが追加されます(多分。知らんけど)。かつて2005年のglibc2.3.6において実装された Safe-Unlinking を彷彿とさせる忌々しい名前ですね。本エントリでは、この Safe Linking を概観してみようと思います。それと同時に、Safe-Linkingのbypass方法についても概観し、ほんの少しだけ触れてみようと思います。



尚、この先触れる内容は実は前々から実装されていたかもしれませんが、自分が気づいた時其れ即ち実装された時ということで、悪しからず。

 

2: Safe-Linking 概観

Safe-Linkingは、2020年8月1日リリースのGlibc 2.32においてリリースが予定され既にmasterブランチに乗っている、heap exploitationに対するmitigationのことである

設計者によると、以下の3つの攻撃に対して防衛的役割を果たすとされている

 Our solution protects against 3 common attacks regularly used in modern day exploits:
Partial pointer override: Modifying the lower bytes of the pointer (Little Endian).
Full pointer override: Hijacking the pointer to a chosen arbitrary location.
Unaligned chunks: Pointing the list to an unaligned address. 

( 参考【A】)

 

 

まずは、実際に Safe-Linking が実装されているglibcでバイナリを動かしたときのheapの様子を見てみることにする

以下のソースコードglibc 2.32 用にビルドした

 

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

int main(void)
{
  char *a = malloc(0x20);
  char *b = malloc(0x20);
  char *c = malloc(0x20);
  char *d = malloc(0x20);

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

  return 0;
}

 

これをfreeの直前まで動かした後のheapが以下のようになる。

f:id:smallkirby:20200718000006p:plain



 

chunk A,B,C の順にmalloc()されており、A,B,Cのbkはkey ( > libc 2.29) であるから &tcache が入っているのは言するに値しないだろう

f:id:smallkirby:20200718000140p:plain

 

それはいいとして、注目すべきはABCのfdである

heap addrのように見えるけど、なんかよくわからん値が入っていることが見て取れる

 

これのおかげで、binコマンドによってtcacheのリストを見ようとすると以下のようになるf:id:smallkirby:20200718000408p:plain

pwndbgが2.32に対応していないため、linked listが崩壊していることが分かる

 

 

また、CのfdのLSBを0x00に書き換えてtcache dupを行おうとすると以下のようになる

 

pwndbg> set {char}0x555555559300 = 0x00
pwndbg> c 

f:id:smallkirby:20200718001132p:plain

 

malloc(): unaligned tcache chunk detected というエラーが出てabortしていることが分かる

 

これにより、少なくとも従来のUAFによるLSB書き換えでのtcache dupはSafe-Linkingによって失敗するということがわかるであろう(後述するが、厳密には「失敗する」よりも「失敗する確率が上がる」の方が正しい)

以下で、その実装を見ていくことにする

 

 

3: Safe-Linkingの実装とその仕組み

実装

まずは tcache_put() の実装を以下に示す。

f:id:smallkirby:20200718003213p:plain

( 左の行数はオレンジ表示が絶対行数、その他が相対行数を表している)

 

+12行目において PROTECT_PTR というマクロに free されたchunkのアドレスtcacheに繋がっている最初のbinのアドレスが渡され、その結果がnextに入っていることが分かる

 

PROTECT_PTRは以下のように定義される

f:id:smallkirby:20200718003353p:plain

見ての通り、freeしたchunkのアドレスを12bit右シフトした値と 従来のnextに入るアドレス のxorを返している

REVEAL_PTRマクロは後ほど出てくるが、xorをするという性質上PROTECT_PTRを使いまわしている

 

 

深い話は後にして、_int_malloc()/ _int_free() を眺める

 以下に _int_free() の実装の一部を示す。

f:id:smallkirby:20200718002822p:plain 変更点は、e->key==tcache だった場合の全探索においてリストを辿る際の for ループにおいて REVEAL_PTR を使っていることくらいである

これは、PROTECT_PTR によって加工した値からもとのアドレスを取り出す操作である

 

 _int_malloc() の変更点はこんな感じ

f:id:smallkirby:20200718011339p:plain

f:id:smallkirby:20200718011425p:plain

 

fastbin関係においても tcache と同様に REVEAL_PTR が使用されていることが分かる

 

但し今回はtcacheについて見たいため、tcache_get() の実装を以下に示す

f:id:smallkirby:20200718013022p:plain



aligned_OK(e) というマクロを呼び、チェックに失敗すると先程まさに現れたエラーメッセージが表示されるようになっている

それでは aligned_OK は(名前から推測こそできるものの)何をしているかというと以下のようになっている

f:id:smallkirby:20200718013244p:plain

 

単純に与えられたポインタと MALLOC_ALIGN_MASK(==15)の論理積がゼロかを判断している。これは、与えられたアドレス p が 0x10 align されているかどうかを判断しているに他ならない

 

 

さてここまでで大凡の仕組みは推測できるだろうが、以下で設計者の言葉(参考【A】)も借りながら仕組みを総まとめする

 

 

仕組み

Safe-Linking は 単方向リストのポインタを加工することで、先にあげたようなポインタの書き換えによる攻撃を回避しようとする

この加工は、_int_free() 時に PROTECT_PTR マクロによって行われる

このマクロがそのchunkのアドレスと本来next(fd)に書き込むはずの値の xor を生成することは、先に見たとおりである

 

先程の例を再掲する

f:id:smallkirby:20200718014033p:plain

上から順に chunk A,B,C,D とし、 ABCはこの順に free されてtcacheに入っている

 

例えばAまでfreeし、次にBをfreeする際のことを考えてみる

このとき、従来ならば B(0x5555555592d0) のfdにはAのアドレスである 0x5555555592a0 が入るはずである

しかし今回の修正により、 PROTECT_PTR(0x5555555592d0, 0x5555555592a0) が呼ばれることになった

この内部では、((((size_t) pos) >> 12) ^ ((size_t) ptr))) という式すなわち 0x5555555592d0>>12  ^ 0x5555555592a0 によって、0x55500000c7f9という値が生成される

f:id:smallkirby:20200718014601p:plain

これはまさしく B の next に入っている値と同一である

 

それでは tcache のリンクを参照する際、すなわち tcache に複数のchunkが繋がった状態で malloc() を呼び、tcache に対して 次のchunkのアドレスを書き込みたいという場合にはどうしているのだろうか

つい先程見たように、Cをmalloc()で取り出した後 B の next には PROTECT_PTR によって加工された値が入っているため、tcacheに直接書き込むわけには行かない (そうしてしまうと、最早もとのアドレスを復元することは不可能になってしまう、復号に必要なのは加工された値とそのアドレスの2点なのだから)

そこで、tcache_get() で見たように REVEAL_PTR マクロによってもとのnextの値を復元している

PROTECT_PTRでは所詮2つの値を xor していただけだったから、復号もxorを行うだけで可能である (そして実際に REVEAL_PTR の内部では PROTECT_PTR を呼んでいる)

そのようにして復元した値をtcacheに書き込むのである

 

 

 

ここで最も重要なのは、「攻撃者は『攻撃の初期の段階においては』heapのアドレスを知らない」という事実である

これは、言わずもがなASLR有効の場合にはアドレス空間は下位3nibbleを除いてランダマイズされるからである

先程 PROTECT_PTR で わざわざ chunk のアドレスを 12bit 分シフトさせていたのは、固定値の3nibbleではなくランダマイズされたアドレス部を用いるためであった

この事実と、「もとのnextを復元するためには加工をした結果の値とそのchunkのheap上の値が必要である」という2つの事実を組み合わせることで、「攻撃者は初期の段階でもとのnextの値を知ることができない」という結論が導かれる

 

 

それでは、nextのもとの値のを知ることができないという事実を用いて如何にしてlinear overflowを検知するのかというと、ここで登場するのが先程の aligned_OK マクロである

このマクロは REVEALED_PTRによって復元したnextの値が0x10 aligneされているかどうかを確認する

よって、linear overflow等でnextを書き換える際に、下1nibble分を適切に書き換えてやらないと、この aligned_OK マクロで殺されることになる

 

 

 

.

..

... 

 

 

 

 

そう、おそらく気づいたと思うが

この mitigation は 15/16の確率でしか攻撃を検知できない

overwriteした1nibbleがたまたま正確な値だった場合、エラーを検知できず書き換えられた値をもとにして REVEAL_PTR されたアドレスを next として認識してしまうことになるのだ

これが本エントリの冒頭でexploitを防ぐものではなく、失敗する確率を上げるものであると言った理由である

設計者の言葉を借りるなら、raise the bar らしい

 

 

 

というわけで、Safe-Linkingの実装と仕組みを概観してきた

上では tcache について見てきたが、この実装は一般の単方向リストに適用できるものであり、fastbin にも Safe-Linkingが適用されている

単純なtcache dup、とりわけよく知られた 0x7F テク等はこれで難しくなる

 

尚、この実装はASLRの生成する3nibble分のエントロピを利用したものであり、新たに実装されたコードは非常に少なくオーバーヘッドが小さい

参考【1】よりベンチマーク試験の結果を以下に示す

f:id:smallkirby:20200718021203p:plain

https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/

 

左が従来のmallocの実装、右がSafe-Linkingを実装したものであるが、その殆どで差異がないことがわかる

寧ろSafe-Linkingを実装したもののほうが高速に動作している項目も多いが、これは環境誤差であると考えられる。すなわち誤差が大きく影響するほどにはSafe-Linking実装によるオーバーヘッドは小さいということが見て取れる

 

 

 

4. House of io

Safe-LinkingのBypassについて、まずは House of io について触れておく

なんか突然Twitterで記事が流れてきた為読んでみた。詳しくは  参考【C】を参照

Safe-Linkingでは next/fd を不正に書き換えたまま2回 malloc() を行うとエラーが出るのは上に見たとおりである。そこで、この bypass 方法では tcache の key をleakした上で、tcacheに直接書き換えたいアドレスを書き込んでいる。tcacheに書き込まれるアドレス自体は PROTECT_PTR されていないため、もしこれができれば tcache_dupすることができる。

但し、事前に key の leak が必要なことに加えて、何よりAAWできないといけないことが、現実/CTFの問題においてはかなり厳しく、そもそもAAWが可能であるならばもっと他に色々とできそうな気がしていて、有効な手法なのかどうかは今の段階では疑わしい気がしている。

ということで、この手法について触れるのはここまでとする。

 

【追記:20200719】

@Awarau1 が House of io についてのブログのRemaster版を公開したと教えてくれた

今はまだ確認できていないが、あとで確認する。もしかしたら自分の解釈が間違っていて、凄く有効な方法なのかもしれない (参考【C-2】)

 

 

5. P' から Lの leak

以下に、PROTECT_PTR の仕組みの外観図を参考【A】より拝借して提示する。

f:id:smallkirby:20200719190036p:plain

https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/

ここで P は tcache の next に書き込まれるはずの本来のアドレス、L は PROTECT_PTR で加工に利用する chunk 自体のアドレス、P' は LとPから PROTECT_PTR によって生成される値である

ここで、free したあとの tcache にたいして 8byte のみ read が可能であるという状況を想定し、「P'からLを復元したい」とする。以下に先程の例を再掲する。

 

f:id:smallkirby:20200719190558p:plain

example

まず、全てのchunkに対してUAF(read)が可能であるならば、Lの値は単にtcacheの先端のchunkのnextを読むだけである。上の例においては A を最初に free しているため tcache の先頭に繋がっているが、AのnextにはLがそのまま格納されていることが見て取れる。

次に、BのP'のみがreadできたとする。このとき、Lはheapのアドレスを12bitシフトしているため、Pと比較して上位3nibbleが全て0になっている。すなわち、P'の上位3nibbleはそのままLの値であることがわかる。更に、Pの続く2nibbleは今leakしたLの上位2nibbleとxorしているため、これも直ちに計算によって求めることができる。この作業を繰り返すことによって、Pのみの情報からLをleakすることが可能である。Lをleakすることができたということは(狭い文脈においては) heap のアドレスを完全に掌握できたことになるため、あとは通常通りのoverwriteを PROTECT_PTR と同じ計算を施してから行えば tcache dup が可能ということになる。(勿論 key は適宜書き換える必要があるが、これは1byteでも書き換えれば可能である。)

 

このように、対象chunkが同一ページ内に配置され、且つその中でのオフセットが既知/操作でき、8(or6)byteのleakが可能な場合においては、従来と全く変わらずに tcache dup が可能になる。(但し全くreadができない状況において next の下1byteだけを書き換えて循環tcacheを作るといったことは難しい)
 

上の画像でBのP'=0x000055500000C7F9のみから L=0x555555559 が復元できることを以下のスクリプトで確かめられる。

Pd = int(raw_input("P': "),16)
L = Pd >> 36
for i in range(3):
  temp = (Pd >> (36-(i+1)*8)) & 0xff
  element = ((L>>4) ^ temp) & 0xff
  L = (L<<8) + element
print("L : "+hex(L))

 

f:id:smallkirby:20200719194641p:plain



 

 

6. Further Attack

 参考【B】に、1byteのoverflowで P' をleakし L を計算して、任意の値を再び加工してoverwriteするPoCが置いてある

やっていることは、普通に consolidation を使って overlapping chunk を作り、生じたUAFで P' をleakするだけなので、特に目新しいことはしていないようである

House of io でもそうだったが、今のところは P' を leak することで通常通り overwrite をするという方法が一般的らしい

 

 

7. アウトロ

設計者は36C3 CTFをやっている最中にコレを思いついたらしいです

俺がOnetimePadをなんとか殺している間に、設計者はpwnerを殺そうとしていたのか...

今回潰された/難しくされた脆弱性もそうですが、Intel CETが秋に出るとかどうとかという噂もあって、なんやかんや長い間放置されてきた脆弱性が消えていくのは、悲しいね
因みにこの話をTSG slackでしたところ、物理こそ最強であり、爆破こそ至高という結論に至りました

怖い人たちですね、僕は違いますが。


まぁ結局はどんどん新しいexploitが見つけられ、過去のexploit達は忘れられていくのでしょう

pwner達は血も涙もない薄情糞野郎ばかりですから

 

 

 続く...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(三浦春馬さんのご冥福を心よりお祈りします)