美文网首页
android su 授权分析

android su 授权分析

作者: 夏日之暮 | 来源:发表于2020-05-30 23:25 被阅读0次

    root 原理

    root的本质就是在系统中植入一个带s位的,owner 用户为root,并且普通用户有可执行权限的 su 文件:

    chiron:/ $ ll /system/xbin/su
    -rwsr-x--x 1 root shell 1370576 2019-06-19 13:32 /system/xbin/su
    
    • 带 s 位是为了让任何用户执行 su 起来的进程的uid 是 root;

    • 一般 root 工具把 su 植入后,还有个 app 来管理授权,当有app执行 su 尝试取得 root 权限时, su 会询问 app 是否给与授权。

    • 接下来我们分别来分析 su 这个可执行文件,然后再看 su 是怎么和权限管理 app 交互的。

    su

    可执行文件 /system/xbin/su 有2种启动模式

    • 第一种: daemon 模式启动,通过 init的配置启动 ,起来后监听客户端发送过来的请求:

      external/koush/Superuser/init.superuser.rc

      # su daemon
      service su_daemon /system/xbin/su --daemon
          seclabel u:r:su:s0
          oneshot
      
      int main(int argc, char *argv[]) {
          return su_main(argc, argv, 1);
      }
      
      int su_main(int argc, char *argv[], int need_client) {
           // start up in daemon mode if prompted
          if (argc == 2 && strcmp(argv[1], "--daemon") == 0) {
              return run_daemon();
          }
          ...
      }
      
      int run_daemon() {
          ...
          if (bind(fd, (struct sockaddr*)&sun, sizeof(sun)) < 0) {
              PLOGE("daemon bind");
              goto err;
          }
          
          if (listen(fd, 10) < 0) {
              PLOGE("daemon listen");
              goto err;
          }
              
          int client;
          while ((client = accept(fd, NULL, NULL)) > 0) {
              if (fork_zero_fucks() == 0) { // fork 一个孙进程并让 init 领养
                  close(fd);
                  return daemon_accept(client); // 被 init 领养的孙进程
              } else {
                  close(client);
              }
          }
          ...
      }
      
      
    • 另外一种是客户端模式启动,也就是其他app要请求 root 权限时执行 su 这种场景。 这种启动模式,启动后解析参数, 把参数发送给 daemon

      int su_main(int argc, char *argv[], int need_client) {
          // start up in daemon mode if prompted
          if (argc == 2 && strcmp(argv[1], "--daemon") == 0) {
              return run_daemon();
          }
          ...//参数解析部分省略
          if (need_client) {
              // attempt to use the daemon client if not root,
              // or this is api 18 and adb shell (/data is not readable even as root)
              // or just always use it on API 19+ (ART)
              if ((geteuid() != AID_ROOT && getuid() != AID_ROOT) ||
                  (get_api_version() >= 18 && getuid() == AID_SHELL) ||
                  get_api_version() >= 19) {
                  // attempt to connect to daemon...
                  LOGD("starting daemon client %d %d", getuid(), geteuid());
                  return connect_daemon(argc, argv, ppid);
              }
          }
          ...
      }
      

    授权过程

    • 客户端执行 su 后,会把请求参数通过 socket 发送给 daemon , 发送的内容除了参数外,还发送了 STDIN_FILENO STDOUT_FILENO STDERR_FILENO 这些标准输入输出文件句柄,这样 daemon 这边代理执行目标命令时,能够把结果输送回客户端进程;

    • su --daemon 收到请求后,会 fork 一个孙进程让 init 进程领养,这个孙进程解析请求参数后,再 fork 一个子进程来最终执行目标命令。

      static int daemon_accept(int fd) {
          .../读取并解析参数
          write_int(fd, 1); //ack
          int child = fork();
          if (child != 0) {
              ...
              return code;
          }
          
          close (fd);
      
          // Become session leader
          if (setsid() == (pid_t) -1) {
              PLOGE("setsid");
          }
          ...
          return run_daemon_child(infd, outfd, errfd, argc, argv);
      }
      
      static int run_daemon_child(int infd, int outfd, int errfd, int argc, char** argv) {
          ...
          return su_main(argc, argv, 0);
      }
      

      su_main 里面决定给客户端 su 权限后,调用allow 来做最终的授权。设置好目标用户的uid和gid 后通过 execvp 系统调用执行最终的请求命令:

      static __attribute__ ((noreturn)) void allow(struct su_context *ctx) {
          .../解析参数
          populate_environment(ctx);
          set_identity(ctx->to.uid);
          ...
          execvp(binary, ctx->to.argv + argc);
          ...
      }
      
      void set_identity(unsigned int uid) {
          /*
           * Set effective uid back to root, otherwise setres[ug]id will fail
           * if uid isn't root.
           */
          if (seteuid(0)) {
              PLOGE("seteuid (root)");
              exit(EXIT_FAILURE);
          }
          if (setresgid(uid, uid, uid)) {
              PLOGE("setresgid (%u)", uid);
              exit(EXIT_FAILURE);
          }
          if (setresuid(uid, uid, uid)) {
              PLOGE("setresuid (%u)", uid);
              exit(EXIT_FAILURE);
          }
      }
      
    • 最后看个例子,执行 adb shell 后进入普通用户权限的shell控制台,在里面执行 su root ./test.sh,后进程的关系如下:

      shell        27483     1 0 20:14:27 ?     00:00:00 adbd --root_seclabel=u:r:su:s0
      shell        30009 27483 0 20:40:37 pts/3 00:00:00 sh -
      root         30018 30009 0 20:40:49 136:3 00:00:00 su root ./test.sh
      root         30019 30018 0 20:40:49 136:3 00:00:00 su root ./test.sh
      root         30024     1 0 20:40:49 ?     00:00:00 su --daemon
      root         30032 30024 0 20:40:49 136:4 00:00:00 su --daemon
      root         30037 30032 0 20:40:49 136:4 00:00:00 sh ./test.sh
      

    Apk 部分

    • android-x86 中, Settings(packages/apps/Settings) 通过引入 koush 这个第三方库来实现 su 权限的管理:

      Settings 的 Android.mk:

      LOCAL_SRC_FILES := $(call all-java-files-under, src) \
      $(call all-java-files-under,../../../external/koush/Widgets/Widgets/src) \
      $(call all-java-files-under,../../../external/koush/Superuser/Superuser/src)
      
    • su daemon 在决定是否给客户端授权时有个查库过程,这个数据库正是 apk 管理的: /data/data/$pkg/databases/su.sqlite

      policy_t database_check(struct su_context *ctx) {
          sqlite3 *db = NULL;
              
          char query[512];
          snprintf(query, sizeof(query), "select policy, until, command from uid_policy where uid=%d", ctx->from.uid);
          int ret = sqlite3_open_v2(ctx->user.database_path, &db, SQLITE_OPEN_READONLY, NULL);
          ...
          int result;
          char *err = NULL;
          struct callback_data_t data;
          data.ctx = ctx;
          data.policy = INTERACTIVE;
          ret = sqlite3_exec(db, query, database_callback, &data, &err);
          sqlite3_close(db);
          if (err != NULL) {
              LOGE("sqlite3_exec: %s", err);
              return DENY;
          }
              
          return data.policy;
      }
      
    • 从上面代码看出,当一个客户端请求 su 权限时,如果这个数据库文件不再,或者没有对应这个客户端的授权记录,默认为 INTERACTIVE,也就是会弹窗询问。 询问过程大致是:

      1. su daemone 创建一个 sever 端的 socket,通过 am 命令启动 com.koushikdutta.superuser.RequestActivity 并把socket 路径传递过去;

      2. RequestActivity 启动后,连上 daemon 的 socket,daemon 把 su 申请信息发送过去:

        static int socket_send_request(int fd, const struct su_context *ctx) {
            ...
            write_token(fd, "version", PROTO_VERSION);
            write_token(fd, "binary.version", VERSION_CODE);
            write_token(fd, "pid", ctx->from.pid);
            write_string_data(fd, "from.name", ctx->from.name);
            write_string_data(fd, "to.name", ctx->to.name);
            write_token(fd, "from.uid", ctx->from.uid);
            write_token(fd, "to.uid", ctx->to.uid);
            write_string_data(fd, "from.bin", ctx->from.bin);
            // TODO: Fix issue where not using -c does not result a in a command
            write_string_data(fd, "command", get_command(&ctx->to));
            write_token(fd, "eof", PROTO_VERSION);
            ...
        }
        
      3. RequestActivity 拿到这些数据后,根据这些信息决定是不是直接拒绝、授权或者弹窗让用户来决定。 最终判决下来后,把结果保存到刚才那个数据库中,并回复 daemon 判决结果:

        void handleAction(boolean action, Integer until) {
            ...
            try {
                mSocket.getOutputStream().write((action ? "socket:ALLOW" : "socket:DENY").getBytes());
            }
            catch (Exception ex) {
            }
            try {
                ...
                    UidPolicy policy = new UidPolicy();
                    policy.policy = action ? UidPolicy.ALLOW : UidPolicy.DENY;
                    policy.uid = mCallerUid;
                    policy.command = null;
                    policy.until = until;
                    policy.desiredUid = mDesiredUid;
                    SuDatabaseHelper.setPolicy(this, policy);
                ...
            }
            catch (Exception ex) {
            }
            finish();
        }
        
      4. daemon 拿到判决结果后,根据结果执行 allow 或者 deny。 不论是 allow 或者 deny ,最终都会通知 apk 里面的 com.koushikdutta.superuser.SuReceiver, apk 在这里可以弹一些对应的toast或者输出一些日志。

        int send_result(struct su_context *ctx, policy_t policy) {
            ...
            char *result_command[] = {
                AM_PATH,
                ACTION_RESULT,//SuReciver
                "--ei",
                "binary_version",
                binary_version,
                "--es",
                "from_name",
                ctx->from.name,
                "--es",
                "desired_name",
                ctx->to.name,
                "--ei",
                "uid",
                uid,
                "--ei",
                "desired_uid",
                desired_uid,
                "--es",
                "command",
                get_command(&ctx->to),
                "--es",
                "action",
                policy == ALLOW ? "allow" : "deny",
                user[0] ? "--user" : NULL,
                user,
                NULL
            };
            return silent_run(result_command);
        }
        

    相关文章

      网友评论

          本文标题:android su 授权分析

          本文链接:https://www.haomeiwen.com/subject/cvpkzhtx.html