首页
社区
课程
招聘
[原创]基础环境检测与对抗
发表于: 2026-10-1 15:32 16861

[原创]基础环境检测与对抗

2026-10-1 15:32
16861

本文记录了基础的Frida和Root的环境检测方案和绕过方案,本人学习时间实在不长,若文章有纰漏,望各位高手们找出后及时提出,我会及时编辑,学习,修改,谢谢各位

在多数教程中,都指导把Frida-server推送到/data/local/tmp中,并使用27042或者27043端口的TCP连接,应用层采取类D-BUS协议,传输RPC调用,再与frida-server建立通信,最终由frida-server完成对目标进程的注入与脚本执行。Spawn 模式下,Frida 通常先请求系统创建目标进程并使其保持暂停状态,然后完成附加和脚本加载,最后恢复进程执行,因此,它通常早于多数 Application.onCreate() 及业务检测,但无法覆盖 Zygote 阶段、链接器初始化以及部分极早期 native 初始化

我们从启动点的源代码开始分析

分析进程注入的源代码可以得知,不论是spawn还是attach都最终调用了attach,不过spawn是先挂起进程,再使用attach附加

他们最终都是通过perform_attach_to()函数实现frida-agent注入

下一步我们看看Linjector的具体实现

根据是否支持匿名内存的方式来确认agent是否要落地到磁盘上,他们最终都都调用了inject_library_fd

InjectSession.open()会通过 ptrace attach 到目标进程并保存寄存器状态。然后 session.inject() 才是真正做注入工作的

用之前 ptrace 停住时的上下文,把 PC/IP 指到 loader_base(loader 入口),把第一个参数设成 HelperLoaderContext*,栈换成刚分配的

之后的代码逻辑就是在src\linux\helpers\loader.c中,入口是frida_load(),里面通过线程启动frida_main()方法,在后者中,通过dlopen() 加载 agent,通过 dlsym() 找到 frida_agent_main,最后调用 agent 入口函数

大概看完上述注入原理,我们可以发现frida的几个特征还是比较明显的,我们以下细细说明

上述提到frida-server 通常被推送到 /data/local/tmp

java层有:

Native层有:

对frida-server进行改名
放到其他目录,比如 /data/local/tmp/.hidden/,不过这只能骗路径写死的弱检测
用 ZygiskFrida,不依赖 frida-server

frida-server 默认监听 27042(和 27043)App 直接尝试连接 127.0.0.1:27042,能连上就说明有 Frida嫌疑

java层实现:

Native层实现:

当然更严谨的要发送D-BUS认证消息,frida-server 使用 D-Bus 协议进行通信,因此会对 D-Bus 的 AUTH 握手请求做出响应,更彻底的检测会遍历 0~65535 所有端口,对每个开放端口都发 D-Bus AUTH 探测

下面是用于说明思路的简化伪代码,不代表完整的协议实现:

或者更绝一点,直接给比较点Hook了,这是因为我们知道认证消息可以包括REJECT和OK

Frida 注入后,其 frida-agent 和运行时引擎会在目标进程的内存中留下痕迹

上文也提到了关于系统是否支持memfd时的Frida注入的差别,Frida常制造匿名rwx权限段来放置frida-agent.so,目的是逃过磁盘上上的文件扫描

Android系统强制执行W^X,但Frida在安装inline hook时,需要修改目标函数指令并写入跳转代码(trampoline),这要求Frida将相关内存页权限修改为rwx,说明我们可以检查内存权限段,当然rwx段并不一定就是,许多合法库也有的

在实际的App加固方案中,检测代码会更为复杂,还有命中后自毁abort()等手段

可以Hook fopen、open、read等文件操作函数,在检测代码读取maps时,动态修改返回的内容,擦除Frida的特征

当然,最简单的方法就是直接上魔改的得了,社区一堆

Linux/Android 内核规定:​同一个进程同时只能被一个调试器(Tracer)附加

Frida 在注入阶段通过 ptrace 附加并暂停目标线程,读写其寄存器和内存,让目标线程临时执行 loader,在其创建新线程后立即返回,Frida 恢复寄存器并 detach

App 在启动的极早期(通常在 .init_array​ 或 JNI_OnLoad​ 中),主动调用 ptrace(PTRACE_TRACEME, 0, 0, 0),如果 Frida 已经附加,App 再调用相关 ptrace 操作可能失败,由此检测出问题

再有一种变体就是开Watcher进程先 ptrace 附加 Worker,占住 ptrace 字段

Hook ptrace,不再调用原始实现,直接返回伪造结果

Frida通过 memfd 加载 frida-agent.so 时,会在目标进程里留下一个 fd,指向 /memfd:frida-agent-64.so

Native实现如下:

魔改 Frida 改 memfd 名称,或 Hook readlink 返回假路径

Frida 运行时依赖 GLib,会创建名称固定的辅助线程,可以通过遍历 /proc/self/task/ 目录,读取各线程的comm文件,匹配特征线程名

伪代码如下:

Hook 文件打开与读取:核心思路是拦截对 /proc​ 目录下 comm​ 或 status​ 文件的读取操作。当发现 App 试图读取特定线程的信息时,检查读取到的内容,如果是 gmain​ 等敏感名称,将其替换为合法的系统线程名(如 epoll​、config_store 等)

或者干脆使用去特征的魔改

对于 fd​ (文件描述符)、status​ (线程状态)、maps​ (内存映射) 这三类基于 /proc​ 文件系统的检测,上述 IO 重定向 方案是通解,只要将敏感文件指向无害文件(如 /dev/null 或提前备份的正常文件),即可实现完美绕过

以上我们大多通过hook libc导出函数来绕过检查,那么假如不走libc,直接 svc进内核,自己实现 字符串比较、读文件,不调你 Hook 的符号,那我们就难以实现反调试

AArch64 下

以下给出实际例子:

我们发现,自实现系统函数一个重要的前提就是它们都有标准的系统调用号,标准的机器码,直接搜索对应函数的特征码,定位到之后再使用Interceptor进行Hook,但是需要处理架构差异

Inline Hook 是 Frida Interceptor 模块的核心机制。原理是直接修改内存中目标函数的前几条指令(Prologue),替换为一条跳转指令(Trampoline/跳板),把执行流引导到 Frida 的处理函数,还有软件断点,调试器插入断点会写入 0xCC (INT 3) 指令,同样会改变 CRC

一般的检测是 CRC/MD5 比对:计算内存中函数前 N 字节的哈希,和磁盘 .so 里原始机器码比对,不一致说明被 Hook

方法一:Hook memcmp

但是Hook strcmp带来的开销太大,我们常Hook哈希函数作为替代

Root的权限判断并没有想象中仅仅修改一个uid这么简单,权限检查是多重配合的,我目前已知是:UID / GID capabilities SELinux seccomp 挂载命名空间

每个进程都有一个task_struct,其内部有一个cred结构体记载了权限信息

每个 App 安装时,PackageManager 分配一个独立 UID,当访问文件的时候,第一层是检查DAC(Discretionary Access Control),根据 UID/GID 和文件权限位(rwx)决定能不能访问,文件属主可以自己 chmod 改权限

举例:

一个 App,UID 是 10123,去读另一个 App 的文件

如果是Root权限,会直接无视这一步的检查,这是因为DAC检查了一个CAP_DAC_OVERRIDE,有这个的默认放行,而Root的能力集cap_effective 里有这一项

SELinux 是强制访问控制(MAC,Mandatory Access Control)

过了DAC权限检查后,并不能直接地获得所有文件的读写权限,否则太易受攻击,而Security-Enhanced Linux采取的是最小权限原则

SELinux的核心是它的安全上下文,格式为:user:role:type:level

他是从域和策略的角度来决定的是否进程能访问某个文件

举例如下:

seccomp (Secure Computing Mode) 是 Linux 内核的一种安全机制,用于限制进程可以调用的系统调用集,本质上是linux系统调用的防火墙,可查看文档df9K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3#2S2L8U0N6Q4x3X3g2G2M7X3N6Q4x3V1k6D9K9h3&6#2P5q4)9J5c8X3#2S2L8W2)9J5k6s2m8S2k6$3g2K6i4K6u0r3L8h3q4F1x3#2)9J5c8Y4y4W2j5$3y4G2L8i4m8Q4y4h3k6J5N6h3I4W2i4K6g2X3j5h3c8V1i4K6u0W2x3#2)9J5k6h3S2@1L8h3H3`.

seccomp 的核心工作原理是基于 cBPF(classic Berkeley Packet Filter),一种内核中的 "虚拟机" 机制,来定义和执行系统调用的过滤规则

BPF就理解成内核里的if-else规则表就好

假设内核要决定:某个系统调用(syscall)不让执行,没有 BPF 的话,内核只能写死

可以把 Seccomp 想象成一个海关检查站:

应用程序 (Application): 发起一个请求

Seccomp 过滤器 (The Checkpoint): 拦截这个请求,并检查手中的“规则手册”(BPF Profiles)

规则:允许删除文件吗?No

内核 (Kernel): 根据 Seccomp 的指令,直接拒绝请求,并可能惩罚该进程(终止运行)

结果:恶意操作未遂,系统安全得到保障

前四层管的是“能不能做这个操作”:

UID/GID:你的身份是什么

capabilities:你有没有这个特权

SELinux:你的域允许不允许

seccomp:这个 syscall 能不能发出去

挂载命名空间管的是你这个进程能看到什么

老版本 Android 的 /system 分区还没有挂载 nosuid,那么这就给了普通 App执行/system/bin/su的机会,大概原理是su文件含有setuid位,以“文件属主”的身份运行,而su的身份显然是Root,现在我们就有了一个Root进程,su已经以较高权限跑起来了,但是它自己还要完成策略和切换,修改 cred uid=0, capabilities...等等参数,然后通常再 exec 成一个 shell
,但是问题在于,Andriod根本不想给你提权的机会,它的/system/bin/下根本不会给你这些su文件,这就带来了检测方案

我们直接对着扫目录就行

Java层:

Native层:

但是很显然地,这个直接mount namespace就解决了,所以我们可以尝试执行提权命令,通过回返判断

Native层:

尽管如此,但仍可能被策略拒绝、隐藏、Hook 执行结果

https://bbs.kanxue.com/thread-292966.htm内有一张不错的流程图

图片描述

了解过 Android 启动过程的应该知道,Bootloader 负责加载并启动内核,内核随后启动 PID 1 的 init,再由 init 按启动配置拉起其他系统服务和进程(可以看这位师傅的https://bbs.kanxue.com/thread-285949.htm),Magisk 正是通过修改 boot 镜像,在早期启动阶段插入自己的逻辑(核心是 magiskinit),从更靠前的位置完成 Root 环境的部署

下面我们对着源代码分析一下关键节点:

用 magiskboot 工具解包 boot 镜像,并找出真正的 ramdisk

把 ramdisk 里的 init 换成 magiskinit,备份原始 ramdisk,记录配置,重新打包

那么在magiskinit里到底做了什么呢,我们追踪到e9bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6@1L8%4m8B7L8$3S2F1N6%4g2Q4x3V1k6y4j5h3N6A6M7$3E0Q4x3V1k6T1L8r3!0T1i4K6u0r3L8h3q4K6N6r3g2J5i4K6u0r3L8X3q4@1K9i4k6W2i4K6u0r3M7%4u0U0i4K6u0r3K9h3&6A6N6q4)9J5c8X3W2F1K9i4c8Q4x3X3g2J5M7H3`.`.

接下来看start

分析first_stage:

其实就是

接下来分析second_stage:

两个patch_root,由于篇幅过长,简单总结

这一阶段主要是处理第一阶段的痕迹,再者就是修改selinux策略,原始 SELinux 策略里没有关于magisk的域,所以 Magisk 必须修改 SELinux 策略,修改策略原理如下

Magisk 用 FIFO 把 init 卡在"加载 SELinux 策略"这一步,趁机把策略换掉,再放 init 走,init 以为自己加载了原始策略,实际上加载的是 Magisk 改过的版本

接下来我们本来应该追exec_init,位于df4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6@1L8%4m8B7L8$3S2F1N6%4g2Q4x3V1k6y4j5h3N6A6M7$3E0Q4x3V1k6T1L8r3!0T1i4K6u0r3L8h3q4K6N6r3g2J5i4K6u0r3L8X3q4@1K9i4k6W2i4K6u0r3M7%4u0U0i4K6u0r3K9h3&6A6N6q4)9J5c8X3#2G2N6h3&6@1i4K6u0W2M7Y4x3`.

但是该文件内部其他段落也比较重要,所以我一并分析了

启动早期,真正的 /data 分区还没挂载,但 Magisk 需要在 /data 下放东西,所以在 /data 上挂一个 tmpfs(内存文件系统),临时充当 /data

magiskinit 干完所有活后,执行真正的 init,把自己替换掉,回到了init

magiskinit 在启动时,通过 patch_rc_scripts() 和 inject_magisk_rc() 函数,将一段启动 magiskd 的配置写入系统的 init.rc 文件,大致如下

此时,magisk --post-fs-data 作为一个客户端被启动。它的首要任务是尝试连接 magiskd 的 Unix Domain Socket由于 magiskd 尚未运行,连接会失败。

随后,客户端代码会调用 connect_daemon() 函数,根据源码逻辑,该函数会检测到 Socket 不存在,然后 fork() 一个子进程。这个子进程会调用 daemon_entry(),从而正式启动 magiskd 守护进程

Magisk维护的守护进程

b80K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6@1L8%4m8B7L8$3S2F1N6%4g2Q4x3V1k6y4j5h3N6A6M7$3E0Q4x3V1k6@1M7X3g2W2i4K6u0r3L8h3q4K6N6r3g2J5i4K6u0r3L8X3q4@1K9i4k6W2i4K6u0r3M7%4u0U0i4K6u0r3j5$3!0J5k6b7`.`.

随后调用handle_requests处理权限校验:

接着是su_daemon_handler

Magisk采取了Magic Mount配合DenyList来做到隐藏机制,是 Magisk 实现 systemless 修改的核心机制,所谓“systemless”,指的是不修改真实的系统分区,而是在当前进程的挂载命名空间里,用 bind mount 把模块文件“覆盖”到系统分区的对应路径上,对上层应用来说,/system/bin/xxx 看起来就是系统原本的文件,实际可能来自模块目录

举例如下:

工作原理:利用挂载命名空间(Mount Namespace)实现“视图隔离”

Linux系统允许每个进程拥有独立的文件系统视图,这就是挂载命名空间,DenyList正是利用了这一机制

隔离视图:当被列入DenyList的应用启动时,Magisk会为它创建一个全新的、干净的挂载命名空间。在这个“房间”里,Magisk的su二进制、模块目录等所有挂载痕迹都被移除了

效果:对于这个应用而言,它看到的系统是“原厂、未修改”的。当它执行检查/system/bin/su、/data/adb/modules等操作时,得到的会是“文件不存在”的结果

Zygisk会伪装成系统原生的“Native Bridge”库,在系统启动时被Zygote进程自动加载

加载后,Zygisk会在Zygote进程中注册一个“fork前”的回调,此时Zygisk会检查目标App是否在DenyList中,或者是否有Zygisk模块需要注入

由于注入在应用代码执行之前就已完成,应用没有机会在启动早期检测到这些模块的存在,而且这个时候进程还没触发UID drop

简单了解了magisk的注入原理后,应继续了解怎么进行检测

Zygisk 会向目标进程注入一个动态库,该库被映射到进程的地址空间,并在内存中留下持久记录,Shamiko 无法将其移除

Shamiko 的功能在于清理文件系统挂载,使得 /data/adb/modules 等目录在目标进程的挂载命名空间中不可见

然而,Zygisk 的注入发生在文件系统隐藏之前,其动态库已经通过 mmap 映射到进程内存中,因此文件系统层面的隐藏无法消除内存中的映射痕迹

读 /proc/self/maps,一行一行看

如果看到

这就是 Zygisk 注入痕迹

可以查询这些关键词

搜到就是有 Zygisk,但关键词只能抓没改名的


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
点赞 ・101
打赏
分享
最新回复 (43)
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2 楼
感谢分享
2026-10-1 18:13
0
雪    币: 10970
活跃值: (9309)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
3 楼
666
2026-10-1 19:02
0
雪    币: 4801
活跃值: (8277)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4 楼
感谢分享
2026-10-1 21:20
0
雪    币: 3269
活跃值: (6400)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5 楼
666
2026-10-1 22:52
0
雪    币: 840
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6 楼
先学习
2026-10-2 00:32
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7 楼
666
2026-10-2 08:11
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8 楼
感谢分享
2026-10-2 09:33
0
雪    币: 9291
活跃值: (6367)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9 楼
感谢大神的分享,学习一下
2026-10-2 11:19
0
雪    币: 6801
活跃值: (12032)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10 楼
2026-10-2 11:36
0
雪    币: 829
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11 楼
感谢分享
2026-10-3 10:53
0
雪    币: 9
活跃值: (2041)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12 楼
感谢分享
2026-10-3 18:05
0
雪    币: 1901
活跃值: (6023)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
13 楼
1234
2026-10-3 21:00
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14 楼
666666
2026-10-4 01:32
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15 楼
厉害
2026-10-4 11:01
0
雪    币: 820
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16 楼
1
2026-10-4 16:17
0
雪    币: 74
活跃值: (900)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17 楼
2423
2026-10-4 17:53
0
雪    币: 1224
活跃值: (2582)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18 楼
1
2026-10-4 17:55
0
雪    币: 1783
活跃值: (2475)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19 楼
666
2026-10-5 19:45
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20 楼
学习
2026-10-6 11:40
0
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21 楼
666
2026-10-6 22:17
0
雪    币: 9406
活跃值: (6308)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22 楼
666
2026-10-7 12:53
0
雪    币: 407
活跃值: (3039)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23 楼
支持一个
2026-10-7 15:57
0
雪    币: 69
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24 楼
666
2026-10-8 09:15
0
雪    币: 3184
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
25 楼
6666
2026-10-8 13:19
0
游客
登录 | 注册 方可回帖
返回