崙?zhàn):從C++算術(shù)運(yùn)算到內(nèi)存修改的底層原理)
在實(shí)際游戲逆向和反外掛對抗中算術(shù)運(yùn)算的底層實(shí)現(xiàn)是理解內(nèi)存數(shù)據(jù)篡改、屬性計(jì)算和邏輯繞過的基石。很多初級逆向者能下斷點(diǎn)、能改數(shù)據(jù)卻不清楚游戲客戶端里一個(gè)簡單的“攻擊力 基礎(chǔ)攻擊 * (1 加成百分比)”是如何在匯編層面被計(jì)算和存儲(chǔ)的導(dǎo)致面對復(fù)雜的屬性加密或校驗(yàn)時(shí)無從下手。本文將從 C 算術(shù)運(yùn)算符與表達(dá)式的編譯原理出發(fā)結(jié)合游戲內(nèi)存修改的典型場景帶你理解從高級語言代碼到 CPU 指令的映射關(guān)系掌握通過逆向算術(shù)邏輯來定位和修改關(guān)鍵游戲數(shù)據(jù)的方法。無論你是想學(xué)習(xí)游戲安全分析還是想理解反外掛機(jī)制的工作原理都需要先過這一關(guān)。1. 理解 C 算術(shù)運(yùn)算的編譯與運(yùn)行時(shí)表示在編寫或分析外掛時(shí)我們操作的目標(biāo)并非 C 源代碼而是編譯后的二進(jìn)制程序。因此理解源代碼中的算術(shù)表達(dá)式如何被編譯器翻譯成機(jī)器指令是進(jìn)行有效逆向分析的先決條件。1.1 從源代碼到匯編指令的映射C 中的算術(shù)運(yùn)算符,-,*,/,%在編譯后并不會(huì)留下任何高級語言的痕跡。編譯器會(huì)根據(jù)操作數(shù)的類型int, float, double 等、所在平臺(tái)的指令集x86, x64, ARM和優(yōu)化等級生成對應(yīng)的 CPU 指令序列。以一個(gè)最簡單的加法為例// C 源代碼 int playerHealth 100; int healAmount 50; playerHealth playerHealth healAmount;在 x86-64 架構(gòu)下使用 MSVC 編譯器且關(guān)閉優(yōu)化Debug 模式后對應(yīng)的匯編代碼可能類似于mov eax, DWORD PTR [playerHealth] ; 將 playerHealth 的值加載到 eax 寄存器 add eax, 50 ; eax 寄存器中的值加上立即數(shù) 50 mov DWORD PTR [playerHealth], eax ; 將計(jì)算結(jié)果存回 playerHealth 的內(nèi)存地址而在開啟優(yōu)化Release 模式后編譯器可能會(huì)進(jìn)行常量傳播甚至直接計(jì)算出結(jié)果150并存儲(chǔ)或者將這段邏輯內(nèi)聯(lián)到更復(fù)雜的表達(dá)式中使得逆向時(shí)難以直接定位到這段獨(dú)立的“加法”操作。1.2 算術(shù)表達(dá)式在內(nèi)存中的體現(xiàn)游戲中的角色屬性生命值、魔法值、攻擊力、金錢通常以基本數(shù)據(jù)類型int, float存儲(chǔ)在進(jìn)程內(nèi)存的特定地址中。算術(shù)表達(dá)式的作用就是讀取這些內(nèi)存地址的值經(jīng)過計(jì)算后再寫回內(nèi)存或另一個(gè)內(nèi)存地址。逆向外掛的常見思路就是定位數(shù)據(jù)通過搜索變動(dòng)數(shù)值如生命值變化找到存儲(chǔ)該屬性的內(nèi)存地址。分析訪問查找哪些匯編指令讀取或改寫了這個(gè)地址。理解計(jì)算分析這些指令構(gòu)成的算術(shù)邏輯即表達(dá)式。實(shí)施修改通過注入代碼或直接修改內(nèi)存/指令改變計(jì)算邏輯或結(jié)果。例如找到存儲(chǔ)“金錢”的地址后你可能會(huì)發(fā)現(xiàn)一段代碼在每次擊殺怪物時(shí)執(zhí)行money money monsterReward。逆向分析就是要識(shí)別出這條加法指令并可能將其改為money money (monsterReward * 10)。1.3 整數(shù)與浮點(diǎn)數(shù)運(yùn)算的逆向差異這是逆向分析中的一個(gè)關(guān)鍵分水嶺處理不當(dāng)會(huì)導(dǎo)致修改失效或游戲崩潰。整數(shù)運(yùn)算通常使用ADD,SUB,IMUL,IDIV等指令。逆向時(shí)相對直觀重點(diǎn)是識(shí)別操作數(shù)是寄存器、內(nèi)存地址還是立即數(shù)。浮點(diǎn)數(shù)運(yùn)算使用ADDSS,SUBSS,MULSS,DIVSS(單精度) 或ADDSD,SUBSD,MULSD,DIVSD(雙精度) 等 SSE/AVX 指令。浮點(diǎn)數(shù)在內(nèi)存中以 IEEE 754 格式存儲(chǔ)直接搜索其整型表示需要轉(zhuǎn)換。例如游戲中的移動(dòng)速度1.5f在內(nèi)存中可能表示為0x3FC00000單精度。如果你用整數(shù)搜索工具搜1是找不到這個(gè)地址的。必須使用 Cheat Engine 等工具的“浮點(diǎn)數(shù)”或“雙浮點(diǎn)數(shù)”掃描類型。運(yùn)算類型典型匯編指令 (x86/x64)內(nèi)存數(shù)據(jù)特點(diǎn)逆向搜索要點(diǎn)整數(shù)運(yùn)算ADD,SUB,IMUL,IDIV,INC,DEC直接以二進(jìn)制補(bǔ)碼形式存儲(chǔ)使用“精確值”或“變動(dòng)值”掃描整型。注意數(shù)據(jù)寬度4字節(jié)、8字節(jié)。浮點(diǎn)運(yùn)算ADDSS/ADDSD,SUBSS/SUBSD,MULSS/MULSD,DIVSS/DIVSD遵循 IEEE 754 標(biāo)準(zhǔn)位模式非直觀必須使用“浮點(diǎn)數(shù)”掃描類型。修改時(shí)也要注意保持格式否則會(huì)讀成 NaN 或 Inf 導(dǎo)致崩潰。2. 環(huán)境與工具準(zhǔn)備搭建逆向分析工作臺(tái)工欲善其事必先利其器。一個(gè)高效的逆向分析環(huán)境至少需要調(diào)試器、內(nèi)存查看器和反編譯器。2.1 核心工具選擇與配置調(diào)試器與反匯編器x64dbg / OllyDbgWindows 平臺(tái)下強(qiáng)大的動(dòng)態(tài)調(diào)試器適合跟蹤程序執(zhí)行、下斷點(diǎn)、修改寄存器和內(nèi)存。x64dbg 對 64 位程序支持更好是目前主流。IDA Pro靜態(tài)反匯編的行業(yè)標(biāo)準(zhǔn)能生成更易讀的偽代碼F5 功能極大提升分析復(fù)雜算術(shù)邏輯的效率。有免費(fèi)版可用。GhidraNSA 開源的反編譯工具功能強(qiáng)大且免費(fèi)是 IDA 的良好替代品。內(nèi)存掃描與修改工具Cheat Engine游戲修改入門必備。核心功能是掃描內(nèi)存中的數(shù)值變化定位變量地址。它還自帶調(diào)試器、匯編注入和腳本編寫功能非常全能。開發(fā)與編譯環(huán)境Visual Studio用于編寫測試用的 C 小程序理解不同編譯選項(xiàng)Debug/Release、優(yōu)化等級對生成代碼的影響。編譯器MSVC、GCC 或 Clang。了解不同編譯器生成的匯編風(fēng)格差異。2.2 創(chuàng)建一個(gè)用于逆向分析的目標(biāo)程序?yàn)榱税踩?、合法地練?xí)我們首先自己編寫一個(gè)包含典型算術(shù)運(yùn)算的小程序作為逆向分析的目標(biāo)。創(chuàng)建一個(gè)名為ArithmeticTarget.cpp的文件#include iostream #include cstdlib #include ctime int main() { std::srand(static_castunsigned int(std::time(nullptr))); // 模擬游戲角色屬性 int baseHealth 1000; float defenseFactor 0.75f; int incomingDamage 0; int finalHealth baseHealth; // 模擬游戲循環(huán) for (int i 0; i 10; i) { // 模擬受到隨機(jī)傷害 incomingDamage 100 (std::rand() % 200); // 傷害在100-300之間 // 核心算術(shù)表達(dá)式最終傷害 原始傷害 * 防御系數(shù) int actualDamage static_castint(incomingDamage * defenseFactor); // 核心算術(shù)表達(dá)式最終生命 當(dāng)前生命 - 最終傷害 finalHealth finalHealth - actualDamage; std::cout Round i 1 : Incoming incomingDamage , Actual actualDamage , Health Left finalHealth std::endl; if (finalHealth 0) { std::cout Player defeated! std::endl; break; } // 模擬每回合回血 5% finalHealth static_castint(finalHealth * 1.05f); } return 0; }使用 Visual Studio 或 g 編譯它# 使用 g g -o ArithmeticTarget.exe ArithmeticTarget.cpp -static-libgcc -static-libstdc關(guān)鍵點(diǎn)建議使用-O0關(guān)閉優(yōu)化編譯第一個(gè)版本這樣生成的匯編代碼更直接易于理解。之后可以用-O2編譯對比觀察編譯器優(yōu)化如何“混淆”我們的算術(shù)邏輯。這個(gè)程序模擬了游戲中最常見的兩種算術(shù)運(yùn)算乘法傷害減免和減法生命扣除以及一個(gè)額外的乘法回血。我們的逆向目標(biāo)就是定位并修改defenseFactor防御系數(shù)和回血系數(shù)1.05f。3. 逆向?qū)崙?zhàn)定位并修改算術(shù)表達(dá)式現(xiàn)在我們以自己編寫的ArithmeticTarget.exe為目標(biāo)使用 Cheat Engine 和 x64dbg 完成一次完整的逆向流程。3.1 使用 Cheat Engine 定位關(guān)鍵變量啟動(dòng)并附加進(jìn)程打開 Cheat Engine點(diǎn)擊左上角電腦圖標(biāo)選擇ArithmeticTarget.exe進(jìn)程。首次掃描程序輸出初始生命值1000。在 Cheat Engine 的“數(shù)值”框輸入1000掃描類型選擇“精確數(shù)值”數(shù)值類型選擇“4字節(jié)”因?yàn)閕nt通常是4字節(jié)點(diǎn)擊“首次掃描”。過濾地址切回游戲程序讓它運(yùn)行一輪生命值會(huì)變化。將新的生命值例如985輸入 Cheat Engine點(diǎn)擊“再次掃描”。重復(fù)此過程直到左側(cè)地址列表只剩下少數(shù)幾個(gè)地址。通常正確的地址其“值”會(huì)隨程序輸出實(shí)時(shí)變化。確認(rèn)地址將疑似地址添加到下方地址列表并鎖定其值為一個(gè)很大的數(shù)如10000。如果程序輸出中的生命值不再減少說明找到了正確的finalHealth地址。找出是什么改寫了這個(gè)地址在地址上右鍵選擇“找出是什么改寫了這個(gè)地址”。Cheat Engine 會(huì)設(shè)置一個(gè)內(nèi)存訪問斷點(diǎn)。切回程序繼續(xù)運(yùn)行當(dāng)有指令寫入該地址時(shí)調(diào)試器會(huì)中斷并顯示對應(yīng)的匯編指令。你很可能看到一條SUB減法或MOV從計(jì)算結(jié)果移動(dòng)過來指令這就是實(shí)現(xiàn)finalHealth finalHealth - actualDamage的關(guān)鍵代碼。3.2 使用 x64dbg/IDA 進(jìn)行靜態(tài)與動(dòng)態(tài)分析附加進(jìn)程打開 x64dbg附加到ArithmeticTarget.exe。定位代碼通過 Cheat Engine 找到的指令地址在 x64dbg 中按CtrlG跳轉(zhuǎn)到該地址。你會(huì)看到類似下面的匯編片段; 假設(shè) finalHealth 的地址是 [ebp-0x04] actualDamage 在 eax 寄存器中 sub dword ptr [ebp-0x04], eax ; 這就是 finalHealth - actualDamage向上分析查看這條SUB指令之前的代碼尋找actualDamage的計(jì)算過程。你可能會(huì)看到; incomingDamage 可能在 [ebp-0x08] mov eax, dword ptr [ebp-0x08] ; eax incomingDamage cvtsi2ss xmm0, eax ; 將整數(shù) incomingDamage 轉(zhuǎn)換為浮點(diǎn)數(shù)到 xmm0 mulss xmm0, dword ptr [ebp-0x0C] ; xmm0 xmm0 * defenseFactor (浮點(diǎn)數(shù)乘法) cvtss2si eax, xmm0 ; 將浮點(diǎn)數(shù)結(jié)果轉(zhuǎn)換回整數(shù)到 eax即 actualDamage這段代碼清晰地展示了actualDamage incomingDamage * defenseFactor的浮點(diǎn)運(yùn)算過程。其中[ebp-0x0C]很可能就是存儲(chǔ)defenseFactor(0.75f) 的內(nèi)存地址。修改邏輯我們的目標(biāo)是讓角色無敵即actualDamage 0。有幾種方法方法A修改內(nèi)存數(shù)據(jù)找到defenseFactor的地址[ebp-0x0C]用 Cheat Engine 將其值從0.75改為0.0。方法B修改匯編指令在 x64dbg 中將mulss xmm0, dword ptr [ebp-0x0C]這條指令改為xorps xmm0, xmm0將 xmm0 寄存器清零這樣無論原始傷害是多少相乘結(jié)果都是0。方法C繞過減法直接找到sub dword ptr [ebp-0x04], eax指令將其改為nop空操作這樣生命值就不會(huì)被扣減。3.3 關(guān)鍵算術(shù)運(yùn)算的逆向特征與修改策略下表總結(jié)了常見算術(shù)表達(dá)式在逆向中的特征和對應(yīng)的外掛修改思路算術(shù)表達(dá)式 (C)可能的匯編指令 (x64)逆向識(shí)別特征外掛修改思路a b cmov eax, [b]; add eax, [c]; mov [a], eaxADD指令兩個(gè)操作數(shù)。1. 將ADD改為MOV直接賦值。2. 增大c的值如果是立即數(shù)或來自某個(gè)地址。a b - cmov eax, [b]; sub eax, [c]; mov [a], eaxSUB指令。1. 將SUB改為NOP或MOV。2. 將[c]改為 0。a b * cmov eax, [b]; imul eax, [c]; mov [a], eax(整數(shù))mulss xmm0, [c](浮點(diǎn))IMUL或MULSS/MULSD指令。1. 將乘數(shù)[c]改為 1整數(shù)或 1.0浮點(diǎn)。2. 將乘法指令替換為MOV。a b / cmov eax, [b]; cdq; idiv dword ptr [c]; mov [a], eax(整數(shù))divss xmm0, [c](浮點(diǎn))IDIV或DIVSS/DIVSD指令。IDIV前常有CDQ或CQO。1. 將除數(shù)[c]改為 1。2. 將除法指令替換為MOV。a b % cidiv指令余數(shù)在edx寄存器中。在IDIV指令后會(huì)使用MOV從edx取余數(shù)。修改使余數(shù)恒為0例如讓b是c的倍數(shù)或直接修改取余數(shù)后的結(jié)果。a 5add dword ptr [a], 5ADD指令第二個(gè)操作數(shù)是立即數(shù)。將立即數(shù)5改大或把ADD改為NOP。4. 進(jìn)階處理加密、校驗(yàn)與反調(diào)試真實(shí)的游戲客戶端不會(huì)將生命值、金錢等關(guān)鍵數(shù)據(jù)以明文int或float形式放在內(nèi)存中。他們會(huì)采用各種混淆和加密技術(shù)。4.1 簡單的線性變換與校驗(yàn)和這是最基本的保護(hù)方式。存儲(chǔ)值 真實(shí)值 * 系數(shù) 偏移量例如真實(shí)生命值100存儲(chǔ)時(shí)可能是100*2 50 250。逆向時(shí)需要找到乘法和加法指令反推出真實(shí)值。校驗(yàn)和游戲可能同時(shí)存儲(chǔ)數(shù)據(jù)和它的校驗(yàn)和如 CRC32。外掛只修改數(shù)據(jù)會(huì)導(dǎo)致校驗(yàn)和不匹配引發(fā)游戲檢測或崩潰。逆向時(shí)需要找到校驗(yàn)和計(jì)算函數(shù)并在修改數(shù)據(jù)后同步更新校驗(yàn)和。在匯編中你可能會(huì)看到類似這樣的模式; 讀取存儲(chǔ)值 mov eax, [healthStored] ; 逆向變換真實(shí)值 (存儲(chǔ)值 - offset) / multiplier sub eax, 50 ; 減去偏移量 50 sar eax, 1 ; 算術(shù)右移1位相當(dāng)于除以2 (如果乘數(shù)是2的冪) mov [healthReal], eax ; 得到真實(shí)生命值用于顯示和計(jì)算修改時(shí)你必須修改healthStored并且要保證(healthStored - 50) / 2等于你期望的真實(shí)生命值。4.2 面對反調(diào)試機(jī)制的策略游戲會(huì)使用IsDebuggerPresent、CheckRemoteDebuggerPresent、NtQueryInformationProcess等 API或通過 Timing Check、代碼完整性校驗(yàn)來檢測調(diào)試器。在 x64dbg 中應(yīng)對使用插件如ScyllaHide可以隱藏調(diào)試器繞過常見的反調(diào)試檢測。手動(dòng)修改 API 返回值在IsDebuggerPresent函數(shù)開頭下斷點(diǎn)將其返回值在rax/eax中強(qiáng)制改為0mov eax, 0。避開檢測點(diǎn)通過靜態(tài)分析IDA找到反調(diào)試代碼所在的模塊或函數(shù)在調(diào)試時(shí)跳過或繞過這些代碼段。4.3 定位加密解密函數(shù)當(dāng)直接搜索數(shù)值失效時(shí)需要尋找負(fù)責(zé)加密和解密的函數(shù)。數(shù)據(jù)訪問斷點(diǎn)對疑似加密的數(shù)據(jù)地址下“訪問”斷點(diǎn)。任何讀取該地址的代碼都會(huì)中斷這其中很可能包含解密函數(shù)。字符串引用在 IDA 或 x64dbg 中搜索可能相關(guān)的字符串如“health”、“damage”、“decrypt”、“decode”這些字符串可能出現(xiàn)在解密函數(shù)附近。調(diào)用棧分析在游戲更新UI顯示生命值的地方下斷點(diǎn)例如調(diào)用sprintf或文本渲染函數(shù)然后查看調(diào)用棧逆向回溯最終找到從加密數(shù)據(jù)到明文數(shù)據(jù)的轉(zhuǎn)換邏輯。5. 常見問題與排查路徑在逆向算術(shù)邏輯時(shí)你會(huì)遇到各種問題。下面是一個(gè)快速排查指南。問題現(xiàn)象可能原因檢查與解決思路找到地址并修改但游戲無反應(yīng)或很快恢復(fù)。1. 地址是動(dòng)態(tài)的每次啟動(dòng)變化。2. 修改的是客戶端顯示值服務(wù)器有校驗(yàn)。3. 數(shù)據(jù)有多層指針。1. 尋找“指針”。用 Cheat Engine 的“找出是什么訪問了這個(gè)地址”找到基地址和偏移。2. 對于網(wǎng)游修改本地內(nèi)存通常無效需尋找發(fā)包函數(shù)修改發(fā)送給服務(wù)器的數(shù)據(jù)。3. 可能修改了錯(cuò)誤的地址用更精確的掃描方式浮點(diǎn)、全部重新掃描。修改后游戲崩潰。1. 修改了代碼段破壞了指令完整性。2. 修改了非對齊的數(shù)據(jù)或錯(cuò)誤的數(shù)據(jù)類型。3. 觸發(fā)了反作弊檢測。1. 確保只修改數(shù)據(jù)段或精心修改代碼段保持指令長度。2. 確認(rèn)數(shù)據(jù)類型4字節(jié)整型、4字節(jié)浮點(diǎn)、8字節(jié)雙精度。浮點(diǎn)數(shù)修改要保證仍是合法浮點(diǎn)值。3. 在修改前暫停游戲線程或使用更隱蔽的寫入方式。無法在匯編中找到直觀的算術(shù)指令。1. 編譯器優(yōu)化如常量傳播、循環(huán)展開。2. 運(yùn)算被內(nèi)聯(lián)到其他復(fù)雜函數(shù)中。3. 使用了向量化指令如 SIMD。1. 使用未優(yōu)化的 Debug 版本進(jìn)行分析理解邏輯后再看 Release 版本。2. 在反編譯器IDA F5, Ghidra中查看高級偽代碼理清邏輯。3. 學(xué)習(xí)基本的 SSE/AVX 指令識(shí)別PADDD,PMULLD,ADDPS等并行運(yùn)算指令。Cheat Engine 掃描不到數(shù)值。1. 數(shù)值類型選錯(cuò)如用整型搜浮點(diǎn)。2. 數(shù)值被加密或編碼。3. 數(shù)值不在主模塊在其他 DLL 中。1. 嘗試所有掃描類型字節(jié)、2字節(jié)、4字節(jié)、8字節(jié)、浮點(diǎn)、雙浮點(diǎn)、字符串。2. 使用“未知初始值”掃描通過數(shù)值變大/變小來過濾。3. 在 Cheat Engine 中勾選“掃描時(shí)包括保留區(qū)域”并確認(rèn)掃描范圍包含了所有加載的模塊。6. 最佳實(shí)踐與安全建議逆向工程是一門需要謹(jǐn)慎和不斷學(xué)習(xí)的技能。僅在合法環(huán)境練習(xí)所有練習(xí)必須在你自己擁有完全控制權(quán)的程序如自己編寫的 demo、明確授權(quán)用于安全研究的 CTF 題目、舊版單機(jī)游戲上進(jìn)行。未經(jīng)授權(quán)對任何在線游戲、商業(yè)軟件進(jìn)行逆向和修改不僅是違反用戶協(xié)議的行為也可能涉及法律風(fēng)險(xiǎn)。理解優(yōu)先于修改不要滿足于找到“神奇”的地址和代碼?;〞r(shí)間理解整個(gè)計(jì)算流程、數(shù)據(jù)結(jié)構(gòu)和類關(guān)系。這能讓你應(yīng)對更復(fù)雜的保護(hù)措施。從簡單到復(fù)雜先從簡單的、無保護(hù)的 C 控制臺(tái)程序開始練習(xí)定位和修改int、float。然后嘗試有簡單混淆的程序最后再研究帶有反調(diào)試、代碼虛擬化的目標(biāo)。做好筆記使用 IDA 的注釋功能、x64dbg 的標(biāo)簽功能或單獨(dú)的文檔記錄下關(guān)鍵函數(shù)的地址、變量的含義、結(jié)構(gòu)體的偏移。逆向是一個(gè)拼圖過程筆記至關(guān)重要。關(guān)注底層原理深入學(xué)習(xí) x86/x64 匯編語言、Windows PE 文件結(jié)構(gòu)、內(nèi)存管理機(jī)制。這些底層知識(shí)是理解一切高級逆向技巧的基礎(chǔ)。利用社區(qū)資源逆向工程社區(qū)有很多開源工具、教程和論壇。遇到問題時(shí)善于搜索和查閱文檔。算術(shù)運(yùn)算的逆向是游戲外掛分析與防御中最基礎(chǔ)、最核心的一環(huán)。它連接著高級語言邏輯與底層機(jī)器狀態(tài)。掌握它意味著你能夠透過內(nèi)存中冰冷的字節(jié)理解并影響程序運(yùn)行的邏輯。接下來的學(xué)習(xí)方向可以是指針與數(shù)據(jù)結(jié)構(gòu)的逆向、函數(shù)調(diào)用約定的分析、以及網(wǎng)絡(luò)協(xié)議的逆向這些技能將共同構(gòu)成你分析和理解復(fù)雜軟件系統(tǒng)的能力圖譜。