最近研究了好几天su+Superuser的源码,感觉大概梳理通了整个大体的思路框架,mark一下。
一.su和Suepruser进行root授权的处理流程
对于su命令行程序在对来自Android应用的Root权限请求处理流程大致如下图所示(因为快要找工作了,为了节约时间花了一副丑到哭的图片):
图中Android应用是申请Root权限的申请者,su命令行程序时Root权限拥有者,因su设置了suid位,因此任何执行它的进程都会获得和它一样的权限,这个好像在之前的文章中提到过,其实细节上并非这么容易,和Superuser用户权限管理apk结合起来使用的话还会有一些仲裁的过程。
描述起来就是:
1.Android应用调用su程序,来申请root权限;
2.su启动LocalSocket服务,LocalSocket的功能和通常我们进程通信的Socket类似,其实是在本地共享一块内存来实现和本地其他进程之间进行通信的Socket服务;
3.su命令行程序通过am命令请求显示Superuser应用的RequestActivity窗口,这个窗口里面显示哪个uid的和user的应用申请权限;
4.Superuser连接到由su进程建立的LocalSocket服务上,至此LocalSocket连接成功,之后进程和Superuser对应的进程可以通过这个LocalSocket来进行信息传递了;
5.LocalSocket的数据通道成功连接库,su程序通过socket传递调用者(申请root权限的Android应用)的一些信息。
6.Superuser将用户的仲裁结果数据返回给su程序,如果用户允许授权则,则 ALLOW root授权,反之DENY。(其中还有用户在一定时间内未作出选择的情况,默认为DENY)
这是从Android应用申请root权限到su和Superuser配合实现用户选择后的仲裁的整个过程。
下面我一步一步分析一下这些过程中的细节
二:从su的main函数开始,分析细节
因为su是一个linux命令行程序,故我们首先在Superuser的源码的jni目录下找到su.c文件,定位到main函数处:
main函数中主要完成了三个主要的工作:
1.初始化调用者数据和效验
| 1)获取调用者调用su命令的命令行参数-from_init函数
| 2)获取su命令的链接路径-user_init函数
| 3)获取调用者的名称
2.通过SQLlite数据库检查申请“Root授权”的Android应用程序是否还需要进一步效验
3.建立LocalSocket服务,并进行相应的数据通信
由于代码比较长,而且联系紧密因此在源码中我对相应的部分进行了注释,建议下载后面我给出的注释后的源码来对照理解,如果你懒得下,没关系,相关的长长的代码我给你贴上老 ~~
int main(int argc, char *argv[]) { // Sanitize all secure environment variables (from linker_environ.c in AOSP linker). /* The same list than GLibc at this point */ static const char* const unsec_vars[] = { "GCONV_PATH", "GETCONF_DIR", "HOSTALIASES", "LD_AUDIT", "LD_DEBUG", "LD_DEBUG_OUTPUT", "LD_DYNAMIC_WEAK", "LD_LIBRARY_PATH", "LD_ORIGIN_PATH", "LD_PRELOAD", "LD_PROFILE", "LD_SHOW_AUXV", "LD_USE_LOAD_BIAS", "LOCALDOMAIN", "LOCPATH", "MALLOC_TRACE", "MALLOC_CHECK_", "NIS_PATH", "NLSPATH", "RESOLV_HOST_CONF", "RES_OPTIONS", "TMPDIR", "TZDIR", "LD_AOUT_LIBRARY_PATH", "LD_AOUT_PRELOAD", // not listed in linker, used due to system() call "IFS", }; const char* const* cp = unsec_vars; const char* const* endp = cp + sizeof(unsec_vars)/sizeof(unsec_vars[0]); while (cp < endp) { unsetenv(*cp); cp++; } /* * set LD_LIBRARY_PATH if the linker has wiped out it due to we're suid. * This occurs on Android 4.0+ */ setenv("LD_LIBRARY_PATH", "/vendor/lib:/system/lib", 0); LOGD("su invoked."); // 第一阶段主要是进行一些初始化和效验 // stx 这个结构体定义了三个成员:from、to和user 表示su调用的上下文 struct su_context ctx = { .from = { .pid = -1, .uid = 0, .bin = "", .args = "", .name = "", }, .to = { .uid = AID_ROOT, .login = 0, .keepenv = 0, .shell = NULL, .command = NULL, .argv = argv, .argc = argc, .optind = 0, .name = "", }, .user = { .android_user_id = 0, .multiuser_mode = MULTIUSER_MODE_OWNER_ONLY, .database_path = REQUESTOR_DATA_PATH REQUESTOR_DATABASE_PATH, .base_path = REQUESTOR_DATA_PATH REQUESTOR }, }; struct stat st; int c, socket_serv_fd, fd;//LocalSocket的句柄 char buf[64], *result; policy_t dballow; struct option long_opts[] = { { "command", required_argument, NULL, 'c' }, { "help", no_argument, NULL, 'h' }, { "login", no_argument, NULL, 'l' }, { "preserve-environment", no_argument, NULL, 'p' }, { "shell", required_argument, NULL, 's' }, { "version", no_argument, NULL, 'v' }, { NULL, 0, NULL, 0 }, }; while ((c = getopt_long(argc, argv, "+c:hlmps:Vvu", long_opts, NULL)) != -1) { switch(c) { case 'c': ctx.to.shell = DEFAULT_SHELL; ctx.to.command = optarg; break; case 'h': usage(EXIT_SUCCESS); break; case 'l': ctx.to.login = 1; break; case 'm': case 'p': ctx.to.keepenv = 1; break; case 's': ctx.to.shell = optarg; break; case 'V': printf("%d\n", VERSION_CODE); exit(EXIT_SUCCESS); case 'v': printf("%s\n", VERSION); exit(EXIT_SUCCESS); case 'u': switch (get_multiuser_mode()) { case MULTIUSER_MODE_USER: printf("%s\n", MULTIUSER_VALUE_USER); break; case MULTIUSER_MODE_OWNER_MANAGED: printf("%s\n", MULTIUSER_VALUE_OWNER_MANAGED); break; case MULTIUSER_MODE_OWNER_ONLY: printf("%s\n", MULTIUSER_VALUE_OWNER_ONLY); break; case MULTIUSER_MODE_NONE: printf("%s\n", MULTIUSER_VALUE_NONE); break; } exit(EXIT_SUCCESS); default: /* Bionic getopt_long doesn't terminate its error output by newline */ fprintf(stderr, "\n"); usage(2); } } if (optind < argc && !strcmp(argv[optind], "-")) { ctx.to.login = 1; optind++; } /* username or uid */ if (optind < argc && strcmp(argv[optind], "--")) { struct passwd *pw; pw = getpwnam(argv[optind]); if (!pw) { char *endptr; /* It seems we shouldn't do this at all */ errno = 0; ctx.to.uid = strtoul(argv[optind], &endptr, 10); if (errno || *endptr) { LOGE("Unknown id: %s\n", argv[optind]); fprintf(stderr, "Unknown id: %s\n", argv[optind]); exit(EXIT_FAILURE); } } else { ctx.to.uid = pw->pw_uid; if (pw->pw_name) strncpy(ctx.to.name, pw->pw_name, sizeof(ctx.to.name)); } optind++; } if (optind < argc && !strcmp(argv[optind], "--")) { optind++; } ctx.to.optind = optind; su_ctx = &ctx; // 初始化from调用者的信息,主要是调用者的用户ID if (from_init(&ctx.from) < 0) { deny(&ctx); } read_options(&ctx); user_init(&ctx); // the latter two are necessary for stock ROMs like note 2 which do dumb things with su, or crash otherwise if (ctx.from.uid == AID_ROOT) {//如果Android应用已经是root权限,就直接允许其获取root权限 LOGD("Allowing root/system/radio."); allow(&ctx); } // 校验superuser是否安装。 // verify superuser is installed if (stat(ctx.user.base_path, &st) < 0) { // send to market (disabled, because people are and think this is hijacking their su) // if (0 == strcmp(JAVA_PACKAGE_NAME, REQUESTOR)) // silent_run("am start -d http://www.clockworkmod.com/superuser/install.html -a android.intent.action.VIEW"); PLOGE("stat %s", ctx.user.base_path); deny(&ctx); } // always allow if this is the superuser uid // superuser needs to be able to reenable itself when disabled... if (ctx.from.uid == st.st_uid) {//如果调用者就是Superuser,那么直接授予root权限。 allow(&ctx); } // check if superuser is disabled completely if (access_disabled(&ctx.from)) { LOGD("access_disabled"); deny(&ctx); } // autogrant shell at this point if (ctx.from.uid == AID_SHELL) { LOGD("Allowing shell."); allow(&ctx); } // deny if this is a non owner request and owner mode only if (ctx.user.multiuser_mode == MULTIUSER_MODE_OWNER_ONLY && ctx.user.android_user_id != 0) { deny(&ctx); } ctx.umask = umask(027); // 在/dev目录下创建一个用于LocalSocket缓存的目录,LocalSocket实际通过内存来传递数据 int ret = mkdir(REQUESTOR_CACHE_PATH, 0770); if (chown(REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid)) { PLOGE("chown (%s, %ld, %ld)", REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid); deny(&ctx); } if (setgroups(0, NULL)) { PLOGE("setgroups"); deny(&ctx); } if (setegid(st.st_gid)) { PLOGE("setegid (%lu)", st.st_gid); deny(&ctx); } if (seteuid(st.st_uid)) { PLOGE("seteuid (%lu)", st.st_uid); deny(&ctx); } // 第二阶段:检查申请“Root授权”的Android应用程序是否还需要进一步效验 dballow = database_check(&ctx);//核对数据库,如果数据库中已经显示授予Root权限就直接授予,拒绝过就直接拒绝 //database_check文件是在db.c文件中实现的 switch (dballow) { case INTERACTIVE: break; case ALLOW: LOGD("db allowed"); allow(&ctx); /* never returns */ case DENY: default: LOGD("db denied"); deny(&ctx); /* never returns too */ } // 第三阶段:建立LocalSocket服务,并进行相应的数据通信 socket_serv_fd = socket_create_temp(ctx.sock_path, sizeof(ctx.sock_path));//根据dev下的路径创建LocalSocket服务 LOGD(ctx.sock_path); if (socket_serv_fd < 0) {//如果LocalSocket创建失败就直接拒绝授权 deny(&ctx); } signal(SIGHUP, cleanup_signal); signal(SIGPIPE, cleanup_signal); signal(SIGTERM, cleanup_signal); signal(SIGQUIT, cleanup_signal); signal(SIGINT, cleanup_signal); signal(SIGABRT, cleanup_signal); if (send_request(&ctx) < 0) { //通过am命令向Superuser发送命令,请求显示RequestActivity窗口 deny(&ctx); } atexit(cleanup); fd = socket_accept(socket_serv_fd);//等待Superuser的连接 //一点Superuser已经连接到su命令建立的LocalSocket上,数据传输就不必再使用am命令了,直接通过数据流传输即可 if (fd < 0) { deny(&ctx); } if (socket_send_request(fd, &ctx)) {//连接成功后,通过已连接的Socket向Superuser发送调用者的信息 deny(&ctx); } if (socket_receive_result(fd, buf, sizeof(buf))) {//接收用户在Superuser的选择窗口中进行的授权选择 deny(&ctx); } close(fd); close(socket_serv_fd); socket_cleanup(&ctx); result = buf; #define SOCKET_RESPONSE "socket:" // socket:ALLOW if (strncmp(result, SOCKET_RESPONSE, sizeof(SOCKET_RESPONSE) - 1)) LOGW("SECURITY RISK: Requestor still receives credentials in intent"); else result += sizeof(SOCKET_RESPONSE) - 1;//让result指针指向“Socket:”后面的数据 if (!strcmp(result, "DENY")) { deny(&ctx); } else if (!strcmp(result, "ALLOW")) { allow(&ctx); } else { LOGE("unknown response from Superuser Requestor: %s", result); deny(&ctx); } } /*到现在为止,我们已经了解了su命令和Superuser在授权root权限时,su端的工作原理, 但是su.c文件中的两个函数allow()和deny()的具体实现我们还没有看到,这两个函数才是 最终允许或拒绝root权限授权的关键/*这里面主要涉及到两个初始化函数from_init和user_init,以及一个描述su调用上下文的结构体su_context。
main函数中首先进行的是调用者信息的初始化工作即from_init函数的功能
static int from_init(struct su_initiator *from) { char path[PATH_MAX], exe[PATH_MAX]; char args[4096], *argv0, *argv_rest;//argv_rest指向真正的su命令行参数的信息 int fd; ssize_t len; int i; int err; from->uid = getuid();//获取实际的用户ID,用于标识Android App(调用者) from->pid = getppid();//获取调用进程的父进程ID /* Get the command line */ snprintf(path, sizeof(path), "/proc/%u/cmdline", from->pid);//获得su调用时的命令行参数 fd = open(path, O_RDONLY); if (fd < 0) { PLOGE("Opening command line"); return -1; } len = read(fd, args, sizeof(args)); err = errno; close(fd); if (len < 0 || len == sizeof(args)) { PLOGEV("Reading command line", err); return -1; } // su \0 a \0 b \0 c // su \0 a ' ' b ' ' c \0 (\0表示字符串结束) argv0 = args; // 指向了第一个命令行参数,也就是su argv_rest = NULL; for (i = 0; i < len; i++) { //len表示总体的命令行长度 if (args[i] == '\0') {//从第一个 \0开始,获取命令行参数到argv_rest if (!argv_rest) { argv_rest = &args[i+1]; } else { args[i] = ' '; } } } args[len] = '\0'; if (argv_rest) { strncpy(from->args, argv_rest, sizeof(from->args));//将命令行参数放入调用者信息from->args中 from->args[sizeof(from->args)-1] = '\0'; } else { from->args[0] = '\0'; } /* If this isn't app_process, use the real path instead of argv[0] */ snprintf(path, sizeof(path), "/proc/%u/exe", from->pid); len = readlink(path, exe, sizeof(exe)); if (len < 0) { PLOGE("Getting exe path"); return -1; } exe[len] = '\0'; if (strcmp(exe, "/system/bin/app_process")) { argv0 = exe; } strncpy(from->bin, argv0, sizeof(from->bin)); from->bin[sizeof(from->bin)-1] = '\0'; struct passwd *pw; pw = getpwuid(from->uid); if (pw && pw->pw_name) { strncpy(from->name, pw->pw_name, sizeof(from->name)); } return 0; }其中包括获得调用者执行su时的命令行参数uid等操作。对照注释看吧。
main函数中第二个主要完成的工作就是:检查申请“Root”授权的Android应用程序是否还需要进一步的效验。然后在本地SQLite数据库中进行查询,如果数据库中显示授予root权限就直接
授予root权限,拒绝过就直接拒绝,如果没有记录就进入到下一阶段,和Superuser进行通信,然后Superuser接收到用户的选择后将选择数据传回到su程序中来决定是否授予新申请root权限的Android应用Roo权限。
main中完成的第三个主要的主要工作就是建立本地LocalSocket服务,等待Superuser的连接,当连接成功后传递调用者信息即初始化后的su_context结构体,Superuser在收到调用者的信息后显示出来让用户仲裁是否授予权限,最后将用户选择回传到su程序,su根据结果来执行deny()或者allow()函数。
三:允许或拒绝root权限授权的关键allow()函数和deny()函数
到现在为止,我们已经了解了su命令和Superuser在授权root权限时,su端的工作原理,但是su.c文件中的两个函数allow()和deny()的具体实现我们还没有看到,这两个函数才是
最终允许或拒绝root权限授权的关键:
static __attribute__ ((noreturn)) void deny(struct su_context *ctx) { char *cmd = get_command(&ctx->to); int send_to_app = 1; // no need to log if called by root if (ctx->from.uid == AID_ROOT) send_to_app = 0; // dumpstate (which logs to logcat/shell) will spam the crap out of the system with su calls if (strcmp("/system/bin/dumpstate", ctx->from.bin) == 0) send_to_app = 0; if (send_to_app) send_result(ctx, DENY); LOGW("request rejected (%u->%u %s)", ctx->from.uid, ctx->to.uid, cmd); fprintf(stderr, "%s\n", strerror(EACCES)); exit(EXIT_FAILURE); } static __attribute__ ((noreturn)) void allow(struct su_context *ctx) { char *arg0; int argc, err; umask(ctx->umask); int send_to_app = 1; // no need to log if called by root if (ctx->from.uid == AID_ROOT) send_to_app = 0; // dumpstate (which logs to logcat/shell) will spam the crap out of the system with su calls if (strcmp("/system/bin/dumpstate", ctx->from.bin) == 0) send_to_app = 0; if (send_to_app) send_result(ctx, ALLOW);//向Superuser回发信息,表明授权成功,这个函数在activity.c中。 //在socket连接关闭时,是通过am命令传递信息到Superuser的。 char *binary; argc = ctx->to.optind; if (ctx->to.command) { binary = ctx->to.shell; ctx->to.argv[--argc] = ctx->to.command; ctx->to.argv[--argc] = "-c"; } else if (ctx->to.shell) { binary = ctx->to.shell; } else { if (ctx->to.argv[argc]) { binary = ctx->to.argv[argc++]; } else { binary = DEFAULT_SHELL; } } arg0 = strrchr (binary, '/'); arg0 = (arg0) ? arg0 + 1 : binary; if (ctx->to.login) { int s = strlen(arg0) + 2; char *p = malloc(s); if (!p) exit(EXIT_FAILURE); *p = '-'; strcpy(p + 1, arg0); arg0 = p; } populate_environment(ctx); set_identity(ctx->to.uid); #define PARG(arg) \ (argc + (arg) < ctx->to.argc) ? " " : "", \ (argc + (arg) < ctx->to.argc) ? ctx->to.argv[argc + (arg)] : "" LOGD("%u %s executing %u %s using binary %s : %s%s%s%s%s%s%s%s%s%s%s%s%s%s", ctx->from.uid, ctx->from.bin, ctx->to.uid, get_command(&ctx->to), binary, arg0, PARG(0), PARG(1), PARG(2), PARG(3), PARG(4), PARG(5), (ctx->to.optind + 6 < ctx->to.argc) ? " ..." : ""); ctx->to.argv[--argc] = arg0; execvp(binary, ctx->to.argv + argc); err = errno; PLOGE("exec"); fprintf(stderr, "Cannot execute %s: %s\n", binary, strerror(err)); exit(EXIT_FAILURE); }
execvp(binary, ctx->to.argv + argc);//这一句才是前面各种效验仲裁的允许获得root权限的最终核心的执行代码。我在这儿就不一行一行的讲解了,其实有的细节部分要结合其他文件中的内容进行理解,大家有兴趣的话可以阅读源码。我有时间进行更深刻的理解的话,会持续更新的。也欢迎大家和我交流。
Superuser源码下载链接:http://download.csdn.net/detail/koozxcv/9487931