シミュレーションによる FIA 2
前回は,gdb を使い,人が手で止めて,あらかじめスキップすべきと分かっている jne だけをスキップしました. これは非常に簡単でしたが,現実の攻撃とはかけ離れています. 実際の攻撃者は,
- どの命令を飛ばすべきか事前にわからない
- いつ飛ばせばよいか分からない
という制約があります.
このため,攻撃者は片っ端から位置を変えて何度も攻撃を試し,場所を探し当てる必要があります.
この総当たりをグリッチキャンペーンと呼びます.
今回は,被害者は victim.c のままに,外部の Python プログラムによって自動でスキップを行い,どこで認証がとれるかを発見するまでの流れを体験します.
考え方
前回手打ちした gdb のコマンドは以下でした
b (break)
r 9999 (run 9999)
set $pc = $pc + <命令長>
c (continue)
今回の Injector がやるのはこれを全命令分自動で繰り替えすことだけです. 1 命令ずつスキップ対象を変えながら,上の 4 行を gdb にバッチ実行させ,出力が GRANTED となることを期待します.
課題
「実行開始から n 命令目を飛ばす」を素直に実行すると,ライブラリにもスキップが必要になり,命令数が何倍にも膨れ上がります. これは避けたいため,今回はライブラリ呼び出しは中に入らないこととします. すこしズルですが,これはあくまで実行時間を節約するための措置であり,脅威モデルや攻撃シナリオには影響を与えません.
実装
def advance_one():
_, ins = cur() # いまの命令
m = re.match(r"call\s+0x([0-9a-f]+)", ins["asm"])
if m and gdb.find_pc_line(int(m.group(1), 16)).symtab is None:
gdb.execute("nexti") # ライブラリ呼び出しはスキップせず進める
else:
gdb.execute("stepi") # 自分のコードはスキップする
for _ in range(K): # K 命令目まで進める
advance_one()
pc, ins = cur()
gdb.execute(f"set $pc = {hex(pc + ins['length'])}") # ← K 命令目をスキップ
gdb.execute("continue")
核となる実装は上記のようになります.
外部の Injector はこの K を 0, 1, 2... と変えて何度も呼び,出力が GRANTED かを確認するだけです.
どこに jne があるかや,そもそもどの命令を飛ばせばいいのかを確認していません.
上記を踏まえて Injector を実装してみてください.必要な機能は以下です
- ローカルで ./victim を実行する
- ライブラリ以外の呼び出しを上から順番にスキップする
- 出力を確認し,
GRANETEDとなる条件を記録
実行結果
今回は ASLR や PIE を有効にして実行してみましょう.
gcc -O0 -g -fpie -pie -o victim victim.c
自分の環境での実行結果は以下のようになりました
u@u-VirtualBox:~/FIA$ python3 injector_offset.py
0 cmpl $0x1,-0x14(%rbp) DENIED
1 jle 0x5555555551bf DENIED
2 mov -0x20(%rbp),%rax CRASH
3 add $0x8,%rax DENIED
4 mov (%rax),%rax DENIED
5 mov %rax,%rdi CRASH
6 call 0x555555555070 DENIED
7 jmp 0x5555555551c4 DENIED
8 mov %eax,-0x4(%rbp) DENIED
9 mov -0x4(%rbp),%eax DENIED
10 mov %eax,%edi DENIED
11 call 0x555555555169 GRANTED <<< check
12 endbr64 DENIED
13 push %rbp CRASH
14 mov %rsp,%rbp DENIED
15 mov %edi,-0x14(%rbp) DENIED
16 movl $0x4d2,-0x4(%rbp) DENIED
17 mov -0x14(%rbp),%eax DENIED
18 cmp -0x4(%rbp),%eax DENIED
19 jne 0x55555555518a GRANTED <<< check
20 mov $0x0,%eax GRANTED <<< check
21 pop %rbp CRASH
22 ret ?
23 test %eax,%eax GRANTED <<< check
24 je 0x5555555551f0 GRANTED <<< check
25 mov -0x4(%rbp),%eax DENIED
26 mov %eax,%esi DENIED
27 lea 0xe1e(%rip),%rax # 0x55555555601aDENIED
28 mov %rax,%rdi DENIED
29 mov $0x0,%eax DENIED
30 call 0x555555555060 DENIED
31 mov $0x0,%eax DENIED
これまでは jne を飛ばすことに注目して今したが,jne 以外にもスキップすると攻撃が成功する命令がいくつかあるようです.
考察
前回は check_pin しか見ていないわけですが,スキップ位置の掃引は main も含めた実行の流れ全体をなぞるため,前回よりもスキップすべき命令が多く分かったということです.
- K=11:
call check_pin: そもそも認証用関数が飛ばされないため,レジスタにはeaxの値が残ったまま関数を素通りします. - K=19:
jne: 前回同様です - K=20:
mov $0x0, %eax: 拒否側の「戻り値を 0 にする」という操作が消え,eaxに残った入力値が漏れて認証が OK となる - K=23:
test %eax, %eax: main で戻り値を確認する命令が消え,フラグが直前の不一致のまま残る - K=24:
je: main の,DENIED ならジャンプが消え,そのまま認証が通る
このように,main 側も攻撃ポイントがあることが分かりました.