android su 授权分析

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);
      }
      
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352