免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!
内容参考于:图灵Python学院
工具下载:
链接:https://pan.baidu.com/s/1bb8NhJc9eTuLzQr39lF55Q?pwd=zy89
提取码:zy89
复制这段内容后打开百度网盘手机App,操作更方便哦
上一个内容:40.安卓逆向2-frida hook技术-过firda检测(四)(通过拦截so文件的创建和拦截检测frida函数过检测)
本次通过查找app中检测frida的函数,然后使用frida对检测frida的函数进行hook,通过hook让它失效来绕过检测
首先还是通过hook加载so文件的函数,看看它是加载到什么文件时进行的退出,然后使用ida反编译so文件,下方是检测app加载so文件的frida代码
function main(){// 这段代码是给一个叫"Frida"的工具用的脚本
// Frida的作用是:可以钻进手机里的APP内部,看看这个APP在偷偷做什么
// 我们这段脚本的具体任务是:盯着APP加载"特殊文件"的行为// 首先,我们要找到APP加载文件时会用到的两个"工具函数"// 第一个工具函数叫"dlopen"
// 所有运行在Linux或安卓系统上的程序,要加载"动态链接库"(一种特殊文件,后缀通常是.so)时,经常会用到它
// "Module.findExportByName(null, "dlopen")"的作用:
// 1. 在系统的所有功能里找(null表示不限制范围)
// 2. 找到名字叫"dlopen"的那个功能,记录下它在内存中的位置
// 3. 把找到的结果存在变量"dlopen"里,方便后面使用
var dlopen = Module.findExportByName(null, "dlopen");// 第二个工具函数叫"android_dlopen_ext"
// 这是安卓系统专门设计的加强版加载工具,功能比dlopen更多一点
// 有些安卓APP会用这个函数来加载特殊的.so文件
// 下面这行代码的作用和上面类似:找到这个函数的位置,存在变量里
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");// 接下来,我们要给第一个工具函数"dlopen"装个"监控器"
// 当APP调用dlopen加载文件时,我们就能立刻知道
// "Interceptor.attach"就是Frida提供的"装监控器"的功能
Interceptor.attach(dlopen, {// 当APP刚开始调用dlopen函数时,会自动执行下面的代码// 可以理解为:监控器发现"有人要开始用这个工具了"onEnter: function (args) {// "args"是APP传给dlopen的参数(就像我们给工具传递的"指令")// dlopen的第一个参数很重要:它告诉工具"要加载的文件在哪里"// 这里的"args[0]"就是取第一个参数(计算机里计数从0开始)var path_ptr = args[0];// 刚才拿到的"path_ptr"其实是个"内存地址"(类似文件在仓库里的货架编号)// 我们需要根据这个编号,找到实际的文件路径(比如"/data/lib/test.so")// "ptr(path_ptr)"是把编号转成Frida能识别的格式// ".readCString()"是按照计算机存储文字的规则,把地址对应的内容读出来var path = ptr(path_ptr).readCString();// 最后,把我们发现的信息打印到屏幕上// 这样我们就能清楚地看到:这个APP用dlopen加载了哪个文件console.log("[发现使用dlopen加载文件:] ", path);},// 当APP用完dlopen函数(加载文件完成后),会执行下面的代码// 可以理解为:监控器发现"这个人用完工具了"onLeave: function (retval) {// 目前这里什么都没做,留空是因为我们暂时只关心"开始加载"这个动作// 如果以后想知道"加载成功了吗",可以在这里处理返回值retval}
});// 下面是给第二个工具函数"android_dlopen_ext"装监控器,原理和上面完全一样
Interceptor.attach(android_dlopen_ext, {// 当APP刚开始调用这个安卓特有的加载函数时onEnter: function (args) {// 同样取第一个参数:要加载的文件地址var path_ptr = args[0];// 把地址转成我们能看懂的文件路径var path = ptr(path_ptr).readCString();// 打印信息:APP用安卓特有的工具加载了哪个文件console.log("[发现使用安卓专用dlopen_ext加载文件:] ", path);},// 当APP用完这个函数后onLeave: function (retval) {// 这里也暂时什么都不做}
});
/**
总结代码的效果:就像我们在 APP 的 "文件加载通道" 上装了两个摄像头,一个盯着普通加载通道,一个盯着安卓专用通道。
只要 APP 从这些通道加载文件(特别是.so 格式的文件),摄像头就会立刻拍下 "文件地址" 并显示出来,让
我们清楚知道这个 APP 在运行时偷偷加载了哪些底层文件。
这种监控在分析 APP 的工作原理、查找恶意软件行为时非常有用。
*/
}
main()
如下图注入上方的代码后,在加载了下图红框的so文件后,frida退出了,这说明 libmsaoaidsec.so 里面有frida检测
查看线程,从下图红框可以看出,libmsaoaidsec.so创建了三个线程,然后退出了,这说明检测frida的代码在这三个线程中,记住这三个值一会要用, 1c544、1b8d4、 26e5c
![]()
// 定义一个函数,名字叫 hook_patch,作用是"钩住"线程创建的行为 function hook_patch() { // 1. 找到系统里负责创建线程的函数(pthread_create)的地址 // 解释: // - pthread_create 是 Linux/Android 系统中创建线程的核心函数,所有程序创建线程都要调用它 // - Module.findExportByName("libc.so", "pthread_create") 意思是:从 libc.so 这个系统库中,查找导出的 pthread_create 函数的地址 // - libc.so 是系统基础库,包含了很多常用的系统函数(比如创建线程、文件操作等) var patch = Module.findExportByName("libc.so", "pthread_create");// 2. 打印找到的 pthread_create 函数的地址(调试用,确认是否找到了目标函数) // 比如可能会输出:[pth_create] 0x7f8a8b2c3d40(这是一个内存地址) console.log("[pth_create]", patch);// 3. 拦截(Hook)这个 pthread_create 函数,监控它的调用 // Interceptor.attach 是 Frida 的拦截函数,第一个参数是要拦截的函数地址(这里就是上面找到的 patch) // 第二个参数是一个对象,里面定义了拦截后的行为(进入函数时做什么,离开函数时做什么) Interceptor.attach(patch, {// onEnter:当被拦截的函数(pthread_create)被调用时,会执行这里的代码onEnter: function (args) {// args 是一个数组,存放了调用 pthread_create 时传入的参数// pthread_create 的函数原型是:// int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg)// 所以 args[0] = 线程ID指针,args[1] = 线程属性,args[2] = 线程要执行的函数(核心!线程启动后会跑这个函数),args[3] = 传给线程函数的参数// 4. 通过线程要执行的函数地址(args[2]),找到它属于哪个模块(.so 文件)// Process.findModuleByAddress(地址) 会返回这个地址所在的模块信息(比如模块名、路径等)var module = Process.findModuleByAddress(args[2]);// 5. 检查是否成功找到模块(避免空值报错)if (module != null) {// 打印线程相关信息:// - module.name:模块的名字(比如 libnative.so,就是这个模块创建了线程)// - args[2].sub(module.base):计算线程函数在模块中的偏移量(相对位置)// 偏移量 = 函数的实际地址 - 模块的基地址(模块加载到内存的起始地址)// 比如模块基地址是 0x1000,函数地址是 0x1200,偏移量就是 0x200console.log("开启线程-->", module.name, args[2].sub(module.base));}},// onLeave:当被拦截的函数(pthread_create)执行完毕,准备返回时,会执行这里的代码// 这里暂时为空,说明不需要处理函数返回后的逻辑onLeave: function (retval) {} }); } function main(){ // 这段代码是给一个叫"Frida"的工具用的脚本 // Frida的作用是:可以钻进手机里的APP内部,看看这个APP在偷偷做什么 // 我们这段脚本的具体任务是:盯着APP加载"特殊文件"的行为// 首先,我们要找到APP加载文件时会用到的两个"工具函数"// 第一个工具函数叫"dlopen" // 所有运行在Linux或安卓系统上的程序,要加载"动态链接库"(一种特殊文件,后缀通常是.so)时,经常会用到它 // "Module.findExportByName(null, "dlopen")"的作用: // 1. 在系统的所有功能里找(null表示不限制范围) // 2. 找到名字叫"dlopen"的那个功能,记录下它在内存中的位置 // 3. 把找到的结果存在变量"dlopen"里,方便后面使用 var dlopen = Module.findExportByName(null, "dlopen");// 第二个工具函数叫"android_dlopen_ext" // 这是安卓系统专门设计的加强版加载工具,功能比dlopen更多一点 // 有些安卓APP会用这个函数来加载特殊的.so文件 // 下面这行代码的作用和上面类似:找到这个函数的位置,存在变量里 var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");// 接下来,我们要给第一个工具函数"dlopen"装个"监控器" // 当APP调用dlopen加载文件时,我们就能立刻知道 // "Interceptor.attach"就是Frida提供的"装监控器"的功能 Interceptor.attach(dlopen, { // 当APP刚开始调用dlopen函数时,会自动执行下面的代码 // 可以理解为:监控器发现"有人要开始用这个工具了" onEnter: function (args) {// "args"是APP传给dlopen的参数(就像我们给工具传递的"指令")// dlopen的第一个参数很重要:它告诉工具"要加载的文件在哪里"// 这里的"args[0]"就是取第一个参数(计算机里计数从0开始)var path_ptr = args[0];// 刚才拿到的"path_ptr"其实是个"内存地址"(类似文件在仓库里的货架编号)// 我们需要根据这个编号,找到实际的文件路径(比如"/data/lib/test.so")// "ptr(path_ptr)"是把编号转成Frida能识别的格式// ".readCString()"是按照计算机存储文字的规则,把地址对应的内容读出来var path = ptr(path_ptr).readCString();// 最后,把我们发现的信息打印到屏幕上// 这样我们就能清楚地看到:这个APP用dlopen加载了哪个文件console.log("[发现使用dlopen加载文件:] ", path);}, // 当APP用完dlopen函数(加载文件完成后),会执行下面的代码 // 可以理解为:监控器发现"这个人用完工具了" onLeave: function (retval) {// 目前这里什么都没做,留空是因为我们暂时只关心"开始加载"这个动作// 如果以后想知道"加载成功了吗",可以在这里处理返回值retval } });// 下面是给第二个工具函数"android_dlopen_ext"装监控器,原理和上面完全一样 Interceptor.attach(android_dlopen_ext, { // 当APP刚开始调用这个安卓特有的加载函数时 onEnter: function (args) {// 同样取第一个参数:要加载的文件地址var path_ptr = args[0];// 把地址转成我们能看懂的文件路径var path = ptr(path_ptr).readCString();// 打印信息:APP用安卓特有的工具加载了哪个文件console.log("[发现使用安卓专用dlopen_ext加载文件:] ", path);if(path.indexOf("libmsaoaidsec.so")!=-1){// 调用查看线程的函数hook_patch()} }, // 当APP用完这个函数后 onLeave: function (retval) {// 这里也暂时什么都不做 } }); /** 总结代码的效果:就像我们在 APP 的 "文件加载通道" 上装了两个摄像头,一个盯着普通加载通道,一个盯着安卓专用通道。 只要 APP 从这些通道加载文件(特别是.so 格式的文件),摄像头就会立刻拍下 "文件地址" 并显示出来,让 我们清楚知道这个 APP 在运行时偷偷加载了哪些底层文件。 这种监控在分析 APP 的工作原理、查找恶意软件行为时非常有用。 */ } main()
然后把apk进行解压,找到它的 libmsaoaidsec.so 文件拖到ida中进行反编译
然后ida加载完后,点击下图红框,然后按CTRL+F
然后搜索1c544、1b8d4、 26e5c这三个,如下图首先是搜索1c544,双击下图红框位置就可以跳转到1c544了,跳转之后按f5转伪c代码
直接把下图红框的代码全选,然后复制给ai大模型,让它解释
如下图ai的解释,这个 1c544 做的是字符串相关和定时任务(无限循环执行周期性任务),跟检测frida无关,所以下一个
然后搜索1b8d4,老样子全选复制给ai大模型
如下图大模型的解释,它有点可疑,根据大模型的解释,1b8d4会检测,还会暂停,它可能是检测firda然后暂停frida
然后接着看最后一个 26e5c
大模型的解释,三个线程都分析完了,现在只有 1b8d4 是最可疑的,接下来通过调用进一步分析(它有一个特征)
再次回到1b8d4 中,鼠标点击下图红框位置,然后按x
可以看谁调用了1b8d4,如下图有两个位置调用了它,但都是在1B924中调用的
1B924 调用 1b8d4,然后双击上图任意一个,然后点击下图红框位置,再次按x,查看 1B924 是谁调用的
如下图只有一个地方调用了 1B924
然后现在的调用栈是 1BEC4 调用 1B924 调用 1b8d4
继续重复上方的步骤,按 x 查看 1BEC4 谁调用的,如下图只有一个位置对1BEC4进行了调用,然后双击它进入函数
然后特征就来了,如下调用1BEC4的函数叫做init_proc,现在的调用栈 init_proc 调用 1BEC4 调用 1B924 调用 1b8d4
然后 init_proc 不懂没关系,给ai大模型,让它解释,如下图ai大模型的结束,很详细,所以 1b8d4 它必然是检测frida的相关函数,然后 1b8d4 里面只做了暂停操作,没有关闭frida的操作,所以调用 1b8d4 函数的1B924 函数才是检测frida的函数,接下来只需要把 1B924 函数进行hook就可以过检测了
hook代码,注意 var 构造函数调用偏移 这个值,可能每个手机不一样,它的找法继续往下看,写后面了
// 主函数:根据IDA中获取的地址信息,替换目标SO中的指定函数
function 根据IDA地址替换函数() {// 1. 获取linker64模块的基地址// 来源:linker64是安卓系统自带的64位动态链接器(负责加载所有SO文件),固定名称为"linker64"// 作用:基地址是模块加载到内存的起始位置,类似"小区大门的地址"var 链接器64基地址 = Module.getBaseAddress("linker64")// 打印基地址用于调试(实际值会随系统/进程变化,例如0x7f8a0000)console.log("linker64模块基地址(内存起始位置):", 链接器64基地址)// 2. 定义call_constructors函数的偏移量// 来源:这个值必须从IDA中获取!步骤是:// a. 用IDA打开目标设备的linker64文件(通常在/system/bin/linker64)// b. 搜索函数名"call_constructors"// c. 记录该函数相对于linker64基地址的偏移(例如0x50C00)// 作用:偏移量是函数在模块内部的位置,类似"小区内某栋楼的门牌号"var 构造函数调用偏移 = 0x50C00 // 这里必须替换为你从IDA中看到的实际值!// 3. 计算call_constructors函数的实际内存地址// 公式:实际地址 = 模块基地址 + 偏移量(类似"小区大门地址 + 门牌号 = 具体住户地址")// 例如:基地址0x7f8a0000 + 偏移0x50C00 = 实际地址0x7f8f0C00var 构造函数调用地址 = 链接器64基地址.add(构造函数调用偏移)console.log("call_constructors函数实际地址:", 构造函数调用地址)// 4. 拦截call_constructors函数// 原因:这个函数是linker64加载SO文件时,用于初始化SO中构造函数的关键函数// 目的:在目标SO(libmsaoaidsec.so)加载完成并初始化时,及时执行替换操作var 拦截器 = Interceptor.attach(构造函数调用地址, {// 当call_constructors函数被调用时(即有SO正在初始化),执行以下代码onEnter: function (参数列表) {console.log("检测到SO文件正在初始化(进入call_constructors函数)")// 5. 检查目标SO是否已加载// 来源:"libmsaoaidsec.so"是你要操作的目标SO文件名(需替换为你的实际SO名)// 作用:确认我们要修改的SO已经被系统加载到内存中var 目标模块 = Process.findModuleByName("libmsaoaidsec.so")// 6. 如果目标SO已加载,则执行替换if (目标模块 != null) {console.log("目标SO已加载:" + 目标模块.name + ",基地址:" + 目标模块.base)// 7. 替换目标SO中的指定函数// ① 目标函数地址计算:// 来源:0x1B924是从IDA中获取的目标函数偏移量,步骤:// a. 用IDA打开libmsaoaidsec.so// b. 找到你要替换的函数(例如sub_1B924)// c. 记录该函数相对于SO基地址的偏移// 公式:目标函数实际地址 = 目标SO基地址 + 偏移量// ② 新函数定义:// 返回值类型"void"和参数列表[]必须与原函数一致(从IDA中查看函数原型获取)Interceptor.replace(目标模块.base.add(0x1B924), // 目标函数的实际内存地址new NativeCallback(function () { // 替换后的新函数console.log("目标函数(偏移0x1B924)已被成功替换!")// 这里可以添加自定义逻辑,例如:// - 返回固定值(如return 0;)// - 修改原函数参数(需在参数列表中定义)// - 调用原函数后修改返回值}, "void", // 新函数返回值类型(必须与原函数一致,从IDA中查)[] // 新函数参数列表(必须与原函数一致,从IDA中查)))// 8. 替换完成后解除拦截// 原因:避免后续加载其他SO时重复执行替换操作拦截器.detach()console.log("已完成替换,解除对call_constructors的拦截")}},// 函数执行结束时的操作(这里不需要,留空)onLeave: function (返回值) {}})
}// 执行主函数,启动整个替换流程
根据IDA地址替换函数()
如下图成功绕过检测,app正常启动
然后 构造函数调用偏移 值的找法,把下图红框的文件,使用 adb pull 下载到电脑上
adb pull /system/bin/linker64 xxxxx
上方的指令执行完后,如下图,就把 linker64 文件下载到电脑上了
然后把它拖到ida中,然后搜索 constructor,下图红框的就是我们要找的,
这个函数的地址50C00
不分析的方式过检测,直接把 1c544、1b8d4、 26e5c 这三个线程全返回空
// 定义一个函数,用于将指定地址的代码替换为"直接返回"
// 作用:让目标函数被调用时直接退出,不执行原来的逻辑
function nop_addr(addr) {// 第一步:修改内存权限为"可读可写可执行"(rwx)// 原因:默认情况下代码段可能没有写权限,无法修改指令// 参数说明:// - addr:要修改的内存地址// - 4:修改的内存大小(4字节,足够存放一条返回指令)// - 'rwx':新的权限(read/write/execute)Memory.protect(addr, 4 , 'rwx');// 第二步:创建一个Arm64架构的指令写入器// 注意:这里假设目标设备是64位ARM架构(手机几乎都是ARM)var w = new Arm64Writer(addr);// 第三步:写入"返回指令"(ret)// 效果:当程序执行到这里时,会直接退出当前函数,不执行后续代码w.putRet();// 第四步:刷新写入的指令(确保生效)w.flush();// 第五步:释放写入器资源(避免内存泄漏)w.dispose();
}// 定义主函数:Hook动态链接器的构造函数调用流程,监控目标SO加载
function hook_call_constructors() {// 声明变量:用于存储动态链接器(linker)的模块信息let linker = null;// 判断当前进程是32位还是64位// Process.pointerSize是指针大小:32位系统为4字节,64位为8字节if (Process.pointerSize === 4) {// 32位系统的动态链接器名为"linker"linker = Process.findModuleByName("linker");} else {// 64位系统的动态链接器名为"linker64"(大部分现代手机是64位)linker = Process.findModuleByName("linker64");}// 声明变量:存储找到的关键函数地址// call_constructors_addr:SO初始化函数的地址// get_soname:获取SO文件名的函数(这里未实际使用)let call_constructors_addr, get_soname;// 枚举动态链接器模块中的所有符号(符号=函数名/变量名+地址)// 作用:从链接器中找到我们需要监控的函数let symbols = linker.enumerateSymbols();// 遍历所有符号,筛选出需要的函数for (let index = 0; index < symbols.length; index++) {let symbol = symbols[index];// 匹配"__dl__ZN6soinfo17call_constructorsEv"符号// 这个符号对应的函数是:动态链接器加载SO时,执行SO内部构造函数的入口// 来源:安卓系统动态链接器的标准符号,可通过符号表查询到if (symbol.name === "__dl__ZN6soinfo17call_constructorsEv") {call_constructors_addr = symbol.address; // 记录这个函数的内存地址} // 匹配"__dl__ZNK6soinfo10get_sonameEv"符号(可选,备用)// 作用:通过这个函数可以获取当前正在加载的SO的文件名else if (symbol.name === "__dl__ZNK6soinfo10get_sonameEv") {// 将函数地址包装为NativeFunction,方便后续调用// 参数说明:// - symbol.address:函数的内存地址// - "pointer":函数返回值类型(返回SO文件名的字符串地址)// - ["pointer"]:函数参数(传入soinfo结构体的指针)get_soname = new NativeFunction(symbol.address, "pointer", ["pointer"]);}}// 打印找到的call_constructors函数地址(调试用,确认是否找到)console.log("call_constructors函数地址:", call_constructors_addr);// 拦截call_constructors函数:监控所有SO的初始化过程var listener = Interceptor.attach(call_constructors_addr, {// 当call_constructors函数被调用时触发(有SO正在加载初始化)onEnter: function (args) {console.log("检测到SO文件正在初始化(进入call_constructors)");// 检查我们关注的目标SO(libmsaoaidsec.so)是否已加载// 注意:这里的SO文件名需要替换为你实际要操作的SO名称var module = Process.findModuleByName("libmsaoaidsec.so");// 如果目标SO已经加载到内存中if (module != null) {console.log("找到目标SO:" + module.name + ",基地址:" + module.base);// 对目标SO中的三个关键函数执行"替换为返回"操作// 0x1c544、0x1b8d4、0x26e5c是函数在SO中的偏移量// 偏移量来源:通过IDA/ Ghidra等反编译工具分析目标SO得到// 计算实际地址公式:实际地址 = SO基地址(module.base) + 偏移量nop_addr(module.base.add(0x1c544)); // 处理第一个函数console.log("0x1c544: 已替换为返回指令(函数被跳过)");nop_addr(module.base.add(0x1b8d4)); // 处理第二个函数console.log("0x1b8d4: 已替换为返回指令(函数被跳过)");nop_addr(module.base.add(0x26e5c)); // 处理第三个函数console.log("0x26e5c: 已替换为返回指令(函数被跳过)");// 完成替换后,解除对call_constructors的拦截// 原因:避免后续加载其他SO时重复执行替换操作listener.detach();console.log("所有目标函数处理完毕,已解除拦截");}},// 函数执行结束时的操作(这里不需要处理,留空)onLeave: function (retval) {}});
}// 执行主函数,启动整个Hook流程
hook_call_constructors();