当前位置:首页 >> 世界杯2017

记录编译aosp+修改源码

安卓系统编译前置知识aosp源码、对应的Linux内核、对应的手机驱动这里的对应指的是要跟aosp系统版本对应,要与手机型号对应推荐电脑配置 硬

adminadmin

安卓系统编译前置知识aosp源码、对应的Linux内核、对应的手机驱动这里的对应指的是要跟aosp系统版本对应,要与手机型号对应推荐电脑配置 硬盘1t以上 内存 32g以上

android10 源码编译的下载1. 源码下载北方用清华镜像源 https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/南方用中科大镜像源 http://mirrors.ustc.edu.cn/help/aosp.html

3. 下载初始化包并解压123mkdir ~/bincd ~/binwget https://mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-20220222.tar

wget可以使用-c选项,来支持断点下载

12md5sum aosp-latest.tartar xvf aosp-latest.tar

4. 配置git123sudo apt-get install gitgit config --global user.email 24358757@qq.comgit config --global user.name "tuzi"

5. 下载repo1234567echo "PATH=~/bin:\$PATH" >> ~/.bashrcsource ~/.bashrcsudo apt-get install curlcurl -sSL 'https://gerrit-googlesource.proxy.ustclug.org/git-repo/+/master/repo?format=TEXT' |base64 -d > ~/bin/repochmod a+x ~/bin/repoexport REPO_URL='https://gerrit-googlesource.proxy.ustclug.org/git-repo'cd aosp

6. 修改默认Python12sudo unlink /usr/bin/pythonsudo ln -s /usr/bin/python3.8 /usr/bin/python

7. 同步指定版本源码12repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest -b android-10.0.0_r11repo 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 availablerepo: Updating release signing keys to keyset ver 2.3warning: repo is not tracking a remote branch, so it will not receive updatesrepo reset: error: Entry ‘.github/workflows/test-ci.yml’ not uptodate. Cannot merge.fatal: 不能重置索引文件至版本 ‘v2.16^0’。

解决方案:

1234cd ~/bin/aosp/.repo/repogit pullcd ~/bin/aosp再次repo init 和 repo sync

android10源码的编译1、安装JDK8123sudo add-apt-repository ppa:openjdk-r/ppasudo apt-get updatesudo apt-get install openjdk-8-jdk

2、安装所需依赖 (Ubuntu 20.04)1sudo 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

3、设备驱动的准备谷歌手机设备驱动下载地址https://developers.google.com/android/drivers

4、查看内核是否存在源码下载完后最好看下里面有没有你手机对应的设备内核如果没有,需要另外下载或者编译https://source.android.google.cn/setup/build/building-kernels#id-version

5、编译源码编译参考以下链接https://source.android.com/setup/build/building

123456make clobbersource build/envsetup.shlunch # 选择设备内核和编译版本 # 增加编译产品选项 修改aosp/device/google/marlin/AndroidProducts.mk -j 是cpu 选择适合自己的make -j24

bonito 为手机代号编译成功后编译生成的文件在home/tuzi/bin/aosp/out/target/product/bonito对应管方刷机包里的img文件 都拖进去即可

编译补充

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

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

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

123source build/envsetup.shlunch xxxmake systemimage -j4

单独编译某个模块 mmm packages/apps/tuzi将单独编译的模块打包到img镜像中 make snod

刷机相关命令123456adb reboot bootloaderfastboot flash boot boot.imgfastboot flash recovery recovery.imgfastboot flash system system.imgfastboot flash bootloader bootloader.imgfastboot boot .img

AOSP源码导入到AndroidStudio

先成功编译一次,再使用以下方法导入

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

12source build/envsetup.shmmm development/tools/idegen/

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

1development/tools/idegen/idegen.sh

安装并打开AndroidStudio,选择Open an existing Android Studio project,找到源码根目录,点击Android.ipr

Frida持久化介绍

Hook的前提需要将代码或者能够完成Hook功能的东西,注入到目标进程中安卓中注入的方式:zygote注入、ptrace注入、文件感染等

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

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

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

frida持久化的几种方案1.Android平台感染ELF文件实现模块注入

2.修改smali,调用System.loadLibrary来加载so

上面两种方法都有成品https://github.com/nszdhd1/UtilScript使用方式见https://bbs.pediy.com/thread-268175.htm3.修改rom 在app启动过程中加载frida-gadget

修改app启动流程1. 打开app时判断是否启用持久化/frameworks/base/core/java/android/app/ActivityThread.java在以上文件中,添加代码

12345678910111213141516// add String curPkgName = data.appInfo.packageName; int curUid = Process.myUid(); if (curUid > 10000) { Persist.LOGD("curPkgName: " + curPkgName + " curUid: " + curUid); Boolean isPersist = Persist.isEnablePersist(curPkgName); Persist.LOGD("isPersist: " + isPersist); if (isPersist) { if(Persist.dotuzi(appContext, curPkgName)){ Persist.LOGD("dotuzi is ok"); }else { Persist.LOGD("dotuzi failed"); }; } }// add

2. 增加自定义包和类123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151package com.tuzi;import android.content.Context;import android.util.Log;import android.os.Process;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import org.json.JSONObject;public class Persist { public static final String SO_NAME = "libtuzi.so"; public static final String SO_CONFIG_NAME = "libtuzi.config.so"; public static final String LIB32_DIR = "/system/lib"; public static final String LIB64_DIR = "/system/lib64"; public static final String SETTINGS_DIR = "/data/system/xsettings/tuzi/persist"; public static final String ENABLE_PERSIST_FILE_NAME = "tuzi_persist"; public static final String CONFIG_JS_DIR = "/data/system/xsettings/tuzi/jscfg"; public static final String CONFIG_JS_FILE_NAME = "config.js"; public static final String TAG_NAME = "tuzi_persist"; public static void LOGD(String msg) { Log.d(TAG_NAME, msg); } private 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) { // 判断文件是否存在 /data/system/xsettings/tuzi/persist/com.tuzi.app/tuzi_persist File enableFile = new File(SETTINGS_DIR, pkgName + File.separator + ENABLE_PERSIST_FILE_NAME); return enableFile.exists(); } // 获取源JS文件路径 private static File getConfigJSPath(String pkgName) { // /data/system/xsettings/tuzi/jscfg/com.tuzi.app/config.js return new File(CONFIG_JS_DIR, pkgName + File.separator + CONFIG_JS_FILE_NAME); } // 拷贝源JS文件到app私有目录 private static File copyJSFile(Context context, String pkgName) { // 判断源JS文件是否存在 File srcJSFile = getConfigJSPath(pkgName); if(!srcJSFile.exists()) { LOGD("srcJSFile not exists"); return null; } // 拷贝源JS文件到app私有目录 // /data/data/com.tuzi.app/files/config.js File dstJSFile = new File(context.getFilesDir(), CONFIG_JS_FILE_NAME); boolean isCopyJSOk = copyFile(srcJSFile, dstJSFile); if(!isCopyJSOk){ LOGD("copyJSFile fail: " + srcJSFile + " -> " + dstJSFile); return null; } return dstJSFile; } // 生成Gadget配置文件 private static boolean genGadgetConfig(Context context, File dstJSFile) { JSONObject jsonObject = new JSONObject(); JSONObject childObj = new JSONObject(); try { childObj.put("type", "script"); childObj.put("path", dstJSFile.toString()); jsonObject.put("interaction", childObj); }catch (Exception e){ e.printStackTrace();return false; } String configFilePath = context.getFilesDir() + File.separator + SO_CONFIG_NAME; boolean isSaveOk = saveFile(configFilePath, jsonObject.toString()); if(!isSaveOk){ LOGD("saveFile fail: " + configFilePath); return false; } return true; } // 拷贝源so文件到app私有目录 private static File copySoFile(Context context) { // 判断源so文件是否存在 // /system/lib/libtuzi.so // /system/lib64/libtuzi.so File srcSoFile = new File(LIB32_DIR, SO_NAME); if(Process.is64Bit()) { srcSoFile = new File(LIB64_DIR, SO_NAME); } if(!srcSoFile.exists()) { LOGD("srcSoFile not exists"); return null; } // 拷贝源so文件到app私有目录 // /data/data/com.tuzi.app/files/libtuzi.so File dstSoFile = new File(context.getFilesDir(), SO_NAME); if(srcSoFile.length() != dstSoFile.length()) { boolean isCopyFileOk = copyFile(srcSoFile, dstSoFile); if(!isCopyFileOk){ LOGD("copySoFile fail: " + srcSoFile + " -> " + dstSoFile); return null; } } return dstSoFile; } // 进行Frida Gadget持久化 public static boolean dotuziPersist(Context context, String pkgName) { File dstJSFile = copyJSFile(context, pkgName); if(null == dstJSFile) return false; if(!genGadgetConfig(context, dstJSFile)) return false; File dstSoFile = copySoFile(context); if(null == dstSoFile) return false; System.load(dstSoFile.toString()); return true; }}

3. 将自定义包加入白名单/build/make/core/tasks/check_boot_jars/package_whitelist.txt

123# // addcom\.tuzi# // add

4. frida-gadget集成到系统将 frida-gadget 放到源码目录,比如如下文件夹中/frameworks/base/cmds/libtuzi

修改源码以下文件,将 frida-gadget 拷贝到编译以后的系统中/build/make/target/product/handheld_system.mk

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

12345# // addPRODUCT_COPY_FILES += \ frameworks/base/cmds/tuzi/frida-gadget-14.2.18-android-arm.so:$(TARGET_COPY_OUT_SYSTEM)/lib/libtuzi.so \ frameworks/base/cmds/tuzi/frida-gadget-14.2.18-android-arm64.so:$(TARGET_COPY_OUT_SYSTEM)/lib64/libtuzi.so# // add

5. 自定义目录设计1234/data/system/xsettings/tuzi/persist/pkgName/tuzi_persist/data/system/xsettings/tuzi/jscfg/pkgName/config.jspersist_tuzi文件存在,则表示开启持久化config.js表示用于注入的hook代码

6. 开机创建自定义目录/system/core/rootdir/init.rc 文件中添加以下数据 625

1234567# // add# /data/system/xsettings/tuzi/persistmkdir /data/system/xsettings 0775 system systemmkdir /data/system/xsettings/tuzi 0775 system systemmkdir /data/system/xsettings/tuzi/persist 0775 system systemmkdir /data/system/xsettings/tuzi/jscfg 0775 system system# // add

7. 创建文件类型SeLinux标签:tuzi_file7.1 在如下文件中添加数据

12/system/sepolicy/public/file.te/system/sepolicy/prebuilts/api/29.0/public/file.te7.2 在以上文件中添加如下数据,两个文件添加的内容需要一致 405

12345# // add# /data/system/xsettings/tuzi/persisttype tuzi_file, file_type, data_file_type, core_data_file_type, mlstrustedobject;# // add

7.3 注意文件不要以注释结尾,每行结束加分号

8. 为自定义目录关联文件类型标签: tuzi_file8.1 在如下文件中添加数据

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

8.2 在以上文件中添加如下数据,两个文件添加的内容需要一致 122 注意文件不要以注释结尾

1234# // add# /data/system/xsettings/tuzi/persist/data/system/xsettings(/.*)? u:object_r:tuzi_file:s0# // add

9. 配置system app访问 tuzi_file 标签文件的权限9.1 在如下文件中添加数据

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

9.2 在以上文件中添加如下数据,两个文件添加的内容需要一致 末尾

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

10. 配置第三方app访问 tuzi_file 标签文件的权限10.1 在如下文件中添加数据

123456789/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

10.2 在以上文件中添加如下数据,两个文件添加的内容需要一致 末尾

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

安卓编译常见错误11. 文件 system/sepolicy/prebuilts/api/29.0/private/untrusted_app_all.te 和 system/sepolicy/private/untrusted_app_all.te 不同12. 修改以下文件,防止报错:tuzi_file 未定义1234567/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

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

13. You have tried to change the API from what has been previously released inan SDK. Please fix the errors listed above修改了代码以后,有些时候需要先 make update-api,再编译

Frida持久化管理app的开发1. 管理app的功能显示已安装app列表

可以对每个app指定需要注入的JS

可以设置是否启用持久化

2. 相应功能实现原理3.1 创建表示启用的文件

/data/system/xsettings/tuzi/persist/pkgName/tuzi_persist

3.2 指定的JS文件复制到以下目录

/data/system/xsettings/tuzi/jscfg/pkgName/config.js

3.3 剩下的复制so、JS文件和加载so的操作,由魔改的dotuziPersist函数完成

system权限的app开发1. system权限app的开发1.1 在 manifest 中加入 android:sharedUserId=”android.uid.system”1.2 将编译出来的app放入 /packages/apps/tuziPersist1.3 编写Android.mk,也放入该文件夹1.4 单独编译指定模块 mmm packages/apps/tuziPersist1.5 编译后的模块在 /out/target/product/sailfish/system/app/ControlAPP1.6 使用 make snod 将编译出来的文件打包成镜像,刷入system.img即可

2. 如果要在编译整个系统时,一起编译这个模块,需要将模块 ControlAPP 加入源码编译链2.1 增加的内置模块,如果为APP,加入到如下文件中

1/build/make/target/product/handheld_product.mk

2.2 增加的内置模块,如果为可执行程序,加入到如下文件中

1/build/make/target/product/base_system.mk

Android.mk1234567891011121314151617181920212223242526272829303132333435# // add# 设置当前工作路径LOCAL_PATH:= $(call my-dir)# 清除变量值include $(CLEAR_VARS)# 生成的模块名称LOCAL_MODULE := ControlAPP# 生成的模块类型LOCAL_MODULE_CLASS := APPS# 生成的模块后缀名,此处为apkLOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)# 设置模块tag,tags取值可以为:user debug eng tests optional# optional表示全平台编译LOCAL_MODULE_TAGS := optional# LOCAL_PRIVILEGED_MODULE := trueLOCAL_BUILT_MODULE_STEM := package.apkLOCAL_DEX_PREOPT := false# 设置源文件LOCAL_SRC_FILES := $(LOCAL_MODULE).apkLOCAL_CERTIFICATE := platform# 设置签名,此处表示保持apk原有签名# LOCAL_CERTIFICATE := PRESIGNED# 此处表示预编译方式include $(BUILD_PREBUILT)

加固与脱壳介绍1. 什么是加固演示加固前后的反编译结果其实就是将原先app的dex文件加密,app运行过程中,解密后再加载反编译时看到的就是壳的代码,或者被抽空的代码

2. 什么是脱壳演示脱壳前后的反编译结果

本质上就是将加固app运行过程中,解密后加载的dex文件保存下来

与加密算法差不多,一个加密的是字符/字节数据,一个加密的是文件(也是字节数据)

逆向加密算法主要要的是过程,做算法还原,而脱壳要的是解密后的dex文件

3. 为什么要脱壳不脱壳反编译时,通常只能看到壳的代码,或者被抽取后的代码360:整体加固以及被vmp化的onCreateijiami:函数体指令被抽取,用nop填充原有数据,或者干脆就变成空函数

4. 分析加固的app是不是必须脱壳?比如可以直接trace指令 但是静态分析配合动态分析才是优选

5. 怎么判断app是否加固反编译查看类数量、类中的方法特征反编译查看类名特征、so特征 libjiagu.so、qihoo查壳工具没有加固的,也可以脱壳

6. 加固的分类dex加固:整体加固、抽取加固、VMP、dex2c等so加固:对so结构进行处理、对so数据进行加密、自定义linker一般处理方式就是so dump,然后so修复

整体加固介绍1. 整体加固1.1 可分为落地加载、内存加载1.2 本质上都是将app自身的dex整体加密,app运行过程中解密后加载1.3 有些壳还会抹掉dex文件头、dex的文件大小filesize等1.4 一般会有字符串加密、资源加密、反调试、签名验证

2. 解决方案2.1 脱壳工具fdex2通过Class类的getDex方法得到DexFile,再通过DexFile的getBytes方法得到dex文件2.2 脱壳工具blackdex通过mCookie来脱壳2.3 脱壳工具FRIDA-DEXDump从内存中搜索dex文件,保存下来2.4 脱壳系统FART、youpk在dex加载、解析、解释执行过程中,找一个合适的时机,得到DexFile内存地址和大小,将解密状态的dex保存下来还可以通过artMethod来得到DexFile

ART下的脱壳原理1. 在线源码查看http://androidxref.com/http://aospxref.com/https://android-opengrok.bangnimang.net/https://cs.android.com/ 需要科学上网

2. ART下的脱壳点2.1 dex的加载流程

通过mCookie脱壳的

通过openCommen函数脱壳的

通过DexFile构造函数脱壳的

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

2.2 dex2oat的编译流程

通过修改dex2oat脱壳的

2.3 类的加载、链接、校验、初始化流程

DexHunter在defineClass进行类解析

LoadMethod、LinkCode

2.4 函数执行过程中的脱壳点

FART: Execute整体脱壳

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

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

3. InMemoryDexClassLoader源码分析早期脱壳点:openCommen、DexFile

整体脱壳的本质:就是找到DexFile对象

4. mCookie脱壳原理ConvertJavaArrayToDexFilesConvertDexFilesToJavaArray

5. DexClassLoader源码分析Android 10 不再从应用进程调用dex2oathttps://developer.android.com/about/versions/10/behavior-changes-10?hl=zh-cn#system-only-oathttps://blog.csdn.net/YZcoder/article/details/120648821早期脱壳点:OpenAndReadMagic、openCommen、DexFile

6. youpk脱壳原理1234567891011121314151617std::list Unpacker::getDexFiles() { std::list dex_files; Thread* const self = Thread::Current(); ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); ReaderMutexLock mu(self, *class_linker->DexLock()); const std::list& 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; const std::string& dex_location = dex_file->GetLocation(); if (dex_location.rfind("/system/", 0) == 0) { continue; } dex_files.push_back(dex_file); } return dex_files;}

fartExt解决抽取壳在脱壳完成后还需要手动修复​ 在优化这个问题前,首先要意识到为什么会需要手动修复,当我们理解了大佬的处理之后,就发现hanbingle大佬为了避免每个函数主动调用都将dex给保存,所以只有文件不存在的时候才保存。也就意味着我们保存的dex是第一个主动调用执行时的dex。如果这个抽取壳是必须函数执行后才会恢复的,那么后面的函数在这个保存dex中都依然是被抽取的。FART的做法是将codeitem保存出来后,然后再修复。所以我将这里优化了一下。

​ 知道问题所在后,优化的思路就清晰了,我采用了比较简单的一种优化方式,就是每个dex文件保存时,将这个dex的地址以及长度给保存下来。最后在所有主动调用完成时,重新将所有dex文件再保存一次。下面看看优化后的相关代码

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667//存放dex的指针和长度static std::map dex_map; //主动调用函数的dump处理extern "C" void dumpArtMethod(ArtMethod* artmethod) REQUIRES_SHARED(Locks::mutator_lock_) { ... int dexfilefp=open(dexfilepath,O_RDONLY,0666); ///dex文件存在则不处理,避免主动调用每次都要重新保存dex if(dexfilefp>0){ close(dexfilefp); dexfilefp=0; }else{ LOG(ERROR) << "mikrom ArtMethod::dumpdexfilebyArtMethod save dex_map"; //将这个地址给保存下来 dex_map.insert(std::pair((void*)begin_,size_)); int fp=open(dexfilepath,O_CREAT|O_APPEND|O_RDWR,0666); ... }}//主动调用完成时,重新保存到文件名_dexfile_repair.dex中extern "C" void dumpDexOver() REQUIRES_SHARED(Locks::mutator_lock_) { if(dex_map.size()<=0){ LOG(ERROR) << "mikrom dumpDexOver dex_map.size()<=0"; return; } char *dexfilepath=(char*)malloc(sizeof(char)*1000); LOG(ERROR) << "mikrom ArtMethod::dumpDexOver"; int result=0; char* packageName=ArtMethod::GetPackageName(); std::map::iterator iter; for(iter = dex_map.begin(); iter != dex_map.end(); iter++) { void* begin_=iter->first; size_t size_=iter->second; int size_int_=(int)size_; memset(dexfilepath,0,1000); sprintf(dexfilepath,"/sdcard/Android/data/%s/files/dump",packageName); mkdir(dexfilepath,0777); memset(dexfilepath,0,1000); sprintf(dexfilepath,"/sdcard/Android/data/%s/files/dump/%d_dexfile_repair.dex",packageName,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) << "mikrom ArtMethod::dumpDexOver,open dexfilepath error"; } fsync(fp); close(fp); memset(dexfilepath,0,1000); } } } if(dexfilepath!=nullptr) { free(dexfilepath); dexfilepath=nullptr; }}

7. dex2oat的脱壳原理https://www.jianshu.com/p/7af31cc5130e

8. fdex2脱壳原理 classObj.getDex().getBytes()9. FART:Execute10. 小结只要能够获取到DexFile的地方,都可以尝试脱壳也可以是间接得到DexFile的地方,比如ArtMathod->getDexFile()

11. 常见脱壳点函数解释执行Execute通过ClassLinker的DexCacheData进一步得到DexFile内存搜索dex文件来dump通过mCookie脱壳的

通过DexFile构造函数脱壳的LoadMethod传入的DexFileLinkCode传入的ArtMethod进一步得到DexFile, ArtMethod->getDexFile()

AOSP源码导入到Clion12345678910111213141516171. Clion的安装2. 生成用于将源码导入Clion的CMakeLists.txt// 打开开关,编译时生成CMakeLists.txtexport SOONG_GEN_CMAKEFILES=1export SOONG_GEN_CMAKEFILES_DEBUG=1// 全编译make -j24// CMakeLists.txt会生成在out/development/ide/clion/art/runtime/libart-arm64-android/CMakeLists.txt3. 用Clion打开该CMakeLists.txt4. tools --> cmake --> Change Project Root选择aosp源码根路径,等解析完毕即可

打造自己的脱壳系统1. FART源码分析2. FART迁移至安卓103. Linux C语言中的open函数https://blog.csdn.net/weixin_39296438/article/details/79422068

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

5. 注意事项5.1 改源码时,多加输出和快照,方便排错和快速还原到上一个状态5.2 移植代码时,关注一些安卓特有的类、对象、方法是否改变,比如fdex2的脱壳函数在8.0之后就没有了

抽取加固介绍1. 抽取加固本质:提取出dex中方法体的字节码,并在方法运行时还原2. 抽取加固的实现形式抽空方法体代码,运行方法后回填,运行完后不再抽取 –> 延时保存抽空方法体代码,运行方法后回填,运行完后又抽取 –> FART、youpk主动调用抽空方法体代码,将原有函数体替换为解密代码,运行时解密执行

3. 抽取加固对原有dex的处理形式原有函数体数据空间置0,保留原有空间对dex文件进行重构,不保留原有空间,在还原数据时,修改CodeItemOffest

抽取加固解决方案1. 解决思路:在函数运行时,保存被抽取的数据fartFART的主动调用的深度是在函数的主动执行过程中 这样的深度肯定是不够的 有些壳必须在函数执行了之后,才会还原出真实的函数。FART的主动调用深度。函数真正执行前就已经被直接结束掉了。所以需要更深的主动调用才能够解决这种抽取壳。

123456789void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty) { //判断传入是self是否为空为空就是fart主动调用组件 if (self== nullptr) { dumpArtMethod(this); return; } ... }

youpkYoupk更深的主动调用判断如果这个函数的第一个指令是goto,就正常执行,然后执行到invoke-static的指令。这个指令完成之后就直接结束掉函数调用。避免真实函数调用会出现异常。

参考Youpk的看看他是如何实现更深的主动调用来解决这个问题的。下面是第一步,先修改默认的解释器为Switch的解释器。这是因为Switch解释器的可读性更加高,方便我们直接修改源码来达到目的。

1static constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;

看看主动调用时Youpk是怎么模拟参数的。

12345678910111213141516171819202122232425262728void Unpacker::invokeAllMethods() { ... auto methods = klass->GetDeclaredMethods(pointer_size); Unpacker::enableFakeInvoke(); for (auto& m : methods) { ArtMethod* method = &m; if (!method->IsProxyMethod() && method->IsInvokable()) { //获取参数个数 uint32_t args_size = (uint32_t)ArtMethod::NumArgRegisters(method->GetShorty()); if (!method->IsStatic()) { args_size += 1; } //模拟参数 JValue result; std::vector args(args_size, 0); if (!method->IsStatic()) { mirror::Object* thiz = klass->AllocObject(self); args[0] = StackReference::FromMirrorPtr(thiz).AsVRegValue(); } method->Invoke(self, args.data(), args_size, &result, method->GetShorty()); } } Unpacker::disableFakeInvoke(); cJSON_ReplaceItemInObject(current, "status", cJSON_CreateString("Dumped")); writeJson(); } }}

这里可以看到Youpk的参数是模拟赋值进去的。而寒冰大佬的做法不大一样。看看FART的函数调用模拟。

12345678extern "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");}

这样肯定没法顺利往后执行。我们先继续参考Youpk的后续。然后看看Youpk的ArtMethod::Invoke的处理,如果是主动调用并且非Native函数就正常执行。

1234567891011121314151617181920212223242526272829303132void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty) { ... //patch by Youlor //++++++++++++++++++++++++++++ //如果是主动调用fake invoke并且不是native方法则强制走解释器 if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this) || (Unpacker::isFakeInvoke(self, this) && !this->IsNative()))) { //++++++++++++++++++++++++++++ if (IsStatic()) { art::interpreter::EnterInterpreterFromInvoke( self, this, nullptr, args, result, /*stay_in_interpreter*/ true); } else { mirror::Object* receiver = reinterpret_cast*>(&args[0])->AsMirrorPtr(); art::interpreter::EnterInterpreterFromInvoke( self, this, receiver, args + 1, result, /*stay_in_interpreter*/ true); } } else { //patch by Youlor //++++++++++++++++++++++++++++ //如果是主动调用fake invoke并且是native方法则不执行 if (Unpacker::isFakeInvoke(self, this) && this->IsNative()) { // Pop transition. self->PopManagedStackFragment(fragment); return; } //++++++++++++++++++++++++++++ ... } ...}

接下来看解释器的EnterInterpreterFromInvoke函数处理。这里Youpk没有什么处理。

1234567void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver, uint32_t* args, JValue* result, bool stay_in_interpreter) { ... JValue r = Execute(self, code_item, *shadow_frame, JValue(), stay_in_interpreter); ...}

看看函数Execute。

123456789101112131415161718static inline JValue Execute( Thread* self, const DexFile::CodeItem* code_item, ShadowFrame& shadow_frame, JValue result_register, bool stay_in_interpreter = false) SHARED_REQUIRES(Locks::mutator_lock_) { ... } else if (kInterpreterImplKind == kSwitchImplKind) { if (transaction_active) { return ExecuteSwitchImpl(self, code_item, shadow_frame, result_register, false); } else { return ExecuteSwitchImpl(self, code_item, shadow_frame, result_register, false); } } ...}

然后这ExecuteSwitchImpl就是关键的解释指令的函数了。到这里有Youpk修改的部分了。先看看修改的代码

12345678910111213141516//++++++++++++++++++++++++++++#define PREAMBLE() \ do { \ inst_count++; \ bool dumped = Unpacker::beforeInstructionExecute(self, shadow_frame.GetMethod(), \ dex_pc, inst_count); \ if (dumped) { \ return JValue(); \ } \ if (UNLIKELY(instrumentation->HasDexPcListeners())) { \ instrumentation->DexPcMovedEvent(self, shadow_frame.GetThisObject(code_item->ins_size_), \ shadow_frame.GetMethod(), dex_pc); \ } \ } while (false)//++++++++++++++++++++++++++++//patch by Youlor

PREAMBLE这个函数基本每个指令执行前都会调用beforeInstructionExecute来判断下。如果这里dump脱壳了,就直接结束掉,这个函数不再往下执行了。如果是上面那种特殊壳,这里就可以暂时先不要dump。让他正常执行先。下面看看里面的逻辑处理

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354//继续解释执行返回false, dump完成返回truebool Unpacker::beforeInstructionExecute(Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) { if (Unpacker::isFakeInvoke(self, method)) { const uint16_t* const insns = method->GetCodeItem()->insns_; const Instruction* inst = Instruction::At(insns + dex_pc); uint16_t inst_data = inst->Fetch16(0); Instruction::Code opcode = inst->Opcode(inst_data); //对于一般的方法抽取(非ijiami, najia), 直接在第一条指令处dump即可 if (inst_count == 0 && opcode != Instruction::GOTO && opcode != Instruction::GOTO_16 && opcode != Instruction::GOTO_32) { Unpacker::dumpMethod(method); return true; } //ijiami, najia的特征为: goto: goto_decrypt; nop; ... ; return; const vx, n; invoke-static xxx; goto: goto_origin; else if (inst_count == 0 && opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) { return false; } else if (inst_count == 1 && opcode >= Instruction::CONST_4 && opcode <= Instruction::CONST_WIDE_HIGH16) { return false; } else if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE)) { //让这条指令真正的执行 Unpacker::disableFakeInvoke(); Unpacker::enableRealInvoke(); return false; } else if (inst_count == 3) { if (opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) { //写入时将第一条GOTO用nop填充 const Instruction* inst_first = Instruction::At(insns); Instruction::Code first_opcode = inst_first->Opcode(inst->Fetch16(0)); CHECK(first_opcode >= Instruction::GOTO && first_opcode <= Instruction::GOTO_32); ULOGD("found najia/ijiami %s", PrettyMethod(method).c_str()); switch (first_opcode) { case Instruction::GOTO: Unpacker::dumpMethod(method, 2); break; case Instruction::GOTO_16: Unpacker::dumpMethod(method, 4); break; case Instruction::GOTO_32: Unpacker::dumpMethod(method, 8); break; default: break; } } else { Unpacker::dumpMethod(method); } return true; } Unpacker::dumpMethod(method); return true; } return false;}

这里可以看到。如果是INVOKE_STATIC就让指令正常执行。其他正常的抽取壳的深度就是在这里。这相当于就是指令执行前进行dump了。但是这里依然没解决特殊壳的深度问题。必须执行完INVOKE_STATIC之后。再进行脱壳并结束掉函数。继续看Youpk下面的处理

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556templateJValue ExecuteSwitchImpl(Thread* self, const DexFile::CodeItem* code_item, ShadowFrame& shadow_frame, JValue result_register, bool interpret_one_instruction) { ... //patch by Youlor //++++++++++++++++++++++++++++ int inst_count = -1; //++++++++++++++++++++++++++++ do { dex_pc = inst->GetDexPc(insns); shadow_frame.SetDexPC(dex_pc); TraceExecution(shadow_frame, inst, dex_pc); inst_data = inst->Fetch16(0); switch (inst->Opcode(inst_data)) { ... case Instruction::GOTO: { PREAMBLE(); int8_t offset = inst->VRegA_10t(inst_data); BRANCH_INSTRUMENTATION(offset); if (IsBackwardBranch(offset)) { HOTNESS_UPDATE(); self->AllowThreadSuspension(); } inst = inst->RelativeAt(offset); break; } ... case Instruction::INVOKE_STATIC: { PREAMBLE(); bool success = DoInvoke( self, shadow_frame, inst, inst_data, &result_register); POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx); break; } case Instruction::INVOKE_STATIC_RANGE: { PREAMBLE(); bool success = DoInvoke( self, shadow_frame, inst, inst_data, &result_register); POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx); break; } ... } //patch by Youlor //++++++++++++++++++++++++++++ bool dumped = Unpacker::afterInstructionExecute(self, shadow_frame.GetMethod(), dex_pc, inst_count); if (dumped) { return JValue(); } //++++++++++++++++++++++++++++ } while (!interpret_one_instruction); // Record where we stopped. shadow_frame.SetDexPC(inst->GetDexPc(insns)); return result_register;} // NOLINT(readability/fn_size)

这里就看到每个指令都执行了PREAMBLE函数。然后每个指令执行完都执行了afterInstructionExecute这个函数。在这里就可以判断,如果执行完的指令是INVOKE_STATIC。就可以直接return结束掉函数执行了。看看Youpk的处理

123456789101112bool Unpacker::afterInstructionExecute(Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) { const uint16_t* const insns = method->GetCodeItem()->insns_; const Instruction* inst = Instruction::At(insns + dex_pc); uint16_t inst_data = inst->Fetch16(0); Instruction::Code opcode = inst->Opcode(inst_data); if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE) && Unpacker::isRealInvoke(self, method)) { Unpacker::enableFakeInvoke(); Unpacker::disableRealInvoke(); } return false;}

这里留意了一下。这个函数固定返回的false。但是通过设置enableFakeInvoke和disableRealInvoke来控制下一个指令执行的时候来进行退出函数到这里基本就走完大致的流程了。

FartExt更深的主动调用和Youpk一样。第一步就是先把解释器给改成使用Switch解释器。但是由于我使用的是AOSP10。所以发现修改部分果然不大一样了。

123456#if ART_USE_CXX_INTERPRETERstatic constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;#elsestatic constexpr InterpreterImplKind kInterpreterImplKind = kMterpImplKind;#endif发现这里变成可以通过编译参数来控制的了。搜索一下ART_USE_CXX_INTERPRETER的使用

123if envTrue(ctx, "ART_USE_CXX_INTERPRETER") { cflags = append(cflags, "-DART_USE_CXX_INTERPRETER=1") }

发现这个好像可以通过cflags来配置了。所以我修改了下runtime下的Android.pb。如果不想改全局的。也可以在源码里面直接判断是主动调用就强制走switch解释器。

1234567cflags: [ // ART is allowed to link to libicuuc directly // since they are in the same module "-DANDROID_LINK_SHARED_ICU4C", "-Wno-error", "-DART_USE_CXX_INTERPRETER=1"],

接着就是ArtMethod::Invoke的时候不要直接结束了。但是这里我们需要留意的是。第一个参数的Thread是fart用来判断是否为主动调用的。为了让后面能正常执行,我就直接把第一个参数给赋值了。而后面的调用流程也是需要判断当前执行函数是否为主动调用。Youpk是用线程和一个变量来控制判断是否为主动调用的。这里使用result=111111在后续判断是否为主动调用

1234567891011121314151617181920212223242526272829303132333435363738394041extern "C" void fartextInvoke(ArtMethod* artmethod) REQUIRES_SHARED(Locks::mutator_lock_) { if(artmethod->IsNative()||artmethod->IsAbstract()){ return; } JValue result; //模拟参数 Thread *self=Thread::Current(); uint32_t temp[100]={0}; uint32_t* args=temp; uint32_t args_size = (uint32_t)ArtMethod::NumArgRegisters(artmethod->GetShorty()); if (!artmethod->IsStatic()) { args_size += 1; } //靠这个值,在后续来判断当前函数是否为主动调用。 result.SetI(111111); LOG(ERROR) << "fartext fartextInvoke"; Unpacker_self_=self; artmethod->Invoke(self, args, args_size, &result,artmethod->GetShorty());} void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty) { ... //add if (result!=nullptr && result->GetI()==111111){ LOG(ERROR) << "fartext artMethod::Invoke Method "<PrettyMethod().c_str(); if (IsStatic()) { art::interpreter::EnterInterpreterFromInvoke( self, this, nullptr, args, result, /*stay_in_interpreter=*/ true); }else{ //注意这里是把非静态的也当静态的方式处理的。避免使用引用类型参数。 art::interpreter::EnterInterpreterFromInvoke( self, this, nullptr, args + 1, result, /*stay_in_interpreter=*/ true); } LOG(ERROR) << "fartext artMethod::Invoke Method Over "<PrettyMethod().c_str(); self->PopManagedStackFragment(fragment); return; } //add end ...}

这里有个问题是上面这种模拟参数的方式,碰到引用类型的参数会报错。所以在处理参数入栈的时候,也要进行判断处理一下。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, ObjPtr receiver, uint32_t* args, JValue* result, bool stay_in_interpreter) { ... if (!method->IsStatic()) { //add 避免使用引用类型的参数 if(result!=nullptr&&result->GetI()==111111){ shadow_frame->SetVReg(cur_reg, args[0]); }else{ CHECK(receiver != nullptr); shadow_frame->SetVRegReference(cur_reg, receiver); } //add end //shadow_frame->SetVRegReference(cur_reg, receiver); ++cur_reg; } uint32_t shorty_len = 0; const char* shorty = method->GetShorty(&shorty_len); for (size_t shorty_pos = 0, arg_pos = 0; cur_reg < num_regs; ++shorty_pos, ++arg_pos, cur_reg++) { DCHECK_LT(shorty_pos + 1, shorty_len); switch (shorty[shorty_pos + 1]) { case 'L': { //add 避免使用引用类型的参数 if(result!=nullptr&&result->GetI()==111111){ shadow_frame->SetVReg(cur_reg, args[0]); break; } //add end ObjPtr o = reinterpret_cast*>(&args[arg_pos])->AsMirrorPtr(); shadow_frame->SetVRegReference(cur_reg, o); break; } case 'J': case 'D': { uint64_t wide_value = (static_cast(args[arg_pos + 1]) << 32) | args[arg_pos]; shadow_frame->SetVRegLong(cur_reg, wide_value); cur_reg++; arg_pos++; break; } default: shadow_frame->SetVReg(cur_reg, args[arg_pos]); break; } } ... if (LIKELY(!method->IsNative())) { //这里把我们主动调用函数的标志继续往后面传递 if(result!=nullptr&&result->GetI()==111111){ JValue r = Execute(self, accessor, *shadow_frame, *result, stay_in_interpreter); if (result != nullptr) { *result = r; } return; }else{ JValue r = Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter); if (result != nullptr) { *result = r; } } } ...}

接下来就开始修改解释器部分的逻辑了。我们只要做到几点处理。就可以搞定这种壳了。1、如果是主动调用并且第一个指令如果不是GOTO的。就直接脱壳并结束2、如果是主动调用并且第一个指令是GOTO的。让他继续执行3、如果第三个指令是INVOKE-STATIC的执行完后直接结束掉

接下来准备改代码。然后碰到一个问题。同样也是AOSP10的版本导致的。Switch解释器的逻辑发生了较大的变动。先看看变成了啥样子

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849templateATTRIBUTE_NO_SANITIZE_ADDRESS void ExecuteSwitchImplCpp(SwitchImplContext* ctx) { ... bool const interpret_one_instruction = ctx->interpret_one_instruction; while (true) { dex_pc = inst->GetDexPc(insns); shadow_frame.SetDexPC(dex_pc); TraceExecution(shadow_frame, inst, dex_pc); inst_data = inst->Fetch16(0); { bool exit_loop = false; InstructionHandler handler( ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, exit_loop); //PREAMBLE变成这种方式调用了 if (!handler.Preamble()) { if (UNLIKELY(exit_loop)) { return; } if (UNLIKELY(interpret_one_instruction)) { break; } continue; } } switch (inst->Opcode(inst_data)) {#define OPCODE_CASE(OPCODE, OPCODE_NAME, pname, f, i, a, e, v) \ case OPCODE: { \ bool exit_loop = false; \ InstructionHandler handler( \ ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, exit_loop); \ handler.OPCODE_NAME(); \ /* TODO: Advance 'inst' here, instead of explicitly in each handler */ \ if (UNLIKELY(exit_loop)) { \ return; \ } \ break; \ }DEX_INSTRUCTION_LIST(OPCODE_CASE)#undef OPCODE_CASE } if (UNLIKELY(interpret_one_instruction)) { break; } } // Record where we stopped. shadow_frame.SetDexPC(inst->GetDexPc(insns)); ctx->result = ctx->result_register; return;} // NOLINT(readability/fn_size)

看到了这两个部分都发生了较大的变化。那个超大的case都不见了。不过也只是处理的方式发生变化。我们跟着调整下就行了。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788templateATTRIBUTE_NO_SANITIZE_ADDRESS void ExecuteSwitchImplCpp(SwitchImplContext* ctx) { ... //add int32_t regvalue=ctx->result_register.GetI(); //这里很重要。需要把我们用来作为主动调用的值给改了。不然调用另外一个函数也会当成fart的主动调用的。 ctx->result_register=JValue(); int inst_count = -1; //当前第几个指令 bool flag=false; //第一个指令是否为goto //add end bool const interpret_one_instruction = ctx->interpret_one_instruction; while (true) { ... //add inst_count++; uint8_t opcode = inst->Opcode(inst_data) //如果是主动调用 if(regvalue==111111){ //第一个指令是goto的处理 if(inst_count == 0 ){ if(opcode == Instruction::GOTO || opcode == Instruction::GOTO_16 || opcode == Instruction::GOTO_32){ LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=0 opcode==GOTO "<PrettyMethod().c_str(); flag=true; }else{ LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=0 opcode!=GOTO "<PrettyMethod().c_str(); dumpArtMethod(shadow_frame.GetMethod()); break; } } //第二个指令是const的处理 if(inst_count == 1){ if(opcode >= Instruction::CONST_4 && opcode <= Instruction::CONST_WIDE_HIGH16){ LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=1 opcode==CONST "<PrettyMethod().c_str(); flag=true; }else{ LOG(ERROR) << "fartext ExecuteSwitchImplCpp Switch inst_count=1 opcode!=CONST "<PrettyMethod().c_str(); dumpArtMethod(shadow_frame.GetMethod()); break; } } } //add end switch (opcode) {#define OPCODE_CASE(OPCODE, OPCODE_NAME, pname, f, i, a, e, v) \ case OPCODE: { \ bool exit_loop = false; \ InstructionHandler handler( \ ctx, instrumentation, self, shadow_frame, dex_pc, inst, inst_data, exit_loop); \ handler.OPCODE_NAME(); \ /* TODO: Advance 'inst' here, instead of explicitly in each handler */ \ if (UNLIKELY(exit_loop)) { \ return; \ } \ break; \ }DEX_INSTRUCTION_LIST(OPCODE_CASE)#undef OPCODE_CASE } //add //指令执行结束后,再判断一下是不是主动调用的 if(regvalue==111111){ //如果这是第3个指令 if(inst_count==2&&flag){ //如果是下面两种操作码,就可以脱壳并结束了。 if(opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE){ dumpArtMethod(shadow_frame.GetMethod()); ArtMethod::disableFartextInvoke(); break; } } //如果主动调用的情况还能执行到第4个指令。那就直接脱壳并结束掉。 if(inst_count>2){ dumpArtMethod(shadow_frame.GetMethod()); ArtMethod::disableFartextInvoke(); break; } } //add end if (UNLIKELY(interpret_one_instruction)) { break; } } // Record where we stopped. shadow_frame.SetDexPC(inst->GetDexPc(insns)); ctx->result = ctx->result_register; return;} // NOLINT(readability/fn_size)

2. 被动调用app正常运行过程中所发生的函数调用只对dex中部分的类完成加载,只对dex中的部分函数完成调用调用函数不全,导致能够恢复的函数有限

3. 主动调用构造虚拟调用,对app中所有函数完成调用在这些函数执行时,保存函数体CodeItem数据保存数据的时机越晚,效果越好

4. 常见的抽取加固脱壳系统DexHunter、Fupk3、FART、youpk . fartExt

类加载器和双亲委派机制双亲委派机制的工作原理是 : 一个类加开始加载他会把这个请求委托给父类的加载器去执行 如果父类加载器还存在其他父类加载器就会进一步向上委托如果父类可以完成加载 就返回成功若是无法加载则会委托给子类子类再去加载 加载之后的类不会被重复加载 也就是说系统类加载只会交给最上层的类加载器会相对安全。

类加载机制分为隐式加载和显式加载

隐式 : 创建类的实例 new 访问类的静态遍历 或者未静态变量赋值调用类的静态方法使用反射来强制创建某个类的接口对应的Java.lang.class对象初始化某个类的子类

显示 :使用loadclass()使用Class.forname()

类加载的过程 :装载 : 查找和导入class文件链接 : 其中解析不走是可以选择的 检查 : 检查载入的class文件数据的正确性 准备 : 给类的静态变量分内存空间 解析 : 江符号引用转成直接引用初始化 : 调用函数 对静态变量静态代码块执行初始化工作

Android系统中的ClassLoader:12345678910111213141516171819202122232425262728293031323334353637ClassLoader为抽象类;BootClassLoader预加载常用类,单例模式。与Java中的BootClassLoader不同,它并不是由C/C++代码实现,而是由Java实现的;BaseDexClassLoader是PathClassLoader、DexClassLoader、 InMemoryDexClassLoader的父类, 类加载的主要逻辑都是在BaseDexClassLoader完成的。SecureClassLoader继承了抽象类ClassLoader,拓展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源。其中重点关注的是PathClassLoader和DexClassLoader.PathClassLoader是Androi d默认使用的类加载器,- 个apk中 的Activity等类便是在其中加载。DexClassLoader可以加载任意目录下的dex/ jar/apk/zip文件,比PathClassLoader更灵活, 是实现插件化、热修复以及dex加壳的重点。Android8. 0新引入InMemoryDexClassLoader,从名字便可看出是用于直接从内存中加载dex。basedexclassloader 可以拿到 子类的所有类 拿到的是通过对dex解析拿到的所有类双亲委派 classloader bootclassloader basedexclassloader secureclassloader urlclassloaderinmemorydexclassloader pathclassloader dexclassloader 安卓四大组件一般是在pathclassloader中进行加载注册的 如果在dexclassloader中去加载dex去进行调用startactivity会出现找不到类的情况如何在dexclassloader中加载的组件类有正常的生命周期app运行中classloader的参与流程bootclassloader加载系统核心库pathclasslocader加载app自身dex进入app自身组件开始执行调用声明application的attachbasecontext调用声明的application的oncreate有多种解决发方案解决方案 替换系统组件类加载器为我们的dexclassloadeer 设置dexclassloader的parent为系统组件类加载器打破原有的双亲关系 在系统组件类加载器和bootclassloader的中间插入我们自己的dexclassloader即可将自己的classloader和系统的elements数组进行合并把自己的类加载写入到pathclassloader

dex加固1.dex整体加固 文件加载和内存加载2.函数抽取: 在函数粒度完成代码的保护3.vmp 和 dex2c : java函数nativie化 或者vmp和函数抽取相结合

so加固的种类1.基于init,init——array以及jni_onload函数的加壳2.基于自定义linker的加壳

vmp虚拟化保护(虚拟机保护)加固厂商完全可以参考aosp源码里解释器的源码进行修改学习 修改smali指令的操作码 或者还是使用原来的codeitem在自己实现的解释器里执行对每一条指令进行映射

一般是所有被vmp保护的函数都使用一个解释器对于相同类型的函数变成相同类型的jni函数映射到相同的地址 他们有相同的修饰符相同的返回值 直接全部映射到相同的地址 对于不同的函数使用不同的 codeitem 开源的例子(ADVMP)被vmp保护的oncreate函数共享一个解释器绑定在了一个相同的地址

dex2c将smali指令进行等价的jni代码进行转换 转换成jni代码在使用ollvm进行混淆 效果也满强的获取到保护的dex后和vmp一样被保护的函数的属性由java转变为nativie如开源的dcc(dex-to-c cmopiler)

脱壳那到dex_file 就可以拿到dex_bigen 和 dex_base 就可以完成dex的写出 也就是脱壳

从加壳的角度来讲ClassLinker::LoadMethod函数执行之后进行hook就可以得到初始化完成以后art对象codeitem的一个偏移在结合dex_file的起始地址就可以得到现在正在初始化的函数的指令部分 这个时候进行一些修改就可以解决函数抽取壳

dex2oat编译流程 : 函数粒度进行编译并不是所有的函数都会被编译 比如类初始化函数 因此对于当一个类被初始化时 该类的初始化函数始终运行在interpreter模式

art下函数执行模式interpreter 由 art下的解释器解释执行quick模式 直接运行dex2oat编译生成的arm指令

interpreter模式流程从artmethod类中的enterinterpreterforminvoke开始分析 execute->executemterplmpl或者executeswitchimpl可以看到对于任何一个运行在interpreter模式的java函数来说最终都会进入到art下的解释器中进行解释执行

fart实现的三个功能一,构造主动调用链让每一个函数都主动调用 但是不影响app的正常运行jni调用java函数标准流程1.得到jclass findclass的原理是通过class_linker的findclass获取目标类2.获取jmethodid3.通过call函数调用 jni中call系列函数最终都会进入到art_method中的invoke解决方案把jmethod转成artmethod 通过自行构建的参数传入art_method中的invoke函数进行主动调用在对传入的参数进行判断 是自行传入的保存下来codeitem的起始地址 然后结束函数

二, 根据artmethod对象定位内存中codeitem的起始地址123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869const DexFile::CodeItem * code_item = artmethod->GetCodeItem(); if (LIKELY(code_item != nullptr)) { int code_item_len = 0; uint8_t *item = (uint8_t *) code_item; //有try 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 { //没try code_item_len = 16 + code_item-> insns_size_in_code_units_ * 2; } memset(dexfilepath, 0, 2000); int size_int = (int) dex_file->Size(); // Length of data uint32_t method_idx = artmethod->get_method_idx(); sprintf(dexfilepath, "/sdcard/fart/%s/%d_%ld.bin", szProcName, size_int, gettidv1()); int fp2 = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666); if (fp2 > 0) { lseek(fp2, 0, SEEK_END); memset(dexfilepath, 0, 2000); int offset = (int) (item - begin_); sprintf(dexfilepath, "{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:", methodname, method_idx, offset, code_item_len); int contentlength = 0; while (dexfilepath[contentlength] != 0) contentlength++; write(fp2, (void *) dexfilepath, contentlength); long outlen = 0; char *base64result = base64_encode((char *) item, (long) code_item_len, &outlen); write(fp2, base64result, outlen); write(fp2, "};", 2); fsync(fp2); close(fp2); if (base64result != nullptr) { free(base64result); base64result = nullptr; } } } }

三,遍历dex中的所有函数并完成主动调用1,手动解析dex文件2.调用getclassnamelist获取所有类名(需要获取classloader当中的mcookie对象)

打造自己的脱壳系统1. 将FART迁移到安卓102. dex重构3. FART存在的问题3.1 调用链深度不够,有些壳将原有函数体替换为解密代码,运行时才解密执行3.2 有些壳设置一些垃圾类,当该类被初始化时自动退出3.3 有些壳设置一些垃圾类,实时检测这些类是否加载3.4 动态加载的dex文件,如果没有修正ClassLoader,不会出现在双亲委派关系中,也不会被FART遍历到3.5 需要手动修复codeitem

4. FART改进方案4.1 学习youpk的调用链深度4.2 不进行类的初始化,或者不主动调用该类4.3 设置配置文件,类似白名单,对指定类进行主动调用,或者避开指定类的调用 利用Frida主动调用FART的函数,对指定类进行脱壳4.4 利用Frida枚举所有ClassLoader,再主动调用FART的函数进行脱壳Exexute脱壳点对于动态加载的dex也可以脱,除非这个dex没有类的初始化函数4.5每个dex文件保存时,将这个dex的地址以及长度给保存下来。最后在所有主动调用完成时,重新将所有dex文件再保存一次

代码追踪1. 追踪Java函数调用关系所在文件:art/runtime/common_dex_operations.h PerformCall函数 的起始位置插入

123456789// add// caller -> calleeArtMethod* 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

2. 追踪jni函数调用关系所在文件:art/runtime/reflection.cc InvokeWithArgArray函数

12345678910111213141516171819// 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

3. 强制运行在解释模式下3.1 所在文件:art/runtime/interpreter/interpreter.cc 增加函数

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

3.2 将解释器改为switch

4. 追踪每一条smali指令所在文件:art/runtime/interpreter/interpreter_switch_impl-inl.h 增加代码

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

1234567// add 插入在shadow_frame.setdexpc(dex_pc)// TraceExecution(shadow_frame, inst, dex_pc);if (shouldTrace) { myTraceExecution(shadow_frame, inst, dex_pc);}// add

所在文件:art/runtime/interpreter/interpreter_common.h 增加函数

123456789101112131415161718192021222324252627// add 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" << 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 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(); }}// add

过root检测平时应⽤测试过程中经常遇⻅Root检测,特别是⾦融类应⽤在启动时进⾏Root环境监测,会向⽤户弹窗提示运⾏环境存在安全⻛险不让APP继续运⾏下⼀步操作或禁⽌安装运⾏(直接闪退)。虽然之前说到magisk配合Shamiko模块可以过掉⼤部分应⽤root检测,⽐较依赖magisk环境,若app对magisk进⾏检测,此时我们⼜要对magiak进⾏隐藏。这⾥的思路是-通过⾃定义su,并去除userdebug编译模式的⼀些特征!

root 常规检测⼿段⼤致可以分为三类

⽬ 录 特 征 遍 历 我 们 来 看 段 检 测 代 码 , 从 代 码 中 可 以 看 会 去 遍 历 “/sbin/su”, “/system/bin/su”,

“/system/sbin/su”, “/system/xbin/su” 等⽬录其次检测magsik、SuperSU等特征。

2.尝试执⾏root后的⼀些操作 对特定⽬录尝试新建⽂件例如 /data、/system、/etc 等(只有root后才有权限操作的⽬录),或者去执⾏典型的,例如su,还有find、mount等命令。

3.读取⼿机编译版本、调试状态 例如读取/system/build.prop中 是test-keys(测试版),还是release-keys(发布版),去获取ro.debuggable、ro.secure的值检测是否有调试状态

其实我们只要通过定制ROM⾃定义su,就可以轻松应对1.⽬录特征遍历和 2.尝试执⾏root后的⼀些操作, 当我 们 把 su 改 成 mysu, 对 应 ⽬ 录 也 ⾃ 定 义 成 mysu , 提 权 命 令 也 会 变 成 mysu , 当应⽤执⾏

对/data、/system、/etc 等⽬录进⾏⽂件操作,没有提权命令mysu⾃然也⽆法完成这些操作,最后我们把test-keys 以及ro.debuggable等特征都改成正常值,这样基本上可以做到ROOT检测通杀了。

⾃定义su123/system/extras/su/Android.mk中su修改为mysu/system/core/libcutils/fs_config.cpp中/system/xbin/su修改为/system/xbin/mysu/system/sepolicy/private/file_contexts中/system/xbin/su修改为/system/xbin/mysu

更改test-keys123456ro.build.tags=test-keys被写⼊在system/build.prop⽂件,我们查看这个⽂件看到autogenerated by buildinfo.sh 是被buildinfo.sh⽂件写⼊的,我们找下这个⽂件在/build/make/tools/buildinfo.sh ro.build.tags的值是读取BUILD_VERSION_TAGS ⽽BUILD_VERSION_TAGS 则是/build/core/Makefile 中BUILD_KEYS 给的值,所以我们将 test-keys 改为release-keys即可

更改userdebug虽然我们改了test-keys 但是ro.build.type、ro.build.display.id、ro.build.flavor、 ro.build.description、ro.build.fingerprint标签中均有userdebug,发布版值应为user 我们先 看下ro.build.type对应的值是来⾃TARGET_BUILD_TYPE

TARGET_BUILD_TYPE 的值⼜是TARGET_BUILD_VARIANT,⼜索引到其他⽂件去了,为了减少出错率,我们就在这⾥直接赋值user

ro.build.flavor 对应TARGET_BUILD_FLAVOR

TARGET_BUILD_FLAVOR := $(TARGET_PRODUCT)-$(TARGET_BUILD_VARIANT) 我们对⽐下值就可以判断出TARGET_BUILD_VARIANT 就是userdebug字符串

我们直接改成TARGET_BUILD_FLAVOR := $(TARGET_PRODUCT)-user

其他⼏个地⽅就可以按照此⽅式挨个去除即可,只在/build/core/Makefile 中改动即可

修改ro.debuggable在/build/core/main.mk可以直接把ro.debuggable指定为0 这样我们就关闭了全局debug模式了,但是平时分析应⽤需要调试所以我就保留了这个功能,借助mprop帮助我们隐藏ro.debuggable的值,这样既能保留全局debug模式,⼜隐藏了ro.debuggable的值,缺点就是每次⼿机重启,需要执⾏⼀下命令,才能做到隐藏。

暂时完结定制系统其实相对来说是一种最简单最快的方式去实现想要实现的功能,使用别的方式虽然更便捷但是相对而言需要花费更多时间。比如使用frida去实现trace smali指令 就需要各种hook 极其复杂优点:花最少的时间做最多的事情缺点:不通用 不能很好的适配各种设备 绕过风控更加复杂

最优其实是根据国内厂商的手机去修改so文件来实现编译系统源码实现的功能 不仅速度快而且也不需要额外处理特殊风控


Top