~/posts/mobile/android-emulator-kernel-structure-location.md

Android 模拟器:任务链表与重定位锚点

以 Android 11 x86_64 AVD 为例,从 comm 搜索验证任务链表,用状态差分筛选 SELinux 字段,再用跨冷启动关系评价重定位锚点。区分构建配置、结构证据、算术模型与运行结果。

date[31:24]
read[23:16]
7 分钟
cat[15:8]
移动端
目录
  1. 0x00调试入口与内核对象是两层问题
  2. 0x01从 comm 候选验证嵌入链表
  3. 0x02选择参照对象而不是记住绝对地址
  4. 0x03把 SELinux 状态变化写成集合约束
  5. 0x04用跨冷启动关系评价重定位锚点
  6. 0x05把算术验证与运行验证分开
  7. 0x06参考资料

Emuroot 的模拟器适配首先是一个结构定位问题:宿主已经能通过 QEMU GDB stub 访问客户机内存,接下来才是辨认任务、凭据和 SELinux 状态。这个前提与从普通 Android 应用寻找内核漏洞不同,也不构成实体手机的通用提权路径。

案例聚焦 2021 年的 Google Play AVD:Android 11、API 30、x86_64,内核为 Linux 5.4 系列。目标是把进程名搜索、双向链表、状态差分和跨冷启动地址关系连接起来。下文保留案例中的数值用于关系检查,但不把它们作为其他系统映像的固定偏移。

调试入口与内核对象是两层问题

宿主侧的历史入口是模拟器的 -qemu -s 参数,再用 GDB 连接本机 1234 端口。-qemu 向底层传递参数;GDB stub 提供的是强于客户机内核策略的调试访问。QEMU 文档 也明确说明了该接口的权限范围以及连接保护责任,测试环境应限制其可达范围。

确定调试入口后,至少还要建立以下对象关系:

需要独立验证的结构关系4 行
对象或字段 用途 交叉检查
task_struct.tasks 遍历任务链表 双向链接一致、有限遍历后闭合
task_struct.pid、comm 确认目标任务 PID 与可观察进程对应,名称仅辅助
task_struct.cred、real_cred 定位凭据 指针目标、身份字段及版本布局
selinux_state.enforcing 观察全局执行模式 配置条件、字段宽度与状态往返变化

Linux 凭据文档 描述了 UID/GID、文件系统身份、capability 集合和 LSM 信息之间的区别。只改变一个 UID 并不描述完整权限结果。凭据还涉及引用计数和复制替换规则;调试器直接写内存并不是内核正常更新凭据的接口。

字段布局依赖 ABI、内核配置和编译结果。同样叫 cred 的结构,也应先核对配置相关成员,再讨论偏移。

从 comm 候选验证嵌入链表

搜索一个短且可控的任务名可以提供起点,例如示例名称 probe_shell。Linux 5.4 的 TASK_COMM_LEN 为 16,名称需要考虑末尾空字符。字符串命中仍可能来自日志或用户态缓冲区,只有结构关系才能把候选变成可靠节点。

tasks 是嵌入 task_struct 的 list_head。next、prev 指向相邻任务的链表节点,而不是相邻任务结构的起始地址。下面是两节点间的示意关系,不表示完整任务集合或实际插入顺序。

flowchart LR
  A["task A / tasks"] -->|next| B["task B / tasks"]
  B -->|prev| A

案例中 comm 候选地址为 C = 0xffffa30441257310,tasks 候选为 T = 0xffffa30441257048。两者相距 0x2c8。若读出 T.next 指向的节点,向该节点加上同一间距,就能检查相邻任务的 comm 候选:

Δ=C−T,Cnext=next⁡(T)+Δ\Delta=C-T,\qquad C_{\mathrm{next}}=\operatorname{next}(T)+\Delta

得到 kworker/3:0、sh 等合理名称,比仅观察到 0xffff 开头的指针更有说服力。但还需检查 next->prev、prev->next、PID 和链表闭合;对齐、高地址及可打印字符串都只是筛选条件。

内核运行期间,任务可能退出,链表也可能变化。一次不一致既可能说明偏移错误,也可能说明采样不处于一致状态;应在受控快照或暂停状态下区分两者,并给遍历设置节点数上限。

选择参照对象而不是记住绝对地址

PID 1 的身份固定,不等于其任务结构地址固定。swapper/0 对应的初始任务更适合作为内核映像内的参照,有助于避开普通任务动态分配位置的变化;它仍随内核地址空间布局随机化(KASLR)移动。

三类变化分别处理3 行
变化来源 现象 应保留的证据
任务动态分配 同类进程出现在不同地址 PID、任务字段、链表关系
同构建的映像随机化 多个内核映像对象整体移动 锚点值与对象之间的差值
内核或配置变化 相对布局也变化 映像标识、ABI、配置及重新验证的偏移

选择目标时应以 PID 为主、comm 为辅;多个 sh 并存时,名字不足以指定一个任务。链表遍历方向也不宜硬编码为“总能先遇到最新进程”,应以当前实现的插入和链接关系为准。

历史适配范围涉及 Android 9、10、11 的 x86 和 x86_64 Google Play AVD。这个范围说明有多个映像参与适配,不说明它们共享布局;每个映像都应有自己的定位参数和验证结果。

把 SELinux 状态变化写成集合约束

先确认字段是否存在,再决定搜索宽度。Linux 5.4 的 selinux_state 在 CONFIG_SECURITY_SELINUX_DEVELOP 条件下包含 bool enforcing;对应配置关闭时,访问函数直接返回启用状态。因此,“每个 5.4 内核都有一个可切换字节”并不是成立的前提。

在匹配构建、配置且可正常切换模式的参照 AVD 上,按同一地址范围和单字节宽度定义:

三次搜索的地址集合3 行
集合 状态与匹配值 案例中的数量
A Enforcing 时为 1 45,624
B Permissive 时仍为 1 45,518
C 同一 Permissive 状态下为 0 2,002,519

先排除切换后仍为 1 的地址,再要求确实读到 0:

A′=A∖BA'=A\setminus B
candidates=A′∩C\mathrm{candidates}=A'\cap C

两个条件的作用不同:差集排除不变项,交集排除读取失败、覆盖范围缺失或变成其他值的项。若内存仍在运行,还应尽量从同一暂停状态提取 B 和 C,避免两次搜索之间的变化混入结果。

案例得到约 50 个候选;再次切回 Enforcing 后,只保留重新为 1 的项,剩下两个地址。记录中的单字节干预与 getenforce 语义检查最终指向 0xffffffffb4718b19。这比一次相关变化更有因果力度,但只适用于该构建与启动状态;干预后应恢复原值。

这个地址只有 16 个十六进制数字。输入调试命令时也应验证地址宽度,避免重复前导 ffff 造成超出 64 位的值。

用跨冷启动关系评价重定位锚点

关闭 KASLR 会改变实验条件。若保留随机化,则应恢复每次启动的实际位置,而不是复用上次地址。

案例在 x86_64 cpu_entry_area 附近寻找能关联随机化内核映像的候选值,采用 4 字节步长读取 8 字节字。这样的重叠读取可能把描述符的分段字段拼成“看起来像指针”的值,所以这是经验性锚点搜索,而不是已经证明了每个命中的结构类型。

设固定读取位置为 P,首次读到 V1,同时已确认 swapper/0 的相关字段地址为 S1。先保存 D = S1 - V1,冷启动后从同一位置读取 V2,再预测 S2 = V2 + D。

案例中的重定位关系5 行
量 值
V1 0xffffffffb2448e00
S1 0xffffffffb3028310
D 0xbdf510,即 12,449,040
V2 0xffffffffa2448e00
预测 S2 0xffffffffa3028310

算术相符只是第一步。预测位置还要通过任务名、PID 和链表关系检查;多次冷启动一致后,才能把这个差值保留为该映像的参数。精确锚点位置 P 与映像哈希在此案例中尚未给出,所以这些数字不组成可直接复用的定位配置。

SELinux 状态对象也可按同样原则与已确认内核参照建立相对关系。升级映像、改变编译配置或入口区布局后,应重新验证,而不是继承差值。

把算术验证与运行验证分开

下面的模型验证集合运算、反向状态筛选、comm 间距与重定位算式。集合使用明确标为示例的小地址;脚本没有连接模拟器或读写内核内存。

emuroot_layout_model.pypython
def changed_to_zero(a, b, c):
    return (a - b) & c

# Illustrative address sets; not guest-memory observations.
a = {0x1000, 0x1001, 0x1002, 0x1003}
b = {0x1000, 0x2000}
c = {0x1001, 0x1002}
assert changed_to_zero(a, b, c) == {0x1001, 0x1002}
assert len(a - b) != len(a) - len(b)
back_to_one = {0x1002}
assert changed_to_zero(a, b, c) & back_to_one == {0x1002}

task_link = 0xffffa30441257048
comm = 0xffffa30441257310
assert comm - task_link == 0x2c8

v1 = 0xffffffffb2448e00
s1 = 0xffffffffb3028310
v2 = 0xffffffffa2448e00
delta = s1 - v1
assert delta == 12449040 == 0xbdf510
assert v2 + delta == 0xffffffffa3028310
assert len("probe_shell".encode("ascii")) < 16
print("PASS: candidate sets; reverse transition; comm delta=0x2c8")
print("PASS: relocation delta=0xbdf510; predicted=0xffffffffa3028310")
定位关系模型的实际输出
$ python emuroot_layout_model.py
PASS: candidate sets; reverse transition; comm delta=0x2c8
PASS: relocation delta=0xbdf510; predicted=0xffffffffa3028310

真正的运行验证还应同时回答:对象是否选对,权限行为是否符合预期,以及实验状态是否已恢复。

运行验证应保留的检查3 步
  1. 1

    结构一致

    记录映像版本、配置、任务 PID、链表闭合及凭据字段关系,不用单个字符串或指针形状代替这些证据。

  2. 2

    语义一致

    把目标进程的身份、capability、受控操作结果和 SELinux 观察分开记录;GDB 写入没有报错只说明写操作完成。

  3. 3

    恢复与复查

    恢复改动前的状态,再用冷启动验证定位关系。快照恢复与真正重新随机化的冷启动应分开标记。

可复用的是带有验证条件的定位过程:名称提供候选,链表约束建立对象身份,状态差分缩小变量集合,冷启动检验重定位关系。固定地址只是其中一次观测的结果。

参考资料

NORMAL~/posts/mobile/android-emulator-kernel-structure-location.md§--
0%zh-CN