1. root检测
反制手段
1.算法助手、对话框取消等插件一键hook
2.分析具体的检测代码
3.利用IO重定向使文件不可读
4.修改Andoird源码,去除常见指纹
fun isDeviceRooted(): Boolean {
return checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
}
fun checkRootMethod1(): Boolean {
val buildTags = android.os.Build.TAGS
return buildTags != null && buildTags.contains("test-keys")
}
fun checkRootMethod2(): Boolean {
val paths = arrayOf("/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
"/system/bin/failsafe/su", "/data/local/su", "/su/bin/su")
for (path in paths) {
if (File(path).exists()) return true
}
return false
}
fun checkRootMethod3(): Boolean {
var process: Process? = null
return try {
process = Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su"))
val bufferedReader = BufferedReader(InputStreamReader(process.inputStream))
bufferedReader.readLine() != null
} catch (t: Throwable) {
false
} finally {
process?.destroy()
}
}
定义了一个 isDeviceRooted()
函数,该函数调用了三个检测 root 的方法:checkRootMethod1()
、checkRootMethod2()
和 checkRootMethod3()
。
checkRootMethod1()
方法检查设备的 build tags
是否包含 test-keys
。这通常是用于测试的设备,因此如果检测到这个标记,则可以认为设备已被 root。
checkRootMethod2()
方法检查设备是否存在一些特定的文件,这些文件通常被用于执行 root 操作。如果检测到这些文件,则可以认为设备已被 root。
checkRootMethod3()
方法使用 Runtime.exec()
方法来执行 which su
命令,然后检查命令的输出是否不为空。如果输出不为空,则可以认为设备已被 root。
2.模拟器检测
fun isEmulator(): Boolean {
return Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || Build.HOST.startsWith("Build") || Build.PRODUCT == "google_sdk"
}
通过检测系统的 Build
对象来判断当前设备是否为模拟器。具体方法是检测 Build.FINGERPRINT
属性是否包含字符串 "generic"
。
3.应用反调试检测
3.1 安卓系统自带调试检测函数
fun checkForDebugger() {
if (Debug.isDebuggerConnected()) {
// 如果调试器已连接,则终止应用程序
System.exit(0)
}
}
3.2 debuggable属性
public boolean getAppCanDebug(Context context)//上下文对象为xxActivity.this
{
boolean isDebug = context.getApplicationInfo() != null &&
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
return isDebug;
}
3.3 ptrace检测
int ptrace_protect()//ptrace附加自身线程 会导致此进程TracerPid 变为父进程的TracerPid 即zygote
{
return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即为已经被调试
}
每个进程同时刻只能被1个调试进程ptrace ,主动ptrace本进程可以使得其他调试器无法调试
3.4 调试进程名检测
int SearchObjProcess()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
pfile=popen("ps","r");
if(NULL==pfile)
{
//LOGA("SearchObjProcess popen打开命令失败!\n");
return -1;
}
// 获取结果
//LOGA("popen方案:\n");
while(fgets(buf,sizeof(buf),pfile))
{
char* strA=NULL;
char* strB=NULL;
char* strC=NULL;
char* strD=NULL;
strA=strstr(buf,"android_server");//通过查找匹配子串判断
strB=strstr(buf,"gdbserver");
strC=strstr(buf,"gdb");
strD=strstr(buf,"fuwu");
if(strA || strB ||strC || strD)
{
return 1;
// 执行到这里,判定为调试状态
}
}
pclose(pfile);
return 0;
}
4. Frida反调试
4.1 文件名检测、端口检测、双进程保护
*文件名检测
/data/local/tmp路径下是否有frida特征文件
应对:修改frida服务端文件名
*端口检测
默认端口是27042
应对:指定端口转发
*双进程保护
app创建子进程挂载自身
应对:使用spawn方式
*so检测
将上面几种方法写到so中进行检测
应对:
hook android_dlopen_ext,通过观察输出在哪个so停止输出就是哪个so在检测,找到目标so后进行patch或hook操作
4.2 检测maps
/proc/self/maps
self为对应进程id,注入frida之后maps文件中就会存在 frida-agent-64.so、frida-agent-32.so 文件
应对:
1.hook strstr、strcmp函数,让检测 frida、agent、REJECT等字符串方法失效
2.重定向maps
3.ebpf hook
用eBPF来hook系统调用并修改参数实现目的,使用bpf_probe_write_user向用户态函数地址写内容直接修改参数
char placeholder[] = "/data/data/com.zj.wuaipojie/maps";
bpf_probe_write_user((void*)addr, placeholder, sizeof(placeholder));
# 自定义 openat 调用 svc 检测 /proc/self/maps
bool anti_anti_maps() {
// 定义一个足够大的字符数组line,用于存储读取的行
const int buf_size = 512;
char buf[buf_size];
int fd; // 文件描述符
// 使用 my_openat 打开当前进程的内存映射文件 /proc/self/maps 进行读取
// AT_FDCWD 表示当前工作目录,"r" 表示只读方式打开
fd = my_openat(AT_FDCWD, "/proc/self/maps", O_RDONLY | O_CLOEXEC, 0);
if (fd != -1) {
// 如果文件成功打开,循环读取每一行
while ((read_line(fd, buf, buf_size)) > 0) {
// 使用strstr函数检查当前行是否包含"frida"字符串
if (strstr(buf, "frida") || strstr(buf, "gadget")) {
// 如果找到了"frida",关闭文件并返回true,表示检测到了恶意库
close(fd);
return true; // Evil library is loaded.
}
}
// 遍历完文件后,关闭文件
close(fd);
} else {
// 如果无法打开文件,记录错误。这可能意味着系统状态异常
// 注意:这里的代码没有处理错误,只是注释说明了可能的情况
}
// 如果没有在内存映射文件中找到"frida",返回false,表示没有检测到恶意库
return false; // No evil library detected.
}
ENTRY(my_openat) // 定义函数入口,标签my_openat
mov x8, __NR_openat // 将openat系统调用号(__NR_openat)移动到x8寄存器,x8用于存储系统调用号
svc #0 // 触发系统调用异常,进入操作系统执行系统调用
cmn x0, #(MAX_ERRNO + 1) // 将函数返回值(存储在x0寄存器)与MAX_ERRNO + 1进行无符号比较
cneg x0, x0, hi // 如果上面的比较结果大于或等于零(即没有错误),则将x0的符号位取反(如果原来是负则变正)
b.hi __set_errno_internal // 如果上面的比较结果大于或等于零(即发生了错误),则跳转到__set_errno_internal进行错误处理
ret // 从函数返回,继续执行调用者代码
END(my_openat) // 标记函数结束
#anti svc调用
#重定向maps
function anti_svc(){
let target_code_hex; // 用于搜索特定汇编指令序列的十六进制字符串
let call_number_openat; // 系统调用号对应的数值,openat
let arch = Process.arch; // 获取当前进程的架构
if ("arm" === arch){ // 如果架构是ARM
target_code_hex = "00 00 00 EF"; // ARM架构下svc指令的十六进制表示
call_number_openat = 322; // openat在ARM架构中的系统调用号
}else if("arm64" === arch){ // 如果架构是ARM64
target_code_hex = "01 00 00 D4"; // ARM64架构下svc指令的十六进制表示
call_number_openat = 56; // openat在ARM64架构中的系统调用号
}else {
console.log("arch not support!"); // 如果架构不支持,打印错误信息
}
if (arch){ // 如果成功获取了架构信息
console.log("\nthe_arch = " + arch); // 打印当前架构
// 枚举进程的内存范围,寻找只读内存段
Process.enumerateRanges('r--').forEach(function (range) {
if(!range.file || !range.file.path){ // 如果内存段没有文件路径,跳过
return;
}
let path = range.file.path; // 获取内存段的文件路径
// 如果文件路径不是以"/data/app/"开头或不以".so"结尾,跳过
if ((!path.startsWith("/data/app/")) || (!path.endsWith(".so"))){
return;
}
let baseAddress = Module.getBaseAddress(path); // 获取so库的基址
let soNameList = path.split("/"); // 通过路径分割获取so库的名称
let soName = soNameList[soNameList.length - 1]; // 获取so库的名称
console.log("\npath = " + path + " , baseAddress = " + baseAddress +
" , rangeAddress = " + range.base + " , size = " + range.size);
// 在so库的内存范围内搜索target_code_hex对应的指令序列
Memory.scan(range.base, range.size, target_code_hex, {
onMatch: function (match){
let code_address = match; // 获取匹配到的指令地址
let code_address_str = code_address.toString(); // 转换为字符串
// 如果地址的最低位是0, 4, 8, c中的任意一个,说明可能是svc指令
if (code_address_str.endsWith("0") || code_address_str.endsWith("4") ||
code_address_str.endsWith("8") || code_address_str.endsWith("c")){
console.log("--------------------------");
let call_number = 0; // 初始化系统调用号
if ("arm" === arch){
// 获取svc指令后面的立即数,作为系统调用号
call_number = (code_address.sub(0x4).readS32()) & 0xFFF;
}else if("arm64" === arch){
call_number = (code_address.sub(0x4).readS32() >> 5) & 0xFFFF;
}else {
console.log("the arch get call_number not support!"); // 如果架构不支持,打印错误信息
}
console.log("find svc : so_name = " + soName + " , address = " + code_address +
" , call_number = " + call_number + " , offset = " + code_address.sub(baseAddress));
// 如果匹配到的系统调用号是openat,挂钩该地址
if (call_number_openat === call_number){
let target_hook_addr = code_address;
let target_hook_addr_offset = target_hook_addr.sub(baseAddress);
console.log("find svc openat , start inlinehook by frida!");
Interceptor.attach(target_hook_addr, {
onEnter: function (args){ // 当进入挂钩函数时
console.log("\nonEnter_" + target_hook_addr_offset + " , __NR_openat , args[1] = " +
args[1].readCString());
// 修改openat的第一个参数为指定路径
this.new_addr = Memory.allocUtf8String("/data/user/0/com.zj.wuaipojie/maps");
args[1] = this.new_addr;
console.log("onEnter_" + target_hook_addr_offset + " , __NR_openat , args[1] = " +
args[1].readCString());
},
onLeave: function (retval){ // 当离开挂钩函数时
console.log("onLeave_" + target_hook_addr_offset + " , __NR_openat , retval = " + retval)
}
});
}
}
},
onComplete: function () {} // 搜索完成后的回调函数
});
});
}
}
上面内容提到hook strstr方式来应对检测,如果应用自定义strstr,因为hook strstr是系统的,对于自定义的方法来说这个应对方法就无效了
1.hook strstr、strcmp函数,让检测 frida、agent、REJECT等字符串方法失效
对于这种自定义strstr的方式,可以通过hook修改程序,让代码在执行自定义strstr之前直接返回
4.3 检测status(线程名)
ls /proc/pid/task 列出线程id
cat /proc/pid/task/线程id/status
应对:
hook strstr、strcmp函数,让检测 frida 特征相关字符串方法失效
4.4 检测inlinehook
通过frida查看一个函数hook之前和之后的机器码,以此来判断是否被frida的inlinehook注入
比如通过检测 libc.so前 N 个字节是否被修改就可以确定是否被Hook
通常inline hook第一条指令是mov 常数到寄存器,然后第二条是一个br 寄存器指令。检查第二条指令高16位是不是0xd61f,就可以判断目标函数是否被inline hook了
应对:
要检测 libc.so 就需要使用到 fread等读取文件函数,对这个函数进行hook并进行判断,如果读取的内容是 N 个字节表示是检测方在调用,直接返回 libc.so 原文件 N 个字节即可
4.5 魔改frida-server服务端
*so检测
将上面几种方法写到so中进行检测
应对:
hook android_dlopen_ext,通过观察输出在哪个so停止输出就是哪个so在检测,找到目标so后进行patch或hook操作
4.6 其他检测方法
1.检测方法签名信息,frida在hook方法的时候会把java方法转为native方法
2.Frida在attach进程注入SO时会显式地校验ELF_magic字段,不对则直接报错退出进程,可以手动在内存中抹掉SO的magic,达到反调试的效果。
if (memcmp (GSIZE_TO_POINTER (start), elf_magic, sizeof (elf_magic)) != 0)
return FALSE;
FILE *fp=fopen("/proc/self/maps","r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "linker64") ) {
start = reinterpret_cast<int *>(strtoul(strtok(line, "-"), NULL, 16));
*(long*)start=*(long*)start^0x7f;
}
}
3.Frida源码中多次调用somain结构体,但它在调用前不会判断是否为空,只要手动置空后Frida一附加就会崩溃
检测点https://github.com/frida/frida-gum/blob/8d9f4578b58c03025aef63652ec4defa19f8061c/gum/backend-linux/gumandroid.c#L1078
somain = api->solist_get_somain ();
gum_init_soinfo_details (&details, somain, api, &ranges);
api->solist_get_head ()
gum_init_soinfo_details (&details, si, api, &ranges);
int getsomainoff = findsym("/system/bin/linker64","__dl__ZL6somain");
*(long*)((char*)start+getsomainoff)=0;
4.还可以去hook加固壳,现在很多加固厂商都antifrida了,从壳中的代码去分析检测思路
5. Frida持久化
5.1 免ROOT 方案
Frida的Gadget是一个共享库,用于免root注入hook脚本。
官方文档
思路:将APK解包后,通过修改smali代码或patch so文件的方式植入frida-gadget,然后重新打包安装。
优点:免ROOT、能过掉一部分检测机制
缺点:重打包可能会遇到解决不了的签名校验、hook时机需要把握
基于obejction的patchapk功能
官方文档
命令:
$ objection patchapk -V 14.2.18 -c config.txt -s demo.apk(注意路径不要有中文)
-V 指定gadget版本
-c 加载脚本配置信息
-s 要注入的apk
注意的问题:
objection patchapk
命令基本上是其他几个系统命令的补充,可尽可能地自动化修补过程。当然,需要先安装并启用这些命令。它们是:
aapt
- 来自:Android aapt - eLinux.orgadb
- 来自:https://developer.android.com/studio/command-line/adb.htmljarsigner
- 来自:http://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.htmlapktool
- 来自:Apktool | Apktoolps:这几个环境工具,aapt、jarsigner都是Android Studio自带的,所以在配置好as的环境即可,abd的环境配置网上搜一下就行,apktool则需要额外配置
另外会遇到的问题,patchapk的功能在patch的时候会下载对应版本的gadget的so,但是网络问题异常慢,所以建议根据链接去下载好,然后放到这个路径下并重命名
C:\Users\用户名\.objection\android\arm64-v8a\libfrida-gadget.so
5.2 ROOT方案
方法一:
思路:可以patch /data/app/pkgname/lib/arm64(or arm)目录下的so文件,apk安装后会将so文件解压到该目录并在运行时加载,修改该目录下的文件不会触发签名校验。
Patch SO的原理可以参考Android平台感染ELF文件实现模块注入
优点:绕过签名校验、root检测和部分ptrace保护。
缺点:需要root、高版本系统下,当manifest中的android:extractNativeLibs为false时,lib目录文件可能不会被加载,而是直接映射apk中的so文件、可能会有so完整性校验
使用方法
python LIEFInjectFrida.py test.apk ./ lib52pojie.so -apksign -persistence
test.apk要注入的apk名称
lib52pojie.so要注入的so名称
然后提取patch后是so文件放到对应的so目录下
方法二:
思路:基于magisk模块方案注入frida-gadget,实现加载和hook。寒冰师傅的FridaManager
优点:无需重打包、灵活性较强
缺点:需要过root检测,magsik检测
方法三:
思路:基于jshook封装好的fridainject框架实现hook
JsHook
5.3 源码定制方案
原理:修改aosp源代码,在fork子进程的时候注入frida-gadget
ubuntu 20.04系统AOSP(Android 11)集成Frida
AOSP Android 10内置FridaGadget实践01
AOSP Android 10内置FridaGadget实践02(完)|