シミュレーションによる FIA
前回は FIA の仕組みを簡単に解説しました. そのうち,電圧を用いた FIA は,一時的に電圧を落とすことで,命令を実行させない仕組みであることを言及しました. 今回は,gdb を使って手動でこれを再現します.
ターゲットプログラム
今回攻撃するプログラムは以下です. 暗証番号が 1234 で,入力が合致していれば 1, 間違っていれば 0 となります.
#include <stdio.h>
#include <stdlib.h>
int check_pin(int entered) {
int correct = 1234;
if (entered == correct) {
return 1; /* 認証OK */
} else {
return 0; /* 拒否 */
}
}
int main(int argc, char **argv) {
int entered = (argc > 1) ? atoi(argv[1]) : 0;
if (check_pin(entered)) {
printf("GRANTED (entered=%d)\n", entered);
} else {
printf("DENIED (entered=%d)\n", entered);
}
return 0;
}
正常系を確認しましょう.
u@u-VirtualBox:~/FIA$ gcc -O0 -g -no-pie -o victim victim.c
u@u-VirtualBox:~/FIA$ ./victim 9999
DENIED (entered=9999)
u@u-VirtualBox:~/FIA$ ./victim 1234
GRANTED (entered=1234)
これから狙うのは,./victim 999 なのに GRANTED と出させることです.
まず gdb を触る前に,アセンブリで何をしているのかを見ておきます.
u@u-VirtualBox:~/FIA$ objdump -d ./victim
0000000000401156 <check_pin>:
401156: f3 0f 1e fa endbr64
40115a: 55 push %rbp
40115b: 48 89 e5 mov %rsp,%rbp
40115e: 89 7d ec mov %edi,-0x14(%rbp)
401161: c7 45 fc d2 04 00 00 movl $0x4d2,-0x4(%rbp)
401168: 8b 45 ec mov -0x14(%rbp),%eax
40116b: 3b 45 fc cmp -0x4(%rbp),%eax
40116e: 75 07 jne 401177 <check_pin+0x21>
401170: b8 01 00 00 00 mov $0x1,%eax
401175: eb 05 jmp 40117c <check_pin+0x26>
401177: b8 00 00 00 00 mov $0x0,%eax
40117c: 5d pop %rbp
40117d: c3
まず 40116b で入力と 1234 を比較しています. 次に 40116e で前の比較結果を確認し,値が等しくなければ拒否されます. 拒否した場合は 401177 へ飛びます. このとき,仮に何らかの方法で 40116e がスキップされれば,次に401170 が実行されます. 401170 は return 1 なので,認証が合格しています. なので,攻撃者としては,jne という命令を飛ばすことで,入力した PIN が何であっても認証が通るようにしたいと分かります.
gdb によるスキップ
間違った PIN で gdb を起動します.
u@u-VirtualBox:~/FIA$ gdb -q ./victim
Reading symbols from ./victim...
check_pin にブレークポイントを設置します.
(gdb) b check_pin
Breakpoint 1 at 0x401161: file victim.c, line 8.
9999 を PIN として入力します.
(gdb) r 9999
Starting program: /home/u/FIA/victim 9999
Breakpoint 1, check_pin (entered=9999) at victim.c:8
8 int correct = 1234;
次に disassemble でスキップしたい命令の場所を確認します.
(gdb) disassemble
Dump of assembler code for function check_pin:
0x0000000000401156 <+0>: endbr64
0x000000000040115a <+4>: push %rbp
0x000000000040115b <+5>: mov %rsp,%rbp
0x000000000040115e <+8>: mov %edi,-0x14(%rbp)
=> 0x0000000000401161 <+11>: movl $0x4d2,-0x4(%rbp)
0x0000000000401168 <+18>: mov -0x14(%rbp),%eax
0x000000000040116b <+21>: cmp -0x4(%rbp),%eax
0x000000000040116e <+24>: jne 0x401177 <check_pin+33>
0x0000000000401170 <+26>: mov $0x1,%eax
0x0000000000401175 <+31>: jmp 0x40117c <check_pin+38>
0x0000000000401177 <+33>: mov $0x0,%eax
0x000000000040117c <+38>: pop %rbp
0x000000000040117d <+39>: ret
End of assembler dump.
先ほども見たように,今回は jne をスキップしたいので,アドレス 40116e と,その次の命令である 401170 を記憶します. さらに,分岐命令の直前まで実行を進めます.
(gdb) break *0x40116e
Breakpoint 2 at 0x40116e: file victim.c, line 9.
(gdb) c
Continuing.
Breakpoint 2, 0x000000000040116e in check_pin (entered=9999) at victim.c:9
9 if (entered == correct) {
今 jne を実行する直前で止まっていることが分かります. C プログラムとしても if (entered == correct) で止まっていることが分かります. 次に,プログラムカウンタ(次に実行される命令の場所)を確認します.
(gdb) print/x $pc
$1 = 0x40116e
(gdb) set $pc=0x401170
(gdb) print/x $pc
$2 = 0x401170
プログラムカウンタはもともと 0x40116e だったので,次に実行されるのは jne のはずでした. ここで,FIA による命令スキップを模擬する操作として,jne がスキップされ,次の命令が実行されるように変更を加えています. このまま実行してみます.
(gdb) c
Continuing.
GRANTED (entered=9999)
[Inferior 1 (process 4642) exited normally]
入力したのは 9999 で間違った PIN なのに,GRANTED となり,認証と突破できたことが分かります.