文章主要讨论了在模拟器下对ARM架构的so文件加载的探究过程,包括环境搭建、frida的版本问题、程序流程探究,并比较了真机与模拟器在so层执行流程上的差异。通过hook dlopen函数和NativeBridgeLoadLibrary函数,获取了加载的so文件,并成功绕过了frida检测。
使用了mumu模拟器进行frida监控,并解决了frida 14.2.18版本在模拟器中的兼容性问题,升级到16.0.11版本。
探究了android真机和模拟器在so层执行流程的差异,并成功hook了b站的dlopen函数,获取了加载的so文件。
通过hook NativeBridgeLoadLibrary函数,成功获取了模拟器加载的so文件,并绕过了frida检测。
理解了模拟器加载so的过程,通过NativeBridgeLoadLibrary函数实现跨架构的SO加载,并探讨了Android Native Bridge机制的作用。
提出了新的问题,如如何hook NativeBridgeLoadLibrary函数,以及如何得到对应的frida检测的so文件,并进行了后续的探究和尝试。
作者
论
坛账号:
chenchenchen777
利用frida 探究对于模拟器下arm so加载
这里是对于模拟器下的arm下的so文件进行的分析探究,实际上对于这些的知识上,相关的文章是很少的,这次也算是对于模拟器的研究和分析了
环境搭建:
这里使用的是
mumu模拟器
进行的
frida监控模拟器arm的so
的加载过程
安装mumu模拟器:
MuMu模拟器官网_安卓12模拟器_网易手游模拟器
同之前的真机端一样,在移动端安装上对应的
frida-server
,以及对应的PC端安装frida,操作和之前其实是大相径庭的
frida的14.2.18问题
由于我PC端的这个frida是14.2.18版本的,所以我一开始尝试的是下载对应的14.2.18版本的
frida-server
的x86_64的。
报错
但是在模拟器中进行启动的时候,发现是会有报错的,但是还是会启动frida-server的服务的,所以还是去尝试过直接注入frida,但是程序会卡住。
通过deepseek来查看了相关可能出现的问题
在本机和模拟器的frida以及frida-server匹配的14.2.18版本下是会出现兼容问题的,于是这里去尝试去实现更换本地frida版本和frida-server版本的操作。
frida的16.0.11
本地端
复制代码 隐藏代码
pip install --upgrade frida==16.0.11
同时更新frida-tools
复制代码 隐藏代码
pip install --upgrade frida-tools
模拟器端
下载对应的frida-server版本
这里不知道是不是模拟器的特点,还是独属于mumu模拟器的,需要把frida-server放入对应的目录(没测试过不放会有什么问题)
然后就是常规的操作了,启动server服务
复制代码 隐藏代码
adb push frida-server /data/local/tmp/
adb shell
su
chmod 777 /data/local/tmp/frida-server-16.0.11-android-x86_64
./data/local/tmp/frida-server-16.0.11-android-x86_64
这里启动起来之后就没有再进行报错输出了,同时尝试了最简单的frida注入操作,发现是没问题的
这里是因为是以spwned启动的b站,是由frida检测的,所以程序崩溃了,但是能够实现注入
程序流程探究
按照我自己对于这个作业的要求,需要是去探究android真机和模拟器在so层执行流程之间的差别。
真机测试
这里去尝试了HOOK了
b站
的dlopen函数,看看执行得到的so文件是什么
复制代码 隐藏代码
function hook_dlopen() {
var interceptor = 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)
}
}
}
)
return interceptor
}
setImmediate(hook_dlopen)
这里其实是哔哩哔哩的frida检测的so的位置了,可以发现的是,我们能够去利用dlopen去实现对于加载的so文件进行打印
模拟器测试
由于模拟器是X86_64的构架模拟的android机来实现的对于程序的执行,可能使用不同的 dlopen 函数变体来加载so文件,所以HOOK代码也略微的进行了修改
复制代码 隐藏代码
function hook_dlopen() {
// Hook 所有可能的 dlopen 变体
const dlopenFuncs = [
'android_dlopen_ext',
'dlopen',
'__loader_dlopen'
];
let interceptors = [];
dlopenFuncs.forEach(funcName => {
let funcPtr = Module.findExportByName(null, funcName);
if (funcPtr) {
let interceptor = Interceptor.attach(funcPtr, {
onEnter: function(args) {
var pathptr = args[0];
if (pathptr && !pathptr.isNull()) {
var path = ptr(pathptr).readCString();
console.log("[LOAD]", path);
// 检查是否是可疑的检测库
if (path && (path.includes("libtt") || path.includes("libbili") ||
path.includes("security") || path.includes("protect"))) {
console.warn("!!! Possible Frida detection library loaded:", path);
// 打印调用栈可以帮助定位谁加载了这个库
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
}
}
}
});
interceptors.push(interceptor);
console.log(`Hooked ${funcName} at ${funcPtr}`);
}
});
// 返回所有拦截器以便后续管理
return interceptors;
}
// 延迟执行以避免错过早期加载的库
setImmediate(hook_dlopen);
对比结果引发的思考和问题
其实是可以发现模拟器也是挂掉了,但是这里会引起思考的是为什么,在真机测试下的HOOK的dlopen相关函数得到的so文件和在模拟器下HOOK得到的so文件
一点不一样
这里b站的版本是7.76.0的版本,在这个版本下面我是写过相关的一个绕过frida检测的帖子的,这个frida检测的so是libmsaoaidsec.so,但是在模拟器端这个so甚至于没有被
'android_dlopen_ext', 'dlopen','__loader_dlopen'
这几个HOOK函数都没有将这个
libmsaoaidsec.so
捕获到,那么可能会去考虑执行流程的问题。
架构之间的兼容问题
由于其实从一开始对于模拟器的研究很少,所以会去考虑的是为什么,在
X86或者是X86_64
的架构的模拟器可以去实现
ARM架构
的指令集。
通过搜索可以得知是主要依赖于
动态二进制翻译
和
系统级的兼容层技术
动态二进制翻译(Dynamic Binary Translation, DBT)
-
作用
:实时将 ARM 指令逐块翻译为 x86_64 指令。
-
流程
-
-
将每段 ARM 指令翻译为等效的 x86_64 指令。
-
系统调用转发
以上是引发思考之后提问
deepseek得到的解答
。
个人理解
所以这里其实和在安卓逆向中的VMP很像了,自定义架构去定义自己的加密函数,实现按照不同的操作(像加减乘除,位运算这些)。
同时也能理解为什么在android虚拟机或者是云手机会需要逆向了,
动态二进制翻译
按照我们逆向手的思路其实就是HOOK每一条指令,然后修改代码,从ARM转为x86_64,实现一个架构的转变。
同时,也是得益于资料的搜索,开始想着和研究
AOSP源码的对于模拟器的执行流程处理
。
AOSP源码中的对于模拟器的执行流程处理
我以前发表过Android真机端的so文件的真实的执行流程的
【新提醒】Android SO文件加载过程探究 - 吾爱破解 - 52pojie.cn
但是这篇文章只是局限于了对于android真机也就是arm的处理,并没有对于这个真实情况下的模拟器进行so层的执行流程分析处理,这里正好利用mumu模拟器看看,对于不同架构下的处理。
Android Native Bridge 机制
首先我们需要了解到的是
Android Native Bridge 机制
,在 MuMu 模拟器中执行 ARM 架构的 SO 文件,其核心依赖
二进制翻译
和
Android Native Bridge 机制
,这里面就包含了我们之前所说的
动态二进制翻译
,从而实现支持跨架构运行的一个过程。
梳理真机下的执行流程
我们首先是通过
System.load()
进入
复制代码 隐藏代码
@CallerSensitive
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
此方法最终调用
Runtime.load0()
,然后进入
nativeLoad()
函数。
Runtime_nativeLoad
→
vm->LoadNativeLibrary
在这里之后调用了
OpenNaitveLibrary
进入
JavaVMExt::LoadNativeLibrary
方法后,最终会调用
dlopen
进行真正的 SO 文件加载。
复制代码 隐藏代码
vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, caller, &error_msg);
在 Android 12 及以上版本,会调用
android_dlopen_ext
返回
__loader_android_dlopen_ext
。
复制代码 隐藏代码
void* __loader_android_dlopen_ext(const char* filename,
int flags,
const android_dlextinfo* extinfo,
const void* caller_addr) {
return dlopen_ext(filename, flags, extinfo, caller_addr);
}
该方法最终调用
dlopen_ext()
。
复制代码 隐藏代码
static void* dlopen_ext(const char* filename,
int flags,
const android_dlextinfo* extinfo,
const void* caller_addr) {
ScopedPthreadMutexLocker locker(&g_dl_mutex);
void* result = do_dlopen(filename, flags, extinfo, caller_addr);
return result;
}
do_dlopen(filename, flags, extinfo, caller_addr)
在
do_dlopen()
中,会调用
find_library()
进行 SO 文件的真正加载。
复制代码 隐藏代码
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
这里是对于soinfo的赋值,同时在这里开始调用so的.init_proc函数,接着调用.init_array中的函数,最后才是JNI_OnLoad函数。最后到达
find_libraries
执行最后的处理。
构架检查
同时在
find_library
的位置,会出现对于ELF文件头读取的字段,通过解析 SO 文件的
ELF 头
(
e_machine
字段)判断架构是否匹配。来确定对于构架的检测
初始化
Native Bridge
当发现了对应的架构不符合ARM,需要进行Native Bridge转换时,那么就会实现初始化Native Bridge
Native Bridge 触发
在
OpenNaitveLibrary函数
中,会去判断是否对于触发Native Bridge
核心决策点
:选择原生 dlopen 或 Native Bridge 加载
通过在这里对于Native Bridge的是否对目标 SO 启用 Native Bridge
当判断到架构并不匹配的时候,就需要去利用Native Bridge 转换架构实现跨架构执行操作,那么就不会进入dlopen的函数,而是进入NativeBridgeLoadLibrary
流程图
个人对于模拟器加载so的过程的理解:
新的问题
在模拟器中,跨架构的 SO 加载会通过
NativeBridgeLoadLibrary
函数(而非标准
dlopen
)完成
有了流程图是对于模拟器执行so文件流程有了一个了解,但是我们需要其实是HOOK,那么我们如何像在真机端这种去
HOOK dlopen
函数就能够去得到
dlopen的参数从而得到加载的so文件名
?
那么是否我们能够去HOOK
NativeBridgeLoadLibrary
函数呢?NativeBridgeLoadLibrary是在
Native Bridge 库
里面的函数,同时也是AOSP源码中的函数。我们能否去定位到这个so文件呢?
提出了疑问,自己的一些猜测,确实可以去尝试一下
这里我为了去确定
NativeBridgeLoadLibrary
函数,产生过这样的代码
复制代码 隐藏代码
Java
.perform(() => {
// 加载 Native Bridge 库
const libnb = Module.load("libhoudini.so");
// 获取 NativeBridgeLoadLibrary 函数地址
constNBLoadLib = Module.getExportByName("libhoudini.so", "NativeBridgeLoadLibrary");
// Hook 函数
Interceptor.attach(NBLoadLib, {
onEnter(args) {
const libpath = args[0].readCString(); // 第一个参数是 SO 路径
const flag = args[1]; // 第二个参数是标志位 (int)
console.log(`[NB] 加载库: ${libpath}, flags=${flag}`);
// 可选:篡改加载路径(如重定向到其他 SO)
if (libpath.includes("target.so")) {
args[0].writeUtf8String("/data/local/tmp/fake.so");
console.log("已重定向 SO 路径!");
}
},
onLeave(retval) {
console.log(`[NB] 返回句柄: ${retval}`);
}
});
});
发现实际上是没有这个函数的
去查看了对应的libhoudini.so文件,发现实际上的是有函数符号,但是并没有NativeBridgeLoadLibrary函数
复制代码 隐藏代码
Java.perform(() => {
let libnb = Module.enumerateExportsSync("libhoudini.so");
libnb.forEach(exp => console.log(exp.name));
});
打印了一下符号
NativeBridgeItf
是
NativeBridgeItf是 Native Bridge 的核心接口表
,通常包含 loadLibrary,但是不知道的是loadLibrary对于这个结构体中的偏移位置。
这里我的想法是,不知道偏移地址就进行爆破HOOK,把所有可能loadLibrary出现在NativeBridgeItf的结构体的偏移都进行hook
复制代码 隐藏代码
Java.perform(() => {
constNativeBridgeItf = Module.findExportByName("libhoudini.so", "NativeBridgeItf");
const callbacks = NativeBridgeItf.readPointer();
// 遍历 0x0 ~ 0x50 的偏移
for (let offset = 0; offset 0x50; offset += Process.pointerSize) {
const func = callbacks.add(offset).readPointer();
Interceptor.attach(func, {
onEnter(args
) {
console.log(`[测试] 偏移 ${offset.toString(16)} 被调用,参数: ${args[0]}`);
}
});
}
});
但是这里出现了内存报错
这里开始去了解了对于这个
NativeBridgeItf
结构体的细节内容,实际上是存在版本差异的。
1. 低版本(Android 5.0~7.0)
核心函数集中在偏移 0x0 ~ 0x28
复制代码 隐藏代码
struct NativeBridgeItf {
// 基础字段
uint32_t version; // 版本号 (e.g., 1)
uint32_t padding; // 对齐填充 (64位下可能不存在)
// 函数指针表
bool (*initialize)(conststruct NativeBridgeRuntimeCallbacks* runtime_cbs); // 0x8 (64位)
void* (*loadLibrary)(constchar* libpath, int flag); // 0x10 (64位)
void* (*getTrampoline)(void* handle, constchar* name, constchar* shorty); // 0x18 (64位)
bool (*isCompatibleWith)(uint32_t bridge_version); // 0x20 (64位)
void* (*getNativeAddress)(void* arm_address); // 0x28 (64位)
// ... 其他扩展函数
};
结构体中的函数指针名通常为
loadLibrary
,对应偏移固定(如 64 位环境下偏移
0x10
)
2.
高版本(Android 8.0+)
复制代码 隐藏代码
struct NativeBridgeItf {
uint32_t version; // 版本号 (e.g., 2 或 3)
uint32_t padding;
// 基础函数(与低版本相同)
bool (*initialize)(...); // 0x8
void* (*loadLibrary)(...); // 0x10
// 扩展函数(新增)
void* (*loadLibraryExt)(const char* libpath, int flag, void* extinfo); // 0x18
void* (*getTrampolineExt)(...); // 0x20
void* (*createNamespace)(...); // 0x28
// ... 其他扩展函数
};
引入扩展接口
loadLibraryExt
,可能替代或补充
loadLibrary
,偏移可能后移(如
0x18
)
在固定的一个
NativeBridgeItf
结构体偏移之下的这个loadLibrary也是固定的,那么我们其实可以去考虑得到这个结构体在对应偏移的位置去得到相应的地址
这里能够看到mumu模拟器模拟的是android12版本的,所以对应的这个
NativeBridgeItf
也是对应的高版本上的结合体。
这里实际上一直在报错,按照的就是内存错误之类的信息,所以我还是打算去老老实实的
看源码
IDA分析结构体
这里对于这些结合体老老实实看看是什么参数
可以比对这这个IDA得到的结构体的结构去看看AOSP源码
可以看到这里是固定的,那么我们就去找对应结构的属性成员进行一个数据的打印
还原真实的NativeBridgeItf结构体数据
这里比对了对于安卓12以及IDA源码,直接去add对于的成员属性偏移来得到对应的结构体的值
复制代码 隐藏代码
Java.perform(() => {
console.log("\n====== 环境信息 ======");
console.log(` 进程架构: ${Process.arch}`);
console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);
constBuild = Java.use("android.os.Build");
console.log("\n====== 模块信息 ======");
const libhoudini = Module.findBaseAddress("libhoudini.so");
if (!libhoudini) {
console.error("[!] libhoudini.so 未加载");
return;
}
console.log(` libhoudini.so 基址: ${libhoudini}`);
console.log("\n====== NativeBridgeItf 符号信息 ======");
constNativeBridgeItf = Module.findExportByName("libhoudini.so", "NativeBridgeItf");
if (!NativeBridgeItf) {
console.error("[!] 找不到 NativeBridgeItf 符号");
return;
}
console.log(` NativeBridgeItf 符号地址: ${NativeBridgeItf}`);
console.log(` NativeBridgeItf 符号地址 偏移寻址 : ${libhoudini.add(0x0701D00)}`);
console.log("\n====== 结构体指针信息 ======");
const callbacks = NativeBridgeItf.readInt();
console.log(` version: ${callbacks}`);
const padding = NativeBridgeItf.add(0x4).readInt();
console.log(` padding: ${padding}`);
const initialize =
NativeBridgeItf.add(0x8).readPointer();
console.log(` initialize addr: ${initialize}`);
const loadLibrary = NativeBridgeItf.add(0x10).readPointer();
console.log(` loadLibrary addr: ${loadLibrary}`);
const getTrampoline = NativeBridgeItf.add(0x18).readPointer();
console.log(` getTrampoline addr: ${getTrampoline}`);
const isSupported = NativeBridgeItf.add(0x20).readPointer();
console.log(` isSupported addr: ${isSupported}`);
const getAppEnv = NativeBridgeItf.add(0x28).readPointer();
console.log(` getAppEnv addr: ${getAppEnv}`);
const isCompatibleWith = NativeBridgeItf.add(0x30).readPointer();
console.log(` isCompatibleWith addr: ${isCompatibleWith}`);
const getSignalHandler = NativeBridgeItf.add(0x38).readPointer();
console.log(` getSignalHandler addr: ${getSignalHandler}`);
const unloadLibrary = NativeBridgeItf.add(0x40).readPointer();
console.log(` unloadLibrary addr: ${unloadLibrary}`);
const getError = NativeBridgeItf.add(0x48).readPointer();
console.log(` getError addr: ${getError}`);
const isPathSupported = NativeBridgeItf.add(0x50).readPointer();
console.log(` isPathSupported addr: ${isPathSupported}`);
const unused_initAnonymousNamespace = NativeBridgeItf.add(0x58).readPointer();
console.log(` unused_initAnonymousNamespace addr: ${unused_initAnonymousNamespace}`);
const createNamespace = NativeBridgeItf.add(0x60).readPointer();
console.log(` createNamespace addr: ${createNamespace}`);
const linkNamespaces = NativeBridgeItf.add(0x68).readPointer();
console.log(` linkNamespaces addr: ${linkNamespaces}`);
const loadLibraryExt = NativeBridgeItf.add(0x70).readPointer();
console.log(` loadLibraryExt addr: ${loadLibraryExt}`);
const getVendorNamespace = NativeBridgeItf.add(0x78).readPointer();
console.log(` getVendorNamespace addr:
${getVendorNamespace}`);
const getExportedNamespace = NativeBridgeItf.add(0x80).readPointer();
console.log(` getExportedNamespace addr: ${getExportedNamespace}`);
const preZygoteFork = NativeBridgeItf.add(0x88).readPointer();
console.log(` preZygoteFork addr: ${preZygoteFork}`);
});
能够看到这里我们也是把对应的成员属性的值给打印出来了
但是虽然我们这里将这些数据都给打印出来了,但是我们其实实际上需要的参数就是loadLibrary参数,因为我们一直希望干的事情就是HOOK loadLibrary参数来类比于HOOK dlopen
复制代码 隐藏代码
Java.perform(() => {
console.log("\n====== 环境信息 ======");
console.log(` 进程架构: ${Process.arch}`);
console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);
constBuild = Java.use("android.os.Build");
console.log("\n====== 模块信息 ======");
const libhoudini = Module.findBaseAddress("libhoudini.so");
if (!libhoudini) {
console.error("[!] libhoudini.so 未加载");
return;
}
console.log(` libhoudini.so 基址: ${libhoudini}`);
console.log("\n====== NativeBridgeItf 符号信息 ======");
constNativeBridgeItf = Module.findExportByName("libhoudini.so", "NativeBridgeItf");
if (!NativeBridgeItf) {
console.error("[!] 找不到 NativeBridgeItf 符号");
return;
}
console.log(` NativeBridgeItf 符号地址: ${NativeBridgeItf}`);
console.log(` NativeBridgeItf 符号地址 偏移寻址 : ${libhoudini.add(0x0701D00)}`);
console.log("\n====== 结构体指针信息 ======");
const callbacks = NativeBridgeItf.readInt();
console.log(` version: ${callbacks}`);
const padding = NativeBridgeItf.add(0x4).readInt();
console.log(` padding: ${padding}`);
const initialize = NativeBridgeItf.add(0x8).readPointer();
console.log(` initialize addr: ${initialize}`);
const loadLibrary = NativeBridgeItf.add
(0x10).readPointer();
console.log(` loadLibrary addr: ${loadLibrary}`);
const getTrampoline = NativeBridgeItf.add(0x18).readPointer();
console.log(` getTrampoline addr: ${getTrampoline}`);
const isSupported = NativeBridgeItf.add(0x20).readPointer();
console.log(` isSupported addr: ${isSupported}`);
const getAppEnv = NativeBridgeItf.add(0x28).readPointer();
console.log(` getAppEnv addr: ${getAppEnv}`);
const isCompatibleWith = NativeBridgeItf.add(0x30).readPointer();
console.log(` isCompatibleWith addr: ${isCompatibleWith}`);
const getSignalHandler = NativeBridgeItf.add(0x38).readPointer();
console.log(` getSignalHandler addr: ${getSignalHandler}`);
const unloadLibrary = NativeBridgeItf.add(0x40).readPointer();
console.log(` unloadLibrary addr: ${unloadLibrary}`);
const getError = NativeBridgeItf.add(0x48).readPointer();
console.log(` getError addr: ${getError}`);
const isPathSupported = NativeBridgeItf.add(0x50).readPointer();
console.log(` isPathSupported addr: ${isPathSupported}`);
const unused_initAnonymousNamespace = NativeBridgeItf.add(0x58).readPointer();
console.log(` unused_initAnonymousNamespace addr: ${unused_initAnonymousNamespace}`);
const createNamespace = NativeBridgeItf.add(0x60).readPointer();
console.log(` createNamespace addr: ${createNamespace}`);
const linkNamespaces = NativeBridgeItf.add(0x68).readPointer();
console.log(` linkNamespaces addr: ${linkNamespaces}`);
const loadLibraryExt = NativeBridgeItf.add(0x70).readPointer();
console.log(` loadLibraryExt addr: ${loadLibraryExt}`);
const getVendorNamespace = NativeBridgeItf.add(0x78).readPointer();
console.log(` getVendorNamespace addr: ${getVendorNamespace}`);
const getExportedNamespace = NativeBridgeItf.add(0x80).readPointer();
console.log(` getExportedNamespace addr: ${getExportedNamespace}
`);
const preZygoteFork = NativeBridgeItf.add(0x88).readPointer();
console.log(` preZygoteFork addr: ${preZygoteFork}`);
Interceptor.attach(loadLibraryExt, {
onEnter: function(args) {
// 根据实际函数原型判断参数,这里假设第一个参数为要加载的库路径
var libName = Memory.readCString(args[0]);
console.log(" loadLibraryExt called with library name: " + libName);
},
onLeave: function(retval) {
console.log(" loadLibraryExt returned: " + retval);
}
});
});
HOOK loadLibraryExt
有了地址去直接HOOK,利用HOOK loadLibraryExt去实现得到在模拟器中加载过的so文件
这里终于是得到了最终的结果了,也是类比于真机HOOK dlopen一样了,这里去HOOK 了loadLibraryExt得到了和手机端一样的结果,这里的最终的libmsaoaidsec.so,就是对于加载过的so文件的输出结果了
绕过frida检测
【新提醒】bilibili XHS frida检测分析绕过 - 吾爱破解 - 52pojie.cn
这里选择的APP是 哔哩哔哩的7.76.0,其中有一个原因就是我之前写过一篇关于这个版本的frida检测绕过,这里我已经和手机端一样的能够去定位frida检测的so文件了。
-
frida的patch点是在
JNI_Onload之前,init之后
的。
-
具体的patch点是在
__system_property_get("ro.build.version.sdk")
的时机
-
HOOK的
pthread_create函数
,发现在libmsaoaidsec.so里面开启了
三个线程
-
绕过的操作就是直接
patch了这三个线程
绕过的
复制代码 隐藏代码
Java.perform(() => {
console.log("\n====== 环境信息 ======");
console.log(` 进程架构: ${Process.arch}`);
console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);
const libhoudini = Module.findBaseAddress("libhoudini.so");
if (!libhoudini) {
console.error("[!] libhoudini.so 未加载");
return;
}
console.log(` libhoudini.so 基址: ${libhoudini}`);
constNativeBridgeItf = Module.findExportByName("libhoudini.so", "NativeBridgeItf");
if (!NativeBridgeItf) {
console.error("[!] 找不到 NativeBridgeItf 符号");
return;
}
console.log(` NativeBridgeItf 符号地址: ${NativeBridgeItf}`);
// 遍历结构体指针
console.log("\n====== 结构体指针信息 ======");
const offsets = {
version: 0x0, padding: 0x4, initialize: 0x8, loadLibrary:
0x10,
getTrampoline: 0x18, isSupported: 0x20, getAppEnv: 0x28,
isCompatibleWith: 0x30, getSignalHandler: 0x38, unloadLibrary: 0x40,
getError: 0x48, isPathSupported: 0x50, unused_initAnonymousNamespace: 0x58,
createNamespace: 0x60, linkNamespaces: 0x68, loadLibraryExt: 0x70,
getVendorNamespace: 0x78, getExportedNamespace: 0x80, preZygoteFork: 0x88
};
Object.keys(offsets).forEach(name => {
console.log(` ${name} addr: ${NativeBridgeItf.add(offsets[name]).readPointer()}`);
});
// Hook loadLibraryExt
Interceptor.attach(NativeBridgeItf.add(offsets.loadLibraryExt).readPointer(), {
onEnter: function(args) {
var libName = Memory.readCString(args[0]);
console.log(` loadLibraryExt called with: ${libName}`);
if (libName.includes("libmsaoaidsec.so")) {
console.log("hooking libmsaoaidsec.so");
hook_system_property_get();
}
},
onLeave: function(retval) {
console.log(` loadLibraryExt returned: ${retval}`);
}
});
});
functionhook_system_property_get() {
var addr = Module.findExportByName(null, "__system_property_get");
if (!addr) {
console.log("__system_property_get not found");
return;
}
console.log("hooking __system_property_get");
Interceptor.attach(addr, {
onEnter: function(args) {
var name = ptr(args[0]).readCString();
if (name.includes("ro.build.version.sdk")) {
console.log("Found ro.build.version.sdk, patching...");
setTimeout(hook_pthread_create, 100);
}
}
});
}
functioncall_function(){
console.log("alearly patch frida");
}
functionhook_pthread_create() {
var pthread_create = Module.findExportByName("libc.so", "pthread_create");
if (!pthread_create) {
console.log("pthread_create not found");
return;
}
var libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
if (!libmsaoaidsec) {
console.log("libmsaoaidsec.so not found"
);
return;
}
console.log(`libmsaoaidsec.so base: ${libmsaoaidsec.base}`);
Interceptor.attach(pthread_create, {
onEnter: function(args) {
var thread_ptr = args[2];
if (thread_ptr.compare(libmsaoaidsec.base) 0 ||
thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {
console.log(`pthread_create other thread: ${thread_ptr}`);
} else {
console.log(`pthread_create libmsaoaidsec.so thread: ${thread_ptr}, offset: ${thread_ptr.sub(libmsaoaidsec.base)}`);
[0x1c544, 0x1b8d4, 0x26e5c].forEach(offset => {
Interceptor.replace(libmsaoaidsec.base.add(offset),
newNativeCallback(() =>console.log(`Interceptor.replace: 0x${offset.toString(16)}`), "void", [])
);
});
}
}
});
}
这里绕过了frida检测,调用了
call_function
函数,打印了一串字符串
总结:
这里只是对于在模拟器上如何实现跨架构进行so加载的探究流程,由于时间有点短,其中很多的细节没有细致的研究,只是对于整个模拟器so加载流程做了一个小小的判断。
我类比于hook dlopen的方法想去HOOK NativeBridgeLoadLibrary 想去得到对应的frida检测的so文件,所以去还原了NativeBridgeItf这个结构体,也是最终去得到了loadLibraryExt函数地址,进行了HOOK loadLibraryExt,也是得到了和之前手机端一样的结果了
之后也是复现了自己在手机端的绕过frida检测,也是成功绕过了
但是确实有了一些对于android虚拟机的理解,比如这种跨架构的过程,好比逆向过程中把每一个指令进行拦截然后用对应的架构语言去解释,然后实现在自己的架构里面进行虚拟加载,确实对于安卓虚拟机有了一些认识。
-官方论坛
www.52pojie.cn
👆👆👆
公众号
设置“星标”,
您
不会错过
新的消息通知