LoginSignup
5
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-10

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

本記事では、HSP3を使ったWindows向けゲームの簡単なチート対策について説明します。前回はメモリハック編を書きました。

概要

実行環境

テストに使用した実行環境の重要な部分は明記しておきます。
OS : Windows 10(64bit) on VMware
HSP : v3.4 (32bit)

対象となるチート

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

目標

今回は、暗号化されたセーブデータを操作するためのHSP3でチート対策モジュールを作ります。このモジュールでは次ようなチートを防止します。

  • セーブデータの改ざん

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

  • ゲーム実行中のメモリ改ざん --> メモリハック編を参照
  • 他人のセーブデータのコピー
  • ソースコードがバレている前提でのチート

ソースコード

いきなりですが、まずは最終的なソースコードを書きます。

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

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

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

関数説明

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のセーブデータです。文字列なんかは丸見えですし、整数や実数も目で見ただけでは分かり難いですが、そのまま記載されています。
01.png

次に適当な鍵で暗号化したexample.hspのセーブデータです。シグネチャ("HSP3")以外は完全に暗号化されています。
02.png

まとめ

  • ちゃんと整数、文字列、小数の型に対応して、予定よりも実用的なモジュールになった。
  • 適当に作ったハッシュ関数なので衝突しないか不安。
  • ファイルの指定したオフセット以降を削除する命令は無いのかな?
5
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3