本帖最後由 xxx9638527410 於 2019-3-30 19:53 編輯
作者:Ganlv
不確定能否附上網址,暫且不提供。
------------------------------------------
最近看見一篇關於CE最新版遊戲的資訊。
個人覺得滿有趣,所以將文章轉載過來。
除此之外,應該有不少人對此感興趣。
所以,至少來說,讓這篇文章沉澱於此。
供未來的那些人,路過時可以閱讀。
--------------------------------------------------
CE 教程:進階篇CE Tutorial Games本文大約1 萬字,閱讀可能需要很長時間。本文包含大量圖片,全文加載可能需要10MB 以上的流量。 注意事項請注意:本文仍有可能更新,全文轉載的話可能需要考慮更新的問題。 我用的是本文起草時(2019年3月26日)的最新版Cheat Engine 6.8.3(在2019年2月9日發布的) 緒言2018 年6 月8 日,Cheat Engine 6.8 發布,軟件中新增了一個Cheat Engine Tutorial Games。這個新的小遊戲有3 關,並不是特別難修改,但是卻很有意思。因為它不再是之前的教學程序那種技能教學,而是一種有目的實戰教學。 每一關目標只有一個,但辦法是多種多樣的。這篇文章盡可能利用不同方法來解決問題。重點不是修改這個小遊戲,重點是理解其中的思路。 教程打開CE Tutorial Games菜單欄→ Help → Cheat Engine Tutorial Games
第1 關
第1 關:每5 次射擊你必須重新裝填,在這個過程中目標會回復血量,嘗試找到一種方法消滅目標。 [Tips] 遊戲中使用空格鍵射擊。 第1 關嘗試 1有數字的時候肯定先嘗試搜索數字,畢竟這個是最方便快捷、最直觀準確的方式。 右下角有個數字5,新掃描,搜索4字節的精確數值5。 射一發之後再搜索4。 不知道你們搜索到沒有,反正我是沒有。 第1 關嘗試 2我懷疑上一種方法有缺陷,可能子彈沒撞到目標和撞到目標時,遊戲的數據是兩種狀態。 我勾選了Pause the game while scanning。
在子彈射出過程中搜索4,結果發現還是不行。 第1 關嘗試 3有時候遊戲中顯示的數字並不是內存中實際存儲的數據,你看到的只是計算結果。 [Info] 如果你學習過簡單的編程知識,你應該了解堆內存和棧內存的區別 堆內存(這裡的堆Heap與數據結構的堆Heap完全無關,這只是一種名稱)通常是使用malloc函數分配的,一旦分配完僅用來存儲某個確定結構、數組、對象,通常直到使用free釋放之前都代表同一遊戲數據。堆內存在傳遞的時候只會傳遞指針。 棧內存,棧內存是不斷復用的,棧內存通常用作函數的局部變量、參數、返回值,函數調用時一層一層嵌套的,調用一個函數,棧就會增長一塊,一個函數調用完返回了,棧就會縮短,下一個函數再調用,這塊內存就會被重新使用。所以棧內存是會快速變化的,搜索到棧內存通常都沒有什麼意義。你應該聽過ESP 和EBP,SP 就是Stack Pointer,棧指針,描述棧頂在什麼內存位置的寄存器。
我猜測他顯示5的時候其實內存中是已發射0顆子彈。5只是一個局部變量的計算結果,他在棧中只存在很短的一段時間,搜索是搜索不到的。 所以顯示5的時候搜索0,顯示4的時候搜索1。
[Comment] 這個地址是在運行之後分配的,你的搜索結果可能和我不一樣
把搜索結果添加到下方地址列表中,點擊左側的小方塊鎖定,這樣就可以了。 [Tips] 這個遊戲中顯示的是5而存儲的是0。類似的,某些遊戲的貨幣可能都是10的倍數,比如500金幣,內存中存儲的可能就是50,而不是顯示的500。例如:植物大戰殭屍。
第1 關嘗試 4你以為這樣就結束了嗎? 這個遊戲真的很有意思。你的最終目的是要打敗敵人,那如果我直接把敵人就設置為1 滴血,會怎麼樣呢? 血條沒有具體數值,我們使用未知初始值(Unknown initial value)來搜索。 類型怎麼選呢?血量這種東西一般我會先試試Float(單精度浮點型),然後再試試4 字節整數,如果不行的話再試試8 字節和雙精度浮點型,再不行的話就方案吧。
這裡搜索了好幾次還剩幾個結果,憑感覺應該是第一個,因為敵人回血回到滿的時候,第一個數值恰好是100。 直接把敵人血量改為1,然後發射子彈。 一發入魂。 第1 關嘗試 5找到剛才那兩個內存地址之後,我們還可以嘗試代碼注入,但是由於第1 關是在太簡單了,沒有必要這樣大費周章請來代碼注入這種複雜的東西,內存修改搞定就行。代碼注入的應用在下一關會提到。 第2 關
第2 關:這兩個敵人和你相比擁有更多的血量、造成更多的傷害。消滅他們。提示/警告:敵人和玩家是相關聯的。 [Tips] 遊戲中使用左右方向鍵控制旋轉,使用上方向鍵控制前進,使用空格鍵射擊。 第2 關嘗試 1敵人兩個人一起打我們,每次要掉4 滴血,我們總共才100 滴血,而我們打敵人,每次大概就掉1/100,而且對面還有兩個人。 這誰頂得住啊!
我們直接搜自己的血量,把血量改成上千,然後激情對射。 來呀,互相傷害啊!
然後... 第2 關Plus
第2 關加強:你將會為之付出代價!啟動究極炸彈。3、2、1。
啊!我死了。
9199血的我被炸到-1滴血。 第2 關嘗試 2怎麼辦呢? 我們發現,我們的子彈飛行速度比較快,我可以先把兩個人都打到只剩1 滴血,然後殺掉其中一個,另一個會啟動究極炸彈,這時我只需要一發小子彈就能把對面打死。
然而。
我太天真了。 對面另外一個雖然也殘血,但是啟動究極炸彈的時候能回血。 第2 關嘗試 3肯定有人覺得麻煩了,你直接搜索敵人血量改成1 不就得了,對面就算回血,就再改成1。
果然,他又回到了21滴血,我再改成1滴血,然後開火。
誰讓他的炸彈飛的慢呢~ 第2 關嘗試 4這麼贏得好像比較不保險,萬一遊戲作者把敵人導彈的速度調的比我們子彈快,那不就完蛋了。 我們來從根本上解決問題。 找出修改我們自己血量的指令,Find out what writes to this address。
然後把這個語句替換成NOP (No operation),原來修改血量的代碼就會變成什麼也不做。
再與敵人打幾個回合,發現,我們不掉血了,但敵人也不掉血了。 Tip/Warning: Enemy and player are related 提示/警告:敵人和玩家是相關聯的。
這就是“共用代碼”(Shared Code),敵人和我們減血的代碼是共用的,不能簡單地修改為NOP,我們需要做一些判斷。 第2 關嘗試 5我們先把指令還原。
你可以分別對這三個地址使用Find out what writes to this address,看看什麼指令寫入了這個地址,你應該會有所發現。
你可以看到,分別向這三個地址寫的指令是同一條指令(指令地址相同,就是圖中的10003F6A3)。 既然這幾個血量的修改是通過相同的代碼,那麼就表示玩家的數據存儲方式和敵人的數據存儲方式是相同的,至少血量都是在+60的位置存儲。 我們的想法是:從儲存玩家和敵人信息的結構體中找出一些差別,然後靠代碼注入構造一個判斷,如果是玩家自己的話則跳過,不扣血。 這裡使用Dissect data/structures。
裡面默認已經有一個地址了,我們再額外添加兩個地址。
然後填入三個血量地址 - 60,要注意這裡需要減掉60,因為+60之後的是血量地址,把這個60減掉才是結構體的開頭。
因為兩個同類的結構肯定不能重疊,所以這裡我可以算一下兩個結構體的距離,一個結構體最大隻有160 字節,再大就會重疊了。 [Tips] 通常情況下,兩個結構體會相距比較遠,你可以適當設置這個數值,比如設置一個1024甚至4096字節之類的,反正你覺得應該足夠就行。
我們的邏輯就是 - if (*(p + 70) == 0) { // 0 表示是玩家自己
- // 什么也不干
- } else {
- // 正常扣血
- }
複製代碼Find out what writes to this address→ Show disassembler→ Tools→ Auto Assemble→ Template→Code injection
這是自動生成的代碼 - alloc(newmem,2048,"gtutorial-x86_64.exe"+3F6A3)
- label(returnhere)
- label(originalcode)
- label(exit)
- newmem: //this is allocated memory, you have read,write,execute access
- //place your code here
- originalcode:
- sub [rax+60],edx
- ret
- add [rax],al
- exit:
- jmp returnhere
- "gtutorial-x86_64.exe"+3F6A03:
- jmp newmem
- nop
- returnhere:
複製代碼代碼注入的原理就是把原來那個位置的指令換成jmp,跳轉到我們新申請的一塊內存中,程序正常運行到這裡就會跳轉到我們新申請的那塊內存中,然後執行我們的指令,我們自己寫的指令的最後一條指令是跳轉回原來的位置,這樣程序中間就會多執行一段我們的指令了。 不過這裡有一點問題, originalcode:
sub [rax+60],edx
ret
add [rax],alret 語句之後是另外一個函數了,我們這樣修改的話,如果有人調用那個函數就會出錯,我們把注入點往前挪一下。 - gtutorial-x86_64.exe+3F6A0 - 48 89 C8 - mov rax,rcx
- gtutorial-x86_64.exe+3F6A3 - 29 50 60 - sub [rax+60],edx
- gtutorial-x86_64.exe+3F6A6 - C3 - ret
- gtutorial-x86_64.exe+3F6A7 - 00 00 - add [rax],al
複製代碼重新生成一個Code injection,注入點設置為上一條語句gtutorial-x86_64.exe+3F6A0。 - alloc(newmem,2048,gtutorial-x86_64.exe+3F6A0)
- label(returnhere)
- label(originalcode)
- label(exit)
- newmem: //this is allocated memory, you have read,write,execute access
- //place your code here
- originalcode:
- mov rax,rcx
- sub [rax+60],edx
- exit:
- jmp returnhere
- gtutorial-x86_64.exe+3F6A0:
- jmp newmem
- nop
- returnhere:
複製代碼其他部分不用動,我們直接在originalcode上修改 originalcode:
mov rax,rcx
cmp [rax+70],0
je exit // 如果等于 0,则表示玩家,跳到 exit,不执行下一条 sub 语句
sub [rax+60],edx
exit:
[Tips] 雙斜線後面是註釋,刪除掉也可以。
然後點擊 Execute 我們可以簡單修改一下,然後File→Assign to current cheat table - [ENABLE]
- alloc(newmem,2048,gtutorial-x86_64.exe+3F6A0)
- label(returnhere)
- label(originalcode)
- label(exit)
- newmem:
- originalcode:
- mov rax,rcx
- cmp [rax+70],0
- je exit
- sub [rax+60],edx
- exit:
- jmp returnhere
- gtutorial-x86_64.exe+3F6A0:
- jmp newmem
- nop
- returnhere:
- [DISABLE]
- gtutorial-x86_64.exe+3F6A0:
- mov rax,rcx
- sub [rax+60],edx
複製代碼 第2 關嘗試 6剛才的代碼改的還不夠好,我們可以像敵人的究極炸彈打我們一樣,將敵人一擊致命。 originalcode 部分修改成 - originalcode:
- mov rax,rcx
- cmp [rax+70],0
- je exit
- mov edx,[rax+60]
- sub [rax+60],edx
複製代碼直接令edx等於敵人血量,然後敵人血量會被扣掉edx,這樣敵人直接就被秒了。 第2 關嘗試 7不,第二關還沒有結束,我們的還可以繼續深入研究下去。 “扣血函數”是同一個的函數,但是調用“扣血函數”的地方肯定是不一樣的。 我們可以找到調用他的位置。 我們在sub [rax+60],edx處下斷點 - gtutorial-x86_64.exe+3F6A0 - 48 89 C8 - mov rax,rcx
- gtutorial-x86_64.exe+3F6A3 - 29 50 60 - sub [rax+60],edx
- gtutorial-x86_64.exe+3F6A6 - C3 - ret
複製代碼發射一發子彈,等待他命中敵人或命中自己,斷點會觸發。 這個斷點應該會觸發3次,每次你需要觀察一下右側寄存器窗口中的rax的數值來判斷這個代表扣誰的血。
然後跟著ret返回到他調用的位置,上一條語句一定是call。 玩家扣血前後的代碼 - gtutorial-x86_64.exe+3DFB8 - E8 E3160000 - call gtutorial-x86_64.exe+3F6A0
- gtutorial-x86_64.exe+3DFBD - 48 8B 4B 28 - mov rcx,[rbx+28]
複製代碼單步執行返回之後指針停留在gtutorial-x86_64.exe+3DFBD,前一條一定是一個call指令,就是call gtutorial-x86_64.exe+3F6A0。 gtutorial-x86_64.exe+3F6A0 這個地址就是之前那個扣血函數。 gtutorial-x86_64.exe+3F6A0 - 48 89 C8 - mov rax,rcx
gtutorial-x86_64.exe+3F6A3 - 29 50 60 - sub [rax+60],edx
gtutorial-x86_64.exe+3F6A6 - C3 - ret同理可以知道敵人被打中時扣血的代碼 左側敵人扣血代碼 gtutorial-x86_64.exe+3E0ED - E8 AE150000 - call gtutorial-x86_64.exe+3F6A0右側敵人扣血代碼 gtutorial-x86_64.exe+3E1D6 - E8 C5140000 - call gtutorial-x86_64.exe+3F6A0這個遊戲很有意思,命中敵人和命中玩家使用的是不同的代碼,僅僅扣血使用的是相同的代碼。 [Comment] 這就是傳說中的面向複製粘貼型編程。
既然他們使用的是不同的代碼,這個fastcall由沒有影響棧平衡,那麼我可以直接把gtutorial-x86_64.exe+3DFB8這一行用NOP替換掉。
[Tips] ret語句的作用是返回調用處,call的時候會往棧頂壓一個返回之後應該執行的地址。 簡單一個ret語句就是跳回棧頂那個地址的位置,然後再把棧頂那個地址彈出 也有ret 8這樣的語句,就是先從棧中彈出8個字節(相當於add esp,8),然後再執行返回。之所以這樣所是因為調用這個函數之前,往棧中壓入了8個字節的參數(比如兩個4字節整數),函數返回之前必須恢復棧平衡。 gtutorial-x86_64.exe+3F6A6這個ret語句,後面沒有參數,應該不會影響棧平衡。
現在也可以讓敵人打我們不掉血,我們打敵人正常掉血了。 第2 關嘗試 8現在來分析一下gtutorial-x86_64.exe+3F6A0這個扣血函數,這個函數總共就3條指令,函數有2個參數,分別是rcx和edx,rcx為結構體的指針,edx為扣血的數量,沒有返回值。 [Info] 這種用寄存器傳遞參數來調用函數方法是典型的fastcall。
我們需要分析一下call之前是什麼確定了edx的值。 以玩家扣血為例,我們需要看call之前的幾行代碼。 - gtutorial-x86_64.exe+3DF98 - FF 90 28010000 - call qword ptr [rax+00000128]
- gtutorial-x86_64.exe+3DF9E - 84 C0 - test al,al
- gtutorial-x86_64.exe+3DFA0 - 0F84 D0000000 - je gtutorial-x86_64.exe+3E076
- gtutorial-x86_64.exe+3DFA6 - 48 8B 53 40 - mov rdx,[rbx+40]
- gtutorial-x86_64.exe+3DFAA - 49 63 C4 - movsxd rax,r12d
- gtutorial-x86_64.exe+3DFAD - 48 8B 04 C2 - mov rax,[rdx+rax*8]
- gtutorial-x86_64.exe+3DFB1 - 8B 50 70 - mov edx,[rax+70]
- gtutorial-x86_64.exe+3DFB4 - 48 8B 4B 28 - mov rcx,[rbx+28]
- gtutorial-x86_64.exe+3DFB8 - E8 E3160000 - call gtutorial-x86_64.exe+3F6A0
複製代碼在call qword ptr [rax+00000128]處下斷點,然後回到遊戲中發射子彈。會發現,剛一發射子彈立刻就斷下來了。我猜測這裡應該是碰撞檢測,然後下面的test和je來做判斷,如果碰撞上了,則執行扣血函數,沒撞上則直接跳過這部分代碼,不扣血。 怎麼驗證一下呢?把je改成jne,看看是不是子彈沒撞上的時候就直接扣血了。 修改以後的確是這樣的,而且之前是一次掉4滴血,現在連自己的子彈都會把自己打掉血,一次會掉5滴血。 把這裡je改成jmp即可。CE會提示原來指令是6字節,新指令是5字節,是否用NOP填充多餘的,選是就行了。 現在子彈會從我們上方飛過,而不與我們產生碰撞,而敵人卻依然會中彈。 第3 關
第3 關:把每個平台標記為綠色可以解鎖那扇門。注意:敵人會將你一擊致命(然後就失敗了)玩的愉快!提示:有很多解決方案。比如:找到與敵人的碰撞檢測,或者Teleport(傳送),或者飛行,或者... 第3 關嘗試 1看樣子,不開掛也能過啊。 第3 關Plus看來我還是too young, too naïve.
第3 關加強:門雖然解鎖了,但是敵人把門堵住了。 第3 關嘗試 2最簡單的就是搜索人物坐標了。把人物直接改到門那裡,不用“通過”敵人,而是直接瞬移過去。 計算機中,2D 遊戲一般是左負、右正,上下的正負不一定。3D 遊戲一般高度方向上正、下負,東西南北的正負不一定。 [Info] 2D 遊戲,如果使用計算機繪圖的坐標系則是下正、上負,如果使用數學中的坐標則是上正、下負。
3D 遊戲也有兩種坐標系。一種是向上為y 軸(這是沿襲2D 坐標的慣例),然後一般是向右為x,向屏幕外為z(也有向屏幕內為z 的)。另一種則是向上為z,水平面中向北為y,向東為x。
搜索Float(單精度浮點型)未知初始值,然後向右移動人物,搜索增大了的數值,然後向左移動人物,搜索減小了的數值,反复幾次,你應該能看到剩下一個唯一的數值。過程中還可以不移動人物,搜索未改變的數值。 [Tips] 你可以在設置中給常用搜索功能添加快捷鍵。這樣不用切出遊戲就可以進行下一次掃描了。
添加到地址列表中,然後改名為“X坐標”。然後復制粘貼,修改地址,把地址+4即為Y坐標。 [Info] 這裡+4還是-4主要看內存中的排列方式。一般X排在Y前面,所以要+4。對於3D遊戲,你搜索高度可能搜到的是Y也可能是Z,你可以使用右鍵→ Browse this memory region,然後右鍵→ Display Type→ Float來看看前後的內存數據,然後在遊戲中移動一下,憑感覺決定X、Y、Z 。
移動一下人物,大概估計一下坐標的範圍,整個遊戲區域對應的X和Y是-1到1直接的值。估計一下門的X坐標,把X坐標改成0.97。 Well Done
你戰勝了全部三個遊戲,幹得漂亮! 第3 關嘗試 3上面的方法很簡單也很實用,不過我們還可以繼續“玩”這個遊戲。 我們可不可以直接把所有的平台都改成綠色呢? 因為每個平台只有兩種狀態,而且只能從紅變成綠,這樣很不利於搜索,而且我也不知道他是怎麼存儲的,不知道紅和綠兩個狀態的值都是多少。 這個我嘗試了很多種辦法,例如: - 紅的時候搜0,綠的時候搜1,然後撞敵人撞死,再搜0。
- 紅的時候搜未知初始值,綠的時候搜改變了的數值,然後撞敵人撞死,再搜改變了的數值。
- 把類型改為Byte(單字節類型),因為bool類型都是佔用1字節的。
- 其實我還懷疑是不是每次撞死都會重新申請內存,這樣就更麻煩了。
最後,我使用“紅的時候搜Byte 類型未知初始值,綠的時候搜改變了的數值,然後撞敵人撞死,再搜改變了的數值”的方法找到了一點線索。雖然沒有找到具體的數據存儲地址,但是我找到了絕對相關的一組數據。這組數據每次顏色轉換都會相應的來回改變。
雖然沒有找到具體與台階有關的數值,但是注意圖中015F1AD8這個值,他的含義似乎是已經點亮的平台的數量 我直接把這個數字改成12的話,雖然沒有讓所有的平台都變綠,但是依然觸發“門解鎖、敵人堵門”這一事件了。 我突然有個想法,就是我直接站在門上,然後把數值修改為12,我已經在門上了,敵人就堵不到我了。 結果真的可以。
第3 關嘗試 44BFEEB60那些255和204看樣子像是RGB值。如果我手動添加4BFEEB60類型設為4字節,顯示十六進制值。結果就是FF00FF00,4個字節分別是ARGB,就是不透明的綠色。紅色的平台則是FFCC0000,不透明的暗紅色。
但是這些數值改了也沒什麼用,應該就是每一像素的顏色。 上面那個015ABE78,手動添加這個地址,並設置成Float類型的話,就會發現,紅色的時候是0.8,綠色的時候是0。同理015ABE7C,紅色的時候是0,綠色的時候是1。 把這兩個數值改成其他的,你會發現平台的顏色也變了。
我又發現一個有趣的現象,如果我把平台的顏色鎖定為紅色,然後讓人物站上去,這時“已變綠平台數”那個計數器會快速增長。所以你有什麼想法? 這就是為什麼我找不到一個bool 型變量來描述平台是否變綠,因為他的代碼根本沒有這樣一個變量,他的邏輯應該大致是這樣的。 - if (collision) {
- R = 0;
- if (G != 1) {
- count++;
- G = 1;
- }
- }
複製代碼如果站到平台上了,則令紅色為0,如果綠色不為1,則計數器+1,並令綠色為1。 這裡面沒有出現flag 這種東西來表示平台是否變綠色,他直接用顏色來判斷的。 第3 關嘗試 5找到與敵人的碰撞檢測,或者Teleport(傳送),或者飛行,或者...
關卡說明中告訴裡一部分思路,TP 已經試過了,現在我們來試試飛行。 所謂的飛行其實就是把重力改小,或者是像玩Flappy Bird 那樣一跳一跳的,可以一直在天上飛著。 首先來找到重力大小。 重力會影響速度,速度影響坐標,我們現在只知道坐標的地址,我們可以通過查找寫入,然後分析附近代碼來找到速度,然後進而找到重力加速度。 y = y0 + vy * t
計算位置需要先讀取Y坐標y0,然後加上速度差,在賦值給y。 這裡有個小技巧,就是對同一個地址同時使用查找寫入和查找訪問,這樣我們很容易地找到了寫入的地址,然後在查找訪問窗口中,寫入地址以前的幾個讀取都很可疑。
第二條寫入指令,在跳起來懸空的時候,計數器不會增加,應該是當人物接觸到地面的時候,防止人物穿過地面用的。我們只看第一條。 Show disassembler,我把gtutorial-x86_64.exe+40491到gtutorial-x86_64.exe+40506截取出來。 - gtutorial-x86_64.exe+4048D - 48 8B 43 28 - mov rax,[rbx+28]
- gtutorial-x86_64.exe+40491 - F3 44 0F10 40 28 - movss xmm8,[rax+28] { 读取 Y 坐标 }
- gtutorial-x86_64.exe+40497 - 48 8B 43 28 - mov rax,[rbx+28]
- gtutorial-x86_64.exe+4049B - F3 0F5A 48 28 - cvtss2sd xmm1,[rax+28] { 再次读取 Y 坐标 }
- gtutorial-x86_64.exe+404A0 - F3 0F5A 53 78 - cvtss2sd xmm2,[rbx+78] { 读取 Y 速度 }
- gtutorial-x86_64.exe+404A5 - F2 0F2A C6 - cvtsi2sd xmm0,esi { esi 是毫秒数 }
- gtutorial-x86_64.exe+404A9 - F2 0F5E 05 AF382400 - divsd xmm0,[gtutorial-x86_64.exe+283D60] { 除以 1000 }
- gtutorial-x86_64.exe+404B1 - F2 0F59 C2 - mulsd xmm0,xmm2 { 速度乘时间 }
- gtutorial-x86_64.exe+404B5 - F2 0F5C C8 - subsd xmm1,xmm0 { Y 坐标减去位移 }
- gtutorial-x86_64.exe+404B9 - F2 44 0F5A C9 - cvtsd2ss xmm9,xmm1 { double 转 float }......
- gtutorial-x86_64.exe+40506 - F3 44 0F11 48 28 - movss [rax+28],xmm9 { 赋值给 [rax+28] }
複製代碼[Comment] 註釋是我自己加的。這個是根據邏輯和感覺猜出來的,也有可能猜錯。不過這個簡單的速度位移公式,一般來說分析應該是正確的。
注意這幾條 gtutorial-x86_64.exe+40497 - 48 8B 43 28 - mov rax,[rbx+28]
gtutorial-x86_64.exe+4049B - F3 0F5A 48 28 - cvtss2sd xmm1,[rax+28] { 再次读取 Y 坐标 }
gtutorial-x86_64.exe+404A0 - F3 0F5A 53 78 - cvtss2sd xmm2,[rbx+78] { 读取 Y 速度 }Y坐標的內存地址是[[["gtutorial-x86_64.exe"+37DC50]+760]+28]+24,rbx應該是一級指針的值[["gtutorial-x86_64.exe"+37DC50]+760],那麼Y速度的地址應該就是[["gtutorial-x86_64.exe"+37DC50]+760]+78。 手動添加Y速度的地址,然後把速度改成3,你會發現人物跳了起來。
剛才設成3跳的有點高,添加一個上箭頭的熱鍵,設置值為1。如果長按的話就會勻速向上飛。
第3 關嘗試 6剛才改速度已經成功了,現在我們來改重力加速度。 還是查找寫入。
- 第1 個在脫離地面之後不計數,應該是地面支撐
- 第2 個隨時都會觸發,應該是重力加速度導致的
- 第3 個是起跳時觸發
- 第4 個則是長按跳躍連跳時觸發
我突然有個想法,起跳時觸發的那條語句,一定有什麼限制他,讓他只能在地面上起跳,而不能在空中起跳。 Show disassembler. - gtutorial-x86_64.exe+3FE9A - C6 43 74 01 - mov byte ptr [rbx+74],01 { 1 }
- gtutorial-x86_64.exe+3FE9E - 80 7B 7C 00 - cmp byte ptr [rbx+7C],00 { 0 }
- gtutorial-x86_64.exe+3FEA2 - 0F85 93000000 - jne gtutorial-x86_64.exe+3FF3B
- gtutorial-x86_64.exe+3FEA8 - 8B 05 823E2400 - mov eax,[gtutorial-x86_64.exe+283D30] { (1.45) }
- gtutorial-x86_64.exe+3FEAE - 89 43 78 - mov [rbx+78],eax
複製代碼經過分析和猜測,[rbx+74]表示是否按下跳躍鍵,[rbx+7C]表示是否懸空。 jne 表示如果懸空則不允許跳。 直接把jne那條語句NOP掉,就可以實現無限連跳了。剛才設置的熱鍵都用不著了。 你也嘗試可以修改gtutorial-x86_64.exe+283D30這個地址的數值,它表示跳躍初速度。 上面的方法修改之後,長按不會一直向上飛,必須像Flappy Bird一樣一下一下的。如果你想長按就一直向上飛,那就把第4條指令前面的jne也NOP掉。 - gtutorial-x86_64.exe+406F8 - 80 7B 7C 00 - cmp byte ptr [rbx+7C],00 { 0 }
- gtutorial-x86_64.exe+406FC - 75 0B - jne gtutorial-x86_64.exe+40709
- gtutorial-x86_64.exe+406FE - 8B 05 2C362400 - mov eax,[gtutorial-x86_64.exe+283D30] { (1.45) }
- gtutorial-x86_64.exe+40704 - 89 43 78 - mov [rbx+78],eax
複製代碼 第3 關嘗試 7剛才跑題了,我們繼續來找重力加速度 v = v0 + g * t
分析一下第2 個指令附近 - gtutorial-x86_64.exe+40709 - F3 0F5A 43 78 - cvtss2sd xmm0,[rbx+78] { 读取速度 }
- gtutorial-x86_64.exe+4070E - F2 0F5C 05 52362400 - subsd xmm0,[gtutorial-x86_64.exe+283D68] { 减去 0.1 }
- gtutorial-x86_64.exe+40716 - F2 0F5A C0 - cvtsd2ss xmm0,xmm0 { double 转 float }
- gtutorial-x86_64.exe+4071A - F3 0F11 43 78 - movss [rbx+78],xmm0 { 写入速度 }
複製代碼這個邏輯好簡單啊,與時間都無關,就是每次計算把Y 速度減0.1。 手動添加地址gtutorial-x86_64.exe+283D68,類型為double,然後把重力加速度調小就行了。
第3 關嘗試 8我們還有什麼辦法?我可不可以把敵人固定住,讓他不要移動,或者移到屏幕外,總之讓他別妨礙我們就行了。 用同樣搜索自己坐標的方法搜索敵人的坐標。只不過自己的坐標可以自己控制,敵人的坐標只能隨他們移動了。 找到3個X坐標之後+4就是Y坐標。 把這些坐標鎖定,可行。把已變綠平台數改成12,這些敵人又不聽話了,又開始堵門了,鎖定似乎對他們不好使。 查找寫入他們的指令
既然他堵住門時會一直觸發第5 條,那麼我就簡單粗暴一點,直接把第5 條指令NOP 掉,這樣我就可以從外部修改這個數值了。
第3 關嘗試 9我們還可以想辦法直接開門。 查找訪問“已變綠平台數”的指令。 只有這一條 gtutorial-x86_64.exe+4098B - 48 63 93 88000000 - movsxd rdx,dword ptr [rbx+00000088]我們分析一下附近 - gtutorial-x86_64.exe+4098B - 48 63 93 88000000 - movsxd rdx,dword ptr [rbx+00000088] { 读取已变绿平台数 }
- gtutorial-x86_64.exe+40992 - 48 8B 43 30 - mov rax,[rbx+30]
- gtutorial-x86_64.exe+40996 - 48 85 C0 - test rax,rax
- gtutorial-x86_64.exe+40999 - 74 08 - je gtutorial-x86_64.exe+409A3
- gtutorial-x86_64.exe+4099B - 48 8B 40 F8 - mov rax,[rax-08] { [rax-08] 为平台数组最大下标 }
- gtutorial-x86_64.exe+4099F - 48 83 C0 01 - add rax,01 { 最大下标 + 1 即为总平台数 }
- gtutorial-x86_64.exe+409A3 - 48 39 C2 - cmp rdx,rax { 比较已变绿平台数和总平台数 }
- gtutorial-x86_64.exe+409A6 - 7C 17 - jl gtutorial-x86_64.exe+409BF
- gtutorial-x86_64.exe+409A8 - 48 8B 43 60 - mov rax,[rbx+60] { 二级指针 }
- gtutorial-x86_64.exe+409AC - C6 40 18 00 - mov byte ptr [rax+18],00 { 开门 }
- gtutorial-x86_64.exe+409B0 - C6 43 7D 01 - mov byte ptr [rbx+7D],01 { 堵门 }
- gtutorial-x86_64.exe+409B4 - 48 8B 43 68 - mov rax,[rbx+68]
- gtutorial-x86_64.exe+409B8 - 48 89 83 80000000 - mov [rbx+00000080],rax
- gtutorial-x86_64.exe+409BF - 48 83 7B 28 00 - cmp qword ptr [rbx+28],00 { 0 }
複製代碼[rbx+00000088]為已變綠平台數,而已變綠平台數的地址為[["gtutorial-x86_64.exe"+37DC50]+760]+88,所以rbx = [["gtutorial-x86_64.exe"+37DC50]+760],所以可以求得開門地址為[[["gtutorial-x86_64.exe"+37DC50]+760]+60]+18,堵門的地址為[["gtutorial-x86_64.exe"+37DC50]+760]+7D。
我們直接執行mov byte ptr [rax+18],00這條開門語句的內容就行了。手動添加開門地址,Byte類型,然後修改為0。這樣我們躲過敵人就可以進門了,不用讓平台變綠,也不會被堵住。
請注意上面動畫中,修改完數值之後右下角門的變化。 第3 關嘗試10終於要到碰撞檢測了。第2 關中,我們讓子彈直接忽略玩家,繼續向前飛。第3 關我們也可以讓敵人忽略玩家,即使碰到了也不會死亡。 碰撞檢測肯定會讀取二者的X、Y 坐標。查找訪問敵人Y 坐標的指令。 最開始只看到1 條指令。 gtutorial-x86_64.exe+39DDE - F3 0F10 4B 28 - movss xmm1,[rbx+28]但是查看附近代碼的時候我看到 call qword ptr [gtutorial-x86_64.exe+3825E0] { ->opengl32.glTranslatef } - gtutorial-x86_64.exe+39DDE - F3 0F10 4B 28 - movss xmm1,[rbx+28]
- gtutorial-x86_64.exe+39DE3 - F3 0F10 05 7D7F2400 - movss xmm0,[gtutorial-x86_64.exe+281D68] { (0.00) }
- gtutorial-x86_64.exe+39DEB - 0F57 C8 - xorps xmm1,xmm0
- gtutorial-x86_64.exe+39DEE - F3 0F10 43 24 - movss xmm0,[rbx+24]
- gtutorial-x86_64.exe+39DF3 - F3 0F10 15 757F2400 - movss xmm2,[gtutorial-x86_64.exe+281D70] { (0.00) }
- gtutorial-x86_64.exe+39DFB - FF 15 DF873400 - call qword ptr [gtutorial-x86_64.exe+3825E0] { ->opengl32.glTranslatef }
複製代碼所以這個應該是在繪圖指令前讀取Y 坐標,這個應該不是碰撞檢測的代碼。 我懷疑是不是碰撞檢測和其他代碼混在一起,所以只有一次讀取。 我沿著這個附近單步調試了很長時間。 終於,一次不經意間我發現問題了。請觀察下面的動圖。 原始的代碼很可能是這樣的。 - float player_w_2 = player_w / 2.0f;
- float enemy_w_2 = enemy_w / 2.0f;
- if (enemy_x - enemy_w_2 < player_x + player_w_2 && player_x - player_w_2 < enemy_x + enemy_w_2) {
- float player_h_2 = player_h / 2.0f;
- float enemy_h_2 = enemy_h / 2.0f;
- if (enemy_y - enemy_h_2 < player_y + player_h_2 && player_y - player_h_2 < enemy_y + enemy_h_2) {
- // 碰撞
- }
- }
複製代碼邏輯短路。如果X 坐標不在敵人寬度範圍內,那麼直接就不用判斷Y 坐標了,就不會對Y 坐標造成訪問。 解決這個問題之後,我們又找到這條語句。 gtutorial-x86_64.exe+39B45 - F3 0F10 43 28 - movss xmm0,[rbx+28]在周圍分析一下。 - gtutorial-x86_64.exe+39B26 - FF 90 E0000000 - call qword ptr [rax+000000E0] { movss xmm0,[100284490]
- xmm0 = 0.1 }
- gtutorial-x86_64.exe+39B2C - F3 0F10 4E 30 - movss xmm1,[rsi+30]
- gtutorial-x86_64.exe+39B31 - F3 0F58 0D 1F822400 - addss xmm1,dword ptr [gtutorial-x86_64.exe+281D58] { (1.00) }
- gtutorial-x86_64.exe+39B39 - F3 0F59 0D 1F822400 - mulss xmm1,[gtutorial-x86_64.exe+281D60] { (0.50) }
- gtutorial-x86_64.exe+39B41 - F3 0F59 C8 - mulss xmm1,xmm0 { xmm1 = 0.5 * 0.1 }
- gtutorial-x86_64.exe+39B45 - F3 0F10 43 28 - movss xmm0,[rbx+28] { 读取敌人 Y 坐标 }
- gtutorial-x86_64.exe+39B4A - F3 0F5C C1 - subss xmm0,xmm1 { 减掉敌人高度的一半 }
- ......
- gtutorial-x86_64.exe+39B56 - C3 - ret
複製代碼跟踪這個函數的返回,你會發現一片新天地。 - gtutorial-x86_64.exe+3A72E - 48 89 CB - mov rbx,rcx { rbx 为敌人指针 }
- gtutorial-x86_64.exe+3A731 - 48 89 D6 - mov rsi,rdx
- gtutorial-x86_64.exe+3A734 - 40 B7 00 - mov dil,00 { 0 }
- gtutorial-x86_64.exe+3A737 - 83 7B 58 00 - cmp dword ptr [rbx+58],00 { 0 }
- gtutorial-x86_64.exe+3A73B - 75 58 - jne gtutorial-x86_64.exe+3A795 { 如果 [rbx+58] != 0,则使用普通碰撞算法,否则使用简化碰撞算法 }
- gtutorial-x86_64.exe+3A73D - 48 89 F1 - mov rcx,rsi { rsi 为玩家指针 }
- gtutorial-x86_64.exe+3A740 - E8 6BFFFFFF - call gtutorial-x86_64.exe+3A6B0 { xmm0 = [rcx+44] 玩家碰撞半径 }
- gtutorial-x86_64.exe+3A745 - 0F28 F0 - movaps xmm6,xmm0 { xmm6 = [rcx+44] 玩家碰撞半径 }
- gtutorial-x86_64.exe+3A748 - 48 89 D9 - mov rcx,rbx { rbx 为敌人指针 }
- gtutorial-x86_64.exe+3A74B - E8 60FFFFFF - call gtutorial-x86_64.exe+3A6B0 { xmm0 = [rcx+44] 敌人碰撞半径 }
- gtutorial-x86_64.exe+3A750 - F3 0F10 4E 24 - movss xmm1,[rsi+24] { xmm1 = 玩家 X }
- gtutorial-x86_64.exe+3A755 - F3 0F5C 4B 24 - subss xmm1,[rbx+24] { xmm1 = 玩家 X - 敌人 X }
- gtutorial-x86_64.exe+3A75A - 0F54 0D 0F641E00 - andps xmm1,[gtutorial-x86_64.exe+220B70] { 取绝对值 }
- gtutorial-x86_64.exe+3A761 - F3 0F59 C9 - mulss xmm1,xmm1 { 平方 }
- gtutorial-x86_64.exe+3A765 - F3 0F10 56 28 - movss xmm2,[rsi+28] { xmm2 = 玩家 Y }
- gtutorial-x86_64.exe+3A76A - F3 0F5C 53 28 - subss xmm2,[rbx+28] { xmm2 = 玩家 Y - 敌人 Y }
- gtutorial-x86_64.exe+3A76F - 0F54 15 FA631E00 - andps xmm2,[gtutorial-x86_64.exe+220B70] { 取绝对值 }
- gtutorial-x86_64.exe+3A776 - F3 0F59 D2 - mulss xmm2,xmm2 { 平方 }
- gtutorial-x86_64.exe+3A77A - F3 0F58 D1 - addss xmm2,xmm1 { 相加 }
- gtutorial-x86_64.exe+3A77E - F3 0F51 D2 - sqrtss xmm2,xmm2 { xmm2 = 敌人、玩家中心距离 }
- gtutorial-x86_64.exe+3A782 - 0F28 CE - movaps xmm1,xmm6
- gtutorial-x86_64.exe+3A785 - F3 0F58 C8 - addss xmm1,xmm0 { xmm1 = 敌人碰撞半径+玩家碰撞半径 }
- gtutorial-x86_64.exe+3A789 - 0F2F CA - comiss xmm1,xmm2
- gtutorial-x86_64.exe+3A78C - 40 0F97 C7 - seta dil
- gtutorial-x86_64.exe+3A790 - E9 B4000000 - jmp gtutorial-x86_64.exe+3A849
- gtutorial-x86_64.exe+3A795 - 83 7B 58 01 - cmp dword ptr [rbx+58],01 { 1 }
- gtutorial-x86_64.exe+3A799 - 0F85 AA000000 - jne gtutorial-x86_64.exe+3A849 { 如果 [rbx+58] != 1 则不判断碰撞,前面已将 dil 设为 0 }
- gtutorial-x86_64.exe+3A79F - 48 89 D9 - mov rcx,rbx
- gtutorial-x86_64.exe+3A7A2 - 48 89 D8 - mov rax,rbx
- gtutorial-x86_64.exe+3A7A5 - 48 8B 00 - mov rax,[rax]
- gtutorial-x86_64.exe+3A7A8 - FF 90 F8000000 - call qword ptr [rax+000000F8] { xmm0 = 敌人 left }
- gtutorial-x86_64.exe+3A7AE - 0F28 F0 - movaps xmm6,xmm0
- gtutorial-x86_64.exe+3A7B1 - 48 89 F1 - mov rcx,rsi
- gtutorial-x86_64.exe+3A7B4 - 48 89 F0 - mov rax,rsi
- gtutorial-x86_64.exe+3A7B7 - 48 8B 00 - mov rax,[rax]
- gtutorial-x86_64.exe+3A7BA - FF 90 00010000 - call qword ptr [rax+00000100] { xmm0 = 玩家 right }
- gtutorial-x86_64.exe+3A7C0 - 0F2F C6 - comiss xmm0,xmm6
- gtutorial-x86_64.exe+3A7C3 - 0F8A 7D000000 - jp gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A7C9 - 0F86 77000000 - jbe gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A7CF - 48 89 F1 - mov rcx,rsi
- gtutorial-x86_64.exe+3A7D2 - 48 89 F0 - mov rax,rsi
- gtutorial-x86_64.exe+3A7D5 - 48 8B 00 - mov rax,[rax]
- gtutorial-x86_64.exe+3A7D8 - FF 90 F8000000 - call qword ptr [rax+000000F8] { xmm0 = 玩家 left }
- gtutorial-x86_64.exe+3A7DE - 0F28 F0 - movaps xmm6,xmm0
- gtutorial-x86_64.exe+3A7E1 - 48 89 D9 - mov rcx,rbx
- gtutorial-x86_64.exe+3A7E4 - 48 89 D8 - mov rax,rbx
- gtutorial-x86_64.exe+3A7E7 - 48 8B 00 - mov rax,[rax]
- gtutorial-x86_64.exe+3A7EA - FF 90 00010000 - call qword ptr [rax+00000100] { xmm0 = 敌人 right }gtutorial-x86_64.exe+3A7F0 - 0F2F C6 - comiss xmm0,xmm6
- gtutorial-x86_64.exe+3A7F3 - 7A 51 - jp gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A7F5 - 76 4F - jna gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A7F7 - 48 89 D9 - mov rcx,rbx
- gtutorial-x86_64.exe+3A7FA - 48 89 D8 - mov rax,rbx
- gtutorial-x86_64.exe+3A7FD - 48 8B 00 - mov rax,[rax]
- gtutorial-x86_64.exe+3A800 - FF 90 08010000 - call qword ptr [rax+00000108] { xmm0 = 敌人 top }
- gtutorial-x86_64.exe+3A806 - 0F28 F0 - movaps xmm6,xmm0
- gtutorial-x86_64.exe+3A809 - 48 89 F1 - mov rcx,rsi
- gtutorial-x86_64.exe+3A80C - 48 89 F0 - mov rax,rsi
- gtutorial-x86_64.exe+3A80F - 48 8B 00 - mov rax,[rax]
- gtutorial-x86_64.exe+3A812 - FF 90 10010000 - call qword ptr [rax+00000110] { xmm0 = 玩家 bottom }
- gtutorial-x86_64.exe+3A818 - 0F2F C6 - comiss xmm0,xmm6
- gtutorial-x86_64.exe+3A81B - 7A 29 - jp gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A81D - 76 27 - jna gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A81F - 48 89 F1 - mov rcx,rsi
- gtutorial-x86_64.exe+3A822 - 48 8B 06 - mov rax,[rsi]
- gtutorial-x86_64.exe+3A825 - FF 90 08010000 - call qword ptr [rax+00000108] { xmm0 = 玩家 top }
- gtutorial-x86_64.exe+3A82B - 0F28 F0 - movaps xmm6,xmm0
- gtutorial-x86_64.exe+3A82E - 48 89 D9 - mov rcx,rbx
- gtutorial-x86_64.exe+3A831 - 48 8B 03 - mov rax,[rbx]
- gtutorial-x86_64.exe+3A834 - FF 90 10010000 - call qword ptr [rax+00000110] { xmm0 = 敌人 bottom }
- gtutorial-x86_64.exe+3A83A - 0F2F C6 - comiss xmm0,xmm6
- gtutorial-x86_64.exe+3A83D - 7A 07 - jp gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A83F - 76 05 - jna gtutorial-x86_64.exe+3A846
- gtutorial-x86_64.exe+3A841 - 40 B7 01 - mov dil,01 { 碰撞设置 dil 为 1 }
- gtutorial-x86_64.exe+3A844 - EB 03 - jmp gtutorial-x86_64.exe+3A849
- gtutorial-x86_64.exe+3A846 - 40 B7 00 - mov dil,00 { 0 }
- gtutorial-x86_64.exe+3A849 - 40 0FB6 C7 - movzx eax,dil { 碰撞函数返回值为 eax }
- gtutorial-x86_64.exe+3A84D - 90 - nop
- gtutorial-x86_64.exe+3A84E - 66 0F6F 74 24 20 - movdqa xmm6,[rsp+20]
- gtutorial-x86_64.exe+3A854 - 48 8D 64 24 30 - lea rsp,[rsp+30]
- gtutorial-x86_64.exe+3A859 - 5E - pop rsi
- gtutorial-x86_64.exe+3A85A - 5F - pop rdi
- gtutorial-x86_64.exe+3A85B - 5B - pop rbx
- gtutorial-x86_64.exe+3A85C - C3 - ret
複製代碼上面是完整的碰撞算法分析。 其實並沒有這麼麻煩,我們只需要知道返回值是eax,如果eax == 1則表示碰撞,eax == 0則表示未碰撞。 我們跟踪ret 返回。 - gtutorial-x86_64.exe+4093A - FF 90 28010000 - call qword ptr [rax+00000128] { eax = 是否碰撞 }
- gtutorial-x86_64.exe+40940 - 84 C0 - test al,al
- gtutorial-x86_64.exe+40942 - 74 11 - je gtutorial-x86_64.exe+40955 { eax == 0 则跳转 }
- gtutorial-x86_64.exe+40944 - 48 8B 4B 28 - mov rcx,[rbx+28]
- gtutorial-x86_64.exe+40948 - 48 8B 43 28 - mov rax,[rbx+28]
- gtutorial-x86_64.exe+4094C - 48 8B 00 - mov rax,[rax]
- gtutorial-x86_64.exe+4094F - FF 90 20010000 - call qword ptr [rax+00000120] { 碰撞之后执行的事件 }
- gtutorial-x86_64.exe+40955 - 48 8B 53 38 - mov rdx,[rbx+38]
複製代碼je修改成jmp即可。
第3 關嘗試11如果[rbx+58] != 0,則使用普通碰撞算法,否則使用簡化碰撞算法,如果[rbx+58] != 1則不判斷碰撞。所以我們可以令[rbx+58] = 2這樣兩個碰撞就都沒了。 手動添加[[[["gtutorial-x86_64.exe"+37DC50]+760]+38]+0]+58,4字節,設置為2。 隱藏問題你有沒有註意到你執行代碼注入以後標題欄會由Step 2變成Step 2 (Integrity check error) 完整性檢查錯誤
遊戲中內置的檢測工具發現你修改了他們的程序指令。怎麼辦呢? 他們是怎麼檢測的呢? 原理很簡單,就是比較代碼區域的內存。 避免被發現的方法並不是如何偽造內存讓他們別發現。通常檢測程序運行在另一個線程,直接關掉那個線程就行了。 首先在地址列表中手動添加我們剛才修改的地址gtutorial-x86_64.exe+3F6A3,然後查找誰在訪問這個地址。 [Tips] 一般正常的程序不會訪問程序代碼部分的內存的,他們運行所要的數據和都在常量區、全局變量區、堆、棧中,代碼區是額外的一個區域,他們之間都是隔離開的。要訪問程序自己的代碼區的程序都不是正常的程序。
我這裡找到了3 個,然後選擇其中一個(我這裡就選第一個了)
Show disassembler,然後下斷點。
然後我們需要做的就是記住標題上的線程編號。然後在Memory Viewer中View→ Threadlist→右鍵點擊剛才的線程編號→ Freeze thread。
你打敗了3 個“遊戲”,並且你打敗了完整性檢查! 幹的真的漂亮! 總結本文通過3個小遊戲的二十多種思路,向你展示了很多破解思路。 您應該學習並理解這種思路,主要是如何通過內存地址找代碼,如果通過代碼找內存地址。 本文還講述了一些小技巧,如何搜索坐標這種未知數值的內存數據。 最後簡單講解了內存校驗的原理與簡易破解方法。 希望您不僅能從本文中學到Cheat Engine 工具的使用方法,還能學到更廣闊的破解思想。 最後如果你想繼續研究,你可以對照GTutorial源代碼進行研究,看看原作者的註釋,你能看到他給你預留了很多變量用於破解。
|