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],eax0x00000000000011d5 <+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 を書き換えることで数当てゲームをチートする方法を紹介しました.
次週は命令ポインタを書き換えることでチートを行います.

参考

  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-03).
  2. 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).

コメントを残す

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