0x1 简介 Frida就是一个「脚本化、可移植、免费且非常流行」的动态插桩工具 。
对于知道插桩是什么意思的朋友来说,不必再过多解释。而如果你不知道插桩是什么意思,大概介绍下,你可以理解为:将你的代码,插入到源程序的某个地方,最终一起执行,而「不必改变源程序的代码」 。
不理解也没关系,忽略它,不影响后面的内容
项目地址:https://github.com/frida/frida
环境配置:
1 pip install frida==16.1.3 frida-tools==12.3.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
1 frida-server-16.1.3-android-arm64
https://github.com/frida/frida/releases
https://github.com/hzzheyang/strongR-frida-android
0x2 注入模式 spawn 将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App
优点 :hook时机早,可以在程序刚启动就执行 。
比如:
JNI_OnLoad
Application.attach
Activity.onCreate
反调试代码
root检测
anti-frida
缺点 :有些 App 检测 spawn 行为
例如:
anti-debug
anti-frida
ptrace 检测
1 frida -U -f 包名 -l hook.js
attach 在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作
优点 :稳定
缺点 :早期函数 hook 不到
例如:
Application.attach
JNI_OnLoad
native 初始化
早期检测函数
这些可能已经执行完了。
1 frida -U -n 包名 -l hook.js
0x3 常用api 模板:
1 2 3 4 5 6 7 8 9 Java .perform (function ( ) { var <class_reference> = Java .use ("<package_name>.<class>" ); <class_reference>.<method_to_hook>.implementation = function (<args> ) { } })
Java.performFrida 中的 context 函数用于为脚本创建一个特殊的上下文,以便与 Android 应用程序中的 Java 代码进行交互。它就像打开一扇门,让你能够访问和操作应用程序内部运行的 Java 代码。进入此上下文后,可以执行诸如钩取方法或访问 Java 类之类的操作,从而控制或观察应用程序的行为。
Frida 在 Android 上提供的 Java API (如 Java.use()、Java.perform() 等)实际上就是:
在 Native 层调用 ART/Dalvik 的内部接口,实现类似 Java 反射的能力
var <class_reference> = Java.use("<package_name>.<class>");
在这里,声明一个变量<class_reference>来表示目标 Android 应用程序中的一个 Java 类。需要指定要与该函数一起使用的类Java.use,该函数以类名作为参数。<package_name>``<package_name>表示 Android 应用程序的包名,<class_name>``<class>表示您要与之交互的类。
<class_reference>.<method_to_hook>.implementation = function(<args>) {}
在选定的类中,可以使用 hook 语法访问要挂钩的方法,并指定要挂钩的方法<class_reference>.<method_to_hook>。可以在此处定义在调用被挂钩方法时要执行的自定义逻辑。arguments``<args>表示传递给函数的参数。
Java hook 1.Hook普通方法、打印参数和修改返回值,替换参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hookTest1 ( ){ Java .perform (function ( ){ var utils = Java .use ("类名" ); utils.$init .overload ....... utils.method .overload ('int' ,'int' ).implementation = function (a, b ){ a = 123 ; b = 456 ; var retval = this .method (a, b); console .log (a, b, retval); return retval; this .method (xxx,xxx); } }) }
2.Hook静态方法和字段
1 2 3 4 5 6 7 function hookTest9 ( ){ Java .perform (function ( ){ var ClassName =Java .use ("类名" ); ClassName .privateFunc ("6666" ); }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hookTest5 ( ){ Java .perform (function ( ){ var utils = Java .use ("类名" ); utils.staticField .value = "我是被修改的静态变量" ; console .log (utils.staticField .value ); Java .choose ("类名" , { onMatch : function (obj ){ obj._privateInt .value = "123456" ; obj.privateInt .value = 9999 ; }, onComplete : function ( ){ } }); }); }
3.主动调用
非静态方法
1 2 3 4 5 6 7 8 9 10 11 function hooktest ( ){ Java .perform (function ( ) { var check = Java .use ("类名" ); var check_obj = check.$new(); var res = check_obj.get_flag ('value' ); console .log ("FLAG " + res); }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hookTest10 ( ){ Java .perform (function ( ) { var ret = null ; Java .choose ("类名" ,{ onMatch :function (instance ){ ret=instance.privateFunc ("aaaaaaa" ); }, onComplete :function ( ){ } }); }) }
4.hook内部类
1 2 3 4 5 6 7 8 9 10 11 12 function hookTest6 ( ){ Java .perform (function ( ){ var innerClass = Java .use ("类名$InnerClass" ); console .log (innerClass); innerClass.$init .implementation = function ( ){ console .log ("eeeeeeee" ); } }); }
native hook so文件加载与JNI注册:https://bbs.kanxue.com/thread-285950.htm
如何获取Frida中特定函数的地址,有很多方法可以做到这一点
使用 Frida API:Module.enumerateExports()
使用 Frida API: Module.getExportByName()
使用 Frida API:Module.findExportByName()
计算偏移量并将add()其映射到 Module.getBaseAddress()地址
使用 Frida API:Module.enumerateImports()
Native函数的基础Hook打印
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 function hookTest2 ( ){Java .perform (function ( ){ var helloAddr = Module .findExportByName ("so文件名" ,"函数名" ); console .log (helloAddr); if (helloAddr != null ){ Interceptor .attach (helloAddr,{ onEnter : function (args ){ console .log (args[0 ]); console .log (this .context .x1 ); console .log (args[1 ].toInt32 ()); console .log (args[2 ].readCString ()); console .log (hexdump (args[2 ])); }, onLeave : function (retval ){ console .log (retval); console .log ("retval" ,retval.toInt32 ()); } }) } } 字符串类型
动态注册函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hookTest6 ( ){ Java .perform (function ( ){ var soAddr = Module .findBaseAddress ("lib52pojie.so" ); console .log (soAddr); var funcaddr = soAddr.add (0x1071C ); console .log (funcaddr); if (funcaddr != null ){ Interceptor .attach (funcaddr,{ onEnter : function (args ){ }, onLeave : function (retval ){ console .log (retval.toInt32 ()); retval.replace ('value' ); } }) } }) }
0x4 frida检测与对抗 文件名/端口检测 修改文件名
1 2 3 ./fs1 -l 0.0.0.0:6666 adb forward tcp:6666 tcp:6666 frida -H 127.0.0.1:6666 进程名 -l hook.js
使用脚本定位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function hook_dlopen ( ) { var android_dlopen_ext = Module .findExportByName (null , "android_dlopen_ext" ); console .log ("addr_android_dlopen_ext" , android_dlopen_ext); Interceptor .attach (android_dlopen_ext, { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr != null && pathptr != undefined ) { var path = ptr (pathptr).readCString (); console .log ("android_dlopen_ext:" , path) } }, onLeave : function (retvel ) { } }) } setImmediate (hook_dlopen);
检测map 什么是map
/proc/self/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 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 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 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 ); } } }); }
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 ); } } }) }
inline hook检测 通过Frida查看一个函数hook之前和之后的机器码,以此来判断是否被Frida的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 ; }
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" ); } }