如何打造一款java遊戲的外掛?javaassist幫你找回飛機大戰遊戲開掛般的感覺...
java序列化機制為java對象的存儲和傳輸提供了一種高效的方案,序列化是將內存中的java對象轉為可見的字節流,而對應的反序列化操作可以從字節流恢復出該java對象,但java對象序列化的便捷和開放性同時也造成了安全問題。反序列化漏洞是java web應用中一種較為主流的漏洞類型,通過向漏洞接口傳入惡意反序列化對象,從而在執行java類反序列化操作時,readObject函數可能導致非預期的代碼/命令執行。
ClassLoader.defineClass是反序列化漏洞代碼中的常見函數,它具有從字節碼byte[]還原出一個Class對象的能力。如果輸入的類字節碼byte[]可控,則defineClass就會根據字節碼構造出可控Class對象,如果該Class對象被初始化,例如對象的newInstance()方法被調用,那麼可控類的static代碼塊就會執行,導致任意代碼執行。
javaassist是一個處理字節碼的類庫,能夠動態的修改class中的字節碼,在產生自定義類對象的字節碼時,非常方便,例如如下代碼:
ClassPool pool = ClassPool.getDefault(); // 獲取類池CtClass payload = pool.makeClass("EvilClass"); // 創建自定義類EvilClass類 payload.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); //設置類的父類為AbstractTransletpayload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //設置類的靜態代碼塊為運行calc命令byte[] evilClass = payload.toBytecode(); // 獲取自定義類的字節碼代碼中創建了EvilClass類,並且在類定義的靜態代碼塊中調用了calc命令。javaassist不僅可以創建新的類及屬性,並且具有改變運行時類屬性的能力,基於這種修改java應用運行時邏輯的能力,可以實現簡單的遊戲外掛。接下來我們嘗試使用javaassist打造一款遊戲外掛,從最經典的飛機大戰遊戲入手。
從github上找一款畫質較高的遊戲:
運行起來大概是這個畫面:
遊戲運行中發現了一個小缺點,就是飛機移動速度比較慢,使得遊戲玩家缺少了一絲緊張感。使用jd-gui查看遊戲jar的代碼發現,按住shift鍵可以加速。
16即為shift的鍵碼值,按下此鍵可以設置this.accelerate變量的值為true。同時在MyPlane類的move函數中如果檢測到this.accelerate值為true,那麼就增加飛機移動的步長,實現加速。但是鬆開shift鍵就會取消對加速屬性的設置:
因此需要按住shift鍵才能實現保持飛機的加速狀態,多餘的按鍵操作降低了玩家的操作手感,並且敵機也會提高移動速度,那麼如何改進呢?如果可以修改應用運行時的move函數,在函數中加入自定義修改步長的代碼,就可以實現了。幸好在java 1.5之後引入了java代理,這是一種可以對java應用運行時環境進行操作的方法,通過以下代碼可以實現:
int pid = 19344; VirtualMachine vm = VirtualMachine.attach(String.valueOf(pid));String agentJarPath = "javaAgent.jar" vm.loadAgent(agentJarPath); vm.detach();使用VirtualMachine.attach函數附加到進程號為pid的進程jvm上,java進程id可以通過jps -l命令查看:
然後通過vm.loadAgent加載java agent jar文件,其中java agent jar文件中包含了修改程序運行時信息的代碼。
1、在manifest中指定Agent-Class屬性,其值為代理類全路徑。本例中為com.agent.plane
2、代理類需要提供public static void agentmain(String args, Instrumentation inst)或public static void agentmain(String args)方法。並且在二者同時存在時以前者優先。
本例中agentmain代碼如下:
private static String className = "com.xiaoqing.game.MyPlane"; private static String methodName = "move";
public static void agentmain(String agentArgs, Instrumentation instrumentation) { System.out.println("agentmain"); instrumentation.addTransformer(new TestTransformer(className, methodName),true); try { Class[] loadedClass = instrumentation.getAllLoadedClasses(); for (Class c : loadedClass) { if (c.getName().equals(className)) { System.out.println("---find!!!---"); Method[] methods = c.getDeclaredMethods(); for(Method method : methods) {System.out.println(method.getName());} instrumentation.retransformClasses(c); } } } catch (Exception e) { } }instrumentation.addTransformer()函數的參數即為修改類屬性的自定義transformer,該transformer需要繼承自ClassFileTransformer。transformer代碼如下所示:
public class TestTransformer implements ClassFileTransformer { private String targetClassName; private String targetVMClassName; private String targetMethodName;
public TestTransformer(String className, String methodName) { this.targetVMClassName = new String(className).replaceAll("\\.", "\\/"); this.targetMethodName = methodName; this.targetClassName = className; }
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (!className.equals(targetVMClassName)) { System.out.println("not do transform:"+className); return classfileBuffer; } try { System.out.println("do transform:"+className); ClassPool classPool=null; try { classPool = ClassPool.getDefault(); } catch (Throwable e) { System.err.println("出錯了:" + e.getMessage()); e.printStackTrace(); } CtClass cls = classPool.get(this.targetClassName); CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName); System.out.println(ctMethod.getName()); ctMethod.insertAt(92, "{ if (this.getClass().getSimpleName().equals(\"MyPlane\")) { step = 10.0; } }"); return cls.toBytecode(); } catch (Exception e) { e.printStackTrace(); System.exit(0); } return classfileBuffer; }}該類中需要實現的主要方法為transform,類加載時會執行該方法,其中參數 classfileBuffer為類原始字節碼,返回值為目標字節碼。
CtClass cls = classPool.get(this.targetClassName);CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);
可以看到通過以上代碼獲取了com.xiaoqing.game.MyPlane類的move方法對應的CtMethod對象,因為我們需要修改move方法,增加飛機的移動步長。
然後使用CtMethod類的insertAt方法插入修改步長的代碼:
ctMethod.insertAt(92, "{ if (this.getClass().getSimpleName().equals(\"MyPlane\")) { step = 10.0; } }");在該方法的92行插入了修改步長的邏輯,並且通過this.getClass().getSimpleName()判斷當前類名稱是否為MyPlane,即為我方戰機,如果是則修改移動速度,實現外掛的效果。修改後的move方法如下:
public void move() { if(accelerate){ step=Constant.PLANE_STEP+2; } else{ step=Constant.PLANE_STEP; } if (this.getClass().getSimpleName().equals("MyPlane")) { step = 10.0; } if(up){ moveY(getY()-step); } if(down){ moveY(getY()+step); } if(right){ moveX(getX()+step); } if(left){ moveX(getX()-step); } }加了攻速後的運行效果如下:
查看ctMethod.insertAt()的代碼實現,可以看到核心邏輯位於javaassist.compiler.Javac類的compileStmnt函數中,該函數將傳入的java語句解析為java語法的語句對象即Stmnt變量,並使用gen的atStmnt方法處理該對象生成字節碼:public void compileStmnt(String src) throws CompileError { Parser p = new Parser(new Lex(src)); SymbolTable stb = new SymbolTable(this.stable);
while(p.hasMore()) { Stmnt s = p.parseStatement(stb); if (s != null) { s.accept(this.gen); } } }其中atStmnt方法如下,this.bytecode即為自定義語句對應的字節碼,此處根據java語句的表達式類型,向bytecode中加入了88這個字節。轉載聲明:原創不易,如需轉載,請註明來源「下呀下」
歡迎加入網絡安全文檔免費分享大家庭!
「下呀下」,做一個有價值的公眾號。網絡安全標準規範、政策法規、趨勢動態、技術乾貨,海量優質資料免費分享!
加群方式:請先加管理員微信:xiayaxia01,備註:進群,管理員同意後即可邀您進群(加群人數較多,若未及時回復請見諒)。
關注「下呀下」,進入「文檔搜索」中心,下載更多網絡安全資料