Android逆向14-Frida检测
来自吾爱破解-正己
https://www.52pojie.cn/thread-1701353-1-1.html
检测文件名、端口名、双进程保护、失效的检测点
检测/data/local/tmp路径下的是否有frida特征文件,server端改名,例如:fr
指定端口转发
1 2 3
| ./fs1 -l 0.0.0.0:6666 adb forward tcp:6666 tcp:6666 frida -H 127.0.0.1:6666 wuaipojie -l hook.js
|
spawn启动过双进程保护
1
| frida -U -f 进程名 -l hook.js
|
PS:学会看注入报错的日志,比如说当app主动附加自身进程时,这时候再注入就会提示run frida as root
(以spawn的方式启动进程即可)
借助脚本定位检测frida的so
1 2 3 4 5 6 7 8 9 10 11 12 13
| function hook_dlopen() { Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); console.log("load " + path); } } } ); }
|
随着firda的版本迭代,以前诸多检测点以失效
例如检测D-Bus
:D-Bus是一种进程间通信(IPC)和远程过程调用(RPC)机制,最初是为Linux开发的,目的是用一个统一的协议替代现有的和竞争的IPC解决方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| bool check_dbus() { struct sockaddr_in sa; int sock; char res[7];
for(int i = 0; i <= 65535; i++) { sock = socket(AF_INET, SOCK_STREAM, 0); sa.sin_port = htons(i); if (connect(sock, (struct sockaddr*)&sa, sizeof(sa)) != -1) { __android_log_print(ANDROID_LOG_VERBOSE, "ZJ595", "FRIDA DETECTION [1]: Open Port: %d", i); memset(res, 0, 7); send(sock, "\x00", 1, 0); send(sock, "AUTH\r\n", 6, 0); usleep(100); if (recv(sock, res, 6, MSG_DONTWAIT) != -1) { if (strcmp(res, "REJECT") == 0) { close(sock); return true; } } } close(sock); } return false; }
|
检测fd
/proc/pid/fd 目录的作用在于提供了一种方便的方式来查看进程的文件描述符信息,这对于调试和监控进程非常有用。通过查看文件描述符信息,可以了解进程打开了哪些文件、网络连接等,帮助开发者和系统管理员进行问题排查和分析工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| bool check_fd() { DIR *dir = NULL; struct dirent *entry; char link_name[100]; char buf[100]; bool ret = false; if ((dir = opendir("/proc/self/fd/")) == NULL) { LOGI(" %s - %d error:%s", __FILE__, __LINE__, strerror(errno)); } else { entry = readdir(dir); while (entry) { switch (entry->d_type) { case DT_LNK: sprintf(link_name, "%s/%s", "/proc/self/fd/", entry->d_name); readlink(link_name, buf, sizeof(buf)); if (strstr(buf, "frida") || strstr(buf, "gum-js-loop") || strstr(buf, "gmain") || strstr(buf, "-gadget") || strstr(buf, "linjector")) { LOGI("check_fd -> find frida:%s", buf); ret = true; } break; default: break; } entry = readdir(dir); } } closedir(dir); return ret; }
|
检测文件
众所周知frida我们一般都会放在data/local/tmp目录下,旧版fridaserver端运行时都会释放到re.frida.server,所以这里在旧版也会被当做一个检测点,而新版已不再释放
检测map

1 2
| adb shell ps | findstr com.zj.wuaipojie cat /proc/12186/maps|grep frida
|

字段 |
描述 |
u0_a504 |
用户ID和应用ID:在Android系统中,u0 代表系统用户(user 0),而a504 是该应用在用户0下的唯一标识符。 |
28082 |
PID(进程ID):该进程在操作系统中的标识符。 |
1935 |
PPID(父进程ID):该进程的父进程的PID。 |
6511212 |
虚拟内存:进程使用的虚拟内存大小,通常以字节为单位。 |
125728 |
共享内存:进程使用的共享内存大小,同样以字节为单位。 |
0 |
CPU时间/线程数:这通常表示进程的CPU时间或者是线程数,具体含义取决于ps 命令的输出格式。 |
S |
状态:其中S 代表睡眠状态(Sleeping),即进程没有在执行,而是在等待某些事件或资源。 |
/proc/self/maps
是一个特殊的文件,它包含了当前进程的内存映射信息。当你打开这个文件时,它会显示一个列表,其中包含了进程中每个内存区域的详细信息。这些信息通常包括:
- 起始地址(Start Address)
- 结束地址(End Address)
- 权限(如可读、可写、可执行)
- 共享/私有标志(Shared or Private)
- 关联的文件或设备(如果内存区域是文件映射的)
- 内存区域的偏移量
- 内存区域的类型(如匿名映射、文件映射、设备映射等)
当注入frida后,在maps文件中就会存在 frida-agent-64.so
、frida-agent-32.so
等文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| bool check_maps() { char line[512]; FILE* fp = fopen("/proc/self/maps", "r"); if (fp) { while (fgets(line, sizeof(line), fp)) { if (strstr(line, "frida") || strstr(line, "gadget")) { fclose(fp); return true; } } fclose(fp); } else { } return false; }
|
方法1
anti脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function anti_maps() { var pt_strstr = Module.findExportByName("libc.so", 'strstr'); var pt_strcmp = Module.findExportByName("libc.so", 'strcmp'); Interceptor.attach(pt_strstr, { onEnter: function (args) { var str1 = args[0].readCString(); var str2 = args[1].readCString(); if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) { this.hook = true; } }, onLeave: function (retval) { if (this.hook) { retval.replace(0); } } });
Interceptor.attach(pt_strcmp, { onEnter: function (args) { var str1 = args[0].readCString(); var str2 = args[1].readCString(); if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) { this.hook = true; } }, onLeave: function (retval) { if (this.hook) { retval.replace(0); } } }); }
|

方法2
重定向maps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| function mapsRedirect() { var FakeMaps = "/data/data/com.zj.wuaipojie/maps"; const openPtr = Module.getExportByName('libc.so', 'open'); const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']); var readPtr = Module.findExportByName("libc.so", "read"); var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]); var MapsBuffer = Memory.alloc(512); var MapsFile = new File(FakeMaps, "w"); Interceptor.replace(openPtr, new NativeCallback(function(pathname, flag) { var FD = open(pathname, flag); var ch = pathname.readCString(); if (ch.indexOf("/proc/") >= 0 && ch.indexOf("maps") >= 0) { console.log("open : ", pathname.readCString()); while (parseInt(read(FD, MapsBuffer, 512)) !== 0) { var MBuffer = MapsBuffer.readCString(); MBuffer = MBuffer.replaceAll("/data/local/tmp/re.frida.server/frida-agent-64.so", "FakingMaps"); MBuffer = MBuffer.replaceAll("re.frida.server", "FakingMaps"); MBuffer = MBuffer.replaceAll("frida-agent-64.so", "FakingMaps"); MBuffer = MBuffer.replaceAll("frida-agent-32.so", "FakingMaps"); MBuffer = MBuffer.replaceAll("frida", "FakingMaps"); MBuffer = MBuffer.replaceAll("/data/local/tmp", "/data"); MapsFile.write(MBuffer); } var filename = Memory.allocUtf8String(FakeMaps); return open(filename, flag); } return FD; }, 'int', ['pointer', 'int'])); }
|
方法3
用eBPF来hook系统调用并修改参数实现目的,使用bpf_probe_write_user向用户态函数地址写内容直接修改参数
1 2
| char placeholder[] = "/data/data/com.zj.wuaipojie/maps"; bpf_probe_write_user((void*)addr, placeholder, sizeof(placeholder));
|
检测status(线程名)
1 2
| ls /proc/pid/task 列出线程id cat /proc/pid/task/线程id/status
|
- 在
/proc/pid/task
目录下,可以通过查看不同的线程子目录,来获取进程中每个线程的运行时信息。这些信息包括线程的状态、线程的寄存器内容、线程占用的CPU时间、线程的堆栈信息等。通过这些信息,可以实时观察和监控进程中每个线程的运行状态,帮助进行调试、性能优化和问题排查等工作。
- 在某些app中就会去读取
/proc/stask/线程ID/status
文件,如果是运行frida产生的,则进行反调试。例如:gmain/gdbus/gum-js-loop/pool-frida
等
- gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。
- gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。
- gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
- pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。
- linjector 是一种用于 Android 设备的开源工具,它允许用户在运行时向 Android 应用程序注入动态链接库(DLL)文件。通过注入 DLL 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。
PS:由于frida可以随时附加到进程,所以写的检测必须覆盖APP的全周期,或者至少是敏感函数执行前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| bool check_status() { DIR *dir = opendir("/proc/self/task/"); struct dirent *entry; char status_path[MAX_PATH]; char buffer[MAX_BUFFER]; int found = false;
if (dir) { while ((entry = readdir(dir)) != NULL) { if (entry->d_type == DT_DIR) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } snprintf(status_path, sizeof(status_path), "/proc/self/task/%s/status", entry->d_name); if (read_file(status_path, buffer, sizeof(buffer)) == -1) { continue; } if (strcmp(buffer, "null") == 0) { continue; } char *line = strtok(buffer, "\n"); while (line) { if (strstr(line, "Name:") != NULL) { const char *frida_name = strstr(line, "gmain"); if (frida_name || strstr(line, "gum-js-loop") || strstr(line, "pool-frida") || strstr(line, "gdbus")) { found = true; break; } } line = strtok(NULL, "\n"); } if (found) break; } } closedir(dir); } return found; }
|
anti脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| function replace_str() { var pt_strstr = Module.findExportByName("libc.so", 'strstr'); var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
Interceptor.attach(pt_strstr, { onEnter: function (args) { var str1 = args[0].readCString(); var str2 = args[1].readCString(); if (str2.indexOf("tmp") !== -1 || str2.indexOf("frida") !== -1 || str2.indexOf("gum-js-loop") !== -1 || str2.indexOf("gmain") !== -1 || str2.indexOf("gdbus") !== -1 || str2.indexOf("pool-frida") !== -1|| str2.indexOf("linjector") !== -1) { this.hook = true; } }, onLeave: function (retval) { if (this.hook) { retval.replace(0); } } });
Interceptor.attach(pt_strcmp, { onEnter: function (args) { var str1 = args[0].readCString(); var str2 = args[1].readCString(); if (str2.indexOf("tmp") !== -1 || str2.indexOf("frida") !== -1 || str2.indexOf("gum-js-loop") !== -1 || str2.indexOf("gmain") !== -1 || str2.indexOf("gdbus") !== -1 || str2.indexOf("pool-frida") !== -1|| str2.indexOf("linjector") !== -1) { this.hook = true; } }, onLeave: function (retval) { if (this.hook) { retval.replace(0); } } })
}
|
检测inlinehook
通过Frida查看一个函数hook之前和之后的机器码,以此来判断是否被Frida的inlinehook注入。

下面的方案以内存中字节和本地对应的字节进行比较,如果不一致,那么可以认为内存中的字节被修改了,即被inlinehook了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| #include <jni.h> #include <string> #include <dlfcn.h> #include "dlfcn/local_dlfcn.h"
bool check_inlinehook() { const char *lib_path; #ifdef __LP64__ lib_path = "/system/lib64/libc.so"; #else lib_path = "/system/lib/libc.so"; #endif
const int CMP_COUNT = 8; const char *sym_name = "open";
struct local_dlfcn_handle *handle = static_cast<local_dlfcn_handle *>(local_dlopen(lib_path)); if (!handle) { return JNI_FALSE; }
off_t offset = local_dlsym(handle, sym_name);
local_dlclose(handle);
FILE *fp = fopen(lib_path, "rb"); if (!fp) { return JNI_FALSE; }
char file_bytes[CMP_COUNT] = {0}; fseek(fp, offset, SEEK_SET); fread(file_bytes, 1, CMP_COUNT, fp); fclose(fp);
void *dl_handle = dlopen(lib_path, RTLD_NOW); if (!dl_handle) { return JNI_FALSE; }
void *sym = dlsym(dl_handle, sym_name); if (!sym) { dlclose(dl_handle); return JNI_FALSE; }
int is_hook = memcmp(file_bytes, sym, CMP_COUNT) != 0;
dlclose(dl_handle);
return is_hook ? JNI_TRUE : JNI_FALSE; }
|
获取hook前字节码的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13
| let bytes_count = 8 let address = Module.getExportByName("libc.so","open")
let before = ptr(address) console.log("") console.log(" before hook: ") console.log(hexdump(before, { offset: 0, length: bytes_count, header: true, ansi: true }));
|
anti脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function hook_memcmp_addr(){ var memcmp_addr = Module.findExportByName("libc.so", "fread"); if (memcmp_addr !== null) { console.log("fread address: ", memcmp_addr); Interceptor.attach(memcmp_addr, { onEnter: function (args) { this.buffer = args[0]; this.size = args[1]; this.count = args[2]; this.stream = args[3]; }, onLeave: function (retval) { console.log(this.count.toInt32()); if (this.count.toInt32() == 8) { Memory.writeByteArray(this.buffer, [0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6]); retval.replace(8); console.log(hexdump(this.buffer)); } } }); } else { console.log("Error: memcmp function not found in libc.so"); } }
|
刷入魔改的frida-server端
注意版本!!!
strongR-frida-android
Syscall&SVC&自定义strstr
在上面的检测对抗中,我们hook了libc.so中的fread、strstr、open等系统函数,但是如果app不讲武德,自实现这些函数,阁下又该如何应对?
在用户空间和内核空间之间,有一个叫做Syscall(系统调用, system call)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。
SVC(软件中断指令)指令:在ARM架构的系统中,svc
是一条特殊的指令,它允许用户态的程序发起一个系统调用。当这条指令被执行时,CPU会从用户态切换到内核态,从而允许内核处理这个请求。
Linux操作系统是一个巨大的图书馆,而syscall
就是这个图书馆的前台服务窗口。当一个应用程序(比如一个读者)需要借阅书籍(获取系统资源或服务)时,它不能直接进入图书馆的内部书架去拿书,因为那样可能会造成混乱和损坏。所以,读者需要通过前台服务窗口,也就是syscall
,来请求它想要的书籍。
svc
就像是图书馆前台服务窗口的内部电话。当读者通过前台窗口提出请求时,前台工作人员会通过内部电话(svc
)来联系图书馆的内部工作人员,请求他们找到并提供所需的书籍。在Linux系统中,当一个程序通过syscall
请求服务时,实际上是通过svc
这条指令通知内核,然后由内核来处理这些请求。
Frida-Sigaction-Seccomp实现对Android APP系统调用的拦截
分享一个Android通用svc跟踪以及hook方案——Frida-Seccomp
基于seccomp+sigaction的Android通用svc hook方案
原创]SVC的TraceHook沙箱的实现&无痕Hook实现思路
原创]Seccomp技术在Android应用中的滥用与防护
原创]批量检测android app的so中是否有svc调用

- 首先当我们长按开机键(电源按钮)开机,此时会引导芯片开始从固化到ROM中的预设代码处执行,然后加载引导程序到RAM。然后启动加载的引导程序,引导程序主要做一些基本的检查,包括RAM的检查,初始化硬件的参数。
- 到达内核层的流程后,这里初始化一些进程管理、内存管理、加载各种Driver等相关操作,如Camera Driver、Binder Driver 等。下一步就是内核线程,如软中断线程、内核守护线程。下面一层就是Native层,这里额外提一点知识,层于层之间是不可以直接通信的,所以需要一种中间状态来通信。Native层和Kernel层之间通信用的是syscall,Native层和Java层之间的通信是JNI。
- 在Native层会初始化init进程,也就是用户组进程的祖先进程。init中加载配置文件init.rc,init.rc中孵化出ueventd、logd、healthd、installd、lmkd等用户守护进程。开机动画启动等操作。核心的一步是孵化出Zygote进程,此进程是所有APP的父进程,这也是Xposed注入的核心,同时也是Android的第一个Java进程(虚拟机进程)。
- 进入框架层后,加载zygote init类,注册zygote socket套接字,通过此套接字来做进程通信,并加载虚拟机、类、系统资源等。zygote第一个孵化的进程是system_server进程,负责启动和管理整个Java Framework,包含ActivityManager、PowerManager等服务。
- 应用层的所有APP都是从zygote孵化而来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| bool anti_anti_maps() { const int buf_size = 512; char buf[buf_size]; int fd; fd = my_openat(AT_FDCWD, "/proc/self/maps", O_RDONLY | O_CLOEXEC, 0); if (fd != -1) { while ((read_line(fd, buf, buf_size)) > 0) { if (strstr(buf, "frida") || strstr(buf, "gadget")) { close(fd); return true; } } close(fd); } else { } return false; }
ENTRY(my_openat) mov x8, __NR_openat svc #0 cmn x0, #(MAX_ERRNO + 1) cneg x0, x0, hi b.hi __set_errno_internal ret END(my_openat)
|
anti脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| function anti_svc(){ let target_code_hex; let call_number_openat; let arch = Process.arch;
if ("arm" === arch){ target_code_hex = "00 00 00 EF"; call_number_openat = 322; }else if("arm64" === arch){ target_code_hex = "01 00 00 D4"; call_number_openat = 56; }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; if ((!path.startsWith("/data/app/")) || (!path.endsWith(".so"))){ return; } let baseAddress = Module.getBaseAddress(path); let soNameList = path.split("/"); let soName = soNameList[soNameList.length - 1]; console.log("\npath = " + path + " , baseAddress = " + baseAddress + " , rangeAddress = " + range.base + " , size = " + range.size); Memory.scan(range.base, range.size, target_code_hex, { onMatch: function (match){ let code_address = match; let code_address_str = code_address.toString(); 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){ 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)); 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()); 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 () {} }); }); } }
|
自定义strstr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| bool anti_str_maps() { char line[512]; FILE* fp = fopen("/proc/self/maps", "r"); if (fp) { while (fgets(line, sizeof(line), fp)) { if (my_strstr(line, "frida") || my_strstr(line, "gadget")) { fclose(fp); return true; } } fclose(fp); } else { } return false; }
__attribute__((always_inline)) static inline char * my_strstr(const char *s, const char *find) { char c, sc; size_t len;
if ((c = *find++) != '\0') { len = my_strlen(find); do { do { if ((sc = *s++) == '\0') return (NULL); } while (sc != c); } while (my_strncmp(s, find, len) != 0); s--; } return ((char *)s); }
|
frida持久化方案
免root方案
Frida的Gadget是一个共享库,用于免root注入hook脚本。
官方文档
思路:将APK解包后,通过修改smali代码或patch so文件的方式植入frida-gadget,然后重新打包安装。
优点:免ROOT、能过掉一部分检测机制
缺点:重打包可能会遇到解决不了的签名校验、hook时机需要把握
基于obejction的patchapk功能
官方文档命令:
1 2 3 4
| objection patchapk -V 14.2.18 -c config.txt -s demo.apk(注意路径不要有中文) -V 指定gadget版本 -c 加载脚本配置信息 -s 要注入的apk
|
注意的问题:
objection patchapk
命令基本上是其他几个系统命令的补充,可尽可能地自动化修补过程。当然,需要先安装并启用这些命令。它们是:
ps:这几个环境工具,aapt、jarsigner都是Android Studio自带的,所以在配置好as的环境即可,abd的环境配置网上搜一下就行,apktool则需要额外配置
另外会遇到的问题,patchapk的功能在patch的时候会下载对应版本的gadget的so,但是网络问题异常慢,所以建议根据链接去下载好,然后放到这个路径下并重命名
1
| C:\Users\用户名\.objection\android\arm64-v8a\libfrida-gadget.so
|
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完整性校验
使用方法
1 2 3
| 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
源码定制方案
原理:修改aosp源代码,在fork子进程的时候注入frida-gadget
ubuntu 20.04系统AOSP(Android 11)集成Frida
AOSP Android 10内置FridaGadget实践01
AOSP Android 10内置FridaGadget实践02(完)|
其他检测思路与反思
1.检测方法签名信息,frida在hook方法的时候会把java方法转为native方法
2.Frida在attach进程注入SO时会显式地校验ELF_magic字段,不对则直接报错退出进程,可以手动在内存中抹掉SO的magic,达到反调试的效果
1 2 3 4
| 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);
|
1 2
| int getsomainoff = findsym("/system/bin/linker64","__dl__ZL6somain"); *(long*)((char*)start+getsomainoff)=0;
|
3.通常inline hook第一条指令是mov 常数到寄存器,然后第二条是一个br 寄存器指令。检查第二条指令高16位是不是0xd61f,就可以判断目标函数是否被inline hook了!
4.还可以去hook加固壳,现在很多加固厂商都antifrida了,从壳中的代码去分析检测思路
反调试现状 |
详细说明 |
检测方式多样 |
从通用检测、hook检测到源码检测,方式层出不穷。源码检测可以针对每行代码都能开发出不同检测方式,Frida指纹过多。 |
检测位置不确定 |
一般是单独开线程跑,也可以在关键函数执行前判断 |
强混淆加大定位难度 |
反调试通常埋几行代码,但结合混淆可达万行代码,不考虑效率可膨胀更多,定位极难 |