目标是在已解锁的android11手机上,通过修改原有系统的方式,以尽量少的痕迹获取(adb)root权限并全局装入frida-gadget。
关于为什么不用magisk或其他框架,因为感觉magisk的解决方案对我来说功能过于冗余,并且重视安全的应用都会试图通过各种姿势检测magisk或其他框架以及root (在某些群天天看到群友问某某应用检测root 怎么办) 一想到有可能会遇到有关magisk的兼容性问题,还要去定位解决,就头大 (也是还没用过magisk)
之前了解过adb root的原理:adbd服务最初以root用户的权限运行,读取判断一些系统配置后会自行降权,切换至shell用户。直接修改系统配置的值虽然操作不难,但是可以被检测到,在某些系统上还会导致log增多影响性能,所以想法是patch系统上的adbd文件使其不会执行降权。
手机的系统发布商向公众提供了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文件夹里。
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找到
在repositories中添加三行
将解包出的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连接手机依次运行
之后在recovery模式下运行adb shell
,提示符已经变为#表示为root权限。
该系统使用虚拟A/B的方式升级,system文件系统在/dev/block/byname/super
中偏移为1024*1024的位置处。为了有效率地在windows下直接编辑android系统中未挂载的ext4文件系统而不用重新刷写,找到了 https://github.com/cubinator/ext4 并添加了覆写文件的方法,fork并修改后的项目见 https://github.com/tacesrever/ext4 ,并编写了一个简陋的socket程序用于转发文件读写操作。
服务器端:
部署方式是使用ndk或IDE编译后,push进recovery模式的系统中运行,同时运行adb forward tcp:4096 tcp:4096
来转发端口。
windows客户端可以使用python脚本就不再写socket方面的代码了,这里用一下pwntools,需要运行pip install pwntools
安装
windows客户端代码:
运行后输出
如果需要修改/path/to/file
,使用如下代码
该方式主要用于修改文件,浏览以及读取文件可以使用7-Zip打开system.img。
能够修改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包的签名。
再使用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将前两行修改为
使用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:
将apex_payload.img放回com.android.adbd.apex:
用com.android.adbd.apex.new覆写/system/apex/com.android.adbd.apex:
之后还需要修改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
,有了这两个文件就可以进行手动展开。比如:
可以在global_macros中找到define('capability_class_set', '{ capability capability2 cap_userns cap2_userns }')
展开为
capability
等类的属性集在access_vectors可以找到其定义
就可以继续展开并转化为cil语法
te文件中还有一种语法是typeattribute su 名称
,对应到cil中会变成(typeattributeset 名称 (类型集))
在类型集中要将su添加进去。
结合这两种修改方式,可以将su.te中的基本策略展开为su.cil,并将typeattribute
定义写进typeattribute.txt(见附件)
最终写出修改plat_sepolicy.cil的代码:
运行之后重启并进入android系统,运行adb shell:
获取到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并将系统挂载为可写:
一种思路是去掉EXT4_FEATURE_RO_COMPAT_SHARED_BLOCKS,挂载为可写并删除文件后,利用删除前该文件的ext4信息修复该文件被释放掉的共享物理块,并重新将这些物理块其在ext4文件系统中重新标记为使用中。
在这里重点感谢一下ext4模块作者,通过print open_read打开的文件就可以看到mappedEntrys - 文件块到物理块的映射列表。
通过计算每个物理块的引用计数,可以知道哪些是共享物理块。在给ext4模块添加了操作bitmap的代码后,写出完整的删除并修复共享块的代码如下
见 https://bbs.pediy.com/thread-266785.htm (已更新)
buildscript {
repositories {
mavenCentral()
}
...
}
buildscript {
repositories {
mavenCentral()
}
...
}
buildscript {
repositories {
maven {
setUrl(
"http://maven.aliyun.com/nexus/content/groups/public/"
)
}
mavenCentral()
}
...
}
buildscript {
repositories {
maven {
setUrl(
"http://maven.aliyun.com/nexus/content/groups/public/"
)
}
mavenCentral()
}
...
}
adb reboot bootloader
fastboot flash
-
-
disable
-
verity
-
-
disable
-
verification vbmeta vbmeta.img.signed
fastboot flash boot boot.img.signed
fastboot reboot recovery
adb reboot bootloader
fastboot flash
-
-
disable
-
verity
-
-
disable
-
verification vbmeta vbmeta.img.signed
fastboot flash boot boot.img.signed
fastboot reboot recovery
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
;
}
}
}
}
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
;
}
}
}
}
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)
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)
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)
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)
MOV X0,
RET
import
ext4
img
=
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())
import
ext4
img
=
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())
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2021-7-13 07:12
被tacesrever编辑
,原因: 更新一下下