👾

自作ゲームのチートを自作する

2023/12/22に公開

記事の概要

気づけばもう年末です。
年末年始はゲームをプレイして過ごす方もいらっしゃるのではないでしょうか。
ゲームには古来よりチートがついて回っています。

チートの原理を、チートを作成する体験を通じて、理解するのが本記事の目的です。
具体的には、自作のじゃんけんゲームのバイナリを書き換えます。

以下が前提です

  • 簡単なC言語のプログラムが読める
  • CPU : x86_64
  • コンパイラ : gcc

※本記事はチートを推奨するものではありません。
技術的な理解を目的としています

チートの作成工程

チートには色々な手法がありますが、今回はプログラム(バイナリ)の一部を書き換える方法をとります。

基本的なチート作成の手順は以下のようになります。

バイナリの解析からやると大変なので、今回は「ゲームのソースコードを入手した」という設定で進めます。

じゃんけんゲーム

今回使用する自作ゲームはCUIのじゃんけんです。
おいおいそれはゲームなのか?という疑問が聞こえてきそうですね!
ゲームです!

プログラムフローは以下のようになっています。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// コンピュータのグーチョキパー選択
char getComputerChoice() {
  srand(time(0));        // シード作成
  int num = rand() % 3;  // 0~2の値をランダムに生成

  if (num == 0) return 'R';  // Rock(ぐー)
  if (num == 1) return 'P';  // Paper(ぱー)
  return 'S';                // Scissors(ちょき)
}

// 勝敗判定
int determineWinner(char user, char computer) {
  if (user == computer) {
    return 0;  // あいこ
  } else if ((user == 'R' && computer == 'S') ||
             (user == 'S' && computer == 'P') ||
             (user == 'P' && computer == 'R')) {
    return 1;  // 勝利
  } else {
    return 2;  // 敗北
  }
}

// 勝敗表示
void displayResult(int result) {
  if (result == 0) {
    printf("あいこ!\n");
  } else if (result == 1) {
    printf("勝利!\n");
  } else {
    printf("敗北!\n");
  }
}

int main() {
  char userChoice, computerChoice;
  int result;

  // ユーザー入力の受付
  printf("グーなら'R'、パーなら'P'、チョキなら'S'を入力してください\n");

  // 簡単にするため、入力チェックはしません
  scanf(" %c", &userChoice);

  // コンピュータのグーチョキパー選択
  computerChoice = getComputerChoice();

  // 勝敗判定
  result = determineWinner(userChoice, computerChoice);
  printf("コンピューター : %c\n", computerChoice);
  printf("ユーザー : %c\n", userChoice);

  // 結果の表示
  displayResult(result);

  return 0;
}

作成するチートについて

じゃんけんのチートとして思い浮かぶのは、「必ず勝利する」でしょうか。
「必ず勝利する」ようにゲームを変更するにはどうすればよいでしょうか?

手に入れたソースコードを見ると、勝敗判定はdetermineWinner関数で行われています。

int determineWinner(char user, char computer) {
  if (user == computer) {
    return 0;  // あいこ
  } else if ((user == 'R' && computer == 'S') ||
             (user == 'S' && computer == 'P') ||
             (user == 'P' && computer == 'R')) {
    return 1;  // 勝利
  } else {
    return 2;  // 敗北
  }
}

勝利時は1を返すようになっています。
この関数が必ず1を返すようにすればよさそうですね。


チートのイメージ

そのためには、図のように必ずreturn 1を通るようにしたいですね。[1]
C言語であればgotoすればよさそうです。

では早速、バイナリでチートを作成していきましょう。

チートの作成

先ほどの「じゃんけんゲーム」をコンパイルします。

gcc -o game game.c

バイナリの編集箇所を確認する

じゃんけんゲームのバイナリを見るために逆アセンブルします。

objdump -d -S -M intel ./game > game.asm

逆アセンブルの結果の一部(game.asm)です。

じゃんけんゲームの逆アセンブル(一部)

うーん、わかりませんね!
godboltというサイトを使用すると、Cのコードとアセンブリの対応関係がわかり便利です。

godboltの表示内容
これを見ると、

  • determineWinner関数はmovzx命令から始まっている
    • そのより前の命令は、スタックのプロローグです(説明は割愛します)
  • return 1に対応するアセンブリは、.L9:(mov命令)と書かれている箇所

ということがわかります。

再度、「実際に逆アセンブルした結果」でdetermineWinner関数の中身を確認してみましょう。

determineWinner関数のアセンブリ

今回やりたいことは、「determineWinner」関数に入ったら即座にreturn 1のところにgotoする"です。
これと同じことをバイナリ(アセンブリ)で行いましょう。
以下はここまで調べた内容である、(C言語とバイナリの対応関係)をまとめたものです。

C言語 バイナリ(アセンブリ)
determineWinner関数の開始位置 0x128F(movzx命令)
return 1;の位置 0x12C3(mov命令)
goto ジャンプ命令

つまりバイナリ目線でやることは、0x128Fから0x12C3にジャンプするです。

ジャンプ命令

ジャンプをする場合、ジャンプ命令を使用します。
今回はジャンプの距離が短いので、ショートジャンプ命令(0xEB)というものを使用します。
ショートジャンプ命令は2バイト命令で、以下のようになっています

  • 最初の1byteはショートジャンプ命令を表す
  • もう1バイトはオフセット(ジャンプする距離)

ジャンプのオフセット(ジャンプ距離)を計算します。
この計算は少しややこしいです。

ショートジャンプの例

このようにメモリアドレス100番にショートジャンプ命令が配置されているとします。
100番にショートジャンプを表す0xEBがあり、101番にオフセット(ジャンプ距離)が置かれます。
ジャンプ先は107番だとします。
この場合、オフセットは緑色の大きさとなります。
つまり、「ジャンプ命令」と「ジャンプ先」の間の個数となります。

バイナリの編集

よって今回の場合は、

  • ジャンプ元のアドレス : 0x128f(0f b6 45 fc)
  • ジャンプ先のアドレス : 0x12c3(b8 01 00 00 00)
  • ジャンプのオフセット : 0x12c3 - 0x1291 = 0x32

となります。

この命令をどう追加すればよいでしょうか?
単純に追加することは不可能なので、既存の命令を上書きします。
そのため、determineWinner関数の開始位置であるmovzx命令を上書きします。

やることが決まったので、バイナリエディタ(今回はvscodeのhex)で編集しましょう!

編集前


編集後

movzxは4バイト命令、ショートジャンプは2バイト命令。
命令サイズに差があるため、余ります(4-2=2)。
余りをnop命令(何もしない命令、0x90)で穴埋めします。

チートの動作検証

では実行してみましょう。

以下はじゃんけんゲームの通常の動作。

通常のじゃんけんゲーム

チート後(バイナリ編集後)の動作

通常のじゃんけんゲーム

「負け」や「あいこ」の場合でも勝利してますね、成功です!
バイナリをたった2バイト(nopを含めると4バイト)書き換えるだけで、必ず勝つことができるようになりました!

脚注
  1. 最初にreturn 1;すればええやん、などと思われるでしょう。記事の都合です。 ↩︎

Aidemy Tech Blog

Discussion