#HSPのチート対策いろいろ -セーブデータ編-
本記事では、HSP3を使ったWindows向けゲームの簡単なチート対策について説明します。前回はメモリハック編を書きました。
##概要
###実行環境
テストに使用した実行環境の重要な部分は明記しておきます。
OS : Windows 10(64bit) on VMware
HSP : v3.4 (32bit)
###対象となるチート
ゲームのチート(不正行為)にもいろいろな種類があります。今回は、ゲームのセーブデータ等、ゲームが終了したあとに残るデータを改ざんするデータファイルハックに対するチート対策方法について考えます。実際のチート対策は逆解析対策やメモリハックへの対策などが必要になってきますので、この記事の内容だけで満足しないようにお願いします。
###目標
今回は、暗号化されたセーブデータを操作するためのHSP3でチート対策モジュールを作ります。このモジュールでは次ようなチートを防止します。
- セーブデータの改ざん
ただし、次のようなチートには対応しません。
- ゲーム実行中のメモリ改ざん --> メモリハック編を参照
- 他人のセーブデータのコピー
- ソースコードがバレている前提でのチート
##ソースコード
いきなりですが、まずは最終的なソースコードを書きます。
#module
// ここは自由に変更してください
#define PROTECT_SIGNATURE "HSP3" // 必ず4バイト
#define PROTECT_KEY "53cur3_k3y!" // XOR鍵
#define PROTECT_SEED 0xFEE1DEAD // ハッシュシード
/*--------------------------------------------------
暗号化されたセーブデータを読み込む
--------------------------------------------------*/
#define global dload(%1, %2, %3, %4=0) dload_ %1, %2, %3, %4
#deffunc dload_ str fname, str dname_, var data, int flag,\
local buf, local size, local hash_dname, local size_data, local chksum, local ret
sdim buf, 64
sdim dname, 64 : dname = dname_
// ファイルを確認
exist fname
size = strsize
if size < 0 {
bsave fname, buf, 0
return -1
}
// シグネチャを確認
bload fname, buf, 4, 0
if buf != PROTECT_SIGNATURE : return -1
// 特定のデータを探索
offset = 4
ret = -1
repeat -1, 1
if offset >= size: break
bload fname, buf, 12, offset
dxor buf, 12 // 復号化
hash_dname = lpeek(buf, 0)
size_data = lpeek(buf, 4)
chksum = lpeek(buf, 8)
// 読みたいデータが来た
if hash_dname == dhash(dname, strlen(dname)) {
offset += 12
sdim buf, size_data
bload fname, buf, size_data, offset
dxor buf, size_data // 復号化
if chksum != dhash(buf, size_data) {
buf = 0
}
if flag == 1: sdim data, size_data
repeat size_data
poke data, cnt, peek(buf, cnt)
loop
ret = 0
break
}
offset += 12 + size_data
loop
return ret
/*--------------------------------------------------
データを暗号化して保存する
--------------------------------------------------*/
#deffunc dsave str fname, str dname_, var data, int size_data,\
local size_data_, local size_fill, local size, local ret
sdim buf, 64
sdim dname, 64 : dname = dname_
// ファイルを確認
exist fname
size = strsize
if size < 0 {
buf = PROTECT_SIGNATURE
bsave fname, buf, 4
size = 4
}
// シグネチャを確認
bload fname, buf, 4, 0
if buf != PROTECT_SIGNATURE : return -1
// 特定のデータを探索
offset = 4
ret = -1
repeat -1, 1
if offset >= size: break
bload fname, buf, 12, offset
dxor buf, 12 // 復号化
hash_dname = lpeek(buf, 0)
size_data_ = lpeek(buf, 4)
chksum = lpeek(buf, 8)
// 書きたいデータセクションがあった
if hash_dname == dhash(dname, strlen(dname)) {
// 情報を変更する
lpoke buf, 0, dhash(dname, strlen(dname))
lpoke buf, 4, size_data
lpoke buf, 8, dhash(data, size_data)
dxor buf, 12 // 暗号化
bsave fname, buf, 12, offset
// 以降のデータをずらす
offset += 12
size_fill = size - offset - size_data_
sdim buf, size_fill
bload fname, buf, size_fill, offset + size_data_
bsave fname, buf, size_fill, offset + size_data
// データを書き込み
dxor data, size_data // 暗号化
bsave fname, data, size_data, offset
// 短くなった場合は余りを削除
if size_data - size_data_ < 0 {
sdim buf, size - (size_data_ - size_data)
bload fname, buf, size - (size_data_ - size_data)
bsave fname, buf, size - (size_data_ - size_data)
}
ret = 0
break
}
offset += 12 + size_data_
loop
// 新規データの書き込み
if ret == -1 {
offset = size
sdim buf, 12
lpoke buf, 0, dhash(dname, strlen(dname))
lpoke buf, 4, size_data
lpoke buf, 8, dhash(data, size_data)
dxor buf, 12 // 暗号化
bsave fname, buf, 12, offset
offset += 12
sdim buf, size_data
memcpy buf, data, size_data, 0, 0
dxor buf, size_data // 暗号化
bsave fname, buf, size_data, offset
ret = 0
}
return ret
/*--------------------------------------------------
XOR
--------------------------------------------------*/
#deffunc dxor var data, int size, local key, local c, local len
len = strlen(PROTECT_KEY)
if len == 0: return
sdim key, len
key = PROTECT_KEY
repeat size
c = peek(data, cnt)
c ^= peek(key, cnt \ len)
poke data, cnt, c
loop
return
/*--------------------------------------------------
ハッシュ関数
--------------------------------------------------*/
#defcfunc dhash var data, int size, local hash
hash = PROTECT_SEED
repeat size
hash = ((hash << 5) + hash) + peek(data, cnt)
loop
return hash
#global
このモジュールは以下のように使用します。
#include "mod_protect.hsp"
// 初期書き込み
data = "Hot Soup Processor"
score = 3141
msg = "こんにちは世界"
dsave "save.dat", "mydata", data, strlen(data)
dsave "save.dat", "score", score, 4
dsave "save.dat", "test", msg, strlen(msg)
// データ更新
data = "HSP"
score = 2718
hp = 345.67
msg = "こんにちは世界!"
dsave "save.dat", "mydata", data, strlen(data)
dsave "save.dat", "score", score, 4
dsave "save.dat", "test", msg, strlen(msg)
dsave "save.dat", "体力", hp, 8 // データを追加
// 読み込み
data = ""
score = 0
msg = ""
hp = 0.0
dload "save.dat", "mydata", data, 1
dload "save.dat", "score", score
dload "save.dat", "test", msg, 1
dload "save.dat", "体力", hp
mes data
mes score
mes msg
mes hp
stop
###関数説明
dsave "filename", "tag", data, data_size
"filename" : セーブデータのファイル名
"tag" : ロード時に使用する、値を特定するためのタグ
data : セーブしたい変数
data_size : dataのサイズ(byte)
dload "filename", "tag", data, flag
"filename" : セーブデータのファイル名
"tag" : セーブ時に指定したタグ
data : ロード先の変数
flag : セーブデータから取得したサイズに合わせて自動でdataを初期化する場合は1を指定
###セーブデータの構造
セーブデータはPROTECT_SIGNATUREで指定される4バイトの文字列をシグネチャとして開始します。その後は基本的に次の4つのデータが連続して詰め込まれます。
- データ名のハッシュ値(4[byte])
- データのサイズn(4[byte])
- データのチェックサム(4[byte])
- データ(n[byte])
データ名のハッシュ値は、例えば変数scoreを"score"として保存したときに、このハッシュ値をタグとしてデータを保存します。次に読み込む(もしくは上書きする)ときは、これと同じタグを持った部分を探索します。データのサイズはそのままデータサイズです。データのチェックサムは、次にくるデータのチェックサムで、これを読み込み時に検証して、一致しない場合はロードしません。暗号化に加えてこのチェックを付けたのは、実は特殊なケースでは今回のセーブデータは復号化できる可能性があるからです。(データサイズの0x00を利用した暗号解読)
今回はデータ名のハッシュ値とデータのチェックサムに使用される関数は同じです。また、最初の12バイトとデータ本体は個別にPROTECT_KEYを鍵としたXOR暗号にかけられます。(PROTECT_KEYを空文字にすると暗号化せずに保存できます。)
よく鍵を1バイトにしてXORしている例を見かけますが、簡単に破られてしまうので鍵長はある程度大きくしてください。
###結果
まずは暗号化無しのexample.hspのセーブデータです。文字列なんかは丸見えですし、整数や実数も目で見ただけでは分かり難いですが、そのまま記載されています。
次に適当な鍵で暗号化したexample.hspのセーブデータです。シグネチャ("HSP3")以外は完全に暗号化されています。
##まとめ
- ちゃんと整数、文字列、小数の型に対応して、予定よりも実用的なモジュールになった。
- 適当に作ったハッシュ関数なので衝突しないか不安。
- ファイルの指定したオフセット以降を削除する命令は無いのかな?