近年来,小程序、小游戏非常流行。
业内人士都知道,小程序或者小游戏就是H5应用,就是html+JS。 反编译此类应用程序很容易,网上也有很多方法教程。
反编译小程序后,可以轻松获得源代码。 经过一些细微的改变,“新”产品就出现了。
因此,类似的应用还有很多。
在这篇文章中,我亲自测试并反编译了一个Momo小程序进行测试,并给出了避免被破解和反编译的方法。
小程序反编译
在笔记本电脑上安装夜神模拟器,并在其中安装Momo和RE管理器(rootexplorer)。
打开Momo,随意使用小程序。
此时反编译android源码,小程序文件就会缓存在本地。
使用rootexplorer找到对应的wxapkg文件并将其复制到笔记本中。 如右图所示:
然后在Node环境下使用unwxapkg解压,如右图:
此时小程序的JS源码、资源等就被反编译了,如右图:
所有文件都可以任意编辑。
JS源码可以任意更改。
因此,存在类似的应用程序也就不足为奇了。
小程序防破解
反编译后的文件中最重要的是js代码,而js代码是可以加密保护的。 加密后,虽然得到了源代码,但很难更改。
例如使用JShaman加密一段JS代码:
JS源码:
透明的js代码,功能非常清晰。
经过混淆加密后,代码变得无法识别,逻辑无法理解反编译android源码,所有字符都被加密:
不仅是JShaman,还有Ty2y,它也是专业的JS代码混淆加密工具。
这样,虽然小程序被反编译了,但即使别人得到了代码,也无法对功能进行任何修改。 小程序的整体安全性可以得到很大的提高。
JS源代码经过加密,保护了产品和版权。
之前我推送过Java代码的编译和反编译,简单介绍了Java编译和反编译相关的知识。 最近在给GitChat写《深入剖析Java语法糖》的时候,用到了很多反编译相关的知识,然后发现哪篇文章有点过时了。 那么,这篇文章就呈现在大家的面前了~
编程语言
在介绍编译和反编译之前编译jdk源码,我们先简单介绍一下编程语言(Programming Language)。 编程语言(Programming Language)分为低级语言(Low-level Language)和高级语言(High-level Language)。
机器语言(Machine Language)和汇编语言(Assembly Language)是直接使用计算机指令编译程序的低级语言。
C、C++、Java、Python等都是高级语言,程序都是用句子(Statement)来编写的,句子是计算机指令的具体表示。
例如,同一句话用C语言、汇编语言和机器语言表达如下:
计算机只能对数字进行计算。 符号、声音、图像在计算机内部都必须用数字来表示,指令也不例外。 上表中的机器语言完全由十六进制数组成。 最早的程序员直接使用机器语言编程,但是非常麻烦。 需要检查大量表格才能确定每个数字代表什么。 语言中的一组数字用助记符(Mnemonic)来表示,直接使用这个助记符来编写汇编器,然后要求汇编器(Assembler)查表将助记符替换为数字,即,汇编语言被翻译成机器语言。
不过汇编语言使用起来也比较复杂,后来又衍生出了Java、C、C++等高级语言。
什么是编译
上面提到了两种语言,低级语言和高级语言。 可以这样简单地理解:低级语言是计算机能理解的语言,高级语言是程序员能理解的语言。
那么如何从高级语言转换为低级语言呢? 虽然这个过程是编译。
从前面的例子也可以看出,C语言中的语句和低级语言中的指令之间并不存在简单的一一对应关系。 一条语句a=b+1; 需要翻译成三个汇编或机器指令。 这个过程称为编译。 (编译),由编译器(Compiler)完成,显然编译器的功能比汇编器复杂得多。 用C语言编写的程序必须经过编译并转换成计算机可以执行的机器指令。 编译需要一些时间。 这是使用中间语言编程的缺点,但更多的是优点。 首先,C语言编程更容易,写出的代码更紧凑,可读性更强,错误更容易纠正。
编译是将人类可以编译、读取和维护的中级计算机语言编写的源代码程序翻译成计算机可以分析和运行的低级机器语言程序的过程。负责此操作的工具进程称为编译器
现在我们知道什么是编译,什么是编译器。 不同的语言有自己的编译器。 Java语言中负责编译的编译器是一个命令:javac
javac是JDK中包含的Java语言编译器。 该工具可以将后缀为.java的源文件编译为可在Java虚拟机上运行的后缀为.class的字节码。
当我们写完HelloWorld.java文件后,我们可以使用javac HelloWorld.java命令来生成HelloWorld.class文件。 这个class类型文件是JVM可以识别的文件。 通常我们把这个过程视为Java语言的编译。 事实上,class文件从来就不是一种可以被机器识别的语言,因为机器只能识别机器语言,而需要JVM将这些class文件类型字节码转换成机器可以识别的机器语言。
什么是反编译
反编译的过程正好与编译相反,就是将已编译的编程语言恢复到未编译的状态,即找到该编程语言的源代码。 就是把机器能理解的语言转换成程序员能理解的语言。 Java语言中的反编译通常是指将class文件转换为java文件。
有了反编译工具,我们可以做很多事情。 最重要的功能就是通过反编译工具,我们可以了解Java编译器生成的字节码。
Java反编译工具
本文主要介绍三个Java反编译工具:javap、jad和CFR
爪哇
javap是jdk自带的一个工具,可以反编译代码,查看java编译器生成的字节码。 javap与其他两个反编译工具最大的区别在于,它生成的文件不是java文件,也不比其他两个工具生成的代码更容易理解。 以一段简单的代码为例。 如果我们要分析Java 7中的switch是如何支持String的,我们首先有以下可以编译的源码:
public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } }
执行以下两条命令:
javac switchDemoString.java javap -c switchDemoString.class
生成的代码如下:
public class com.hollis.suguar.switchDemoString { public com.hollis.suguar.switchDemoString(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String world 2: astore_1 3: aload_1 4: astore_2 5: iconst_m1 6: istore_3 7: aload_2 8: invokevirtual #3 // Method java/lang/String.hashCode:()I 11: lookupswitch { // 2 99162322: 36 113318802: 50 default: 61 } 36: aload_2 37: ldc #4 // String hello 39: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 42: ifeq 61 45: iconst_0 46: istore_3 47: goto 61 50: aload_2 51: ldc #2 // String world 53: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 56: ifeq 61 59: iconst_1 60: istore_3 61: iload_3 62: lookupswitch { // 2 0: 88 1: 99 default: 110 } 88: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 91: ldc #4 // String hello 93: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 96: goto 110 99: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 102: ldc #2 // String world 104: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 107: goto 110 110: return }
我个人的理解是javap并不是将字节码反编译成java文件,而是生成我们能理解的字节码。 其实javap生成的文件仍然是字节码,只是程序员可以稍微理解一下。 如果你掌握了字节码,你仍然可以理解上面的代码。 其实就是将String转换成hashcode,然后进行比较。
个人认为编译jdk源码,一般情况下,我们不会太多使用javap命令,一般只有在真正需要查看字节码的时候才会使用。 但是字节码中间暴露出来的东西是最全的,有机会一定要用到。 比如我在分析synchronized原理的时候,用到了javap。 通过javap生成的字节码,发现synchronized底层是依靠ACC_SYNCHRONIZED标志以及monitorenter和monitorexit这两条指令来实现同步的。
贾德
jad是一个比较好的反编译工具,只要下载一个执行工具,就可以反编译class文件。 里面还是源码。 用jad反编译后内容如下:
命令:jad switchDemoString.class
public class switchDemoString { public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; case 113318802: if(s.equals("world")) System.out.println("world"); break; } } }
看,这段代码你一定看得懂,因为这不是标准的java源代码。 这就很清楚了,原字符串的切换是通过equals()和hashCode()方法实现的。
不过jad已经很久没有更新了。 在反编译Java7生成的字节码时,偶尔会出现不支持的问题。 反编译Java 8的lambda表达式时,会彻底失败。 例如,它将直接
病例报告表
jad很好用,可惜已经很久没有更新了,只能换一个新的工具了。 CFR是一个不错的选择。 与jad相比,它的句式可能稍微复杂一些,但好在他可以工作。
比如我们用cfr来反编译刚才的代码。 执行命令:
java -jar cfr_0_125.jar switchDemoString.class --decodestringswitch false
产生以下代码:
public class switchDemoString { public static void main(String[] arrstring) { String string; String string2 = string = "world"; int n = -1; switch (string2.hashCode()) { case 99162322: { if (!string2.equals("hello")) break; n = 0; break; } case 113318802: { if (!string2.equals("world")) break; n = 1; } } switch (n) { case 0: { System.out.println("hello"); break; } case 1: { System.out.println("world"); break; } } } }
通过这段代码也可以获取字符串的switch是通过equals()和hashCode()方法实现的推理。
与Jad相比,CFR的参数较多,还是刚才的代码。 如果我们使用以下命令,输出将会不同:
java -jar cfr_0_125.jar switchDemoString.class public class switchDemoString { public static void main(String[] arrstring) { String string; switch (string = "world") { case "hello": { System.out.println("hello"); break; } case "world": { System.out.println("world"); break; } } } }
所以 --decodestringswitch 意味着解码开关支持字符串的详细信息。 类似的还有 --decodeenumswitch、--decodefinally、--decodelambdas 等。在我关于语法糖的文章中,我使用 --decodelambdas 反编译 lambda 表达式。 源代码:
public static void main(String... args) { List strList = ImmutableList.of strList.forEach( s -> { System.out.println(s); } );
java -jar cfr_0_125.jar lambdaDemo.class --decodelambdas false 反编译代码:
public static /* varargs */ void main(String ... args) { ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"u516cu4f17u53f7uff1aHollis", (Object)"u535au5ba2uff1awww.hollischuang.com"); strList.forEach((Consumer)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)()); } private static /* synthetic */ void lambda$main$0(String s) { System.out.println(s); }
CFR还有很多其他参数,用于不同的场景。 读者可以使用 java -jar cfr_0_125.jar --help 来了解更多信息。 这里我就不一一介绍了。
如何避免反编译
自从我们有了可以反编译Class文件的工具后,如何保护Java程序就成为了开发者非常重要的挑战。 然而魔高一尺,道高一尺。 当然,也有相应的技术来处理反编译。 不过,这里需要指出的是,就像网络安全防护一样,无论付出多少努力,实际上只是增加了攻击者的成本。 它不能被完全阻止。
典型的应对策略包括以下内容: