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 を勉強してチートの方法を探してみましょう.

参考

  1. 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).

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です