前言
我刚为一个金融机构完成了一个移动应用的渗透测试。我写下这些主要是为将来的手工反编译工作做个笔记。我看了很多文章,测试了一些用于Android应用反编译的工具,但是他们大多数是用于分析恶意软件的。有时候我需要做渗透测试而进行反编译和测试应用。
很多时候,是分析恶意软件还是分析应用都无所谓,但它们是有区别的。例如,当测试一个银行或者金融应用(跟一个团队一起):
- 我们可以确定这个应用不是恶意的,所以我们可以安全地使用真实设备
- 混淆通常只是到DEX层面,而且不会修复本地代码(Dalvik VM),因为它们想保证便携性
- 我们需要能够运行和测试应用,而不仅仅是提取字符串来猜测应用的功能(对于一些恶意软件分析,只需要提取字符串)
- 有时候我们需要修改和重新打包应用从而越过root验证、SSL pinning等等,然后重新分发APK给团队成员(通常测试不需要重新打包一个恶意软件)
大家可能会问:如果是为了渗透测试,为什么不直接要应用的debug版本呢?在很多情况下可以这么做,这使得我们的工作变得很容易。而有些情况下,由于银行和应用提供者之间的合同(或者其他法律或技术原因),他们只提供一个Play Store或者iTunes链接。
我不能告诉大家我测试的应用,但我可以说下所使用的加壳方法。
试试自动工具
在手工开始工作之前,有几个反编译工具和网站可以在很多混淆场景提供帮助。APK Deguard是其中之一。它最大只支持16Mb的APK文件,所以如果有很多资源文件就要删了确保不会超过限制。这个工具可以识别库文件,所以有时候可以完美地得到重构方法和类名。不幸的是,它也有很多bug:有些变量是从类里消失的方法、有时候它生成4个字节大小的类(就是null)。
我试过几个其他的看起不错的工具,例如simplify(确实不错,但我测试它时,很慢)。我还试了Dex-Oracle(没用)。JADX也有一些简单的编译重命名工具,但这种情况下不够用。
每当我发现一个工具不起作用,我通常会花一些时间看看能不能让它工作。最后发现手工有时是最好的。
使用XPosed框架
有些情况下,使用XPosed框架是很好的,我们可以记录下任何方法,或者替换存在的方法。有一点我很不喜欢,就是每次更新模块都需要重启(或者软重启)。
还有几个模块,例如JustTrustMe,可以和很多应用一起使用,用来绕过SSL pinning。但它不是对所有应用起作用。例如,上次我发现对Instagram不起作用(但当然,可能有人打了补丁可以用了)。还有RootCloak,也可以在很多应用隐藏root信息,但这个模块已经有些时间没更新了。
难过的是我测试过的应用,这些工具都不能用,应用还是可以检测到设备的root信息,而且也不能绕过SSL pinning。
使用Frida
Frida也是一个有趣的工具,很多时候有用。已经有一些基于Frida的有趣的脚本,例如:appmon。
Frida和XPosed都有一个缺点:函数内部执行跟踪,例如我们无法在一个方法中打印一个确定的值。
解包和重打包
这种情况很常见:检查应用是否检查它自己的签名。首先,我使用一个锁定bootloader、没有root的真实设备(不是模拟器)。我们可以用apktool解包应用:
apktool d app.apk
cd app
apktool b
对dist/app.apk重签名然后在设备上安装。我遇到的情况是:应用无法运行,只显示一个提示“App is not official”。
查找原始字符串
我们可以用:
grep -r const-string smali/
来提取所有代码里的所有字符串。我遇到的情况是:没能找到很多字符串。我找到的字符串,是用于加载类的。这意味着当我们重命名一个类时要小心,因为它可能作为一个字符串在某些地方被引用。
插入日志代码
通过一些努力,我们可以调试一个小项目,但我更喜欢为两件事做调试日志:反编译字符串和跟踪执行。
为了插入调试信息,我创建了一个Java文件然后转换成smali代码。这个方法可以打印任何Java对象。首先,在smali文件夹下增加用于调试的smali文件。
手工插入日志代码,我们只需要:
invoke-static {v1}, LLogger;->printObject(Ljava/lang/Object;)V
用我们想要打印的寄存器替换v1。
大多数时候,反编译函数在所有地方都有相同的参数和返回值,在这个情况下,签名是:
.method private X(III)Ljava/lang/String;
我们可以写一个脚本:
- 查找反编译函数
- 插入一个调用来记录字符串
打印反编译函数中的结果字符串是容易的,但有一个问题:这字符串是从哪来的(哪一行,哪个文件)?
我们可以像这样插入更详细的日志代码:
const-string v1, "Line 1 file http.java"
invoke-static {v1}, LMyLogger;->logString(Ljava/lang/String;)V
但这需要有未使用的寄存器来存字符串(需要追踪现在哪个寄存器是未使用的),或者我们可以增加本地寄存器数量然后使用最后一个寄存器(在函数已经使用了所有寄存器时不起作用)。
我用了另一个方法:我们可以用一个堆栈跟踪(StackTrace)来跟踪这个方法在哪被调用。要识别行号,我们只需要在smali文件中,在调用反编译函数之前增加新的“.line”指令。为了让编译的类名便于记忆,在smali最前面增加“.source”。刚开始我们还不知道这个类是做什么用的,所以只需要用uuid给它一个唯一标识符。
跟踪启动
在Java里,我们可以创建静态初始化器(static initializer),然后当类第一次被使用时它将会被执行。我们可以在 <clinit> 开始处增加日志代码:
class Test {
static {
System.out.println("test");
}
}
这里我用了UUID(随机生成UUID然后将它当做字符串放在每个类里),它将帮助我处理编译命名。
class Test {
static {
System.out.println("c5922d09-6520-4b25-a0eb-4f556594a692");
}
}
如果这个信息出现在logcat里,我们就可以知道类被调用/使用了。我可以像这样编辑命名:
vi $(grep -r UUID smali|cut -f 1 -d ':' )
或者我们也可以设置一个文件夹,放置带有到原始文件链接的UUID。
编写新的smali代码
我们可以手工编写简单的smali代码,但更复杂的代码我们应该用Java来写,然后再转换成smali。确保它在设备上有效也是一个不错的主意。
javac *.java
dx --dex --output=classes.dex *.class
zip Test.zip classes.dex
apktool d Test.zip
现在我们得到一个可以插入的smali(复制到smali文件夹)
这个方法也可以用来测试应用本身的部分代码。我们可以提取smali代码,加上main,然后运行。
adb push Test.zip /sdcard/
adb shell ANDROID_DATA=/sdcard dalvikvm -cp /sdcard/Test.zip NameOfMainClass
从Java层面思考
应用里有几个类从字节数组中提取一个dex文件为临时命名,然后移除该文件。这个数组时加密的,文件名时随机的。我们想知道的第一件事是:这个文件是否重要?我们需要修复它吗?
为了保存文件,我们可以修复反编译字符串:如果它返回“delete”,我们就返回“canRead”。函数的签名是兼容的,即“()Z”(一个不接受参数并且返回布尔值的函数)
事实证明替换文件(修复)有点困难。在smali代码中看起来有点复杂,但总体来说有这些方面:
- 使用SecureRandom随机生成几个unicode字符
- 将内建的数组解密成内存中的一个zip文件
- 以固定偏移量(offset)读取zip文件
- 手动压缩(deflate)zip文件
- 将解压结果写入一个步骤1生成的随机dex文件名
- 加载dex文件
- 删除临时的dex文件
我尝试修复字节数组,但我还需要调整内部很多数字(大小和偏移量)。在从Java层面思考后,答案是只需要创建一个可以完成我们想要做的的Java代码。所以这才是我所做的:
我创建一个叫“FakeOutputStream”的类,然后修改代码让它不是查找java.io.FileOutputStrem,而是加载FakeOutputStream。
FakeOutputStream将把源代码写入/sdcard/orig-x-y,x和y是偏移量和大小。相反地,它会加载/sdcard/fake-x-y的内容然后写入到临时文件。
注意:当我第一次运行这个应用时,它会生成/sdcard/orig-x-y,并且我可以逆向生成的DEX。我也可以修改这个dex文件并且把它当做/sdcard/fake-x-y push,然后这个文件会被加载。
是时候修复了
所有文件内容解密后,我们就可以开始修复工作了,例如移除root检测,包签名检测,调试检测,SSL pinning检测等等。
在主APK外动态获取dex文件有一个优势:我们可以轻易地通过在应用外替换dex文件来测试增加替换函数。
发表评论
您还未登录,请先登录。
登录