libmsaoaidsec.so检测监测——绕过Hook脚本"

admin 2026-10-21 18:38:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

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 原创,转载请注明来自看雪社区

第十届安全开发者峰会【议题征集】-欢迎投稿

往期推荐

ret2dlresolve分析

ELF GOT Hook 实战

面向复现的逆向工程实践:Hermes 在设备刷写、提权与 Frida 魔改中的自动化能力验证

把 .o 变成 .ko:GKI 安全特性的铁幕

实战APP全流程分析(检测绕过/登录分析/视频解锁/native加密/广告绕过)

球分享

球点赞

球在看

点击阅读原文查看更多


【安全圈】新的JanuscapeLinux漏洞允许在Intel和AMD设备上实现虚拟机逃逸" 网络安全文章

【安全圈】新的JanuscapeLinux漏洞允许在Intel和AMD设备上实现虚拟机逃逸"

公开资料显示,一个名为Januscape的16年历史的Linux内核漏洞(CVE-2026-53359)允许攻击者从虚拟机逃逸并在宿主机上执行任意代码。该漏洞源于KVM/x86中shadowMMU的use-after-free缺陷,影响Intel和AMD架构,对多租户公共云环境构成显著风险。管理员应确认补丁81ccda30b4e8已应用到宿主机内核以防范攻击。
libmsaoaidsec.so检测监测——绕过Hook脚本" 网络安全文章

libmsaoaidsec.so检测监测——绕过Hook脚本"

本文分析了libmsaoaidsec.so在AndroidNative安全分析中的早期检测机制,指出其检测逻辑在.init等阶段执行,导致在android_dlopen_ext的onLeave阶段Hook已过时。提出在onEnter阶段通过Hook\\_\\_system_property_get作为早期锚点,基于函数偏移和返回值判断检测逻辑并实施绕过策略,如返回值替换或写入RET指令。提供了Frida脚本设计思路,包括监控SO加载、捕获初始化锚点、动态观测检测分支及绕过关键函数。
第1课:专栏导论与安全边界" 网络安全文章

第1课:专栏导论与安全边界"

本课为安全工程专栏导论,明确学习边界:仅用于防御研究,禁止攻击滥用。核心是阅读远控源码,将其转化为企业可执行的检测、审计与加固方案。强调授权隔离环境,区分合法运维工具与恶意样本。后续课程将围绕源码审计、风险识别、检测工程和应急响应展开,每课输出检测点与加固建议。
近四成Skill\"带病上岗\" 360发布报告敲响AI智能体安全警钟" 网络安全文章

近四成Skill\"带病上岗\" 360发布报告敲响AI智能体安全警钟"

360发布《AISkill生态安全报告(2026上半年)》,检测近7万个公开Skill样本发现近四成存在安全缺陷,包括权限超配、明文密钥泄露及链式攻击风险。报告指出生态扩张速度远超安全建设,传统静态扫描无法应对新型威胁,提出先检测再准入后运行的全生命周期安全治理思路,并为企业、开发者、平台提供分层落地指南。
评论:0   参与:  0