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