TSecType — 防作弊安全型別

本篇定位

這是 Phase 9 的第 6 篇,也是最關鍵的安全型別解析。 讀完本篇你會了解:為什麼用 Cheat Engine 找到的 HP 位址直接改值會讓遊戲崩潰,以及如何用正確的方式讀寫遊戲的加密數值。


一個現實問題:Cheat Engine 找到的 HP 是假的

如果你曾嘗試用 Cheat Engine 掃描 MapleStory 的 HP,你可能發現:

  • 搜尋到一個地址,顯示值是 9999
  • 你改成 1
  • 遊戲沒有死,有時候直接崩潰,有時候什麼都沒發生

原因:你改的不是 HP,你改的是 HP 的加密版本。

想像這個情境

銀行保險庫的門上有一個「假把手」——看起來像把手,摸起來像把手,但轉動它什麼都不會發生(或是觸發警報)。真正的開門機制在別處,而且是加密的。

TSecType<T> 就是那個假把手。遊戲把重要數值藏在一層加密後面,讓外部的修改工具(包括 Cheat Engine)無法直接生效。


為什麼不能直接讀寫 HP/MP/座標?

MapleStory 對所有重要的數值(HP、MP、玩家座標、物品數量等)都使用 TSecType<T> 加密儲存。直接用 Cheat Engine 找到的「HP 位址」其實存的是加密後的值,不是你看到的數字。

直接寫入加密欄位 = 立即崩潰

若不透過 SetData()/GetData() 操作,而是直接對 TSecType 的記憶體位址寫入原始數值,checksum 驗證失敗後,GetData() 回傳 NULL,幾乎必然導致遊戲崩潰或無聲的防作弊觸發。


TSecData — 加密資料結構

template <typename T>
class TSecData {
public:
    T     data;       // 加密後的數值(不是原始值!)
    BYTE  bKey;       // XOR 加密金鑰
    BYTE  FakePtr1;   // 取自外層 TSecType::FakePtr1 的低位元組
    BYTE  FakePtr2;   // 取自外層 TSecType::FakePtr2 的低位元組
    WORD  wChecksum;  // 完整性驗證碼
};
// sizeof(TSecData<long>) == 0x0C(12 bytes)

TSecType — 安全數值包裝器

template <typename T>
class TSecType {
private:
    DWORD FakePtr1;        // 隨機 DWORD(初始化時 rand())
    DWORD FakePtr2;        // 隨機 DWORD(初始化時 rand())
    TSecData<T>* m_secdata; // 指向加密資料
};
// sizeof(TSecType<long>) == 0x0C(12 bytes)

FakePtr1 / FakePtr2 的低位元組(LOBYTE)會存入 m_secdata,作為指標驗證:如果有人複製記憶體區塊,FakePtr 的值不會匹配,GetData() 就能偵測到篡改。


加密演算法(SetData)

void SetData(T data) {
    m_secdata->bKey = LOBYTE(rand());  // 每次寫入都重新產生隨機 key
    // 初始 checksum 依型別大小設定
    m_secdata->wChecksum = sizeof(T) > 1 ? 39525 : (WORD)(-26011);
 
    for (BYTE i = 0, key = m_secdata->bKey; i < sizeof(T) + 1; i++) {
        if (i > 0) {
            // key 根據上一個明文位元組滾動演進
            key = (key ^ plaintext_byte[i-1]) + key + 42;
            // checksum 用循環移位更新
            wChecksum = (8 * wChecksum) | (key + (wChecksum >> 13));
        }
        if (i < sizeof(T)) {
            if (!key) key = 42;  // key 不能為 0
            encrypted[i] = plaintext[i] ^ key;  // XOR 加密
        }
    }
}

關鍵特徵:

  • 滾動 XOR:每個位元組的 key 都基於前一個位元組計算,不是固定 key
  • 位置依賴:加密結果與每個位元組在數值中的位置有關
  • 每次寫入重新加密rand() 的 key 讓靜態掃描無法預測

解密演算法(GetData)

T GetData() {
    T decrypted;
    WORD wChecksum = 0;
 
    for (BYTE i = 0, key = m_secdata->bKey; i < sizeof(T) + 1; i++) {
        if (i > 0) {
            key = encrypted[i-1] + key + 42;  // 注意:用加密值而非明文滾動 key
            wChecksum = i > 1
                ? (8 * wChecksum) | (key + (wChecksum >> 13))
                : (key + 4) | 0xD328;
        }
        if (i < sizeof(T)) {
            if (!key) key = 42;
            decrypted[i] = encrypted[i] ^ key;  // XOR 解密
        }
    }
 
    // 三重驗證
    if (m_secdata->wChecksum != wChecksum
     || LOBYTE(FakePtr1) != m_secdata->FakePtr1
     || LOBYTE(FakePtr2) != m_secdata->FakePtr2)
    {
        return NULL;  // 完整性驗證失敗 → 回傳 0
    }
    return decrypted;
}

運算子多載(用起來像普通數值)

TSecType<long> hp(100);
 
long current = hp;         // operator T()  → GetData()
hp = 200;                  // operator=(T)  → SetData()
hp += 50;                  // operator+=    → GetData() + 50 → SetData()
hp -= 10;                  // operator-=
hp *= 2;                   // operator*=
hp /= 3;                   // operator/=
bool same = (hp == &hp2);  // operator==

SECPOINT — 安全座標型別

class SECPOINT {
public:
    TSecType<long> y;  // Y 座標(加密儲存)
    TSecType<long> x;  // X 座標(加密儲存)
};

MapleStory 的玩家、怪物座標都用 SECPOINT 儲存,並非直接可讀的整數。

// 讀取玩家座標
SECPOINT* pos = reinterpret_cast<SECPOINT*>(playerBase + POSITION_OFFSET);
long x = pos->x.GetData();
long y = pos->y.GetData();
 
// 設定玩家座標(DLL 中)
pos->x.SetData(500);
pos->y.SetData(300);

ZtlSecure — Thread-local 安全型別

ZtlSecure.h 提供另一種更輕量的加密,使用 ZtlSecureTear(寫入)和 ZtlSecureFuse(讀取):

#define ZTLSECURE_CHECKSUM 0xBAADF00D
#define ZTLSECURE_ROTATION 5
 
// 加密(tear):產生 key + 加密資料,回傳 checksum
unsigned int checksum = ZtlSecureTear(&storage, value);
 
// 解密(fuse):驗證 checksum,回傳明文
T value = ZtlSecureFuse(&storage, checksum);

ZtlSecure vs TSecType 差異:

TSecTypeZtlSecure
用途長期存活的物件欄位(HP/MP)短期的 stack/register 數值
checksum 存放TSecData由呼叫者保管
再加密時機每次 SetData()每次 Tear()

如何在 DLL 中安全修改遊戲數值

// ❌ 錯誤:直接寫記憶體 → checksum 失效 → 崩潰
*(long*)(playerBase + HP_OFFSET) = 9999;
 
// ✅ 正確:透過 TSecType API
TSecType<long>* pHP = reinterpret_cast<TSecType<long>*>(playerBase + HP_OFFSET);
pHP->SetData(9999);
 
// ✅ 正確:讀取時
long currentHP = pHP->GetData();

本篇重點整理

讀完這篇你應該記住的事

  1. TSecType 不是普通整數:遊戲裡的 HP/MP/座標都被加密存放
  2. 一定要用 GetData() / SetData():直接讀寫記憶體必崩潰
  3. SECPOINT 是座標:x 和 y 都是 TSecType<long>,不是 int
  4. Cheat Engine 找到的值是假的:那是加密後的版本,不能直接改

相關筆記