这篇文章是上一篇博客的后续分析,主要介绍向init进程发送热拔插信息后init进程的处理流程。
首先我们来了解一个数据结构,uevent,如下:
struct uevent {
const char *action;
const char *path;
const char *subsystem;
const char *firmware;
int major;
int minor;
};
内核收到的信息如下:
ACTION=add
DEVPATH=/../data/local/tmp
SUBSYSTEM=firmware
FIRMWARE=../../../data/local/tmp/hotplug
通过如下函数parse_event进行解析:
static void parse_event(const char *msg, struct uevent *uevent)
{
while(*msg) {
if(!strncmp(msg, "ACTION=", 7)) {
msg += 7;
uevent->action = msg;
} else if(!strncmp(msg, "DEVPATH=", 8)) {
msg += 8;
uevent->path = msg;
} else if(!strncmp(msg, "SUBSYSTEM=", 10)) {
msg += 10;
uevent->subsystem = msg;
} else if(!strncmp(msg, "FIRMWARE=", 9)) {
msg += 9;
uevent->firmware = msg;
} else if(!strncmp(msg, "MAJOR=", 6)) {
msg += 6;
uevent->major = atoi(msg);
} else if(!strncmp(msg, "MINOR=", 6)) {
msg += 6;
uevent->minor = atoi(msg);
}
while(*msg++);
}
}
经过解析之后,uevent的结构为:
Action="add"
path="/../data/local/tmp"
subsystem="firmware"
firmware="../../../data/local/tmp/hotplug"
之后来到处理firmware的核心函数:
static void process_firmware_event(struct uevent *uevent)
{
l = asprintf(&root, SYSFS_PREFIX"%s/", uevent->path);
//root为/sys/../data/local/tmp/=/data/local/tmp/
l = asprintf(&loading, "%sloading", root);
//loading为/data/local/tmp/loading
l = asprintf(&data, "%sdata", root);
//data为/data/local/tmp/data 其内容为指向/proc/sys/kernel/hotplug的符号链接
l = asprintf(&file, FIRMWARE_DIR"/%s", uevent->firmware);
//file为/etc/firmware/../../../data/local/tmp/hotplug=/data/local/tmp/hotplug
loading_fd = open(loading, O_WRONLY);
data_fd = open(data, O_WRONLY);
fw_fd = open(file, O_RDONLY);
load_firmware(fw_fd, loading_fd, data_fd);
}
最后来到load_firmware函数,把hotplug中的数据写到/proc/sys/kernel/hotplug中
其内容变为/data/local/tmp/exploid
static int load_firmware(int fw_fd, int loading_fd, int data_fd)
{
while (len_to_copy > 0) {
char buf[PAGE_SIZE];
nr = read(fw_fd, buf, sizeof(buf));
len_to_copy -= nr;
while (nr > 0) {
nw = write(data_fd, buf + nw, nr);
nr -= nw;
}
}
}
最后/proc/sys/kernel/hotplug中写入了我们的恶意程序,只要再次受到如wifi打开、usb插入等热拔插信息,内核就会以root权限加载我们的程序再一次执行,从而达到提权的目的。
漏洞修复
修复方法很简单,只需要给system/core/init/devices.c文件打个补丁。
具体内容及注释如下:
static int open_uevent_socket(void)
{
+ setsockopt(s, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on));
//在open_uevent_socket对套接字增加一个选项 SO_PASSCRED,这样可以让套接字增加一个认证,让接收者可以知道发送者的uid和gid :-)
}
void handle_device_fd(int fd)
{
+ for(;;) {
+ char msg[UEVENT_MSG_LEN+2];
+ char cred_msg[CMSG_SPACE(sizeof(struct ucred))];
+ struct iovec iov = {msg, sizeof(msg)};
+ struct sockaddr_nl snl;
+ struct msghdr hdr = {&snl, sizeof(snl), &iov, 1, cred_msg, sizeof(cred_msg), 0};
+
+ ssize_t n = recvmsg(fd, &hdr, 0);
+ if (n <= 0) {
+ break;
+ }
- while((n = recv(fd, msg, UEVENT_MSG_LEN, 0)) > 0) {
- struct uevent uevent;
+ if ((snl.nl_groups != 1) || (snl.nl_pid != 0)) {
+ /* 如果不是内核的多播信息则抛弃 */
+ continue;
+ }
+
+ struct cmsghdr * cmsg = CMSG_FIRSTHDR(&hdr);
+ if (cmsg == NULL || cmsg->cmsg_type != SCM_CREDENTIALS) {
+ /* 如果发送者的认证没有则抛弃 */
+ continue;
+ }
+
+ struct ucred * cred = (struct ucred *)CMSG_DATA(cmsg);
+ if (cred->uid != 0) {
+ /* 消息不是来自于root用户则抛弃 */
+ continue;
+ }
}
}