俗话说隔行如隔山,感觉上是一回事,自己动手又是另一回事.这两天回家就帮亲戚家孩子做外挂,本以为很简单,结果泡广海逛看雪的,研究了三个半晚上才在今天接近凌晨时大体弄好.万幸自己一直在混软件这碗饭,并没真正的隔行,最多是"一座山上两座峰,搭个吊桥就能通"罢了.
前两天博文中提过一次,这次帮亲戚做外挂为了省事没做封包的(事实上没写过外挂,也不敢写封包的),只是利用程序自身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)
这样,就可以取得本机在CALL时需要的EAX数值了,我机器上是[00D52070]。
要在程序找到一个数值的变化规律是不容易的事情,但是找一个地址却是相对容易的,只要游戏不更新,这个地址就是固定的,有了这个通用的地址,我们的程序就可以运行在不同配置的机器之上.
另外还有一点需要说明的是,bp send命令触发的断点仅在OD截获程序发包时启动,这点找CALL这篇文章没有提及,这里补充一下.
现在必要的条件已经都具备了,我来用Java编写一个Java汇编的演示类,以演示代码注入方式触发目标程序CALL.
- package org.loon.test.os;
- 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;
- }
-
- 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;
- }
-
- if ("find".equalsIgnoreCase(eventName)) {
- this.txtIntPtr.setText(OSProcess.readProcessMemory(pid, 0x456D68));
- }
-
- ASM asm = new ASM();
-
- asm._PUSHAD();
-
-
- 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();
-
- 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报警请不要少见多怪...下个版本争取干掉杀软^^