一、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函数中,通过判断函数名称中是否为
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