阅读 2881

Android逆向之某APP逆向实践

原文链接

记录一次某Android APP反编译获取源码、请求抓包、破解请求加密算法、使用python模拟请求实现登录的逆向过程,仅说明逆向思路和过程,APP信息和内容不会公开。

步骤1. 尝试apk反编译

下载要反编译的apk后,执行" apktool d app.apk"反编译,发现抱错如下:

I: Using Apktool 2.5.0 on app.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
Exception in thread "main" brut.androlib.err.RawXmlEncounteredException: Could not decode XML
        at brut.androlib.res.decoder.XmlPullStreamDecoder.decode(XmlPullStreamDecoder.java:149)
        at brut.androlib.res.decoder.XmlPullStreamDecoder.decodeManifest(XmlPullStreamDecoder.java:155)
        at brut.androlib.res.decoder.ResFileDecoder.decodeManifest(ResFileDecoder.java:162)
        at brut.androlib.res.AndrolibResources.decodeManifestWithResources(AndrolibResources.java:204)
        at brut.androlib.Androlib.decodeManifestWithResources(Androlib.java:134)
        at brut.androlib.ApkDecoder.decode(ApkDecoder.java:122)
        at brut.apktool.Main.cmdDecode(Main.java:179)
        at brut.apktool.Main.main(Main.java:82)
Caused by: java.io.IOException: Invalid chunk type (121).
        at brut.androlib.res.decoder.AXmlResourceParser.doNext(AXmlResourceParser.java:906)
        at brut.androlib.res.decoder.AXmlResourceParser.next(AXmlResourceParser.java:102)
        at brut.androlib.res.decoder.AXmlResourceParser.nextToken(AXmlResourceParser.java:112)
        at org.xmlpull.v1.wrapper.classic.XmlPullParserDelegate.nextToken(XmlPullParserDelegate.java:105)
        at brut.androlib.res.decoder.XmlPullStreamDecoder.decode(XmlPullStreamDecoder.java:142)
复制代码

去github上查看使用的版本为2.5.0的apktool源码,找到报错代码:

if (chunkType < CHUNK_XML_FIRST || chunkType > CHUNK_XML_LAST) {
    throw new IOException("Invalid chunk type (" + chunkType + ").");
}
复制代码

猜测由于加固修改了AndroidManifest.xml内容造成chunkType异常,直接将apk修改后缀.zip解压,得到编译后的AndroidManifest.xml,使用010 editor打开该文件,去010 editor模板下载页面下载 AndroidManifest.bt模板,导入后运行该模板:

010_editor.jpg

发现多出了些多余字节,删除之后保存执行java -jar AXMLPrinter2.jar AndroidManifest.xml > ok.xml成功,可正常反编译AndroidManifest.xml。

将删除多余字节的AndroidManifest.xml替换到apk中,重新使用apktool反编译,成功的得到了AndroidManifest.xml、资源文件以及smali字节码文件。

利用这些反编译文件可以做一些简单分析,还可以用来实现smali插桩、修改SSL证书等功能。

步骤2. 查看加壳代码

将上面得到的smali字节码利用smali.jar转为dex,或者直接解压apk得到dex,使用jadx-gui反编译得到java源码,查看只有几个类,确认该apk已加固。

查看代码发现有Application子类,通过查看AndroidManifest.xml的application-android:name标签确认使用该自定义Application,代码如下:

application.jpg

通过一些类、方法命名可知代码已经过混淆,attachBaseContext先于onCreate最先执行,查看C0009a.m34a方法如下:

jni.jpg

程序刚启动时会把使用的一些文件,包括.so文件拷贝到dataDir下,并且加载了该动态库。

在手机中安装apk,直接运行,在目录"/data/data/包名"下可以找到这个ELF文件,通过"adb pull"命令拉取到本地电脑上。

之所以在手机中获取,而不是直接拷贝下lib下的so文件,是因为程序加固修改了lib下的so文件,而"/data/data/包名"下的so是可通过"System.loadLibrary"直接加载,格式正确的so文件。

步骤3. 分析加壳过程

  1. attachBaseContex
    • 将加密的dex文件从assets里读取出来,保存到dataDir下
    • 使用自定义的DexClassLoader加载解密后的原dex文件
  2. onCreate
    • 反射修改ActivityThread类,并将Application指向原dex文件中的Application
    • 创建原Application对象,并调用原Application的onCreate方法启动原程序

通过加壳包名、加壳代码、so文件命名等可知,这个APP的加壳方式并不是市场常见的加壳方式。

步骤4. 脱壳环境准备

无论是Frida脱壳还是Xposed脱壳,都需要一台root的手机。为了方便直接使用VMOS创建一个已root的虚拟机系统,创建完毕安装此APP,安装后点击运行直接闪退,通过adb连接查看日志,发现报错"不可在模拟器上运行!",发现应用做了模拟器检测,并且不知道具体检测手段不好绕过,直接采用真机。

使用的手机是一台小米 8,系统是Android 10.0,小米手机解锁Bootloader比较方便,手机绑定小米账号后,下载小米手机解锁工具解锁bl,解锁后通过安装Magisk实现手机root。

root后安装此应用,点击运行直接闪退,通过adb连接查看日志,发现报错"手机已root!",应用也做了root检测,为了让app正常运行起来,首先要绕过app的root检测。

下载magisk模块EdXposed完整框架并安装,安装后EdXposed安装RootCloak模块,并且在magisk设置中打开MagiskHide,重命名magisk包名,完成后重启,点击应用运行已经能够正常运行起来。

为了后续脱壳方便,magisk安装adb_root模块,让adb可以通过adb root获取到root权限。

步骤5. 应用脱壳

一般脱壳首先要从内存中dump出dex:

  • hook法:Android 5.0~8.0版本可以用hook libart.so中的OpenMemory方法,8.0及8.0以上可以用hook libdexfile.so中的OpenCommon方法(OpenCommon源码),Android 10.0的libdexfile位于/apex/com.android.runtime/lib/libdexfile.so,通过获取到dex的内存地址和大小,然后从内存中导出。

  • 特征匹配法:dex的header中magic魔数固定"64 65 78 0a 30 ?? ?? 00",可用魔数和其他dex的特征皮匹配,从内存中直接搜索找到dex地址,再通过dex的header中的file_size获取dex文件大小,hluwa/FRIDA-DEXDump就是这种实现方法。

脱壳的技术还有很多,感兴趣可以查看Android逆向之逆向工具 中整理的脱壳工具,这里直接采用hluwa/FRIDA-DEXDump将dex从内存中导出,导出后使用jadx将导出的dex导出为gradle项目,获取得到源码导入Android studio。

步骤6. 请求抓包

手机安装Charles的https证书之后,使用charles抓包APP发现无法抓包,使用Drony + Charles使用 VPN 导流后进行抓包成功,直接采用HttpCanary安装https证书后也可以抓包成功。防止被抓包的方式有多种,比如okhttp禁用代理等方式,可以hook相关代码绕过。抓包后导出:

http.jpg

步骤7. 请求解密

查看项目源码,项目是基于okhttp+rxjava+retrofit的MVP架构,猜测request加密和response解密代码位于"okhttp3.Interceptor"的实现类,找到了实现类,但是方法具体实现已经被抽取到外部:

code.jpg

继续查看代码,找到了sm2、sm3、sm4算法的实现类,算法介绍:

算法类型说明
sm2非对称加密公钥密码算法,公钥64字节,私钥32字节,每次加密密文不同
sm3摘要算法杂凑算法,输出32字节
sm4对称加密分组对称密码算法,密钥16字节,有CBC和ECB两种模式

编写Xposed插件,hook sm2算法加密解密,sm3加密,sm4加密解密方法,可以从方法参数和返回值获取加密密钥、请求内容以及加密后的数据等信息,因为程序已经加壳,需要通过程序的Application的attachBaseContext方法获取真实classLoader:

public class CustomHook implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        String strPackageName = "com.sgcc.wsgw.cn";
            XposedHelpers.findAndHookMethod("xx.xx.Application", lpparam.classLoader,
                    "attachBaseContext", Context.class, new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            super.afterHookedMethod(param);
                            Context context = (Context) param.args[0];
                            TruClassLoader classLoader = context.getClassLoader();
                            //hook sm2、sm3、sm4加解密代码
                        }

                    });
        }
    }
}
复制代码

hook方法参数后可以看到加密前的请求内容,并推算出加解密方式后用python实现,模拟发送请求,测试成功:

login.jpg

步骤8. 抽取方法

因为APP很多核心实现方法都被抽取到外部,so代码做了混淆,是三代壳一种实现方式,虽然可以通过hook方法推算出请求加解密,很多实现逻辑还是看不到。

可以通过修改Android源码并刷机,在dex加载类之后,第一次调用之前做主动调用,壳这时会将dex method解密,获取dex的code_item之后写入dex便可以得到抽取的方法。

开源的脱壳技术如FARTAUPK就是使用类似方法实现三代壳脱壳的。

总结

APP使用了模拟器检测,root检测,Xposed检测,代码混淆,so代码混淆,dex method抽取动态解密,消息sm2对称加密,消息sm4非对称加密,消息sm3防篡改等技术提高了安全性。但是root检测不难绕过,绕过之后可以直接在内存dump出dex,可以看到部分代码实现,通过hook函数推算出加密解密算法和密钥,也可以通过复杂写的三代壳脱壳方式得到完整代码。可以通过继续加强root检测,Frida检测,Magisk检测,针对FART等脱壳技术检测,SSL改为双向认证,sm4密钥设置规则等方式进一步提高安全性。

参考

文章分类
Android
文章标签