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
