帝国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文件操作的代码请联系我获取:
1 package form; 2 3 import java.awt.Dimension; 4 import java.awt.Toolkit; 5 import java.awt.event.ActionEvent; 6 import java.awt.event.ActionListener; 7 import java.util.Hashtable; 8 import java.util.List; 9 10 import javax.swing.JButton; 11 import javax.swing.JComboBox; 12 import javax.swing.JDialog; 13 import javax.swing.JLabel; 14 import javax.swing.JOptionPane; 15 import javax.swing.JScrollPane; 16 import javax.swing.JTextArea; 17 import javax.swing.JTextField; 18 import javax.swing.UIManager; 19 20 import test.ViewMethodCall; 21 22 /** 23 * 24 * @author RyanShaw 25 */ 26 public class FrmMain extends JDialog implements ActionListener { 27 28 private static final long serialVersionUID = -8049035809432056277L; 29 30 private boolean debug = false; 31 32 33 /** 34 * 调试寻找数据类型提示 35 */ 36 private JLabel lblFindType; 37 /** 38 * 调试寻找的数据 39 */ 40 private JTextField txtFindValue; 41 /** 42 * 调试寻找数据提示 43 */ 44 private JLabel lblFindValue; 45 /** 46 * 调试寻找的数据类型 47 */ 48 private JComboBox comFindType; 49 50 /** 51 * 调试按钮 52 */ 53 private JButton btnDbg; 54 55 /** 56 * 出现寻找数据的方法调用 57 */ 58 private JTextArea txtMethodCalls; 59 /** 60 * 数据方法调用滚动支持 61 */ 62 private JScrollPane spMethodCalls; 63 /** 64 * 整个调试流程里方法调用的顺序 65 */ 66 private JTextArea txtMethodTrace; 67 /** 68 * 方法调用流程滚动支持 69 */ 70 private JScrollPane spMethodTrace; 71 72 public FrmMain() { 73 setTitle("帝国OL注入式调试工具"); 74 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 75 setSize(640,360); 76 setResizable(false); 77 setLayout(null); 78 79 // 设置窗体居中 80 Toolkit kit = Toolkit.getDefaultToolkit(); 81 Dimension screenSize = kit.getScreenSize(); 82 int screenWidth = screenSize.width; 83 int screenHeight = screenSize.height; 84 int windowWidth = this.getWidth(); 85 int windowHeight = this.getHeight(); 86 setLocation(screenWidth / 2 - windowWidth / 2, screenHeight / 2 87 - windowHeight / 2); 88 89 initComponents(); 90 } 91 92 private void initComponents() { 93 lblFindType = new JLabel("数据类型:"); 94 lblFindType.setSize(80,24); 95 lblFindType.setLocation(28, 18); 96 97 comFindType = new JComboBox(new String[]{"数字","字串"}); 98 comFindType.setSize(80, 24); 99 comFindType.setLocation(100, 18); 100 101 lblFindValue = new JLabel("寻找数值:"); 102 lblFindValue.setSize(80, 24); 103 lblFindValue.setLocation(200, 18); 104 105 txtFindValue = new JTextField(); 106 txtFindValue.setSize(145, 24); 107 txtFindValue.setLocation(260, 18); 108 109 btnDbg = new JButton("启动调试"); 110 btnDbg.setSize(80,24); 111 btnDbg.setLocation(28, 48); 112 btnDbg.addActionListener(this); 113 114 txtMethodCalls = new JTextArea(); 115 txtMethodCalls.setSize(568, 100); 116 txtMethodCalls.setLocation(28, 80); 117 //txtMethodCalls.setEditable(false); 118 //txtMethodCalls.setLineWrap(true); 119 //txtMethodCalls.setWrapStyleWord(true); 120 121 spMethodCalls = new JScrollPane(); 122 spMethodCalls.setViewportView(txtMethodCalls); 123 spMethodCalls.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 124 spMethodCalls.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 125 spMethodCalls.setSize(568, 100); 126 spMethodCalls.setLocation(28, 80); 127 128 txtMethodTrace = new JTextArea(); 129 txtMethodTrace.setSize(568, 100); 130 txtMethodTrace.setLocation(28, 200); 131 //txtMethodTrace.setEditable(false); 132 //txtMethodTrace.setLineWrap(true); 133 //txtMethodTrace.setWrapStyleWord(true); 134 135 spMethodTrace = new JScrollPane(); 136 spMethodTrace.setViewportView(txtMethodTrace); 137 spMethodTrace.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 138 spMethodTrace.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 139 spMethodTrace.setSize(568, 100); 140 spMethodTrace.setLocation(28, 200); 141 142 add(lblFindType); 143 add(comFindType); 144 add(lblFindValue); 145 add(txtFindValue); 146 add(btnDbg); 147 //add(txtMethodCalls); 148 //add(txtMethodTrace); 149 add(spMethodCalls); 150 add(spMethodTrace); 151 } 152 153 @Override 154 public void actionPerformed(ActionEvent e) { 155 debug = !debug; 156 if(debug) { 157 btnDbg.setText("停止调试"); 158 switch(comFindType.getSelectedIndex()){ 159 case 0: 160 String val = txtFindValue.getText().trim(); 161 if(val.isEmpty()) return; 162 try{ 163 int intval = Integer.parseInt(val); 164 ViewMethodCall.enableDebug(ViewMethodCall.DEBUG_TYPE_INT, intval); 165 }catch(Exception ex){ 166 JOptionPane.showMessageDialog(this, ex.getMessage()); 167 debug = !debug; 168 btnDbg.setText("启动调试"); 169 } 170 break; 171 case 1: 172 String val1 = txtFindValue.getText().trim(); 173 if(val1.isEmpty()) return; 174 ViewMethodCall.enableDebug(ViewMethodCall.DEBUG_TYPE_STR, val1); 175 } 176 }else{ 177 btnDbg.setText("启动调试"); 178 ViewMethodCall.disableDebug(); 179 txtMethodCalls.setText(""); 180 txtMethodTrace.setText(""); 181 Hashtable<String,Integer> callcounter = ViewMethodCall.getDebugResult(); 182 for(String methodname : callcounter.keySet()){ 183 txtMethodCalls.append(methodname); 184 txtMethodCalls.append(" "); 185 txtMethodCalls.append(callcounter.get(methodname).toString()); 186 txtMethodCalls.append(" "); 187 } 188 List<String> calltrace = ViewMethodCall.getMethodCallStackTrace(); 189 for(String tracele : calltrace){ 190 txtMethodTrace.append(tracele); 191 txtMethodTrace.append(" "); 192 } 193 } 194 } 195 196 /*public static void main(String[] args) { 197 java.awt.EventQueue.invokeLater(new Runnable() { 198 public void run() { 199 try { 200 UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); 201 } catch (Exception e) { 202 203 } 204 new FrmMain().setVisible(true); 205 } 206 }); 207 }*/ 208 }
1 package test; 2 3 import java.util.ArrayList; 4 import java.util.Hashtable; 5 import java.util.List; 6 7 import javax.swing.UIManager; 8 9 import form.FrmMain; 10 11 public class ViewMethodCall { 12 public static final int DEBUG_TYPE_INT = 1; 13 public static final int DEBUG_TYPE_STR = 0; 14 15 private Hashtable<String, Integer> callcounter = new Hashtable<String, Integer>(); 16 private List<String> callStackTrace = new ArrayList<String>(); 17 private boolean debugenable = false; 18 private int debugType = DEBUG_TYPE_INT; 19 private String debugStr = null; 20 private int debugInt = -1; 21 private static ViewMethodCall me = new ViewMethodCall(); 22 private ViewMethodCall(){ 23 java.awt.EventQueue.invokeLater(new Runnable() { 24 public void run() { 25 try { 26 UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); 27 } catch (Exception e) { 28 29 } 30 new FrmMain().setVisible(true); 31 } 32 }); 33 } 34 static {} 35 36 public static void beCall(String methodName, Object[] params) { 37 if(!me.debugenable) return; 38 me.callStackTrace.add(methodName); 39 switch(me.debugType){ 40 case DEBUG_TYPE_INT: 41 for(Object obj : params) 42 if(obj != null && obj instanceof Integer && (Integer)obj == me.debugInt) 43 if(me.callcounter.contains(methodName)) 44 me.callcounter.put(methodName, me.callcounter.get(methodName) + 1); 45 else 46 me.callcounter.put(methodName, 1); 47 break; 48 case DEBUG_TYPE_STR: 49 for(Object obj : params) 50 if(obj != null && obj instanceof String && obj.equals(me.debugStr)) 51 if(me.callcounter.contains(methodName)) 52 me.callcounter.put(methodName, me.callcounter.get(methodName) + 1); 53 else 54 me.callcounter.put(methodName, 1); 55 break; 56 } 57 } 58 59 public static void enableDebug(int type, Object value){ 60 me.debugenable = true; 61 me.callcounter.clear(); 62 me.callStackTrace.clear(); 63 me.debugType = type; 64 switch(type){ 65 case DEBUG_TYPE_INT: 66 me.debugInt = (Integer) value; 67 break; 68 case DEBUG_TYPE_STR: 69 me.debugStr = (String) value; 70 break; 71 } 72 } 73 74 public static void disableDebug(){ 75 me.debugenable = false; 76 } 77 78 public static Hashtable<String, Integer> getDebugResult(){ 79 return me.callcounter; 80 } 81 82 public static List<String> getMethodCallStackTrace(){ 83 return me.callStackTrace; 84 } 85 }
如果看不懂请不要深究,不然到时候查水表被多带走一个&=*
(最后编辑时间2013-11-09 16:24:31)