俗話說隔行如隔山,感覺上是一回事,自己動手又是另一回事.這兩天回家就幫親戚家孩子做外掛,本以為很簡單,結果泡廣海逛看雪的,研究了三個半晚上才在今天接近凌晨時大體弄好.萬幸自己一直在混軟件這碗飯,并沒真正的隔行,最多是"一座山上兩座峰,搭個吊橋就能通"罷了.
前兩天博文中提過一次,這次幫親戚做外掛為了省事沒做封包的(事實上沒寫過外掛,也不敢寫封包的),只是利用程序自身CALL指令在模擬事件, 寫的"內掛"罷了.個人以為,寫這類東西主要難在脫殼和過防護,過去便一馬平川,真正開發時的問題反而很少.
大前天寫過一個Java的匯編混合類庫,剛才寫外掛時順便給它添了點東西,又根據廣海的找CALL入門示例延伸出一個Java版的實現,現發布于此博客,歡迎有興趣者幫忙繼續完善類庫.
——————————————————————————————————————————————————————
愚以為,Java做外掛開發有優點如下:
1、隱蔽性高:由于標準Java程序在運行時僅會顯示主線程,也就是Java或者Javaw,開發商將很難據此判斷外掛進程的使用狀態(想查標題和窗體類名識別?1秒能隨機換N個);而對于特征碼識別,Java的解釋型特點令class完全能夠做到動態隨機構建,完全可以自動變更特征碼,讓屏蔽軟件防不勝防。也就是說,大多數針對exe文件的監測方法,并不能很好的對Java通用,Java外掛將有較高的隱蔽性。當然,也許你會說廠商可以干脆停止所有Java進程。是的,可與此同時,這也意味著廠商的開發人員承認了自己的無能——由于識別不出犯人,只好見人就殺。如果偏巧趕上個用永中office之類Java軟件工作的主,結果沒保存就被游戲強斷了(或者開著Java進不去游戲,一樣會有人抗議),便有熱鬧可看^^另外相關人等大可以在背后寫信鼓動Sun或者IBM對廠商提出交涉(有炒作的價值,他們不一定不管哦~),再不然借機把相關資料和截圖發往各大Java社區,不用咱們動手,他們的游戲就會被瘋狂修改,甚至連源碼都被發上網來……
2、先天適合注入操作:我們都知道JNI不光能調用本地接口,也能讓本地程序動態調用指定Java類及相關函數,而對于其他語言來說,這是較難實現的;只要善于利用這點,我們便可以令實現了JNI接口的DLL文件充當發報器,而讓Java平臺充當中間件,最大限度的擴展功能,并以此為基礎提供便利的進程間通信。
3、跨平臺:我們心理都很清楚,Java并沒有真正意義上的跨平臺,Java本身即是一個平臺,只不過針對不同的操作系統有不同的版本罷了。但是,只要對于用戶體驗而言,Java是在跨平臺,便已經足夠了。如果那天你開發的網游在其他系統出了新版,你同樣可以在不改變原有外掛UI及外部接口的情況下追過去,而且是游戲敢做到什么系統,你就敢追到什么系統。還有一點,對于完全脫機版的封包外掛而言,Java能讓你的用戶在絕大多數時候,絕大多數系統中,堅持掛機不動搖……
4、開發封包類外掛便利:Java提供的網絡通訊功能是非常強大的,并且有大量第三方組件支持,對于脫機的封包外掛而言,Java實現遠比C/C++實現簡單,另外跨平臺在第三點已經強調了。
5、開發周期短,適合團隊協作:Java先天具有適合商業開發的基因,團隊協作開發對于熟悉Java者也只是小菜一碟罷了。Java代碼的可讀性相較C/C++或者Delphi而言要更好些,調試也更簡單,有助于你的外掛早日上市。
缺憾:
不知道。(對于Java所有缺點,鄙人認為都有方案解決,所以暫時沒想到缺點:))
關于找Call部分,直接轉載自廣海社區,發帖人為[gao6621],原文地址:http://ghoffice.com/bbs/read-htm-tid-50497-keyword-%B3%AC%BC%B6.html
************************************轉載開始************************************
首先說明,這個教程以一個找CALL的練習程序為例子。之所以不拿游戲,因為游戲找CALL時間長了,不適合做教程,而且本練習涵蓋參數。我將說明為什么這么調用,為什么這么寫!
好的。偶們好的,偶們這節課需要用到的程序為【wygailf】制作的一個找CALL測試程序。首先感謝他!

這個就是我們用到的程序,OK,打開他并且用OD附加進程!

并使其進入“運行”狀態
好的,下面我沒開始找CALL,首先說明一下,CTRL+F9這個是“運行到返回”。為什么要按這個按鈕?----就我的理解,假設把程序比做很多層的一個盒子,而CALL就是我們要從盒子里拿出來的東西。
那么,如果我們想拿出來CALL,怎么辦?當然是打開盒子,取出盒子,再打開盒子,取出盒子.....而這個CTRL+F9就是這個打開盒子到取出盒子的過程。運行到返回,顧名思義,就是運行到RET(返回)截止。
而這個RET也正是跳出本層的一個關鍵點。每一個RET都有可能是一層。所以這樣也就解釋了為什么有的時候按三下CTRL+F9和四下CTRL+F9的原因了。
好,說的就這些。下面LET'S GO!
我們首先下斷點bp send。

然后回車。如果不確定自己是否成功的下了斷點。可以在OD中按ALT+B來查看
好的,這個就是我們下的斷點了。始終就是斷點有效,也可以暫停斷點。選擇一個斷點,敲空格。這個斷點就變成了“禁用”。這樣就算暫停了斷點。OK這里不再贅述,我們開始。
==============================================================================================================
首先,我們來個HP藥水試試!

在這里選擇吃藥。然后OD會斷下!
這里是程序斷下的地方,我們可以看到下方有如下注釋:
SEND來自.....說明這里是send函數被斷開的地方。
繼續,CTRL+F9我每一步都會記錄下來,一點一點給新手解釋為什么!
上圖是我按下第一次CTRL+F9之后轉到的RET。這里再順便說一句。看到這里的RET 10了沒有。這里是RET 10就是有4個參數的RET,一個參數占4個字節。那么按照這么說來應該是 RET 16才對啊。其實這里的10是16進制的10,那么16=10(16進制)=4x4所以這里是RET 10。好,下面說正題:看到上面的CALL了沒有。CALL WS2_32....當你看到這個的時候,你就可以毫不猶豫的再次按下CTRL+F9了,說明你現在還在系統的范圍內。還沒有進入到程序。為什么系統跟程 序不一樣?因為程序是依托在WINDOWS平臺運行的。那么如果程序要干什么事情,就要跟WINDOWS打聲招呼。也就是SEND函數!程序跟系統說,我 要做動作了,系統說批準,程序說我要喝藥,系統說批準,程序說我去哪里喝藥,系統說CALL!好的,這里的CALL就是我們需要的CALL了。:-)
第二次CTRL+F9,這里的CALL還是差不多,還在系統層內。我們繼續!
OK,以上是第三次按CTRL+F9所看到的信息。
看到這里了嗎,已經是程序層了。也就說這里很有可能就是我們需要的CALL了。OK。我們來測試一下,我們來看這一段
lewei2000提醒:以下幾點解釋有誤,初學者略過,樓主還需深化匯編知識
看 第一行:mov dword ptr fs:[eax],edx -------這里的意思是:將指針賦值到dword,后面有FS就是注釋[eax],edx其實就是將edx中的值復制到eax中,使eax和edx相 等(由于版主的指正,這里要聲明,此處的理解為自己的理解。并不是正規的解釋方法,僅供參考而已)。
看第二行:push 入棧,入棧就是把一個數值放到寄存器中,其實跟mov是一樣的,那么這里push到哪里了呢?因為第一行eax已經被占用,那么這里就應該是ebx。也就說這里是賦值EBX。
看第三行:lea eax,pword ptr ss:[edp-4]。這里LEA指令指將操作結果保存到eax,好既然是eax我們就不管他了。至于是什么結果,我們一會看。
看第四行:CALL不解釋。就是我們需要的東西。(但其實不是)
看第五行:ret 返回的意思。
好的,假設這段代碼是我們需要的代碼,那我們該怎么去表達它,在程序中如何去寫?
這里留個懸念,因為我事先測試過,這里不是我們需要的CALL,我就不說了。等找到正確CALL的時候我再講解如何去寫CALL。
好繼續CTRL+F9下圖
看這里,跟上面一樣,這個返回沒用,我們繼續CTRL+F9。
好,這里又出現了一個CALL,那我們想想是不是這個呢?如何去調用這個CALL?
首先,我們要測試一下它是不是一個帶參數的CALL,至于怎么測試呢?---靠,那就用程序CALL一下唄!但這里的CALL是個有參數的CALL。我們繼續。
我們如何知道這個CALL調用了什么參數?試想一下,CALL調用參數,要在哪里看?當然是在CALL中看了,如何看?那就讓我們去CALL那里了,如何去?--斷點~!(周杰倫唱的)
好,在上面CALL那里下斷點~選擇CALL,按F2
這里前面的地址變成了紅色的。這樣就算斷點成功了,這里斷了前面就不用斷了,我們在ALT+B中刪除以前的SEND斷點。(Delete鍵刪除)
OK,我們讓程序恢復到運行狀態!
好,我們看到,測試程序中顯示,使用了一個補血藥品。OK,我們繼續按“吃血”!

好的,看到斷點了沒有,正好斷在我們剛才下斷點的地方。說明不管這個CALL正確與否,我們吃血的過程都要調用這個CALL,這樣就離正確很接近了。

我們剛才說看CALL的參數,CALL的參數其實就是在調用CALL的時候所需要的運行環境,在什么條件下,CALL執行之后是吃血,什么情況下是吃藍。
好的。我們現在利用到了寄存器。看寄存器中的提示,有兩個紅色,說明當我們調用CALL的時候,它使用了寄存器中的兩個地址。那么這個就是CALL的
運行環境了,也就是說,只要在我們調用這個CALL的時候,寄存器EAX中有值00D51FE4和寄存器ECX中有值0042ABE4就可以運行。
好,這里我們寫一個小程序來調用CALL。
************************************轉載結束************************************
總算粘貼完了~~~以下開始是Java版的原創內容.
說實話,上面這個例子也算直觀到了極限,乃至于不開OD也能直接通過反匯編猜出觸發點......
反匯編截圖如下:

注意看,所有call附近都能找到技能名的中文字符串|||,要是網游也都寫成這樣就好了~~~
當然這無關緊要,最主要的講解目的上述文章已經足夠實現了.但是,文章中卻有些地方沒有點到,我在此略微做下補充.
客觀上講,這篇找CALL入門的作者似乎是有意沒有說全,尤其是關于寄存器中的數值部分.我們都知道,內存中的數據并不是絕對恒定的,大多數時候寄存器顯示的實際上是可變值,只是某些情況下某些數值相對固定于本機而已.比如轉載的例子中,作者獲得的數值為005D1FE4,而在我自家機器上卻是00D52070,如果強行在我機器上注入該示例作者的數值,則會引發如下圖事件.

點了一次回城,沒想到卻讓測試程序崩潰回老家結婚去了|||
為什么呢?原因很簡單,在不同機器上,寄存器中EAX這個值是不同的,數據錯誤當然會造成程序異常.
但是,我們總不可能分別為每一個用戶都訂制程序吧?即便我們把源碼給出去讓用戶自行修改后編譯,估計一般用戶也不會(^^).所以這時,就需要用其它手段來獲得這個變化的EAX數值.
要找出測試程序在不同機器上的EAX值并不難.對于我們這代人而言,以前大多曾干過類似的事情.而說到我第一次干這種事,還是在智冠剛出單機版金庸群俠轉時.當時我用DOS下某游戲修改器調十級野球拳,卻幾乎不會用,僅僅是看書改的......到了現今,修改單機游戲各項參數對我們來講已經是再簡單不過的一件事情,用游戲修改工具我們可以輕松檢索出游戲中的各種數據并使之變化為我們的滿意結果.(據說有部分神仙級網游,也可以直接用修改器修改,服務器照認不誤.另外我承認N年前玩網金建號時偶用修改大師改初始根骨了- -)
好了,說到這里大家應該都知道我想說什么了,我們很清楚游戲中的數值并非必須依照編碼變化,人為也可以加以修改的.但是,我們的修改的數據,都改到什么地方去了呢?實際上,在修改游戲時,和數據項始終對應的,就是地址項.數值可以變,但是保存數值的地址卻絕對沒法變.無論我們將單機金庸中的生命金錢武功悟性等數字項改成多高或多低,也始終不能令游戲主角的攻擊力變成[葵花寶典]這組字符串后,還令游戲正常運行(實際上可以做到的,但就不是單純的修改數值了).那么,對于我們已經能得到的本機EAX數值,能不能找到存放這個數值的地址呢?---答案是肯定的.
這里我們使用Cheat Engine來附加[游戲找CALL練習實例one]進程進行(用游戲修改大師,金山游俠等同樣能找到,CE功能更多而已),而后以16進搜索我機器上的EAX數值[00D52070](不固定,都去找自己機器上的去~),以精確方式進行查找.
這時,CE截取到數值如下圖:

我們可以清楚地看到地址與數值的對應關系,在本次操作中包含有[00D52070]的地址有40個,但究竟那個是我們需要的呢?
修改過游戲的都知道,此刻只需耐心多操作幾次,觀察地址中數值的變化,總會有一部分數據被剃掉,而另一些卻始終存在.最后,我們可以發現,頂頭的00456D68這個地址,就是我們所需要的.
這時,我們在Java程序中只要用LocalOS中OSProcess類下的readProcessMemory讀取練習程度的進程內存中的00456D68地址。
即使用:
- OSProcess.readProcessMemory(pid, 0x456D68)
要在程序找到一個數值的變化規律是不容易的事情,但是找一個地址卻是相對容易的,只要游戲不更新,這個地址就是固定的,有了這個通用的地址,我們的程序就可以運行在不同配置的機器之上.
另外還有一點需要說明的是,bp send命令觸發的斷點僅在OD截獲程序發包時啟動,這點找CALL這篇文章沒有提及,這里補充一下.
現在必要的條件已經都具備了,我來用Java編寫一個Java匯編的演示類,以演示代碼注入方式觸發目標程序CALL.
- package org.loon.test.os;
- /**
- * Copyright 2008
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- *
- * @project loonframework
- * @author chenpeng
- * @email:ceponline@yahoo.com.cn
- * @version 0.1
- */
- import java.awt.Dimension;
- import javax.swing.JButton;
- import java.awt.Rectangle;
- import java.awt.event.WindowAdapter;
- import java.awt.event.WindowEvent;
- import java.awt.EventQueue;
- import java.awt.SystemColor;
- import java.awt.Color;
- import javax.swing.JOptionPane;
- import javax.swing.JPanel;
- import javax.swing.JFrame;
- import javax.swing.JTextField;
- import javax.swing.JLabel;
- import org.loon.framework.os.ASM;
- import org.loon.framework.os.OSProcess;
- public class TestCallForm extends JFrame {
- private static final long serialVersionUID = 1L;
- private JPanel jContentPane = null;
- private JButton btnHP = null;
- private JButton btnHome = null;
- private JButton btnBaseEax = null;
- private JButton btnIce = null;
- private JButton btnFire = null;
- private JButton btnSP = null;
- private JTextField txtIntPtr = null;
- private JLabel jLabel = null;
- public TestCallForm() {
- super();
- initialize();
- }
- private void initialize() {
- this.setResizable(false);
- this.setSize(238, 315);
- this.setContentPane(getJContentPane());
- this.setTitle("Java外掛開發入門示例");
- this.setLocationRelativeTo(null);
- this.addWindowListener(new WindowAdapter() {
- public void windowClosing(WindowEvent e) {
- System.exit(0);
- }
- });
- }
- private JPanel getJContentPane() {
- if (jContentPane == null) {
- jLabel = new JLabel();
- jLabel.setBounds(new Rectangle(30, 20, 180, 30));
- jLabel.setForeground(Color.white);
- jLabel.setText("寄存器EAX值(針對本機環境)");
- jContentPane = new JPanel();
- jContentPane.setLayout(null);
- jContentPane.setSize(new Dimension(236, 241));
- jContentPane.setBackground(SystemColor.activeCaption);
- jContentPane.add(getBtnHP(), null);
- jContentPane.add(getBtnHome(), null);
- jContentPane.add(getBaseIntPtr(), null);
- jContentPane.add(getBtnIce(), null);
- jContentPane.add(getBtnFire(), null);
- jContentPane.add(getBtnSP(), null);
- jContentPane.add(getTxtIntPtr(), null);
- jContentPane.add(jLabel, null);
- }
- return jContentPane;
- }
- private JButton getBtnHP() {
- if (btnHP == null) {
- btnHP = new JButton();
- btnHP.setBounds(new Rectangle(15, 106, 95, 30));
- btnHP.setText("吃血");
- btnHP.addMouseListener(new java.awt.event.MouseAdapter() {
- public void mouseClicked(java.awt.event.MouseEvent e) {
- clickEvent("hp");
- }
- });
- }
- return btnHP;
- }
- private JButton getBtnHome() {
- if (btnHome == null) {
- btnHome = new JButton();
- btnHome.setBounds(new Rectangle(15, 195, 200, 30));
- btnHome.setText("回城");
- btnHome.addMouseListener(new java.awt.event.MouseAdapter() {
- public void mouseClicked(java.awt.event.MouseEvent e) {
- clickEvent("home");
- }
- });
- }
- return btnHome;
- }
- private JButton getBtnIce() {
- if (btnIce == null) {
- btnIce = new JButton();
- btnIce.setBounds(new Rectangle(120, 150, 95, 30));
- btnIce.setText("冰系魔法");
- btnIce.addMouseListener(new java.awt.event.MouseAdapter() {
- public void mouseClicked(java.awt.event.MouseEvent e) {
- clickEvent("ice");
- }
- });
- }
- return btnIce;
- }
- private JButton getBtnFire() {
- if (btnFire == null) {
- btnFire = new JButton();
- btnFire.setBounds(new Rectangle(15, 150, 95, 30));
- btnFire.setText("火系魔法");
- btnFire.addMouseListener(new java.awt.event.MouseAdapter() {
- public void mouseClicked(java.awt.event.MouseEvent e) {
- clickEvent("fire");
- }
- });
- }
- return btnFire;
- }
- private JButton getBtnSP() {
- if (btnSP == null) {
- btnSP = new JButton();
- btnSP.setBounds(new Rectangle(120, 106, 95, 30));
- btnSP.setText("加藍");
- btnSP.addMouseListener(new java.awt.event.MouseAdapter() {
- public void mouseClicked(java.awt.event.MouseEvent e) {
- clickEvent("sp");
- }
- });
- }
- return btnSP;
- }
- private JButton getBaseIntPtr() {
- if (btnBaseEax == null) {
- btnBaseEax = new JButton();
- btnBaseEax.setBounds(new Rectangle(15, 235, 200, 30));
- btnBaseEax.setText("獲得本機EAX數值");
- btnBaseEax.addMouseListener(new java.awt.event.MouseAdapter() {
- public void mouseClicked(java.awt.event.MouseEvent e) {
- clickEvent("find");
- }
- });
- }
- return btnBaseEax;
- }
- private JTextField getTxtIntPtr() {
- if (txtIntPtr == null) {
- txtIntPtr = new JTextField();
- txtIntPtr.setBounds(new Rectangle(18, 57, 199, 30));
- txtIntPtr.setText("00D52070");
- }
- return txtIntPtr;
- }
- /**
- * 觸發事件
- *
- * @param eventName
- */
- private void clickEvent(final String eventName) {
- int pid = OSProcess.findWindowProcessId("TForm1", "游戲找CALL練習實例one");
- if (pid == 0) {
- JOptionPane.showMessageDialog(this, "您的游戲程序尚未啟動,外掛無法加載!");
- return;
- }
- int eaxPtr = 0;
- try {
- eaxPtr = ASM.getHexStringToInt(this.txtIntPtr.getText().trim());
- } catch (Exception ex) {
- JOptionPane.showMessageDialog(this, "寄存器數值設定格式有誤,外掛無法加載!");
- return;
- }
- // 從基址獲取寄存器中eax數值
- if ("find".equalsIgnoreCase(eventName)) {
- this.txtIntPtr.setText(OSProcess.readProcessMemory(pid, 0x456D68));
- }
- // 實例化asm類以進行java與匯編混合操作
- ASM asm = new ASM();
- // 保存所有寄存器,即全部進棧
- asm._PUSHAD();
- // 示例程序執行時,目標寄存器eax中的必備數值(PS:在我的機器上是00D52070,
- // 而找Call測試程序作者提供的是00D51FE4,請自行查找。錯誤時目標程序將崩潰。)
- asm._MOV_EAX(eaxPtr);
- // 吃紅
- if ("hp".equalsIgnoreCase(eventName)) {
- asm._MOV_EDX(0x453028);
- asm._CALL(0x452E98);
- }
- // 吃藍
- else if ("sp".equalsIgnoreCase(eventName)) {
- asm._MOV_EDX(0x453040);
- asm._CALL(0x452E98);
- }
- // 火系魔法
- else if ("fire".equalsIgnoreCase(eventName)) {
- asm._MOV_ECX(0x45309C);
- asm._MOV_EDX(2);
- asm._CALL(0x452DF8);
- // 冰系魔法
- } else if ("ice".equalsIgnoreCase(eventName)) {
- asm._MOV_ECX(0x45307C);
- asm._MOV_EDX(1);
- asm._CALL(0x452DF8);
- }
- // 回城
- else if ("home".equalsIgnoreCase(eventName)) {
- asm._MOV_EDX(0x45305C);
- asm._CALL(0x452E98);
- }
- // 還原所有寄存器,即全部出棧
- asm._POPAD();
- // 結尾標記,操作開始執行
- asm._RET();
- // 要求進行代碼注入的進程id
- asm.doInject(pid);
- }
- public static void main(String[] args) {
- EventQueue.invokeLater(new Runnable() {
- public void run() {
- TestCallForm callForm = new TestCallForm();
- callForm.setVisible(true);
- }
- });
- }
- }
現在我們執行代碼,可以見到結果如下圖:

最后,再額外補充兩點:
一,示例程序和真正的CALL外掛開發雖然原理上一樣,工作量卻是天差地別的,時間不充裕者請不要輕易嘗試--|||
二,這個示例僅僅演示了localos的一部分功能,比如dll注入的接口在其中也提供了, 有興趣者可以嘗試一下,但要注意權限問題.
程序源碼及示例下載地址:http://code.google.com/p/greenvm/downloads/list (暫時先丟這里,源碼在jar內)
OD下載地址:http://download.csdn.net/source/940795
PS:由于本例中有些敏感API的調用,運行時殺軟對Javaw.exe報警請不要少見多怪...下個版本爭取干掉殺軟^^
浙公網安備 33010602011771號