首页
社区
课程
招聘
[原创]内核级过Root检测
发表于: 9小时前 184

[原创]内核级过Root检测

9小时前
184

上一篇讲的是怎么把代码塞进目标进程。
这一篇讲的是更底层的问题:环境本身是脏的。

(系列第二篇 · 内核态伪装)
第一篇:frida 进场 2.5 秒就被杀,我写了套零线程注入
第三篇:加固不让你抓包?那就到内核层动手

企业级加固真正开始干活的时候发现另一件事:这个环境本身,一眼就能看出来被动过。

加固根本不需要检测 frida。它只要读一眼这些,就知道你不是善茬。

先看看市面上主流方案留下了什么:

Magisk

KernelSU

两者共同

加固查什么?查文件、查进程表、查挂载、读 /proc/cmdline。

它的检测本质是对比「应然」和「实然」:

常规方案是改用户态。改完之后,跟你"应该有的样子"对不上,就露馅。

加固查的每一样东西 —— /proc/cmdline、进程表、maps —— 数据全部由内核产出。

而内核嘛……我可以自己编一个。

那么:

没有任何一个用户态文件需要藏,因为谎言是在数据出厂前就编好的。

代价是每次系统更新、换机型都要重新编译。

纪律也很重要:

Linux 里"是不是 root"不是天赋,是一张证件。

每个进程在内核里都挂着一份 struct cred,记着 uid、gid 和 capability 集。

所谓提权,就是把这张证件换成全 0 的。

常规方案(su)是在用户态放一个 setuid 0 的文件,谁执行谁换证。

我的做法:在内核里开一个后门命令,进程一发这个命令,内核亲手给它换证。

不能新增 syscall —— 那会在 syscall 表上留一行新记录,一扫就看见。

所以借用一个已有的 prctl,在里面加一个私有 option:

__sc_swap() 里做的事很朴素:

uid 2000 就是 adb shell。

只有从电脑插线进来的 shell 能提权,App 哪怕反编译出来知道了 0x4d52 这个魔数,也调不动(直接 EPERM)。

而提权工具本身就是 /data/local/tmp/ 下一个普通二进制文件。

谁都能删,删了随时再推。

它本身不含任何权限,权限完全在内核里。

最后这条值得说一下:__always_inline 让 __sc_swap 不产生 kallsyms 符号。

/proc/kallsyms 里查不到,逆向也对不上号。

换了 uid 0 的证件,只是过了 Unix 层。

Android 还有第二套保险:SELinux。

就算你是 root,策略说你这个 domain 不能读那个文件,一样拒绝。

常规做法是 setenforce 0 —— 全局关掉。

这一下就露馅了。

加固读一下 getenforce,返回不是 Enforcing,直接判定环境异常。

SELinux 的所有权限检查最终都汇聚到一个入口函数 avc_has_perm_noaudit。

补丁只在这里加三行:

securebits 是 cred 里一个平时没人用的字段,征用一个 bit 当暗号。

配合上面换证时打的标记。

这大概是整个框架里我最喜欢的一处:不破坏系统的任何可观测状态,只给自己开小灶。

对比 setenforce 0 那种"把保安室炸了"的做法,高下立判。

剩下的就是逐个补检测面。

每个补丁的格式都一样:谁在问 → 问哪个接口 → 答真话还是假话。

加固读 /proc/cmdline 看 androidboot.verifiedbootstate。刷过机的是 orange。

补丁在内核启动早期把这个字符串等长替换成 green。

等长很重要 —— 长度变了会破坏后面参数的解析。

加固遍历 /proc/*/comm 找名字。

补丁在 proc_pid_readdir() 里跳过命中的进程 —— 那个 pid 直接不出现在列表里。

加固读 /proc/self/maps 找 frida-agent-*.so、/memfd:。

这个补丁我翻过车,而且是两次,值得单独讲 —— 见下一节。

加固轮询 /proc/self/status 的 TracerPid,非 0 说明正被调试。

补丁在 task_state() 里:

意思是:uid ≥ 10000(App 世界)读时永远返回 0;uid < 10000(自己人)读时返回真值。

这个双视图设计很关键 —— 藏东西的人必须自己看得见藏哪了,不然没法调试。

这是最狠的一招:检测到异常直接 kill 自己,让你连排查机会都没有。

补丁拦在 sys_kill / sys_tgkill / sys_tkill 三个入口:

效果:加固照常检测、照常判定异常、照常发起自杀 —— 但进程永远死不掉。

我们还能继续观察它。

加固遍历 /data/local/tmp 找提权工具、boot 镜像、frida 相关文件名。

补丁在 filldir() —— 所有目录列表输出的必经之路。

一个补丁管住 ls、opendir、find 所有姿势。

uname -r 会显示 4.14.268-Marisa-KSU 这种自编后缀,一眼看出刷过机。

补丁按调用者 comm 匹配,命中的进程看到的版本变成 3.10.0-lowmem(一个古老低调的版本号)。

这个补丁也翻过车:v1 只匹配了主进程名,结果注入实际发生在 helper 子进程里,helper 的 uname 没被伪装,照样露馅。

教训:伪装面要覆盖"所有会报信的进程",漏一个就白干。

回到 maps 那个补丁。

思路很直接:命中就整行不输出 —— 假装这段映射不存在。

结果:agent 挂死,CPU 100%。

想了一下才明白:agent 自己也要读自己的 maps 来定位注入段。

它读不到自己的映射,就陷入忙等。

这告诉我们一件事:

视图伪造不能"一刀切地删",因为观察者不止敌人一个。

地址、权限、偏移全部照常输出,只把路径字符串换成无辜的系统库:

加固看到的是"一段普通的媒体库映射"。

agent 看到自己的映射范围完整无损。

所有观察者都满意。

这套框架里有五条思维模型,我觉得比代码本身更有价值:

① 改报告,不改事实

进程确实存在、文件确实存在、信号确实发了 —— 但 /proc 说不存在、readdir 说没有、kill 返回 0。


[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)

收藏
点赞 ・19
打赏
分享
最新回复 (4)
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2 楼
学习一下
6小时前
0
雪    币: 311
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3 楼
666
6小时前
0
雪    币: 0
活跃值: (1225)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4 楼
666666
5小时前
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5 楼
感谢分享
2小时前
0
游客
登录 | 注册 方可回帖
返回