查看原文
其他

Android 11 手动root笔记

tacesrever 看雪学苑 2022-07-01

本文为看雪论坛精华文章
看雪论坛作者ID:tacesrever



目标是在已解锁的android11手机上,通过修改原有系统的方式,以尽量少的痕迹获取(adb)root权限并全局装入frida-gadget。

关于为什么不用magisk或其他框架,因为感觉magisk的解决方案对我来说功能过于冗余,并且重视安全的应用都会试图通过各种姿势检测magisk或其他框架以及root (在某些群看到群友问某某应用检测root 怎么办) 一想到有可能会遇到有关magisk的兼容性问题,还要去定位解决,很头大 (也是还没用过magisk)。


1


root思路


之前了解过adb root的原理:adbd服务最初以root用户的权限运行,读取判断一些系统配置后会自行降权,切换至shell用户。直接修改系统配置的值虽然操作不难,但是可以被检测到,在某些系统上还会导致log增多影响性能,所以想法是patch系统上的adbd文件使其不会执行降权。



2


获取原系统镜像文件


手机的系统发布商向公众提供了OTA包下载途径,特点是zip文件内含payload.bin。可以使用 https://github.com/vm03/payload_dumper 解包其中的镜像文件。

使用前需要运行如下命令以安装其依赖:pip install protobuf bsdiff4。

在git clone https://github.com/vm03/payload_dumper之后解压OTA包将payload.bin移至payload_dumper文件夹下,运行python payload_dumper.py payload.bin,输出在output文件夹里。



3


 获取recovery模式下的root权限


recovery模式的系统文件在boot.img中,可以使用 https://github.com/cfig/Android_boot_image_editor 解包。

使用前需要安装较高版本的java,我安装的是jdk16,安装后需要手动将jdk中的bin路径添加到系统变量中。此工具使用gradle进行项目部署及运行,需要联网从maven仓库下载一些包,如感觉速度慢则需要高速国际网络或者换源。

这里讲一下换源的方法:在git clone https://github.com/cfig/Android_boot_image_editor之后编辑其中的build.gradle.kts找到:

buildscript {    repositories {        mavenCentral()    }    ...}

在repositories中添加三行:

buildscript {    repositories {        maven {          setUrl("http://maven.aliyun.com/nexus/content/groups/public/")        }        mavenCentral()    }    ...}

将解包出的boot.img与vbmeta.img复制进Android_boot_image_editor文件夹里,运行gradlew unpack输出的文件系统在build/unzip_boot/root下。

编辑build/unzip_boot/vbmeta.avb.json将header.flags改为2来禁用全局verity,否则手机不会接受我们修改后的boot.img。

编辑build/unzip_boot/root/prop.default,将ro.secure改为0,ro.adb.secure改为0,ro.debuggable改为1,该修改仅影响recovery模式下的系统属性。
 
运行gradlew pack,usb连接手机依次运行:

adb reboot bootloaderfastboot flash --disable-verity --disable-verification vbmeta vbmeta.img.signedfastboot flash boot boot.img.signedfastboot reboot recovery

之后在recovery模式下运行adb shell,提示符已经变为#表示为root权限。




4


在recovery模式下修改system文件的方式


该系统使用虚拟A/B的方式升级,system文件系统在/dev/block/byname/super中偏移为1024*1024的位置处。

为了有效率地在windows下直接编辑android系统中未挂载的ext4文件系统而不用重新刷写,找到了 https://github.com/cubinator/ext4 并添加了覆写文件的方法,fork并修改后的项目见 https://github.com/tacesrever/ext4 ,并编写了一个简陋的socket程序用于转发文件读写操作。

服务器端:


#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <errno.h> #define BLOCK_SIZE 4096#define LISTEN_PORT 4096 typedef enum {    READ,    SEEK,    WRITE,    TELL,    OPEN,    CLOSE} foperate; typedef struct {    foperate op;    u_int64_t arg;} fcommand; int recv_all(int sock_fd, u_char* buffer, uint64_t size) {    uint64_t recved_size = 0, recv_size;    while (recved_size < size) {        recv_size = recv(sock_fd, buffer + recved_size, size, MSG_WAITALL);        if(recv_size <= 0) {            break;        } else {            recved_size += recv_size;        }    }    return recved_size;} int send_all(int sock_fd, u_char* buffer, uint64_t size) {    uint64_t sended_size = 0, send_size;    while (sended_size < size) {        send_size = send(sock_fd, buffer + sended_size, size, MSG_WAITALL);        if(send_size <= 0) {            break;        } else {            sended_size += send_size;        }    }    return sended_size;} int main(int argc, const char**argv) {    int super_fd, sock_fd, client_addr_size, conn_fd, shutdown;    uint64_t transfered_size, transfer_size;    struct sockaddr_in listen_addr, client_addr;    u_char* buffer;     fcommand command;    buffer = (u_char*)malloc(BLOCK_SIZE);    super_fd = open("/dev/block/by-name/super", O_RDWR | O_SYNC);    sock_fd = socket(PF_INET, SOCK_STREAM, 0);    memset(&listen_addr, 0, sizeof(listen_addr));    listen_addr.sin_family = AF_INET;    listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);    listen_addr.sin_port = htons(LISTEN_PORT);    bind(sock_fd, (struct sockaddr*)&listen_addr, sizeof(listen_addr));    listen(sock_fd, 2);     while (1) {        client_addr_size = sizeof(client_addr);        conn_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &client_addr_size);        shutdown = 0;        while (1) {            if(recv_all(conn_fd, &command.op, 4) != 4) {                close(conn_fd);                break;            };            if(recv_all(conn_fd, &command.arg, 8) != 8) {                close(conn_fd);                break;            };             switch (command.op) {            case READ:                transfered_size = 0;                while (transfered_size < command.arg) {                    transfer_size = command.arg - transfered_size > BLOCK_SIZE ? BLOCK_SIZE : command.arg - transfered_size;                    read(super_fd, buffer, transfer_size);                    if(send_all(conn_fd, buffer, transfer_size) != transfer_size) {                        shutdown = 1;                        break;                    }                    transfered_size += transfer_size;                }                break;            case SEEK:                lseek64(super_fd, command.arg, SEEK_SET);                break;            case WRITE:                transfered_size = 0;                while (transfered_size < command.arg) {                    transfer_size = command.arg - transfered_size > BLOCK_SIZE ? BLOCK_SIZE : command.arg - transfered_size;                    if(recv_all(conn_fd, buffer, transfer_size) != transfer_size) {                        shutdown = 1;                        break;                    }                    write(super_fd, buffer, transfer_size);                    transfered_size += transfer_size;                }                 break;            case TELL:                command.arg = lseek64(super_fd, 0, SEEK_CUR);                if(send_all(conn_fd, &command.arg, 8) != 8) {                    shutdown = 1;                };                break;            case OPEN:                if(recv_all(conn_fd, buffer, command.arg) != command.arg) {                    shutdown = 1;                    break;                };                buffer[command.arg] = 0;                close(super_fd);                super_fd = open(buffer, O_RDWR | O_SYNC);                break;            case CLOSE:                shutdown = 1;                break;            }             if(shutdown) {                close(conn_fd);                break;            }        }    }}

部署方式是使用ndk或IDE编译后,push进recovery模式的系统中运行,同时运行adb forward tcp:4096 tcp:4096来转发端口。

windows客户端可以使用python脚本就不再写socket方面的代码了,这里用一下pwntools,需要运行pip install pwntools安装。

windows客户端代码:

from pwn import *import ext4 class RemoteStream:    def __init__(self) -> None:        self.remote = remote('127.0.0.1', 4096)     def read(self, size) -> bytes:        self.remote.send(p32(0) + p64(size))        return self.remote.recvn(size)     def seek(self, offset, whence):        self.remote.send(p32(1) + p64(offset))     def write(self, data):        self.remote.send(p32(2) + p64(len(data)))        self.remote.send(data)     def tell(self) -> int:        self.remote.send(p32(3) + p64(0))        return u64(self.remote.recvn(8))     def open(self, path):        self.remote.send(p32(4) + p64(len(path)))        self.remote.send(path)     def flush(self):        pass rfs = RemoteStream()system = ext4.Volume(rfs, offset=1024*1024)ext4.Tools.list_dir(system, system.root)

运行后输出:


如果需要修改/path/to/file,使用如下代码:

rfs = RemoteStream()system = ext4.Volume(rfs, offset=1024*1024)targetfile = system.root.get_inode("path", "to", "file").open_read()targetdata = open('editedfile', 'rb').read() # 本地读取修改后的数据# 因为是在文件系统上直接覆写文件,修改后的文件大小一定要和原文件相同targetfile.rewrite(targetdata)

该方式主要用于修改文件,浏览以及读取文件可以使用7-Zip打开system.img。


5


获取root权限


5.1 修改系统apex包


能够修改system文件之后,目标是patch adbd,然而adbd还被封装在/system/apex/com.android.adbd.apex中。

使用7-Zip拿出com.android.adbd.apex后发现可以直接解压。解压后发现其中有个文件是apex_payload.img,又是一个小型的ext4文件系统镜像,而adbd就在这个文件系统里。

借助ext4模块可以编辑apex_payload.img中的adbd文件,但是改完了要怎么把apex_payload.img放回去?重新打包的话并不能保证com.android.adbd.apex文件的大小不变,也就无法再将其放回system文件系统里了。

这时发现这个apex文件压缩方式是仅存储,也就是并没有压缩,apex_payload.img的完整数据可以直接在这个文件中找到。

因为patch adbd并不会改变该文件的大小,于是可以用python里的replace替换该文件,同时需要替换该文件的crc32。实验发现幸好android系统并不会验证在/system/apex/目录下已经安装了的apex包的签名。

5.2 patch adbd


再使用7-Zip将adbd从apex_payload.img拿出来,之后使用ida打开adbd。
 
adbd判断是否降权的依据一是调用__android_log_is_debuggable最终判断ro.debuggable,这个函数是导入函数,存在对其的包装函数,ida会将其自动识别出其名称为.__android_log_is_debuggable,使用G跳转到它,按ctrl+alt+k使用keypatch将前两行修改为:

MOV             X0, #1RET

使用keypatch前需要已经安装keystone:pip install keystone-engine不然按此快捷键不会有反应。


因为不想运行adb root留下service.adb.root属性痕迹,所以接下来还要patch掉对它的判断。

搜索对字符串service.adb.root的引用然后F5往下找找到判断逻辑,在if处按tab转回汇编代码会定位到条件跳转,为一行CBZ指令,将此CBZ改为B指令。


 
改完之后点击左上角菜单Edit->Patch program->Apply patches to input file...,如果没有备份可以选中弹窗左下角的Create backup,点击OK。
 
之后复制原apex_payload.img为apex_payload.img.bak后,将修改后的adbd放回apex_payload.img:

import ext4img = open("apex_payload.img", "rb+")imgfs = ext4.Volume(img, offset=0)target_adbd = imgfs.root.get_inode("bin", "adbd").open_read()target_adbd.rewrite(open("adbd", "rb").read())

将apex_payload.img放回com.android.adbd.apex:

import zlibfrom pwn import *apex = bytearray(open("com.android.adbd.apex", "rb").read())org_img = open("apex_payload.img.bak", "rb").read()org_crc = p32(zlib.crc32(org_img))new_img = open("apex_payload.img", "rb").read()new_crc = p32(zlib.crc32(new_img))apex = apex.replace(org_img, new_img)apex = apex.replace(org_crc, new_crc)open("com.android.adbd.apex.new", "wb").write(apex)

用com.android.adbd.apex.new覆写/system/apex/com.android.adbd.apex:

rfs = RemoteStream()system = ext4.Volume(rfs, offset=1024*1024)apex = system.root.get_inode("system", "apex", "com.android.adbd.apex").open_read()apex.rewrite(open("com.android.adbd.apex.new", "rb").read())


5.3 patch selinux


之后还需要修改selinux策略使得未降权的adbd能够正常运行。该系统版本是release版本,selinux策略中不包含对u:r:su:s0的一系列允许策略。

如果只是单纯关闭selinux,不仅会降低系统安全性,而且很容易被app发现你没有开启selinux,可能会开心地从设备上读取各种信息并送入可疑设备名单,所以这里选择patch selinux策略。

system的主要selinux策略文件在/system/etc/selinux/plat_sepolicy.cil,使用编辑器打开发现是文本文件,并且里面有大量自动生成的注释,这就在不能改变文件大小的条件下给我提供了可操作的空间,可以删掉注释并把自定义的selinux策略添加进去。

cil文件中基本的selinux策略语法为(行为 主体 客体 (类别 (属性集)))说明当主体对客体的某些类别的某些属性产生访问事件时所采取的行为。


之后问题是对u:r:su:s0制限解除的策略代码要怎么写,首先要找一些参考资
料。aosp中完整的sepolicy策略项目在https://android.googlesource.com/platform/system/sepolicy ,在该项目目录下可以找到prebuilts/api/系统api版本/public/su.te,此文件中将dontaudit改为allow后就是我们需要的制限解除代码。


然而从te到cil还需要一层编译转换。看了看文档后发现可以选择手动编译,te文件中类似调用函数的宏定义在system/sepolicy/prebuilts/api/系统api版本/public/global_macros里。

类别的属性集定义在system/sepolicy/prebuilts/api/系统api版本/private/access_vectors,有了这两个文件就可以进行手动展开。比如:

allow su self:capability_class_set *;

可以在global_macros中找到define('capability_class_set', '{ capability capability2 cap_userns cap2_userns }')展开为:

allow su self:capability *;allow su self:capability2 *;allow su self:cap_userns *;allow su self:cap2_userns *;

capability等类的属性集在access_vectors可以找到其定义:

common cap{    chown    dac_override    ...}class capabilityinherits cap

就可以继续展开并转化为cil语法:

; allow su self:capability_class_set *;(allow su self (capability (chown dac_override dac_read_search fowner fsetid kill setgid setuid setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice sys_resource sys_time sys_tty_config mknod lease audit_write audit_control setfcap)))(allow su self (capability2 (mac_override mac_admin syslog wake_alarm block_suspend audit_read)))(allow su self (cap_userns (chown dac_override dac_read_search fowner fsetid kill setgid setuid setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice sys_resource sys_time sys_tty_config mknod lease audit_write audit_control setfcap)))(allow su self (cap2_userns (mac_override mac_admin syslog wake_alarm block_suspend audit_read)))

te文件中还有一种语法是typeattribute su 名称,对应到cil中会变成(typeattributeset 名称 (类型集))在类型集中要将su添加进去。

结合这两种修改方式,可以将su.te中的基本策略展开为su.cil,并将typeattribute定义写进typeattribute.txt(见附件)。

最终写出修改plat_sepolicy.cil的代码:

import ext4from pwn import *class RemoteStream:    def __init__(self) -> None:        self.remote = remote('127.0.0.1', 4096)     def read(self, size) -> bytes:        self.remote.send(p32(0) + p64(size))        return self.remote.recvn(size)     def seek(self, offset, whence):        self.remote.send(p32(1) + p64(offset))     def write(self, data):        self.remote.send(p32(2) + p64(len(data)))        self.remote.send(data)     def tell(self) -> int:        self.remote.send(p32(3) + p64(0))        return u64(self.remote.recvn(8))     def open(self, path):        self.remote.send(p32(4) + p64(len(path)))        self.remote.send(path)     def flush(self):        pass def zipcil():    orgcil = open('plat_sepolicy.cil').readlines()    newcil = []     for line in orgcil:        line = line.strip()        if line.startswith(';'):            pass        elif len(line):            newcil.append(line.encode('latin-1'))     open('plat_sepolicy_ziped.cil', 'wb').write(b'\n'.join(newcil)) def build_newcil():    oldlen = len(open('plat_sepolicy.cil').read())    sepolicy = open('plat_sepolicy_ziped.cil').read()    typeattributes = open('typeattribute.txt').readlines()    for line in typeattributes:        attr_name = line.strip().split(' ')[-1]        pre = "typeattributeset " + attr_name + " ("        end = ")"        start = sepolicy.find(pre) + len(pre)        end = sepolicy.find(end, start)        if start == len(pre)-1:            sepolicy = sepolicy + '\n(typeattributeset ' + attr_name + " (su))"        else:            groups = sepolicy[start:end].split(' ')            if 'su' not in groups:                sepolicy = sepolicy[:start] + 'su ' + sepolicy[start:]     payload = open('su.cil').read()    sepolicy = sepolicy + '\n' + payload + '\n;'    sepolicy += ' '*(oldlen - len(sepolicy))    open('plat_sepolicy_patched.cil', 'wb').write(sepolicy.encode('latin-1')) def rwrite():    s = RemoteStream()    system = ext4.Volume(s, offset=1024*1024)    sepolicy = system.root.get_inode("system", "etc", "selinux", "plat_sepolicy.cil").open_read()    sepolicy.rewrite(open('plat_sepolicy_patched.cil', 'rb').read())zipcil()build_newcil()rwrite()

运行之后重启并进入android系统,运行adb shell:




6


将system挂载为可写


获取到root权限后发现仍然无法将system挂载为可写,一番查阅发现该系统镜像使用了EXT4_FEATURE_RO_COMPAT_SHARED_BLOCKS特性。

虽然可以通过使用RemoteStream大法强行更改ext4 superblock结构的s_feature_ro_compat去掉。

EXT4_FEATURE_RO_COMPAT_SHARED_BLOCKS(0x4000)从而可以挂载为可写,但是要删除原有文件释放空间的话仍然会出现问题。

因为该特性正如其名SHARED_BLOCKS,各个文件之间相同内容的块(ext4下块大小为4096)被合并了,在文件驱动无视该特性删除文件时,其他含有相同内容的文件也会被破坏。

可以通过如下代码去掉EXT4_FEATURE_RO_COMPAT_SHARED_BLOCKS

from pwn import *#省略 class RemoteStreamrfs = RemoteStream()rfs.seek(1024*1024 + 0x400 + 0x64)s_feature_ro_compat = u32(rfs.read(4))if s_feature_ro_compat & 0x4000:    s_feature_ro_compat = s_feature_ro_compat ^ 0x4000rfs.seek(1024*1024 + 0x400 + 0x64)rfs.write(p32(s_feature_ro_compat))


6.1 删除system内文件释放空间的方式


一种思路是去掉EXT4_FEATURE_RO_COMPAT_SHARED_BLOCKS,挂载为可写并删除文件后,利用删除前该文件的ext4信息修复该文件被释放掉的共享物理块,并重新将这些物理块其在ext4文件系统中重新标记为使用中。

在这里重点感谢一下ext4模块作者,通过print open_read打开的文件就可以看到mappedEntrys - 文件块到物理块的映射列表。

要清楚system文件系统是被一次性制作出来,并未经过删改的。可以相信如果文件没有共享块则它的映射一定是连续的一整块。

如果看见分了很多块出来,而且序号靠后的文件块反倒被映射到了序号靠前的物理块,则这些块就是需要修复的共享块。



7


安装frida-gadget


见: https://bbs.pediy.com/thread-266785.htm




END




 


看雪ID:tacesrever

https://bbs.pediy.com/user-home-888604.htm

  *本文由看雪论坛 misskings 原创,转载请注明来自看雪社区




# 往期推荐






公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存