Android系统沙盒定制

  1. 一、Frida持久化
    1. 1、概述
    2. 2、功能
    3. 3、环境配置
    4. 4、配置git
    5. 5、下载repo
    6. 6、修改默认python
    7. 7、同步指定版本源码
    8. 8、安装JDK8
    9. 9、安装所需依赖 (Ubuntu 20.04)
    10. 10、设备驱动的准备
    11. 11、编译源码
    12. 12、AOSP源码导入到AndroidStudio
    13. 13、Frida持久化介绍
    14. 14、编译补充
    15. 15、修改APP启动流程
    16. 16、实现doWhitebirdPersist和isEnablePersist
    17. 17、将frida-gadget集成到系统
    18. 18、自定义目录设计
    19. 19、创建文件类型SeLinux标签
    20. 20、为自定义目录关联文件类型标签: whitebird_file
    21. 21、配置system app访问 whitebird_file 标签文件的权限
    22. 22、配置用户app访问 whitebird_file标签文件的权限
    23. 23、编译以及解决常见问题
    24. 24、Frida持久化管理App
  2. 二、加固与脱壳
    1. 1、什么是加固
      1. 未加密
      2. 360加固——整体加固
      3. 爱加密——抽取壳
    2. 2、什么是脱壳
    3. 3、为什么要脱壳
    4. 4、分析加固的app是不是必须脱壳
    5. 5、怎么判断app是否加固
    6. 6、加固的分类
    7. 7、整体加固
      1. 介绍
      2. 解决方案
    8. 8、ART下的脱壳原理—整体加固
      1. 1、在线源码查看
      2. 2、ART下的脱壳点
      3. 3、InMemoryDexClassLoader源码分析
        1. InMemoryDexClassLoader
        2. BaseDexClassLoader
        3. initByteBufferDexPath
        4. DexFile
        5. openInMemoryDexFiles
        6. DexFile_openInMemoryDexFilesNative
        7. CreateCookieFromOatFileManagerResult
        8. ConvertDexFilesToJavaArray
        9. ConvertJavaArrayToDexFiles
        10. OpenDexFilesFromOat
        11. OpenDexFilesFromOat_Impl
        12. ArtDexFileLoader.Open
        13. DexFileLoader::OpenCommon
      4. 4、DexClassLoader源码分析
        1. DexClassLoader
        2. DexPathList
        3. makeDexElements
        4. loadDexFile
        5. DexFile
        6. DexFile.loadDex
        7. openDexFile
        8. openDexFileNative
        9. OpenDexFilesFromOat
        10. ArtDexFileLoader::Open
        11. OpenWithMagic
        12. OpenFile
      5. 5、youpk脱壳原理
      6. 6、dex2oat的脱壳原理
      7. 7、FDex2
      8. 8、常见脱壳点
      9. 9、aosp导入Clion
        1. 1、Clion的安装
        2. 2、生成用于将源码导入Clion的CMakeLists.txt
        3. 3、用Clion打开该CMakeLists.txt
        4. 4、tools –> cmake –> Change Project Root
      10. 10、fart源码分析
    9. 9、抽取加固
      1. 介绍
        1. 1、抽取加固本质:
        2. 2、抽取加固的实现形式
        3. 3、抽取加固对原有dex的处理形式
      2. 解决方案
        1. 1、解决思路:
        2. 2、被动调用
        3. 3、主动调用
        4. 4、常见的抽取加固脱壳系统
    10. 10、类加载器
      1. 1、抽取加固解决方案
      2. 2、如何调用app中的所有函数呢?
      3. 3、安卓中常见的类加载器
        1. 3.1、BootClassLoader
        2. 3.2、BaseDexClassLoader
        3. 3.3、PathClassLoader:
        4. 3.4、DexClassLoader:
        5. 3.5、InMemoryDexClassLoader:
      4. 4、代码测试
    11. 11、双亲委派机制
      1. 1、类加载
      2. 2、双亲委派机制的工作原理
      3. 3、为什么要有双亲委派
      4. 4、加固对类加载器的影响
        1. 4.1、如何获取所有的ClassLoader
        2. 4.2、如何先得到一个ClassLoader
        3. 4.3、普通app运行流程
        4. 4.4、加固app运行流程
      5. 4.5、加固对类加载器的修正方式
    12. 12、fart源码分析
      1. 遍历所有ClassLoader
      2. 遍历所有的类
      3. 遍历类中 所有的函数
      4. 类的加载和初始化流程
        1. 类加载
        2. 类初始化
      5. 方法调用流程
      6. dumpMethodCode_method
      7. 将FART迁移到安卓10
    13. 13、dex重构
    14. 14、FART存在的问题
    15. 15、FART改进方案
    16. 16、使用frida调用fart函数接口
      1. 对某个dex进行脱壳
      2. 对某个类进行脱壳
    17. 17、其他加固形式
      1. 1、VMP
      2. 2、dex2c
      3. 3、多种方式混合加固
  3. 三、代码追踪
    1. 1、方法调用流程回顾
      1. java层直接调用
      2. 通过jni调用
    2. 2、方法的解释执行流程
    3. 3、invoke指令的执行流程
    4. 4、强制解释执行原理
    5. 5、追踪Java函数调用关系
    6. 6、追踪jni函数调用关系
      1. ManagerStack介绍
    7. 7、强制运行在解释模式下
    8. 8、追踪每一条smali指令
  4. 四、监控网络访问
    1. 1、常见的抓包方式
      1. 1.1、使用抓包工具
      2. 1.2、Hook抓包
      3. 1.3、监控网络访问
    2. 2、okhttp3源码分析
  5. 五、定制内核
    1. 1、安卓系统组成结构
    2. 2、什么是Linux系统调用(syscall)
    3. 3、如何hook Linux内核级别的函数
    4. 4、逆向分析系统调用
    5. 5、获取源码编译内核
    6. 6、修改内核源码,实现SVC监控

一、Frida持久化

1、概述

定制安卓系统其实就是修改安卓系统源码,达到辅助app逆向分析的目的

2、功能

1、frida-gadget免root、脱离PC、持久化Hook

2、追踪函数调用关系

3、代码追踪

4、整体壳和抽取壳的脱壳

3、环境配置

Ubuntu20.04:http://mirrors.aliyun.com/ubuntu-releases/20.04/

aosp源码:https://mirrors.ustc.edu.cn/aosp-monthly

//下载aosp源码
mkdir ~/bin
cd ~/bin
wget https://mirrors.ustc.edu.cn/aosp-monthly/aosp-latest.tar
wget可以使用-c选项,来支持断点下载
//先计算md5与https://mirrors.ustc.edu.cn/aosp-monthly/aosp-latest.tar.md5是否一致
md5sum aosp-latest.tar
tar xvf aosp-latest.tar

4、配置git

sudo apt-get install git	安装git

git config --global user.email 767778848@qq.com	配置邮箱

git config --global user.name "whitebird"	配置用户名

5、下载repo

echo "PATH=~/bin:\$PATH" >> ~/.bashrc
source ~/.bashrc
sudo apt-get install curl
curl -sSL  'https://gerrit-googlesource.proxy.ustclug.org/git-repo/+/master/repo?format=TEXT' |base64 -d > ~/bin/repo
chmod a+x ~/bin/repo
export REPO_URL='https://gerrit-googlesource.proxy.ustclug.org/git-repo'
cd aosp

6、修改默认python

sudo unlink /usr/bin/python
sudo ln -s /usr/bin/python3.8 /usr/bin/python

7、同步指定版本源码

repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-10.0.0_r4
repo sync

a) 代号和细分版本号可查看以下链接
    https://source.android.com/setup/start/build-numbers?hl=zh_cn
b) 选个有驱动的,支持机型多的分支
c) 谷歌手机设备驱动下载地址
		https://developers.google.com/android/drivers
d)同步之前先打个虚拟机快照

然后等一会就行了,这个时间比较漫长

repo sync的时候有可能会遇到的问题

info: A new version of repo is available
repo: Updating release signing keys to keyset ver 2.3
warning: repo is not tracking a remote branch, so it will not receive updates
repo reset: error: Entry '.github/workflows/test-ci.yml' not uptodate. Cannot merge.
fatal: 不能重置索引文件至版本 'v2.16^0'。

解决方案:

cd ~/bin/aosp/.repo/repo
git pull
cd ~/bin/aosp
再次repo init 和 repo sync

8、安装JDK8

sudo apt-get update
sudo apt-get install openjdk-8-jdk

9、安装所需依赖 (Ubuntu 20.04)

sudo apt-get install git-core gnupg flex bison build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig libncurses5

参考以下地址

https://source.android.com/setup/build/initializing?hl=zh-cn

10、设备驱动的准备

谷歌手机设备驱动下载地址

https://developers.google.com/android/drivers

移动到ubuntu中bin/aosp下

在ubuntu下中解压后会有两个sh文件,需要我们运行,且均需要回车键往下阅读到8.d左右的位置输入I ACCEPT

运行完的效果

11、编译源码

参考https://source.android.com/docs/setup/build/building?hl=zh-cn

make clobber
source build/envsetup.sh
lunch	# 选择设备内核和编译版本
		# 增加编译产品选项 修改aosp/device/google/marlin/AndroidProducts.mk
make -j8

pixel3的代号是,也就是blueline

我们编译的选项其实有三个版本

user	无root权限
userdebug	带有调试和root权限,su获取超级权限
usereng 带有调试和root权限,超级adb

但是我们看上面的种类不全,可以通过指令修改

现在已经选完了,我们再输入choosecombo

然后make -j8开始编译

12、AOSP源码导入到AndroidStudio

1、进入源码根目录,运行如下命令, 会在源码目录下的out/host/linux-x86/framework目录下生成idegen.jar文件

source build/envsetup.sh
mmm development/tools/idegen/

2、在源码根目录下继续执行如下命令,会在根目录下生成android.iml和android.ipr两个文件,这两个文件是AndroidStudio的工程配置文件

development/tools/idegen/idegen.sh

3、下载Android Studio linux版本

wget https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2022.2.1.20/android-studio-2022.2.1.20-linux.tar.gz

4、解压Android Studio压缩包

tar -zxvf android-studio-2022.2.1.20-linux.tar.gz

5、运行studio.sh

不用导入,默认初始化就行了,安装完成后先自己构建一个demo,为了让android studio下载好gradle

下载好后我们修改sdk为API29,也就是Android10,还有安装ndk和cmake

之后就是build一下apk,没有问题就可以导入AOSP了

这个大概要半个小时以上

13、Frida持久化介绍

1、Hook的前提

需要将代码或者能够完成Hook功能的东西,注入到目标进程中

安卓中注入的方式:zygote注入、ptrace注入、文件感染等

2、frida-server

利用ptrace注入,需要root权限,为了方便Hook代码修改,还设计成了需要与PC端连接

3、frida-gadget

当Hook代码修改测试完毕,可以通过它来实现免root、脱离PC,但是它本身没有注入功能,需要将其打包到app中

4、魔改系统

在app启动过程中,自动加载frida-gadget,就可以不用修改app了,更通用

14、编译补充

1、编译报错或者修改系统文件以后,都可以直接make,已经编译的部分会跳过

2、make clean 会清除已经编译的,全部重来,在编译不同lunch选项时使用

3、单独编译system.img,在根目录下

source build/envsetup.sh
lunch xxx
make systemimage -j4

4、单独编译某个模块 mmm packages/apps/whitebird

​ 将单独编译的模块打包到img镜像中 make snod

5、更多其他编译方式参考百度

15、修改APP启动流程

为什么要在程序的MainActivity最开始就加载frida-gadget

因为如果我们运行一段时间再加载,可能在加载之前的一些执行函数我们就无法hook了

当我们点击图标启动APP时,最终会走到ActivityThread,所以我们可以在这里加入加载frida-gadget的代码,实现打开app时判断是否启用持久化/frameworks/base/core/java/android/app/ActivityThread.java

在这里加入代码,具体的app启动流程我们后面再分析

// add start
        //packageName
        String curPkgName = data.appInfo.packageName;
        int curUid = 0;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.BASE_1_1) {
            curUid = Process.myUid();
        }
        if (curUid > 10000) {
            LOGD("curPkgName: " + curPkgName + " curUid: " + curUid);
            Boolean isPersist = isEnablePersist(curPkgName, "whitebird_persist");
            LOGD("isPersist: " + isPersist);
            if (isPersist) {
                //todo copy so and js path
                String JSPath = getAppJSPath(curPkgName);
                LOGD("JSPath: " + JSPath);
                if (JSPath != null) {
                    boolean isOk = doWhitebirdPersist(appContext, LIB32_WHITEBIRD, LIB64_WHITEBIRD, JSPath);
                    LOGD("doWhitebirdPersist: " + isOk);
                } else {
                    LOGD("JSPath is null");
                }
            }
        }
// add end

首先获取当前应用程序的包名,然后获取当前进程的用户 ID (UID) ,一般大于10000属于用户进程,小于10000的通常是系统进程,然后判断是否想要进行持久化,如果想要进行持久化,就通过doWhitebirdPersist(appContext, LIB32_WHITEBIRD, LIB64_WHITEBIRD, JSPath);进行持久化的操作

16、实现doWhitebirdPersist和isEnablePersist

上面的isEnablePersist和doWhitebirdPersist都没有实现,所以我们要去实现这些方法

// add start
private static boolean isDebug = true;
private static void LOGD(String msg) {
    if(isDebug) {
        Log.d(TAG, msg);
    }
}
public static final String LIB32_WHITEBIRD = "/system/lib/libwhitebird.so";
public static final String LIB64_WHITEBIRD = "/system/lib64/libwhitebird.so";
public static final String SETTINGS_DIR = "/data/system/xsettings/tmp/persist";
public static final String CONFIG_JS_DIR = "/data/system/xsettings/tmp/jscfg";

public static boolean saveFile(String filePath, String textMsg) {
    try{
        FileOutputStream fileOutputStream = new FileOutputStream(filePath);
        fileOutputStream.write(textMsg.getBytes("utf-8"));
        fileOutputStream.flush();
        fileOutputStream.close();
        return true;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}
private static boolean copyFile(File srcFile, File dstFile) {
    try{
        FileInputStream fileInputStream = new FileInputStream(srcFile);
        FileOutputStream fileOutputStream = new FileOutputStream(dstFile);
        byte[] data = new byte[16 * 1024];
        int len = -1;
        while((len = fileInputStream.read(data)) != -1) {
            fileOutputStream.write(data,0, len);
            fileOutputStream.flush();
        }
        fileInputStream.close();
        fileOutputStream.close();
        return true;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

//判断app是否打开自动注入脚本功能
public static boolean isEnablePersist(String pkgName, String methodType) {
    File enableFile = new File(SETTINGS_DIR, pkgName + "/" + methodType);
    return enableFile.exists();
}

public static String getAppJSPath(String pkgName) {
    File file = new File(CONFIG_JS_DIR, pkgName + "/config.js");
    return file.getAbsolutePath();
}

public static boolean doWhitebirdPersist(Context context, String so32Path, String so64Path, String srcJSPath) {
    // where is gadget from
    // /system/lib/libwhitebird.so
    File srcSoFile = new File(so32Path);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if(Process.is64Bit()) { srcSoFile = new File(so64Path); }
    }
    if(!srcSoFile.exists()) {
        LOGD("srcSoFile not exists");
        return false;
    }
    //where is gadget to
    // /data/data/pkgName/files/libwhitebird.so
    File dstSoFile = new File(context.getFilesDir(), "libwhitebird.so");
    // copy so to current directory
    if(srcSoFile.length() != dstSoFile.length()) {
        boolean isCopyFileOk = copyFile(srcSoFile, dstSoFile);
        if(!isCopyFileOk){
            LOGD("copySoFile fail: " + srcSoFile + " -> " + dstSoFile);
            return false;
        }
    }
    // /data/system/xsettings/tmp/jscfg/config.js
    File srcJSFile = new File(srcJSPath);
    if(!srcJSFile.exists()) {
        LOGD("srcJSFile not exists");
        return false;
    }
    // /data/data/pkgName/files/config.js
    File dstJSFile = new File(context.getFilesDir(), "config.js");
    boolean isCopyJSOk = copyFile(srcJSFile, dstJSFile);
    if(!isCopyJSOk){
        LOGD("copyJSFile fail: " + srcJSFile + " -> " + dstJSFile);
        return false;
    }

    String configFilePath = context.getFilesDir() + File.separator + "libwhitebird.config.so";
    // gen config
    try {
        JSONObject jsonObject = new JSONObject();
        JSONObject childObj = new JSONObject();
        childObj.put("type", "script");
        childObj.put("path", dstJSFile.toString());
        jsonObject.put("interaction", childObj);
        boolean isSaveOk = saveFile(configFilePath, jsonObject.toString());
        if(!isSaveOk){
            LOGD("saveFile fail: " + configFilePath);
            return false;
        }
        // load gadget
        System.load(dstSoFile.toString());
        return true;
    } catch (Exception e) {
        LOGD("happen error: " + e.toString());
    }
    return false;
}

17、将frida-gadget集成到系统

我们上面的代码中有一步是把/system/lib64和/system/lib下的libwhitebird.so复制到app的私有目录下,而这个libwhitebird.so需要我们在编译系统的时候放进去,libwhitebird.so是frida-gadget改的名字。

1、首先将 frida-gadget 放到源码目录,比如如下文件夹中

/frameworks/base/cmds/libwhitebird

2、修改源码以下文件,将 frida-gadget 拷贝到编译以后的系统中

/build/make/target/product/handheld_system.mk

添加以下数据,自动拷贝文件

# // add
PRODUCT_COPY_FILES += \
    frameworks/base/cmds/libwhitebird/frida-gadget-16.0.11-android-arm.so:system/lib/libwhitebird.so \
    frameworks/base/cmds/libwhitebird/frida-gadget-16.0.11-android-arm64.so:system/lib64/libwhitebird.so
# // add  

18、自定义目录设计

我们前面设计的目录都比较长和复杂,其实可以写在aosp里面,让系统帮我们生成好路径

/system/core/rootdir/init.rc 文件中添加以下数据

# // add

    # /data/system/xsettings/tmp/persist
    mkdir /data/system/xsettings 0775 system system
    mkdir /data/system/xsettings/tmp 0775 system system
    mkdir /data/system/xsettings/tmp/persist 0775 system system
    mkdir /data/system/xsettings/tmp/jscfg 0775 system system
    # // add

19、创建文件类型SeLinux标签

创建文件类型SeLinux标签:whitebird_file

在下面两个路径添加数据

/system/sepolicy/public/file.te
/system/sepolicy/prebuilts/api/29.0/public/file.te

两个文件必须完全一样

# // add
# /data/system/xsettings/tmp/persist
type whitebird_file, file_type, data_file_type, core_data_file_type, mlstrustedobject;
# // add end

20、为自定义目录关联文件类型标签: whitebird_file

在下面两个路径添加数据

/system/sepolicy/private/file_contexts
/system/sepolicy/prebuilts/api/29.0/private/file_contexts

两个文件必须完全一样

# // add
# /data/system/xsettings/tmp/persist
/data/system/xsettings(/.*)?	u:object_r:whitebird_file:s0
# // add

21、配置system app访问 whitebird_file 标签文件的权限

在下面的两个路径中添加数据

/system/sepolicy/private/system_app.te
/system/sepolicy/prebuilts/api/29.0/private/system_app.te

两个文件的内容必须完全一致

# // add
# add for accessing whitebird_file
allow system_app whitebird_file:dir  { getattr setattr open read write remove_name create add_name search rmdir };
allow system_app whitebird_file:file { getattr setattr open read write create unlink };

给目录和文件都设置了权限,当然只有系统APP拥有对该路径的权限是不够的,我们还需要给用户APP设置访问权限

22、配置用户app访问 whitebird_file标签文件的权限

/system/sepolicy/private/untrusted_app.te
/system/sepolicy/private/untrusted_app_25.te
/system/sepolicy/private/untrusted_app_27.te
/system/sepolicy/private/untrusted_app_all.te

/system/sepolicy/prebuilts/api/29.0/private/untrusted_app.te
/system/sepolicy/prebuilts/api/29.0/private/untrusted_app_25.te
/system/sepolicy/prebuilts/api/29.0/private/untrusted_app_27.te
/system/sepolicy/prebuilts/api/29.0/private/untrusted_app_all.te

与上面一样,修改文件都是同步进行的

# // add
# add for accessing whitebird_file
allow untrusted_app whitebird_file:dir  { getattr open read write search rmdir };
allow untrusted_app whitebird_file:file { getattr open read write };

23、编译以及解决常见问题

如果报:文件 system/sepolicy/prebuilts/api/29.0/private/untrusted_app_all.te 和 system/sepolicy/private/untrusted_app_all.te 不同

那就说明我们需要查看两个文件是否相同,通常情况下可能是打错了单词、或者多了一个换行,建议把其中一个文件的内容全部拷贝到另外一个文件中。

编译流程

source build/envsetup.sh
lunch
make -j6//根据虚拟机性能来,我这里选择6个线程

修改以下文件,防止报错:whitebird_file 未定义
/system/sepolicy/private/compat/26.0/26.0.ignore.cil 17
/system/sepolicy/private/compat/27.0/27.0.ignore.cil 16
/system/sepolicy/private/compat/28.0/28.0.ignore.cil 15

/system/sepolicy/prebuilts/api/29.0/private/compat/26.0/26.0.ignore.cil 17
/system/sepolicy/prebuilts/api/29.0/private/compat/27.0/27.0.ignore.cil 16
/system/sepolicy/prebuilts/api/29.0/private/compat/28.0/28.0.ignore.cil 15

在以上文件中加入数据 whitebird_file
两个文件添加的内容需要一致

24、Frida持久化管理App

源码地址:https://github.com/Whitebird0/Frida_Gadget_Manager

1. 管理app的功能
    显示已安装app列表
    可以对每个app指定需要注入的JS
    可以设置是否启用持久化

2. 相应功能实现原理
    2.1 创建表示启用的文件
	/data/system/xsettings/tmp/persist/pkgName/whitebird_persist
    2.2 指定的JS文件复制到以下目录
	/data/system/xsettings/tmp/jscfg/pkgName/config.js
    2.3 剩下的复制so、JS文件和加载so的操作,由魔改的doWhitebirdPersist函数完成
    
3. system权限app的开发
3.1 在 manifest 中加入 android:sharedUserId="android.uid.system"
3.2 将编译出来的app放入 /packages/apps/whitebirdPersistDemo
3.3 编写Android.mk
3.4 单独编译指定模块 mmm packages/apps/whitebirdPersistDemo
3.5 编译后的模块在 /out/target/product/sailfish/system/app/ControlAPP
3.6 使用 make snod 将编译出来的文件打包成镜像,刷入system.img即可

4. 如果要在编译整个系统时,一起编译这个模块,需要将模块 ControlAPP 加入源码编译链
4.1 增加的内置模块,如果为APP,加入到如下文件中
/build/make/target/product/handheld_product.mk
4.2 增加的内置模块,如果为可执行程序,加入到如下文件中
/build/make/target/product/base_system.mk

android.mk

# ///ADD START
# ///ADD END
# 设置当前工作路径
LOCAL_PATH:= $(call my-dir)


# 清除变量值
include $(CLEAR_VARS)
# 生成的模块名称
LOCAL_MODULE := ControlAPP


# 生成的模块类型
LOCAL_MODULE_CLASS := APPS
# 生成的模块后缀名,此处为apk
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
# 设置模块tag,tags取值可以为:user debug eng tests optional
# optional表示全平台编译
LOCAL_MODULE_TAGS := optional

# LOCAL_PRIVILEGED_MODULE := true

LOCAL_BUILT_MODULE_STEM := package.apk

LOCAL_DEX_PREOPT := false

# 设置源文件
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk

LOCAL_CERTIFICATE := platform

# 设置签名,此处表示保持apk原有签名
# LOCAL_CERTIFICATE := PRESIGNED
# 此处表示预编译方式
include $(BUILD_PREBUILT)

编译后的模块在 /out/target/product/sailfish/system/app/ControlAPP

现在我们可以直接把app拿出来放在之前魔改的系统上运行,因为编译该app时的系统和之前刷的系统是同一个,所以可以不用重新打包

二、加固与脱壳

1、什么是加固

其实就是将原先app的dex文件加密,app运行过程中,解密后再加载,反编译时看到的就是壳的代码,或者被抽空的代码

未加密

360加固——整体加固

爱加密——抽取壳

程序中的类都还在,但是函数体的代码都没了,也就是被抽取了,看smali代码,发现函数体都被nop了

2、什么是脱壳

本质上就是将加固app在运行过程中,解密后加载的dex文件保存下来。与加密算法差不多,一个加密的是字符/字节数据,一个加密的是文件(也是字节数据)。逆向加密算法主要要的是过程,做算法还原,而脱壳要的是解密后的dex文件。

3、为什么要脱壳

不脱壳反编译时,通常只能看到壳的代码,或者被抽取后的代码

360:整体加固以及被vmp化的onCreate

ijiami:函数体指令被抽取,用nop填充原有数据,或者干脆就变成空函数

4、分析加固的app是不是必须脱壳

能直接逆向出算法的不需要

比如自吐算法

比如能快速定位到关键代码所在so的

虽然app被加固了,但是不影响hook,所以我们依然可以跑沙盒

5、怎么判断app是否加固

1、反编译查看类数量、类中的方法特征

2、反编译查看类名特征、so特征 libjiagu.so、qihoo

3、查壳工具

4、没有加固的,也可以脱壳

6、加固的分类

dex加固:整体加固、抽取加固、VMP、dex2c等

so加固:对so结构进行处理、对so数据进行加密、自定义linker

一般处理方式就是so dump,然后so修复

7、整体加固

介绍

1、可分为落地加载、内存加载

落地加载会在本地生成dex文件,内存加载不会在本地生成dex文件

2、本质上都是将app自身的dex整体加密,app运行过程中解密后加载

3、 有些壳还会抹掉dex文件头、dex的文件大小filesize等

一般会有字符串加密、资源加密、反调试、签名验证

解决方案

1、脱壳工具fdex2

通过Class类的getDex方法得到DexFile,再通过DexFile的getBytes方法得到dex文件

2、脱壳工具blackdex

通过mCookie来脱壳

3、脱壳工具FRIDA-DEXDump

从内存中搜索dex文件,保存下来

4、脱壳系统FART、youpk

在dex加载、解析、解释执行过程中,找一个合适的时机,得到DexFile内存地址和大小,将解密状态的dex保存下来,还可以通过artMethod来得到DexFile

8、ART下的脱壳原理—整体加固

1、在线源码查看

http://androidxref.com/
https://cs.android.com/ 需要科学上网

2、ART下的脱壳点

1、dex的加载流程

通过mCookie脱壳的

通过openCommen函数脱壳的

通过DexFile构造函数脱壳的

youpk:通过ClassLinker的DexCacheData进一步得到DexFile

壳可能会自己实现上面的函数,绕过系统函数

2、dex2oat的编译流程

通过修改dex2oat脱壳的,安卓10默认不支持dex2oat

https://developer.android.com/about/versions/10/behavior-changes-10?hl=zh-cn#system-only-oat

3、 类的加载和初始化流程

DexHunter在defineClass进行类解析

LoadMethod、LinkCode

函数执行过程中的脱壳点

FART: Execute整体脱壳

FART:ArtMethod::invoke函数中进行dump CodeItem

youpk:直接到解释执行的函数中进行dump CodeItem

3、InMemoryDexClassLoader源码分析

InMemoryDexClassLoader

将dexBuffer包装到了一个数组里,最终调用父类的构造函数BaseDexClassLoader

BaseDexClassLoader

第一行是又调用了父类的构造函数,BaseDexClassLoader 的父类是ClassLoader

调用自身的构造函数,也就是赋值操作

现在我们回到BaseDexClassLoader

this.sharedLibraryLoaders = null;
this.pathList = new DexPathList(this, librarySearchPath);

这两句和dex关系不大,我们看最后一行代码this.pathList.initByteBufferDexPath(dexFiles);

initByteBufferDexPath

上面的代码没什么用,主要看下面框的两行代码,将dexFiles封装成dex对象, 然后把dex对象又封装成dexElements,所以DexFile就是进行dex加载的地方,我们可以进去分析一下

DexFile

这里就是打开dex并赋值给了mCookie,我们之前聊的mCookie就在这

mCookie这里其实是java long类型的数组

openInMemoryDexFiles

底层是走openInMemoryDexFilesNative,看名字知道是一个native函数·

函数注册

DexFile_openInMemoryDexFilesNative

通过AllocateDexMemoryMap申请内存dex_data,如果array是空的,就从buffer获取dex并复制到dex_data中,如果array不为空,就从array复制到dex_data

OpenDexFilesFromOat结束后,dex已经加载完成,返回dex_files,传入CreateCookieFromOatFileManagerResult生成cookie返回。

CreateCookieFromOatFileManagerResult

我们在这个函数里看看如何把dex_files转换成mCookie

返回一个array作为cookie,所以cookie的类型就是java的long类型的数组,array的来源是ConvertDexFilesToJavaArray

ConvertDexFilesToJavaArray

我们需要看一下这个数组中的成员到底是什么

vec是dex_files

生成了java的long aray数组,数组中的每一个成员是vec[i].get(),也就是取出了vec中的dex,并转为指针

ConvertJavaArrayToDexFiles

在上面还有个ConvertJavaArrayToDexFiles,是把java array转换成DexFiles,其实也就是把array中每一个成员强转为DexFile*,所以我们可以通过mcookie得到每一个dex的指针.

OpenDexFilesFromOat我们在上面没有分析,现在来分析一下

OpenDexFilesFromOat

OpenDexFilesFromOat_Impl

先对dex头获取,并且进行校验。

对dex_mem_maps进行遍历,加载每一个dex,调用了Open进行加载

std::unique_ptr<const DexFile> dex_file(dex_file_loader.Open	(
      DexFileLoader::GetMultiDexLocation(i, dex_location.c_str()),
      location_checksum,
      std::move(dex_mem_maps[i]),
      (vdex_file == nullptr) && Runtime::Current()->IsVerificationEnabled(),
      kVerifyChecksum,
      &error_msg));

这里其实有两个函数, std::unique_ptr<const DexFile>::dex_file()和dex_file_loader.Open()

std::unique_ptr<const DexFile>::dex_file()会调用DexFile的构造函数,这里是一个脱壳点

先看dex_file_loader.Open,其实是ArtDexFileLoader.Open

ArtDexFileLoader.Open

得到dex的起始地址和大小,所以这个地方也是脱壳点。由于OpenCommon传入了begin和size,所以OpenCommon也是一个脱壳点

DexFileLoader::OpenCommon

跟进去看看

底层都是调用DexFile

4、DexClassLoader源码分析

DexClassLoader

调用了父类的构造函数

调用自身的构造函数

重点关注DexPathList

DexPathList

makeDexElements

先判断是否是目录,如果不是就判断是否以.dex结尾,然后尝试loadDexFile

loadDexFile

DexFile

DexFile.loadDex

最后也是走DexFile

也是openDexFile

openDexFile

这里就需要进入so层进行分析了

openDexFileNative

第三个参数是dex,现在还没有加载,可以发现CreateCookieFromOatFileManagerResult和OpenDexFilesFromOat之前都分析过了,只不过参数不一样而已,所以我们也跟进去看看吧

OpenDexFilesFromOat

由于Android10删除了oat,所以这下面的if是不走的,不过系统App是有aot的

这里的执行流程已经和之前的差不多了

ArtDexFileLoader::Open

OpenWithMagic

OpenFile会打开加载dex,所以我们可以再进入OpenFIle里面看看

OpenFile

通过MemMap::MapFile把dex加载到内存中,我们也得到了map.Size()和map.Begin()

OpenCommon是我们分析InMemoryDexClassLoader时找到的脱壳点,所以后面的流程是一样的了。OpenCommon是一个通用的脱壳点。

5、youpk脱壳原理

https://github.com/Youlor/unpacker

std::list<const DexFile*> Unpacker::getDexFiles() {
  std::list<const DexFile*> dex_files;
  Thread* const self = Thread::Current();
  ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
  ReaderMutexLock mu(self, *class_linker->DexLock());
  const std::list<ClassLinker::DexCacheData>& dex_caches = class_linker->GetDexCachesData();
  for (auto it = dex_caches.begin(); it != dex_caches.end(); ++it) {
    ClassLinker::DexCacheData data = *it;
    const DexFile* dex_file = data.dex_file;
    dex_files.push_back(dex_file);
  }
  return dex_files;
}

这里获取了DexFile*的dex_file,里面包含size和begin,所以可以直接dump下来dex

之前我们在分析CreateCookieFromOatFileManagerResult的时候就遇到过GetClassLinker

当array是空的时候,就会进行linker->IsDexFileRegistered(soa.Self(), *dex_file)

继续看FindDexCacheDataLocked

这里拿dex_file和DexCacheData进行对比,DexCacheData是一个结构体,里面有dex_file

我们看一下class_linker->GetDexCachesData()的GetDexCachesData

返回的就是DexCacheData的list,而DexCacheData就是结构体,里面的DexFile*dex_file就是我们想要的

6、dex2oat的脱壳原理

Android10已经 不存在了,仅适用于Android6以下

https://www.jianshu.com/p/7af31cc5130e

    // Ensure opened dex files are writable for dex-to-dex transformations.
    for (const auto& dex_file : dex_files_) {
      if (!dex_file->EnableWrite()) {
        PLOG(ERROR) << "Failed to make .dex file writeable '" << dex_file->GetLocation() << "'\n";
      }
////////////////////////////分割线 以下为添加的代码///////////////////////////////////////////////////////////
      std::string dex_name = dex_file->GetLocation();
      LOG(INFO) << "dex2oat::dex_file name-->" << dex_name;

      if (dex_name.find("jiagu") != std::string::npos
        || dex_name.find("cache") != std::string::npos
        || dex_name.find("files") != std::string::npos
        || dex_name.find("tx_shell") != std::string::npos
        || dex_name.find("app_dex") != std::string::npos
        || dex_name.find("nagain") != std::string::npos) {
        int nDexLen = dex_file->Size();
        char pszDexFileName[260] = {0};
        sprintf(pszDexFileName, "%s_%d", dex_name.c_str(), nDexLen);
        int fd = open(pszDexFileName, O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
        if (fd > 0) {
          if (write(fd, (char*)dex_file->Begin(), nDexLen) <= 0) {
            LOG(INFO) << "dex2oat::write dex file failed-->" << pszDexFileName;
          } else {
            LOG(INFO) << "dex2oat::write dex file success-->" << pszDexFileName;
          }
          close(fd);
        } else {
          LOG(INFO) << "dex2oat::open dex file failed-->" << pszDexFileName;
        }
      }
////////////////////////////分割线 以上为添加的代码///////////////////////////////////////////////////////////
    }

其实也就是获取了dex_file,得到了dex_file.begin和dex_file.Size,然后直接写出就行了。dex_file位于setup函数里面

7、FDex2

通过Class下的getDex获取Dex,再调用Dex下的getBytes获得Dex

8、常见脱壳点

1、函数解释执行Execute

2、通过ClassLinker的DexCacheData进一步得到DexFile

3、内存搜索dex文件来dump

4、通过mCookie脱壳的

5、通过DexFile构造函数脱壳的

6、LoadMethod传入的DexFile

dex_file是DexFile&类型,也就是DexFile*

7、LinkCode传入的ArtMethod进一步得到DexFile, ArtMethod->getDexFile()

9、aosp导入Clion

我们利用clion更容易查看和修改代码

1、Clion的安装

直接官网下载解压安装就行了

2、生成用于将源码导入Clion的CMakeLists.txt

// 打开开关,编译时生成CMakeLists.txt

export SOONG_GEN_CMAKEFILES=1
export SOONG_GEN_CMAKEFILES_DEBUG=1

CMakeLists.txt会生成在out/development/ide/clion/art/runtime/libart-arm64-android/CMakeLists.txt

3、用Clion打开该CMakeLists.txt

4、tools –> cmake –> Change Project Root

选择aosp源码根路径,等解析完毕即可

10、fart源码分析

类的初始化函数始终运行在ART下的inpterpreter模式,那么最终必然进入到interpreter.cc文件中的Execute函数,进而进入ART下的解释器解释执行,因此,我们便可以选择在Execute或者其他解释执行流程中的函数中进行dex的dump操作。

在Execute函数中,通过判断函数名称中是否为决定要不要调用dumpDexFileByExecute,即判断传入的是否为静态代码块,对于加了壳的App来说静态代码块是肯定存在的。如果Execute传入的是静态代码块则调用dumpDexFileByExecute函数,并传入一个ArtMethod指针。

shadow_frame.GetMethod()其实就是ArtMethod->GetMethod()

extern "C" void dumpdexfilebyExecute(ArtMethod* artmethod)  REQUIRES_SHARED(Locks::mutator_lock_) {
    		//申请buffer
			char *dexfilepath=(char*)malloc(sizeof(char)*1000);	
    		//判断buffer是否申请成功
			if(dexfilepath==nullptr)
			{
				LOG(ERROR)<< "ArtMethod::dumpdexfilebyArtMethod,methodname:"<<artmethod->PrettyMethod().c_str()<<"malloc 1000 byte failed";
				return;
			}
			int result=0;
			int fcmdline =-1;
			char szCmdline[64]= {0};
			char szProcName[256] = {0};
			int procid = getpid();
    		// 获取进程名,也就是包名
			sprintf(szCmdline,"/proc/%d/cmdline", procid);
    		// 打开一个名为“szCmdline”的文件进行读取
			fcmdline = open(szCmdline, O_RDONLY,0644);
			if(fcmdline >0)
			{
				result=read(fcmdline, szProcName,256);
				if(result<0)
				{
					LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open cmdline file error";
					}
				close(fcmdline);
				
			}
			// 判断szProcName是否存在
			if(szProcName[0])
			{
					  // 获取dex_file
					  const DexFile* dex_file = artmethod->GetDexFile();
                	  // 得到dex_file在内存中的起始地址
					  const uint8_t* begin_=dex_file->Begin();  // Start of data.
                	  // 得到dex_file在内存中的大小
					  size_t size_=dex_file->Size();  // Length of data.
					  // 清空buffer
					  memset(dexfilepath,0,1000);
					  int size_int_=(int)size_;
					  
					  memset(dexfilepath,0,1000);
                 	 // dexfilepath=/sdcard/fart
					  sprintf(dexfilepath,"%s","/sdcard/fart");
                	 // 创建该目录,并赋予权限
					  mkdir(dexfilepath,0777);
					 // 清空buffer		  
					  memset(dexfilepath,0,1000);
                	 // dexfilepath=/sdcard/fart/%s 即app的进程名
					  sprintf(dexfilepath,"/sdcard/fart/%s",szProcName);
                	 // 创建该目录,并赋予权限
					  mkdir(dexfilepath,0777);
							  	  
					  memset(dexfilepath,0,1000);
                	 // dexfilepath=/sdcard/fart/%s/%d_dexfile_execute.dex" 即app的进程名 +dex的大小	
					  sprintf(dexfilepath,"/sdcard/fart/%s/%d_dexfile_execute.dex",szProcName,size_int_);
                	 // 如果已经存在就不用再dump了
					  int dexfilefp=open(dexfilepath,O_RDONLY,0666);
					  if(dexfilefp>0){
						  close(dexfilefp);
						  dexfilefp=0;
						  
						  }else{
									  int fp=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
									  if(fp>0)
									  {
                                          // dump dex文件
										  result=write(fp,(void*)begin_,size_);
										  if(result<0)
										  {
											  LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open dexfilepath error";
											  }
										  fsync(fp); 
										  close(fp);  
										  memset(dexfilepath,0,1000);
                                          // dexfilepath=/sdcard/fart/%s/%d_classlist_execute.txt 创建一个txt保存dex中的类名
										  sprintf(dexfilepath,"/sdcard/fart/%s/%d_classlist_execute.txt",szProcName,size_int_);
										  int classlistfile=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
											if(classlistfile>0)
											{
                                                //获取dex中的类数量
												for (size_t ii= 0; ii< dex_file->NumClassDefs(); ++ii) 
												{
                                                    // 获取类的描述符,也就是类名
													const DexFile::ClassDef& class_def = dex_file->GetClassDef(ii);
													const char* descriptor = dex_file->GetClassDescriptor(class_def);
                                                    // 循环写入类名
													result=write(classlistfile,(void*)descriptor,strlen(descriptor));
													if(result<0)
													{
														LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
														
														}
													const char* temp="\n";
													result=write(classlistfile,(void*)temp,1);
													if(result<0)
													{
														LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";
														
														}
													}
												  fsync(classlistfile); 
												  close(classlistfile); 
												
												}
										  }


									  }

					
			}
			
			if(dexfilepath!=nullptr)
			{
				free(dexfilepath);
				dexfilepath=nullptr;
			}
		
}

先导入这一段,我们把名字改了,防止有些厂商会检测函数名

进行函数导入声明,saveDexFileByExecute是在art_method.cc中实现的

修改好后直接编译刷入手机就行了

360的整体加固效果

现在放入我们的脱壳机里

app运行后在我们自定义目录下已经有dex文件和包含类名的txt文件了,现在拿出来放到jadx看看

已经恢复了

tips:
Linux C语言中的open函数
https://blog.csdn.net/weixin_39296438/article/details/79422068

Linux C语言判断文件是否存在
https://blog.csdn.net/kunkliu/article/details/108294089

9、抽取加固

介绍

1、抽取加固本质:

提取出dex中方法体的字节码,并在方法运行时还原

2、抽取加固的实现形式

抽空方法体代码,运行方法后回填,运行完后不再抽取 –> 延时保存

抽空方法体代码,运行方法后回填,运行完后又抽取 –> FART、youpk主动调用

抽空方法体代码,将原有函数体替换为解密代码,运行时解密执行

3、抽取加固对原有dex的处理形式

原有函数体数据空间置0,保留原有空间

对dex文件进行重构,不保留原有空间,在还原数据时,修改CodeItemOffest

解决方案

1、解决思路:

在函数运行时,保存被抽取的数据

2、被动调用

app正常运行过程中所发生的函数调用

只对dex中部分的类完成加载,只对dex中的部分函数完成调用

调用函数不全,导致能够恢复的函数有限

3、主动调用

构造虚拟调用,对app中所有函数完成调用

在这些函数执行时,保存函数体CodeItem数据

保存数据的时机越晚,效果越好

4、常见的抽取加固脱壳系统

DexHunter、Fupk3、FART、youpk

10、类加载器

1、抽取加固解决方案

主动调用app中的所有函数,在函数执行过程中,将方法体的CodeItem保存下来

2、如何调用app中的所有函数呢?

所有函数 – 所有类 – 所有dex – 所有ClassLoader

因为ClassLoader是用来加载dex的,所以找到所有的ClassLoader就能找到所有的dex,而找到所有的dex就能找到所有的类,同时也能找到所有的类中的所有函数

3、安卓中常见的类加载器

3.1、BootClassLoader

单例模式,用来加载系统类

3.2、BaseDexClassLoader

是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,dex和类加载的主要逻辑都是在BaseDexClassLoader完成的

3.3、PathClassLoader:

是默认使用的类加载器,用于加载app自身的dex,比如我们写的一些关键代码

3.4、DexClassLoader:

用于实现插件化、热修复、dex加固等,常在代码中调用

3.5、InMemoryDexClassLoader:

安卓8.0以后引入,用于内存加载dex,dex不落地

4、代码测试

简单演示动态加载

层级关系:DexClassLoader - PathClassLoader - BootClassLoader

this.dynamicDex = new DexClassLoader(
        DexPath,
        context.getCacheDir().getAbsolutePath(),
        getApplicationInfo().nativeLibraryDir,
        MainActivity.class.getClassLoader());
MainActivity.class.getClassLoader();
Log.d("whitebird", "path: " + MainActivity.class.getClassLoader());
Log.d("whitebird", "path parent: " + MainActivity.class.getClassLoader().getParent());
Log.d("whitebird", "boot: " + String.class.getClassLoader());
Log.d("whitebird", "boot parent: " + String.class.getClassLoader().getParent());
Log.d("whitebird", "dex: " + dynamicDex);
Log.d("whitebird", "dex parent: " + dynamicDex.getParent());

parent需要聊到双亲委派机制,这个我们后面再说

通过getParent可以获取父类加载器

上面的代码中,我们将DexClassLoader的最后一个参数改为了MainActivity.class.getClassLoader(),其实就是我们自己写的代码所属于的类加载器,也就是PathClassLoader,我们打印一下验证

层级关系是符合的,而且BootClassLoader没有父类加载器了

11、双亲委派机制

1、类加载

隐式加载

显示加载:Class.forName加载、使用loadClass加载

2、双亲委派机制的工作原理

2.1、如果一个类加载器收到了类加载请求,会先把这个请求委托给父类的加载器去执行

2.2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次类推,最终到达顶层的启动类加载器

2.3、如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载

3、为什么要有双亲委派

3.1、避免重复加载,已经加载的Class,可以直接读取

3.2、更加安全,无法自定义类来替代系统的类,可以防止核心API库被随意篡改

3.3、方便脱壳机主动调用

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先,检查请求的类是否已经被加载过了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        // 如果父类加载器抛出ClassNotFoundException
        // 说明父类加载器无法完成加载请求
        }
        if (c == null) {
            // 在父类加载器无法加载的时候
            // 再调用本身的findClass方法来进行类加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

4、加固对类加载器的影响

4.1、如何获取所有的ClassLoader

先得到一个ClassLoader,再通过ClassLoader的parent属性向上遍历

4.2、如何先得到一个ClassLoader

PathClassLoader加载dex以后,会记录在LoadedApk的mClassLoader属性中,默认使用这个ClassLoader去寻找类

4.3、普通app运行流程

BootClassLoader加载系统核心库

PathClassLoader加载app自身dex,然后放到mClassLoader中

4.4、加固app运行流程

BootClassLoader加载系统核心库

PathClassLoader加载壳的dex ,此时mClassLoader还是存放PathClassLoader

壳的dex/so加载原先app自身dex,比如使用DexClassLoader、InMemoryDexClassLoader,不过大概率不会使用这些api,因为系统api可能会成为脱壳点,基本上都是会自己实现。

4.5、加固对类加载器的修正方式

对于普通app,用mClassLoader去加载app自身dex是可以找到的,但是对于加固app,如果我们用mClassLoader去加载app自身dex,很显然是找不到的,而此时会触发双亲委派机制,往上找,也就是用BootClassLoader去加载,但是肯定也是找不到的。因此,我们需要修正类加载器。

第一种方式:插入ClassLoader

BootClassLoader
DexClassLoader
PathClassLoader  mClassLoader

这样子当mClassLoader加载不了app自身的dex时,就会调用DexClassLoader来加载dex

第二种方式:替换ClassLoader

BootClassLoader
PathClassLoader
DexClassLoader mClassLoader

12、fart源码分析

遍历所有ClassLoader

//通过反射调用函数
public static Object invokeStaticMethod(String class_name,
                                        String method_name, Class[] pareTyple, Object[] pareVaules) {

    try {
        Class obj_class = Class.forName(class_name);
        Method method = obj_class.getMethod(method_name, pareTyple);
        return method.invoke(null, pareVaules);
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return null;

}

//通过反射获取成员
public static Object getFieldOjbect(String class_name, Object obj,
                                    String filedName) {
    try {
        //动态加载类
        Class obj_class = Class.forName(class_name);
        //使用Java反射来获取fieldName加载类中字符串指定的字段
        Field field = obj_class.getDeclaredField(filedName);
        //将字段的可访问性更改为true
        field.setAccessible(true);
        //从给定对象中检索字段的值
        return field.get(obj);
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NullPointerException e) {
        e.printStackTrace();
    }
    return null;

}

public static ClassLoader getClassloader() {
    ClassLoader resultClassloader = null;
    //调用了currentActivityThread静态方法,获取 sCurrentActivityThread,是一个ActivityThread对象
    Object currentActivityThread = invokeStaticMethod(
            "android.app.ActivityThread", "currentActivityThread",
            new Class[]{}, new Object[]{});
    //从android.app.ActivityThread类的实例currentActivityThread中获取了mBoundApplication字段
    Object mBoundApplication = getFieldOjbect(
            "android.app.ActivityThread", currentActivityThread,
            "mBoundApplication");
    //从android.app.ActivityThread类的实例currentActivityThread获得了mInitialApplication
    Application mInitialApplication = (Application) getFieldOjbect("android.app.ActivityThread",
            currentActivityThread, "mInitialApplication");
    //从android.app.ActivityThread$AppBindData内部类的实例mBoundApplication中获取了info属性
    Object loadedApkInfo = getFieldOjbect(
            "android.app.ActivityThread$AppBindData",
            mBoundApplication, "info");
    //从android.app.LoadedApp类的实例loadedApkInfo中获取了mApplication
    Application mApplication = (Application) getFieldOjbect("android.app.LoadedApk", loadedApkInfo, "mApplication");
    //调用了mApplication的getClassLoader获取类加载器
    resultClassloader = mApplication.getClassLoader();
    return resultClassloader;
}

public static void fart() {
    //获取一个Classloader
    ClassLoader appClassloader = getClassloader();
    //将父类加载器赋值给了parentClassloader
    ClassLoader parentClassloader=appClassloader.getParent();
    //判断是否是BootClassLoader
    if(appClassloader.toString().indexOf("java.lang.BootClassLoader")==-1)
    {
        //如果不是 BootClassLoader 的实例,执行这里的代码
        fartwithClassloader(appClassloader);
    }
    //遍历父类加载器
    while(parentClassloader!=null){
        if(parentClassloader.toString().indexOf("java.lang.BootClassLoader")==-1)
        {
            // 如果不是 BootClassLoader 的实例,执行这里的代码
            fartwithClassloader(parentClassloader);
        }
  
        parentClassloader=parentClassloader.getParent();
    }
}

如果仅仅只是动态加载,没有加入到双亲委派关系中,只能用Frida枚举类加载器

遍历所有的类

public static Object getFieldOjbect(String class_name, Object obj,
                                    String filedName) {
    try {
        Class obj_class = Class.forName(class_name);
        Field field = obj_class.getDeclaredField(filedName);
        field.setAccessible(true);
        return field.get(obj);
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NullPointerException e) {
        e.printStackTrace();
    }
    return null;

}

 public static Object getClassFieldObject(ClassLoader classloader, String class_name, Object obj,
                                             String filedName) {
        try {
            Class obj_class = classloader.loadClass(class_name);//Class.forName(class_name);
            Field field = obj_class.getDeclaredField(filedName);
            field.setAccessible(true);
            Object result = null;
            result = field.get(obj);
            return result;
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;

    }

public static Field getClassField(ClassLoader classloader, String class_name,
                                  String filedName) {

    try {
        Class obj_class = classloader.loadClass(class_name);//Class.forName(class_name);
        Field field = obj_class.getDeclaredField(filedName);
        field.setAccessible(true);
        return field;
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return null;

}

public static void fartwithClassloader(ClassLoader appClassloader) {
     //申请List存放所有的dex
        List<Object> dexFilesArray = new ArrayList<Object>();
    //加载dalvik.system.BaseDexClassLoader类,从中获取appClassloader对象的pathList局部变量
        Object pathList_object = getFieldOjbect("dalvik.system.BaseDexClassLoader", appClassloader, "pathList");
    //加载dalvik.system.DexPathList类,从中获取pathList_object对象的dexElements
        Object[] ElementsArray = (Object[]) getFieldOjbect("dalvik.system.DexPathList", pathList_object, "dexElements");
    //此时的ElementsArray每一个成员都是一个Element
        Field dexFile_fileField = null;
        try {
            //获取dexFile的字段ID,便于后期的遍历
            dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element", "dexFile");
        } catch (Exception e) {
                e.printStackTrace();
            } catch (Error e) {
                e.printStackTrace();
            }
        Class DexFileClazz = null;
        try {
            DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
        } catch (Exception e) {
                e.printStackTrace();
            } catch (Error e) {
                e.printStackTrace();
            }
        Method getClassNameList_method = null;
        Method defineClass_method = null;
        Method dumpDexFile_method = null;
        Method dumpMethodCode_method = null;
		//遍历获得DexFile的方法,可以主动调用
        for (Method field : DexFileClazz.getDeclaredMethods()) {
            if (field.getName().equals("getClassNameList")) {
                getClassNameList_method = field;
                getClassNameList_method.setAccessible(true);
            }
            if (field.getName().equals("defineClassNative")) {
                defineClass_method = field;
                defineClass_method.setAccessible(true);
            }
            if (field.getName().equals("dumpDexFile")) {
                dumpDexFile_method = field;
                dumpDexFile_method.setAccessible(true);
            }
            if (field.getName().equals("dumpMethodCode")) {
                dumpMethodCode_method = field;
                dumpMethodCode_method.setAccessible(true);
            }
        }
        Field mCookiefield = getClassField(appClassloader, "dalvik.system.DexFile", "mCookie");
        Log.v("ActivityThread->methods", "dalvik.system.DexPathList.ElementsArray.length:" + ElementsArray.length);//5个
    //循环遍历dex,长度从ElementsArray获取,每一个成员就是一个Element
        for (int j = 0; j < ElementsArray.length; j++) {
            Object element = ElementsArray[j];
            Object dexfile = null;
            try {
                //从对象中取出dexFile
                dexfile = (Object) dexFile_fileField.get(element);
            } catch (Exception e) {
                e.printStackTrace();
            } catch (Error e) {
                e.printStackTrace();
            }
            if (dexfile == null) {
                Log.e("ActivityThread", "dexfile is null");
                continue;
            }
            if (dexfile != null) {
                //把得到的dex对象放到dexFilesArray
                dexFilesArray.add(dexfile);
                //通过appClassloader加载dalvik.system.DexFile,然后获取dexfile对象中的mCookie
                Object mcookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mCookie");
                //mCookie和mInternalCookie是一样的
                if (mcookie == null) {
                    //通过appClassloader加载dalvik.system.DexFile,然后获取dexfile对象中的mInternalCookie
                    Object mInternalCookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mInternalCookie");
                    if(mInternalCookie!=null)
                    {
						mcookie=mInternalCookie;
					}else{
							Log.v("ActivityThread->err", "get mInternalCookie is null");
							continue;
							}
                    
                }
                String[] classnames = null;
                try {
                    //主动调用getClassNameList,传入我们获得的mcookie,得到所有的类名
                    classnames = (String[]) getClassNameList_method.invoke(dexfile, mcookie);
                } catch (Exception e) {
                    e.printStackTrace();
                    continue;
                } catch (Error e) {
                    e.printStackTrace();
                    continue;
                }
                if (classnames != null) {
                    for (String eachclassname : classnames) {
                        //获取dex中所有的函数和主动调用,还有dumpMethodCode
                        loadClassAndInvoke(appClassloader, eachclassname, dumpMethodCode_method);
                    }
                }

            }
        }
        return;
    }

DexPathList的构造函数,在BaseDexClassLoader类中被调用赋值给了pathList

pathList是DexPathList类型,我们进去看看

dexElements就是 DexPathList构造函数的返回值,我们现在进入makeDexElements分析

dexElements就是elements,而elements是Element类型的数组,将加载后的dex封装成Element对象,然后加入数组中。

关系如下:

dex->Element->dexElements->pathList

所有要想获得dex,先获取pathList,然后获取dexElements

Element是一个内部类,我们用构造函数创建Element对象时,里面其实把dex赋值给了它的成员dexFile

mInternalCookie是由mCookie赋值来的

getClassNameList通过传入mCookie,也就是是dexFile*,就可以获取所有的类名

遍历类中 所有的函数

public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
        Class resultclass = null;
        Log.i("ActivityThread", "go into loadClassAndInvoke->" + "classname:" + eachclassname);
        try {
            //加载dex中的每一个类
            resultclass = appClassloader.loadClass(eachclassname);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        } catch (Error e) {
            e.printStackTrace();
            return;
        }
        if (resultclass != null) {
            try {
                //得到所有的构造函数
                Constructor<?> cons[] = resultclass.getDeclaredConstructors();
                for (Constructor<?> constructor : cons) {
                    if (dumpMethodCode_method != null) {
                        try {
                            //对构造函数进行主动调用+dump
                            dumpMethodCode_method.invoke(null, constructor);
                        } catch (Exception e) {
                            e.printStackTrace();
                            continue;
                        } catch (Error e) {
                            e.printStackTrace();
                            continue;
                        }
                    } else {
                        Log.e("ActivityThread", "dumpMethodCode_method is null ");
                    }

                }
            } catch (Exception e) {
                e.printStackTrace();
            } catch (Error e) {
                e.printStackTrace();
            }
            try {
                //得到所有的方法
                Method[] methods = resultclass.getDeclaredMethods();
                if (methods != null) {
                    for (Method m : methods) {
                        if (dumpMethodCode_method != null) {
                            try {
                                //对方法进行主动调用+dump
                               dumpMethodCode_method.invoke(null, m);
                             } catch (Exception e) {
                                e.printStackTrace();
                                continue;
                            } catch (Error e) {
                                e.printStackTrace();
                                continue;
                            }
                        } else {
                            Log.e("ActivityThread", "dumpMethodCode_method is null ");
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } catch (Error e) {
                e.printStackTrace();
            }
        }
    }

类的加载和初始化流程

类加载

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先,检查请求的类是否已经被加载过了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        // 如果父类加载器抛出ClassNotFoundException
        // 说明父类加载器无法完成加载请求
        }
        if (c == null) {
            // 在父类加载器无法加载的时候
            // 再调用本身的findClass方法来进行类加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

findClass是被重载的,需要我们去查看安卓源代码

basedexclassloader里有findClass

pathList就是DexPathList

现在我们要找 element的findClass

跟到loadClassBinaryName

注意 result = defineClassNative(name, loader, cookie, dexFile);

是一个native函数

这个函数先通过ConvertJavaArrayToDexFiles把mCookie转为dexFiles,然后获取类描述符,通过循环遍历dexFiles,FindClassDef,同时也把dexFile放到了dex_cache中

注意函数,进行dex重构

ObjPtr<mirror::Class> result = class_linker->DefineClass(soa.Self(),
                                                         descriptor.c_str(),
                                                         hash,
                                                         class_loader,
                                                         *dex_file,
                                                         *dex_class_def);

我们主要看LoadClass

加载了静态字段和实例字段,加载了VirtualMethod和DirectMethod

继续分析LoadMethod,设置了CodeItemOffset,执行完LoadMethod后,我们会得到一个ArtMethod

LinkCode

先获取quick_code,类似oat,jit,如果有判断是否启动解释模式

看一下ShouldUseInterpreterEntrypoint

判断是否是native方法,如果是则返回false

判断是否是quick_code,如果是则返回true

判断是否只在解释模式下执行,如果是则返回true

类初始化

先判断是否已经初始化了,防止多线程,然后判断是否能进行初始化,后面就是类验证

判断是否是一个接口,或者是否是一个父类,如果是一个父类就先初始化父类

对类静态字段进行初始化和调用了类的初始化函数

得到之后通过invoke调用

方法调用流程

native函数直接去源代码里看,命名规则 类_方法名 Method_invoke

分析一下InvokeMethod

最后将javaMethod变成了ArtMethod

往下走分析下InvokeMethodImpl

这里的 InvokeWithArgArray在使用jni调用javaMethod时也会使用到

所以java的方法最终会走到 ArtMethod的invoke执行

这里可以作为抽取加固的脱壳点,保存codeitem之后然后返回

判断是否解释执行、是否是native函数,然后进入EnterInterpreterFromInvoke

其中有 Execute,是我们之前整体加固的脱壳点

dumpMethodCode_method

native函数

extern "C" ArtMethod* jobject2ArtMethod(JNIEnv* env, jobject javaMethod) {
  ScopedFastNativeObjectAccess soa(env);
  ArtMethod* method = ArtMethod::FromReflectedMethod(soa, javaMethod);
  return method;
}
extern "C" void myfartInvoke(ArtMethod* artmethod)  REQUIRES_SHARED(Locks::mutator_lock_) {
	JValue *result=nullptr;//结果
	Thread *self=nullptr;//线程
	uint32_t temp=6;
	uint32_t* args=&temp;//参数数组
	uint32_t args_size=6;//参数数量
	artmethod->Invoke(self, args, args_size, result, "fart");
}

static void DexFile_dumpMethodCode(JNIEnv* env, jclass,jobject method) {
  if(method!=nullptr)
  {
		  ArtMethod* proxy_method = jobject2ArtMethod(env, method);
		  myfartInvoke(proxy_method);
	  }	 

  return;
}

其实就是把传入的jobject的method转换成了ArtMethod,然后调用ArtMethod的Invoke

Invoke里加了代码

如果传入的self是nullptr,就会执行dumpArtMethod

extern "C" void dumpArtMethod(ArtMethod* artmethod)  REQUIRES_SHARED(Locks::mutator_lock_) {
        char *dexfilepath=(char*)malloc(sizeof(char)*1000);	
        if(dexfilepath==nullptr)
        {
            LOG(ERROR) << "ArtMethod::dumpArtMethodinvoked,methodname:"<<artmethod->PrettyMethod().c_str()<<"malloc 1000 byte failed";
            return;
        }
        int result=0;
        int fcmdline =-1;
        char szCmdline[64]= {0};
        char szProcName[256] = {0};
        int procid = getpid();
        sprintf(szCmdline,"/proc/%d/cmdline", procid);
        fcmdline = open(szCmdline, O_RDONLY,0644);
        if(fcmdline >0)
        {
            result=read(fcmdline, szProcName,256);
            if(result<0)
            {
                LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open cmdline file file error";									
            }
            close(fcmdline);
        }

        if(szProcName[0])
        {
                  const DexFile* dex_file = artmethod->GetDexFile();
                  const uint8_t* begin_=dex_file->Begin();  // Start of data.
                  size_t size_=dex_file->Size();  // Length of data.

                  memset(dexfilepath,0,1000);
                  int size_int_=(int)size_;

                  memset(dexfilepath,0,1000);
                  sprintf(dexfilepath,"%s","/sdcard/fart");
                  mkdir(dexfilepath,0777);

                  memset(dexfilepath,0,1000);
                  sprintf(dexfilepath,"/sdcard/fart/%s",szProcName);
                  mkdir(dexfilepath,0777);

                  memset(dexfilepath,0,1000);
                  sprintf(dexfilepath,"/sdcard/fart/%s/%d_dexfile.dex",szProcName,size_int_);
                  int dexfilefp=open(dexfilepath,O_RDONLY,0666);
                  if(dexfilefp>0){
                      close(dexfilefp);
                      dexfilefp=0;

                      }else{
                                  int fp=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
                                  if(fp>0)
                                  {
                                      result=write(fp,(void*)begin_,size_);
                                      if(result<0)
                                        {
                                            LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,open dexfilepath file error";

                                        }
                                      fsync(fp); 
                                      close(fp);  
                                      memset(dexfilepath,0,1000);
                                      sprintf(dexfilepath,"/sdcard/fart/%s/%d_classlist.txt",szProcName,size_int_);
                                      int classlistfile=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
                                        if(classlistfile>0)
                                        {
                                            for (size_t ii= 0; ii< dex_file->NumClassDefs(); ++ii) 
                                            {
                                                const DexFile::ClassDef& class_def = dex_file->GetClassDef(ii);
                                                const char* descriptor = dex_file->GetClassDescriptor(class_def);
                                                result=write(classlistfile,(void*)descriptor,strlen(descriptor));
                                                if(result<0)
                                                {
                                                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";

                                                    }
                                                const char* temp="\n";
                                                result=write(classlistfile,(void*)temp,1);
                                                if(result<0)
                                                {
                                                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write classlistfile file error";

                                                    }
                                                }
                                              fsync(classlistfile); 
                                              close(classlistfile); 

                                            }
                                      }


                                  }
                    //获取code_item
                      const DexFile::CodeItem* code_item = artmethod->GetCodeItem();
                      if (LIKELY(code_item != nullptr)) 
                      {				  
                              int code_item_len = 0;
                              uint8_t *item=(uint8_t *) code_item;
                              if (code_item->tries_size_>0) {
                                  const uint8_t *handler_data = (const uint8_t *)(DexFile::GetTryItems(*code_item, code_item->tries_size_));
                                  uint8_t * tail = codeitem_end(&handler_data);
                                  code_item_len = (int)(tail - item);
                              }else{
                                  code_item_len = 16+code_item->insns_size_in_code_units_*2;
                              }  
                                  memset(dexfilepath,0,1000);
                                  int size_int=(int)dex_file->Size();  
                                  uint32_t method_idx=artmethod->GetDexMethodIndexUnchecked();
                                  sprintf(dexfilepath,"/sdcard/fart/%s/%d_ins_%d.bin",szProcName,size_int,(int)gettidv1());
                                  int fp2=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666);
                                  if(fp2>0){
                                      lseek(fp2,0,SEEK_END);
                                      memset(dexfilepath,0,1000);
                                      int offset=(int)(item - begin_);
                                      sprintf(dexfilepath,"{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:",artmethod->PrettyMethod().c_str(),method_idx,offset,code_item_len);
                                      int contentlength=0;
                                      while(dexfilepath[contentlength]!=0) contentlength++;
                                      result=write(fp2,(void*)dexfilepath,contentlength);
                                      if(result<0)
                                                {
                                                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";

                                                    }
                                      long outlen=0;
                                      char* base64result=base64_encode((char*)item,(long)code_item_len,&outlen);
                                      result=write(fp2,base64result,outlen);
                                      if(result<0)
                                                {
                                                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";

                                                    }
                                      result=write(fp2,"};",2);
                                      if(result<0)
                                                {
                                                    LOG(ERROR) << "ArtMethod::dumpdexfilebyArtMethod,write ins file error";

                                                    }
                                      fsync(fp2); 
                                      close(fp2);
                                      if(base64result!=nullptr){
                                          free(base64result);
                                          base64result=nullptr;
                                          }
                                       }

                        }


        }

        if(dexfilepath!=nullptr)
        {
            free(dexfilepath);
            dexfilepath=nullptr;
        }

}

Codeltem起始地址的获取:artMethod->GetCodeltem()

Codeltem长度的计算:dex_file->GetCodeItemSize(const Codeltem& code)

MethodIndex的获取:artMethod->GetDexMethodIndex()

将FART迁移到安卓10

先在core-java-com下创建一个文件夹whitebird,然后创建了一个类saveDex,里面装了一些工具方法

将fart源码中的图中方法进行移植过来,我们先看fartthread,修改名字为whitebirdthread,里面开启了一个线程,防止app卡顿,然后休眠60秒后开始脱壳

上面代码之前我们分析过,后面都一样,修复一下,然后将自己实现的类和包加入白名单

我们需要在ActivityThread中启动脱壳的线程,需要在makeApplication之后,为了让壳的代码跑完,类加载,防止出问题

现在saveMethodCode是在c层实现,我们需要去clion中修改,进行函数注册

函数的实现

还需要对调用的函数进行声明

在java_lang_reflect_Method.cc进行实现

在art_method.cc进行实现

Invoke里我们也要修改

由于参数是我们乱传的,所以当判断self为nullptr时,说明是我们自己调用,此时dump完就可以返回了,往后面继续执行可能会报错

前面是整体加固脱壳,我们重点关注后面抽取加固脱壳

const dex::CodeItem* code_item = artMethod->GetCodeItem();
       if (LIKELY(code_item != nullptr))
       {
           uint8_t *item=(uint8_t *) code_item;
           uint32_t code_item_len = dex_file->GetCodeItemSize(*code_item);
           memset(dexFilePath,0,1000);
           int size_int=(int)dex_file->Size();
           uint32_t method_idx=artMethod->GetDexMethodIndex();
           sprintf(dexFilePath,"/data/data/%s/whitebird/%d_ins_%d.bin",szProcName,size_int,(int)gettidv1());
           int fp2=open(dexFilePath,O_CREAT|O_APPEND|O_RDWR,0666);
           if(fp2>0){
               lseek(fp2,0,SEEK_END);
               memset(dexFilePath,0,1000);
               int offset=(int)(item - begin_);
               sprintf(dexFilePath,"{name: '%s',method_idx: %d,offset:%d,code_item_len:%d,ins:'",
                       artMethod->PrettyMethod().c_str(),
                       method_idx,
                       offset,
                       code_item_len);
               int contentlength=0;
               while(dexFilePath[contentlength]!=0) contentlength++;
               result=write(fp2,(void*)dexFilePath,contentlength);
               if(result<0)
               {
                   LOG(ERROR) << "ArtMethod::saveArtMethod,write ins file error";

               }
               long outlen=0;
               char* base64result=base64_encode((char*)item,(long)code_item_len,&outlen);
               result=write(fp2,base64result,outlen);
               if(result<0)
               {
                   LOG(ERROR) << "ArtMethod::saveArtMethod,write ins file error";

               }
               result=write(fp2,"'};",3);
               if(result<0)
               {
                   LOG(ERROR) << "ArtMethod::saveArtMethod,write ins file error";

               }
               fsync(fp2);
               close(fp2);
               if(base64result!=nullptr){
                   free(base64result);
                   base64result=nullptr;
               }
           }

       }

然后就可以编译刷机了,运行完查看目录下文件

重点关注

这里的dex是通过整体加固方式脱下来的,bin是通过抽取加固脱下来的

MainActivity已经修复了,但是AES、DES这些没有,因为MainActivity在程序执行时被动调用了且没有还原回去,所以我们等待60s后通过整体加固可以脱下来,但是AES、DES等并没有被执行,需要通过bin文件修复

如果把上面的dex直接拖入新版jadx会报错,需要修改jadx的配置,在文件->首选项,yes改为no

13、dex重构

项目地址:https://github.com/dqzg12300/dexfixer

导入idea,对项目做一些修改

package com.android.dx.unpacker;

import com.android.dex.util.ByteInput;
import com.android.dex.util.ByteOutput;
import com.google.gson.Gson;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class MethodCodeItemFile
{
    private ByteBuffer data;
    private Map<Integer, MethodCodeItem> map;
    private String jsondata;

    public MethodCodeItemFile(File file)
    {
        try (InputStream inputStream = new FileInputStream(file))
        {
            //把读取后的结果放到jsondata中
            loadFrom(inputStream);
            this.map = new HashMap<>();
            Gson gson=new Gson();
            //将jsondata数据按;分割,因为bin文件中以;分割
            String[] items=this.jsondata.split(";");
            for(int i=0;i<items.length;i++){
                //解析json数据并且赋值
                String codeJson=items[i];
                JsonCodeItem codedata= gson.fromJson(codeJson,JsonCodeItem.class);
                MethodCodeItem codeItem = new MethodCodeItem();
                codeItem.index = codedata.method_idx;
                codeItem.descriptor = codedata.name;
                codeItem.size = codedata.code_item_len;
                byte[] buff= Base64.getDecoder().decode(codedata.ins);
                codeItem.code = buff;
                this.map.put(codeItem.index, codeItem);
            }

//            while (data.hasRemaining())
//            {
//                MethodCodeItem codeItem = new MethodCodeItem();
//                codeItem.index = readInt();
//                codeItem.descriptor = readCString();
//                codeItem.size = readInt();
//                codeItem.code = readByteArray(codeItem.size);
//                this.map.put(codeItem.index, codeItem);
//            }
        }
        catch (Exception e)
        {
            System.out.println("Warn: " + file.getPath() + " maybe invalid format!");
            e.printStackTrace();
        }
    }

    public Map<Integer, MethodCodeItem> getMethodCodeItems()
    {
        return this.map;
    }

    public byte[] readByteArray(int length)
    {
        byte[] result = new byte[length];
        data.get(result);
        return result;
    }

    public int readInt()
    {
        return data.getInt();
    }

    public String readCString()
    {
        byte b;
        StringBuilder s = new StringBuilder("");
        do
        {
            b = data.get();
            if (b != 0)
            {
                s.append((char) b);
            }
        }
        while (b != 0);
        return String.valueOf(s);
    }

    private void loadFrom(InputStream in) throws IOException
    {
        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
        byte[] buffer = new byte[8192];

        int count;
        while ((count = in.read(buffer)) != -1)
        {
            bytesOut.write(buffer, 0, count);
        }

//        this.data = ByteBuffer.wrap(bytesOut.toByteArray());
//        this.data.order(ByteOrder.LITTLE_ENDIAN);
        this.jsondata=bytesOut.toString();
    }
}

删除了对json数据加引号的操作,因为我们在保存bin时已经加了引号

配置参数然后运行

拖入jadx看看

已经修复完成了

将项目打包成jar包

已经编译好了,可以直接拿来修复dex

14、FART存在的问题

1、调用链深度不够,有些壳将原有函数体替换为解密代码,运行时才解密执行

2、有些壳设置一些垃圾类,当该类被初始化时自动退出

3、有些壳设置一些垃圾类,实时检测这些类是否加载

4、动态加载的dex文件,如果没有修正ClassLoader,不会出现在双亲委派关系中,也不会被FART遍历到

15、FART改进方案

1、学习youpk的调用链深度

2、不进行类的初始化,或者不主动调用该类

通过过滤类或者使用loadclass,而不是Class.forName

3、设置配置文件,类似白名单,对指定类进行主动调用,或者避开指定类的调用

利用Frida主动调用FART的函数,对指定类进行脱壳

4、利用Frida枚举所有ClassLoader,再主动调用FART的函数进行脱壳

Exexute脱壳点对于动态加载的dex也可以脱,除非这个dex没有类的初始化函数

16、使用frida调用fart函数接口

对某个dex进行脱壳

先给自动开启抽取脱壳的线程注释掉,这样就不会在app启动时进行抽取脱壳,但是实现抽取脱壳的函数仍然在系统中,可以作为api调用

先获取一下类加载器

Java.perform(function () {
    Java.choose("dalvik.system.DexClassLoader",{
        onMatch:function (instance){
            console.log(instance);
        },onComplete:function (){

        }
    })
})

拿到DexClassLoader,我们就可以传入fartwithClassloader

Java.perform(function () {
    Java.choose("dalvik.system.DexClassLoader",{
        onMatch:function (instance){
            console.log(instance);
           var saveDex = Java.use("com.whitebird.saveDex");
           console.log(saveDex);
           saveDex.whietbirdwithClassloader(instance);
        },onComplete:function (){

        }
    })
})

我对fart源码做了改名,fartwithClassloader就是下面的witebirdwithClassloader

调用完看看/data/data/com.xiaojianbang.app/whitebird

已经有一些bin文件了

对某个类进行脱壳

比如我们想要脱DES,代码如下

Java.perform(function () {
    var saveDex = Java.use("com.whitebird.saveDex");
    var classloader = saveDex.getClassloader();
    console.log("classloader:",classloader);

    var dexFile = Java.use("dalvik.system.DexFile");
    var params = [Java.use("java.lang.Object").class];
    var dexFileclszz = dexFile.class;
    var saveMethodCode = dexFileclszz.getDeclaredMethod('saveMethodCode',params);
    console.log("saveMethodCode: ", saveMethodCode);
    saveMethodCode.setAccessible(true);
    saveDex.loadClassAndInvoke(classloader,'com.xiaojianbang.encrypt.DES',saveMethodCode);
})

17、其他加固形式

1、VMP

VMP保护一般针对部分函数,这些函数会被native化

定位解释器是关键,找到映射关系便可恢复

VMP通常共用一个解释器

被VMP保护的函数,通常会注册到同一个地址上,或者函数逻辑相似

2、dex2c

通过词法分析、语法分析等,进行Java到C的等价转换,彻底还原难度大

会有大量的JNI相关的api调用

dex2c通常注册在不同的地址上,并且函数逻辑不相似

3、多种方式混合加固

先部分函数VMP加固,再抽取加固,再整体加固

也就会出现别人所说的脱不干净,可能就是某些函数是VMP加固的

三、代码追踪

1、方法调用流程回顾

java层直接调用

method的invoke是一个native函数,我们去安卓源码里看

还得看InvokeMethod

这里的方法把javaMethod转成了ArtMethod

往下找到InvokeMethodImpl,传入了artmethod,进去分析

继续跟进去

最终就走到了method->Invoke了

通过jni调用

通过jni调用其实就是CallVoidMethod

然后继续跟InvokeVirtualOrInterfaceWithVarArgs

还是走到了 InvokeWithArgArray,后面的流程就一样了,也是调用method->Invoke

2、方法的解释执行流程

首先把函数放入函数栈中,然后判断是否走解释执行,如果是则进入EnterInterpreterFromInvoke,进去分析

如果不是native方法,就进行Excute,而Excute是我们整体加固的脱壳点

我们需要注意下面的代码

在文件中搜索kMterpImplKind,表示选择解释器

kMterpImplKind代表汇编实现的解释器,kSwitchImplKind代表C++实现的switch解释器

我们改成走C++的代码,方便我们后续修改代码

设置好后我们继续往后面分析

都走的同一个函数

里面就是执行函数了

获取dexpc,也就是指向smali指令的地址,然后取出指令,在while循环中进行执行

3、invoke指令的执行流程

上面过程结束后,对于smali中的函数调用就会调用DoInvoke执行

当我们走到 invoke-static {v1}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher;的时候,这个时候shadow_frame是decryptAES的栈帧

此时shadow_frame.GetMethod()就是decryptAES

called_method就是在decryptAES里面被调用的函数,因此我们在下面可以打印调用关系

sf_method->called_method

而且此时我们只能获得sf_method的参数,而不能得到called_method的参数,因为called_method还没有执行,因此还得往后分析

这里传入了called_method,所以要进去分析

还得进度DoCallCommon分析

进行参数赋值和创建一个新的栈帧

这时候参数和寄存器已经处理好了,callee_frame是被调用函数的栈帧

通过判断是否走解释模式,进入ArtInterpreterToInterpreterBridge或者ArtInterpreterToCompiledCodeBridge

又进行Execute执行,属于递归套娃

4、强制解释执行原理

我们上面的分析都走的解释执行,所以解释执行是前提

之前我们分析class_linker的时候,LoadClass会执行LoadMethod和LinkCode,而LinkCode里去判断了是否走解释执行模式

ShouldUseInterpreterEntrypoint去申请是否走解释执行模式,所以我们要让这个函数返回true

如果是native函数,返回false

如果quickcode是空,返回true

如果InterpreOnly,表示只能解释执行,返回true

看一下InterpreOnly这个函数

InterpreOnly返回interpret_only_,interpret_only_由ForceInterpretOnly()赋值,所以我们想让ForceInterpretOnly()执行,所以我们可以在安卓系统中主动调用这个函数

5、追踪Java函数调用关系

所在文件:art/runtime/common_dex_operations.h  PerformCall函数
    // add
    ArtMethod* callee = callee_frame->GetMethod();
    std::ostringstream oss;
    oss << "[PerformCall] " << caller_method->PrettyMethod() << " --> " << callee->PrettyMethod();
    if(strstr(oss.str().c_str(),"PerformCallBefore")){
        LOG(ERROR) << oss.str();
    }
    // add

我们先以360加固的app为例,因为它是走在解释执行模式的,用frida去hook strstr的参数

Java.perform(function(){
    var strStr = Module.findExportByName("libc.so", "strstr");
    console.log("[*] strstr addr: " + strStr);
    Interceptor.attach(strStr,{
        onEnter: function(args){
            var arg0= ptr(args[0]).readCString();
            var arg1= ptr(args[1]).readCString();
            if(arg1.indexOf("PerformCallBefore")>=0){
                console.log("[*] strstr hooked"+arg0);
            }

        },
        onLeave: function(retval){
        }
    })
})

当点击MD5加密后,就打印日志了,输入onClick定位关键代码

但是这里有个native函数,而native函数里的jni函数执行流程并没有打印出来,也就说明jni函数没有走perform流程

6、追踪jni函数调用关系

JniMethodStart是jni函数调用的最开始执行的方法

我们注意图中的圈出来的代码

ArtMethod* native_method = *self->GetManagedStack()->GetTopQuickFrame();

通过self,其实也就是Thread对象,然后调用了GetManagedStack,获取了ManagedStack对象,从里面调用

GetTopQuickFrame拿到了native_method

我们的思路就由此得到,代码如下

// add
ArtMethod* artMethod = nullptr;
Thread* self = Thread::Current();
const ManagedStack* managedStack = self->GetManagedStack();
if(managedStack != nullptr) {
    ArtMethod** tmpArtMethod = managedStack->GetTopQuickFrame();
    if(tmpArtMethod != nullptr) {
        artMethod = *tmpArtMethod;
    }
}
if(artMethod != nullptr) {
    std::ostringstream oss;
    oss << "[InvokeWithArgArray before] " << artMethod->PrettyMethod() << " --> "<< method->PrettyMethod();
    if(strstr(oss.str().c_str(),"InvokeWithArgArrayBefore")){
        LOG(ERROR) << oss.str();
    }
}
// add

jni函数执行到这时,此时的method其实被调用函数,soa.Self就是Thread*,所以我们可以使用soa.Self(),或者Thread::Current(),而函数调用其实有管理栈的,就是managedStack,如果我们以a->b->c为例,c在执行时,我们通过GetTopQuickFrame得到的就是栈顶函数,也就是b,这样我们就得到了调用函数,剩下的就和java函数调用一样进行打印就行了。

ManagerStack介绍

ManagerStack是用来表示函数栈的,它有三个成员

TaggedTopQuickFrame tagged_top_quick_frame_;
ManagedStack* link_;
ShadowFrame* top_shadow_frame_;

link_可以取出再上一层的ManagerStack

tagged_top_quick_frame_指向Quick Frame Stack的栈顶成员,类型是ArtMethod*,所以tagged_top_quick_frame_是ArtMethod**类型

top_shadow_frame_指向Shadow Frame Stack的栈顶成员,是一个结构体,里面的第一个成员保存函数的栈帧,第二个成员link指向上一个函数的结构体,第三个成员是指向ArtMethod,所以通过shadow_frame的getMethod,可以得到ArtMethod

测试一下效果

比之前多了一些输出,说明我们的修改是成功的,jni函数调用输出功能正常

Java.perform(function(){

    var strStr = Module.findExportByName("libc.so", "strstr");
    console.log("[*] strstr addr: " + strStr);
    Interceptor.attach(strStr,{
        onEnter: function(args){
            var arg0= ptr(args[0]).readCString();
            var arg1= ptr(args[1]).readCString();
            if(arg1.indexOf("PerformCallBefore")>=0){
                console.log("[*] PerformCallBefore hooked"+arg0);
            }
            if(arg1.indexOf("InvokeWithArgArrayBefore")>=0){
                console.log("[*] InvokeWithArgArrayBefore hooked"+arg0);
            }
        },
        onLeave: function(retval){
        }
    })

})

7、强制运行在解释模式下

在文件:art/runtime/interpreter/interpreter.cc 增加函数

// add
extern "C" void forceInterpret(){
    Runtime* runtime = Runtime::Current();
    runtime->GetInstrumentation()->ForceInterpretOnly();
    LOG(WARNING) << "forceInterpret is called";
}
// add

这个之前说过原理,同时将解释器改为switch

function hook_strstr(){
    var strStr = Module.findExportByName("libc.so", "strstr");
    console.log("[*] strstr addr: " + strStr);
    Interceptor.attach(strStr,{
        onEnter: function(args){
            var arg0= ptr(args[0]).readCString();
            var arg1= ptr(args[1]).readCString();
            if(arg1.indexOf("PerformCallBefore")>=0){
                console.log("[*] PerformCallBefore hooked"+arg0);
            }
            if(arg1.indexOf("InvokeWithArgArrayBefore")>=0){
                console.log("[*] InvokeWithArgArrayBefore hooked"+arg0);
            }
        },
        onLeave: function(retval){
        }
    })
}

function forceInterpret(){
    var artModule = Process.findModuleByName("libart.so");
    var forInterpretAddr = artModule.getExportByName("forceInterpret");
    console.log("forceInterpret address:"+forceInterpret);
    Interceptor.attach(forInterpretAddr,{
        onEnter:function (args) {
        console.log("onEnter forceInterpret");

        },onLeave :function (retval){
            console.log("onleave forceInterpret");
        }
    })
    var forceInterpretFunc = new NativeFunction(forInterpretAddr,"void",[]);
    forceInterpretFunc();
}
setImmediate(forceInterpret);

8、追踪每一条smali指令

也就是这里,只不过我们没开启。但是有个问题:如果直接开启,每个方法执行都会经过这里,就会导致手机很卡,所以我们需要做过滤

可以看到可以打印每一条执行的smali代码和寄存器值

先定义一个函数,用来开启smail指令追踪功能

// add
bool shouldTrace = false;
if(strstr(shadow_frame.GetMethod()->PrettyMethod().c_str(), "ExecuteSwitchImplCppBefore")) {
    shouldTrace = true;
}
// add


//add
//TraceExecution(shadow_frame, inst, dex_pc);
if (shouldTrace) {
    MyTraceExecution(shadow_frame, inst, dex_pc);
}
//add

MyTraceExecution是我们自己实现的smali代码追踪功能,其实也就是利用了TraceExecution的代码

通过shouldTrace去开启,而shouldTrace是通过if(strstr(shadow_frame.GetMethod()->PrettyMethod().c_str(), “ExecuteSwitchImplCppBefore”))成立才会赋值为true,我们到时候直接frida去hook strstr,返回1就行了

static inline void MyTraceExecution(const ShadowFrame& shadow_frame, const Instruction* inst,const uint32_t dex_pc)REQUIRES_SHARED(Locks::mutator_lock_) {
        std::ostringstream oss;
        oss <<"[FuncName:]"<< shadow_frame.GetMethod()->PrettyMethod()<<"\t\t"
            << android::base::StringPrintf("[Address] 0x%x: ", dex_pc)
            << inst->DumpString(shadow_frame.GetMethod()->GetDexFile()) << "\t [Regs]";
        for (uint32_t i = 0; i < shadow_frame.NumberOfVRegs(); ++i) {
            uint32_t raw_value = shadow_frame.GetVReg(i);
            ObjPtr<mirror::Object> ref_value = shadow_frame.GetVRegReference(i);
            oss << android::base::StringPrintf(" vreg%u=0x%08X", i, raw_value);
            if (ref_value != nullptr) {
                if (ref_value->GetClass()->IsStringClass() &&
                    !ref_value->AsString()->IsValueNull()) {
                    oss << "/java.lang.String \"" << ref_value->AsString()->ToModifiedUtf8() << "\"";
                } else {
                    oss << "/" << ref_value->PrettyTypeOf();
                }
            }
        }
    if(strstr(oss.str().c_str(), "MyTraceExecutionBefore")) {
        LOG(ERROR)<<oss.str().c_str();
    }
    }

刷机就行了,frida代码

function hook_strstr(){
    var strStr = Module.findExportByName("libc.so", "strstr");
    console.log("[*] strstr addr: " + strStr);
    Interceptor.attach(strStr,{
        onEnter: function(args){
            this.arg0= ptr(args[0]).readCString();
            this.arg1= ptr(args[1]).readCString();

            if(this.arg1.indexOf("MyTraceExecutionBefore")>=0){
                console.log("[*] MyTraceExecutionBefore hooked"+this.arg0);
            }
        },
        onLeave: function(retval){
            if(this.arg1.indexOf("ExecuteSwitchImplCppBefore")>=0){
               retval.replace(1)
            }
        }
    })
}

建议APP启动后,再使用这个函数,否则APP会非常卡

四、监控网络访问

1、常见的抓包方式

1.1、使用抓包工具

绕过抓包相关检测,让抓包工具能够正常抓包

好处是可以更舒服、更全的看到数据包

缺点是可能需要逆向寻找检测点,不同框架有不同的检测证书方法

1.2、Hook抓包

通过Hook直接获取通信过程中的明文数据

通信过程中的数据会一步步交给系统函数来进行加密,Java层和JNI层

比如libssl.so,可以Hook固定的系统相关函数

使用自定义ssl进行加密,需要逆向找到相关加密函数来Hook

1.3、监控网络访问

与Hook抓包差不多,区别是通过修改系统代码实现

2、okhttp3源码分析

doGet

package com.example.okhttp3;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        OkHttpClient client = new OkHttpClient.Builder().build();
        new Thread(){
            public void run(){
             Request request = new Request.Builder()
                .url("https://www.baidu.com/")
                .get()
                .addHeader(
                        "User-Agent",
                        "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36"
                )
                .build();
        try {
            Response response = client.newCall(request).execute();
            Log.d("whitebird", "response: " + response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}.start();
    }
}

在xml中记得授予权限

<uses-permission android:name="android.permission.INTERNET" />

得到的结果

2023-09-22 18:02:30.711  7599-7633  whitebird               com.example.okhttp3                  D  response: <!DOCTYPE html><!--STATUS OK--><html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta content="always" name="referrer"><meta name="theme-color" content="#ffffff"><meta name="description" content="全球领先的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。"><link rel="shortcut icon" href="https://www.baidu.com/favicon.ico" type="image/x-icon" /><link rel="search" type="application/opensearchdescription+xml" href="/content-search.xml" title="百度搜索" /><link rel="icon" sizes="any" mask href="https://www.baidu.com/favicon.ico"><link rel="dns-prefetch" href="//dss0.bdstatic.com"/><link rel="dns-prefetch" href="//dss1.bdstatic.com"/><link rel="dns-prefetch" href="//ss1.bdstatic.com"/><link rel="dns-prefetch" href="//sp0.baidu.com"/><link rel="dns-prefetch" href="//sp1.baidu.com"/><link rel="dns-prefetch" href="//sp2.baidu.com"/><link rel="dns-prefetch" href="//pss.bdstatic.com"/><link rel="apple-touch-icon-precomposed" href="https://psstatic.cdn.bcebos.com/video/wiseindex/aa6eef91f8b5b1a33b454c401_1660835115000.png"><title>百度一下,你就知道</title><style index="newi" type="text/css">#form .bdsug{top:39px}.bdsug{display:none;position:absolute;width:535px;background:#fff;border:1px solid #ccc!important;_overflow:hidden;box-shadow:1px 1px 3px #ededed;-webkit-box-shadow:1px 1px 3px #ededed;-moz-box-shadow:1px 1px 3px #ededed;-o-box-shadow:1px 1px 3px #ededed}.bdsug li{width:519px;color:#000;font:14px arial;line-height:25px;padding:0 8px;position:relative;cursor:default}.bdsug li.bdsug-s{background:#f0f0f0}.bdsug-store span,.bdsug-store b{color:#7A77C8}.bdsug-store-del{font-size:12px;color:#666;text-decoration:underline;position:absolute;right:8px;top:0;cursor:pointer;display:none}.bdsug-s .bdsug-store-del{display:inline-block}.bdsug-ala{display:inline-block;border-bottom:1px solid #e6e6e6}.bdsug-ala h3{line-height:14px;background:url(//www.baidu.com/img/sug_bd.png?v=09816787.png) no-repeat left center;margin:6px 0 4px;font-size:12px;font-weight:400;color:#7B7B7B;padding-left:20px}.bdsug-ala p{font-size:14px;font-weight:700;padding-left:20px}#m .bdsug .bdsug-direct p{color:#00c;font-weight:700;line-height:34px;padding:0 8px;margin-top:0;cursor:pointer;white-space:nowrap;overflow:hidden}#m .bdsug .bdsug-direct p img{width:16px;height:16px;margin:7px 6px 9px 0;vertical-align:middle}#m .bdsug .bdsug-direct p span{margin-left:8px}#form .bdsug .bdsug-direct{width:auto;padding:0;border-bottom:1px solid #f1f1f1}#form .bdsug .bdsug-direct p i{font-size:12px;line-height:100%;font-style:normal;font-weight:400;color:#fff;background-color:#2b99ff;display:inline;text-align:center;padding:1px 5px;*padding:2px 5px 0;margin-left:8px;overflow:hidden}.bdsug .bdsug-pcDirect{color:#000;font-size:14px;line-height:30px;height:30px;background-color:#f8f8f8}.bdsug .bdsug-pc-direct-tip{position:absolute;right:15px;top:8px;width:55px;height:15px;display:block;background:url(https://pss.bdstatic.com/r/www/cache/static/protocol/https/global/img/pc_direct_42d6311.png) no-repeat 0 0}.bdsug li.bdsug-pcDirect-s{background-color:#f0f0f0}.bdsug .bdsug-pcDirect-is{color:#000;font-size:14px;line-height:22px;background-color:#f5f5f5}.bdsug .bdsug-pc-direct-tip-is{position:absolute;right:15px;top:3px;width:55px;height:15px;display:block;background:url(https://pss.bdstatic.com/r/www/cache/static/protocol/https/global/img/pc_direct_42d6311.png) no-repeat 0 0}.bdsug li.bdsug-pcDirect-is-s{background-color:#f0f0f0}.bdsug .bdsug-pcDirect-s .bdsug-pc-direct-tip,.bdsug .bdsug-pcDirect-is-s .bdsug-pc-direct-tip-is{background-position:0 -15px}.bdsug .bdsug-newicon{color:#929292;opacity:.7;font-size:12px;display:inline-block;line-height:22px;letter-spacing:2px}.bdsug .bdsug-s .bdsug-newicon{opacity:1}.bdsug .bdsug-newicon i{letter-spacing:0;font-style:normal}.bdsug .bdsug-feedback-wrap{display:none}.tog

doPost

package com.example.okhttp3;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import java.io.IOException;

import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        OkHttpClient client = new OkHttpClient.Builder().build();
        new Thread(){
            public void run(){
                FormBody formBody = new FormBody.Builder().add("user", "whitebird").add("pass", "123456").build();
             Request request = new Request.Builder()
                .url("https://www.baidu.com/")
                .post(formBody)
                .addHeader(
                        "User-Agent",
                        "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36"
                )
                .build();
        try {
            Response response = client.newCall(request).execute();
            Log.d("whitebird", "response: " + response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}.start();
    }
}

通过调试源码发现

此时的socket是java.net.Socket,里面还没有装数据

通过Socket对象的getOutputStream()方法可以获取到一个用于向服务器发送数据的OutputStream对象。这个方法返回的是一个字节输出流。

okhttp的三个检测点

检测tls,检测hostname,检测证书

往下继续看

这里的socket是我们需要的,里面有数据,类型class com.android.org.conscrypt.Java8FileDescriptorSocket

我们现在去安卓源码网站分析Java8FileDescriptorSocket的getOutputStream

没有搜到,去父类里面看看

继续关注SSLOutputStream

是一个内部类,我们的数据很有可能通过这里的write写入

继续往下分析

这里的ssl是一个NativeSsl

在里面找到write方法

这里需要关注

NativeCrypto.SSL_write(ssl, this, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);

是一个native函数

所以我们只要hook这里的write就能获得提交的数据,同理hook read就能获得返回的响应数据

先获取了数据长度,然后把java字节数组转换成了c中的数组中,判断发生数据的大小,如果小于1024(1M)就直接调用sslWrite,否则就循环发送数据,还是走sslWrite,所以只需要分析sslWrite就行了

里面又调用了SSL_write(ssl, buf, len);

boringssl是由谷歌从openssl改过来的,ssl_lib.cc编译以后libssl.so中

这两个就是r0capture的hook点

直接点击write_app_data搜索不到,需要前面加上版本ssl3_write_app_data

in就是之前的buf,传入了do_ssl3_write

这里后面就开始进行数据加密了

后面我们不怎么关注了,最后会走到sock_write

里面用的send和write都是libc里面的函数了

最终选择的hook点

libssl.so中//明文状态
int SSL_write(SSL *ssl, const void *buf, int num) //提交
int SSL_read(SSL *ssl, void *buf, int num)//返回
libc.so中 //考虑到有些app会绕过正常流程,直接使用write和read
ssize_t write(int fd, const void * buf, size_t count)
ssize_t read(int fd, void * buf, size_t count)

代码添加位置,注意还得添加头文件

//add
#include<ctype.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
//add

/add
if(getuid()>10000){
        if(SSL_LOG_PATH == nullptr){
            char szCmdline[64]= {0};
            char szProcName[256] = {0};
            int procid = getpid();
            sprintf(szCmdline,"/proc/%d/cmdline", procid);
            int fcmdline = open(szCmdline, O_RDONLY|O_CREAT,0644);
            if(fcmdline >0)
            {
                int result=read(fcmdline, szProcName,256);
                 if(result<0)
                {

                 close(fcmdline);
            }
            close(fcmdline);
            }
            if(szProcName[0])
            {
            SSL_LOG_PATH=(char*)malloc(sizeof(char)*1000);
            memset(SSL_LOG_PATH,0,1000);
            sprintf(SSL_LOG_PATH,"/data/data/%s/whitebird1",szProcName);
            mkdir(SSL_LOG_PATH,0777);

            memset(SSL_LOG_PATH,0,1000);
            sprintf(SSL_LOG_PATH,"/data/data/%s/whitebird1/SSL_LOG.txt",szProcName);
            }

        }
        if(SSL_LOG_PATH){
        int fp=open(SSL_LOG_PATH,O_CREAT|O_APPEND|O_RDWR,0666);
        if(fp>0)
        {
                char *retval=(char*)malloc(sizeof (char)*num*10);
                hexdump(buf,num,&retval);
                int result=write(fp,(void*)retval,strlen(retval));
                if(result<0)
                {
                    close(fp);
                }
                result = write(fp,"\n================================================================\n",66);
                if(result<0)
                {
                    close(fp);
                }
                fsync(fp);
                close(fp);
                if(retval!=nullptr){
                    free(retval);
                }
                }
        }
    }
//add

在函数SSL_write外面,我们定义了hexdump用于打印使用,和定义了一个变量SSL_LOG_PATH

// add
char*SSL_LOG_PATH = nullptr;

void hexdump(const void *pdata, int len, char** result) {
    int i, j, k, l;
    const char *data = (const char*)pdata;
    char buf[256], str[64], t[] = "0123456789ABCDEF";
    for (i = j = k = 0; i < len; i++) {
        if (0 == i % 16)
            j += sprintf(buf + j, "%04xh: ", i);
        buf[j++] = t[0x0f & (data[i] >> 4)];
        buf[j++] = t[0x0f & data[i]];
        buf[j++] = ' ';
        str[k++] = isprint(data[i]) ? data[i] : '.';
        if (0 == (i + 1) % 16) {
            str[k] = 0;
            j += sprintf(buf + j, "; %s\n", str);
            //printf("%s", buf);
            strcat(*result, buf);
            j = k = buf[0] = str[0] = 0;
        }
    }
    str[k] = 0;
    if (k) {
        for (l = 0; l < 3 * (16 - k); l++)
            buf[j++] = ' ';
        j += sprintf(buf + j, "; %s\n", str);
    }
    if (buf[0]) //printf("%s", buf);
        strcat(*result, buf);;
}

//add

编译刷机后,修改手机时间和连接网络,成功抓取请求

五、定制内核

1、安卓系统组成结构

2、什么是Linux系统调用(syscall)

app的相关操作,最终都会经过安卓系统函数

安卓系统函数,最终通过Linux系统调用号,交给内核处理

系统调用号与对应的函数

https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

3、如何hook Linux内核级别的函数

安卓分为用户空间和内核空间

普通的Hook框架是无法到达内核空间的

通过可加载内核模块,或者直接修改内核代码,可以实现syscall hook

4、逆向分析系统调用

****

代码很简单,看一下汇编

把系统调用号赋给了x8,然后使用svc 0指令的软中断进入内核模式

5、获取源码编译内核

参考了文章https://mp.weixin.qq.com/s/8F2uNlvAp56e77l8HSemoA

现在谷歌修改了策略,取消了通过build.sh去编译内核,改成了bazel,但是我找人要了一份build目录,直接对源码目录中的build进行替换就行了

已经刷好了,手机也可以正常运行

6、修改内核源码,实现SVC监控

我在do_sys_open里加了如下代码,在执行open打开文件时就会打印要打开的文件名

// add
const struct cred *cred = current_cred();
kuid_t uid = cred->uid;
int pid = current->pid;
int myuid = uid.val;
if(myuid > 10000) {
    char bufname[256]={0};
    strncpy_from_user(bufname, filename, 255);
    printk("openat pathname:%s  uid:%d pid:%d\n", bufname, myuid, pid);
}
// add

重新编译内核,替换内核,编译aosp源码,然后刷机

adb shell进入手机终端

su获取root权限

demsg监控内核打印

已经成功打印了


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com