libmsaoaidsec.so 检测监测——绕过 Hook 脚本
xiusi
xiusi
看雪学苑
2026年7月8日 17:59
上海
在小说阅读器读本章
去阅读
一、问题背景
在 Android Native 安全分析中,很多安全组件并不会等到 JNI_OnLoad 后才启动检测,而是会将部分关键逻辑放在.init \\、\\ .init\_proc 或 .init\_array 等更早的初始化阶段。libmsaoaidsec.so 的检测逻辑就存在这类特征:如果在 android_dlopen_ext 的 onLeave 阶段再安装 Hook,库的初始化逻辑通常已经执行完毕,部分反调试、注入检测或完整性校验可能已经触发,最终表现为 App 闪退、退出或关键流程异常。
二、Android SO 加载流程与 Hook 时机
Android 加载 Native SO 时,通常会经过以下流程:
需要注意的是,android\_dlopen\_ext 的 onLeave 并不等价于“SO 刚刚映射完成” 。当 onLeave 被触发时,加载器通常已经执行完 .init 和 .init\_array 中的初始化函数。如果目标库把检测逻辑放在这些早期阶段,那么在 onLeave 再安装 Hook 往往已经太晚。
更合适的思路是:在 android\_dlopen\_ext 的 onEnter 中识别目标 SO 加载事件,然后寻找 .init\_proc 内部调用的外部导入函数作为二级锚点。这样既可以保证目标 SO 已经进入加载流程,又能在其初始化逻辑执行过程中插入 Hook。
三、早期 Hook 锚点定位
对 libmsaoaidsec.so 的 init_proc 进行分析,可以看到其早期逻辑中调用了 sub_123F0:
__int64 __fastcall init_proc(__int64 a1)
{
...
StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
v15 = *(StatusReg + 40);
s_1 = &StatusReg - 250;
*off_47FB8 = sub_123F0(a1);
...
}
继续查看 sub_123F0,可以发现其调用了 __system_property_get 读取系统属性:
__int64 sub_123F0()
{
_QWORD nptr[2];
nptr[1] = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
nptr[0] = 0LL;
__system_property_get(\"ro.build.version.sdk\", nptr);
return atoi(nptr);
}
对应导入表中也可以看到 __system_property_get:
extern:00000000000A76D8 IMPORT __imp___system_property_get
extern:00000000000A76D8 ; CODE XREF: __system_property_get+C
extern:00000000000A76D8 ; DATA XREF: LOAD:__system_property_get_0
由此可以确定一个较理想的早期 Hook 链路:
- Hook
android_dlopen_ext。 - 在
onEnter中判断当前加载路径是否包含libmsaoaidsec.so。 - 命中目标库后,立即 Hook
__system_property_get。 - 当
__system_property_get(\"ro.build.version.sdk\", ...)被调用时,说明执行已经进入目标库初始化流程。 - 此时通过
Process.findModuleByName(\"libmsaoaidsec.so\")获取模块基址。 - 基于 模块基址 + 函数偏移 安装检测监测 Hook 和绕过 Patch。
这个锚点的优势在于: \_\_system\_property\_get 是 .init\_proc 早期调用的外部导入函数,触发时机早于大多数检测逻辑,且比盲目轮询模块加载更稳定。
四、检测点与绕过策略总览
脚本中主要通过函数返回值判断不同检测逻辑是否启用,并对部分关键分支进行返回值替换或直接写入 RET 指令。
4.1 函数偏移与检测含义
4.2 复合判断条件
部分检测逻辑不是由单个函数返回值决定,而是由多个函数组合判断。
Frida / 注入痕迹检测:
const shouldCheck = lastSubCAA8Ret === 248 && !lastSub12D9CRet;
含义如下:
- sub\_CAA8() 返回 248
- sub\_12D9C() 返回假值
- 两个条件同时满足时,进入 Frida / 注入痕迹检测
轮询代码 CRC 校验:
const shouldCheck = lastSubC830Ret !==1 || !!lastSub95C8Ret;
含义如下:
- sub\_C830() 不等于 1,或者
- sub\_95C8() 返回真值
- 任一条件满足时,进入 轮询代码 CRC 校验
这类复合条件非常适合通过日志进行动态观察,因为它不仅能显示某个函数是否被调用,还能帮助判断当前运行环境下到底触发了哪一类检测。
五、核心 Frida Hook 设计
整体脚本可以拆分为四个部分:
- 目标库加载监控
Hook android_dlopen_ext,捕获 libmsaoaidsec.so 加载事件。
* 早期初始化锚点
Hook __system_property_get,等待 ro.build.version.sdk 被读取。
* 检测函数监测
基于 base + offset Hook 关键函数,记录返回值并输出检测状态。
* 绕过逻辑安装
通过返回值替换和写入 RET 指令跳过特定检测函数。
5.1 监控目标 SO 加载
function hookAndroidDlopenExt() {
const androidDlopenExt = Module.findExportByName(null, 'android_dlopen_ext');
if (androidDlopenExt === null) {
logError('android_dlopen_ext not found');
return;
}
Interceptor.attach(androidDlopenExt, {
onEnter(args) {
const pathPtr = args[0];
if (pathPtr.isNull()) {
return;
}
let path = null;
try {
path = pathPtr.readCString();
} catch (e) {
return;
}
if (path.indexOf(TARGET_SO) === -1) {
return;
}
logInfo('detected loading of ' + path);
locateInitAnchor();
}
});
logInfo('hooked android_dlopen_ext at ' + androidDlopenExt);
}
这里选择 onEnter 而不是 onLeave,关键原因在于:目标库的初始化函数会在 android_dlopen_ext返回前执行。如果等待 onLeave,很多早期检测已经错过。
5.2 借助 __system_property_get 捕获初始化阶段
function locateInitAnchor() {
if (propHooked) {
return;
}
const sysPropGet = Module.findExportByName(null, '__system_property_get');
if (sysPropGet === null) {
logError('__system_property_get not found');
return;
}
Interceptor.attach(sysPropGet, {
onEnter(args) {
const namePtr = args[0];
if (namePtr.isNull()) {
return;
}
let name = null;
try {
name = namePtr.readCString();
} catch (e) {
return;
}
if (name === null || name.indexOf(INIT_ANCHOR_PROPERTY) === -1) {
return;
}
if (retPatched) {
return;
}
logInfo('__system_property_get(\"' + name + '\", ' + args[1] + ') hit');
const module = Process.findModuleByName(TARGET_SO);
if (module === null) {
logError(TARGET_SO + ' not found by Frida module query');
return;
}
logInfo('found ' + TARGET_SO + ' base = ' + module.base + ', size = 0x' + module.size.toString(16));
installTargetHooks(module.base);
}
});
propHooked = true;
logInfo('hooked __system_property_get at ' + sysPropGet);
}
这段逻辑的核心是过滤属性名:
const INIT_ANCHOR_PROPERTY = 'ro.build.version.sdk';
只有当目标库读取该属性时,才认为命中了早期初始化锚点。随后再获取 libmsaoaidsec.so 的基址并安装 Hook,避免对其他模块调用 __system_property_get 的行为造成干扰。
5.3 基于返回值判断检测是否启用
以 sub_CC64 为例,该函数返回 218 时,可以判断 Magisk / root 检测相关逻辑被启用:
function hookSubCC64(base) {
if (subCC64Hooked) {
return;
}
const subAddr = base.add(SUB_CC64_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_CC64 entered at ' + subAddr);
},
onLeave(retval) {
const ret = retval.toInt32();
log('sub_CC64 retval int = ' + ret + ', equals 218 => ' + (ret === 218 ? 'YES' : 'NO'));
logDecision(ret === 218, ret === 218 ? MSG_MAGISK_ROOT_CHECK : MSG_NO_MAGISK_ROOT_CHECK);
}
});
subCC64Hooked = true;
logInfo('hooked sub_CC64 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_CC64_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_CC64 at ' + subAddr + ': ' + e);
}
}
这种写法适合做动态观测:每个检测函数在进入和返回时都会打印日志,最终通过 [DETECT] 或 [SKIP] 输出当前检测分支是否激活。
5.4 通过返回值替换绕过检测
对于部分校验函数,脚本直接在 onLeave 阶段替换返回值。
例如 sub_16720:
function hookSub16720Return666(base) {
if (sub16720Hooked) {
return;
}
const subAddr = base.add(SUB_16720_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
this.checkOff = args[0];
this.checkLen = args[1].toInt32();
log('sub_16720 entered at ' + subAddr + ', check_off = ' + this.checkOff + ', check_len = ' + this.checkLen);
},
onLeave(retval) {
const original = retval.toInt32();
retval.replace(ptr(666));
log('sub_16720 retval original = ' + original + ', hook result = 666, check_off = ' + this.checkOff + ', check_len = ' + this.checkLen);
}
});
sub16720Hooked = true;
logInfo('hooked sub_16720 return patch at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_16720_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_16720 at ' + subAddr + ': ' + e);
}
}
这里的重点不是固定值 666 本身,而是 通过修改返回值改变上层分支判断结果。
5.5 对线程创建或轮询检测入口写入 RET
脚本还对若干偏移直接写入 ARM64 RET 指令,使相关函数立即返回:
const RET_PATCH_OFFSETS = [
0x8CAC,
0x19E0C,
0x1C544,
0x1B8D4,
0x26E5C
];
对应 Patch 实现如下:
function ret_64(addr) {
try {
Memory.protect(addr, 4, 'rwx');
const writer = new Arm64Writer(addr);
writer.putRet();
writer.flush();
writer.dispose();
logPatch('patched RET at ' + addr);
return true;
} catch (e) {
logError('failed to patch RET at ' + addr + ': ' + e);
return false;
}
}
function patchRetChecks(base) {
if (retPatched) {
return;
}
logPatch('patching RET offsets, base = ' + base);
for (let i = 0; i < RET_PATCH_OFFSETS.length; i++) {
const offset = RET_PATCH_OFFSETS[i];
const addr = base.add(offset);
logPatch('patching RET at offset 0x' + offset.toString(16) + ', addr = ' + addr);
ret_64(addr);
}
retPatched = true;
}
这种方式适用于已经确认目标函数主要用于检测、线程创建或轮询校验的场景。
六、完整 Hook 脚本
下面是整理后的完整 Frida 脚本。脚本目标为 ARM64 环境下的 libmsaoaidsec.so,通过早期锚点安装 Hook,并输出各类检测分支状态。
'use strict';
const TARGET_SO = 'libmsaoaidsec.so';
const INIT_ANCHOR_PROPERTY = 'ro.build.version.sdk';
const SUB_C830_OFFSET = 0xC830;
const SUB_95C8_OFFSET = 0x95C8;
const SUB_CA28_OFFSET = 0xCA28;
const SUB_CAA8_OFFSET = 0xCAA8;
const SUB_CAE8_OFFSET = 0xCAE8;
const SUB_CC64_OFFSET = 0xCC64;
const SUB_CEA4_OFFSET = 0xCEA4;
const SUB_CEE4_OFFSET = 0xCEE4;
const SUB_12D9C_OFFSET = 0x12D9C;
const SUB_16720_OFFSET = 0x16720;
const SUB_25A48_OFFSET = 0x25A48;
const MSG_FRIDA_CHECK = '进行 Frida / 注入痕迹检测';
const MSG_NO_FRIDA_CHECK = '不进行 Frida / 注入痕迹检测';
const MSG_ANTI_DEBUG_CHECK = '进行反调试、TracerPid、线程状态检测';
const MSG_NO_ANTI_DEBUG_CHECK = '不进行反调试、TracerPid、线程状态检测';
const MSG_ADB_CHECK = '进行 ADB / USB 调试环境检测';
const MSG_NO_ADB_CHECK = '不进行 ADB / USB 调试环境检测';
const MSG_CRC32_CHECK = '进行目标区域 CRC32 计算,并与预期 CRC32 值对比';
const MSG_NO_CRC32_CHECK = '不进行目标区域 CRC32 计算';
const MSG_POLLING_CRC_CHECK = '进行轮询代码 CRC 校验';
const MSG_NO_POLLING_CRC_CHECK = '不进行轮询代码 CRC 校验';
const MSG_MAGISK_ROOT_CHECK = '进行 Magisk / root 检测';
const MSG_NO_MAGISK_ROOT_CHECK = '不进行 Magisk / root 检测';
const MSG_NATIVE_ENV_CHECK = '进行 Native 层环境检测';
const MSG_NO_NATIVE_ENV_CHECK = '不进行 Native 层环境检测';
const MSG_CODE_CRC_APK_V1_CHECK = '进行代码 CRC / APK v1 签名校验';
const RET_PATCH_OFFSETS = [
0x8CAC,
0x19E0C,
0x1C544,
0x1B8D4,
0x26E5C
];
let propHooked = false;
let retPatched = false;
let subC830Hooked = false;
let sub95C8Hooked = false;
let subCA28Hooked = false;
let subCAA8Hooked = false;
let subCAE8Hooked = false;
let subCC64Hooked = false;
let subCEA4Hooked = false;
let subCEE4Hooked = false;
let sub12D9CHooked = false;
let sub16720Hooked = false;
let sub25A48Hooked = false;
let lastSubCAA8Ret = null;
let lastSub12D9CRet = null;
let lastSubC830Ret = null;
let lastSub95C8Ret = null;
const COLOR = {
reset: '\\x1b[0m',
dim: '\\x1b[2m',
red: '\\x1b[31m',
green: '\\x1b[32m',
yellow: '\\x1b[33m',
cyan: '\\x1b[36m',
bold: '\\x1b[1m'
};
function colorize(color, message) {
return color + message + COLOR.reset;
}
function log(message) {
console.log('[hook] ' + message);
}
function logInfo(message) {
console.log(colorize(COLOR.cyan, '[hook] ' + message));
}
function logDecision(active, message) {
const color = active ? COLOR.bold + COLOR.yellow : COLOR.dim;
const prefix = active ? '[DETECT] ' : '[SKIP] ';
console.log(colorize(color, prefix + message));
}
function logPatch(message) {
console.log(colorize(COLOR.green, '[patch] ' + message));
}
function logError(message) {
console.error(colorize(COLOR.red, '[error] ' + message));
}
function ret_64(addr) {
try {
Memory.protect(addr, 4, 'rwx');
const writer = new Arm64Writer(addr);
writer.putRet();
writer.flush();
writer.dispose();
logPatch('patched RET at ' + addr);
return true;
} catch (e) {
logError('failed to patch RET at ' + addr + ': ' + e);
return false;
}
}
function patchRetChecks(base) {
if (retPatched) {
return;
}
logPatch('patching RET offsets, base = ' + base);
for (let i = 0; i < RET_PATCH_OFFSETS.length; i++) {
const offset = RET_PATCH_OFFSETS[i];
const addr = base.add(offset);
logPatch('patching RET at offset 0x' + offset.toString(16) + ', addr = ' + addr);
ret_64(addr);
}
retPatched = true;
}
function logFridaTraceDecision(tag) {
if (lastSubCAA8Ret === null || lastSub12D9CRet === null) {
return;
}
const shouldCheck = lastSubCAA8Ret === 248 && !lastSub12D9CRet;
log(tag + ': sub_CAA8() = ' + lastSubCAA8Ret + ', sub_12D9C() = ' + lastSub12D9CRet + ', condition (sub_CAA8() == 248 && !sub_12D9C()) => ' + shouldCheck);
logDecision(shouldCheck, shouldCheck ? MSG_FRIDA_CHECK : MSG_NO_FRIDA_CHECK);
}
function logPollingCrcDecision(tag) {
if (lastSubC830Ret === null || lastSub95C8Ret === null) {
return;
}
const shouldCheck = lastSubC830Ret !== 1 || !!lastSub95C8Ret;
log(tag + ': sub_C830() = ' + lastSubC830Ret + ', sub_95C8() = ' + lastSub95C8Ret + ', condition (sub_C830()!=1 || sub_95C8()) => ' + shouldCheck);
logDecision(shouldCheck, shouldCheck ? MSG_POLLING_CRC_CHECK : MSG_NO_POLLING_CRC_CHECK);
}
function hookSubC830(base) {
if (subC830Hooked) {
return;
}
const subAddr = base.add(SUB_C830_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_C830 entered at ' + subAddr);
},
onLeave(retval) {
lastSubC830Ret = retval.toInt32();
log('sub_C830 retval int = ' + lastSubC830Ret + ', equals 1 => ' + (lastSubC830Ret === 1 ? 'YES' : 'NO'));
logPollingCrcDecision('sub_C830.onLeave');
}
});
subC830Hooked = true;
logInfo('hooked sub_C830 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_C830_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_C830 at ' + subAddr + ': ' + e);
}
}
function hookSub95C8(base) {
if (sub95C8Hooked) {
return;
}
const subAddr = base.add(SUB_95C8_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_95C8 entered at ' + subAddr);
},
onLeave(retval) {
lastSub95C8Ret = retval.toInt32();
log('sub_95C8 retval int = ' + lastSub95C8Ret + ', bool = ' + (!!lastSub95C8Ret));
logPollingCrcDecision('sub_95C8.onLeave');
}
});
sub95C8Hooked = true;
logInfo('hooked sub_95C8 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_95C8_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_95C8 at ' + subAddr + ': ' + e);
}
}
function hookSubCA28(base) {
if (subCA28Hooked) {
return;
}
const subAddr = base.add(SUB_CA28_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_CA28 entered at ' + subAddr);
},
onLeave(retval) {
const ret = retval.toInt32();
log('sub_CA28 retval int = ' + ret + ', equals 167 => ' + (ret === 167 ? 'YES' : 'NO'));
logDecision(ret === 167, ret === 167 ? MSG_ADB_CHECK : MSG_NO_ADB_CHECK);
}
});
subCA28Hooked = true;
logInfo('hooked sub_CA28 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_CA28_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_CA28 at ' + subAddr + ': ' + e);
}
}
function hookSubCAA8(base) {
if (subCAA8Hooked) {
return;
}
const subAddr = base.add(SUB_CAA8_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_CAA8 entered at ' + subAddr);
},
onLeave(retval) {
lastSubCAA8Ret = retval.toInt32();
log('sub_CAA8 retval int = ' + lastSubCAA8Ret + ', equals 248 => ' + (lastSubCAA8Ret === 248 ? 'YES' : 'NO'));
logFridaTraceDecision('sub_CAA8.onLeave');
}
});
subCAA8Hooked = true;
logInfo('hooked sub_CAA8 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_CAA8_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_CAA8 at ' + subAddr + ': ' + e);
}
}
function hookSubCAE8(base) {
if (subCAE8Hooked) {
return;
}
const subAddr = base.add(SUB_CAE8_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_CAE8 entered at ' + subAddr);
},
onLeave(retval) {
const ret = retval.toInt32();
log('sub_CAE8 retval int = ' + ret + ', equals 249 => ' + (ret === 249 ? 'YES' : 'NO'));
logDecision(ret === 249, ret === 249 ? MSG_ANTI_DEBUG_CHECK : MSG_NO_ANTI_DEBUG_CHECK);
}
});
subCAE8Hooked = true;
logInfo('hooked sub_CAE8 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_CAE8_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_CAE8 at ' + subAddr + ': ' + e);
}
}
function hookSubCC64(base) {
if (subCC64Hooked) {
return;
}
const subAddr = base.add(SUB_CC64_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_CC64 entered at ' + subAddr);
},
onLeave(retval) {
const ret = retval.toInt32();
log('sub_CC64 retval int = ' + ret + ', equals 218 => ' + (ret === 218 ? 'YES' : 'NO'));
logDecision(ret === 218, ret === 218 ? MSG_MAGISK_ROOT_CHECK : MSG_NO_MAGISK_ROOT_CHECK);
}
});
subCC64Hooked = true;
logInfo('hooked sub_CC64 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_CC64_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_CC64 at ' + subAddr + ': ' + e);
}
}
function hookSubCEA4Return666(base) {
if (subCEA4Hooked) {
return;
}
const subAddr = base.add(SUB_CEA4_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_CEA4 entered at ' + subAddr);
},
onLeave(retval) {
const original = retval.toInt32();
retval.replace(ptr(666));
log('sub_CEA4 retval original = ' + original + ', hook result = 666');
}
});
subCEA4Hooked = true;
logInfo('hooked sub_CEA4 return patch at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_CEA4_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_CEA4 at ' + subAddr + ': ' + e);
}
}
function hookSubCEE4(base) {
if (subCEE4Hooked) {
return;
}
const subAddr = base.add(SUB_CEE4_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_CEE4 entered at ' + subAddr);
},
onLeave(retval) {
const ret = retval.toInt32();
log('sub_CEE4 retval int = ' + ret + ', equals 203 => ' + (ret === 203 ? 'YES' : 'NO'));
logDecision(ret === 203, ret === 203 ? MSG_CRC32_CHECK : MSG_NO_CRC32_CHECK);
}
});
subCEE4Hooked = true;
logInfo('hooked sub_CEE4 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_CEE4_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_CEE4 at ' + subAddr + ': ' + e);
}
}
function hookSub12D9C(base) {
if (sub12D9CHooked) {
return;
}
const subAddr = base.add(SUB_12D9C_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_12D9C entered at ' + subAddr);
},
onLeave(retval) {
const raw = retval.toInt32();
lastSub12D9CRet = raw !== 0;
log('sub_12D9C retval bool = ' + lastSub12D9CRet + ', raw int = ' + raw);
logFridaTraceDecision('sub_12D9C.onLeave');
}
});
sub12D9CHooked = true;
logInfo('hooked sub_12D9C at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_12D9C_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_12D9C at ' + subAddr + ': ' + e);
}
}
function hookSub25A48(base) {
if (sub25A48Hooked) {
return;
}
const subAddr = base.add(SUB_25A48_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
log('sub_25A48 entered at ' + subAddr);
},
onLeave(retval) {
const ret = retval.toInt32();
const shouldCheck = (ret & 1) === 0;
log('sub_25A48 retval int = ' + ret + ', (retval & 1) == 0 => ' + shouldCheck);
if (shouldCheck) {
logDecision(true, MSG_NATIVE_ENV_CHECK);
logDecision(true, MSG_CODE_CRC_APK_V1_CHECK);
} else {
logDecision(false, MSG_NO_NATIVE_ENV_CHECK);
}
}
});
sub25A48Hooked = true;
logInfo('hooked sub_25A48 at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_25A48_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_25A48 at ' + subAddr + ': ' + e);
}
}
function hookSub16720Return666(base) {
if (sub16720Hooked) {
return;
}
const subAddr = base.add(SUB_16720_OFFSET);
try {
Interceptor.attach(subAddr, {
onEnter(args) {
this.checkOff = args[0];
this.checkLen = args[1].toInt32();
log('sub_16720 entered at ' + subAddr + ', check_off = ' + this.checkOff + ', check_len = ' + this.checkLen);
},
onLeave(retval) {
const original = retval.toInt32();
retval.replace(ptr(666));
log('sub_16720 retval original = ' + original + ', hook result = 666, check_off = ' + this.checkOff + ', check_len = ' + this.checkLen);
}
});
sub16720Hooked = true;
logInfo('hooked sub_16720 return patch at ' + subAddr + ' (base ' + base + ' + 0x' + SUB_16720_OFFSET.toString(16) + ')');
} catch (e) {
logError('failed to hook sub_16720 at ' + subAddr + ': ' + e);
}
}
function installTargetHooks(base) {
hookSubC830(base);
hookSub95C8(base);
hookSubCA28(base);
hookSubCAA8(base);
hookSubCAE8(base);
hookSubCC64(base);
hookSubCEA4Return666(base);
hookSubCEE4(base);
hookSub12D9C(base);
hookSub16720Return666(base);
hookSub25A48(base);
patchRetChecks(base);
}
function locateInitAnchor() {
if (propHooked) {
return;
}
const sysPropGet = Module.findExportByName(null, '__system_property_get');
if (sysPropGet === null) {
logError('__system_property_get not found');
return;
}
Interceptor.attach(sysPropGet, {
onEnter(args) {
const namePtr = args[0];
if (namePtr.isNull()) {
return;
}
let name = null;
try {
name = namePtr.readCString();
} catch (e) {
return;
}
if (name === null || name.indexOf(INIT_ANCHOR_PROPERTY) === -1) {
return;
}
if (retPatched) {
return;
}
logInfo('__system_property_get(\"' + name + '\", ' + args[1] + ') hit');
const module = Process.findModuleByName(TARGET_SO);
if (module === null) {
logError(TARGET_SO + ' not found by Frida module query');
return;
}
logInfo('found ' + TARGET_SO + ' base = ' + module.base + ', size = 0x' + module.size.toString(16));
installTargetHooks(module.base);
}
});
propHooked = true;
logInfo('hooked __system_property_get at ' + sysPropGet);
}
function hookAndroidDlopenExt() {
const androidDlopenExt = Module.findExportByName(null, 'android_dlopen_ext');
if (androidDlopenExt === null) {
logError('android_dlopen_ext not found');
return;
}
Interceptor.attach(androidDlopenExt, {
onEnter(args) {
const pathPtr = args[0];
if (pathPtr.isNull()) {
return;
}
let path = null;
try {
path = pathPtr.readCString();
} catch (e) {
return;
}
if (path.indexOf(TARGET_SO) === -1) {
return;
}
logInfo('detected loading of ' + path);
locateInitAnchor();
}
});
logInfo('hooked android_dlopen_ext at ' + androidDlopenExt);
}
setImmediate(function () {
logInfo('script loaded');
hookAndroidDlopenExt();
});
七、运行方式与效果验证
使用 Frida spawn 模式启动目标 App,并加载 Hook 脚本:
frida -U -f com.xingin.xhs -l hook.js
示例输出如下:
Spawning `com.xingin.xhs`...
[hook] script loaded
[hook] hooked android_dlopen_ext at 0x7459d37100
Spawned `com.xingin.xhs`. Resuming main thread!
[hook] detected loading of /data/app/.../lib/arm64/libmsaoaidsec.so
[hook] hooked __system_property_get at 0x744f7475f0
[hook] __system_property_get(\"ro.build.version.sdk\", 0x7309710790) hit
[hook] found libmsaoaidsec.so base = 0x72cd001000, size = 0xaa000
[hook] hooked sub_C830 at 0x72cd00d830 (base 0x72cd001000 + 0xc830)
[hook] hooked sub_95C8 at 0x72cd00a5c8 (base 0x72cd001000 + 0x95c8)
[hook] hooked sub_CA28 at 0x72cd00da28 (base 0x72cd001000 + 0xca28)
[hook] hooked sub_CAA8 at 0x72cd00daa8 (base 0x72cd001000 + 0xcaa8)
[hook] hooked sub_CAE8 at 0x72cd00dae8 (base 0x72cd001000 + 0xcae8)
[hook] hooked sub_CC64 at 0x72cd00dc64 (base 0x72cd001000 + 0xcc64)
[hook] hooked sub_CEA4 return patch at 0x72cd00dea4 (base 0x72cd001000 + 0xcea4)
[hook] hooked sub_CEE4 at 0x72cd00dee4 (base 0x72cd001000 + 0xcee4)
[hook] hooked sub_12D9C at 0x72cd013d9c (base 0x72cd001000 + 0x12d9c)
[hook] hooked sub_16720 return patch at 0x72cd017720 (base 0x72cd001000 + 0x16720)
[hook] hooked sub_25A48 at 0x72cd026a48 (base 0x72cd001000 + 0x25a48)
继续观察 Patch 结果:
[patch] patching RET offsets, base = 0x72cd001000
[patch] patching RET at offset 0x8cac, addr = 0x72cd009cac
[patch] patched RET at 0x72cd009cac
[patch] patching RET at offset 0x19e0c, addr = 0x72cd01ae0c
[patch] patched RET at 0x72cd01ae0c
[patch] patching RET at offset 0x1c544, addr = 0x72cd01d544
[patch] patched RET at 0x72cd01d544
[patch] patching RET at offset 0x1b8d4, addr = 0x72cd01c8d4
[patch] patched RET at 0x72cd01c8d4
[patch] patching RET at offset 0x26e5c, addr = 0x72cd027e5c
[patch] patched RET at 0x72cd027e5c
检测分支输出示例:
[hook] sub_CAE8 retval int = 249, equals 249 => YES
[DETECT] 进行反调试、TracerPid、线程状态检测
[hook] sub_CA28 retval int = 0, equals 167 => NO
[SKIP] 不进行 ADB / USB 调试环境检测
[hook] sub_CEE4 retval int = 0, equals 203 => NO
[SKIP] 不进行目标区域 CRC32 计算
[hook] sub_95C8.onLeave: sub_C830() = 1, sub_95C8() = 0, condition (sub_C830()!=1 || sub_95C8()) => false
[SKIP] 不进行轮询代码 CRC 校验
从输出可以确认:
- libmsaoaidsec.so 的加载事件被成功捕获。
- \_\_system\_property\_get(\"ro.build.version.sdk\", ...) 早期锚点命中。
- 目标 SO 基址被正确获取。
- 多个 base + offset Hook 成功安装。
- 线程或轮询相关入口已经被写入 RET。
- 日志能够清晰区分 [DETECT] 与 [SKIP] 分支,便于后续分析。
八、总结
本文围绕 libmsaoaidsec.so 的早期初始化检测,整理了一套可复用的 Frida 分析思路:
- Hook android\_dlopen\_ext.onEnter 捕获目标 SO 加载事件。
- 利用 .init\proc 中的外部导入函数 \\_system\_property\_get 作为早期锚点。
- 在读取 ro.build.version.sdk 时获取目标 SO 基址。
- 基于 base + offset 监测关键检测函数返回值。
- 结合返回值替换与 ARM64 RET Patch 绕过特定检测入口。
这类分析的关键不在于某一个固定偏移,而在于方法论:先解决 Hook 时机,再定位初始化锚点,最后围绕检测函数建立可观测、可验证、可迁移的动态分析链路。
看雪ID:xiusi
https://bbs.kanxue.com/user-home-1036360.htm
\*本文为看雪论坛优秀文章,由 xiusi 原创,转载请注明来自看雪社区
第十届安全开发者峰会【议题征集】-欢迎投稿
往期推荐
面向复现的逆向工程实践:Hermes 在设备刷写、提权与 Frida 魔改中的自动化能力验证
实战APP全流程分析(检测绕过/登录分析/视频解锁/native加密/广告绕过)
球分享
球点赞
球在看
点击阅读原文查看更多
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论