Last weekend, I played Midnightsun Quals and had a lot of fun with the kernel challenge brohammer. Since I learned a ton of new things from it, I thought it would be nice to make a writeup for it. Before I start, I would like to thank my fellow teammate c3bacd17 for working on this challenge with me and offering me some amazing insight in the way to approach this. As I proceed with the writeup, feel free to let me know if I made any mistakes in my explanations!
Starting off, we notice that KASLR, SMEP, and SMAP is off; this should make exploitation much easier. Additionally, we were given the source:
The kernel had a syscall added that gave us an arbitrary one bit flip on any specified address. Usually in a CTF, one of the first things to do with bit flipping challenges is to enable unlimited bits (usually due to signed comparisons), but here, an unsigned long is used, so achieving unlimited bit flips is impossible (if it was, this challenge would have been trivial). Now looking into this challenge name “brohammer,” it sounds suspiciously similar to
rowhammer, an attack against DRAM to induce bit flips, which could lead to privilege escalation if page table entries were corrupted to point to physical memory containing a page table of the exploit process. We can use a similar idea to target the page directory/table related information.
In this challenge's kernel, there is 4 level paging. According to the Intel Manual Volume 3 section 4.5, this means that each virtual address maps to physical address based in the following way: the CR3 register stores the physical address of the PML4, and bits 47:39 from the vaddr specify the offset in the table for the respective page directory pointer table's physical location. If the 7th bit (PS flag) is set in this obtained value, then the entry just refers to a 1 gb page and the rest of the vaddr bits are used as linear offsets. Otherwise, it provides the pointer to the physical location of a page directory table and the vaddr's bits from 29:21 specify the offset in the table. If the PS bit is set in this obtained value, then it maps to a 2 mb page; otherwise, it goes to a page table, and the next 9 bits from vaddr is used to compute the offset for the entry to a 4 kb page, from which the final physical location is obtained.
Each entry also holds multiple control bits (refer to Table 4-14 to 4-20), but for this challenge, what really mattered are the following bits: bit 1 (R/W), bit 2, (Usermode/Supervisor), and bit 63 for NX.
To ease c3bacd17 and my attempts to look through such data, we briefly wrote a parser to dig around physical memory for the aforementioned tables with the help of qemu memory mode ("pmemsave 0 0x8000000 memdump" to cover the amount of given memory and “info tlb” were really helpful for this challenge, thanks to this
writeup of the prequel to this challenge). The first idea I had was to attempt to gain usermode access to kernel memory; the brohammer function sounded like a nice target. Looking at the vaddr to paddr conversion in our parser, we note the following (note this section mapped as a 2 mb page):
Looking at the bits of the value 0000000000000000000000000000000000000001000000000000000111100001, we thought that we could just set bit 2 to enable usermode access to win! However, that leaves the question of writeability, and additionally, we didn't even gain usermode acccess there afterwards. Looking at Section 4.6 of the same volume, we discovered the following:
“Access rights are also controlled by the mode of a linear address as specified by the paging-structure entries controlling the translation of the linear address. If the U/S flag (bit 2) is 0 in at least one of the paging-structure entries, the address is a supervisor-mode address. Otherwise, the address is a user-mode address.”
Well, the page directory table value already violates that rule, so our target will still be considered to be within supervisor access only. The same applies to R/W.
Now, we just kept digging around through the physical memory dump, until c3bacd17 noticed that we could target physmap as well. Without KASLR, it always has the virtual address starting at 0xffff880000000000, and it is a large and continuous region that behaves as a direct mapping to physical memory (the starting location would thus map to 0 in physical memory).
Notice how the entire chain so far has the usermode and writeable bit set, and the address at 0x18fb060: 0x18001e3, where the PS bit is set (2 mb page), as well as the writeable bit. If we toggle the usermode bit, then we can actually modify a large portion of memory (2 mb) starting from 0x1800000 from userspace. This is really useful as this region holds the physical address 0x18fb040, which contains the page directory entry for where the kernel loads in memory (another 2 mb page) as 0x1000000 is the default physical load address for Linux Kernel; the address of startup_64 from kallsyms and 0xffff880001000000 (direct offset from physmap to default kernel physical load) map to the same physical location.
At this point, our exploitation strategy is ready to go. We flip the usermode bit to on for the page directory entry at 0x18fb060 to enable usermode access to this region of page directory related information. Now, with usermode access there, we can flip the writeable and usermode bit for the entry at 0x18fb040, and by referencing the offset of the brohammer function from the vaddr of kernel base based on physmap, we can now rewrite the code there due to the changed permissions. I just injected a simple commit_creds(init_cred) shellcode. Here is the final exploit:
Interestingly enough, as a few other players pointed out, the TLB caches permissions as well for the virtual to physical mappings, so this would have been problematic for the exploit in real life (as QEMU's behavior isn't exactly correct I believe). However, section 4.10.4.3 mentions how it would actually work fine after the first attempt at access (which triggers a spurious page-fault).
Thanks once again for the interesting challenge, as well as my teammate c3bacd17 for working with me and proofreading this writeup!