今天是来到陌生城市的第66天,隔离起来的第52天,距离原定的复工日期已经18天。
总算等到重回正轨的好消息了。
国外的情况还是很严峻啊,祝福国外的朋友平安顺利。
This time is a challenge from last week’s CTF game organized by XCTF with many Chinese universities. This chanllenge is a linux kernel exploitation designed by SixStar Team. I didn’t finished it during the game, most of the time I spent on searching for objects to refill the size 0x20-0x70
, only at very last moment I realize there was a freelist harderned in the kernel. Many teams solved it by unexpected solution because of the deployment mistake, which is unpleasant, but it is still a good challenge.
I learned the solution from Kernoob: kmalloc without SMAP, thanks Kirin! Based on his writeup, I will make some notes about the debugging and details of the bypass.
Vulnerability
You can download the challenge file from: https://github.com/hebtuerror404/CTF_competition_warehouse_2020/tree/master/2020_Fight_with_virus/Pwn/Kernoob.
The kernel module is really simple, it provides add
, delete
, show
, edit
with ioctl. No KASLR, No SMAP.
These is an obvious UAF, the first thought is using comman tty_struct
tricks to hijack control flow. However the allocate size is restricted to 0x20-0x70
, which is too small for comman refilling objects. The correct solution is bypassing the freelist harderned and achieve arbitrary write.
The freelist harderned
There is a post illustrates this implementation in depth, Linux kernel 4.14 SLAB_FREELIST_HARDENED 简单分析. I will pick the important code here.
patch1:
1 | static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr, |
patch2:
1 | static inline void *freelist_dereference(const struct kmem_cache *s, |
For the first patch, if we look at a freed chunk with a successor, instead of a vaild pointer, its fd
is xored with a cookie
and the address of the chunk itself.
1 | pwndbg> x/4gx 0xffff880004dbac00 |
We still could use the UAF to change a freed chunks’fd to make the kmalloc
return wanted address, but we need to know the cookie
and the address of current chunk, also the prefetch_freepointer
needs to take care, it will checks the the fd
of fake chunk.
Because we don’t have the kernel with debug symbols – Of course we can compile a kernel with debug symbols and debuging with the source code, but in that way we could not load the vulnerable kernel module because of the module verification. If you know some tricks about force loading a kernel module in CTF challenge, please let me know. Anyway, we’d better look at the assembly code from the given kernel binary.
First thing is to located the address of __kmalloc
from the vm.
1 | # cat /proc/kallsyms | grep __kmalloc |
And inside this function, we could scroll down a little bit and see the harderned code:
1 | .text:FFFFFFFF81242D21 mov r11, [r8] | r8: ptr_addr |
This is compiled from freelist_ptr
and prefetch_freepointer
, but they are inlined into __kmalloc
.
Exploitation
Leaking cookie
With the UAF and show
operation, we can easily leak the obfuscated pointer. According to (void *)((unsigned long)ptr ^ s->random ^ ptr_addr)
, we also need the slab chunk address and the real pointer to calculate s->random
(cookie). Kirin’s writeup suggested that we can get a pointer chunk+0x28
when allocating chunks size of 0x60
. I’ve tried other size but only 0x60
works, and the pointers won’t appear every time, so we need to search for it.
If the pointer is presented, we record the index of that chunk and calculate its address.
1 | int victim_idx[2] = {0}; |
And we can calculate the cookie after deleteing two chunks, victim_idx[0]
is the end of the freelist, and its fd = cookies ^ chunk_addr
. So we can get the cookie.
1 | delete(fd, victim_idx[0]); |
Faking FD
Let’s see if we can get arbitrary address by faking the fd. Now we have the freelist like: freelist -> 1 -> 0
, if we faking victim_idx[1]
‘s fd by the rule and allocate twice, it should give us what we want.
1 | size_t fake_usermem = mmap(0xdead0000, 0x10000, PROT_READ | PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,0,0); |
And we can see 0xdead0000
is put on the pool.
1 | pwndbg> x/4gx $pool+0x180 |
In this way we could write arbitray 4 bytes on the pool
. Our final goal is tricking the the kmalloc
return a pointer on pool
, so that we can achieve arbitrary write. Let’s reason what will happen if we directly faking a pointer inside pool
as fd.
1 | .text:FFFFFFFF81242D21 mov r11, [r8] | assume r8 == 0xffffffffc00044c0(pool), it points to a chunk addr like:0xffff880004e2d540 |
Of cause we can choose a address inside pool where points to null pointer, but that will still leads to invaild memory access panic.
1 | .text:FFFFFFFF81242D21 mov r11, [r8] | assume r8 == 0xffffffffc0004660(pool+0x1a0), it points to a chunk addr like:0 |
As mentioned above, we can write arbitrary 4 bytes on the pool, for example if we write 0x55554444
on 0xffffffffc0004650
, and fake the fd as 0xffffffffc000464c
, the situation will like:
1 | .text:FFFFFFFF81242D21 mov r11, [r8] | assume r8 == 0xffffffffc000464c, it points to a chunk addr like:0x5555444400000000 |
so the idea is write (module_base ^ cookie) >> 32
on the address pool_a
, and faking the fd as pool_a-4
, so during the xor, the higher 4 byte will be zero. Then rbx
becomes an address we can mmap from userspace. Don’t forget to set [rbx] = rbx ^ cookie
to bypass prefetch.
Then we can have a pointer inside pool on the pool, an arbitrary write primitive. Then we can overwrite modprode_path
, and perform the modprode trick.
exp.c
1 |
|
Wrapup
The harderned mechanism is hard to trace at first, I spent a lot of time on trying for a stable breakpoint. The bypass is a little bit confused at the first step, but you could debug and approch the goal step by step.