逆向破解mac Chalres v4.0.2

背景

Charles是一款十分优秀的抓包软件,尤其是在mac操作系统下。Charles是一款商用软件,其体验版虽然能够使用全部功能,但是有以下几个使用上不方便的地方:

  1. 启动时有10s的提示购买窗口,并且每过4分钟,当你再次点击Charles进行操作时,还会弹出一个持续5s的提示购买窗口。提示购买窗口截图如下:

  2. 一旦运行超过30分钟,就会弹出下面的error提示窗口,并且终止程序:

出于学习和研究目的,在成功破解了Charles之后,发现Charles破解可以作为一个十分典型的Jar包破解教学,因为其破解流程具有以下几个特点:

  1. Jar经过了代码混淆。
  2. 破解思路多。
  3. 可以利用的破解工具多。

在详细地讲解Charles破解之前,我先介绍一下环境:

  • macOS 10.11.6
  • charles v4.0.2
  • jdk 1.8

好了,接下来,让我们来详细地讲解一下如何对Charles进行破解。

破解流程

思路一

代码分析和定位

在背景中我们介绍过了,Charles体验版未对功能做过阉割,所以第一种破解思路是取消掉Charles对体验版做的限制。不管怎么样,所有Jar包破解的第一步,都是通过反编译软件来阅读代码。这里我们使用的是JD-GUI,这是一款十分优秀的java反编译软件,除了能够阅读反编译之后的代码之外,还可以进行搜索。

我们首先进入Charles,在mac下可以通过右键Charles的图标,然后选择显示包内容。进入到Java目录,然后用JD-GUI打开charles.jar:

经过观察之后,我们发现charles.jar经过了代码混淆,这样程序内部的很多逻辑只能靠经验去猜测了。由于提示购买窗口那里有个”Delay”,所以我们先搜索所有出现过”Delay”字符串的地方。通过分析搜索结果,我们大胆猜测SplashWindow是用来处理窗口的相关逻辑,并且setDelay方法是用来设置窗口的显示时间:

然后我们搜索所有调用setDelay方法的代码,十分幸运的是,我们一下子就发现了设置启动界面10s delay时间和设置每过4分钟后的5s delay时间的代码位置。

删除启动界面的10s delay

第一处是位于com.xk72.charles.gui的V.class中:

为了给我们接下来的操作打好基础,我们首先需要对jar包进行解压缩。但是由于Charles做过了基于类名的代码混淆,这会导致jar包无法在文件名不区分大小写的操作系统上解压出来(比如a.class会在解压的过程中被A.class给覆盖掉)。这是一种十分常见的混淆技术,由于我们最常用的windows和mac操作系统都是不区分文件名大小写的操作系统,所以这种混淆办法可以将大部分的bad boy给拦截在外。下图为ProGuard混淆工具开启类名混淆的截图:

为了解决这种情况,一般我都会使用Linux系统来解压经过类名混淆过的Jar包。解压之后,我们通过JBE字节码修改工具来对V.class的字节码进行修改。对于JBE的使用方法,大家可以参考我写的另一篇文章:http://www.jianshu.com/p/a61f0f44705b。修改V.class的方法很简单,只需要将run方法对应的字节码删除,只留下return即可。

删除每过4分钟的5s delay

第二处位于SplashWindow的a方法中:

修改方法很简单,只需要将对应的字节码除了return之外全部删除,使其成为一个空方法即可。

删除半个小时退出的代码

最后,我们需要删去半个小时退出的代码。定位代码的思路很简单,搜索error对话框中出现过的字符串即可,这里我们通过JD-GUI搜索”unlicensed copy”,从搜索结果中,我们可以找到com.xk72.charles包下的e.class中有对应的逻辑:

修改方法同上,利用JBE将run方法对应的字节码除了return之外全部删除,使其成为一个空方法即可。

最后

将上述所有hack过的class文件打包进原来的jar包中(一定要在linux系统下),替换原来charles app下的charles.jar,启动后我们发现已被成功破解。

思路二

代码分析和定位

其实思路一并不是一种常规思路,大部分情况下,我们都会对处理”License”的方法进行破解。如果大家完整地走过一遍思路一的方法,那么肯定会发现在com.xk72.charles包下有一个叫做License.class的类,并且该类的a方法经常被外部调用,用来判断是否已经注册成功,比如每过4分钟显示提示购买窗口的逻辑:

修改判断是否已经注册方法

我们可以大胆地认定该方法是用来判断是否已经注册的。顺着该思路,我们先来尝试将该方法修改为永远返回true。通过JBE将a方法对应的字节码修改为:

1
2
iconst_1
ireturn

然后启动Chalres发现一直卡在Loading Preferences界面。为了找出原因,再介绍一个小技巧,我们可以通过命令行的方式来启动程序:java -jar ../Charles.app/Contents/Java/charles.jar,然后我们可以看到控制台会输出对应的错误堆栈。从错误堆栈中我们可以看到有空指针的异常:

可以看到是License的b方法:

修改显示License名称方法

我们大胆猜测该方法是用来显示License名称的,我们将其修改为返回一个固定的字符串(自己想叫什么就叫什么)。读者如果对java字节码不是很熟悉的话,有一种最简单的做法是自己先写一个demo java程序,然后用JBE查看对应的字节码:

1
2
ldc "wooyoo"
areturn

继续运行时又抛出了VerifyError:

这个错误是JVM字节码校验失败产生的。该问题很大概率是因为JBE的bug,为了解决这个问题,我们有以下3种解决方法:

  1. 关闭JVM的字节码校验。
  2. 使用javassist来修改字节码。
  3. 自己编译License
关闭JVM的字节码校验

为了关闭JVM的字节码校验,我们需要添加额外的JVM启动参数。由于我的电脑的java环境是1.8,所以只能通过添加-noverify参数,如果是1.7的话,还可以使用-XX:-UseSplitVerifier。我们可以在Charles程序目录下的info.plist文件里面添加关闭参数:

1
2
3
4
5
......
<key>JVMOptions</key>
<array>
<string>-noverify</string>
......
使用javassist来修改字节码

javassist常用来动态地修改字节码,广泛地用于各个java框架里面(这里用它来破解软件真是有点不好意思)。修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath("/path/to/charles.jar");
CtClass license = classPool.get("com.xk72.charles.License");
// 修改a方法
CtMethod aOldMethod = license.getDeclaredMethod("a", null);
CtMethod aNewMethod = CtNewMethod.copy(aOldMethod, license, null);
aOldMethod.setName("a_old");
aNewMethod.setBody("{return true;}");
aNewMethod.setName("a");
license.addMethod(aNewMethod);
// 修改b方法
CtMethod bOldMethod = license.getDeclaredMethod("b", null);
CtMethod bNewMethod = CtNewMethod.copy(bOldMethod, license, null);
bOldMethod.setName("b_old");
bNewMethod.setBody("{return \"wooyoo\";}");
bNewMethod.setName("b");
license.addMethod(bNewMethod);
// 输出新的License.class
license.writeFile("/path/to/License.class");
}
}

需要注意的是,在使用javassist的时候,我们无法移除原来方法里面的逻辑。基于此,我们有以下两种做法:

  • 先copy原来方法,然后调用setBody方法往新方法中塞入自己的逻辑。上述代码采用的就是这种方法。

  • 可以在原来的方法中调用insertBefore方法,比如:

    1
    2
    CtMethod bMethod = license.getDeclaredMethod("b", null);
    bMethod.insertBefore("{if(1<2) return \"wooyoo\"}")

    我们通过一个肯定会执行的if语句,来达到仅执行自己逻辑的效果。

自己编译License

该方法也是参考了网络上另一个人的破解文章:http://blog.csdn.net/endlu/article/details/52175787,我这里就不再赘述了,有兴趣的读者可以看这篇文章。

最后

将上述hack过的License.class文件打包进原来的jar包中。这里我们不一定要在Linux系统下完成打包,可以借用jar命令直接完成class文件的替换:jar -uvf charles.jar com/xk72/charles/License.class。然后将新的jar包替换原来charles app下的charles.jar,启动后我们发现已被成功破解。

总结

再次声明以上教程都是基于学习和研究的目的,我们强烈建议大家有能力的话还是要购买正版软件。好了我们来总结一下通过这次破解我们需要掌握的知识点:

  • 经过类名混淆过的Jar无法在不区分文件名大小写的操作系统上进行压缩或者解压缩。我们可以借用Linux操作系统来完成压缩或者解压缩操作。
  • 破解的思路一般都是修改处理”License”的逻辑。但是我们不能局限于此,比如在本例中,我们可以去除掉体验版的限制,同样达到破解的目的。
  • 某些情况下如果破解不成功,我们可以通过命令行启动程序,然后通过观察控制台输出的错误堆栈来看到底是哪里出了问题。另外如果程序有日志文件的话,我们还可以去日志文件中分析错误原因。
  • 我们可以通过JBE、javassist甚至是自己重新编译的方式来修改原来的字节码。

参考资料

坚持原创技术分享,您的支持将鼓励我继续创作!