帝國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("\t"); 185 txtMethodCalls.append(callcounter.get(methodname).toString()); 186 txtMethodCalls.append("\n"); 187 } 188 List<String> calltrace = ViewMethodCall.getMethodCallStackTrace(); 189 for(String tracele : calltrace){ 190 txtMethodTrace.append(tracele); 191 txtMethodTrace.append("\n"); 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)