安卓漏洞学习(六):Android su提权实现

suid机制失效了怎么办?

上讲给出的答案是在设备启动时由init进程开启一个su daemon 守护进程,当有程序调用su时,就作为client与这个server通信,由远程的server完成所有操作。今天我们来学习一个简单实现。

su daemon守护进程(server)的创建

su.c
int main(int argc, char const *argv[])
{
    struct su_args args = {
            .uid = 0,
            .argc = 1,
            .args = "sh"
    };

    return argc == 2 && strcmp(argv[1], "--daemon") == 0 ?   
           start_daemon() : exec_su(&args);
}
su程序既作为服务端程序,也作为申请权限客户端程序,通过参数--daemon进行控制。

作为服务端程序运行时,需要在init进程中用–daemon参数启动。执行以下程序

su_daemon.c
int start_daemon() {
    int fd = -1;
    int status = -1;

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));

    if (getuid() != 0 || getgid() != 0) {
        LOGE("daemon must run with root user\n");
        goto bail;
    }

    if ((fd = open_local_server(LOCAL_SOCKET_PATH, &addr, 5)) == -1) {
        LOGE("failed to open local socket\n");
        goto bail;
    }

    LOGD("ok, now waiting for a new client socket ...\n");

    int client = accept(fd, NULL, NULL); //接收客户端连接
    status = handle_client_socket(fd, client);

    bail:
    if (fd != -1) close(fd);
    unlink(LOCAL_SOCKET_PATH);
    return status;
}


int handle_client_socket(int server_fd, int client_fd)
{
    int status = -1;

    struct su_args args;
    int bytes;

    if (client_fd == -1) {
        LOGE("invalid client fd: -1\n");
        goto bail;
    }

    LOGD("connect ok, now reading su_args ...\n");

    if ((bytes = read(client_fd, &args, sizeof(args))) != sizeof(args)) {
        LOGE("failed when reading struct su_args info\n"
             "expected %d bytes, actually %d bytes\n", (int) sizeof(args), bytes);
        goto bail;
    }

    LOGD("su_args = {uid = %d, args = %s}\n", args.uid, args.args);
    LOGD("done, check the client\n");

    struct ucred cred;
    socklen_t len = sizeof(cred);
    if (getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED, &cred, &len)) {
        LOGE("failed to getsockopt\n");
        goto bail;
    }

    LOGD("ucred = {pid = %d, uid = %d, gid = %d}\n", cred.pid, cred.uid, cred.gid);
    LOGD("start superuser activity ...\n");

    status = start_request_activity(cred.uid, cred.pid);
    send_result_broadcast(cred.uid, status);

    if (status == -1) {
        LOGE("denyed by SuperUser app\n");
        goto bail;
    }

    pid_t child = fork();

    if (child == 0) {
        // 子进程
        close(client_fd);
        close(server_fd);
        LOGD("start  exec_su_args ===========================================\n");
        exec_su_args(&cred, &args);
    }

    // 父进程
    LOGD("waiting child process %d ...\n", child);
    if (child != -1) waitpid(child, &status, 0);

    status = WEXITSTATUS(status);
    LOGD("exit code %d\n", status);


bail:
    if (write(client_fd, &status, sizeof(int)) != sizeof(int)) {
        LOGE("failed when send result code to the client\n");
    }
    if (client_fd != -1) close(client_fd);
    return status;
}

发起su授权时client与server的通信

如要执行的命令,uid等,我把这部分封装到一个su_args结构体中。client端在连接到server后,把su_args发送过去,自身陷入阻塞状态,等待server的返回值

struct su_args {
    uid_t uid;
    int argc;
    char args[256];
};

发起su授权时,client端代码

int exec_su(struct su_args *args) {
    int fd = -1;
    int status = -1;

    struct sockaddr_un addr;

    if ((fd = open_local_client(LOCAL_SOCKET_PATH, &addr)) == -1) {
        LOGE("failed when open client socket\n");
        goto bail;
    }

    if (connect(fd, (struct sockaddr*) &addr, sizeof(addr)) == -1) {
        LOGE("failed to connect to the server\n");
        goto bail;
    }

    status = handle_server_socket(fd, args);

bail:
    if (fd != -1) close(fd);
    return status;
}


static int handle_server_socket(int fd, struct su_args* args)
{
    int status = -1;
    int bytes;

    LOGD("connect ok, now send su_args struct ...\n");

    if ((bytes = write(fd, args, sizeof(*args))) != sizeof(*args)) {
        LOGE("failed to send su_args struct\n"
             "expected %d bytes, actually %d bytes\n", (int) sizeof(*args), bytes);
        goto bail;
    }

    LOGD("send done, now waiting for exit code ...\n");
    if ((bytes = read(fd, &status, sizeof(int))) != sizeof(int)) {
        status = 1;
        LOGE("failed when receiving exit code\n");
        goto bail;
    }

    LOGD("exit code %d\n", status);
bail:
    return status;
}

server端接收到授权请求后,调用app,由用户在界面点击授权

su daemon与android app(superuser) 处在两个完全不同的运行环境里,一个是传统的linux进程,一个运行在java虚拟机上,那么二者是怎么通信的呢?
答案是am命令。am是android内置的一个命令,可以实现在命令行环境下启动activity,service,发送广播等,使用十分广泛。
既然能够唤起android app,那么就要想办法接收数据返回。这里是创建了一个临时的socket来接收数据。下面的代码在创建socket后倒计时20秒,如果因为各种各样的原因无法读到数据,那么直接拒绝。

int start_request_activity(uid_t uid, pid_t pid)
{
    int status = -1;

    char uid_s[8];
    char path[32];
    snprintf(uid_s, sizeof(uid_s), "%d", uid);
    snprintf(path, sizeof(path), "/dev/su.d.%d", pid);

    const pid_t child = fork();
    if (child == -1) {
        LOGE("failed to fork child process\n");
        return -1;
    }
    else if (child == 0) {
        execlp("am", "am", "start",
               "-n", SUPERUSER_PACKAGE_NAME "/" SUPERUSER_REQUEST_ACTIVITY,
               "--ei", "caller_uid", uid_s,
               "--es", "socket_path", path,
               NULL);
        exit(errno);
    }

    waitpid(child, &status, 0);
    status = WEXITSTATUS(status);

    LOGD("am exit code %d\n", status);

    if (status != 0) {
        return -1;
    }

    int fd = -1;
    int client = -1;
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));

    if ((fd = open_local_server(path, &addr, 1)) == -1) {
        LOGE("failed to open tmp socket: %s\n", path);
        return -1;
    }

    LOGD("socket ready, count 20 sec ...\n");

    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(fd, &fds);

    struct timeval time = {
            .tv_sec     =   20,
            .tv_usec    =   0
    };
    //等待App数据返回
    if ((status = select(fd + 1, &fds, NULL, NULL, &time)) <= 0) {
        status = -1;
        LOGE("timeout, give up this socket \n");
        goto bail;
    }

    if ((client = accept(fd, NULL, NULL)) == -1) {
        LOGE("failed to accept client socket\n");
        goto bail;
    }

    if (read(client, &status, sizeof(status)) != sizeof(status)) {
        status = -1;
        LOGE("failed to read result data from client\n");
        goto bail;
    }
bail:
    unlink(path);
    if (fd != -1) close(fd);
    if (client != -1) close(client);
    return status;
}


int send_result_broadcast(uid_t uid, int result)
{
    pid_t child = fork();
    if (child == -1) {
        LOGE("failed to fork child process \n");
        return -1;
    }
    else if (child == 0) {

        char uid_s[8];
        char result_s[8];

        snprintf(uid_s, sizeof(uid_s), "%d", uid);
        snprintf(result_s, sizeof(uid_s), "%d", result);

        execlp("am", "am", "broadcast",
               "-n", SUPERUSER_PACKAGE_NAME "/" SUPERUSER_RESULT_BROADCAST,
               "-a", SUPERUSER_PACKAGE_NAME "/" SUPERUSER_RESULT_BROADCAST,
               "--ei", "caller_uid", uid_s,
               "--ei", "su_result", result_s,
               NULL);
        exit(errno);
    }

    int status;
    waitpid(child, &status, 0);
    status = WEXITSTATUS(status);

    LOGD("send broadcast result : %d\n", status);

    return status == 0 ? 0 : -1;
}

在这里插入图片描述

用户在界面同意授权后,新创建一个进程,执行以下代码

void exec_su_args(struct ucred* cred, struct su_args* args)
{
    int status = -1;
    int fin = -1, fout = -1, ferr = -1;

    LOGD("convert args=========================\n");
    char** argv = split_cmd_line(args->args);
    for (int i = 0; argv[i] != NULL; i ++) {
        LOGD("argv[%d] = '%s\n'", i, argv[i]);
    }

    LOGD("open remote stdio\n");
    char buff[64];

    snprintf(buff, sizeof(buff), "/proc/%d/fd/0", cred->pid);
    fin = open(buff, O_RDONLY);

    snprintf(buff, sizeof(buff), "/proc/%d/fd/1", cred->pid);
    fout = open(buff, O_WRONLY);

    snprintf(buff, sizeof(buff), "/proc/%d/fd/2", cred->pid);
    ferr = open(buff, O_WRONLY);

    if (fin == -1 || fout == -1 || ferr == -1) {
        LOGD("failed to open std\n");
        goto bail;
    }

    if (dup2(fin, 0) == -1 || dup2(fout, 1) == -1 || dup2(ferr, 2) == -1) {
        LOGD("failed to dup remote std\n");
        goto bail;
    }

    LOGD("exec su args\n");
    setuid(args->uid);
    chdir("/");
    execvp(argv[0], argv);

bail:
    if (fin != -1) close(fin);
    if (fout != -1) close(fout);
    if (ferr != -1) close(ferr);
    exit(status);
}

这个过程通过将在su daemon中创建具有root权限的进程,并通过execvp函数执行授权的命令,同时,将标准输入、输出流重定向到请求授权的进程。

注意事项

server的accept部分,应包裹在一个死循环中,这样才能一直接受请求
在代理client端标准输入输出时,这里是直接open然后dup重定向文件描述符,superuser则用了虚拟终端,并开了子线程向远程转发
实际测试时要关闭selinux为宽容模式,否则socket连接不成功

补充:关于su android C程序编译

su 程序是要编译成Linux elf程序的。参考文章中给出的编译环境是编译成.so。
如果想编译成elf程序,按照下面步骤:
在…\app\src\main\jni目录下
1、编写Android.mk

LOCAL_PATH := $(call my-dir) 
include $(CLEAR_VARS)          #会清理除了LOCAL_PAT外的其他LOCAL文件路径
LOCAL_CFLAGS += -std=c99       #使用c语言c99规范
LOCAL_CFLAGS += -pie -fPIE     #相当于在源文件中增加宏定义,安卓5.0以上需要添加,否则编译出来无法使用
LOCAL_LDFLAGS += -pie -fPIE    #相当于在源文件中增加宏定义,安卓5.0以上需要添加,否则编译出来无法使用
LOCAL_ARM_MODE := arm          #模块指令集
LOCAL_LDLIBS := -llog
LOCAL_MODULE    := su    #模块名称(最后生成的可执行文件的名字,可以按照需求修改)
LOCAL_SRC_FILES := su.c su_daemon.c su_exec.c socket.c   #源文件名(需要替换成我们自己的.c文件)
include $(BUILD_EXECUTABLE)    #编译为可执行文件

2、编写Application.mk

APP_ABI := x86

3、源码的\app\src\main\jni目录下,执行:
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk
ndk-build命令需要安装android NDK,安装指导文章网上很多,自己找。

参考:https://www.jianshu.com/p/6bc251ee9026
源代码:https://github.com/nlifew/superuser

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值