libmsaoaidsec.so 是一个 Android Native 层共享库,文件格式为 ELF64 AArch64。从初始化入口、线程创建逻辑和大量 /proc、系统属性、APK/ZIP 访问行为来看,该库主要承担运行环境检测、反调试、反注入、签名校验和代码完整性校验等职责。本文以 IDA 静态分析结果为基础,将该库的检测逻辑整理为三条主线: 整体检测面可以概括为:该库不是单一反调试函数,而是一组组合式检测。几个明显特征如下。字符串解密的核心模式可以抽象为。不同函数的局部 key 表达可能存在差异,但整体都是“密文栈上构造 -> 循环 XOR -> 运行时得到明文”的模式:解密出的关键字符串包括 /proc/self/task、/proc/self/maps、gum-js-loop、gmain、frida-agent、sys.usb.config、META-INF/CERT.RSA、META-INF/ANDROID.RSA 等。这些字符串基本覆盖了后文的检测逻辑。在进入线程调度、APK 签名校验和代码 CRC 之前,.init_proc 会先调用 sub_25A48()。该函数受全局开关 dword_48850 控制,当 sub_CC64() 返回 218 时才进入 sub_23B18():.init_proc 对返回值的使用方式比较关键:也就是说,sub_23B18() 返回 1 时并不是直接 exit_group(0),而是让 .init_proc 跳过后续 native 检测初始化。这一点决定了它更像“反检测检测/风险打标/降级入口”,而不是最终风控拦截点。sub_23B18() 运行时解密出三个关键字符串:函数语义可以概括为:它没有直接调用 getenv("MAGISKTMP"),而是通过 /proc/self/maps 找到当前栈映射,再扫描栈顶附近的原始字符串区域。Android/Linux 进程启动时,argv、envp、auxv 等数据会放在初始栈上。即使后续通过 unsetenv() 或修改 environ 隐藏环境变量,原始字符串仍可能残留在栈映射中。从常规防护直觉看,检测到 Magisk/root 痕迹后应该更严格。但这里的代码行为是:MAGISKTMP 命中后 sub_23B18() 返回 1,再由 .init_proc 跳过 sub_1BEC4、sub_13728、sub_23AD4、sub_9150 等后续初始化。更合理的解释是:这不是最终风控决策,而是“反检测检测”或高风险环境标记。在 Magisk/Zygisk 环境下,maps、linker、ART、线程和属性读取结果都可能被改写,继续启动大量本地检测线程既容易暴露检测逻辑,也可能产生不稳定结果。因此样本选择在本地 native 层短路后续检测,把这个高风险状态交给其他链路处理,例如 Java 层、服务端风控、功能降级或静默打标。线程调度主线从 .init_proc 开始。初始化函数会读取 /proc/<pid>/cmdline,只有当进程名不包含 : 时才进入 sub_1BEC4()。在 Android 应用中,com.xxx:push、com.xxx:remote 这类进程名通常表示子进程或带后缀进程。该库选择在主进程中创建主要检测线程,避免在多进程场景下重复执行重检测逻辑。部分恢复伪代码:sub_1BEC4() 的语义很简洁:这个全局 pid 后续会被 sub_1B8D4 使用,尤其是在 sub_1AB54() 中用于判断 TracerPid 的父进程是否为主进程。换句话说,它不仅是记录当前进程 ID,还参与了后续的 self-debugger 白名单判断。部分恢复伪代码:sub_1B924() 是一个经过控制流扁平化处理的调度函数。剥离状态机后,它的核心工作是:关键线程创建条件如下:这种设计说明检测功能并非全部固定开启,而是受全局配置值控制。全局配置可能由静态数据、初始化逻辑或上层参数共同决定。sub_1C544() 由 sub_1CEF8() 创建。sub_1CEF8() 会解析 libart.so 中与 PrettyMethod 相关的函数入口,并将该入口地址作为参数传给 sub_1C544()。因此 sub_1C544() 不只检查进程环境,也会检查 ART 函数入口是否出现可疑 prologue。该线程的主循环可以概括为:部分恢复伪代码:sub_1BFAC() 遍历 /proc/self/task,逐个读取线程状态文件:它重点搜索线程名中是否出现:命中后会调用 exit(0) 退出进程。这里的检测思路非常直接:Frida 注入后通常会带来较稳定的线程名特征,因此扫描 /proc/self/task 可以在无需 ptrace 的情况下发现运行时注入痕迹。部分恢复伪代码:sub_1C158() 遍历 /proc/self/fd,通过 lstat 和 readlink 读取文件描述符指向的真实路径。它重点搜索:如果 fd 链接中出现该关键字,说明当前进程可能存在注入器相关文件或匿名映射痕迹,函数会直接触发退出。sub_1C26C() 的检测分为两层:第一层扫描 /proc/self/maps,筛选可疑映射路径;第二层打开这些映射文件,解析文件内容并搜索 Frida agent 特征。这样即使 agent 文件名被修改,只要文件内部仍保留 _AGENT_1.0 或 frida-agent 等特征,也可能被检测到。部分恢复伪代码:sub_1C26C() 打开 /proc/self/maps,关注可执行映射及可疑路径。重点字符串包括:这段循环中的几个辅助函数可以按下表理解:因此,该函数不仅简单搜索 maps 字符串,还会结合映射文件内容检查 agent 特征。其目标是发现放在 /data/local/tmp 等位置的注入组件,尤其是 Frida agent 一类动态加载对象。部分恢复伪代码:sub_26334(a1) 检查传入地址处的 4 字节指令。如果入口指令为以下值之一,会触发动态 exit_group(0):这类指令常见于 AArch64 上的字面量加载跳板形态,可用于识别某些 inline hook 或 trampoline。结合 sub_1CEF8() 传入的 ART 函数入口来看,该检测用于判断 libart 中关键函数是否被改写。sub_1B8D4() 是一个周期性反调试线程。默认轮询间隔约为 2000000 us,即 2 秒;如果 sub_CB28() 返回值大于等于 100,则使用该返回值作为轮询间隔。部分恢复伪代码:部分恢复伪代码:sub_1AE48() 打开:逐行搜索:返回语义如下:TracerPid 是 Linux/Android 反调试中最常见的检测点之一。调试器、部分注入器或 ptrace 型保护工具都会改变该字段。部分恢复伪代码:sub_1AB54(tracer_pid) 会继续读取:并搜索:随后将 tracer 进程的父进程 pid 与 sub_1BEC4() 保存的主进程 pid 比较:这个逻辑不是简单地禁止所有 TracerPid != 0,而是保留了 自调试
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291377.htm
[原创]libmsaoaidsec.so 检测体系分析
314 浏览
11 回复
tql
tql
学习一下
tql
有样本吗?
只有magisk? 没kernelSu那些吗?
学习一下
学习一下
daniu
那如何绕过libmsao 有系统性的文章讲解吗 ?
tql