gdb を用いてプロ演1で実装した数当てゲームをチートしてみた.~フラグレジスタの書き換え~
こんにちは.あらい大先生です.
前回の記事では,p コマンドを用いて秘密の数字をカンニングすることで数当てゲームをチートしました.
今回は視点を変えて,条件分岐の仕組みを活用してチートを行います.
前提知識
今回のチートを行うのにあたり,必要な知識を以下で説明します.
ソースコード(再掲)
以下にソースコードを再掲します.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main (void) {
srandom(time(NULL));
int rand_num = random();
int input_num;
printf("Please guess the secret number: ");
scanf("%d", &input_num);
if (input_num == rand_num) {
printf("Correct!\n");
} else {
printf("Incorrect...\n");
}
printf("The secret number is %d\n", rand_num);
return 0;
}
逆アセンブル(再掲)
main 関数を逆アセンブルした結果も再掲します.
Dump of assembler code for function main:
0x0000000000001185 <+0>: push rbp
0x0000000000001186 <+1>: mov rbp,rsp
0x0000000000001189 <+4>: sub rsp,0x10
0x000000000000118d <+8>: mov edi,0x0
0x0000000000001192 <+13>: call 0x1060 <time@plt>
0x0000000000001197 <+18>: mov edi,eax
0x0000000000001199 <+20>: call 0x1030 <srandom@plt>
0x000000000000119e <+25>: call 0x1070 <random@plt>
0x00000000000011a3 <+30>: mov DWORD PTR [rbp-0x4],eax
0x00000000000011a6 <+33>: lea rdi,[rip+0xe5b] # 0x2008
0x00000000000011ad <+40>: mov eax,0x0
0x00000000000011b2 <+45>: call 0x1050 <printf@plt>
0x00000000000011b7 <+50>: lea rax,[rbp-0x8]
0x00000000000011bb <+54>: mov rsi,rax
0x00000000000011be <+57>: lea rdi,[rip+0xe64] # 0x2029
0x00000000000011c5 <+64>: mov eax,0x0
0x00000000000011ca <+69>: call 0x1080 <__isoc99_scanf@plt>
0x00000000000011cf <+74>: mov eax,DWORD PTR [rbp-0x8]
0x00000000000011d2 <+77>: cmp DWORD PTR [rbp-0x4],eax
0x00000000000011d5 <+80>: jne 0x11e5 <main+96>
0x00000000000011d7 <+82>: lea rdi,[rip+0xe4e] # 0x202c
0x00000000000011de <+89>: call 0x1040 <puts@plt>
0x00000000000011e3 <+94>: jmp 0x11f1 <main+108>
0x00000000000011e5 <+96>: lea rdi,[rip+0xe49] # 0x2035
0x00000000000011ec <+103>: call 0x1040 <puts@plt>
0x00000000000011f1 <+108>: mov eax,DWORD PTR [rbp-0x4]
0x00000000000011f4 <+111>: mov esi,eax
0x00000000000011f6 <+113>: lea rdi,[rip+0xe45] # 0x2042
0x00000000000011fd <+120>: mov eax,0x0
0x0000000000001202 <+125>: call 0x1050 <printf@plt>
0x0000000000001207 <+130>: mov eax,0x0
0x000000000000120c <+135>: leave
0x000000000000120d <+136>: ret
End of assembler dump.
今回も 0x00000000000011d2 <+77>: cmp DWORD PTR [rbp-0x4],eax
と 0x00000000000011d5 <+80>: jne 0x11e5 <main+96>
に注目します.
フラグレジスタ
フラグレジスタとは,CPU の内部にあるレジスタの一種です.
条件分岐命令などがフラグレジスタの内容を読み取り,その後の動作へ反映させます.
このブログでは x86-64 アーキテクチャのゼロフラグ (ZF) を扱います(他のフラグを知りたい方は [1] を参照すると良いでしょう).
ゼロフラグは「演算の結果が 0 か否か」を判断するフラグです.
具体的には,演算の結果が
- 0 なら
ZF = 1
- 0 以外なら
ZF = 0
になります.
cmp 命令
前回の記事で,cmp DWORD PTR [rbp-0x4],eax
で [rbp-0x4]
と eax
を比較していることを説明しました.
ただ,比較と言っても実際には [rbp-0x4] - eax
の結果をフラグレジスタへ反映する処理を行っています.
また,この命令の実行前後においては [rbp-0x4]
が秘密の数字,eax
が入力された数値であることも確認しました.
秘密の数字を正しく予想できると [rbp-0x4] = eax
が成り立つため,[rbp-0x4] - eax
の結果は 0 になり,ZF = 1
となります.
一方,予想を間違えると [rbp-0x4] ≠ eax
となるため,ZF = 0
となります.
条件分岐命令
Intel のドキュメント [2] を読むと様々な条件分岐命令が記載されていますが,このブログでは jne 命令のみを扱います.
ドキュメントには「等しくない(ZF=0
)場合 short ジャンプする」と記載されています.
前回の記事と先ほどの cmp 命令での説明を考慮すると,「[rbp-0x4] - eax
が等しくない(ZF=0
)場合,不正解を出力する処理へ分岐する」ということになります.
つまり,jne 命令を実行する直前に ZF = 1
となるように書き換えれば,正解を出力する処理へ分岐させることができそうです.
手順
それでは,jne 命令を実行する直前に ZF = 1
となるようにフラグレジスタを書き換えてみましょう.
まずは b (break) コマンドで 0x00000000000011d5 <+80>: jne 0x11e5 <main+96>
にブレイクポイントを設定します.
(gdb) b *(main+80)
Breakpoint 1 at 0x11d5
等が出力されてブレイクポイントを設定することができたら,r (run) コマンドでプログラムを実行します.
(gdb) r
プログラムを実行すると,Please guess the secret number:
が出力されるので,適当な数字を入力します.
ここでは 67890 と入力することにします.
すると,jne 命令を実行する直前でプログラムが一時停止します(前回の記事と同様に disas main コマンドで確認することができます).
このときのフラグレジスタを p コマンドで確認してみましょう.
このブログでは x86-64 アーキテクチャの CPU で動作するプログラムなので,eflags
がフラグレジスタに相当します.
(gdb) p $eflags
$1 = [ IF ]
ZF = 0
なので,[] の中に ZF
はありません(ただし,ここで乱数を正しく予想できた場合,ZF = 1
となるので [ZF IF]
と出力されます).
前回の記事と同様に,p コマンドでフラグレジスタを書き換えることができます.ZF = 1
にするには eflags
の下位から 6 bit 目を 1 にすれば良いので,(元の eflags
) OR (1 を 6 bit 左へシフトした値) を行えば良いということになります.
(gdb) p $eflags |= (1 << 6)
$2 = [ ZF IF ]
eflags の書き換えを済ませたら,c (continue) コマンドでプログラムの実行を再開させます.
(gdb) c
すると,以下の出力を確認することができます.
Correct!
The secret number is 677208251
予想を外しましたが,Correct! が出力されました.eflags
を書き換えることで,数当てゲームをチートすることができました.
おわりに
今週は eflags
を書き換えることで数当てゲームをチートする方法を紹介しました.
次週は命令ポインタを書き換えることでチートを行います.
参考
- Wikibooks:X86アセンブラ/x86アーキテクチャ,入手先〈https://ja.wikibooks.org/wiki/X86%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9/x86%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3〉(参照 2022-11-03).
- Intel Corporation.:インテル® エクステンデッド・メモリ64 テクノロジ・ソフトウェア・デベロッパーズ・ガイド,入手先〈https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/EM64T_VOL1_30083402_i.pdf〉(参照 2022-11-03).