# CE指針基礎學習紀錄 :::info 學習、參考來源 [你能学会的Cheat Engine零基础入门教程 -- 影片](https://www.bilibili.com/video/BV1nR4y1u7PZ/) https://steamcommunity.com/sharedfiles/filedetails/?id=2823180512 https://home.gamer.com.tw/creationDetail.php?sn=5243370 https://forum.gamer.com.tw/C.php?bsn=2417&snA=26363 ::: ## CE數據類型與暫存器代號 #### 數據類型 |类型 | 占用空间 | | -------- | -------- | |Bit(整型) | 1 位| |Byte(整型)|8 位(字节)| |2 byte(整型)| WORD(字)| |4 Bytes(整型)| DWORD(双字)| |8 Bytes(整型)| QWORD(四字)| |Float(单浮点) |DWORD(双字)| |Double(双浮点)| QWORD(四字)| |String(字符串) |任意长度| |Array of bytes(AOB) |任意长度| #### 32位元記憶體位置代號表 32位元寄存器分為8種,分別為以下 EAX、EBX、ECX、EDX、ESI - EDI、ESP、EBP |暫存器名稱 | 功能 | | -------- | -------- | |EAX|累加器(accumulator), 它是許多加法乘法指令的預設暫存器| |EBX|基底位址(base)暫存器, 在記憶體尋址時存放基底位址| |ECX|計數器(counter), 是重複(REP)前綴指令和LOOP指令的內定計數器 |EDX|總是被用來放整數除法所產生的餘數| |ESI/EDI|來源/目標索引暫存器(source/destination index),因為在許多字串操作指令中, DS:ESI指向來源字串,而ES:EDI指向目標字串| |EBP|基址指標(BASE POINTER), 它最常被用作高階語言函式呼叫的"框架指標"(frame pointer)| |ESP|專門用作堆疊指針,被形象稱為棧頂指針,堆疊的頂部是位址小的區域,壓入堆疊的資料越多,ESP也就越來越小。在32位元平台上,ESP每次減少4位元組| >https://blog.csdn.net/gettogetto/article/details/76793553 #### 64位元記憶體位置代號表 64位元寄存器則分為16種,相比32位元多出8種,分別為以下 RAX、RBX、RCX、RDX、RSI - RDI、RSP、RBP、r8、r9、r10、r11、r12、r13、r14、r15 功能與32位元相同,多出的8個值為通用值 |暫存器名稱|全稱|功能| | -------- | -------- | ---------| |rax|累加器(Accumulator)|返回值| |rcx|计数器(Count Register) |第二个参数| |rdx|数据寄存器(Data Register) |第三个参数| |rbx|基址寄存器(Base Register) |被调用者保存| |rsp|堆栈指针寄存器(Stack Pointer)|栈顶指针| |rbp|基址指针寄存器(Base Pointer)|被调用者保存| |rsi|源变址寄存器(Source Index)|第二个参数| |rdi|目的变址寄存器(Destination Index)|第一个参数| |r8|通用|第五个参数| |r9|通用|第六个参数| |r10|通用|调用者保存(簡單說就是使用之前要先儲存原值)| |r11|通用|调用者保存| |r12|通用|被调用者保存(簡單說就是隨便用,呼叫子函數之前要備份它,以防他被修改)| |r13|通用|被调用者保存| |r14|通用|被调用者保存| |r15|通用|被调用者保存| >https://juejin.cn/post/7026387609706299405 ## 記憶體布局相關知識 記憶體是從下往上塞的,高處表示地址數值大(用於儲存heap、stack[堆、棧]),低處表示地址更小(通常用於儲存指令等固定大小的值) 從圖上看指令會儲存在最低處,因為他不會去做變動,所以重新載入程式時指令都會加載到固定的位置 ![image](https://hackmd.io/_uploads/BkMWzfpRA.png) 再往上看兩段是全局變數,保存的是數據(EX.血量等基礎數值),位置同指令會是固定的 ![image](https://hackmd.io/_uploads/H1_mmG6R0.png) 再往上是heap(堆),裡面的數據是各種不完整的碎片,在遊戲執行時去根據真實需求分配的,用完之後要進行回收(就是釋放記憶體位置),所以heap內數據沒有固定位置,每次執行都有可能會不同 ![image](https://hackmd.io/_uploads/rJdcQGp0R.png) 最上面是stack(棧),連續儲存的數據,但起始位子一般是隨機的(雖然是出於安全考量,但確實導致每次執行存放位置都不一樣) ![image](https://hackmd.io/_uploads/HJ8S4zaRA.png) 所以未追到基地址,平常在做修改的時候所動到的任何數值都是儲存在會變化的地方,重開就沒了(因為位置被free掉了) ![image](https://hackmd.io/_uploads/Syih4M6CR.png) ## 指針追溯 >但其實現在很多新遊戲不好追指針 這邊用教學6、7來練手及熟悉操作, ### 教學6 - 基指指針 抓取數值>查看是甚麼訪問了這個地址 這邊有4個選項,根據組語的暫存器數值傳遞順序,是由右到左,選第2條就是因為eax把值給上一層指針[rdx] ![image](https://hackmd.io/_uploads/SyQoozpAR.png) 點開詳細訊息,會顯示CE推測的rdx位置>01609470 ![image](https://hackmd.io/_uploads/HypQpfaAA.png) >有時候會牽扯到偏移量,像這種後面沒有+-XX的就代表偏移是0 CE中綠色的就代表基址 ![image](https://hackmd.io/_uploads/HkLkAM6R0.png) 點擊地址欄位就可以更改地址,勾選指針可以設定他的偏移量,這邊是偏移0 ![image](https://hackmd.io/_uploads/rkYvAfpCA.png) 這邊他顯示0000000A 是因為他是16進制顯示,右鍵改成10進制顯示就是看得懂的數值了 ![image](https://hackmd.io/_uploads/B1pTAfa0A.png) 更改後 ![image](https://hackmd.io/_uploads/B1lfyX6CR.png) ### 教學8 -- 多級指針 用相同的方式先找到一級指針[rsi+18]>01658F20 ![image](https://hackmd.io/_uploads/S1LibXpRR.png) >這邊偏移量是18 在用相同的方式找出是甚麼訪問這個地址,找到2級指針>016AB0A0 cmp qword ptr [rsi],00 意思是00跟qword ptr [rsi] (rsi裡面的值),做比較 ![image](https://hackmd.io/_uploads/ryBOfma0C.png) >第二條的話只是把 記憶體中的[rsi]值放到CPU中的rsi暫存區準備做計算,他的指針數值也跟一級指針一樣,所以不是要找的目標 >![image](https://hackmd.io/_uploads/rkFwQXp0A.png) 確定好下一級指針後繼續往下追溯三級指針>01671350 ![image](https://hackmd.io/_uploads/HJ94N76A0.png) >這邊注意他有偏移量18 同上,四級指針>01658CA0 ![image](https://hackmd.io/_uploads/H1xqEXaCC.png) >這邊注意他偏移量是10 這樣就可以追到他的基址 ![image](https://hackmd.io/_uploads/rJKCN760C.png) 從基址地址添加上他的偏移量(從四級指針往上填到一級指針,可以去對指針數值確認有沒有問題) ![image](https://hackmd.io/_uploads/SJ8-rQT0R.png) 這邊將基指換成顯示10進制,可以看到他跟一開始抓到的動態地址相同 ![image](https://hackmd.io/_uploads/BJ8UrQaCR.png) #### 自動指針追溯 找到血量地址後選擇自動指針掃描,教學裡面全部預設就好 ![image](https://hackmd.io/_uploads/rJkLIXpRR.png) >如果是64位元的遊戲,要打開高級選項把[必須是32位元]取消勾選,然後最大級別盡量拉到8-10以上 >但如果是大型遊戲很少會讓他掃,因為太吃電腦效能了 >![image](https://hackmd.io/_uploads/HyFuUX6A0.png) 掃描完成後存檔,然後視窗不要關掉,重開或重進遊戲再找一次血量地址,把新地址複製下來讓指針掃描器重新掃描一次 ![image](https://hackmd.io/_uploads/B1TUDQaAR.png) >通常大型遊戲要多篩幾次結果才會越來越少 這邊指針路徑還有1萬多個,但要撇除除了我們搜索的程式外的其他dll之類的程式,可以看到這邊很多條都指向到同一個指針,隨便挑一條用就好了 ![image](https://hackmd.io/_uploads/SJRz_7p0A.png) ## 指令修改 >現代遊戲的主流修改方式,直接從最底層指令去修改可以一勞永逸的使用,不用擔心指針跟變量問題,但可能在遊戲更新後新增了指令之類的造成地址跑掉 ### 基礎知識 指令最開始的部分通常是一個單詞,表示這邊在做甚麼操作 ![image](https://hackmd.io/_uploads/r1h7cX60C.png) 後面的就是暫存器中的數據 ![image](https://hackmd.io/_uploads/Bkaq97aR0.png) 結果去向寫在前面,最多只有一個 ![image](https://hackmd.io/_uploads/Sy1R9mpRC.png) 後面是數據來源,可能會有多個來源,都是寫在最後 ![image](https://hackmd.io/_uploads/rJJgjQpCC.png) 寄存器就是存在CPU內做運算的東西,但CPU內的能運算量很少,所以會從訪存(訪問內存aka記憶體)內取得儲存過去的數值來做運算 在x86中為了保持運算效能,一條指令中最多只會有一個訪存,其他都只能是CPU內部的暫存器 ![image](https://hackmd.io/_uploads/B1Og2maA0.png) []內就是CPU丟過去記憶體所儲存的數據,所以CPU需要的時候會從這裡去要數據 ![image](https://hackmd.io/_uploads/Hy3OnmTR0.png) ### 相關組語(匯編)知識及常用語句 在所有組語的暫存器中,數據都是從右給到左邊 | 命令例子 | 功能 | | -------- | -------- | | mov ebx,0000FFFF | Move,暫存器直接赋值 | | mov ebx,eax | Move,将右边直接给左边 | | mov ebx,[eax] | Move,将右边所指的值给左边。[]代表括号内的是指针(指針存在於記憶體內),操作时,操作其指向的内存值 | | cmp ebx,eax| Compare,比较两寄存器值,若相等则 ZF位 置1(左减右)| | je `<label>` | Jump if Equal, ZF = 1 时跳转,可跳转至标记段代码 | | jne `<label>` | Jump if Not Equal, ZF = 0 时跳转,可跳转至标记段代码 | | jmp `<label>` | Jump | | PUSHFD| Push eFLAGS Register onto the Stack | | Nop | 空指令,啥都不做 | | sub dword ptr [rsi+000007E0],01 | sub是減法、dword ptr [rsi+000007E0]是取記憶體位置中的值、01是減去1 | | add/addss | 加法、通常跟sub替換用於加減,addss為浮點數運算 | | [xmm] | 暫存器,用於儲存單/多精度浮點數 | ### 指令編碼 從CE的反編譯中可以看到[字節]部分有一堆編碼,每一行長度不一樣 ![image](https://hackmd.io/_uploads/SJQOCXTAA.png) 表示做甚麼操作的編碼一般都在最開頭,長度是1-3字節,後面都是數據結果與去向 ![image](https://hackmd.io/_uploads/rkVTRXpR0.png) >Ex. 89 46 18 中的 [89]就是操作碼mov,後面就是結果與去向的運算 要編寫腳本的話可以選擇工具>自動匯編 ![image](https://hackmd.io/_uploads/BkpPfEpRC.png) 模板>全部注入>選中想要的指令 ![image](https://hackmd.io/_uploads/Bk9pMVaR0.png) 這邊會自動產生模板 ![image](https://hackmd.io/_uploads/SJpbQEaAR.png) ### AoB AoB解釋:(Array Of Byte)在内存中搜索特定的一串数据,以决定注入代码的位置 簡單來說就是遊戲可能每次更新後會加入新的指令造成指令地址偏移,透過AoB的話,可以將指令一部分內容當成數據讓內存掃描器去做尋找 ![image](https://hackmd.io/_uploads/SJInXV60R.png) 比如抓這一段的指令>83 AE E0 07 00 00 01 ![image](https://hackmd.io/_uploads/HyHDNVa0C.png) 將數值類型設定為字節數組(AoB),將那行指令的字節貼上,記得取消勾選可寫,因為指令是不可寫的 ![image](https://hackmd.io/_uploads/B1ozBEpRA.png) 確認是可以搜尋到相對的指令區塊 ![image](https://hackmd.io/_uploads/H1tuB4aAC.png) 但在版本更新後他的地址極有可能會跑掉,所以要換一種搜尋方式,輸入下文讓他去抓那一段的指令 這邊的話我們得知前1-3碼是操作碼,所以會是固定的,後面因為是暫存器所以每次開啟程式結果都會不一樣,這邊可以用`x`來當作通配符使用 Ex:原代碼`83 AE E0 07 00 00 01` 這邊得知 `83`=`sub` 後面`AE E0 07 00 00 01`為暫存運算,所以替換後會變成`83xxxxxxxxxxxx` 一開始在篩選時會比較多結果,因為很多地方都有用到相同的指令,所以這邊輸入更多下文來讓他找到正確的位置 >這邊輸入`83xxxxxxxxxxxx48xxxxxxE8xxxxxxxx8Bxxxxxxxxxx41xxxxxxxxxx`來找到確切指令位置 >分別對應以下這段 >![image](https://hackmd.io/_uploads/rJP5vVaAC.png) ![image](https://hackmd.io/_uploads/SJYwI460C.png) 這邊的話可以回到反編譯sub指令部分>自動匯編>模板>AoB注入 ![image](https://hackmd.io/_uploads/Hynp_VpRA.png) 把他原本地址區塊改成可用的AoB,這樣就可以隨時調用了 ![image](https://hackmd.io/_uploads/ryxlt4TC0.png) ### 腳本編寫 ![image](https://hackmd.io/_uploads/rkWoc4TCC.png) 簡單來說運作原理就是設個斷點讓原程式跳去執行我們在記憶體中設立newmen的位置,執行完再回來原程式 ![image](https://hackmd.io/_uploads/r16A546CR.png) ![image](https://hackmd.io/_uploads/By5_jNp0R.png) ### CE自動匯編函數意義 |函数 |参数 |作用| |--------|-------|-------| |alloc |alloc(SymbolName, Size, AllocateNearThisAddress OPTIONAL) |分配一個Size位元組的記憶體區塊,並在腳本中定義SymbolName,指向所分配記憶體區塊的開頭| |dealloc|dealloc(SymbolName) |釋放使用 alloc 分配的記憶體區塊。| |label |label(LabelName)| 允許將“LabelName”一詞用作符號。| |aobScanModule |aobScanModule(SymbolName, ModuleName, AOBString) |掃描模組 ModuleName 使用的記憶體以尋找 AOBString 定義的特定位元組模式,並將結果位址設為符號 SymbolName。| |registerSymbol| registerSymbol(SymbolName) |將符號新增到使用者定義的符號清單中,以便作弊表和記憶體瀏覽器可以使用該名稱而不是位址。| |unregisterSymbol| unregisterSymbol(SymbolName)| 從使用者定義的符號清單中刪除符號。如果該符號不存在,則不會發生錯誤。| | dd | dd (int) 0 | dd是給數據分配空間,(int)是數據類型,最後是數值 | ### 取消傷害的腳本 ``` { Game : Tutorial-x86_64 Version: Date : 2024-10-04 Author : user This script does blah blah blah } // 遊戲敘述巴拉巴拉 [ENABLE] //CE中的激活,編寫時只需要關注這裡 aobscanmodule(INJECT,Tutorial-x86_64.exe,83xxxxxxxxxxxx48xxxxxxE8xxxxxxxx8Bxxxxxxxxxx41xxxxxxxxxx) // should be unique (指令位置) alloc(newmem,$1000,INJECT) // 預設會有,就是調用一部分記憶體給這段腳本使用 label(code) label(return) label(damage) // 我自己新增的標籤 registerSymbol(damage) // 幫damage標籤註冊記憶體區塊,全域符號 newmem: //透過alloc調用得到的,就是分配記憶體給他,所有改動盡量在newmem內執行 code: //code標籤,原代碼 sub dword ptr [rsi+000007E0],[damage] //[damage]是label,整段指令意思是傷害改為0 jmp return damage: //剛剛上面的自定義標籤 dd (int) 0 //dd是給數據分配空間,(int)是數據類型,最後是數值 INJECT: //與絕對地址綁定 jmp newmem nop 2 return: registersymbol(INJECT) [DISABLE] // CE中的未激活,就是讓指令維持原樣 INJECT: db 83 AE E0 07 00 00 01 unregistersymbol(INJECT) dealloc(newmem) { // ORIGINAL CODE - INJECTION POINT: Tutorial-x86_64.exe+2DB57 Tutorial-x86_64.exe+2DB20: 55 - push rbp Tutorial-x86_64.exe+2DB21: 48 89 E5 - mov rbp,rsp Tutorial-x86_64.exe+2DB24: 48 8D A4 24 C0 FD FF FF - lea rsp,[rsp-00000240] Tutorial-x86_64.exe+2DB2C: 48 89 9D E0 FD FF FF - mov [rbp-00000220],rbx Tutorial-x86_64.exe+2DB33: 48 89 B5 E8 FD FF FF - mov [rbp-00000218],rsi Tutorial-x86_64.exe+2DB3A: 48 89 CE - mov rsi,rcx Tutorial-x86_64.exe+2DB3D: 48 C7 85 F0 FE FF FF 00 00 00 00 - mov qword ptr [rbp-00000110],00000000 Tutorial-x86_64.exe+2DB48: 48 C7 45 F8 00 00 00 00 - mov qword ptr [rbp-08],00000000 Tutorial-x86_64.exe+2DB50: 90 - nop Tutorial-x86_64.exe+2DB51: 8B 9E E0 07 00 00 - mov ebx,[rsi+000007E0] // ---------- INJECTING HERE ---------- Tutorial-x86_64.exe+2DB57: 83 AE E0 07 00 00 01 - sub dword ptr [rsi+000007E0],01 // ---------- DONE INJECTING ---------- Tutorial-x86_64.exe+2DB5E: 48 8D 4D F8 - lea rcx,[rbp-08] Tutorial-x86_64.exe+2DB62: E8 A9 B3 FD FF - call Tutorial-x86_64.exe+8F10 Tutorial-x86_64.exe+2DB67: 8B 8E E0 07 00 00 - mov ecx,[rsi+000007E0] Tutorial-x86_64.exe+2DB6D: 41 B9 FF 00 00 00 - mov r9d,000000FF Tutorial-x86_64.exe+2DB73: 4C 8D 85 F8 FE FF FF - lea r8,[rbp-00000108] Tutorial-x86_64.exe+2DB7A: 48 C7 C2 FF FF FF FF - mov rdx,FFFFFFFFFFFFFFFF Tutorial-x86_64.exe+2DB81: 48 63 C9 - movsxd rcx,ecx Tutorial-x86_64.exe+2DB84: E8 07 85 FD FF - call Tutorial-x86_64.exe+6090 Tutorial-x86_64.exe+2DB89: 45 31 C0 - xor r8d,r8d Tutorial-x86_64.exe+2DB8C: 48 8D 95 F8 FE FF FF - lea rdx,[rbp-00000108] } ``` 另一種寫法是調用暫存器位置 EX 在這邊可以看到rcx接下來有被覆蓋,然後上面的rcx值有給到rsi,所以在這一段中rcx是可以任意調用的 ![image](https://hackmd.io/_uploads/r1aD0EpRR.png) ``` code: mov rec,[damage] // 將damage的值移到rcx內 sub dword ptr [rsi+000007E0],rcx //調用rex的值 也就是0 jmp return damage: dd (int) 0 ```