HSPのチート対策いろいろ -セーブデータ編-

#HSPのチート対策いろいろ -セーブデータ編-
本記事では、HSP3を使ったWindows向けゲームの簡単なチート対策について説明します。前回は[メモリハック編](http://qiita.com/ptr-yudai/items/3b834cd1765adb17ba96)を書きました。

##<i class="fa fa-cubes"></i>概要
###<i class="fa fa-cube"></i>実行環境
テストに使用した実行環境の重要な部分は明記しておきます。
OS : Windows 10(64bit) on VMware
HSP : v3.4 (32bit)


###<i class="fa fa-cube"></i>対象となるチート
ゲームのチート(不正行為)にもいろいろな種類があります。今回は、ゲームのセーブデータ等、ゲームが終了したあとに残るデータを改ざんする**データファイルハック**に対するチート対策方法について考えます。実際のチート対策は逆解析対策やメモリハックへの対策などが必要になってきますので、この記事の内容だけで満足しないようにお願いします。


###<i class="fa fa-cube"></i>目標
今回は、暗号化されたセーブデータを操作するためのHSP3でチート対策モジュールを作ります。このモジュールでは次ようなチートを防止します。

- セーブデータの改ざん

ただし、次のようなチートには対応しません。

- ゲーム実行中のメモリ改ざん --> [メモリハック編](http://qiita.com/ptr-yudai/items/3b834cd1765adb17ba96)を参照
- 他人のセーブデータのコピー
- ソースコードがバレている前提でのチート

##<i class="fa fa-cubes"></i>ソースコード
いきなりですが、まずは最終的なソースコードを書きます。

```hsp:mod_protect.hsp
#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
```

このモジュールは以下のように使用します。

```hsp:example.hsp
#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
```

###<i class="fa fa-cube"></i>関数説明
`dsave "filename", "tag", data, data_size`
"filename" : セーブデータのファイル名
"tag" : ロード時に使用する、値を特定するためのタグ
data : セーブしたい変数
data_size : dataのサイズ(byte)

`dload "filename", "tag", data, flag`
"filename" : セーブデータのファイル名
"tag" : セーブ時に指定したタグ
data : ロード先の変数
flag : セーブデータから取得したサイズに合わせて自動でdataを初期化する場合は1を指定

###<i class="fa fa-cube"></i>セーブデータの構造
セーブデータはPROTECT_SIGNATUREで指定される4バイトの文字列をシグネチャとして開始します。その後は基本的に次の4つのデータが連続して詰め込まれます。

- データ名のハッシュ値(4[byte])
- データのサイズn(4[byte])
- データのチェックサム(4[byte])
- データ(n[byte])

データ名のハッシュ値は、例えば変数scoreを"score"として保存したときに、このハッシュ値をタグとしてデータを保存します。次に読み込む（もしくは上書きする）ときは、これと同じタグを持った部分を探索します。データのサイズはそのままデータサイズです。データのチェックサムは、次にくるデータのチェックサムで、これを読み込み時に検証して、一致しない場合はロードしません。暗号化に加えてこのチェックを付けたのは、実は特殊なケースでは今回のセーブデータは復号化できる可能性があるからです。（データサイズの0x00を利用した暗号解読）
今回はデータ名のハッシュ値とデータのチェックサムに使用される関数は同じです。また、最初の12バイトとデータ本体は個別にPROTECT_KEYを鍵としたXOR暗号にかけられます。（PROTECT_KEYを空文字にすると暗号化せずに保存できます。）
よく鍵を1バイトにしてXORしている例を見かけますが、簡単に破られてしまうので鍵長はある程度大きくしてください。

###<i class="fa fa-cube"></i>結果
まずは暗号化無しのexample.hspのセーブデータです。文字列なんかは丸見えですし、整数や実数も目で見ただけでは分かり難いですが、そのまま記載されています。
![01.png](https://qiita-image-store.s3.amazonaws.com/0/151385/c6c79c4c-f5e6-0756-a225-727e07e1870b.png)

次に適当な鍵で暗号化したexample.hspのセーブデータです。シグネチャ("HSP3")以外は完全に暗号化されています。
![02.png](https://qiita-image-store.s3.amazonaws.com/0/151385/3e739e00-47cb-8ccf-f402-d4715b3a8123.png)

##<i class="fa fa-cubes"></i>まとめ
- ちゃんと整数、文字列、小数の型に対応して、予定よりも実用的なモジュールになった。
- 適当に作ったハッシュ関数なので衝突しないか不安。
- ファイルの指定したオフセット以降を削除する命令は無いのかな？