superuser root 原理详细分析

时间:2022-02-15 06:30:54

Root 方法:
通过fastboot 刷入指定的recovery.img, 替换了系统原生的recovery, 进入recovery,刷入root相关文件,以达到root目的。

目前市面常见的 root 管理工具为:
supersu、superuser、kingroot 等,因为 supersu、 kingroot 不开源, 逆向分析其底层实现 较为困难,所以本次是以 开源项目 superuser 进行分析的。

Root 原理:
root 的主要原理,是将apk层传入的本应放在shell进程中执行的命令,放到daemonsu 创建 进程sush中执行。 其中Daemonsu 为开机时启动的su 守护进程(user为root)。
这个过程最重要的为:apk、su、daemonsu、sush、superuser 之间的通信。
通信过程大概为:
1、三方进程调用su,su 通过socket 与 daemonsu 通信,
2、daemonsu 创建sush,
3、sush 通过 am 启动 superuser apk ,让用户选择是否授予其root权限。
4、superuser 通过socket 告知 sush 用户选择的结果
5、sush 根据 apk 传过来的结果,选择继续执行或中断执行
大概为

apk <———-> su ———-> daemonsu———>sush <———->superuser

apk<———->su: 三方进程通过Runtime 与process 与 su进程通信
su —————–> daemonsu: su 进程与 daemonsu 通过socket 进行通信,daemonsu 进程会创建sush,进行下一步操作。
sush <——–>superuser : sush 进程通过am 启动 superuser apk,让用户选择是否允许授予root权限,之后superuser 将 用户选择的结果,通过 socket 告知 sush,如果apk进程被允许的话,sush 执行 apk的命令,否则中断执行。

详细代码的流程为:
daemonsu 的启动:
daemonsu 用于创建三方apk 执行命令的root进程。通过刷机添加或修改系统的init.rc 文件以达开机启动的目的,superuser 是刷入了一个init.superuser.rc 文件,在开机时执行:
service su_daemon /system/xbin/su --daemon
命令来自启,此命令会走到su的main 方法中:

int main(int argc, char *argv[]) {                           
return su_main(argc, argv, 1);
}
int su_main(int argc, char *argv[], int need_client) {
if (argc == 2 && strcmp(argv[1], "--daemon") == 0) {
return run_daemon();
}
………….

然后会调用到daemon.c中:

 int run_daemon() {
int fd;
struct sockaddr_un sun;

fd = socket(AF_LOCAL, SOCK_STREAM, 0);

if (bind(fd, (struct sockaddr*)&sun, sizeof(sun)) < 0) {
PLOGE("daemon bind");
goto err;
}

if (listen(fd, 10) < 0) {
PLOGE("daemon listen");
goto err;
} while ((client = accept(fd, NULL, NULL)) > 0) {
if (fork_zero_fucks() == 0) {
close(fd);
return daemon_accept(client);
}
。。。。。。。。。。

此过程是创建一个socket,等待和申请root 的进程进行通信,

apk <———-> su 通信的流程为:
这个流程较为简单,通过Runtime与Process 即可:

 Process process = Runtime.getRuntime().exec("su");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
DataOutputStream outPutStream = new DataOutputStream(process.getOutputStream());

之后通过 bufferedReader 获取底层的执行结果, 通过 outPutStream将命令传入底层执行。

su —————-> daemonsu
当apk进程调用su 来获取root时,会走到su的main方法中,对应的代码为:

int main(int argc, char *argv[]) { 
return su_main(argc, argv, 1);
}
int su_main(int argc, char *argv[], int need_client) {
。。。。。。。
if (need_client) {
if ((geteuid() != AID_ROOT && getuid() != AID_ROOT) ||
(get_api_version() >= 18 && getuid() == AID_SHELL) ||
get_api_version() >= 19) {
return connect_daemon(argc, argv, ppid);
}
}
。。。。。。。

main调用 su_main,并传入 1, 那么 肯定会走到 if分支,然后调用 connect_daemon去连接daemonsu进程,并将 命令 与ppid 传入。

int connect_daemon(int argc, char *argv[], int ppid) {

struct sockaddr_un sun;
int socketfd = socket(AF_LOCAL, SOCK_STREAM, 0);
memset(&sun, 0, sizeof(sun));
sun.sun_family = AF_LOCAL;
sprintf(sun.sun_path, "%s/server", REQUESTOR_DAEMON_PATH);

memset(sun.sun_path, 0, sizeof(sun.sun_path));
memcpy(sun.sun_path, "\0" "SUPERUSER", strlen("SUPERUSER") + 1);

if (0 != connect(socketfd, (struct sockaddr*)&sun, sizeof(sun)))
PLOGE("connect");
exit(-1);
}
write_int(socketfd, getpid());
write_string(socketfd, pts_slave);
write_int(socketfd, uid);
write_int(socketfd, ppid);
。。。。。。
int code = read_int(socketfd);
close(socketfd);
LOGD("client exited %d", code);

return code;
}

在此方法中,主要是穿件创建socket并连接到daemonsu,将当前的数据写入,等待对端完成。su与daemonsu 通信所使用的 sockaddr 的sun.sun_path 是相同的。

daemonsu———>sush
su通过socket 与daemonsu 通信,daemonsu在接收到连接后,会fork出一个sush子进程进行后续操作,父进程在子进程完成后,向socket 写入数据,通知一下 对端(su),然后等待下一个连接的到来:

int run_daemon() {
。。。。。。。。。 while ((client = accept(fd, NULL, NULL)) > 0) {
if (fork_zero_fucks() == 0) {
close(fd);
return daemon_accept(client);
}
。。。。。。。。。。

在daemonsu接受到root申请调用 daemon_accept,在此方法中,会fork出 sush,

static int daemon_accept(int fd) {

is_daemon = 1;
int pid = read_int(fd);
char *pts_slave = read_string(fd);
daemon_from_uid = read_int(fd);
daemon_from_pid = read_int(fd);

int child = fork();

if (child != 0) {
if (write(fd, &code, sizeof(int)) != sizeof(int)) {
PLOGE("unable to write exit code");
}
。。。。。。
return code;
}
return run_daemon_child(infd, outfd, errfd, argc, argv);

到此将来用于执行命令的进程sush 就创建完毕了。接下来是 sush 与上层apk superuser的通信,来让用户确认是否授予 申请者 root权限。

sush <———->superuser
sush 是一个底层的进程,与superuser 没有直接的关系,要与上层的superuser apk通信这里是使用的是am 命令,并在调用am命令时,将sush的sock传入,以便达到sush 与superuser的双向通信。ps: supersu 也是使用am 命令,
之前fork出子进程sush,并调用 了run_daemon_child

static int run_daemon_child(int infd, int outfd, int errfd, int argc, char** argv) {
。。。。。
return su_main(argc, argv, 0);
}

注意此处又走到su 里面的su_main方法,并且传入的值为0,

因为本次need_client 传入的为0,那么 将跳过if( need_client)分支,直接往下走,往下会调用到 database_check来检查 superuser 数据库中是否允许 apk进程申请root,会出现桑格返回值:

INTERACTIVE:交互式,需要用户确认是否授予权限,返回此值,会继续执行
ALLOW:允许apk进程申请root,返回此值,会调用 allow, 执行apk的命令
DENY: 不允许apk进程申请root,返回此值,会调用 deny,收尾一下,然后结束。

int su_main(int argc, char *argv[], int need_client) {
。。。。
if (need_client) {
。。。。
}
。。。。
dballow = database_check(&ctx);
switch (dballow) {
case INTERACTIVE:
break;
case ALLOW:
allow(&ctx);
case DENY:
default:
deny(&ctx);
}

本次要说的是 INTERACTIVE 型, 如果是此类型,会创建一个socket ,并调用send_request方法使用am命令去建立与上层superuser的 socket联系,之后上层通过socket 获取到 ,请求 root进程的相关信息,然后弹窗等待上层 用户的选择,然后通过 socket获取,结果,根据结果去选择 allow 或deny

     socket_serv_fd = socket_create_temp(ctx.sock_path, sizeof(ctx.sock_path));                                                                     
if (send_request(&ctx) < 0) {
deny(&ctx);
}
//等待上层建立socket 连接
fd = socket_accept(socket_serv_fd);
if (fd < 0) {
deny(&ctx);
}
//将请求root进程的进程信息,通过socket 告诉superuser.apk
if (socket_send_request(fd, &ctx)) {
deny(&ctx);
}
//获取superuser.apk 中用户的选择结果
if (socket_receive_result(fd, buf, sizeof(buf))) {
deny(&ctx);
}
//根据结果 选择是否执行
if (!strcmp(result, "DENY")) {
deny(&ctx);
} else if (!strcmp(result, "ALLOW")) {
allow(&ctx);
} else {
deny(&ctx);
}

send_request 方法中使用am 与上层建立socket 联系的代码为:

int send_request(struct su_context *ctx) {
char *request_command[] = {
AM_PATH,
ACTION_REQUEST,
"--es",
"socket",
ctx->sock_path,
user[0] ? "--user" : NULL,
user,
NULL
};
return silent_run(request_command);
}
在此方法中拼凑出要执行的命令  request_command,在此处可以看到 通过 am的—es 参数,将soket 传入,上层接受时,会获取此soket,以此达到了,底层与上层的soket 通信,其中 AM_PATH与 ACTION_REQUEST的值为:
#define AM_PATH "/system/bin/app_process", "/system/bin", "com.android.commands.am.Am"
#define ACTION_REQUEST "start", "-n", REQUESTOR "/" REQUESTOR_PREFIX ".RequestActivity"

slient_run 方法中比较简单,fork 出一个子进程,执行am命令,父进程返回0。
到此就会走到上层apk superuser 的 RequestActivity.java类中, 他做的操作也比较简单,就是通过intent 获取传过来的socket,使用socket 获取 底层传过来的 请求root 进程的信息,弹窗让用户选择,然后将用户的选择结果,写入数据库,并通过socket 回传给sush。

socket_send_request:底层sush 将请求root 进程的相关信息通过socket 传输给superuser.apk
socket_receive_result: 获取用户选择的结果
allow: 执行用户指定的命令,并收尾
deny: 不执行,直接收尾。

Ps:
用户申请到root 权限后,仅仅代表 他可以切换到root 去执行一些 操作,
如果不去切换,依然没有权限执行例如,在同样申请到root的情况下:

先通过su,切换到root,然后执行后续dumpsys power命令,执行成功,可以读取到有效信息

Process process = Runtime.getRuntime().exec("su");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
DataOutputStream outPutStream = new DataOutputStream(process.getOutputStream());
outPutStream.writeBytes("dumpsys power\n");
outPutStream.flush();
String line = bufferedReader.readLine();

即使在apk,已经被授权root 的情况下,直接执行 dumpsys power 命令,依然是执行失败的 (因为没有切换到root,相当于没有授权的情况去执行),
读取到的信息为: Permission Denial: can’t dump PowerManager from from pid****

Process process = Runtime.getRuntime().exec("dumpsys power");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = bufferedReader.readLine();

遗留疑问: 请求root 的apk进程,后续 通过 process 怎么与 sush 进行通信 执行之后命令的,还需要继续查一下。

process.java 与sush 通信的 的原理参考:
堪称经典 的pipe fork exec 的理解和综合使用
http://blog.chinaunix.net/uid-20395453-id-3264826.html

superuser 的源码为:https://github.com/koush/Superuser