FSB
Challenge
|
|
この章について
FSB/FSAについて調べたことがある人は、Exerciseまで飛ばしてしまってOKです。 詰まったら是非戻ってきてください。FSB: Format String Bugとは
Format String
FSB (Format String Bug) は、printf()
等のフォーマット文字列を受け取る関数における
フォーマット指定子と引数の数の不整合によって発生するバグです。
以下のようなコードを考えます:
|
|
ここでフォーマット指定子は%s
, %d
, %p
の3つであり、それに対応する引数もbuf
,32
,&num
の3つが与えられています。
それに対して、以下のようなコードではどうなるでしょうか:
|
|
フォーマット指定子3つに対して、対応する引数は一つも与えられていません。 これを実行すると、以下のような結果が得られました:
|
|
それぞれ x/
, -1439696680
, 0x55c7a3f8edc0
が出力されました。
以下、その理由について少し考えてみます。
x64における引数渡し
今回のx64環境では、関数への引数はレジスタとスタックによって渡されます。
整数型引数は、RDI
, RSI
, RDX
, RCX
, R8
, R9
のレジスタに順番に渡されます。
7個目以降の引数はスタックに積まれていきます。
浮動小数点数引数は、XMM0
からXMM7
のレジスタに順番に渡されます。
例として、以下のような関数呼び出し時を考えると:
|
|
call printf
の直前のレジスタ及びスタックの値は以下のようになります:
gef> registers
$rax : 0x0
$rbx : 0x0
$rcx : 0x3
$rdx : 0x2
$rsp : 0x00007fffffffe360 -> 0x0000000000000006
$rbp : 0x00007fffffffe390 -> 0x0000000000000001
$rsi : 0x1
$rdi : 0x0000555555556008 -> '%p %p %p %p %p %p %p : %p %p %p\n'
$rip : 0x0000555555555189 <main+0x40> -> 0xc48348fffffec2e8
$r8 : 0x4
$r9 : 0x5
gef> tele $rsp 5
0x00007fffffffe360|+0x0000|000: 0x0000000000000006 <- $rsp
0x00007fffffffe368|+0x0008|001: 0x0000000000000007
0x00007fffffffe370|+0x0010|002: 0x0000000000000008
0x00007fffffffe378|+0x0018|003: 0x0000000000000009 ('\t'?)
0x00007fffffffe380|+0x0020|004: 0x000000000000000a ('\n'?)
FSBの原理
FSBに話を戻します。
フォーマット文字列中の指定子は、現れた順に対応する引数を参照します。
文字列が %s %s %s
であった場合には、順に2,3,4番目の引数を参照していくことになります
(1番目の引数は、フォーマット文字列自体です)。
指定子が7個以上ある場合には、スタックを順に参照していきます。
スタック上の引数は、呼び出し直前のRSP
が指す場所に置いてあります。
先程の例のように フォーマット指定子の数と引数の数が一致しない場合でも、
printf()
は引数があると想定される場所(レジスタ・スタック)から値を参照してしまいます 。
|
|
つまり、この例ではRSI
, RDX
, RCX
に格納された値を参照します。
指定子が7個以上あった場合には、スタックの値を参照することになります。
以上がFSBの原理です。
Note: `%s`と出力される値
RSI
に格納された値を参照しますと書きましたが、フォーマット指定子によってはその値がそのまま出力されるわけではありません。
例えば、%s
の場合には対応する引数の値自体ではなく、その値がポインタとして指し示すアドレスにある文字列が出力されることになります。FSA: Format String Attack
FSBを利用した攻撃を FSA: Format String Attack といいます。 FSBが顕在化し、FSAが可能になるような典型的な例は以下のような場合です:
|
|
上の例では、ユーザ入力をそのままprintf
の第1引数として渡してしまっています。
この場合ユーザは任意のフォーマット文字列を指定できることになります。
本章に限らず、exploitには R: READ
/ W: WRITE
の2つのベクトルがあります。
基本的にはexploitを成功させるにはR/Wの両方が必要となり、RとWを順を追って獲得していくことを目指します。
FSAは、条件が揃えばRWのどちらも獲得可能な攻撃手法です。
Note: WRITEプリミティブだけを使ったexploit
もちろんexploitの中には、Wだけを使ったものも存在します。 例としては、Heap Exploitテクニックの1種である House of Corrosion が挙げられます。FSAでのREADプリミティブ
.textベースのleak
ここからは、Challengeファイルを使って進めていきます。 ソースコードの抜粋は以下のとおりです:
|
|
buf
に対してユーザ入力を格納し、それをそのままprintf
の第1引数として渡しています。典型的なFSBです。
GDBを開き、loop
関数に入ったあとでブレークポイントで2番目のprintf
で実行を止めてみます:
gef> b loop
gef> run
gef> b printf
gef> c
gef> c
このとき、仮にABCDEFGH
を入力していたとするとRDI
レジスタは以下のようになります:
$rdi : 0x00007fffffffe1f0 -> 'ABCDEFGHIJK'
さて、PIEが有効化されたプログラムは実行するたびに.text
セクションがロードされるアドレスが変わります。
まずはこの.text
baseをleakしましょう。
このleakが必要な理由はwin()
アドレスのleakにおいて説明します。
.text
baseをleakするにあたって知りたいのは、main
, win
, loop
などの.text
セクション内にある関数のアドレスです。
tele
コマンドを使って、使えそうなアドレスがスタック内に落ちていないか探してみましょう:
gef> tele $rsp 40
0x00007fffffffe1e8|+0x0000|000: 0x0000555555555293 <loop+0x90> -> 0x4800000d80058d48 <- retaddr[1] <- $rsp
#### loop() stack #####################################################
0x00007fffffffe1f0|+0x0008|001: 'ABCDEFGHIJK' <- $rdi
0x00007fffffffe1f8|+0x0010|002: 0x00000000004b4a49 ('IJK'?)
0x00007fffffffe200|+0x0018|003: 0x0000000000008000
0x00007fffffffe208|+0x0020|004: 0x0000000000000040 ('@'?)
0x00007fffffffe210|+0x0028|005: 0x000000000000000c ('\x0c'?)
0x00007fffffffe218|+0x0030|006: 0x0000000000000040 ('@'?)
0x00007fffffffe220|+0x0038|007: 0x0000000000000040 ('@'?)
0x00007fffffffe228|+0x0040|008: 0x000000000000000c ('\x0c'?)
0x00007fffffffe230|+0x0048|009: 0x0000000000000000
0x00007fffffffe238|+0x0050|010: 0x00007ffff7c8af6d <_IO_file_write+0x2d> -> 0xc329482e78c08548
0x00007fffffffe240|+0x0058|011: 0x0000000000000002
0x00007fffffffe248|+0x0060|012: 0x00007ffff7e1a780 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x00007fffffffe250|+0x0068|013: 0x0000000000000001
0x00007fffffffe258|+0x0070|014: 0x00007ffff7e1a803 <_IO_2_1_stdout_+0x83> -> 0xe1ba70000000000a ('\n'?)
0x00007fffffffe260|+0x0078|015: 0x0000000000000d68 ('h\r'?)
0x00007fffffffe268|+0x0080|016: 0x00007ffff7c8ca61 <_IO_do_write+0xb1> -> 0x008083b70fc58949
0x00007fffffffe270|+0x0088|017: 0x000055555555601b -> 'Welcome to EchoServer...!'
0x00007fffffffe278|+0x0090|018: 0x000000000000000a ('\n'?)
0x00007fffffffe280|+0x0098|019: 0x00007ffff7e1a780 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x00007fffffffe288|+0x00a0|020: 0x000055555555601b -> 'Welcome to EchoServer...!'
0x00007fffffffe290|+0x00a8|021: 0x0000555555558020 <stdout@GLIBC_2.2.5> -> 0x00007ffff7e1a780 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x00007fffffffe298|+0x00b0|022: 0x00007ffff7e16600 <_IO_file_jumps> -> 0x0000000000000000
0x00007fffffffe2a0|+0x00b8|023: 0x00007ffff7ffd040 <_rtld_global> -> 0x00007ffff7ffe2e0 -> 0x0000555555554000 -> ...
0x00007fffffffe2a8|+0x00c0|024: 0x00007ffff7c8cf43 <_IO_file_overflow+0x103> -> 0xffff57850ffff883
0x00007fffffffe2b0|+0x00c8|025: 0x0000000000000019
0x00007fffffffe2b8|+0x00d0|026: 0x00007ffff7e1a780 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x00007fffffffe2c0|+0x00d8|027: 0x000055555555601b -> 'Welcome to EchoServer...!'
0x00007fffffffe2c8|+0x00e0|028: 0x00007ffff7c8102a <puts+0x15a> -> 0xff19e98b75fff883
0x00007fffffffe2d0|+0x00e8|029: 0x00007ffff7e1a6a0 <_IO_2_1_stderr_> -> 0x00000000fbad2087
0x00007fffffffe2d8|+0x00f0|030: 0x00007ffff7c81765 <setvbuf+0xf5> -> 0x1945138b01f88348
0x00007fffffffe2e0|+0x00f8|031: 0x0000000000000000
0x00007fffffffe2e8|+0x0100|032: 0x00007fffffffe320 -> 0x0000000000000001
0x00007fffffffe2f0|+0x0108|033: 0x00007fffffffe438 -> 0x00007fffffffe6be -> '/home/wataru/Documents/p3land-challs/challs/fsb/src/fsb'
0x00007fffffffe2f8|+0x0110|034: 0xc425c43ec6330900 <- canary
0x00007fffffffe300|+0x0118|035: 0x00007fffffffe320 -> 0x0000000000000001 <- $rbp
0x00007fffffffe308|+0x0120|036: 0x000055555555532f <main+0x81> -> 0x00c3c900000000b8 <- retaddr[2]
今はprintf
をcallした直後であり、RBP
はまだ退避されていません。よって、RSP+0x8
がloop()
のスタックフレーム上端、RBP+0x10
がmain()
のスタックフレーム上端になります。
loop
はmain
に帰るはずのため、loop
スタックフレームの底にはmain
のアドレスが入っているはずです。
pwndbg
やgef
等の拡張を使っている場合には、スタック上の値がそれぞれどのような値なのかをヒントとして出力してくれます。
上の例はgef
の出力であり、loop
スタックフレームの底(+0x0120
)にはmain+0x81
が入っていることが分かります。今回はこれを出力すれば良さそうです。
Note: 関数のプロローグとスタック
x64においては、関数を呼び出す命令 call
を実行すると、現在の関数内の次の命令アドレス(RIP
)がスタックにpushされます。
また、関数呼び出し直後にはRBP
の値をスタックにpushし、RSP
の値をRBP
に退避させたあとでその関数用のRSP
を確保します。
これらの一連の処理を 関数のプロローグ と言ったりすることがあります。
先程の例でRSP+0x8
がloop()
のスタックフレーム上端なのは、call
した直後であり RA: Return Address (loop
)がスタックに積まれているためです。また、RBP+0x10
がmain()
のスタックフレーム上端なのは、loop
のプロローグでRAとRBP
がスタックに積まれているためです。
loop
のスタックフレーム上端、つまりスタックに積まれる引数の先頭は001
番目です(gefの表示は0-originのため+1
)。
対してリークしたい値(RA)が入っているのは036
番目です。よって、リークしたい値はスタック引数の(36 - 1) + 1
番目です(off-by-oneに注意)。
レジスタに入る引数が5個ある(6レジスタの内1つはフォーマット文字列そのもの)ことを考えると、リークしたい値は引数全体の((36 - 1) + 1) + 5 == 41
番目になります。
フォーマット指定子では、 %<num>$<specifier>
のように指定することで何番目の引数を使うかを決めることができます 。つまり、以下のフォーマット指定子は同値になります:
|
|
よって、41番目の引数を参照したい場合には %41$p
と書くことができます。実際にこれを入力すると以下のようになります:
|
|
確かにmain
っぽいアドレスが得られましたね!
実際にはこれがmain
の先頭ではなく main+0x81
であるため、リークしたい値から0x81
を引いてあげる必要があります。
引数のインデックス計算
今回は説明する都合上順を追って理詰めで引数が何番目に当たるかを計算しました。
実際にexploitをする際には、勿論このような計算をしてもいいですが、個人的には取り敢えず適当に
"%40$p:%41$p:%42$p:..."
のようにアドレスを大量に出力させる手法を取ります。
みなさんも一度上の方法で大量にスタック上の値を出力させ、何番目の引数を利用すればleakしたい値を出力できるか試してみてください。
win()
アドレスのleak
このChallengeでは、お誂え向きにシェルをくれるwin()
関数が定義されています:
|
|
win()
やmain()
が実際にどのアドレスに配置されるかはASLRの影響を受けるため実行してみるまでわかりません。
しかし、 win()
とmain()
のアドレスの相対的な位置関係はASLRには影響されません 。
つまり、main
のアドレスがleakできればwin
のアドレスを計算することができます。
nm
コマンドでmain
とwin
のアドレスを調べてみます:
|
|
win
とmain
のアドレス差分は 0x11e9 - 0x12ae
であることが分かります。
先程leakしたmain
のアドレス (0x5630e1f0732f - 0x81)
より、win
のアドレスは (0x5630e1f0732f - 0x81) + (0x11e9 - 0x12ae) == 0x5630e1f071e9
であることが分かります。
Stack Baseのleak
.text
baseがleakできたため、次はstack
のアドレスをleakしましょう。
なぜstack
のアドレスが必要なのかはのちほど説明します。
x64のCalling Conventionを考えると、先程leakしたloop()
のRA(main)のすぐ上にはRBP
があるはずです:
0x00007fffffffe2f8|+0x0110|034: 0xc425c43ec6330900 <- canary
0x00007fffffffe300|+0x0118|035: 0x00007fffffffe320 -> 0x0000000000000001 <- $rbp
0x00007fffffffe308|+0x0120|036: 0x000055555555532f <main+0x81> -> 0x00c3c900000000b8 <- retaddr[2]
これはmain
のスタックフレーム下端のアドレスであるため、これがleakできればスタックアドレスがleakできることになります。
該当する引数は、41 - 1 == 40
です。これを実際に入力してみると:
|
|
スタックのアドレスが得られました。
スタックのアドレスも実行ごとに変わりますが、ここでleakしたアドレスとloop
のスタックフレーム上端のアドレスの差分は実行ごとに不変です。
今回は差分が 0x7fffffffe320 - 0x7fffffffe1f0 == 0x130
であるため、leakしたアドレスから0x130
を引くことで常にloop
のスタックフレーム上端アドレスを得ることができます。
FSAでのWRITEプリミティブ
さて、ここまででleakができたので次は書き込みです。
先程の関数プロローグの話でも出てきたように、スタックフレームの底にはRAが置いてあります。 この値を書き換えてしまうと書き換えた値にRIPを飛ばすことができます。
Note: カナリアとWRITE
WRITEプリミティブは大きく分けて以下の2つがあります:
- Buffer Overflow 等の線形WRITE
- FSA や OoB(Out of Bound) 等の任意アドレスWRITE (AAW)
線形WRITEの場合には、RAを書き換える前にRAの直前に置いてあるカナリアを書き換えてしまうことになります。 この場合には、以下の方法でカナリアをバイパスする必要があるので少し面倒になります:
- フレーム中のカナリアの値をleakして、それを書き込む
- マスターカナリアを上書きする
なお、カナリアのもとの値は Thread Local Storage (TLS) に入っており、全ての関数において共通です。 関数のプロローグにおいてスタックに書き込むことになります。
%n
指定子
フォーマット指定子の内、書き込みを行うことのできる指定子は %n
だけです。
これは、%n
までに出力された文字列の長さを引数で指定されるアドレスに書き込む指定子です。
|
|
上の例の場合%n
までには9文字分が出力されるため、n
には9が書き込まれることになります。
また、%n
には書き込み先の変数の大きさを指定することができます。%n
だと4byte、%hn
だと2byteを書き込みます。
スタックに書き込み対象のアドレスを書き込む
方針としては、%n
を使ってloop
スタックフレームのRAをwin()
のアドレスに書き換えたいです。
ここで注意したいこととして、 %n
が取る引数は書き込み対象のアドレスです。よって、書き込みを行う前にスタック上にこのアドレスを積んでおく必要があります 。
今回のケースではこれはとても簡単です。
先程のgef
の出力でも分かるように、main
のスタックフレーム上端にはbuf
が配置されています。
よって、書き込み対象のアドレスを表す8byteを入力として与えてやれば %7$n
で書き込み対象のアドレスに書き込むことが出来るようになります。
なお、今回書き込み対象のアドレスが0x00007fffffffe308
であることからもわかるように、ASCII文字で表すことができません。pwntools
を使って入力を与えてあげましょう:
|
|
この入力直後のloop
スタックは以下のようになります:
0x00007fffffffe1f0|+0x0000|000: 0x00007fffffffe308
0x00007fffffffe1f8|+0x0008|001: 0x000000000000000a
0x00007fffffffe200|+0x0010|002: 0x0000000000008000
0x00007fffffffe208|+0x0018|003: 0x0000000000000040 ('@'?)
0x00007fffffffe210|+0x0020|004: 0x000000000000000c ('\x0c'?)
確かにスタックの意図した場所に書き込み対象のアドレスが入っていますね。
FSAで書き込みをする際の注意事項
ここまででの説明でRAを書き換えてwin
にRIP
を飛ばすことができます。ただし、FSAで書き込みを行う場合には数点注意事項があります。
まず、一度の書き込みでRAを書き換えようとすると出力文字列が異常に多くなってしまうということです。
今回の例ではmain
のアドレス0x5630e1f071e9
を書き込みますが、そのためには0x5630e1f071e9
文字分を事前に出力する必要があります。
これだけ大量の出力をしようとすると、リモートとの通信に異常時間がかかるかプログラムが落ちてしまうことになります。
そのため、 2byteずつに分けて書き込むことが推奨されます 。
例えば、今回の場合には 0x71e9
, 0xe1f0
, 0x5630
の3つにわけて書き込むことで、
必要な出力文字数は max(0x71e9, 0xe1f0, 0x5630) == 0xe1f0
ですみます。
分割して出力する場合には、事前にスタックに積んでおく対象アドレスも2byteずつずらして3つ書き込んでおく必要があることに注意してください。
続いて、%n
で任意の値を出力するために事前に出力する文字列は、%<width>c
を使うと良いです。
例えば%500c
と入力すると500文字が出力されます。
この際はもちろん対応するレジスタに格納されたchar
値が使われますが、出力する文字自体はなんでも良いので問題ありません。これを使わないと、0xe1f0
文字の出力のために0xe1f0
文字だけ入力してやる必要があります。
今回は用意されているbuf
のサイズが0x100
しかないため、0xe1f0
文字を入力することはできません。
以上でFSAを使って本Challengeを解くことが出来るかと思います。 ぜひリモートサーバでflagを取得してみてください。
Exercise
1. FSB Basic RW
|
|