RootHVD 一步步获取鸿蒙模拟器Root

首先

10月初,终于是官宣了Harmony Next系统。月底就已经有获取模拟器 Root 的方案了

HarmonyOS Next 模拟器 root | wuxianlin

HarmonyOSNext模拟器Root(无视模拟器镜像完整性验证)HarmonyOSNext模拟器Root,无视模拟 - 掘金

我在DB1版本的模拟器中就通过类似的方法拿到了 root 权限。但是短短2个月之后的 DB3 就无法使用了。于是对于新的root获取方式的研究直到最近收到了上文的启发,才继续研究了一下,终于是又重获了 Root.

本文就以技术随笔的形式,记录一下一步步获取到Root的思考过程

Topic1 : 获取到Root权限就一定是漏洞?

在 Android 时代的华为手机中,如果能获取到 root 权限就有机会在 PSIRT 上得到不菲的漏洞奖励。这主要是得益于 SecureBoot 保障了设备整个启动过程中是未被篡改的,再加上带有 Bootloader 锁的设备只允许刷入厂商认证的系统镜像;

那么对于一个模拟器设备呢?

Android 的大部分模拟器都是通过 qemu 来完成的。HarmonyOS 也是一样的,在开源的OpenHarmony 就提供了qemu的运行编译选项 (虽然按照教程文档编译可能会被坑到怀疑人生). 因此模拟器很难基于设备可信根来保障加载分区的完整性。此时,获取到Root权限理论上可以像 Android 刷入Magisk 一样简单;

这就是为什么要选择模拟器来拿 Root 用于安全研究;

Topic2 : Root 需要修改那些文件?

启动HarmonyOS模拟器之后,通过 ps 命令查看对应的进程,可以看到两个重要的文件路径:

  • /Users/xxx/.Huawei/Emulator/deployed : 指向模拟器实例目录,保存实例相关文件,比如cache, user_data 等分区内容;

  • /Users/xxx/Library/Huawei/Sdk : 指向模拟器系统镜像,保存Kernel image, system, vendor等分区镜像;

一般需要直接系统配置文件都会保存在 system 分区,但是看下这个目录保存的内容会发现image_signature 目录保存了各个分区的签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ tree system-image/HarmonyOS-NEXT
system-image/HarmonyOS-NEXT
└── phone_arm
├── Image
├── features.ini
├── image_signature
│ ├── Image.hwp7s
│ ├── ramdisk.img.hwp7s
│ ├── sys_prod.img.hwp7s
│ ├── system.img.hwp7s
│ ├── userdata.img.hwp7s
│ └── vendor.img.hwp7s
├── info.json
├── ramdisk.img
├── sdk-pkg.json
├── sys_prod.img
├── system.img
├── userdata.img
└── vendor.img

Emulator 在启动过程中会检查关键分区的完整性:

1
2
3
4
5
6
7
$ cat Emulator.log | grep "verify succeed"
[Info] [CheckImage.cpp(CheckSignUser:110)]USERimage is correct, verify succeed.
[Info] [CheckImage.cpp(CheckSign:89)]SYSimage is correct, verify succeed.
[Info] [CheckImage.cpp(CheckSignUser:110)]USERimage is correct, verify succeed.
[Info] [CheckImage.cpp(CheckSign:89)]SYSimage is correct, verify succeed.
[Info] [CheckImage.cpp(CheckSignUser:110)]USERimage is correct, verify succeed.
[Info] [CheckImage.cpp(CheckSign:89)]SYSimage is correct, verify succeed.

因此贸然的修改 system.img 并不是一个明智的选择。可以选择修改qemu生成的system.img.qcow2 文件来绕过对 system.img 完整性的检查

1
2
$ file system.img.qcow2
system.img.qcow2: QEMU QCOW2 Image (v3), has backing file (path /Users/listennter/Library/Huawei/Sdk/system-image/HarmonyOS-NEXT/phone_arm//system.img), 2508193792 bytes

qcow2 文件链接了 system.img 的路径,为了在修改方便首先将 qcow2 转换为 ext2 文件系统, 然后就可以使用mount挂在到linux系统上:

1
2
$ qemu-img convert -O raw system.img.qcow2 system.img.raw
# mount -t ext4 -o loop system.img.raw ./hello

下面就开始调整系统中各项配置文件了

hdcd.cfg

这个文件位于 /system/etc/init/hdcd.cfg, 其功能等效于 Android 系统中的 rc 文件,init 进程会依次加载各个 cfg 文件,完成系统服务的加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"services" : [{
"name" : "hdcd",
"path" : ["/system/bin/hdcd"], // 服务入口文件;
"caps" : ["CAP_NET_RAW"], // capability
"uid" : "shell", // hdcd service uid
"gid" : [ "shell", "log", "readproc", "file_manager", "user_data_rw", "netsys_socket" ], // hdcd service gid
"socket" : [{
"name" : "hdcd",
"family" : "AF_UNIX",
"type" : "SOCK_SEQPACKET",
"protocol" : "default",
"permissions" : "0660",
"uid" : "shell",
"gid" : "shell"
}],
"critical" : [ 0, 5, 10 ],
"apl" : "normal",
"permission" : [
...
],
"permission_acls" : [
...
],
"sandbox" : 0,
"start-mode" : "condition",
"secon" : "u:r:hdcd:s0", // hdcd service label
"disabled" : 1
}
]

那么实际上需要修改 uid/gid/secon 即可:

  • “uid” : “root”

  • “gid” : [“root”, ….]

  • “secon” : “u:r:su:s0” (当然,参考了其他的blog发现这里无法调整成 su)

system_common.cil

该文件为 selinux policy 文件,保存路径为 /system/etc/selinux/system_common.cil

(type sh) 变更为 (typepermissive sh), 让sh变成一个不受selinux限制的特权用户;

同时,这里也出现了第一个迷惑行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ls -al system/etc/selinux
total 2644
drwxr-xr-x. 5 root root 4096 Oct 19 18:47 .
drwxr-xr-x. 85 root root 4096 Oct 19 18:47 ..
drwxr-xr-x. 2 root root 4096 Oct 19 18:47 compatible
drwxr-xr-x. 2 root root 4096 Oct 19 18:47 compatible_developer
-rw-r--r--. 1 root root 624 Oct 19 18:47 config
-rw-r--r--. 1 root root 45 Oct 8 10:02 ignore_cfg
-rw-r--r--. 1 root root 81510 Oct 19 18:46 system.cil
-rw-r--r--. 1 root root 65 Oct 19 18:46 system.cil.sha256
-rw-r--r--. 1 root root 2415868 Oct 19 18:46 system_common.cil
-rw-r--r--. 1 root root 168602 Oct 19 18:46 system_developer.cil
-rw-r--r--. 1 root root 65 Oct 19 18:46 system_developer.cil.sha256

明明已经有 .sha256 文件来提供完整性校验能力,但是init在启动过程中却没有检查,导致了我直接修改 system_common.cil 文件内容仍然可以启动成功;

ohos.para & hdc.para

这两个文件相当于Android prop的各种属性

1
2
3
4
5
6
7
// system/etc/param/ohos.para
const.secure=0
const.debuggable=1


// system/etc/param/hdc.para 很明显这个文件定义了hdcd的开启模式,感觉该不该都可;
persist.hdc.mode.usb = "enable"

完成全部修改后,安全umount,然后重新把分区转化为qcow2格式并替换掉 system.img.qcow2

1
2
$ sudo umount /home/xxx/hm/system.img.raw
$ qemu-img convert -O qcow2 system_out.img system_out.qcow2

启动HarmonyOS模拟器,发现 hdcd 服务挂掉了,Fxxk …

Bug & Fix the Bug & RCA

首先是查看 kernel log,发现 init 正常的拉起了 hdcd,但是,hdcd 貌似异常退出了

1
2
3
4
5
[    9.999149][    T1] [pid=1][Init][INFO][init_common_service.c:664]ServiceStart started info hdcd(pid 827 uid 0)
[ 9.999460][ T827] [pid=827][Init][INFO][init_service_socket.c:144]CreateSocket hdcd success
[ 10.001896][ T827] [pid=827][Init][INFO][init_service_socket.c:152]CreateSocket restoreContentRecurse /dev/unix/socket/hdcd success
[ 10.001960][ T827] [pid=827][Init][INFO][init_service.c:118]ServiceExec hdcd
[ 10.001996][ T827] [pid=827][Init][ERROR][init_service.c:132][startup_failed]failed to execv 0 1 hdcd

由于DB1和DB3版本的模拟器并未出现过 hdcd 异常退出的问题,只要从它们的system.img中复制一个 hdcd 过来就不会崩溃了;当我开心的重新启动模拟器并看到shell提示符已经是”#”时,最令我崩溃的BUG出现了:

1
2
3
4
5
6
7
8
9
10
11
# id 
/bin/sh : /bin/id : Operation not permitted
# ls
/bin/sh : /bin/ls : Operation not permitted
# whoami
/bin/sh : /bin/whoami : Operation not permitted
# getenforce
Enforcing
# setenforce 0
# getenforce
Permissive

当 hdcd 以shell uid 启动的时候,whoami等一系列系统命令都可以运行,但是只要切换到root 用户运行系统命令就会提示 Operation not permitted;

这个莫名其妙的bug完全超越了我对 Linux 系统的理解,起初怀疑是在/bin/sh或者是kernel中埋入了一些小trick,但是IDA一顿逆向也没有发现任何的异常,于是我放弃了,想想能不能用其他的途径在 uid = shell的情况下把root能力补充上

目前能够做到的包括:

  1. gid = root : 已经具备了大部分目录的访问权限,但是,无法读写app的私有文件;

  2. typepermissive sh : 这赋予了hdc shell不被selinux规则拦截的权限;

  3. 自定义 capability : 在hdcd.cfg文件中,可以任意定义 capability;

  4. uid = shell : 与root权限差距最大的部分;

最有操作空间的部分就是任意添加 capability 了,其中有几个对安全研究非常又帮助的

  • CAP_SYS_PTRACE : 调试进程权限;

  • CAP_FOWNER : 忽略文件所有权;

  • CAP_SETUID : 设置进程 uid;

  • CAP_SETGID : 设置进程 gid;

CAP_SETUID/CAP_SETGID 这两个 capability 非常的吸引人,在之前获取Android root的blog中,在缺少了 seccomp-bpf 规则下,基于这两个 capability 就可以 fork 一个uid=root的进程;

给hdcd赋予了一系列capability之后,基于类似的原理来试试将 hdcd shell 的子进程uid设置为root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
#include <sys/wait.h>

int set_uid_to_root() {
if (setuid(0) != 0) {
perror("Failed to set UID to root");
exit(EXIT_FAILURE);
}

pid_t pid = fork();
if (pid < 0) {
std::cerr << "Failed to fork process" << std::endl;
return 1;
} else if (pid == 0) {
// 子进程:执行shell
execl("/system/bin/sh", "sh", nullptr);
// 如果execl返回,说明出错了
std::cerr << "Failed to execute shell" << std::endl;
return 1;
} else {
// 父进程:等待子进程结束
int status;
waitpid(pid, &status, 0);
}
}

使用 DevEco 提供的 clang++ 编译之后上传到 /data/local/tmp 目录下运行:

1
2
3
4
5
6
7
8
9
# id
uid=0(root) gid=2000(shell) groups=2000(shell),1006(file_manager),1007(log),1008(user_data_rw),1097(netsys_socket),3009(readproc) context=u:r:sh:s0
# getenforce
Enforcing
# setenforce
usage: setenforce [ Enforcing | Permissive | 1 | 0 ]
# setenforce 0
# getenforce
Permissive

神奇的一幕发生了,fork出来的root进程居然不会提示 Operation not permitted;原本只是想分析下抛出错误的原因,没想到误打误撞拿到了root …

虽然通过上面一步步踩坑终于在最后拿到了完整的Root Shell,但还是不知道产生这一系列BUG的原因,直到看到其他 blog 直接用hdcd.root.cfg覆盖了原本的配置, 才发现了问题的根因:

1
2
3
4
5
6
7
8
"services" : [{
"name" : "hdcd",
"path" : ["/system/bin/hdcd"],
"caps" : ["CAP_NET_RAW"], // Here's the point.
"uid" : "root",
"gid" : [ "root", "shell", "log", "readproc", "file_manager", "user_data_rw", "netsys_socket" ], // hdcd service gid
...
}]

Capabilities 是将 root 权限分解成多个独立的能力赋予给非root用户。原本root用户就具备了所有的Capability,然而 "caps" : ["CAP_NET_RAW"] 导致 init 进程又给已经是root用户的hdcd重新赋予了Capability,导致 hdcd 的一系列崩溃;

实际上,移除掉 caps 的部分再重新打包 qcow2 即可