Javassist 在逆向工程中的应用 安全研究部陈庆 关键词 :Java Javassist class 调试逆向工程摘要 :Javassist 是一种面向程序员的 Java Class 修改工具, 以库的形式封装了诸多 API 供程序员使用, 使得可以在 Java 源码级别直接修改 Java Class, 而不必直接面对 Java Bytecode 和 JVM 在 Java 逆向工程 无源码 Java 程序调试排错中,Javassist 大有用武之地 本文以几份短小精悍的完整代码演示 Javassist 的使用 一 引言 avassist 顾名思义, 是个 Java 辅助工具 按官方说法它使得字 J节码处理变得简单, 在 Java 源码级处理字节码, 给用户提供 封装好的 API 操作 class, 把中间各种繁杂的细节隐藏起来 官网是 : http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/ 上述文字还是太抽象, 本文将演示 Javassis 与逆向工程较紧密 的部分用法 如果有耐心, 建议将其自带教程完整看一遍 : http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/tutorial/ tutorial.html 二 一个假想的研究目标 target_0.java 如下, 是我们的假想敌, 也就是 Javassist 的作用 对象 它会检查提供的命令行参数, 如果满足指定检查方案, 就显示 "You are a clever boy!" 我们的目标是在 Javassist 的介入下使得无论命令行参数是啥,target_0 都显示 "You are a clever boy!" import java.security.messagedigest; class Hash private static final char[] HEXDIGITS = '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' ; private static String gethexstr byte[] in ) 49
return gethexstr messagedigest.digest) ) int len = in.length; int i; StringBuilder out = new StringBuilder len * 2 for i = 0; i < len; i++ ) catch Exception e ) throw new RuntimeException e ) out.append HEXDIGITS[ in[i] >> 4 ) & 0x0F ] out.append HEXDIGITS[ in[i] & 0x0F ] return out.tostring) public class target_0 public static String gethash String algorithm, String text ) MessageDigest messagedigest; if null == text ) return null private static boolean VerifyHash String text ) return Hash.gethash "SHA1", text ).equals "7BCD168 6FE4BCE0A06655740FE54D0C233855872" ) public static void main String[] args ) String text; try boolean exit = false; if 1!= args.length ) messagedigest = MessageDigest.getInstance algorithm System.err.println "Usage: target_0 <secret>" messagedigest.update text.getbytes) 50
else CtClass old_class = ClassPool.getDefault).get "Hash" if VerifyHash args[0] ) ) System.out.println "You are a clever boy!" old_class.detach C t M e t h o d o l d _ m e t h o d = o l d _ c l a s s. getdeclaredmethod "gethash" old_class.removemethod old_method return; CtMethod new_method = CtNewMethod.make "public static String gethash String algorithm, String text )" + $ javac -g:none target_0.java $ java -jar target_0.jar anything "" + " System.out.pr intln \" M odif ied\" " 不带调试信息编译 target_0.java, 制做 target_0.jar, 尽可能 + 符合我们最有可能碰上的那种情形 为了不过度分散注意力以及演示方便, 假设未做混淆 三 重新实现或简单修改指定 method crack_target_0_d.java 重新实现 Hash.gethash), 返回固定值, 于是 VerifyHash) 始终返回 true class crack_target_0_d public static void main String[] argv ) throws Exception " return \"7BCD1686FE4BCE0A06655740FE54D 0C233855872\" " + "", old_class old_class.addmethod new_method old_class.toclass target_0.main argv 51
$ javac -g -cp "javassist.jar;target_0.jar" crack_target_0_ d.java $ java -cp "javassist.jar;target_0.jar;." crack_target_0_d anything Modified You are a clever boy! 值得一提的是, 上述代码没有修改硬盘上的静态文件, 是将 ).insertbefore "" + " System.out.println \"Modified\" " + " return \"7BCD1686FE4BCE0A06655740FE54D 0C233855872\" " + "" Hash.class 加载到内存中再修改, 所有的改动仅仅停留在内存中, 并未写回硬盘, 可以说 crack_target_0_d 是个 Loader 如果一定要写回硬盘, 将 toclass) 改成 writefile), 但我不推荐 关于这里用到的 API, 参看 : http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/html/ old_class.toclass target_0.main argv crack_target_0_e.java 用了更省事的 insertbefore), 在 Hash.gethexstr) 入口处插入代码 class crack_target_0_e public static void main String[] argv ) throws Exception 有人可能认为在 Hash.gethash) 入口处插入同样的代码也能达到同样效果, 实际上这里有名堂 把前面的 "gethexstr" 改成 "gethash", 执行中触发 : Exception in thread "main" java.lang.classformaterror: Illegal exception table end_pc 38 in method Hash.gethashLjava/ lang/string;ljava/lang/string;)ljava/lang/string; at target_0.verifyhashunknown Source) "Hash" CtClass old_class old_class.detach = ClassPool.getDefault).get at target_0.mainunknown Source) at crack_target_0_e.maincrack_target_0_e.java) 简单点说,Hash.gethash) 中有 try/catch, 直接在方法入口插 old _ c lass.get D eclaredmethod "gethexstr " 代码, 会影响与 try/catch 相关的定位, 涉及 class 格式中 method 52
附属 "Exception table" 的修正, 看上去 insertbefore) 不会进行这种修正 与 insertbefore 类似的有 insertafter addcatch insertat 等等, 参看 : http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/html/ javassist/ctbehavior.html 四 替换对指定 method 的调用代码 crack_target_0_n.java 在 Hash.class 中搜索调用 Hash. gethexstr) 的代码, 将找到的调用代码替换成另外两条代码 import javassist.expr.*; class crack_target_0_n_expreditor extends ExprEditor p u b l i c v o i d e d i t M e t h o d C a l l e x p r ) t h r o w s CannotCompileException if expr.getclassname).equals "Hash" ) && expr.getmethodname).equals "gethexstr" ) ) expr.replace "" + " System.out.println \"Modified\" " + " $_ = \"7BCD1686FE4BCE0A06655740FE54D 0C233855872\";" + "" public class crack_target_0_n public static void main String[] argv ) throws Throwable CtClass.debugDump = "./debugdump/"; ClassPool pool = ClassPool.getDefault pool.get "Hash" ).instrument new crack_target_0_n_ ExprEditor) new Loader pool ) ).run "target_0", argv 53
虽然 crack_target_0_n.java 只是在内存中修改了 Hash.class, digest 但 Javassist 提供调试设置, 允许开发人员出于调试目的获取被修改过的 Hash.class CtClass.debugDump 指定一个目录, 被修改过的 Hash.class 将写入该目录 用 JD-GUI 查看被修改过的 Hash.class,gethash) 已被修改 : public static String gethashstring paramstring1, String paramstring2) if null == paramstring2 ) return null; try M e s s a g e D i g e s t l o c a l M e s s a g e D i g e s t = MessageDigest.getInstance paramstring1 localmessagedigest.update paramstring2.getbytes) Object localobject = null; String str = null; System.out.println "Modified" str = "7BCD1686FE4BCE0A06655740FE54D 0C233855872"; return str; catch Exception localexception ) throw new RuntimeException localexception 五 在字节码级别操作 class 一般来说 Javassist 鼓励在 Java 源码级操作 class, 但它确实支持 byteocde 级的操作 crack_target_0_i.java 在加载 target_0. * 后面的代码原来是 : * * return gethexstr localmessagedigest.digest) byte[] arrayofbyte = localmessagedigest. class 时修改了 VerifyHash) 的 bytecode, 相当于直接 return true ) import javassist.bytecode.*; class crack_target_0_i_translator implements Translator p u b l i c v o i d s t a r t C l a s s P o o l p o o l ) t h r o w s NotFoundException, CannotCompileException 54
System.out.println "\nmodified\n" public void onload ClassPool pool, String classname ) ConstPool cp = cf.getconstpool throws NotFoundException, CannotCompileException if classname.equals "target_0" ) ) Bytecode bc = new Bytecode cp, 1, 0 * iconst_1 * ireturn ClassFile cf = pool.get classname ).getclassfile MethodInfo mi = cf.getmethod "VerifyHash" CodeAttribute ca = mi.getcodeattribute bc.addiconst 1 bc.addreturn CtClass.intType CodeAttribute tca = bc.tocodeattribute CodeIterator try ci = ca.iterator tca.setmaxlocals ca.getmaxlocals) tca.computemaxstack mi.setcodeattribute tca int int index; op; catch BadBytecode e ) * 显示 VerifyHash) 的 opcode e.printstacktrace while ci.hasnext) ) index = ci.next op = ci.byteat index System.out.println Mnemonic.OPCODE[op] public class crack_target_0_i 55
public static void main String[] argv ) throws Throwable 演示性玩具, 不具备实际价值, 至少目前是这样的 出于演示完备 性, 提供 crack_target_0_m.java 演示之 它先保存原来的 Hash. ClassPool pool = ClassPool.getDefault class, 然后在内存中修改 Hash.class,reload 修改过的版本, 然 Loader loader = new Loader Translator t = new crack_target_0_i_translator loader.addtranslator pool, t loader.run "target_0", argv $ java -cp "javassist.jar;target_0.jar;." crack_target_0_i anything ldc aload_0 invokestatic ldc invokevirtual ireturn Modified You are a clever boy! 对于编写 Loader, 前面演示的技术足以应付绝大多数需求 可能有很多小变种, 但本质上没有区别 六 鸡肋一般的 HotSwap 支持最后提一下 Javassist 提供的 HotSwapper 类, 这几乎是个 后 reload 原来的版本 import javassist.util.hotswapper; public class crack_target_0_m public static void main String[] argv ) throws Throwable HotSwapper hs = new HotSwapper 31337 System.out.println Hash.gethash "SHA1", "anything" ) * 保存原来的 class ClassPool pool = ClassPool.getDefault CtClass old_class = pool.get "Hash" old_class.detach byte[] old_buf = old_class.tobytecode * 修改 class method if old_class.isfrozen) ) 56
System.out.println "\nreload original version\n" old_class.defrost hs.reload "Hash", old_buf target_0.main argv C t M e t h o d o l d _ m e t h o d = o l d _ c l a s s. getdeclaredmethod "gethash" old_class.removemethod old_method CtMethod new_method = CtMethod.make "public static String gethash String algorithm, String text )" + "" + " System.out.pr intln \" M odif ied\" " + " return \"7BCD1686FE4BCE0A06655740FE54D 0C233855872\" " + "", old_class old_class.addmethod new_method byte[] new_buf = old_class.tobytecode System.out.println "\nreload modified version\n" hs.reload "Hash", new_buf target_0.main argv $ javac -cp "tools.jar;javassist.jar;target_0.jar" crack_ target_0_m.java $ java -agentlib:jdwp=transport=dt_socket,address=127.0.0. 1:31337,server=y,suspend=n -cp "tools.jar;javassist.jar;target_0. jar;." crack_target_0_m anything Listening for transport dt_socket at address: 31337 8867C88B56E0BFB82CFFAF15A66BC8D107D6754A reload modified version Modified You are a clever boy! reload original version 编译时需要 tools.jar 执行时必须以调试模式启动 Javassist 能干些什么完全取决于你对 Java 语言 class 格式以及 JVM 的理解程度 比如有人用它给无调试信息的 class 添加行号信息, 这样某些动态调试工具就可以更有作为 ; 有人用它反混淆等等 本文演示了 Javassist 的基本技术, 更多高级用法等待着大家在实际需求产生后去探索并应用 57