製品版のソーシャルゲームにおいてチート対策は必須であります。
そのために通信データの改竄などいろいろ対策するべきところはありますが、
ここではメモリ上の数値の改竄対策を考えてみたいと思います。
制作も大詰めでリリースが近づいている。
そんな時にそう言えばメモリの改竄対策ってしてないなと気付いてしまう……
class Player {
public:
int maxHp;
int currentHp;
int atk;
int def;
};
しかし、こんな感じのクラスがいっぱいあって今更暗号化機能なんて入れられない!
本来なら全ての計算をサーバー側でやるべきだがゲームデザインやその他の理由で難しい
って時になるべく修正が少なくてすむように考えてみました。
暗号化アルゴリズムを作成する
今回は単純なものを作成します。
入力値に定数を足す。復号時は足した定数を引いてやるという簡単なものです。
template <class T>
class salty_cipher {
public:
T encrypt(T unencrypted) const {
return unencrypted + 12345;
}
T decrypt(T encrypted) const {
return encrypted - 12345;
}
};
プリミティブ型のラッパークラスを作成する
暗号化アルゴリズムを後で変更出来るようにテンプレートパラメーターとしておきます。
また暗黙的変換により暗号化、復号が出来るようにコンストラクタと各種演算子のオーバーロードもしておきます。
template <class T, class Cipher>
class cipher_value {
private:
Cipher _cipher;
T _encrypted;
public:
cipher_value() {
}
// 暗号化用コンストラクタ
cipher_value(const T &value) : _encrypted(_cipher.encrypt(value)) {
}
// 暗号化用代入演算子
cipher_value &operator=(const T &unencrypted) {
_encrypted = _cipher.encrypt(unencrypted);
return *this;
}
// 復号
operator T() {
return _cipher.decrypt(_encrypted);
}
};
その他の演算子もオーバーロード
プリミティブ型と同じように扱えるように算術演算子や比較演算子を作成全部作成します。
cipher_value &operator+=(const cipher_value &other) {
_encrypted = _cipher.encrypt(static_cast<T>(*this) + static_cast<T>(other));
return *this;
}
cipher_value &operator+=(const T &unencrypted) {
_encrypted = _cipher.encrypt(static_cast<T>(*this) + unencrypted);
return *this;
}
cipher_value &operator-=(const cipher_value &other) {
_encrypted = _cipher.encrypt(static_cast<T>(*this) - static_cast<T>(other));
return *this;
}
cipher_value &operator-=(const T &unencrypted) {
_encrypted = _cipher.encrypt(static_cast<T>(*this) - unencrypted);
return *this;
}
/* ### 以下略 ### */
使ってみる
// 別名テンプレート
template <class T>
using salty_value = cipher_value<T, salty_cipher<T>>;
void test_salty_value() {
// ここで暗号化
salty_value<int> encrypted = 12345;
// 元の型に暗黙的に変換することで復号される
int unencrypted = encrypted;
printf("value = %d\n", unencrypted);
}
Playerクラスを修正する
これでメモリ改竄対策はOK?
class Player {
public:
salty_value<int> maxHp;
salty_value<int> currentHp;
salty_value<int> atk;
salty_value<int> def;
};
演算子をオーバーロードしているので大抵の処理はそのままで通ると思います。
ただしprintfやCocos2d-xで使うCCLOGのような関数の引数として渡すときなど
暗黙的な変換が出来ない場合はキャストする必要があります。
salty_value<int> encrypted = 12345;
// これはダメ
printf("value = %d\n", encrypted);
// これはOK
printf("value = %d\n", static_cast<int>(encrypted));
もうちょっと複雑にしてみる
演算子を全部オーバーロードしたベースクラスともう少し暗号化アルゴリズムを複雑にしてみたものをGitHubに置いておきました。
https://github.com/idaisuke/ferrum/tree/master/include/ferrum/encryption
fe::xor_cipher_value
ランダムで生成したキーとXOR演算を行って暗号化する。
fe::aes_cipher_value
ランダムで生成したキーとaes_128_ecbで暗号化する。
(OpenSSLが必要)
#include "xor_cipher_value.h"
#include "aes_cipher_value.h"
class Player {
public:
fe::xor_cipher_value<int> maxHp;
fe::xor_cipher_value<int> currentHp;
fe::aes_cipher_value<int> atk;
fe::aes_cipher_value<int> def;
};
最後に
正直なところこれだけではチート対策としては不十分です。怪しいところをいじれば目的の値にはならなくとも数値は変わってしまいます。
これに加えて改竄を検知したらアプリを強制終了させるなどのやり方も必要そうです。