gdb を用いてプロ演1で実装した数当てゲームをチートしてみた.~命令ポインタの書き換え~
こんにちは.あらい大先生です.
前回,前々回の記事で,gdb はレジスタの値を書き換えられることを確認しました.
実は「次に実行する命令はコレですよ」ということを示すレジスタ(命令ポインタと呼ばれています)があります.
そのレジスタの中身を書き換えれば,数字の入力を行わずに Correct! を出力させることができます.
そのため,今回は命令ポインタを書き換えることで数当てゲームをチートしてみます.
前提知識
今回のチートを行うのにあたり,必要な知識を以下で説明します.
- ソースコード(再掲)
- 逆アセンブル(再掲)
- 命令ポインタ
ソースコード(再掲)
以下にソースコードを再掲します.
#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.
今回も 0x00000000000011d5 <+80>: jne 0x11e5 <main+96>
に注目します.
また,前回,前々回の記事を通じて,jne 命令の分岐先の <main+96>
は不正解のときの分岐先であることも確認しました.
逆に言えば,jne 命令で分岐が発生せずに 0x00000000000011d7 <+82>: lea rdi,[rip+0xe4e]
からの命令を実行してくれれば,正解の処理を行ってくれます.
命令ポインタ
命令ポインタとは,CPU の内部にあるレジスタの一種です.
命令ポインタには,分岐が起きないという仮定したときの,次に実行する命令のアドレスが格納されています [1].
このブログでは x86-64 アーキテクチャを扱うので,rip が命令ポインタです.
逆アセンブルの節より,「次に実行する命令は 0x00000000000011d7 <+82>: lea rdi,[rip+0xe4e]
です」となれば良いので,rip に *(main + 82)
を代入すれば良さそうです.
手順
それでは,rip に *(main + 82)
を代入してみましょう.
今回はブレイクポイントを設定する代わりに,start コマンドでプログラムの先頭で一時停止させます.
(gdb) start
前回,前々回の r (run) コマンドとは異なり,数字の入力を求められません.
念のため,disas main コマンドで =>
の位置を確認してみましょう.
(gdb) disas main
すると,以下の出力が得られます.
0x0000555555555185 <+0>: push rbp
0x0000555555555186 <+1>: mov rbp,rsp
=> 0x0000555555555189 <+4>: sub rsp,0x10
0x000055555555518d <+8>: mov edi,0x0
0x0000555555555192 <+13>: call 0x555555555060 <time@plt>
0x0000555555555192 <+13>: call 0x555555555060
は,ソースコードの time(NULL)
に対応しています.
これよりも前の 0x0000555555555189
に =>
があるので,確かにプログラムの(ほぼ)先頭で一時停止しています.
また,p コマンドを用いて,このときの rip も確認してみましょう.
(gdb) p $rip
$1 = (void (*)()) 0x555555555189 <main+4>
=>
が指すアドレスと一致しています.=>
は rip なのかもしれません.
この rip に *(main + 82)
を代入してみましょう.
(gdb) p $rip = *(main + 82)
$2 = (void (*)()) 0x5555555551d7 <main+82>
disas main コマンドで =>
の位置を確認してみましょう.
0x00005555555551d2 <+77>: cmp DWORD PTR [rbp-0x4],eax
0x00005555555551d5 <+80>: jne 0x5555555551e5 <main+96>
=> 0x00005555555551d7 <+82>: lea rdi,[rip+0xe4e] # 0x55555555602c
0x00005555555551de <+89>: call 0x555555555040 <puts@plt>
0x00005555555551e3 <+94>: jmp 0x5555555551f1 <main+108>
=>
が 0x00005555555551d7 <+82>: lea rdi,[rip+0xe4e] # 0x55555555602c を指しています.
これは *(main + 82)
に対応しています.=>
は rip ということを確認できました.
命令ポインタを変更することができたので,c (continue) コマンドでプログラムの実行を再開させてみましょう.
(gdb) c
すると,以下の出力を確認することができます.
Correct!
The secret number is 21845
数字を予想せずに,Correct! を出力することができました.
おわりに
3回にわたって数当てゲームをチートする方法を紹介しました.
今回の記事で一旦このシリーズを終了しますが,他にもチートを行う方法はあると思います.
ぜひ計算機や gdb を勉強してチートの方法を探してみましょう.
参考
- 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-13).