对混淆的Android应用进行渗透测试

阅读量    27542 | 评论 1   稿费 180

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

前言

我刚为一个金融机构完成了一个移动应用的渗透测试。我写下这些主要是为将来的手工反编译工作做个笔记。我看了很多文章,测试了一些用于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;

我们可以写一个脚本:

  1. 查找反编译函数
  2. 插入一个调用来记录字符串

打印反编译函数中的结果字符串是容易的,但有一个问题:这字符串是从哪来的(哪一行,哪个文件)?

我们可以像这样插入更详细的日志代码:

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代码中看起来有点复杂,但总体来说有这些方面:

  1. 使用SecureRandom随机生成几个unicode字符
  2. 将内建的数组解密成内存中的一个zip文件
  3. 以固定偏移量(offset)读取zip文件
  4. 手动压缩(deflate)zip文件
  5. 将解压结果写入一个步骤1生成的随机dex文件名
  6. 加载dex文件
  7. 删除临时的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文件来测试增加替换函数。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多