我是怎样使用javassist将代码注入到帝国OL并进行调试的
帝国OL是拉阔一款手机网络游戏(腾讯也有代理),我在中学时代玩儿过。
帝国OL还维护着KJava版本游戏客户端,这意味着我们可以在PC端使用模拟器玩儿游戏。
不过这篇文章我主要是关注如何通过代码注入拦截其客户端代码调用并测试其方法内容的。
声明:本人并没有任何对于帝国OL游戏代码的逆向工程、改编、分发或从中获利的行为,本篇文章的执行数据和工作流程及所有言论和工作都是为了学习之用,如果文章中的内容侵犯了任何个人或集体的利益,请联系我关闭本篇文章。在您观看此篇文章时则视为您已经默认了此文章为学习性质,否则请勿继续向下观看。
帝国OL游戏客户端代码是混淆加密过的,在最新版客户端中(截至时间2013-11-09,客户端适用手机型号N5800)共有一个启动类和149个方法提供类,和一般混淆结果一样,我们如果阅读class文件或通过反编译工具查看会发现它类的类名、方法名和全局变量名都是诸如a/aa/ba/bc等无意义、无规律的名称,而且加密过后的代码是无法从反编译结果直接再次编译的。
那我们还能对游戏运行流程进行调试吗?比如监控游戏方法调用?
答案是肯定的。我们可以使用Java的一个第三方类库,专门用于对class文件进行操作。
我先将所需的工具和jar包发上来,大家可以下载或搜索下载。
Javassist:对Java字节码文件进行操作的类库,看起来和Java自己的reflection API很像,不过在对class文件进行操作时功能更加强大。
Kemulator:这个东西大家肯定不陌生,最常用的就是在电脑上玩儿手机游戏。我推荐0.9.4版本,比较稳定。
jd-gui:Java字节码文件反编译工具,使用很方便。不过对于双重循环有时翻译不出来……我们主要用于查看一点信息
好,我们现在理一下思路:Javassist有一个功能,就是在某个方法执行之前或之后插入代码,而且还能拿到此次方法调用的所有参数类型和值。
(注:此方法必须有方法体,且不是private的,当然,本身private方法我们也可以变成public,但为了保留原来代码的完整性,我在这次操作里没有改动)
那我们可以这样:向游戏函数中每个方法中插入一条语句,这条语句很简单,就是为了调用我们的某个函数,并把参数传递过来,我们进行操作。
就像这样:
public static void beCall(String methodName, Object[] params) {
}
这个beCall函数就是要插入到游戏原来代码方法中的内容。beCall函数接收两个参数,第一个我定义为方法的签名,包括方法名和参数类型,第二个参数是方法被调用时传递的参数。
下面是我注入后的代码:
从上图的情况来看,我们对class的操作是成功的,我们只需要把ViewMethodCall拷入帝国OL客户端jar包即可。
(ViewMethodCall即我上文的类,beCall是公开的静态函数)
由于此篇文章涉及到某些政策问题,我就不将详细步骤贴出来了。
下面是我最后实现的功能:
我可以用左侧的窗体进行调试,调试主要在beCall函数接收到的参数值中寻找,比如下图我就是在寻找当游戏角色坐标改变时游戏内部的方法调用过程:
然后我立即移动至24,当移动过程完成立即点击“停止调试”,因为在调试过程中的计算是十分占用计算机运算效率和内存的:
由于政策因素(-_- 如果我被查水表大家为我默哀),我只贴出两个关键类的代码,我对于class文件操作的代码请联系我获取:
package form; import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Hashtable;
import java.util.List; import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.UIManager; import test.ViewMethodCall; /**
*
* @author RyanShaw
*/
public class FrmMain extends JDialog implements ActionListener { private static final long serialVersionUID = -8049035809432056277L; private boolean debug = false; /**
* 调试寻找数据类型提示
*/
private JLabel lblFindType;
/**
* 调试寻找的数据
*/
private JTextField txtFindValue;
/**
* 调试寻找数据提示
*/
private JLabel lblFindValue;
/**
* 调试寻找的数据类型
*/
private JComboBox comFindType; /**
* 调试按钮
*/
private JButton btnDbg; /**
* 出现寻找数据的方法调用
*/
private JTextArea txtMethodCalls;
/**
* 数据方法调用滚动支持
*/
private JScrollPane spMethodCalls;
/**
* 整个调试流程里方法调用的顺序
*/
private JTextArea txtMethodTrace;
/**
* 方法调用流程滚动支持
*/
private JScrollPane spMethodTrace; public FrmMain() {
setTitle("帝国OL注入式调试工具");
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
setSize(640,360);
setResizable(false);
setLayout(null); // 设置窗体居中
Toolkit kit = Toolkit.getDefaultToolkit();
Dimension screenSize = kit.getScreenSize();
int screenWidth = screenSize.width;
int screenHeight = screenSize.height;
int windowWidth = this.getWidth();
int windowHeight = this.getHeight();
setLocation(screenWidth / 2 - windowWidth / 2, screenHeight / 2
- windowHeight / 2); initComponents();
} private void initComponents() {
lblFindType = new JLabel("数据类型:");
lblFindType.setSize(80,24);
lblFindType.setLocation(28, 18); comFindType = new JComboBox(new String[]{"数字","字串"});
comFindType.setSize(80, 24);
comFindType.setLocation(100, 18); lblFindValue = new JLabel("寻找数值:");
lblFindValue.setSize(80, 24);
lblFindValue.setLocation(200, 18); txtFindValue = new JTextField();
txtFindValue.setSize(145, 24);
txtFindValue.setLocation(260, 18); btnDbg = new JButton("启动调试");
btnDbg.setSize(80,24);
btnDbg.setLocation(28, 48);
btnDbg.addActionListener(this); txtMethodCalls = new JTextArea();
txtMethodCalls.setSize(568, 100);
txtMethodCalls.setLocation(28, 80);
//txtMethodCalls.setEditable(false);
//txtMethodCalls.setLineWrap(true);
//txtMethodCalls.setWrapStyleWord(true); spMethodCalls = new JScrollPane();
spMethodCalls.setViewportView(txtMethodCalls);
spMethodCalls.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
spMethodCalls.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
spMethodCalls.setSize(568, 100);
spMethodCalls.setLocation(28, 80); txtMethodTrace = new JTextArea();
txtMethodTrace.setSize(568, 100);
txtMethodTrace.setLocation(28, 200);
//txtMethodTrace.setEditable(false);
//txtMethodTrace.setLineWrap(true);
//txtMethodTrace.setWrapStyleWord(true); spMethodTrace = new JScrollPane();
spMethodTrace.setViewportView(txtMethodTrace);
spMethodTrace.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
spMethodTrace.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
spMethodTrace.setSize(568, 100);
spMethodTrace.setLocation(28, 200); add(lblFindType);
add(comFindType);
add(lblFindValue);
add(txtFindValue);
add(btnDbg);
//add(txtMethodCalls);
//add(txtMethodTrace);
add(spMethodCalls);
add(spMethodTrace);
} @Override
public void actionPerformed(ActionEvent e) {
debug = !debug;
if(debug) {
btnDbg.setText("停止调试");
switch(comFindType.getSelectedIndex()){
case 0:
String val = txtFindValue.getText().trim();
if(val.isEmpty()) return;
try{
int intval = Integer.parseInt(val);
ViewMethodCall.enableDebug(ViewMethodCall.DEBUG_TYPE_INT, intval);
}catch(Exception ex){
JOptionPane.showMessageDialog(this, ex.getMessage());
debug = !debug;
btnDbg.setText("启动调试");
}
break;
case 1:
String val1 = txtFindValue.getText().trim();
if(val1.isEmpty()) return;
ViewMethodCall.enableDebug(ViewMethodCall.DEBUG_TYPE_STR, val1);
}
}else{
btnDbg.setText("启动调试");
ViewMethodCall.disableDebug();
txtMethodCalls.setText("");
txtMethodTrace.setText("");
Hashtable<String,Integer> callcounter = ViewMethodCall.getDebugResult();
for(String methodname : callcounter.keySet()){
txtMethodCalls.append(methodname);
txtMethodCalls.append("\t");
txtMethodCalls.append(callcounter.get(methodname).toString());
txtMethodCalls.append("\n");
}
List<String> calltrace = ViewMethodCall.getMethodCallStackTrace();
for(String tracele : calltrace){
txtMethodTrace.append(tracele);
txtMethodTrace.append("\n");
}
}
} /*public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
} catch (Exception e) { }
new FrmMain().setVisible(true);
}
});
}*/
}
FrmMain
package test; import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List; import javax.swing.UIManager; import form.FrmMain; public class ViewMethodCall {
public static final int DEBUG_TYPE_INT = 1;
public static final int DEBUG_TYPE_STR = 0; private Hashtable<String, Integer> callcounter = new Hashtable<String, Integer>();
private List<String> callStackTrace = new ArrayList<String>();
private boolean debugenable = false;
private int debugType = DEBUG_TYPE_INT;
private String debugStr = null;
private int debugInt = -1;
private static ViewMethodCall me = new ViewMethodCall();
private ViewMethodCall(){
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
} catch (Exception e) { }
new FrmMain().setVisible(true);
}
});
}
static {} public static void beCall(String methodName, Object[] params) {
if(!me.debugenable) return;
me.callStackTrace.add(methodName);
switch(me.debugType){
case DEBUG_TYPE_INT:
for(Object obj : params)
if(obj != null && obj instanceof Integer && (Integer)obj == me.debugInt)
if(me.callcounter.contains(methodName))
me.callcounter.put(methodName, me.callcounter.get(methodName) + 1);
else
me.callcounter.put(methodName, 1);
break;
case DEBUG_TYPE_STR:
for(Object obj : params)
if(obj != null && obj instanceof String && obj.equals(me.debugStr))
if(me.callcounter.contains(methodName))
me.callcounter.put(methodName, me.callcounter.get(methodName) + 1);
else
me.callcounter.put(methodName, 1);
break;
}
} public static void enableDebug(int type, Object value){
me.debugenable = true;
me.callcounter.clear();
me.callStackTrace.clear();
me.debugType = type;
switch(type){
case DEBUG_TYPE_INT:
me.debugInt = (Integer) value;
break;
case DEBUG_TYPE_STR:
me.debugStr = (String) value;
break;
}
} public static void disableDebug(){
me.debugenable = false;
} public static Hashtable<String, Integer> getDebugResult(){
return me.callcounter;
} public static List<String> getMethodCallStackTrace(){
return me.callStackTrace;
}
}
ViewMethodCall
如果看不懂请不要深究,不然到时候查水表被多带走一个&=*
欢迎您移步我们的交流群,无聊的时候大家一起打发时间:
或者通过QQ与我联系:
(最后编辑时间2013-11-09 16:24:31)
我是怎样使用javassist将代码注入到帝国OL并进行调试的的更多相关文章
- 转:EasyHook远程代码注入
EasyHook远程代码注入 最近一段时间由于使用MinHook的API挂钩不稳定,经常因为挂钩地址错误而导致宿主进程崩溃.听同事介绍了一款智能强大的挂钩引擎EasyHook.它比微软的detours ...
- 32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式
32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式 一丶RadAsm的配置和使用 用了怎么长时间的命令行方式,我们发现了几个问题 1.没有代码提醒功能 2.编写代码很慢,记不住各 ...
- 阿里云提示Discuz uc.key泄露导致代码注入漏洞uc.php的解决方法
适用所有用UC整合 阿里云提示漏洞: discuz中的/api/uc.php存在代码写入漏洞,导致黑客可写入恶意代码获取uckey,.......... 漏洞名称:Discuz uc.key泄露导致代 ...
- 【原】iOS动态性(三) Method Swizzling以及AOP编程:在运行时进行代码注入
概述 今天我们主要讨论iOS runtime中的一种黑色技术,称为Method Swizzling.字面上理解Method Swizzling可能比较晦涩难懂,毕竟不是中文,不过你可以理解为“移花接木 ...
- Java链接MySQL练习题:格式化日期、性别;避免代码注入
一.查询人员名单,按序号 姓名 性格(男或女) 民族(某族) 生日(年月日)输出 import java.sql.*; import java.text.SimpleDateFormat; publi ...
- Android 反编译 代码注入之HelloWorld
为了向经典的"Hello, World"致敬,我们也从一个简单的程序开始HelloWorld.apk.当你把这个APK安装到手机上运行后,在屏幕上就显示一行文字"Hell ...
- apk反编译(4)Smali代码注入
转自 : http://blog.sina.com.cn/s/blog_5674d18801019i89.html 应用场景 Smali代码注入只能应对函数级别的移植,对于类级别的移植是无能为力的.具 ...
- 注入攻击-SQL注入和代码注入
注入攻击 OWASP将注入攻击和跨站脚本攻击(XSS)列入网络应用程序十大常见安全风险.实际上,它们会一起出现,因为 XSS 攻击依赖于注入攻击的成功.虽然这是最明显的组合关系,但是注入攻击带来的不仅 ...
- Method Swizzling以及AOP编程:在运行时进行代码注入-备用
概述 今天我们主要讨论iOS runtime中的一种黑色技术,称为Method Swizzling.字面上理解Method Swizzling可能比较晦涩难懂,毕竟不是中文,不过你可以理解为“移花接木 ...
随机推荐
- Java8 利用Lambda处理List集合循环给另外一个List赋值过滤处理
1.利用stream().forEach()循环处理List; List<String> list = Lists.newArrayList();//新建一个List 用的google提供 ...
- Android中asset文件夹和raw文件夹区别与用法
*res/raw和assets的相同点: 1.两者目录下的文件在打包后会原封不动的保存在apk包中,不会被编译成二进制. *res/raw和assets的不同点:1.res/raw中的文件会被映射到R ...
- 微软BI 之SSIS 系列 - Merge, Merge Join, Union All 合并组件的使用以及Sort 排序组件同步异步的问题
开篇介绍 SSIS Data Flow 中有几个组件可以实现不同数据源的数据合并功能,比如 Merger, Merge Join 和 Union All.它们的功能比较类似,同时也比较容易混淆,下面是 ...
- 外网IP监测上报程序(使用Poco库的SMTPClientSession发送邮件)
目录 IPReport 项目介绍 编译说明 安装使用说明 获取外网IP方式 邮件发送关键代码 IPReport 代码地址https://gitee.com/solym/IPReport 项目介绍 外网 ...
- mysql和redis的区别
一..redis和mysql的区别总结 (1)类型上 从类型上来说,mysql是关系型数据库,redis是缓存数据库 (2)作用上 mysql用于持久化的存储数据到硬盘, ...
- 基于Ubuntu部署 memcached 服务
系统要求:Ubuntu 16.04.1 LTS 64 位操作系统 安装并启动 memcached 服务 安装 memcached 使用apt-get安装 memcached sudo apt-get ...
- MySql按周,按月,按日分组统计数据
知识关键词:DATE_FORMAT select DATE_FORMAT(create_time,'%Y%u') weeks,count(caseid) count from tc_case grou ...
- CountDownLatch、CyclicBarrier及Semaphore的用法示例
一.参考blog https://www.cnblogs.com/dolphin0520/p/3920397.html 二.CountDownLatch 个人把它类比于一个持有计数的闸门,每到达这个闸 ...
- unity3d的playmaker插件使用教程,三、对象出入触发,声音播放
对象出入触发是游戏常见的情形.包含同一时候声音播放 首先建立进去区域.新建一个立方体,去掉mesh render. 而且选中 is trigger同意进入 样例里用了unity3d的第一人视角控制,可 ...
- Json返回结果为null属性不显示解决方法
返回时null属性不显示:String str = JSONObject.toJSONString(obj); 返回为null属性显示:String str = JSONObject.toJSONSt ...