我们在上一篇文章中介绍如何在QEMU上执行iOS并启动一个交互式bash shell,在第这篇文章中,我们将详细介绍为实现这些目标所进行的一些具体的项目研究。

本文的研究项目是以该项目为基础进行的,我们本次的目的是,在没有安全监控器的情况下,在不同的iPhone上启动版本略微不同的iOS内核,同时在运行时修补内核以使其启动,运行预先存在ramdisk映像以及没有交互式I/O的launchd 服务。在这篇文章中,我们将介绍:

1.如何将代码作为新设备类型插入QEMU项目中。

2.如何在不运行时或事先修补内核的情况下启动内核;

3.如何在EL3中加载和执行安全监控器映像;

4.如何添加新的静态信任缓存,以便可以执行自签名的可执行文件;

5.如何添加新的launchd 项以执行交互式shell,而不是ramdisk上的现有服务;

6.如何建立完整的串行I/O;

该项目现在可以在qemu-aleph-git上获得,其中包含qemu-scripts-aleph-git所需的脚本。

QEMU代码

为了能够稍后在更新版本的QEMU上重新设置代码并添加对其他iDevices和iOS版本的支持,我们将所有QEMU代码更改都移动到了新模块——hw/arm/n66_iphone6splus.c 中,它是QEMU中iPhone 6s plus(n66ap)iDevice的主要模块,可用于:

1.定义新设备类型;

2.在不同的异常级别的内存中定义UART内存映射I/O、加载的内核、安全监控器、启动参数,设备树,信任缓存的内存布局。

3.定义iDevice的专有寄存器(当前什么都不做,只是作为通用寄存器操作);

4.定义设备的功能和属性,例如支持EL3并在安全监控器入口点开始执行。

5.将内置定时器中断连接到FIQ;

6.获取用于定义以下文件的命令行参数:内核映像,安全监控器映像,设备树,ramdisk,静态信任缓存,内核启动参数。

另一个主要模块是hw/arm/xnu.c,它负责:

1.将设备树加载到内存中,并将ramdisk和静态信任缓存地址添加到实际加载它们的设备树中。

2.将ramdisk加载到内存中;

3.将静态信任缓存加载到内存中;

4.将内核映像加载到内存中;

5.将安全监控器映像加载到内存中;

6.加载和设置内核并保护监控器启动参数。

在没有补丁的情况下启动内核

基于原有的项目,我们已经能够使用不同的iOS版本和不同的iPhone启动到用户模式,同时使用内核调试器在运行时修补内核。之所以修补程序是因为:在更改设备树并从ramdisk启动之后,我们封装了一个不返回的函数,等待一个永远不会发生的事件。通过在内核调试器中放置断点并挨个运行,我们发现不返回函数是IOSecureBSDRoot(),它可以在Apple发布的xnu-4903.221.2版本的XNU代码中找到:

1.jpg

在运行时,调试内核本身时发生的情况:

2.jpg

此函数不返回,因为对pe->callPlatformFunction()的调用不会返回。对于这个函数,我们没有任何参考代码,所以内核被反汇编:

3.jpg

通过检查这个函数,我们可以看到不返回函数对x19中对象的特定成员进行了大量处理,并且流程根据这些成员而变化。我们尝试了一些方法来了解这些成员所代表的具体内容,但都没有成功。这些成员似乎确实处于特殊的偏移状态,所以过了一段时间,我们试着使用Ghidra在整个内核中搜索使用对象及其成员在偏移量0x10a,0x10c和0x110的函数 ,很幸运!我们找到了这个函数:

4.jpg

在这个函数中,很容易看出当prop secure-root-prefix不在设备树中时,偏移量为0x110的成员保持不变,值为0,且原始函数(pe->callPlatformFunction())返回,可以看出,没有必要修补内核。

加载安全监控器映像

现在,我们能够将iPhone X映像启动到用户模式。此映像直接启动到EL1并且没有安全监控器。于是,我们决定使用iPhone 6s plus的另一个映像,因为Apple在那里留下了很多符号,我们认为这会使研究变得更简单。事实证明,没有KTRR(内核文本只读区域)的KPP(内核补丁保护)设备有一个安全的监控器映像,需要加载自己的启动参数,并在EL3中执行。该项目的这一部分是关于查找内核文件中嵌入的安全监控器映像,加载它,理解启动参数结构,加载映像和配置QEMU以开始执行EL3中的入口点。完成这些步骤后,仍然没有成功。原因似乎是安全监控器映像尝试解析内核库(通过内核启动参数读取)中的内核mach-o标头,且我们没有在该基地址处的内核映像。这一切都发生在以下函数中:

5.jpg

我们相信这个函数负责KPP功能,并假设它根据内核部分应有的权限保存内核部分的映射,但是这个假设仍然需要验证。

从原有项目的代码中可以看出,virt_base参数指向的是加载内核的最低段:

static uint64_t arm_load_macho(struct arm_boot_info *info, uint64_t *pentry, AddressSpace *as)
{
    hwaddr kernel_load_offset = 0x00000000;
    hwaddr mem_base = info->loader_start;

    uint8_t *data = NULL;
    gsize len;
    bool ret = false;
    uint8_t* rom_buf = NULL;
    if (!g_file_get_contents(info->kernel_filename, (char**) &data, &len, NULL)) {
        goto out;
    }
    struct mach_header_64* mh = (struct mach_header_64*)data;
    struct load_command* cmd = (struct load_command*)(data + sizeof(struct mach_header_64));
    // iterate through all the segments once to find highest and lowest addresses
    uint64_t pc = 0;
    uint64_t low_addr_temp;
    uint64_t high_addr_temp;
    macho_highest_lowest(mh, &low_addr_temp, &high_addr_temp);
    uint64_t rom_buf_size = high_addr_temp - low_addr_temp;
    rom_buf = g_malloc0(rom_buf_size);
    for (unsigned int index = 0; index < mh->ncmds; index++) {
        switch (cmd->cmd) {
            case LC_SEGMENT_64: {
                struct segment_command_64* segCmd = (struct segment_command_64*)cmd;
                memcpy(rom_buf + (segCmd->vmaddr - low_addr_temp), data + segCmd->fileoff, segCmd->filesize);
                break;
            }
            case LC_UNIXTHREAD: {
                // grab just the entry point PC
                uint64_t* ptrPc = (uint64_t*)((char*)cmd + 0x110); // for arm64 only.
                pc = VAtoPA(*ptrPc);
                break;
            }
        }
        cmd = (struct load_command*)((char*)cmd + cmd->cmdsize);
    }
    hwaddr rom_base = VAtoPA(low_addr_temp);
    rom_add_blob_fixed_as("macho", rom_buf, rom_buf_size, rom_base, as);
    ret = true;

    uint64_t load_extra_offset = high_addr_temp;

    uint64_t ramdisk_address = load_extra_offset;
    gsize ramdisk_size = 0;

    // load ramdisk if exists
    if (info->initrd_filename) {
        uint8_t* ramdisk_data = NULL;
        if (g_file_get_contents(info->initrd_filename, (char**) &ramdisk_data, &ramdisk_size, NULL)) {
            info->initrd_filename = NULL;
            rom_add_blob_fixed_as("xnu_ramdisk", ramdisk_data, ramdisk_size, VAtoPA(ramdisk_address), as);
            load_extra_offset = (load_extra_offset + ramdisk_size + 0xffffull) & ~0xffffull;
            g_free(ramdisk_data);
        } else {
            fprintf(stderr, "ramdisk failed?!\n");
            abort();
        }
    }

    uint64_t dtb_address = load_extra_offset;
    gsize dtb_size = 0;
    // load device tree
    if (info->dtb_filename) {
        uint8_t* dtb_data = NULL;
        if (g_file_get_contents(info->dtb_filename, (char**) &dtb_data, &dtb_size, NULL)) {
            info->dtb_filename = NULL;
            if (ramdisk_size != 0) {
                macho_add_ramdisk_to_dtb(dtb_data, dtb_size, VAtoPA(ramdisk_address), ramdisk_size);
            }
            rom_add_blob_fixed_as("xnu_dtb", dtb_data, dtb_size, VAtoPA(dtb_address), as);
            load_extra_offset = (load_extra_offset + dtb_size + 0xffffull) & ~0xffffull;
            g_free(dtb_data);
        } else {
            fprintf(stderr, "dtb failed?!\n");
            abort();
        }
    }

    // fixup boot args
    // note: device tree and args must follow kernel and be included in the kernel data size.
    // macho_setup_bootargs takes care of adding the size for the args
    // osfmk/arm64/arm_vm_init.c:arm_vm_prot_init
    uint64_t bootargs_addr = VAtoPA(load_extra_offset);
    uint64_t phys_base = (mem_base + kernel_load_offset);
    uint64_t virt_base = low_addr_temp & ~0x3fffffffull;
    macho_setup_bootargs(info, as, bootargs_addr, virt_base, phys_base, VAtoPA(load_extra_offset), dtb_address, dtb_size);

    // write bootloader
    uint32_t fixupcontext[FIXUP_MAX];
    fixupcontext[FIXUP_ARGPTR] = bootargs_addr;
    fixupcontext[FIXUP_ENTRYPOINT] = pc;
    write_bootloader("bootloader", info->loader_start,
                         bootloader_aarch64, fixupcontext, as);
    *pentry = info->loader_start;

    out:
    if (data) {
        g_free(data);
    }
    if (rom_buf) {
        g_free(rom_buf);
    }
    return ret? high_addr_temp - low_addr_temp : -1;

在本文的例子中,这个段被映射到加载的mach-o标头的地址下面。这意味着virt_base不指向内核mach-o标头,因此不能使用上面提到的安全监控器代码。我们尝试解决这个问题的一种方法是将virt_base设置为mach-o标头的地址,但这使得一些内核驱动程序代码加载到virt_base之下,这搞砸了很多东西,比如下面的函数:

vm_offset_t
ml_static_vtop(vm_offset_t va)
{
	for (size_t i = 0; (i < PTOV_TABLE_SIZE) && (ptov_table[i].len != 0); i++) {
		if ((va >= ptov_table[i].va) && (va < (ptov_table[i].va + ptov_table[i].len)))
			return (va - ptov_table[i].va + ptov_table[i].pa);
	}
	if (((vm_address_t)(va) - gVirtBase) >= gPhysSize)
		panic("ml_static_vtop(): illegal VA: %p\n", (void*)va);
	return ((vm_address_t)(va) - gVirtBase + gPhysBase);
}

我们尝试的另一种方法是跳过安全监控器的执行,直接从EL1中的内核入口点开始。不过在我们点击第一条SMC指令时,该方法就失效了。它可能通过在使用SMC的地方修补内核来解决这个问题,但我们不想这样做。最终我们还是将virt_base设置为低于最低加载段的较低地址,并且在该位置只有整个原始kernelcache文件的另一个副本。这个解决方案可以将virt_base置于内核中实际使用的所有虚拟地址之下,让它指向内核的mach-o标头,并将内核按段优先加载到其首选地址,在那里执行。

信任缓存

在本节中,我们将介绍为加载非Apple自签名可执行文件所做的所有工作。 iOS系统通常只执行受信任的可执行文件,这些可执行文件要么在信任缓存中,要么由Apple或已安装的配置文件签名。关于这一主题的更多资料可以在这里http://www.newosxbook.com/articles/CodeSigning.pdf找到,一般来说,信任缓存有三种类型:

1.内核缓存中硬编码的信任缓存;

2.可以在运行时从文件加载的信任缓存;

3. 从设备树指向内存中的信任缓存;

我们在本文中主要分析第3种,以下函数包含最高级逻辑,,用于检查可执行文件是否有基于信任缓存或其他方式批准执行的代码签名:

8.jpg

如果我们深入研究,我们最终会得到这个检查静态信任缓存的函数:

9.jpg

我们可以看到,使用了XREF,值的设置如下所示:

10.jpg

上述函数解析原始信任缓存格式,你也可以按照代码和错误消息进行测试,得出的信任缓存格式为:

struct cdhash {
    uint8_t hash[20]; //first 20 bytes of the cdhash
    uint8_t hash_type; //left as 0
    uint8_t hash_flags; //left as 0
};

struct static_trust_cache_entry {
    uint64_t trust_cache_version; //should be 1
    uint64_t unknown1; //left as 0
    uint64_t unknown2; //left as 0
    uint64_t unknown3; //left as 0
    uint64_t unknown4; //left as 0
    uint64_t number_of_cdhashes;
    struct cdhash[];
};

struct static_trust_cache_buffer {
    uint64_t number_of_trust_caches_in_buffer;
    uint64_t offsets_to_trust_caches_from_beginning_of_buffer[];
    struct static_trust_cache_entry entries[];
};

而且似乎即使结构在缓冲区中支持多个信任缓存,代码实际上也将大小限制为1。

从此函数执行XREF后,我们将获得以下代码:

12.jpg

因此,可以推测出,这些数据是从设备树中读取的。

现在,剩下要做的就是将信任缓存加载到内存中,并从设备树指向它。我们必须决定把它放在内存中的哪个位置。内核数据顶部有一个内核启动参数,它指向内核,ramdisk,设备树和启动参数之后的地址。我们尝试的第一个位置靠近内核数据地址的顶部(在它之前和之后)。这不是很好,因为下面的代码是匹配上面的程序集的代码:

void
arm_vm_prot_init(boot_args * args)
{

	segLOWESTTEXT = UINT64_MAX;
	if (segSizePRELINKTEXT  && (segPRELINKTEXTB < segLOWESTTEXT)) segLOWESTTEXT = segPRELINKTEXTB;
	assert(segSizeTEXT);
	if (segTEXTB < segLOWESTTEXT) segLOWESTTEXT = segTEXTB;
	assert(segLOWESTTEXT < UINT64_MAX);

	segEXTRADATA = segLOWESTTEXT;
	segSizeEXTRADATA = 0;

	DTEntry memory_map;
	MemoryMapFileInfo *trustCacheRange;
	unsigned int trustCacheRangeSize;
	int err;

	err = DTLookupEntry(NULL, "chosen/memory-map", &memory_map);
	assert(err == kSuccess);

	err = DTGetProperty(memory_map, "TrustCache", (void**)&trustCacheRange, &trustCacheRangeSize);
	if (err == kSuccess) {
		assert(trustCacheRangeSize == sizeof(MemoryMapFileInfo));

		segEXTRADATA = phystokv(trustCacheRange->paddr);
		segSizeEXTRADATA = trustCacheRange->length;

		arm_vm_page_granular_RNX(segEXTRADATA, segSizeEXTRADATA, FALSE);
	}

	/* Map coalesced kext TEXT segment RWNX for now */
	arm_vm_page_granular_RWNX(segPRELINKTEXTB, segSizePRELINKTEXT, FALSE); // Refined in OSKext::readPrelinkedExtensions

	/* Map coalesced kext DATA_CONST segment RWNX (could be empty) */
	arm_vm_page_granular_RWNX(segPLKDATACONSTB, segSizePLKDATACONST, FALSE); // Refined in OSKext::readPrelinkedExtensions

	/* Map coalesced kext TEXT_EXEC segment RWX (could be empty) */
	arm_vm_page_granular_ROX(segPLKTEXTEXECB, segSizePLKTEXTEXEC, FALSE); // Refined in OSKext::readPrelinkedExtensions

	/* if new segments not present, set space between PRELINK_TEXT and xnu TEXT to RWNX
	 * otherwise we no longer expect any space between the coalesced kext read only segments and xnu rosegments
	 */
	if (!segSizePLKDATACONST && !segSizePLKTEXTEXEC) {
		if (segSizePRELINKTEXT)
			arm_vm_page_granular_RWNX(segPRELINKTEXTB + segSizePRELINKTEXT, segTEXTB - (segPRELINKTEXTB + segSizePRELINKTEXT), FALSE);
	} else {
		/*
		 * If we have the new segments, we should still protect the gap between kext
		 * read-only pages and kernel read-only pages, in the event that this gap
		 * exists.
		 */
		if ((segPLKDATACONSTB + segSizePLKDATACONST) < segTEXTB) {
			arm_vm_page_granular_RWNX(segPLKDATACONSTB + segSizePLKDATACONST, segTEXTB - (segPLKDATACONSTB + segSizePLKDATACONST), FALSE);
		}
	}

	/*
	 * Protection on kernel text is loose here to allow shenanigans early on.  These
	 * protections are tightened in arm_vm_prot_finalize().  This is necessary because
	 * we currently patch LowResetVectorBase in cpu.c.
	 *
	 * TEXT segment contains mach headers and other non-executable data. This will become RONX later.
	 */
	arm_vm_page_granular_RNX(segTEXTB, segSizeTEXT, FALSE);

	/* Can DATACONST start out and stay RNX?
	 * NO, stuff in this segment gets modified during startup (viz. mac_policy_init()/mac_policy_list)
	 * Make RNX in prot_finalize
	 */
	arm_vm_page_granular_RWNX(segDATACONSTB, segSizeDATACONST, FALSE);

	/* TEXTEXEC contains read only executable code: becomes ROX in prot_finalize */
	arm_vm_page_granular_RWX(segTEXTEXECB, segSizeTEXTEXEC, FALSE);


	/* DATA segment will remain RWNX */
	arm_vm_page_granular_RWNX(segDATAB, segSizeDATA, FALSE);

	arm_vm_page_granular_RWNX(segBOOTDATAB, segSizeBOOTDATA, TRUE);
	arm_vm_page_granular_RNX((vm_offset_t)&intstack_low_guard, PAGE_MAX_SIZE, TRUE);
	arm_vm_page_granular_RNX((vm_offset_t)&intstack_high_guard, PAGE_MAX_SIZE, TRUE);
	arm_vm_page_granular_RNX((vm_offset_t)&excepstack_high_guard, PAGE_MAX_SIZE, TRUE);

	arm_vm_page_granular_ROX(segKLDB, segSizeKLD, FALSE);
	arm_vm_page_granular_RWNX(segLINKB, segSizeLINK, FALSE);
	arm_vm_page_granular_RWNX(segPLKLINKEDITB, segSizePLKLINKEDIT, FALSE); // Coalesced kext LINKEDIT segment
	arm_vm_page_granular_ROX(segLASTB, segSizeLAST, FALSE); // __LAST may be empty, but we cannot assume this

	arm_vm_page_granular_RWNX(segPRELINKDATAB, segSizePRELINKDATA, FALSE); // Prelink __DATA for kexts (RW data)

	if (segSizePLKLLVMCOV > 0)
		arm_vm_page_granular_RWNX(segPLKLLVMCOVB, segSizePLKLLVMCOV, FALSE); // LLVM code coverage data

	arm_vm_page_granular_RWNX(segPRELINKINFOB, segSizePRELINKINFO, FALSE); /* PreLinkInfoDictionary */

	arm_vm_page_granular_RNX(phystokv(args->topOfKernelData), BOOTSTRAP_TABLE_SIZE, FALSE); // Boot page tables; they should not be mutable.
}

我们可以从中看到,当我们有一个静态信任缓存时,segEXTRADATA被设置为信任缓存缓冲区,而不是segLOWESTTEXT。

在以下两个函数中,我们可以看到,如果gVirtBase和segEXTRADATA之间的数据有意义,那么可怕的事情就会发生:

static void
arm_vm_physmap_init(boot_args *args, vm_map_address_t physmap_base, vm_map_address_t dynamic_memory_begin __unused)
{
	ptov_table_entry temp_ptov_table[PTOV_TABLE_SIZE];
	bzero(temp_ptov_table, sizeof(temp_ptov_table));

	// Will be handed back to VM layer through ml_static_mfree() in arm_vm_prot_finalize()
	arm_vm_physmap_slide(temp_ptov_table, physmap_base, gVirtBase, segEXTRADATA - gVirtBase, AP_RWNA, FALSE);

	arm_vm_page_granular_RWNX(end_kern, phystokv(args->topOfKernelData) - end_kern, FALSE); /* Device Tree, RAM Disk (if present), bootArgs */

	arm_vm_physmap_slide(temp_ptov_table, physmap_base, (args->topOfKernelData + BOOTSTRAP_TABLE_SIZE - gPhysBase + gVirtBase),
			     real_avail_end - (args->topOfKernelData + BOOTSTRAP_TABLE_SIZE), AP_RWNA, FALSE); // rest of physmem

	assert((temp_ptov_table[ptov_index - 1].va + temp_ptov_table[ptov_index - 1].len) <= dynamic_memory_begin);

	// Sort in descending order of segment length.  LUT traversal is linear, so largest (most likely used)
	// segments should be placed earliest in the table to optimize lookup performance.
	qsort(temp_ptov_table, PTOV_TABLE_SIZE, sizeof(temp_ptov_table[0]), cmp_ptov_entries); 

	memcpy(ptov_table, temp_ptov_table, sizeof(ptov_table));
}


void
arm_vm_prot_finalize(boot_args * args __unused)
{
	/*
	 * At this point, we are far enough along in the boot process that it will be
	 * safe to free up all of the memory preceeding the kernel.  It may in fact
	 * be safe to do this earlier.
	 *
	 * This keeps the memory in the V-to-P mapping, but advertises it to the VM
	 * as usable.
	 */

	/*
	 * if old style PRELINK segment exists, free memory before it, and after it before XNU text
	 * otherwise we're dealing with a new style kernel cache, so we should just free the
	 * memory before PRELINK_TEXT segment, since the rest of the KEXT read only data segments
	 * should be immediately followed by XNU's TEXT segment
	 */

	ml_static_mfree(phystokv(gPhysBase), segEXTRADATA - gVirtBase);

	/*
	 * KTRR support means we will be mucking with these pages and trying to
	 * protect them; we cannot free the pages to the VM if we do this.
	 */
	if (!segSizePLKDATACONST && !segSizePLKTEXTEXEC && segSizePRELINKTEXT) {
		/* If new segments not present, PRELINK_TEXT is not dynamically sized, free DRAM between it and xnu TEXT */
		ml_static_mfree(segPRELINKTEXTB + segSizePRELINKTEXT, segTEXTB - (segPRELINKTEXTB + segSizePRELINKTEXT));
	}

	/*
	 * LowResetVectorBase patching should be done by now, so tighten executable
	 * protections.
	 */
	arm_vm_page_granular_ROX(segTEXTEXECB, segSizeTEXTEXEC, FALSE);

	/* tighten permissions on kext read only data and code */
	if (segSizePLKDATACONST && segSizePLKTEXTEXEC) {
		arm_vm_page_granular_RNX(segPRELINKTEXTB, segSizePRELINKTEXT, FALSE);
		arm_vm_page_granular_ROX(segPLKTEXTEXECB, segSizePLKTEXTEXEC, FALSE);
		arm_vm_page_granular_RNX(segPLKDATACONSTB, segSizePLKDATACONST, FALSE);
	}

	cpu_stack_alloc(&BootCpuData);
	arm64_replace_bootstack(&BootCpuData);
	ml_static_mfree(phystokv(segBOOTDATAB - gVirtBase + gPhysBase), segSizeBOOTDATA);

#if __ARM_KERNEL_PROTECT__
	arm_vm_populate_kernel_el0_mappings();
#endif /* __ARM_KERNEL_PROTECT__ */


#if defined(KERNEL_INTEGRITY_KTRR)
	/*
	 * __LAST,__pinst should no longer be executable.
	 */
	arm_vm_page_granular_RNX(segLASTB, segSizeLAST, FALSE);

	/*
	 * Must wait until all other region permissions are set before locking down DATA_CONST
	 * as the kernel static page tables live in DATA_CONST on KTRR enabled systems
	 * and will become immutable.
	 */
#endif

	arm_vm_page_granular_RNX(segDATACONSTB, segSizeDATACONST, FALSE);

#ifndef __ARM_L1_PTW__
	FlushPoC_Dcache();
#endif
	__builtin_arm_dsb(DSB_ISH);
	flush_mmu_tlb();
}

现在,根据上述观察,我们决定将信任缓存缓冲区放在我们放置在virt_base的原始内核文件之后。当然,这仍然没有奏效。在设置页面表的代码之后,我们找到了从表中卸载此内存位置的位置,并最终了解到原始内核文件结束后的几个页面会在某些时候从内存中被卸载。查看代码:

15.jpg

你可以阅读该函数并查看是否实现了二进制搜索,在缓冲区中对哈希值进行排序后,该函数并最终可以正常工作了。

Bash launchd项

此时,我们有能力执行我们自己的自签名非Apple可执行文件,所以我们希望launchd执行bash而不是ramdisk上存在的服务。为此,我们删除了/System/Library/LaunchDaemons/ 中的所有文件,并添加了一个新文件com.apple.bash.plist,其中包含以下内容:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict>
        <key>EnablePressuredExit</key>
        <false/>
        <key>Label</key>
        <string>com.apple.bash</string>
        <key>POSIXSpawnType</key>
        <string>Interactive</string>
        <key>ProgramArguments</key>
        <array>
                <string>/iosbinpack64/bin/bash</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>StandardErrorPath</key>
        <string>/dev/console</string>
        <key>StandardInPath</key>
        <string>/dev/console</string>
        <key>StandardOutPath</key>
        <string>/dev/console</string>
        <key>Umask</key>
        <integer>0</integer>
        <key>UserName</key>
        <string>root</string></dict></plist>

这使得launchd尝试并执行bash,但尝试结果失败,原因是ramdisk没有动态加载器缓存。为了解决这个问题,我们将dyld缓存从完整磁盘映像复制到ramdisk(在调整ramdisk大小后,它将有足够的空间)。但尝试结果又失败,问题似乎仍然是缺少相同的库,即使有了dyld缓存也不行。为了调试它,我们需要更好地了解故障发生的位置。最后,我们发现加载缓存发生在dyld里面:

static void mapSharedCache(){
	uint64_t cacheBaseAddress = 0;
	// quick check if a cache is already mapped into shared region	if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
		sSharedCache = (dyld_cache_header*)cacheBaseAddress;
		// if we don't understand the currently mapped shared cache, then ignore#if __x86_64__		const char* magic = (sHaswell ? ARCH_CACHE_MAGIC_H : ARCH_CACHE_MAGIC);#else		const char* magic = ARCH_CACHE_MAGIC;#endif		if ( strcmp(sSharedCache->magic, magic) != 0 ) {
			sSharedCache = NULL;
			if ( gLinkContext.verboseMapping ) {
				dyld::log("dyld: existing shared cached in memory is not compatible\n");
				return;
			}
		}
		// check if cache file is slidable		const dyld_cache_header* header = sSharedCache;
		if ( (header->mappingOffset >= 0x48) && (header->slideInfoSize != 0) ) {
			// solve for slide by comparing loaded address to address of first region			const uint8_t* loadedAddress = (uint8_t*)sSharedCache;
			const dyld_cache_mapping_info* const mappings = (dyld_cache_mapping_info*)(loadedAddress+header->mappingOffset);
			const uint8_t* preferedLoadAddress = (uint8_t*)(long)(mappings[0].address);
			sSharedCacheSlide = loadedAddress - preferedLoadAddress;
			dyld::gProcessInfo->sharedCacheSlide = sSharedCacheSlide;
			//dyld::log("sSharedCacheSlide=0x%08lX, loadedAddress=%p, preferedLoadAddress=%p\n", sSharedCacheSlide, loadedAddress, preferedLoadAddress);		}
		// if cache has a uuid, copy it 
		if ( header->mappingOffset >= 0x68 ) {
			memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
		}
		// verbose logging		if ( gLinkContext.verboseMapping ) {
			dyld::log("dyld: re-using existing shared cache mapping\n");
		}
	}
	else {#if __i386__ || __x86_64__		// <rdar://problem/5925940> Safe Boot should disable dyld shared cache		// if we are in safe-boot mode and the cache was not made during this boot cycle,		// delete the cache file		uint32_t	safeBootValue = 0;
		size_t		safeBootValueSize = sizeof(safeBootValue);
		if ( (sysctlbyname("kern.safeboot", &safeBootValue, &safeBootValueSize, NULL, 0) == 0) && (safeBootValue != 0) ) {
			// user booted machine in safe-boot mode			struct stat dyldCacheStatInfo;
			//  Don't use custom DYLD_SHARED_CACHE_DIR if provided, use standard path			if ( my_stat(MACOSX_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME, &dyldCacheStatInfo) == 0 ) {
				struct timeval bootTimeValue;
				size_t bootTimeValueSize = sizeof(bootTimeValue);
				if ( (sysctlbyname("kern.boottime", &bootTimeValue, &bootTimeValueSize, NULL, 0) == 0) && (bootTimeValue.tv_sec != 0) ) {
					// if the cache file was created before this boot, then throw it away and let it rebuild itself					if ( dyldCacheStatInfo.st_mtime < bootTimeValue.tv_sec ) {
						::unlink(MACOSX_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME);
						gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
						return;
					}
				}
			}
		}#endif		// map in shared cache to shared region		int fd = openSharedCacheFile();
		if ( fd != -1 ) {
			uint8_t firstPages[8192];
			if ( ::read(fd, firstPages, 8192) == 8192 ) {
				dyld_cache_header* header = (dyld_cache_header*)firstPages;
		#if __x86_64__				const char* magic = (sHaswell ? ARCH_CACHE_MAGIC_H : ARCH_CACHE_MAGIC);
		#else				const char* magic = ARCH_CACHE_MAGIC;
		#endif				if ( strcmp(header->magic, magic) == 0 ) {
					const dyld_cache_mapping_info* const fileMappingsStart = (dyld_cache_mapping_info*)&firstPages[header->mappingOffset];
					const dyld_cache_mapping_info* const fileMappingsEnd = &fileMappingsStart[header->mappingCount];
					shared_file_mapping_np	mappings[header->mappingCount+1]; // add room for code-sig 
					unsigned int mappingCount = header->mappingCount;
					int codeSignatureMappingIndex = -1;
					int readWriteMappingIndex = -1;
					int readOnlyMappingIndex = -1;
					// validate that the cache file has not been truncated					bool goodCache = false;
					struct stat stat_buf;
					if ( fstat(fd, &stat_buf) == 0 ) {
						goodCache = true;
						int i=0;
						for (const dyld_cache_mapping_info* p = fileMappingsStart; p < fileMappingsEnd; ++p, ++i) {
							mappings[i].sfm_address		= p->address;
							mappings[i].sfm_size		= p->size;
							mappings[i].sfm_file_offset	= p->fileOffset;
							mappings[i].sfm_max_prot	= p->maxProt;
							mappings[i].sfm_init_prot	= p->initProt;
							// rdar://problem/5694507 old update_dyld_shared_cache tool could make a cache file							// that is not page aligned, but otherwise ok.							if ( p->fileOffset+p->size > (uint64_t)(stat_buf.st_size+4095 & (-4096)) ) {
								dyld::log("dyld: shared cached file is corrupt: %s" DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME "\n", sSharedCacheDir);
								goodCache = false;
							}
							if ( (mappings[i].sfm_init_prot & (VM_PROT_READ|VM_PROT_WRITE)) == (VM_PROT_READ|VM_PROT_WRITE) ) {
								readWriteMappingIndex = i;
							}
							if ( mappings[i].sfm_init_prot == VM_PROT_READ ) {
								readOnlyMappingIndex = i;
							}
						}
						// if shared cache is code signed, add a mapping for the code signature						uint64_t signatureSize = header->codeSignatureSize;
						// zero size in header means signature runs to end-of-file						if ( signatureSize == 0 )
							signatureSize = stat_buf.st_size - header->codeSignatureOffset;
						if ( signatureSize != 0 ) {
                            int linkeditMapping = mappingCount-1;
							codeSignatureMappingIndex = mappingCount++;
							mappings[codeSignatureMappingIndex].sfm_address		= mappings[linkeditMapping].sfm_address + mappings[linkeditMapping].sfm_size;#if __arm__ || __arm64__							mappings[codeSignatureMappingIndex].sfm_size		= (signatureSize+16383) & (-16384);#else							mappings[codeSignatureMappingIndex].sfm_size		= (signatureSize+4095) & (-4096);#endif							mappings[codeSignatureMappingIndex].sfm_file_offset	= header->codeSignatureOffset;
							mappings[codeSignatureMappingIndex].sfm_max_prot	= VM_PROT_READ;
							mappings[codeSignatureMappingIndex].sfm_init_prot	= VM_PROT_READ;
						}
					}#if __MAC_OS_X_VERSION_MIN_REQUIRED	
					// sanity check that /usr/lib/libSystem.B.dylib stat() info matches cache					if ( header->imagesCount * sizeof(dyld_cache_image_info) + header->imagesOffset < 8192 ) {
						bool foundLibSystem = false;
						if ( my_stat("/usr/lib/libSystem.B.dylib", &stat_buf) == 0 ) {
							const dyld_cache_image_info* images = (dyld_cache_image_info*)&firstPages[header->imagesOffset];
							const dyld_cache_image_info* const imagesEnd = &images[header->imagesCount];
							for (const dyld_cache_image_info* p = images; p < imagesEnd; ++p) {
 								if ( ((time_t)p->modTime == stat_buf.st_mtime) && ((ino_t)p->inode == stat_buf.st_ino) ) {
									foundLibSystem = true;
									break;
								}
							}					
						}
						if ( !sSharedCacheIgnoreInodeAndTimeStamp && !foundLibSystem ) {
							dyld::log("dyld: shared cached file was built against a different libSystem.dylib, ignoring cache.\n"
									"to update dyld shared cache run: 'sudo update_dyld_shared_cache' then reboot.\n");
							goodCache = false;
						}
					}#endif
#if __IPHONE_OS_VERSION_MIN_REQUIRED					{
						uint64_t lowAddress;
						uint64_t highAddress;
						getCacheBounds(mappingCount, mappings, lowAddress, highAddress);
						if ( (highAddress-lowAddress) > SHARED_REGION_SIZE ) 
							throw "dyld shared cache is too big to fit in shared region";
					}#endif
					if ( goodCache && (readWriteMappingIndex == -1) ) {
						dyld::log("dyld: shared cached file is missing read/write mapping: %s" DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME "\n", sSharedCacheDir);
						goodCache = false;
					}
					if ( goodCache && (readOnlyMappingIndex == -1) ) {
						dyld::log("dyld: shared cached file is missing read-only mapping: %s" DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME "\n", sSharedCacheDir);
						goodCache = false;
					}
					if ( goodCache ) {
						long cacheSlide = 0;
						void* slideInfo = NULL;
						uint64_t slideInfoSize = 0;
						// check if shared cache contains slid info						if ( header->slideInfoSize != 0 ) {
							// <rdar://problem/8611968> don't slide shared cache if ASLR disabled (main executable didn't slide)							if ( sMainExecutable->isPositionIndependentExecutable() && (sMainExecutable->getSlide() == 0) )
								cacheSlide = 0;
							else {
								// generate random slide amount								cacheSlide = pickCacheSlide(mappingCount, mappings);
								slideInfo = (void*)(long)(mappings[readOnlyMappingIndex].sfm_address + (header->slideInfoOffset - mappings[readOnlyMappingIndex].sfm_file_offset));
								slideInfoSize = header->slideInfoSize;
								// add VM_PROT_SLIDE bit to __DATA area of cache								mappings[readWriteMappingIndex].sfm_max_prot  |= VM_PROT_SLIDE;
								mappings[readWriteMappingIndex].sfm_init_prot |= VM_PROT_SLIDE;
							}
						}
						if ( gLinkContext.verboseMapping ) {
							dyld::log("dyld: calling _shared_region_map_and_slide_np() with regions:\n");
							for (int i=0; i < mappingCount; ++i) {
								dyld::log("   address=0x%08llX, size=0x%08llX, fileOffset=0x%08llX\n", mappings[i].sfm_address, mappings[i].sfm_size, mappings[i].sfm_file_offset);
							}
						}
						if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, codeSignatureMappingIndex, cacheSlide, slideInfo, slideInfoSize) == 0) {
							// successfully mapped cache into shared region							sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
							sSharedCacheSlide = cacheSlide;
							dyld::gProcessInfo->sharedCacheSlide = cacheSlide;
							//dyld::log("sSharedCache=%p sSharedCacheSlide=0x%08lX\n", sSharedCache, sSharedCacheSlide);							// if cache has a uuid, copy it							if ( header->mappingOffset >= 0x68 ) {
								memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
							}
						}
						else {#if __IPHONE_OS_VERSION_MIN_REQUIRED							throw "dyld shared cache could not be mapped";#endif							if ( gLinkContext.verboseMapping ) 
								dyld::log("dyld: shared cached file could not be mapped\n");
						}
					}
				}
				else {
					if ( gLinkContext.verboseMapping ) 
						dyld::log("dyld: shared cached file is invalid\n");
				}
			}
			else {
				if ( gLinkContext.verboseMapping ) 
					dyld::log("dyld: shared cached file cannot be read\n");
			}
			close(fd);
		}
		else {
			if ( gLinkContext.verboseMapping ) 
				dyld::log("dyld: shared cached file cannot be opened\n");
		}
	}
	
	// remember if dyld loaded at same address as when cache built	if ( sSharedCache != NULL ) {
		gLinkContext.dyldLoadedAtSameAddressNeededBySharedCache = ((uintptr_t)(sSharedCache->dyldBaseAddress) == (uintptr_t)&_mh_dylinker_header);
	}
	
	// tell gdb where the shared cache is	if ( sSharedCache != NULL ) {
		const dyld_cache_mapping_info* const start = (dyld_cache_mapping_info*)((uint8_t*)sSharedCache + sSharedCache->mappingOffset);
		dyld_shared_cache_ranges.sharedRegionsCount = sSharedCache->mappingCount;
		// only room to tell gdb about first four regions		if ( dyld_shared_cache_ranges.sharedRegionsCount > 4 )
			dyld_shared_cache_ranges.sharedRegionsCount = 4;
		const dyld_cache_mapping_info* const end = &start[dyld_shared_cache_ranges.sharedRegionsCount];
		int index = 0;
		for (const dyld_cache_mapping_info* p = start; p < end; ++p, ++index ) {
			dyld_shared_cache_ranges.ranges[index].start = p->address+sSharedCacheSlide;
			dyld_shared_cache_ranges.ranges[index].length = p->size;
			if ( gLinkContext.verboseMapping ) {
				dyld::log("        0x%08llX->0x%08llX %s%s%s init=%x, max=%x\n", 
					p->address+sSharedCacheSlide, p->address+sSharedCacheSlide+p->size-1,
					((p->initProt & VM_PROT_READ) ? "read " : ""),
					((p->initProt & VM_PROT_WRITE) ? "write " : ""),
					((p->initProt & VM_PROT_EXECUTE) ? "execute " : ""),  p->initProt, p->maxProt);
			}
		#if __i386__			// If a non-writable and executable region is found in the R/W shared region, then this is __IMPORT segments			// This is an old cache.  Make writable.  dyld no longer supports turn W on and off as it binds			if ( (p->initProt == (VM_PROT_READ|VM_PROT_EXECUTE)) && ((p->address & 0xF0000000) == 0xA0000000) ) {
				if ( p->size != 0 ) {
					vm_prot_t prot = VM_PROT_EXECUTE | PROT_READ | VM_PROT_WRITE;
					vm_protect(mach_task_self(), p->address, p->size, false, prot);
					if ( gLinkContext.verboseMapping ) {
						dyld::log("%18s at 0x%08llX->0x%08llX altered permissions to %c%c%c\n", "", p->address, 
							p->address+p->size-1,
							(prot & PROT_READ) ? 'r' : '.',  (prot & PROT_WRITE) ? 'w' : '.',  (prot & PROT_EXEC) ? 'x' : '.' );
					}
				}
			}
		#endif		}
		if ( gLinkContext.verboseMapping ) {
			// list the code blob			dyld_cache_header* header = (dyld_cache_header*)sSharedCache;
			uint64_t signatureSize = header->codeSignatureSize;
			// zero size in header means signature runs to end-of-file			if ( signatureSize == 0 ) {
				struct stat stat_buf;
				// FIXME: need size of cache file actually used				if ( my_stat(IPHONE_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME, &stat_buf) == 0 )
					signatureSize = stat_buf.st_size - header->codeSignatureOffset;
			}
			if ( signatureSize != 0 ) {
				const dyld_cache_mapping_info* const last = &start[dyld_shared_cache_ranges.sharedRegionsCount-1];
				uint64_t codeBlobStart = last->address + last->size;
				dyld::log("        0x%08llX->0x%08llX (code signature)\n", codeBlobStart, codeBlobStart+signatureSize);
			}
		}#if __IPHONE_OS_VERSION_MIN_REQUIRED		// check for file that enables dyld shared cache dylibs to be overridden		struct stat enableStatBuf;
		// check file size to determine if correct file is in place. 
		// See <rdar://problem/13591370> Need a way to disable roots without removing /S/L/C/com.apple.dyld/enable...		sDylibsOverrideCache = ( (my_stat(IPHONE_DYLD_SHARED_CACHE_DIR "enable-dylibs-to-override-cache", &enableStatBuf) == 0)
									&& (enableStatBuf.st_size < ENABLE_DYLIBS_TO_OVERRIDE_CACHE_SIZE) );#endif	
	}}

通过使用上一篇文章中介绍过的有趣功能,我们可以在gdb内核调试器中调试用户模式应用程序,然后通过逐步对此功能进行调试,并查看失败的原因。为此,我们使用HLT指令对dyld进行了修补,修改后的QEMU将其视为断点。然后我们用jtool重新签名了可执行文件,并将新的签名添加到静态信任缓存中。

18.jpg

于是,我们终于弄清了失败的原因:

						if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, codeSignatureMappingIndex, cacheSlide, slideInfo, slideInfoSize) == 0) {
							// successfully mapped cache into shared region
							sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
							sSharedCacheSlide = cacheSlide;
							dyld::gProcessInfo->sharedCacheSlide = cacheSlide;
							//dyld::log("sSharedCache=%p sSharedCacheSlide=0x%08lX\n", sSharedCache, sSharedCacheSlide);
							// if cache has a uuid, copy it
							if ( header->mappingOffset >= 0x68 ) {
								memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
							}
						}
						else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED
							throw "dyld shared cache could not be mapped";
#endif
							if ( gLinkContext.verboseMapping ) 
								dyld::log("dyld: shared cached file could not be mapped\n");
						}

19.jpg

好在我们正在运行内核调试器,所以可以直接进入内核系统调用并查看它失败的位置。另外,我们还获得了一些版本的代码:

int
shared_region_map_and_slide_np(
	struct proc				*p,
	struct shared_region_map_and_slide_np_args	*uap,
	__unused int					*retvalp)
{
	struct shared_file_mapping_np	*mappings;
	unsigned int			mappings_count = uap->count;
	kern_return_t			kr = KERN_SUCCESS;
	uint32_t			slide = uap->slide;
	
#define SFM_MAX_STACK	8
	struct shared_file_mapping_np	stack_mappings[SFM_MAX_STACK];

	/* Is the process chrooted?? */
	if (p->p_fd->fd_rdir != NULL) {
		kr = EINVAL;
		goto done;
	}
		
	if ((kr = vm_shared_region_sliding_valid(slide)) != KERN_SUCCESS) {
		if (kr == KERN_INVALID_ARGUMENT) {
			/*
			 * This will happen if we request sliding again 
			 * with the same slide value that was used earlier
			 * for the very first sliding.
			 */
			kr = KERN_SUCCESS;
		}
		goto done;
	}

	if (mappings_count == 0) {
		SHARED_REGION_TRACE_INFO(
			("shared_region: %p [%d(%s)] map(): "
			 "no mappings\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm));
		kr = 0;	/* no mappings: we're done ! */
		goto done;
	} else if (mappings_count <= SFM_MAX_STACK) {
		mappings = &stack_mappings[0];
	} else {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(): "
			 "too many mappings (%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 mappings_count));
		kr = KERN_FAILURE;
		goto done;
	}

	if ( (kr = shared_region_copyin_mappings(p, uap->mappings, uap->count, mappings))) {
		goto done;
	}


	kr = _shared_region_map_and_slide(p, uap->fd, mappings_count, mappings,
					  slide,
					  uap->slide_start, uap->slide_size);
	if (kr != KERN_SUCCESS) {
		return kr;
	}

done:
	return kr;
}

通过逐步调试调试器中的代码,我们发现对_shared_region_map_and_slide()的调用实际上是失败的。

/*
 * shared_region_map_np()
 *
 * This system call is intended for dyld.
 *
 * dyld uses this to map a shared cache file into a shared region.
 * This is usually done only the first time a shared cache is needed.
 * Subsequent processes will just use the populated shared region without
 * requiring any further setup.
 */
int
_shared_region_map_and_slide(
	struct proc				*p,
	int					fd,
	uint32_t				mappings_count,
	struct shared_file_mapping_np		*mappings,
	uint32_t				slide,
	user_addr_t				slide_start,
	user_addr_t				slide_size)
{
	int				error;
	kern_return_t			kr;
	struct fileproc			*fp;
	struct vnode			*vp, *root_vp, *scdir_vp;
	struct vnode_attr		va;
	off_t				fs;
	memory_object_size_t		file_size;
#if CONFIG_MACF
	vm_prot_t			maxprot = VM_PROT_ALL;
#endif
	memory_object_control_t		file_control;
	struct vm_shared_region		*shared_region;
	uint32_t			i;

	SHARED_REGION_TRACE_DEBUG(
		("shared_region: %p [%d(%s)] -> map\n",
		 (void *)VM_KERNEL_ADDRPERM(current_thread()),
		 p->p_pid, p->p_comm));

	shared_region = NULL;
	fp = NULL;
	vp = NULL;
	scdir_vp = NULL;

	/* get file structure from file descriptor */
	error = fp_lookup(p, fd, &fp, 0);
	if (error) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map: "
			 "fd=%d lookup failed (error=%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm, fd, error));
		goto done;
	}

	/* make sure we're attempting to map a vnode */
	if (FILEGLOB_DTYPE(fp->f_fglob) != DTYPE_VNODE) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map: "
			 "fd=%d not a vnode (type=%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 fd, FILEGLOB_DTYPE(fp->f_fglob)));
		error = EINVAL;
		goto done;
	}

	/* we need at least read permission on the file */
	if (! (fp->f_fglob->fg_flag & FREAD)) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map: "
			 "fd=%d not readable\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm, fd));
		error = EPERM;
		goto done;
	}

	/* get vnode from file structure */
	error = vnode_getwithref((vnode_t) fp->f_fglob->fg_data);
	if (error) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map: "
			 "fd=%d getwithref failed (error=%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm, fd, error));
		goto done;
	}
	vp = (struct vnode *) fp->f_fglob->fg_data;

	/* make sure the vnode is a regular file */
	if (vp->v_type != VREG) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "not a file (type=%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp),
			 vp->v_name, vp->v_type));
		error = EINVAL;
		goto done;
	}

#if CONFIG_MACF
	/* pass in 0 for the offset argument because AMFI does not need the offset
		of the shared cache */
	error = mac_file_check_mmap(vfs_context_ucred(vfs_context_current()),
			fp->f_fglob, VM_PROT_ALL, MAP_FILE, 0, &maxprot);
	if (error) {
		goto done;
	}
#endif /* MAC */

	/* make sure vnode is on the process's root volume */
	root_vp = p->p_fd->fd_rdir;
	if (root_vp == NULL) {
		root_vp = rootvnode;
	} else {
		/*
		 * Chroot-ed processes can't use the shared_region.
		 */
		error = EINVAL;
		goto done;
	}

	if (vp->v_mount != root_vp->v_mount) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "not on process's root volume\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name));
		error = EPERM;
		goto done;
	}

	/* make sure vnode is owned by "root" */
	VATTR_INIT(&va);
	VATTR_WANTED(&va, va_uid);
	error = vnode_getattr(vp, &va, vfs_context_current());
	if (error) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "vnode_getattr(%p) failed (error=%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name,
			 (void *)VM_KERNEL_ADDRPERM(vp), error));
		goto done;
	}
	if (va.va_uid != 0) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "owned by uid=%d instead of 0\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp),
			 vp->v_name, va.va_uid));
		error = EPERM;
		goto done;
	}

	if (scdir_enforce) {
		/* get vnode for scdir_path */
		error = vnode_lookup(scdir_path, 0, &scdir_vp, vfs_context_current());
		if (error) {
			SHARED_REGION_TRACE_ERROR(
				("shared_region: %p [%d(%s)] map(%p:'%s'): "
				 "vnode_lookup(%s) failed (error=%d)\n",
				 (void *)VM_KERNEL_ADDRPERM(current_thread()),
				 p->p_pid, p->p_comm,
				 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name,
				 scdir_path, error));
			goto done;
		}

		/* ensure parent is scdir_vp */
		if (vnode_parent(vp) != scdir_vp) {
			SHARED_REGION_TRACE_ERROR(
				("shared_region: %p [%d(%s)] map(%p:'%s'): "
				 "shared cache file not in %s\n",
				 (void *)VM_KERNEL_ADDRPERM(current_thread()),
				 p->p_pid, p->p_comm,
				 (void *)VM_KERNEL_ADDRPERM(vp),
				 vp->v_name, scdir_path));
			error = EPERM;
			goto done;
		}
	}

	/* get vnode size */
	error = vnode_size(vp, &fs, vfs_context_current());
	if (error) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "vnode_size(%p) failed (error=%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name,
			 (void *)VM_KERNEL_ADDRPERM(vp), error));
		goto done;
	}
	file_size = fs;

	/* get the file's memory object handle */
	file_control = ubc_getobject(vp, UBC_HOLDOBJECT);
	if (file_control == MEMORY_OBJECT_CONTROL_NULL) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "no memory object\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name));
		error = EINVAL;
		goto done;
	}

	/* check that the mappings are properly covered by code signatures */
	if (!cs_system_enforcement()) {
		/* code signing is not enforced: no need to check */
	} else for (i = 0; i < mappings_count; i++) {
		if (mappings[i].sfm_init_prot & VM_PROT_ZF) {
			/* zero-filled mapping: not backed by the file */
			continue;
		}
		if (ubc_cs_is_range_codesigned(vp,
					       mappings[i].sfm_file_offset,
					       mappings[i].sfm_size)) {
			/* this mapping is fully covered by code signatures */
			continue;
		}
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "mapping #%d/%d [0x%llx:0x%llx:0x%llx:0x%x:0x%x] "
			 "is not code-signed\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name,
			 i, mappings_count,
			 mappings[i].sfm_address,
			 mappings[i].sfm_size,
			 mappings[i].sfm_file_offset,
			 mappings[i].sfm_max_prot,
			 mappings[i].sfm_init_prot));
		error = EINVAL;
		goto done;
	}

	/* get the process's shared region (setup in vm_map_exec()) */
	shared_region = vm_shared_region_trim_and_get(current_task());
	if (shared_region == NULL) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "no shared region\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name));
		error = EINVAL;
		goto done;
	}

	/* map the file into that shared region's submap */
	kr = vm_shared_region_map_file(shared_region,
				       mappings_count,
				       mappings,
				       file_control,
				       file_size,
				       (void *) p->p_fd->fd_rdir,
				       slide,
				       slide_start,
				       slide_size);
	if (kr != KERN_SUCCESS) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "vm_shared_region_map_file() failed kr=0x%x\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name, kr));
		switch (kr) {
		case KERN_INVALID_ADDRESS:
			error = EFAULT;
			break;
		case KERN_PROTECTION_FAILURE:
			error = EPERM;
			break;
		case KERN_NO_SPACE:
			error = ENOMEM;
			break;
		case KERN_FAILURE:
		case KERN_INVALID_ARGUMENT:
		default:
			error = EINVAL;
			break;
		}
		goto done;
	}

	error = 0;

	vnode_lock_spin(vp);

	vp->v_flag |= VSHARED_DYLD;

	vnode_unlock(vp);

	/* update the vnode's access time */
	if (! (vnode_vfsvisflags(vp) & MNT_NOATIME)) {
		VATTR_INIT(&va);
		nanotime(&va.va_access_time);
		VATTR_SET_ACTIVE(&va, va_access_time);
		vnode_setattr(vp, &va, vfs_context_current());
	}

	if (p->p_flag & P_NOSHLIB) {
		/* signal that this process is now using split libraries */
		OSBitAndAtomic(~((uint32_t)P_NOSHLIB), &p->p_flag);
	}

done:
	if (vp != NULL) {
		/*
		 * release the vnode...
		 * ubc_map() still holds it for us in the non-error case
		 */
		(void) vnode_put(vp);
		vp = NULL;
	}
	if (fp != NULL) {
		/* release the file descriptor */
		fp_drop(p, fd, fp, 0);
		fp = NULL;
	}
	if (scdir_vp != NULL) {
		(void)vnode_put(scdir_vp);
		scdir_vp = NULL;
	}

	if (shared_region != NULL) {
		vm_shared_region_deallocate(shared_region);
	}

	SHARED_REGION_TRACE_DEBUG(
		("shared_region: %p [%d(%s)] <- map\n",
		 (void *)VM_KERNEL_ADDRPERM(current_thread()),
		 p->p_pid, p->p_comm));

	return error;
}

通过在内核调试器中逐步执行此函数,我们发现原理真正的故障在下图中的那个部分:

	/* make sure vnode is owned by "root" */
	VATTR_INIT(&va);
	VATTR_WANTED(&va, va_uid);
	error = vnode_getattr(vp, &va, vfs_context_current());
	if (error) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "vnode_getattr(%p) failed (error=%d)\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp), vp->v_name,
			 (void *)VM_KERNEL_ADDRPERM(vp), error));
		goto done;
	}
	if (va.va_uid != 0) {
		SHARED_REGION_TRACE_ERROR(
			("shared_region: %p [%d(%s)] map(%p:'%s'): "
			 "owned by uid=%d instead of 0\n",
			 (void *)VM_KERNEL_ADDRPERM(current_thread()),
			 p->p_pid, p->p_comm,
			 (void *)VM_KERNEL_ADDRPERM(vp),
			 vp->v_name, va.va_uid));
		error = EPERM;
		goto done;
	}

问题是缓存文件不是由root拥有的,所以我们找到了一种在OSX上安装ramdisk并启用文件权限的方法,并对文件进行了限制。这使得bash能够工作!现在我们只有stdout,但是没有输入支持。

UART交互式I/O

现在,剩下的就是启用UART输入。在查看并反转了一些串行I/O处理代码之后,我们找到了这个部分,它会决定是否启用UART输入(默认情况下是关闭的):

23.jpg

这段代码读取一个全局值,并检查#1。如果已打开,则启用UART输入。通过检查这个全局值,我们可以看到它在以下部分中设置:

24.jpg

该值是由serial启动参数得出的,最后,通过将serial启动参数设置为2,我们得到了一个交互式bash shell!

我们本次研究的目的是让iOS系统在无需事先或在启动过程中修复内核的情况下顺利启动,使用新模块扩展QEMU执行arm64 XNU系统的功能,并获得交互式bash shell。我们会在本文中介绍如何在QEMU上执行iOS并启动一个交互式bash shell。在第二篇文章中,我们将详细介绍为实现这些目标所进行的一些研究。在本次研究中,我们选择的iOS版本和设备是iOS 12.1和iPhone 6s Plus,因为与通常删除大多数符号的其他iOS内核映像相比,这个特定的iOS 12映像在内核映像中导出了许多符号。这带来了一些更大的挑战,因为它是一个使用安全监控器映像的非KTRR设备(Kernel Text Readonly Region,内核文本只读区域)。需要说明的是本文的研究是在这个项目的研究基础上进行的。另一个变化是我希望这个功能在外部模块中,以后可以扩展并用于为其他iOS设备和版本创建模块,而不是将代码放在核心QEMU代码中。

原有项目的介绍

你可以点此,获取包含qemu-scripts-aleph-git所需的脚本。该脚本允许使用只读安装的ram盘启动到用户模式,可以添加新的可执行文件和启动项(启动之前),并且通过模拟UART通道与用户通信,还可以使用复制到ram盘的主盘映像中的dyld缓存进行通信。以下是使用原有项目运行交互式bash shell的演示过程:

1.jpg

这使你可以使用你选择的任何权限执行你想要的任何用户模式进程,并使用内核调试器调试进程或内核:

2.jpg

原有项目的一些限制:

1.在安装ram盘之前,有一个很长的挂起过程(大概几秒);

2.该面目的方法仅适用于以只读方式安装的ram盘映像,并且大小最高为2GB;

3.我们只能通过UART与Guest iOS通信,目前没有其他通信渠道可用;

4.没有基本的硬件支持:屏幕,触摸,wifi,BT或其他任何东西;

5.目前仅支持单个CPU的模拟。

改进过程

要启动该过程,我们首先需要准备内核映像、安全监控器映像,设备树(device tree),静态信任缓存和ram盘映像。要获取映像,我们需要首先获取iOS 12.1更新文件。这实际上是一个zip文件,我们可以提取的内容如下:

Downloads jonathanafek$ unzip iPhone_5.5_12.1_16B92_Restore.ipsw
Archive:  iPhone_5.5_12.1_16B92_Restore.ipsw
   creating: Firmware/
  inflating: Restore.plist           
   creating: Firmware/usr/
   creating: Firmware/usr/local/
  inflating: BuildManifest.plist     
  inflating: Firmware/Mav10-7.21.00.Release.plist  
   creating: Firmware/all_flash/
  inflating: Firmware/all_flash/DeviceTree.n66ap.im4p.plist  
  inflating: Firmware/all_flash/LLB.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/LLB.n66.RELEASE.im4p  
  inflating: Firmware/all_flash/sep-firmware.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/iBoot.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/iBoot.n66m.RELEASE.im4p  
  inflating: Firmware/all_flash/iBoot.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/DeviceTree.n66ap.im4p  
  inflating: Firmware/all_flash/sep-firmware.n66m.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone-lightning.im4p  
   creating: Firmware/dfu/
  inflating: Firmware/dfu/iBSS.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone-lightning.im4p  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/dfu/iBEC.n66m.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBSS.n66.RELEASE.im4p  
  inflating: Firmware/048-32459-105.dmg.trustcache  
  inflating: Firmware/dfu/iBSS.n66m.RELEASE.im4p  
  inflating: Firmware/dfu/iBEC.n56.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/sep-firmware.n56.RELEASE.im4p  
  inflating: Firmware/Mav13-5.21.00.Release.bbfw  
  inflating: Firmware/all_flash/sep-firmware.n66m.RELEASE.im4p  
  inflating: Firmware/all_flash/LLB.n66m.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/iBoot.n66.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBSS.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/DeviceTree.n66map.im4p.plist  
  inflating: Firmware/all_flash/DeviceTree.n56ap.im4p.plist  
  inflating: Firmware/all_flash/LLB.n66.RELEASE.im4p.plist  
   creating: Firmware/AOP/
  inflating: Firmware/AOP/aopfw-s8000aop.im4p  
  inflating: Firmware/dfu/iBEC.n56.RELEASE.im4p  
  inflating: Firmware/all_flash/LLB.n66m.RELEASE.im4p  
  inflating: Firmware/all_flash/iBoot.n66.RELEASE.im4p  
  inflating: Firmware/all_flash/sep-firmware.n66.RELEASE.im4p  
  inflating: Firmware/048-31952-103.dmg.trustcache  
  inflating: Firmware/all_flash/sep-firmware.n66.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBSS.n66.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/DeviceTree.n66map.im4p  
  inflating: Firmware/dfu/iBSS.n66m.RELEASE.im4p.plist  
  inflating: Firmware/all_flash/[email protected]~iphone.im4p  
  inflating: Firmware/all_flash/iBoot.n66m.RELEASE.im4p.plist  
  inflating: 048-32651-104.dmg       
  inflating: Firmware/all_flash/LLB.n56.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBEC.n66.RELEASE.im4p  
  inflating: Firmware/dfu/iBEC.n66.RELEASE.im4p.plist  
  inflating: Firmware/dfu/iBEC.n66m.RELEASE.im4p  
  inflating: kernelcache.release.iphone7  
  inflating: Firmware/048-32651-104.dmg.trustcache  
  inflating: Firmware/Mav13-5.21.00.Release.plist  
  inflating: Firmware/all_flash/DeviceTree.n56ap.im4p  
  inflating: Firmware/Mav10-7.21.00.Release.bbfw  
  inflating: 048-32459-105.dmg       
  inflating: kernelcache.release.n66  
 extracting: 048-31952-103.dmg

接下来,我们需要复制用来支持项目继续进行的脚本存储库:

Downloads jonathanafek$ git clone [email protected]:alephsecurity/xnu-qemu-arm64-scripts.git
Cloning into 'xnu-qemu-arm64-scripts'...
remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 16 (delta 4), reused 16 (delta 4), pack-reused 0
Receiving objects: 100% (16/16), 5.16 KiB | 5.16 MiB/s, done.
Resolving deltas: 100% (4/4), done.

并提取ASN1的内核映像:

Downloads jonathanafek$ python xnu-qemu-arm64-scripts/asn1kerneldecode.py kernelcache.release.n66 kernelcache.release.n66.asn1decoded

该解码映像现在就包括压缩内核和安全监控器映像,把它们都提取出来:

Downloads jonathanafek$ python xnu-qemu-arm64-scripts/decompress_lzss.py kernelcache.release.n66.asn1decoded kernelcache.release.n66.out
Downloads jonathanafek$ python xnu-qemu-arm64-scripts/kernelcompressedextractmonitor.py kernelcache.release.n66.asn1decoded securemonitor.out

现在,让我们准备一个我们可以启动的设备树(关于设备树的更多细节将在第二篇文章中介绍)。首先,从ASN1编码文件中提取它:

Downloads jonathanafek$ python xnu-qemu-arm64-scripts/asn1dtredecode.py Firmware/all_flash/DeviceTree.n66ap.im4p Firmware/all_flash/DeviceTree.n66ap.im4p.out

然后,解析它并修改它,以使我们的内核在QEMU上启动:

Downloads jonathanafek$ python xnu-qemu-arm64-scripts/read_device_tree.py Firmware/all_flash/DeviceTree.n66ap.im4p.out Firmware/all_flash/DeviceTree.n66ap.im4p.out.mod

现在我们必须设置ram盘,首先,用ASN1解码它:

Downloads jonathanafek$ python xnu-qemu-arm64-scripts/asn1rdskdecode.py ./048-32651-104.dmg ./048-32651-104.dmg.out

接下来,调整它的大小,使其具有动态加载程序缓存文件的空间(bash和其他可执行文件需要这些空间),安装它,并强制使用它的文件权限:

Downloads jonathanafek$ hdiutil resize -size 1.5G -imagekey diskimage-class=CRawDiskImage 048-32651-104.dmg.out
Downloads jonathanafek$ hdiutil attach -imagekey diskimage-class=CRawDiskImage 048-32651-104.dmg.out
Downloads jonathanafek$ sudo diskutil enableownership /Volumes/PeaceB16B92.arm64UpdateRamDisk/

现在,让我们通过双击常规更新磁盘映像来安装它:048-31952-103.dmg。

在ram磁盘中创建一个动态加载器缓存目录,将缓存从更新映像复制到root:

Downloads jonathanafek$ sudo mkdir -p /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/Caches/com.apple.dyld/
Downloads jonathanafek$ sudo cp /Volumes/PeaceB16B92.N56N66OS/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/Caches/com.apple.dyld/
Downloads jonathanafek$ sudo chown root /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64

从rootlessJB或iOSBinaries获取适用于iOS的预编译用户模式工具,包括bash。或者,按照此处的描述编译自己的iOS控制台二进制文件。

Downloads jonathanafek$ git clone https://github.com/jakeajames/rootlessJB
Cloning into 'rootlessJB'...
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 253 (delta 2), reused 0 (delta 0), pack-reused 247
Receiving objects: 100% (253/253), 7.83 MiB | 3.03 MiB/s, done.
Resolving deltas: 100% (73/73), done.
Downloads jonathanafek$ cd rootlessJB/rootlessJB/bootstrap/tars/
tars jonathanafek$ tar xvf iosbinpack.tar
tars jonathanafek$ sudo cp -R iosbinpack64 /Volumes/PeaceB16B92.arm64UpdateRamDisk/
tars jonathanafek$ cd -

配置launchd以不执行任何服务:

Downloads jonathanafek$ sudo rm /Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/LaunchDaemons/*

现在,通过在/Volumes/PeaceB16B92.arm64UpdateRamDisk/System/Library/LaunchDaemons/com.apple.bash.plist下创建一个新文件,来,将其配置为启动交互式bash shell,其中包含以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>EnablePressuredExit</key>
        <false/>
        <key>Label</key>
        <string>com.apple.bash</string>
        <key>POSIXSpawnType</key>
        <string>Interactive</string>
        <key>ProgramArguments</key>
        <array>
                <string>/iosbinpack64/bin/bash</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>StandardErrorPath</key>
        <string>/dev/console</string>
        <key>StandardInPath</key>
        <string>/dev/console</string>
        <key>StandardOutPath</key>
        <string>/dev/console</string>
        <key>Umask</key>
        <integer>0</integer>
        <key>UserName</key>
        <string>root</string>
</dict>
</plist>

附带说明一下,你可以将iOS映像中找到的二进制plist文件转换成文本xml格式,然后用以下命令返回二进制格式:

Downloads jonathanafek$ plutil -convert xml1 file.plist
Downloads jonathanafek$ vim file.plist
Downloads jonathanafek$ plutil -convert binary1 file.plist

对于启动守护进程,iOS同时接受xml和二进制plist文件。

由于新二进制文件不是由Apple签名的,因此它们需要被我们将要创建的静态信任缓存所信任。为此,我们需要获得jtool(也可以通过Homebrew :brew cask install jtool)。一旦有了该工具,我们就必须在希望被信任的每个二进制文件上运行它,提取其CDHash的前40个字符,并将其放在一个名为tchashes的新文件中。 以下是jtool的执行过程:

Downloads jonathanafek$ jtool --sig --ent /Volumes/PeaceB16B92.arm64UpdateRamDisk/iosbinpack64/bin/bash
Blob at offset: 1308032 (10912 bytes) is an embedded signature
Code Directory (10566 bytes)
                Version:     20001
                Flags:       none
                CodeLimit:   0x13f580
                Identifier:  /Users/jakejames/Desktop/jelbreks/multi_path/multi_path/iosbinpack64/bin/bash (0x58)
                CDHash:      7ad4d4c517938b6fdc0f5241cd300d17fbb52418b1a188e357148f8369bacad1 (computed)
                # of Hashes: 320 code + 5 special
                Hashes @326 size: 32 Type: SHA-256
 Empty requirement set (12 bytes)
Entitlements (279 bytes) :
--
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>platform-application</key>
    <true/>
    <key>com.apple.private.security.container-required</key>
    <false/>
</dict>
</plist>

在上面的执行过程中,我们需要在tchashes中写入7ad4d4c517938b6fdc0f5241cd300d17fbb52418。为方便起见,以下命令将从我们放入映像的每个二进制文件中提取正确的哈希部分:

Downloads jonathanafek$ for filename in $(find /Volumes/PeaceB16B92.arm64UpdateRamDisk/iosbinpack64 -type f); do jtool --sig --ent $filename 2&>/dev/null; done | grep CDHash | cut -d' ' -f6 | cut -c 1-40
ebe945ddbb4dbeb1ee9624e6ba1932d2ec61cfde
7ad4d4c517938b6fdc0f5241cd300d17fbb52418
0cf1b00e3bf76ab51c56da7ca888e89359f1d1c4
c9c1e21c3f3593c99f4e7c91c64d7f3106ad29ce
522dda7f40fe6aa6e2038bc66c9cb31660a43429
dc040d340f1fcfb493394e77d9944aa164e23ca3
f975cd0eec230299d1b8d9b0e3b54ae7cf660d92
728be7f7a78f400742e887f7ac93306145f822c0
4f4ca5aa3e506d145f344d59504630b85ddefffc
0d274c72cefbff705db0ed0fda29fb6f4cacf4c9
ebcf9073fd59db7c59a5212b0824faf1d7b30e39
cf784ea216e6b49f66a3cc81aeceaf7ac39b71d7
9d625c7eaadc8fd3eb57d9facca294b1a5afab8a
90c02c153e636cac74ca09e7e3dc89c0508a1393
59cba1c5ce169d4cd454d43e3a3c6fa824cf2764
9ff1194d135e979a632033ec2df63ba0cfe4682a
d11b49576e0f6645c4c9f234497f51219173dce8
7a01f3e7bcda18b26297c3936c9e256ddf8f9fe3
b7fd47df9b6652f2810cc789d5903a082af2570d
68a32f0a35bbb23f4f272ca99186521618c08d21
e04fa65a33c4b69d2338688ee72ea13d624a4255
b400373e16a7f82fa56d318038ec7b4b28e2593f
65859385e11b910de3841e53a833ab4c4b855282
2eae1b42c4f6bb95e3226aff8cb93a539c0a6263
c305e094747ba274f37e3063b826a5e41e5e2549
41620d4632bf6f071388033f8cf267123df16489
3bf1f6c49e3bcd775041864085893bf9b1ab3870
bb2d9c166635fc693e99355e84984aa61692c6f3
3bb79fd3568c3620a2bd7bad004ab759bec4e331
7c60ae6060d7bf2772c6b4b0c04b605c4e62a7a7
b904a692d548c3323621c17212121aca0c733088
6fe1d88bcbdd97d273533d695c04279f8ddf5e32
4165a869f1b35bdff90b74116499c1c210f27ddb
414ebc5e48c94d60b2018e4c83a323426bc0ac74
62b2b303c31e5fc9d5210b736d8d632eee28d24f
871e0ea84b71cd01e45e261542e9b2dd08fb81ab
0912c647e222bd04f05b837a8286519bd8ae2393
bd6d7d7f51b639da99e0581096534273b4f040ed
27ed9a3b21392bc459619293a6b36fe2c3b8ddac
e92565cbfdb0bd41d069384689ffae715e61b216
164fc2d96f9decd643ac33fc279b2078e51f5c88
3e0529b705d666af4f25c8c18fc7992f6934cf6f
176f273cb276085052519054d042508dc8d562b4
18762f5c54d935759f02248b032576bdc93be260
22d2f02d3be49da4819534553ad5ac37c0ace28c
e76bf6e8e84b656ee61b1ff10b38eab23607ae82
84bbc455477d6737f738b649c5afd3d4a069abee
57fe14db863b48f19cdec3c884c5dfad1bff6a12
e6ee59194bd768c3e3cc140009b6a729c7700a11
f1c25d5ac4e3924deaa3418a9ba309e15c09f502
e962bfddead7da46f23b6f4dc448df085e946940
26d34ca63bc69c8e81c15672258f3b8cbaf4ba4c
7fc69d2fc1f57ca555b07d6de51c82f74915c6bd
85f3c5263835d90b776886f92e8536ceb2f46036
0f1214d8a6138f170c2654a6f81c40586fbebaac
dc995e91bc0b67c52b969c91c1d68b09bbf94ec2
5d46a9681b4a3cc84a69083288e76aa969ec3a43
3c0db01f7aaf0a5b935dfcc51f6b2534013795ad
8422f07e41b2951e4138b88e013eab5773ae52f7
f9c4cca6b141064b7ae97131ff3969386d624718
259733b48f2f4fa88ba4f2e5f519bd40a6a3750d
8e06a919d28c3c0376b1207981d70b3bda99b6bc
68cd528c435b417c6f0022a132d459fc25d6e039
d176fa07a7ea5bfe88b9d2d703f3c65b4298b2e6
30f3d6e1d00614a0a9e8e8a3d4f31b8c68066091
698587325d71b9d51c22ae26e0c2de8ca70f6dc8
ccf27e4d7b62f1f839cfb9d70340efd1a2b77532
928a02f17cef27a5528ae055a467a18528f2aff5
4d24ada94fa70d27a684867541266f264261ce36
ab3e7808ee41f4536ece24091d1f166c5f0e9b63
e492332b87adc07406503ca857b6f3e2a3f0625d
d121b2de1778563183087238c4675316176f159d
12fe31a31132f7c0bab2857c0b3ac3c71cdb9dae
d6bc5428d129dd76695519b9b7f201daa9eb87de
685660477e1f851a90ace593670e5288d2168a24
94a493c2909f8b563e0076956bec7a1941455ed3
13c2e0251ba0469f2e1ec3d61da61c664822c791
e6332fc916f9b06f4987ecbaa23bbf4fa374c68f
1f6f82bcc994a4559d891d3a9e187268632da0b9
f864bd7891b9a0970f3ea05f13f7769289e62803
ba84abbeb198b91cbefec678096c8fd17387657d
d537ff6ab7d2bf38b0f18e964ad3525f2761b535
1acf88c15c1a08b3387b62969a34a95196632932
345d3b92a7f8a11c0872ec9ec439b5a6a2ada104
067b54e23cd6bc5b007113929dc4e2d2868228b6
11794790670afe1b651ed838362bb955e1503706
973674b1cf5f51119fa655ad2393df3dee9f44cc
c59738382faa4b7f803359d0c92dd53d6479ffb8
e3285e8252c44404675876ae0104f02cdc36574c
41c139fa86a3e67d49566d11a7d1d14fe375b564
b52692291cc4d9c9f09bc0ba650904d889674218
65713ffe304718b3b6a8b710b7db0467e52ca5aa
f2e77f5600970036ffdd5a06067491c5799a2ebd
cb08034d4647f2cc921b62ea648a76b5635fcc13
a9fc0262a6925ec1c18b0bf627c04c60fa5b5ecf
3736f93cc5f88d138f58016fdce2c3c3af979c43
183cd29cea8ba53f6e5d28d87e37b0cc603106c6
cd0281c8fa808c3f0f0b74db8c262a6997f52d03
e3016edd7acfa4d24d2eacec4918f3018d9d2449
ddd943f2a4192b3eabbb0580c64ff23ea7c31387
e3285e8252c44404675876ae0104f02cdc36574c
41c139fa86a3e67d49566d11a7d1d14fe375b564
b52692291cc4d9c9f09bc0ba650904d889674218
8af0e498ca73e05155f10fe7c26cfbdd9762ff24
73657606cb288c85f909da3ec4b92d7f8819ae79
918a3cf30a9c9d6ee2872c670421e528883221ae
dcf5eeaefc7ec3e7a0166676f6ee564761f78bc6
994ada738587ba622bfe36b987e9bfa246ff3858
d6f9c9107eb6dc237040d18debd4244c3e4c1320
f0e0c6a7e5c4545bac0d9ebf7811997f5c7076ad
38a790a40cca659fb8a0942ba140aa07309a17aa
070472831955773d78c9f33aff696c0a67b06bda
4ca98aac5e3b9174beaa2e4175e33fdcddee6866
44bd100692ded0637a763d324490db7435216f8c
a28a364092033230a6045fd288cb503aedbdd072
bbbe8ea84bdc4f3004398895ee58979a55b744c0
a09ee84582821397aa68d81350ed07b9902d09cb
8f8f612996a91e4fb26deacf2c88b8eda42da7a2
504d7c5b0a0e72a3dc5177ec571f591f3dae2ade
c0b0dea10a283f9d904bad52c53e20b129ae278c
5b089432710347242dfb6ccfdfea6fc523d9fe60
40af3f97ae3dc743f638c82f4ed78bce13687c83
7b3d463b62ce306c86d88e7ec0e52964c073c223
580eb965a96782a1fd005bd8a27100abca8430e1
330efc667ea608575d863b10a41a73e49f31d1c6
5827c3ef16144d298fd04342fc7041dd3b20d35e
f9bce1706a98b2492750aaa977806549f7d010f7
eeeaeb163512c31c6462f41c6bc3b6a228224bee
2ae51c0fac8b5656ec91693e7f9846a9c4af8069
92c89c47a734cad1a36756155ea3043e406ae565
be0e71c532033d79d519951f0450cdca44f835c3
feff0ce891c71c69f581b19a70b30ffd4c407205
8b0f3f0c620f008d4b85b7aff69933d3aae6098e
296124c76c9f0201480678a012a1df2e6835c521
a1876907ad59843dc5ed1390c78c88698504b9d8
e3190fc3865f02092ab6725b25c485ea5c143e3b
8bbd9944ebc23ce2001a4837732ba082c040d0f4
6408ed0d9df71e7bdde2faa985e5c07911a43503
ca2b47f582135e00a9720215cc09881dd9b49b85
e7e478f2e7f9715d9b540c9f8d12993c83ece0c1
25ac265b51c484680decaf8903b0b3c12c5ff81c
5a37eb16c2eaba8dcb55d9edb3ba98a0ee09afd0

上面的输出应保存在tchashes中,然后我们可以创建静态信任缓存blob:

Downloads jonathanafek$ python xnu-qemu-arm64-scripts/create_trustcache.py tchashes static_tc

由于我们现在已准备好了所有映像和文件,现在是卸载这两个卷的好时机。卸载后,我们可以得到QEMU代码(有关QEMU工作的更详细信息将在本系列的第二篇文章中介绍):

Downloads jonathanafek$ git clone [email protected]:alephsecurity/xnu-qemu-arm64.git
Cloning into 'xnu-qemu-arm64'...
remote: Enumerating objects: 377340, done.
remote: Total 377340 (delta 0), reused 0 (delta 0), pack-reused 377340
Receiving objects: 100% (377340/377340), 187.68 MiB | 5.32 MiB/s, done.
Resolving deltas: 100% (304400/304400), done.
Checking out files: 100% (6324/6324), done.

编译它:

Downloads jonathanafek$ cd xnu-qemu-arm64
xnu-qemu-arm64 jonathanafek$ ./configure --target-list=aarch64-softmmu --disable-capstone
Install prefix    /usr/local
BIOS directory    /usr/local/share/qemu
firmware path     /usr/local/share/qemu-firmware
binary directory  /usr/local/bin
library directory /usr/local/lib
module directory  /usr/local/lib/qemu
libexec directory /usr/local/libexec
include directory /usr/local/include
config directory  /usr/local/etc
local state directory   /usr/local/var
Manual directory  /usr/local/share/man
ELF interp prefix /usr/gnemul/qemu-%M
Source path       /Users/jonathanafek/Downloads/xnu-qemu-arm64
GIT binary        git
GIT submodules    ui/keycodemapdb dtc
C compiler        cc
Host C compiler   cc
C++ compiler      c++
Objective-C compiler clang
ARFLAGS           rv
CFLAGS            -O2 -g
QEMU_CFLAGS       -I/opt/local/include/pixman-1 -I$(SRC_PATH)/dtc/libfdt -D_REENTRANT -I/opt/local/include/glib-2.0 -I/opt/local/lib/glib-2.0/include -I/opt/local/include -m64 -mcx16 -DOS_OBJECT_USE_OBJC=0 -arch x86_64 -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -Wstrict-prototypes -Wredundant-decls -Wall -Wundef -Wwrite-strings -Wmissing-prototypes -fno-strict-aliasing -fno-common -fwrapv  -Wno-error=address-of-packed-member -Wno-string-plus-int -Wno-initializer-overrides -Wexpansion-to-defined -Wendif-labels -Wno-shift-negative-value -Wno-missing-include-dirs -Wempty-body -Wnested-externs -Wformat-security -Wformat-y2k -Winit-self -Wignored-qualifiers -Wold-style-definition -Wtype-limits -fstack-protector-strong -I/opt/local/include -I/opt/local/include/p11-kit-1 -I/opt/local/include  -I/opt/local/include/libpng16 -I/opt/local/include
LDFLAGS           -framework Hypervisor -m64 -framework CoreFoundation -framework IOKit -arch x86_64 -g
QEMU_LDFLAGS      -L$(BUILD_DIR)/dtc/libfdt
make              make
install           install
python            python -B
smbd              /usr/sbin/smbd
module support    no
host CPU          x86_64
host big endian   no
target list       aarch64-softmmu
gprof enabled     no
sparse enabled    no
strip binaries    yes
profiler          no
static build      no
Cocoa support     yes
SDL support       no
GTK support       no
GTK GL support    no
VTE support       no
TLS priority      NORMAL
GNUTLS support    yes
GNUTLS rnd        yes
libgcrypt         no
libgcrypt kdf     no
nettle            yes (3.4.1)
nettle kdf        yes
libtasn1          yes
curses support    yes
virgl support     no
curl support      yes
mingw32 support   no
Audio drivers     coreaudio
Block whitelist (rw)
Block whitelist (ro)
VirtFS support    no
Multipath support no
VNC support       yes
VNC SASL support  yes
VNC JPEG support  no
VNC PNG support   yes
xen support       no
brlapi support    no
bluez  support    no
Documentation     yes
PIE               no
vde support       no
netmap support    no
Linux AIO support no
ATTR/XATTR support no
Install blobs     yes
KVM support       no
HAX support       yes
HVF support       yes
WHPX support      no
TCG support       yes
TCG debug enabled no
TCG interpreter   no
malloc trim support no
RDMA support      no
fdt support       git
membarrier        no
preadv support    no
fdatasync         no
madvise           yes
posix_madvise     yes
posix_memalign    yes
libcap-ng support no
vhost-net support no
vhost-crypto support no
vhost-scsi support no
vhost-vsock support no
vhost-user support yes
Trace backends    log
spice support     no
rbd support       no
xfsctl support    no
smartcard support no
libusb            no
usb net redir     no
OpenGL support    no
OpenGL dmabufs    no
libiscsi support  no
libnfs support    no
build guest agent yes
QGA VSS support   no
QGA w32 disk info no
QGA MSI support   no
seccomp support   no
coroutine backend sigaltstack
coroutine pool    yes
debug stack usage no
mutex debugging   no
crypto afalg      no
GlusterFS support no
gcov              gcov
gcov enabled      no
TPM support       yes
libssh2 support   no
TPM passthrough   no
TPM emulator      yes
QOM debugging     yes
Live block migration yes
lzo support       no
snappy support    no
bzip2 support     yes
NUMA host support no
libxml2           yes
tcmalloc support  no
jemalloc support  no
avx2 optimization no
replication support yes
VxHS block device no
capstone          no
docker            no

xnu-qemu-arm64 jonathanafek$ make -j16
xnu-qemu-arm64 jonathanafek$ cd -

接下来要做的就是执行:

Downloads jonathanafek$ ./xnu-qemu-arm64/aarch64-softmmu/qemu-system-aarch64 -M iPhone6splus-n66-s8000,kernel-filename=kernelcache.release.n66.out,dtb-filename=Firmware/all_flash/DeviceTree.n66ap.im4p.out.mod,secmon-filename=securemonitor.out,ramdisk-filename=048-32651-104.dmg.out,tc-filename=static_tc,kern-cmd-args="debug=0x8 kextlog=0xfff cpus=1 rd=md0 serial=2" -cpu max -m 6G -serial mon:stdio
iBoot version:
corecrypto_kext_start called
FIPSPOST_KEXT [38130750] fipspost_post:156: PASSED: (6 ms) - fipspost_post_integrity
FIPSPOST_KEXT [38201250] fipspost_post:162: PASSED: (2 ms) - fipspost_post_hmac
FIPSPOST_KEXT [38233562] fipspost_post:163: PASSED: (0 ms) - fipspost_post_aes_ecb
FIPSPOST_KEXT [38275375] fipspost_post:164: PASSED: (1 ms) - fipspost_post_aes_cbc
FIPSPOST_KEXT [41967250] fipspost_post:165: PASSED: (153 ms) - fipspost_post_rsa_sig
FIPSPOST_KEXT [44373250] fipspost_post:166: PASSED: (99 ms) - fipspost_post_ecdsa
FIPSPOST_KEXT [44832437] fipspost_post:167: PASSED: (18 ms) - fipspost_post_ecdh
FIPSPOST_KEXT [44861312] fipspost_post:168: PASSED: (0 ms) - fipspost_post_drbg_ctr
FIPSPOST_KEXT [44922625] fipspost_post:169: PASSED: (2 ms) - fipspost_post_aes_ccm
FIPSPOST_KEXT [44994250] fipspost_post:171: PASSED: (2 ms) - fipspost_post_aes_gcm
FIPSPOST_KEXT [45042125] fipspost_post:172: PASSED: (1 ms) - fipspost_post_aes_xts
FIPSPOST_KEXT [45109687] fipspost_post:173: PASSED: (2 ms) - fipspost_post_tdes_cbc
FIPSPOST_KEXT [45167062] fipspost_post:174: PASSED: (1 ms) - fipspost_post_drbg_hmac
FIPSPOST_KEXT [45178250] fipspost_post:197: all tests PASSED (300 ms)
Darwin Image4 Validation Extension Version 1.0.0: Tue Oct 16 21:46:27 PDT 2018; root:AppleImage4-1.200.18~1853/AppleImage4/RELEASE_ARM64
AppleS8000IO::start: chip-revision: A0
AppleS8000IO::start: this: <ptr>, TCC virt addr: <ptr>, TCC phys addr: 0x202240000
AUC[<ptr>]::init(<ptr>)
AUC[<ptr>]::probe(<ptr>, <ptr>)
AppleCredentialManager: init: called, instance = <ptr>.
ACMRM: init: called, ACMDRM_ENABLED=YES, ACMDRM_STATE_PUBLISHING_ENABLED=YES, ACMDRM_KEYBAG_OBSERVING_ENABLED=YES.
ACMRM: _loadRestrictedModeForceEnable: restricted mode force-enabled = 0 .
ACMRM-A: init: called, .
ACMRM-A: _loadAnalyticsCollectionPeriod: analytics collection period = 86400 .
ACMRM: _loadStandardModeTimeout: standard mode timeout = 259200 .
ACMRM-A: notifyStandardModeTimeoutChanged: called, value = 259200 (modified = YES).
ACMRM: _loadGracePeriodTimeout: device lock timeout = 3600 .
ACMRM-A: notifyGracePeriodTimeoutChanged: called, value = 3600 (modified = YES).
AppleCredentialManager: init: returning, result = true, instance = <ptr>.
AUC[<ptr>]::start(<ptr>)
virtual bool AppleARMLightEmUp::start(IOService *): starting...
AppleKeyStore starting (BUILT: Oct 17 2018 20:34:07)
AppleSEPKeyStore::start: _sep_enabled = 1
AppleCredentialManager: start: called, instance = <ptr>.
ACMRM: _publishIOResource: AppleUSBRestrictedModeTimeout = 259200.
AppleCredentialManager: start: initializing power management, instance = <ptr>.
AppleCredentialManager: start: started, instance = <ptr>.
AppleCredentialManager: start: returning, result = true, instance = <ptr>.
AppleARMPE::getGMTTimeOfDay can not provide time of day: RTC did not show up
: apfs_module_start:1277: load: com.apple.filesystems.apfs, v748.220.3, 748.220.3, 2018/10/16
com.apple.AppleFSCompressionTypeZlib kmod start
IOSurfaceRoot::installMemoryRegions()
IOSurface disallowing global lookups
apfs_sysctl_register:911: done registering sysctls.
com.apple.AppleFSCompressionTypeZlib load succeeded
L2TP domain init
L2TP domain init complete
PPTP domain init
BSD root: md0, major 2, minor 0
apfs_vfsop_mountroot:1468: apfs: mountroot called!
apfs_vfsop_mount:1231: unable to root from devvp <ptr> (root_device): 2
apfs_vfsop_mountroot:1472: apfs: mountroot failed, error: 2
hfs: mounted PeaceB16B92.arm64UpdateRamDisk on device b(2, 0)
: : Darwin Bootstrapper Version 6.0.0: Tue Oct 16 22:26:06 PDT 2018; root:libxpc_executables-1336.220.5~209/launchd/RELEASE_ARM64
boot-args = debug=0x8 kextlog=0xfff cpus=1 rd=md0 serial=2
Thu Jan  1 00:01:05 1970 localhost com.apple.xpc.launchd[1] <Notice>: Restore environment starting.
Thu Jan  1 00:01:05 1970 localhost com.apple.xpc.launchd[1] <Notice>: Early boot complete. Continuing system boot.
Thu Jan  1 00:01:06 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Error>: Could not read path: path = /AppleInternal/Library/LaunchDaemons, error = 2: No such file or directory
Thu Jan  1 00:01:06 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Error>: Could not read path: path = /System/Library/NanoLaunchDaemons, error = 2: No such file or directory
Thu Jan  1 00:01:06 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Error>: Failed to bootstrap path: path = /System/Library/NanoLaunchDaemons, error = 2: No such file or directory
bash-4.4# export PATH=$PATH:/iosbinpack64/usr/bin:/iosbinpack64/bin:/iosbinpack64/usr/sbin:/iosbinpack64/sbin
bash-4.4# id
uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),20(staff),29(certusers),80(admin)
bash-4.4# pwd
/
bash-4.4# ls -la
total 18
drwxr-xr-x  17 root    wheel  748 Jun 10  2019 .
drwxr-xr-x  17 root    wheel  748 Jun 10  2019 ..
-rw-r--r--   1 root    wheel    0 Oct 20  2018 .Trashes
drwx------   2 mobile  staff  170 Jun 10  2019 .fseventsd
drwxr-xr-x   4 root    wheel  136 Oct 20  2018 System
drwxr-xr-x   2 root    wheel  272 Oct 20  2018 bin
dr-xr-xr-x   3 root    wheel  660 Jan  1 00:01 dev
lrwxr-xr-x   1 root    wheel   11 Oct 20  2018 etc -> private/etc
drwxr-xr-x   7 root    wheel  374 Jun 10  2019 iosbinpack64
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt1
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt2
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt3
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt4
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt5
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt6
drwxr-xr-x   2 root    wheel   68 Oct 20  2018 mnt7
drwxr-xr-x   4 root    wheel  136 Oct 20  2018 private
drwxr-xr-x   2 root    wheel  510 Oct 20  2018 sbin
drwxr-xr-x   9 root    wheel  306 Oct 20  2018 usr
lrwxr-xr-x   1 root    admin   11 Oct 20  2018 var -> private/var
bash-4.4#

此时,我们就会得到一个交互式bash shell!

请注意,最后一个标志(-serial mon:stdio)会将所有shell组合(例如Ctrl + C)转发给shell。要关闭QEMU,请关闭其(空)窗口。

要获得内核调试器,应将-S -s添加到QEMU命令行中,然后可以在支持此体系结构的gdb控制台中执行target remote :1234 。有关如何获取此gdb并执行此操作的更多详细信息,请参见此处。你还可以在OSX上使用mac端口获取相关的gdb,同时将multiarch和python27选项添加到gdb端口。

总的来说,我们对原来的项目进行了以下改进

1.在安装ram盘之前,无需长时间悬挂即可快速启动。

2.添加支持,以将iOS模拟为USB设备并通过usbmuxd进行通信。这将使我们能够通过SSH连接,因此使用scp复制文件,拥有更强大的终端,对网络协议进行安全研究,使用gdbserver调试用户模式应用程序等。

3.添加对模拟物理存储的支持,以使用r/w安装的盘,该盘不是ram盘,提供的空间大于2GB。

4.增加对设备的支持,如屏幕,触摸,wifi, BT等。

5.添加对更多苹果产品和iOS版本的支持。

由于ASLR的存在,用户应用程序在每次启动时都会加载到不同的地址,并且可以彼此共享虚拟地址,因此在调试用户模式应用程序时,在gdb中的静态虚拟地址上使用常规断点可能会具有挑战性。因此,我添加了另一个有趣的功能来帮助调试此内核调试器中的用户模式应用程序。当QEMU遇到HLT aarch64指令时,它会在gdb中中断,就好像它是一个gdb断点一样。所以在内核调试器中调试用户模式应用程序时,您所要做的就是使用HLT指令对应用程序进行修补,例如使用ghidra。

hlt1.jpg

hlt2.jpg

hlt3.jpg

然后使用带有任何所需权限的jtool进行签名:

Downloads jonathanafek$ ./jtool/jtool --sign --ent ent.xml --inplace bin

之后,你需要将新的CDHash添加到tchashes文件中,并重新创建静态信任缓存。

这样,当gdb在用户模式应用程序中遇到HLT指令时,就会触发断点,我们就可以在内核调试器中调试应用程序了:

hlt4.jpg

2019-07-03-image-19.jpg

我们知道HTTPS加密安全协议可有效阻止中间人攻击,也可以让中间人或者运营商监测用户实时的访问信息。目前很多运营商会通过流量劫持的方式在用户访问的页面里插入广告,使用HTTPS加密的网页则不会受影响。

而在DNS领域此前都是没有加密的,即便网页是HTTPS连接,但运营商依然可以看到用户浏览的网站网页地址。DNS over HTTPS(DoH)是专门为DNS服务器推出的TLS加密功能,从用户发出访问请求开始全程加密阻止运营商查看网页地址。DoH是一个从远程通过HTTPS加密传输来解析网域名称的协议,主要目的是为了改善使用者的隐私与安全,以避免受到监听或操控。目前,DoH正申请成为RFC 8484标准。

今年六月,Google宣布,从2016年开始展开实验的DNS over HTTPS(DoH)加密DNS服务已迈入正式版,使用者现可直接在dns.google网域上,以DoH来解析网域名称系统(DNS)。

除了Google之外,包括Mozilla基金会与Cloudflare也都是DoH的支持者。早在2018年,Mozilla 就在 Firefox 中测试 DNS over HTTPs。

然而 Network Security 的研究人员,已经发现了首个利用 DoH 协议的恶意软件,它就是基于 Lua 编程语言的 Godlua 。这个名字源于其Lua代码库和其中一个样本的源代码中的神奇数字“God”,这款后门程序可利用 DoH 来隐藏其 DNS 流量。

Netlab 研究人员在研究报告中提到,他们发现了一个可疑的 ELF 文件,但最初误以为它只是一款加密货币挖矿木马。

虽然研究人员尚未确认或否认Godlua的任何加密货币挖掘功能,但他们已确认其行为更像是DDoS木马。研究人员观察到该文件在被感染系统上作为“基于Lua的后门”工作,并且已经注意到至少有一次针对liuxiaobei.com的DDoS攻击。到目前为止,研究人员已经发现了至少两个版本,都使用DNS而不是传统的DNS请求。

通过使用DNS over HTTPS,恶意软件应用可以通过加密的HTTPS连接隐藏其DNS流量,从而允许Godlua逃避被动DNS监控,这种恶意行为已经让网络安全专家感到震惊。

本来对DNS over HTTPS的使用就存在争议,Godlua的出现让那些反对者更加有理由来反对了。

F5端点检查器(F5 Networks Endpoint Inspector)是一个安全检查应用程序,它可以被web浏览器调用,来扫描客户端的运行是否符合安全要求。

近期有研究人员发现,如果F5端点检查器可能会被恶意网站滥用,通过特殊构造的页面触发RCE。

具体利用过程,请看此视频

下面我们来详细说明一下这个过程:

1.可以看到,如果浏览http://naughty.website/网站,该网站包含一个“特别制作”的f5-epi:// URI,这会触发F5端点检查器的运行。

2.弹出一个UAC框,显然是试图运行一个由合法的F5网络证书签名的进程。

3.然后弹出powershell.exe,作为f5instd.exe的子进程运行,该进程具有管理员权限。

实事求是地讲,这并不是一个真正意义上的漏洞利用过程。所以研究人员在向F5报告时,该公司并不承认这是一个漏洞,原因何在呢?他们认为有太多的先决条件可以让该进程发生意外。为了说明这个情况,我会进行以下测试。

以下是不利用这个“非漏洞”的基本先决条件:

1.受影响的用户必须具有管理员权限,因此他们可以单击UAC弹出窗口;

2.攻击者必须拥有可信的代码签名证书才能签署恶意CAB文件;

如果你想触发漏洞(对于F5签名的二进制文件,只有一个干净的UAC弹出窗口),CAB文件必须由“F5 Networks Inc”、“F5 Networks”或“uRoam Inc”签名。f5instd.exe二进制检查签名“签名者名称”字段中的字符串:

2.png

你可以决定使用这些名称中的任何一个来获得受信任的代码签名证书的真实程度。我要说的是,“uRoam Inc”可能并不存在,因为F5在2003年买下了它。如果你想尝试一种更复杂的方法来执行任意代码,可以看下图。

3.png

然而,无论如何,即使CAB签名中的“签名者名称”不是这三个字符串里的一个,任意代码仍然会运行,用户只需点击另一个警告即可。

实际的漏洞利用过程,请点此视频观看。

注意:弹出的URL白名单框通常显示在新的网页上,但它确实不一致。

另一个提示会询问用户,他们是否真的想要提取恶意CAB文件,尽管它不是由F5或uRoam签名的。

不管怎样,这就是F5说它不是漏洞的原因。具体说明如下:

我们的团队已审核了你们分析的报告,并确定它不是一个漏洞。安装弹出窗口清楚地显示签名者和名称,并显示警告消息。此行为与用户从网络下载文件并单击它以在浏览器中运行它没有什么不同。其中也没有自动权限升级的情况,如果安装需要管理员权限,而用户没有同意,那么其他人将无法安装该软件包。

但,其中的问题却是:

1.为什么允许CAB签署第三方证书呢,阻止所有第三方证书肯定会更不容易被利用?

2.在提取和处理CAB之前,为什么不检查证书中的“签署者名称”字段? 

3.为什么要在2019年通过提取CAB文件来更新你的软件?有更好的方法来做到这一点;

4.这与用户从互联网上下载文件并点击它进行运行是不同的;

5.除此之外,F5是CVE编号机构(CNA),所以他们可以自行决定那些东西是不安全的;

3.让公司决定问题是漏洞而不是意外状况时,是否存在固有的利益冲突?

时间线

2019.5.23:向F5报告

2019.5.27:F5确认收到

2019.5.29:由F5给出事件号码

2019.6.3:F5要求我们提供CVSS评分。

2019.6.19:F5回复说这不是漏洞。

在今年Pwn2Own中,有研究人员发现利用一个越界读漏洞竟然实现了Safari沙箱逃逸,然后利用kextutil中存在的TOCTOU获得内核代码执行权限。

目前该漏洞已经被命名为CVE-2019-8603,简单来说,这是一个存在于Dock和苹果卸载网站(com.apple.uninstalld)中的堆越界读取漏洞,该漏洞将导致攻击者调用CFRelease并在macOS上实现Safari浏览器沙盒逃逸,最终获取到目标设备的root权限。

另外,在测试过程中,CVE-2019-8606允许研究者通过kextutil中的竞态条件(race condition),用root权限实现内核代码执行。如果再借助一个qwertyoruiopz和bkth开发的WebKit引擎漏洞(远程代码执行漏洞),则研究人员可以完全实现Safari沙箱逃逸。

在本文发布时,该漏洞已经在最新的macOS 10.14.5版本中被修复了,所以本文我们可以对它的来龙去脉进行详细复盘了。

详细复盘

本来研究人员是计划测试一个模糊测试工具的代码覆盖率,代码覆盖(Code coverage)是软件测试中的一种度量,描述程式中源代码被测试的比例和程度,所得比例称为代码覆盖率。但测试到AXUnserializeCFType函数时,却发现发现了CVE-2019-8603漏洞,这个函数是去年的Pwn2Own中发现的一个简单解析器,不过当时并没有发现其中有什么漏洞。不过在发现有漏洞后,研究人员对这个函数进行了仔细研究,发现它竟然是CoreFoundation对象序列化的另一种实现方式。Core Foundation框架(CoreFoundation.framework)是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能。而AXUnserializeCFType则是HIServices框架的一部分,而且代码就存储在对应的dylib库中。

可以通过此函数对CFAttributedString对象进行反序列化,CFAttributedString是一个字符串,其中每个字符与CFDictionary相关联,CFDictionary包含了描述给定字符的任意属性。这些属性可以是颜色,字体或用户关心的任何其他内容。而在本文的示例中,CFDictionary的属性是代码执行。

为了帮助大家更直观地了解CFDictionary的属性,研究人员专门列出了数据结构,数据结构使用游程长度(run-length)压缩,而不是为每个单独的字符专门分配字典:

// from CFAttributedString.c
struct __CFAttributedString {
    CFRuntimeBase base;
    CFStringRef string;
    CFRunArrayRef attributeArray;  // <- CFRunArray of CFDictinaryRef's
};

// from CFRunArray.c
typedef struct {
    CFIndex length;
    CFTypeRef obj;
} CFRunArrayItem;

typedef struct _CFRunArrayGuts {	/* Variable sized block. */
    CFIndex numRefs;                        /* For "copy on write" behavior */
    CFIndex length;                         /* Total count of values stored by the CFRunArrayItems in list */
    CFIndex numBlocks, maxBlocks;           /* These describe the number of CFRunArrayItems in list */
    CFIndex cachedBlock, cachedLocation;    /* Cache from last lookup */
    CFRunArrayItem list[0]; /* GCC */
} CFRunArrayGuts;

/* Definition of the CF struct for CFRunArray */
struct __CFRunArray {
    CFRuntimeBase base;
    CFRunArrayGuts *guts;
};

例如,字符串“attribuis hard”可以在内部表示为3个CFRunArrayItems:

1.从索引0开始,长度11,“粗体”属性标识;

2.从索引11开始,长度4,没有属性标识;

3.从索引15开始,长度4,“斜体”属性标识。

显然有一些不变量必须被维护,例如所有运行的union类型跨越整个字符串,并且没有任何运行重叠。

反序列化函数cfAttributedStringUnserialize有两个执行路径,第一个路径很简单,它只读取一个字符串,并为属性dict调用带有NULL的CFAttributedStringCreate。这意味着第二个路径必须解析一个字符串,以及一个包含了范围和关联字典的列表,然后调用内部函数_CFAttributedStringCreateWithRuns:

CFAttributedStringRef _CFAttributedStringCreateWithRuns(
        CFAllocatorRef alloc,
        CFStringRef str,
        const CFDictionaryRef *attrDictionaries,
        const CFRange *runRanges,
        CFIndex numRuns) { ...

解析器将会确保运行的次数和字典的数量匹配,但是它不会对实际的字符串范围信息执行任何验证。同样_CFAttributedStringCreateWithRuns也无法做到这一点。

    for (cnt = 0; cnt < numRuns; cnt++) {
	CFMutableDictionaryRef attrs = __CFAttributedStringCreateAttributesDictionary(alloc, attrDictionaries[cnt]);
	__CFAssertRangeIsWithinLength(len, runRanges[cnt].location, runRanges[cnt].length); // <- ouch
	CFRunArrayReplace(newAttrStr->attributeArray, runRanges[cnt], attrs, runRanges[cnt].length);
	CFRelease(attrs);
    }

因此,研究者将能够使用完全可控的range以及newLength值来调用CFRunArrayReplace。

void CFRunArrayReplace(CFRunArrayRef array, CFRange range, CFTypeRef newObject, CFIndex newLength) {
    CFRunArrayGuts *guts = array->guts;
    CFRange blockRange;
    CFIndex block, toBeDeleted, firstEmptyBlock, lastEmptyBlock;

    // [[ 1 ]]

    // ??? if (range.location + range.length > guts->length) BoundsError;
    if (range.length == 0) return;

    if (newLength == 0) newObject = NULL;

    // [...]

    /* This call also sets the cache to point to this block */

    // [[ 2 ]]
    block = blockForLocation(guts, range.location, &blockRange);
    guts->length -= range.length;

    /* Figure out how much to delete from this block */
    toBeDeleted = blockRange.length - (range.location - blockRange.location);
    if (toBeDeleted > range.length) toBeDeleted = range.length;

    /* Delete that count */

    // [[ 3 ]]
    if ((guts->list[block].length -= toBeDeleted) == 0) FREE(guts->list[block].obj);
    ...

仔细观察上图中的代码段[[ 1 ]],很明显,编写这段代码的人虽然对传入的参数有所怀疑,但却没有更改函数签名以返回错误信息。

而在代码段[[2 ]]中,错误执行就开始了:如果range.location太大,那么blockForLocation就会返回一个越界索引。这意味着,代码段[[ 3 ]]的FREE在调用CFRelease(使用越界索引获取的指针来实现调用)时,漏洞就被触发。

漏洞被触发后,又会导致对objc_release的调用,objc_release会开启vtable查询,为release选择器寻找需要的Objective-C函数。

id objc_msgSend(id obj, SEL sel, ...)
{
  __objc2_class *cls; // r10
  __int128 *v3; // r11
  __int64 i; // r11

  if ( !obj )
    return 0LL;
  if ( obj & 1 )
  {
    // [...]
  }
  else
  {
    cls = (*obj & 0x7FFFFFFFFFF8LL);
  }
  v3 = &cls->vtab[cls->mask & sel];
  if ( sel == *v3 )
    return (*(v3 + 1))();   // <- OUCH!!

请注意,如果研究人员完全控制传入的obj值,则可以轻松输入第一次检查的else情况并达到间接调用,前提是研究人员可以将伪造的对象放在已知位置,并且知道release选择器的地址。幸运的是,在本所讲的沙盒逃逸环境中,这几个前提条件都可以被满足,因为所有代码库都会映射到系统范围的相同地址,其中就包括选择器在内。

堆内存

研究人员为了将这个漏洞转换为对CFRelease的受控调用,就必须将某些值放在被越界访问的CFRunArray之后。为此,研究人员需要通过使用解析器本身自带的分配和释放原语来实现这一点。具体地说,解析器允许他们创建一个字典并重复设置解析对象,然后这些对象又在输入流中被解析出来。

通过向字典中添加一个新对象,研究人员可以分配一个对象。稍后通过覆盖该对象,对象将被释放。这个原语足以创建许多具有可预测性的数据序列了,其中就有CFRunArray和负责控制数据的CFString对象。

Dock中的漏洞测试

要在Dock中触发了这个安全漏洞,需要连续两次使用该漏洞,首先利用Dock托管的com.apple.dock.server服务,然后再在WebContent沙箱中进行访问,基于Mach的协议是通过MIG (Mach接口生成器)创建的。

研究人员测试时,攻击的是消息ID 96508的处理程序,研究人员并没有真正地将其进行什么处理。只要AXUnserializeCFType可以接收并解析某些数据,并将它们作为联接外部的内存描述符即可。

MIG还为测试提供了数千兆字节的数据,这些数据可以映射到接收器的地址空间中,说到这你可能明白了这就是大家都熟知的堆喷射技术,这样研究人员就可以将任意数据存放到他们想要的位置了。

接下来,就是要确保堆喷射对象的每一个页面都要有重复相同的数据(约800MiB),这些数据由下面这两个部分组成:

1.伪造的对象用来触发间接调用,并输入一个小型JOP stub来对堆结构进行pivot处理,PIVOT是通过将表达式某一列中的唯一值转换为输出中的多个列来旋转表值表达式,并在必要时对最终输出中所需的任何其余列值执行聚合;

2.ROP链可以完成所有自动化的过程。

请注意,此时的漏洞还明显无法完成信息的泄漏。

苹果卸载网站(com.apple.uninstalld)中的漏洞测试

由于研究者的真实目的是使用root权限实现内核代码的执行,所以在上面的分析快结束的时候,他们在谷歌中搜索了一下AXUnserializeCFType,并发现了Project Zero的1219漏洞,这是一个非常简单的越界漏洞,在2017年被发现。当时安全研究人员Ian Beer认为,该漏洞不会解析那些不受信任的数据。

但无论如何,Ian Beer当时已经提到,攻击者会以root身份运行的com.apple.uninstalld服务与Dock对话,并在Dock提供的数据上调用AXUnserializeCFType。所以,攻击者可能会冒充Dock并为为uninstalld提供有效载荷。

不过在实测时,研究人员遇到了以下问题:

1.要让uninstalld执行任何操作,就必须为其提供授权令牌,该令牌具有嵌入在Dock二进制文件中的特定权限。

2.研究人员实际上并没有在Dock中映射出他们自己的代码,这可能是由于在2018年的某个时候该漏洞被添加了某些代码签名机制。

3. 在Dock运行时,研究人员无法在com.apple.dock.server端口上注册,因为Dock占用了该端口。

在创建和转储授权令牌之后,为什么不能直接终止Dock并从另一个进程的端口上注册,这个问题目前研究人员也没有搞清楚。无论如何,研究人员最终还是在Dock内运行的ROP链中执行以下所有操作:

1.调用AuthorizationCreate和AuthorizationMakeExternalForm以生成具有uninstalld权限的令牌;

2.生成一个名为fakedock的二进制文件,用于注册一个mach服务;

3.查找fakedock服务;

4.将com.apple.dock.server服务的接收端以及授权令牌发送到fakedock;

5.潜伏下来。

之后,fakedock将等待接收权限和令牌,然后模拟com.apple.dock.server服务。然后它会与uninstalld进行会话,以使其开始卸载一个应用程序,卸载时,该应用程序将依次触发 “连接”并通过研究人员需要以适当方式处理和响应的特定MIG调用序列接收漏洞的有效载荷,uninstalld的ROP链只是使用研究人员的最终含有root权限的有效载荷来调用系统。

利用kextutil中存在的TOCTOU获得内核代码执行权限

TOCTOU是time-of-check-to-time-of-use的缩写,是竞争危害 (race hazard) 又名竞态条件 (race condition)的一种,它是指计算机系统的资料与权限等状态的检查与使用之间,因为某特定状态在这段时间已改变所产生的软件漏洞。有了上面的测试,kextutil允许研究人员以root身份加载内核扩展,但它会执行某些检查,例如代码是否签名以及是否得到用户的批准。要执行漏洞,显然需要绕过这些检查,在无需用户交互的条件下,来加载研究人员自己的未签名代码。研究人员绕过文件检查的首选方法是使用竞争条件,本文使用的是符号链接。

在进行了最近由逻辑错误专家CodeColorist详细描述的所有检查之后,kextutil会将所有的函数调用请求加载进IOKit!OSKextLoadWithOptions,并向内核发送一个加载请求。

但是,如果提供的kext路径是符号链接,就可以将它直接用来连接不同的操作了。

现在,需要满足几个条件,才能使完全实现漏洞的利用,其中一个就是交换符号链接目的地址的时机是否正确。为了实现这一点,研究人员运行了kextutil -verbose 6 -load / path / to / kext,该命令会输出大量调试信息,并提供一个完整的POSIX管道来作为STDOUT。研究人员可以在代码执行的过程中在特定的地址生成管道溢出,并挂起进程,这意味着研究人员可以替换掉符号链接并清除管道中的数据。

最终,研究人员就能够成功加载未签名的kext,进行任意代码执行,并最终实现Safari沙盒逃逸。

不过,在此漏洞利用完成后,安全研究人员Linus Henze则提出了一种更可靠的方法来触发竞争条件。由于kextutil实际上有一个标志-i,它会在安全检查后提示用户,但在加载kext之前,并没有出现“你想现在改变你的符号链接并继续加载其他东西吗?”的提示。

近日Preempt研究小组发现了两个关键的微软漏洞,这两个漏洞都和三个NTLM中的逻辑漏洞有关,这些漏洞允许攻击者在任何Windows计算机上远程执行恶意代码,或对支持Windows集成身份验证(WIA)的任何web服务器(如Exchange或ADFS)进行身份验证。根据测试,目前所有 Windows 版本都会受到这两个漏洞的影响。更糟糕的是,这两个漏洞可绕过微软此前已部署的所有安全保护措施。

在允许的环境下,Kerberos是首选的认证方式。在这之前,Windows主要采用另一种认证协议——NTLM(NT Lan Manager)。NTLM使用在Windows NT和Windows 2000 Server(or later)工作组环境中(Kerberos用在域模式下)。在AD域环境中,如果需要认证Windows NT系统,也必须采用NTLM。较之Kerberos,基于NTLM的认证过程要简单很多。NTLM采用一种质询/应答(Challenge/Response)消息交换模式,下图反映了Windows2000下整个NTLM认证流程。

1.png

NTLM容易受到中继攻击,这允许攻击者捕获身份验证并将其转发到另一台服务器,从而使他们能够冒用经过身份验证的用户的特权在远程服务器上执行所有的操作。

NTLM Relay是Active Directory环境中最常见的攻击技术之一,在Active Directory环境中,攻击者先会攻击一台计算机,然后通过使用针对受感染服务器的NTLM身份验证向其他计算机扩展。

微软之前已经开发了几个缓解NTLM RELAY攻击的方法,但是研究人员发现这些方法都存在着以下缺陷:

1.基于消息完整性代码(MIC)字段确保攻击者不会篡改NTLM消息的措施:Preempt研究人员发现,只要绕过消息完整性代码(MIC)字段,攻击者就可以删除“MIC”保护并修改NTLM身份验证流中的各个字段,比如签名协商。

2.基于SMB会话签名防止攻击者发送NTLM身份验证消息来建立SMB和DCE/RPC会话的措施,Preempt研究人员发现,许攻击只要绕过SMB会话签名,就可以将NTLM身份验证请求转发到域中的任何服务器,包括域控制器,同时伪造一个签名会话来执行远程代码执行。如果转发的身份验证属于特权用户,则意味着整个域都能被攻击。

3.基于增强的身份验证保护(EPA)防止攻击者将NTLM消息转发到TLS会话的措施,Preempt研究人员发现,攻击者只要绕过身份验证保护(EPA),就可以修改NTLM消息来生成合法的通道绑定信息。这允许攻击者使用被攻击用户的特权连接到各种web服务器,并执行以下操作:通过中继到OWA服务器,读取用户的电子邮件;甚至通过中继到ADFS服务器,连接到云资源。

Preempt首席技术官Roman Blachman表示:

即使NTLM Relay是一项古老的技术,企业也无法彻底放弃对NTLM协议的使用,因为它会破坏许多应用程序。因此,它仍然给企业带来了巨大的风险,尤其是在不断发现新的漏洞的情况下,公司需要首先确保所有Windows系统都经过修补和安全配置。此外,公司可以通过获得网络NTLM可见性来进一步保护其环境。

在Preempt向微软公司披露了上述漏洞后,微软在6月11日的时候发布了CVE-2019-1040和CVE-2019-1019补丁来应对这些问题。

即使执行了这些修复,管理员还需要对某些配置加以更改,才能确保有效的防护。

保护策略

为了保护公司免受这些漏洞的攻击,建议管理员进行以下操作:

1.执行修补程序:确保为工作站和服务器打上了所需的补丁,要注意,单独的补丁是不够的,公司还需要进行配置更改,以便得到完全的保护。

2.配置更改:

2.1强制SMB签名:为了防止攻击者发起更简单的NTLM RELAY攻击,请务必在网络中的所有计算机上启用 SMB 签名。

2.2禁用NTLMv1:该版本相当不安全,建议通过适当的组策略来完全禁用。

2.3强制LDAP/S签名:为了防止LDAP中的NTLM RELAY攻击,在域控制器上强制LDAP签名和LDAPS通道绑定。

2.4强制实施EPA:为了防止NTLM在web服务器上被黑客用来发动中继攻击,强制所有web服务器(OWA、ADFS)只接受EPA的请求。

3. 减少NTLM的使用:因为即便采用了完整的安全配置,NTLM 也会比 Kerberos 带来更大的安全隐患,建议在不必要的环境中彻底弃用。

Windows NTLM安全协议漏洞史

其实早在2017年7月,安全公司 Preempt 专家就发现了 NTLM 协议存在两处关键漏洞,允许攻击者创建新域名管理员帐户、接管目标域名。

调查显示,第一处关键漏洞涉及 NTLM 中继器内未受保护的 Lightweight Directory Access Protocol(LDAP)协议;而第二处 NTLM 漏洞影响 RDP Restricted-Admin 模式,允许攻击者在不提供密码的情况下远程访问目标计算机系统。

Yubikey是一个小型的USB设备,在电脑看来,它是一个USB键盘设备。

几年前,我有一个受安全漏洞影响的YubiKey,为了解决这个问题,Yubico公司免费送给我一个全新的YubiKey。由于我在收到新的YubiKey之后没有使用旧的YubiKey进行身份验证,我决定看看是否可以将它变成类似于USB Rubber Ducky的东西,USB Rubber Ducky是一款模仿人工键盘输入的设备,外形和U盘一样,模拟键盘输入速度可达到1000个字符每分钟,并且适合任何操作系统,包括安卓等移动OS,它使用的是它特定的脚本语言,用记事本就可以编写,通过他配套的jar程序编译成inject.bin放到sd卡里运行。事实证明我能够做到这一点,具体步骤(总共4步),我会在下面讲到。

第1步:下载YubiKey个性化工具

YubiKey在其网站上提供了一个名为YubiKey Personalization Tool(YPT)的程序,可用于在Linux,Windows或Mac上自定义YubiKey的不同功能。我在这篇文章中使用的是Linux版本,但Windows和Mac版本应该非常相似。

如果你像我一样使用Linux版本,则可能需要使用YubiKey提供的源代码构建程序。有关如何操作的说明,你可以在源代码附带的README文件中找到,很容易理解,所以我不在这里介绍它们。

第2步:使用静态密码对YubiKey进行编程

在默认配置中,YubiKey将在使用时输入唯一的身份验证令牌,并且该令牌会在每次使用时改变。不过,YubiKey也可以通过编程输入静态的用户定义密码。由于YubiKey可以像普通键盘一样将数据输入计算机,我想知道除了标准字母,数字和符号之外,它是否可以用来按CTRL、ALT或Windows键等更有趣的键。为了测试这一点,我启动了YPT并从顶部的栏中选择了静态密码选项。然后在“静态密码”页面上,单击标有“扫描代码”的按钮。

1.png

为了理解一切是如何工作的,我首先用非常简单的静态密码“abcdef”编写YubiKey。为此,我在“静态密码”窗口中选择了以下选项:

1.配置插槽:配置插槽1

2.键盘:US Keyboard

3.密码:abcdef

当我在密码字段中输入密码时,十六进制值开始显示在其右侧的扫描代码字段中。我注意到了这一点,并决定在用静态密码编写YubiKey之后,为我想输入的每个键标识十六进制值。这样,我就可以用无法输入密码字段的键(如CTRL和ALT)来编程了。

以下屏幕截图显示了我上面列出的所有设置,以及通过输入密码生成的扫描代码:

2.png

接下来,单击“Write Configuration”将静态密码写入我的YubiKey。第一次执行此操作时,会弹出一个对话框,要求我确认是否要覆盖YubiKey上插槽1的当前配置。我选中了标有“不再显示此消息”的框,然后单击“是”将更改写入设备。

3.png

注意:如果你正在使用你自己的YubiKey,请确保它不是你当前用于身份验证的。将新配置写入YubiKey将删除存储在你选择的配置槽中的设置,你必须重新编写YubiKey程序,并将其重新注册到你使用的服务中,以便再次将其用于多因素身份验证。如果你只使用YubiKey上的一个配置插槽进行身份验证,则可以安全地覆盖另一个配置插槽。但是,如果你不确定,最好将你的YubiKey从你首先使用的任何服务中注销,或者直接使用另一个YubiKey。

写完更改后,我打开了一个文本编辑器,并按下YubiKey上的硬件按钮,YubiKey果然在屏幕上输入了密码“abcdef”。

4.png

第3步:识别YubiKey的十六进制密钥代码

现在我已经确认我可以让YubiKey输入一系列预定义的键,接下来我要做的就是弄清楚是否可以通过在YPT中指定十六进制“扫描码”来使其按下更有趣的键。为了将扫描码映射到相应的按键,我使用了一种技术含量很低的方法,在YPT的密码字段中键入字母“a”到“z”,并在扫描码字段中观察结果。这导致十六进制值04通过1D出现在扫描码字段中。

5.png

对键盘上所有其他可打印的键以及每个键的大写版本都重复了这个过程,最后我将收集到的所有十六进制值以及尚未与键盘上的键匹配的值范围都记录下来。我把我能破译的所有字符组织成一个表格,然后,我注意到一个模式。扫描代码似乎被分成了两部分,小写字母都位于00-7F和大写字母之间,或者“key + Shift”版本存在于80-FF之间的相同位置,在下表中可以更清楚地看到这一点。

6.png

现在要做的就是确定每个未知范围内十六进制值生成的按键,因为在YPT的“扫描代码”字段中输入十六进制值没有显示任何输出,并且因为我预期在未知范围内按下的许多键是不生成任何可打印输出的键(例如CTRL键),所以我需要一种方法来捕获YubiKey生成的原始按键。为此,我决定使用Linux工具xinput和我编写的xinput-keylog-decoder脚本来解码输出。

如果你不熟悉xinput,它我可以告诉你,它就是一个命令行工具,通常包含在许多Linux发行版中以及图形桌面环境中。当这些系统受到攻击时,它也常被滥用为键盘记录器,为此我创建了xinput-keylog-decoder工具。

由于YubiKey本质上是一个键盘,我开始捕捉它的按键时所做的第一件事就是在xinput中标识它的ID号。我通过运行不带任何参数的xinput命令检查了这一点,并确定其ID为16,如下面的输出所示。

7.png

默认情况下,xinput-keylog-decoder附带的示例脚本会记录所有连接到系统的键盘的输入,但知道YubiKey的ID后,我就可以在解析输出时专门针对该设备。

接下来,我可以打开了三个终端窗口并运行命令来记录和分析YubiKey生成的按键,下面的屏幕截图解释了每个命令的用途。

8.jpg

第一个终端窗口:停止当前运行的任何xinput进程,启动一个新的xinput进程,并启动无限循环以从键盘读取输入。这是我在YubiKey输入系统时一直选择的终端窗口。这样,它输入的任何内容都不会干扰其他终端窗口。

./stop-logging.sh >0; rm *txt; ./start-logging.sh; while true; do read; sleep 0.1; done

第二个终端窗口:每隔一秒会在屏幕上显示test-output.16.txt的原始输出。 test-output.16.txt是自动保存键盘ID 16的按键的文件。通过显示xinput输出的原始密钥代码,我可以得到更多的信息,以防xinput-keylog-decoder.py无法解码第三个终端窗口中的输入内容。

watch -n 1 tail test-output.16.tx

第三个终端窗口:每秒,解码keylog文件并将其显示为人性化文本。

watch -n 1 ./xinput-keylog-decoder.py test-output.16.txt

最后,在将十六进制扫描码编程到YubiKey中时,我首先在两个已知字符之间输入它们,通常是“a”(扫描码04)和“b”(扫描码05)。通过这种方式,我可以确认按下目标按键之前和之后的按键。并且通过这种方式,我可以识别按键是否对其他按键产生了影响,下面是针对扫描代码“2A”的示例。

在第一个屏幕截图中,你可以看到未识别的扫描代码“2A”,夹在“a”和“b”的扫描代码之间。另外,你可能还会注意到密码字段中“a”和“b”之间的明显空白。

9.png

在下一个屏幕截图中,我选择了第一个终端窗口,并按下YubiKey上的按钮。乍一看,似乎只按了“b”键而忽略了“a”。但是,在检查了第二个终端窗口后,你可以看到三个键被依次按下和释放。在第三个终端窗口中,来自第二个终端窗口的代码被解码成人性化的格式,很明显按下的键是“a”,退格键和“b”。这解释了为什么“a”没有出现在第一个终端窗口中,并将目标扫描码“2A”标识为退格键。

10.jpg

在以这种方式识别密钥后,我接下来所做的就是按CTRL + C以停止第一个终端窗口中的运行循环,再次运行该命令(清除日志并重新启动记录器),然后重复上面的过程。在对每个未识别的十六进制值重复这些步骤之后,我确认了每个可能的扫描代码生成的按键,并将它们收集到下面的表中。

11.png

在解码扫描码时,我还观察到,YubiKey会在一些按键序列结束时自动按下回车键。在某些情况下,我能够通过使用扫描代码“00”终止序列来防止这种行为,但它并不总是有效。以下有一个用于演示的YubiKey的屏幕截图,它被配置为输入字母“a”到“z”,以及按下YubiKey按钮后输出的屏幕截图。请注意,“z”键(扫描码“1D”)是编入YubiKey的最后一个键,但是YubiKey无论如何都会在字符串的末尾按下回车键。这与在前一个示例中解码退格键代码时观察到的行为不同,在前一个示例中,回车键没有被按下。

12.png

13.png

按键序列的长度和YubiKey的输出速度(可从YPT中的“设置”屏幕进行配置)似乎都会影响此行为。在我的测试中,额外的回车键没有出现在以标准输出字符速率输入的长度小于23个键的序列中。但是,将字符速率减慢60 ms会导致回车键在短至一个键的序列上自动按下。如果你不希望让YubiKey在结束时自动按回车键,那么在YubiKey上创建有效载荷时要注意这一点。

第4步:创建有用的有效载荷

由于所有扫描代码与他们按下的键匹配,所以我现在准备开始构建有效载荷。不幸的是,我测试过的扫描码都没有按下我希望找到的CTRL,ALT或Windows键。因此,虽然它可以用来输入一个很长的一行程序,但它不是一个理想的完全自动化的命令注入工具或如Rubber Ducky或Teensy那样的USB Drop。

即使YubiKey不按CTRL,ALT或Windows键,它仍然可以访问其他几个可能有趣的键,包括:

1.Shift(使用“Shift + No effect”扫描代码);

2.功能键(F1-F12);

3.菜单键(相当于鼠标右键);

4. Escape键;

5.Shift键与所有已识别的键组合使用;

虽然在将可执行有效载荷注入目标系统时,这些密钥可能不是首选,但是在攻击KIOSK自助服务设备时,它们非常有用。

由于完全保护kiosk的软件的安全性都很差,kiosk制造商通常会从键盘上删除按键,单击右键设备上的按钮,或者完全删除这两种设备以支持触摸屏。但是,自助服务终端上的USB端口经常会处于暴露状态,以便技术人员可以将它们连接自己的键盘进行故障排除。在这种情况下,使用自己键盘(如本示例中的YubiKey)的攻击者只需将键盘插入自助服务终端,并使用众多众所周知的方法中的一种来突破受限制的shell并控制电脑即可。

攻击kiosk上受限制的shell的第一步通常是打开一个新的应用程序窗口,可能是一个对话框,也可能是一个新的浏览器窗口,或者其他任何东西。这通常是键盘最有帮助的一步,因为其余的攻击通常可以用来自一个指向设备的最小输入来完成。下表描述了YubiKey可以注入的按键以尝试执行第一步。

14.jpg

考虑到这些功能,我创建了下面的三个有效载荷,以使用我的YubiKey来破解kiosk设备。

YubiKey载荷

有效载荷1:含有简单功能键和粘滞键测试

· 扫描码:522c3a3b3c3d3e3f404142434445e6e6e6e6e6e652

· 输出字符率:标准

· 按键执行:

1.激活粘滞键对话框中的超链接(如果存在):按下向上箭头,空格键;

2.依次按下每个功能键:F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12;

3.连续按5次Shift键打开“粘滞键”对话框,为了安全起见,再连续按6次Shift键;

4.在“粘滞键”对话框中选择超链接,并尝试阻止Enter键在按下向上箭头时关闭窗口;

有效载荷2:浏览器热键和粘滞键

· 扫描码:3f2a06b3a83f4dca06b3283c443e3b3d40ab2c29e5115128454142435113113ae6e6e6e6e652

· 输出字符速率:减慢60 ms

· 按键执行:

1.在一个新的浏览器窗口中打开“c:”:依次按下F6,Backspace,输入“c:”,Shift + Enter;

2.打开“c:”(Chrome):依次按下F6,End,Shift + Home,输入“c:”,Enter;

3.依次按下功能键:F3,F11,F5,F2,F4;

4.尝试按下F7并关闭对话框;

5.打开一个新的浏览器窗口:依次按下Shift + Menu,n,Down,Enter;

6.试试F12 / Web开发者控制台:按下F12;

7.尝试F8和F9:依次按下F8,F9;

8.打开打印对话框或新浏览器:依次按下F10, Down, p, n;

9.尝试 F1/Help:按下F1;

10.打开“粘滞键”对话框:连续五次按下Shift键t;

11. 防止回车键关闭“粘滞键”对话框:按下向上箭头;

有效载荷3:Shift +单击右键

· 扫描码:e5

· 输出字符率:标准

· 按下执行键:Shift +菜单键

第一个有效载荷非常简单:它依次按下向上箭头,空格键,每个功能键(F1-F12),然后连续六次按下Shift键,然后再按向上箭头。此有效载荷的目的是测试每个功能键以查看它是否提供了访问自助服务终端上的其他功能的方法,然后反复按Shift键以打开“粘滞键”对话框。打开“粘滞键”对话框后,可以再次按下YubiKey上的按钮,按下向上箭头和空格键将打开对话框中的超链接,以导航到Windows的“易于访问”设置。这是我为YubiKey创建的第一个有效载荷,实际攻击效果特别好。

第二个有效载荷是通过调整功能键的使用来改进第一个有效载荷,以反映它们在常见web浏览器中的功能。例如,按F7然后立即尝试F8有没有意义,因为在大多数浏览器中按F7会出现提示,这可以有效阻止F8在浏览器的上下文中被按下。就像在第一个有效载荷中一样,仍然需要按下每个功能键以及粘滞键序列。这个有效载荷是我在撰写本文时创建的一个新载荷,因此还没有实际测试过。到目前为止,它在实验室环境中运行得很好。

最后,第三个有效载荷只需按Shift键和菜单键。这实际上与按住Shift键并单击鼠标右键的结果是一样的,它使我能够向自助服务终端添加一个单击鼠标右键的操作。如果我可以在资源管理器窗口中单击右键,它还提供了PowerShell或命令提示符的快捷方式。我通常将此有效载荷保留在我的YubiKey上的插槽2中。

总结

本文的示例充分说明了YubiKey是如何从一个可信赖的电子设备演变成为一个攻击工具的,虽然YubiKey是一款出色的双因素身份验证设备,但它肯定缺少一些可以使其成为理想的USB HID攻击工具的功能性,而且已经有其他产品比它做得更好。可能YubiKey作为攻击工具的主要优势在于它看起来像是YubiKey,在不允许使用闪存驱动器的高安全性环境中,可能会偷偷插入一个YubiKey。在近距离的社会工程场景中,说服员工打开公共Internet kiosk的设备,以便对电子邮件帐户进行“身份验证”,这可能比插入一些无法识别的设备更容易得手。

前言

Guardicore Labs 的安全研究人员发布了一份报告,在该报告中,Guardicore Labs团队称发现大量MS-SQL和PHPMyAdmin服务器遭到黑客的攻击并将这些服务器用于挖掘乌龟币(TurtleCoin)。此次黑客代号为“Nansh0u”,根据目前的证据,黑客可能来自中国。

注:TurtleCoin(乌龟币TRTL)是2017年12月新出的一个区块链项目,指在提供快速安全稳定的数字货币,目前关注的人不多。

报告称,5万多台受到攻击的服务器都属于医疗保健、电信、媒体和 IT 公司等在内的 ,一旦受到攻击,目标服务器就会被恶意载荷攻击。另外,黑客还安装了一个复杂的内核模式 rootkit 来防止恶意软件被终止。

这并非典型的加密攻击,因为它使用的是APT攻击中经常出现的技术,例如伪造证书和特权升级漏洞。

该攻击活动于 4 月初被首次发现,但其实它早在 2 月 26 号就开始运行了。据Guardicore Labs团队的描述,每天新增的受害者超过700人,并且在调查过程中,共发现了20个版本的有效恶意载荷,并且每周还会至少创建一个新的有效载荷,受攻击的计算机数量在一个月内就已翻倍。

深入分析攻击中所用的工具、漏洞和攻击威力

被攻击的电脑包括5万多台服务器,属于医疗、电信、媒体和IT行业。一旦受到攻击,目标服务器就会受到恶意有效载荷的攻击。不过攻击者会将受攻击服务器上的密码管理器删除,并安装了一个复杂的内核模式rootkit,以防止恶意软件被终止。

Guardicore Labs在调查期间,总共发现了20个不同版本的有效载荷版本的发布和部署。研究人员联系了攻击服务器的托管服务提供商以及rootkit证书的颁发者。结果他们发现,攻击服务器被关闭,证书也被撤销了。

Nansh0u攻击并不是典型的加密货币挖掘攻击,本次攻击使用了APT中常见的技术,比如伪造证书和特权升级漏洞。

在本文中,研究人员将详细描述这些攻击,并为该攻击活动提供一个完整的IoC存储库,包括一个用于检测受攻击设备的脚本。

本次攻击的发现始于一次异常事件

4月初,在Guardicore Global Sensor Network (GGSN)中检测到的三次攻击引起了Guardicore Labs团队的注意。这三家公司的源IP地址都源自南非,由VolumeDrive ISP托管。这些事件的攻击过程、使用的攻击工具和攻击步骤都一模一样。

根据这个线索,研究人员找到了具有很多相似攻击模式的样本,最早的样本是2月26号的。截止发稿日期,每天新增的受害者超过700人。调查中,研究人员总共发现了20个版本的恶意有效载荷,并且每周还会至少创建一个新的有效载荷,并在创建之后立即投入使用。

1.png

有效载荷创建的时间轴

在这个时间轴上,包含5台攻击服务器和6台连接回(connect-back server)服务器,这表明,攻击者已经建立一个长期的攻击流程和框架。

通过访问攻击者的基础设施,研究人员发现了相关的文件服务器,并对攻击活动的攻击范围进行深入了解。下图显示了攻击的计算机的数量如何在一个月内翻倍的:

2.png

攻击者文件服务器的总下载次数,在一个月内翻了一番

MS-SQL命令执行

每次攻击都是从对MS-SQL服务器的一系列身份验证尝试开始,直至最后使用管理特权成功进行登录。

3.png

成功登录Guardicore的Guardicore Global Sensor Network (GGSN)

然后,执行一系列MS-SQL命令,完成如下操作(括号内的数字表示相关代码行):

1.配置服务器设置,以允许攻击流程[1]顺利进行;

2.在c:\ProgramData\2.vbs [2]中创建一个Visual-Basic脚本文件;

3.执行此脚本并通过HTTP [3]将两个文件下载到c:\ProgramData;

4.在一个命令行[4]中运行这两个文件。

4.jpg

对基础设施发起攻击

攻击者的服务器都运行HFS,HFS是一种HTTP文件服务器,提供不同类型的文件。由于其中一个文本文件包含字符串Nansh0u,因此研究人员才将本次的攻击活动命名为“Nansh0u”。

攻击者的基础结构包含对MS-SQL服务器进行成功的端到端攻击所需的3个模块:

1.端口扫描程序;

2.MS-SQL暴力破解工具;

3.远程代码执行程序。

5.png

Nansh0u的攻击流程

端口扫描程序负责查询MS-SQL服务器

攻击者通过扫描IP地址并检查典型的MS-SQL端口是否打开来查询MS-SQL服务器。找到对应服务器后,端口扫描程序就自动向该服务器发送暴力破解工具。

6.png

端口列表和端口扫描程序的日志文件,攻击者的日志显示,扫描结果可以追溯到3月初

暴力破解工具负责具体攻击

该工具尝试使用成千上万个通用凭据登录到每个MS-SQL服务器。一旦身份验证成功,则服务器的地址、用户名和密码将被保存到某个文件中,供将来使用。

7.png

Guardicore Centra和攻击者词典中的匹配凭据

另外,攻击者使用的通用凭据字典可以在IoC存储库中找到。

代码执行模块负责攻击时间

通过运行端口扫描程序和暴力破解工具,攻击者获得了被攻击服务器的详细信息列表,其中包括IP地址、端口、用户名和密码等信息。接下来一步,便是登录到受害者的设备并对其发起攻击。

在HFS上,在一个名为chuan的文件夹中,研究人员发现了两个有趣的组件。第一个脚本名为Mssql.log,脚本的命令正是触发攻击的命令。

8.jpg

第二个文件是一个名为Usp10.exe的可执行文件,此程序负责接收服务器的地址和凭据以及Mssql.log的内容。它的功能很简单,就是登录到被攻击的服务器并执行Mssql.log脚本,在受害者的设备上继续发起攻击。

9.jpg

Usp10.exe是攻击模块,负责在MS-SQL受害者设备上编写和执行下载程序脚本(2.vbs)。一旦MS-SQL脚本在受害者设备上运行完毕,就会开始执行恶意载荷。

利用和有效载荷:在受害计算机上执行的文件

在研究人员所看到的所有攻击中,攻击者都执行了一个命令行,其中包含两个可执行文件:

10.jpg

此命令负责执行特权升级攻击,该攻击使用系统特权运行恶意载荷。在攻击者的武器库中有两个版本的PE漏洞:apexp.exe和apexp2012.exe,以及许多有效载荷版本。

权限升级漏洞

apexp.exe和apexp2012.exe是两个已知权限升级漏洞(CVE-2014-4113)的变异漏洞,将任何程序传递给这些可执行程序都将使用系统权限运行它。

apexp.exe被称为Apolmy漏洞,它会影响Windows的桌面和服务器版本。这是一个带有生产级(production-level)代码的武器化漏洞。另一方面,apexp2012.exe类似于概念验证,而不是操作漏洞,并且可以在Windows 8.1上运行。研究人员发现apexp2012.exe可以在一个中文黑客论坛上下载。

虽然两个版本使用相同的漏洞,但是它们执行内核模式代码的目的不同。Apolmy版本将系统进程访问令牌复制到它自己的进程。有了这个令牌,攻击流程就可以运行有效载荷,并完全控制受害设备。

第二个版本使用了Cesar Cerrudo推广的方法。在这个方法中,该漏洞将SeDebugPrivilege添加到令牌中。使用此Windows特权,漏洞会将代码注入到winlogon进程。注入的代码创建一个新进程,该进程继承了winlogon的系统权限,提供与以前版本相同的权限。

有效载荷:rootkit和挖矿的Dropper

研究人员从攻击者的服务器和Guardicore Global Sensor Network (GGSN)收集了20个有效载荷样本。每个有效载荷实际上是一个包装器,具有以下几个功能:

1.对加密货币进行挖掘;

2.通过编写注册表运行键创建持久性攻击;

3.使用rootkit保护挖矿进程避免被终止;

4.使用看门狗机制确保挖矿工作持续执行。

有效载荷会生成dllhot.exe或canlang.exe,这取决于生成的有效载荷,dllhot.exe或canlang.exe为在四个不同的挖掘池挖掘名为TurtleCoin的加密货币。

11.jpg

为什么签名驱动程序是异常的?

内核模式驱动程序是在操作系统内核中运行的可执行文件,因此,它们具有访问敏感数据结构和资源的权限。

自Windows 10和Windows Server 2016起,Microsoft仅允许Microsoft签名的驱动程序以内核模式运行。要获得此类签名,开发人员必须向Microsoft提供其驱动程序的版本并通过大量测试。所以,恶意驱动程序被签名的几率非常低。

内核模式驱动程序

许多有效载荷会删除内核模式驱动程序,被删除的驱动程序会被随机命名,并放置在AppData/Local/Temp中。它的编译时间表明它是在2016年创建的,然而,大多数杀毒软件并没有检测到该驱动程序文件是恶意的。

研究人员发现该驱动程序具有由顶级证书颁发机构Verisign颁发的数字签名,不过该证书已过期,其名称为一家中国的空壳公司——杭州虎天网络科技有限公司。Guardicore Labs已经与Verisign联系,并提供了相关细节,以及时将该证书撤销。

12.png

Verisign发布的驱动程序数字签名

与许多其他恶意驱动程序不同,这个驱动程序受到VMProtect的保护。VMProtect 是新一代的软件保护系统,将保护后的代码放到虚拟机中运行,这将使分析反编译后的代码和破解变得极为困难。

驱动程序的设计目的指在保护进程并防止用户终止其恶意进程,它会创建一个名为SA6482的设备,允许进程与其通信。该设备接收进程ID(PID),这些ID意味着挖矿进行是受到保护的。

驱动程序通过在进程和线程对象类型上注册回调来保护进程,每次访问受保护进程或其任何线程时都会触发这些回调,并允许驱动程序修改每次访问尝试中给出的访问权限。

13.jpg

驱动程序本身包含额外的rootkit功能,比如与物理硬件设备通信以及修改这个特定恶意软件未使用的内部Windows进程对象。可以看出此次攻击的攻击技术相当先进,例如,apexp.exe利用内核模式漏洞来执行具有系统权限的代码。

另一个例子是被不同有效载荷不断删除的驱动程序,获取打包驱动程序的签名证书并非易事,需要认真规划和执行。此外,该驱动程序实际上支持从Windows 7到Windows 10的所有版本,包括beta版。

这些高级攻击手段与攻击者所采取的几个SecOps决策很不相符。首先,攻击者通常不会将整个基础设施保存在没有激活身份验证控制的文件服务器上。日志、受害者列表、用户名、二进制文件,研究人员只需单击鼠标即可完成所有操作。此外,所有二进制文件都有其原始时间戳,一个经验丰富的恶意软件开发者会篡改这些代码,从而使分析过程复杂化。

从IP扫描阶段到对受害设备的攻击和挖掘,整个攻击活动都经过了精心的设计。然而,各种错别字和错误意味着这不是一个经过充分测试的操作。例如,研究人员发现lcn.exe载荷的两个版本并不匹配。这两个载荷运行的都是相同的挖矿工具,但是使用的是交换的命令行参数,这表明第一个挖矿工具提供的钱包地址是错误的。

14.jpg

另外,服务器上的许多程序都是用EPL编程语言编写的。EPL是一种专用的、基于中文的编程语言,用于快速应用程序开发。

根据以上的这些证据,这次攻击很大程度上也是由中国的攻击者发动的:

1.攻击者选择使用基于中文的编程语言EPL编写工具;

2.为这次攻击活动部署的一些文件服务器是中文的HFS;

3.服务器上的许多日志文件和二进制文件都包含中文字符串;

缓解措施

在Windows MS-SQL服务器上发起这种攻击的原因是因为,服务器使用了较弱的用户名和密码。尽管听起来很不可思议,但千里之提毁于蚁穴,这足以说明设置一个好的密码是多么的重要。在此我们强烈建议,用户、企业以及组织机构使用强密码来保护其网络资产。 

另外,研究人员还发布了一个开源的PowerShell脚本,为用户提供了一种检测受攻击设备的简单方法。

IP

15.jpg

哈希值

685f1cbd4af30a1d0c25f252d399a666 xfa3BEB.tmp

c5c99988728c550282ae76270b649ea1 DesktopLayer.exe

70857e02d60c66e27a173f8f292774f1 apexp.exe

68862438fae4c937107999ff9d8ff709 apexp2012.exe

3ccb047b631ed6cab34ef11ccf43e47f sisr8Aj.sys

1f9007fbf6a37781f7880c10fc57a277 dllhot.exe

5899fde33dc7cf35477b998c714454eb dllhot.exe

1ad8d0594f9baffe332ccfefb25475df apexd.exe

1873944ee02b9e68af2d4997da5e5426 avast.exe

e6b9054759e4d2d10fcf42d47d9e9221 avast.exe

1770c9bf4a41c5115425d76df052b6a2 killtrtl.exe

2d740789efd7f16bff42651ae69b0893 kvast.exe

876e504b8ddb231d8eeaefa2b9e38093 kvast.exe

e27490ae6debe3be25794b4dcbaa8e24 gold.exe

1f0606c722693c9307ebf524c53f3375 kvast.exe

19594b72fc16539a5122217e6e3bb116 avast.exe

6dd0276e1f66f672e8c426c53b3125a5 rock.exe

82e55177fa37a34dca1375d542c06ac0 rock.exe

7c4b1ebba507bc2d0085278d28a899b2 rocks.exe

c06c3a79f70bfd5474bab8a13acdb87e rocks.exe

8ca92722641c73758e5a762033e09b11 lt.exe

9887d95973ac89c802571c2bbd346cbf canlang.exe

252d1721335108cdc643d36c40d4eaf6 lolcn.exe

b9161d07b4954d071ae0f26c81e56807 lolcn.exe

3425fc4d60a7401c934c73a12a30742b lcn.exe

93610bed2e15e2167a67c0e18fee7e08 lcn.exe

b79f7a7947cb7e9ea1f0d7648e765cee tl.exe

df4bacb064a4668e444fd67585ea1d82 tls.exe

文件路径

攻击脚本

C:\ProgramData\2.vbs attack script

文件的漏洞利用

C:\ProgramData\apexp.exe

C:\ProgramData\apexp2012.exe

被有效负载删除的文件

cfg.bat

canlang.exe

dllhot.exe

上文我们对特权文件操作的原理和可能发生漏洞的地方,进行了理论上的分析。本文我们接着将对象管理器(ObjectManager)符号链接以及漏洞利用的示例和思路。

对象管理器(ObjectManager)符号链接

对象管理器是一个执行体的子系统,所有其他的执行体子系统,特别是系统调用必须通过它来获得对WindowsNT资源的访问,这使得对象管理器成为资源管理的基础设施。对象管理器用来避免在其他子系统中管理资源带来的冗余与不安全。在对象管理器视角,每个资源都是一个对象,不论是物理资源(如文件系统或外设),还是逻辑资源(如一个互斥锁)。

虽然NTFS确实提供了文件系统符号链接,但是在Windows上,没有特权的用户不能在文件系统上创建符号链接。因为,它需要SeCreateSymbolicLinkPrivilege (创建符号链接权限),默认情况下只向管理员授予权限。

然而,无此特权的用户可以在Windows的对象管理器中创建符号链接,顾名思义,它可以管理诸如进程,部分和文件之类的对象。对象管理器使用符号链接,例如用于与相应设备相关联的驱动器字母和命名管道。用户可以在诸如\RPC CONTROL\之类的可写对象目录中创建对象符号链接,这些符号链接可以指向任意路径(包括文件系统上的路径),不管该路径当前是否存在。

当对象符号链接与NTFS交叉结合使用时,对象符号链接特别有趣。实际上,作为一个非特权用户,我们可以用对象管理器符号链接将一个安装点链接到\RPC Control\目录。

6.png

这给了我们一些行为有点像文件系统符号链接的东西,在上图中,C:\Dir\file.txt可以被解析为C:\Other\stuff.any。它们当然不是完全等同的,但在很多情况下,这足以滥用程序。

你可以使用CreateMountPoint和CreateDosDeviceSymlink分别执行这些步骤,但是CreateSymlink工具只会在一个方便的命令中实现了这项技术。

机会锁(opportunistic locks)

CIFS(通用Internet文件系统)中引入了一叫做opportunistic locks的锁,简称oplocks。 客户端可以锁定一个文件,但是这个锁可以随时被服务器撤消,oplocks的目的是让客户端上的文件缓存更加安全。

简而言之,机会锁就是是一个可以放在文件上的锁,当其他进程想要访问该文件时,它可以得到通知,同时延迟这些进程的访问,以便锁定进程可以在解除锁之前让文件处于适当的状态。最初设计的目的是让用户通过SMB缓存客户端-服务器文件访问,进而通过调用文件句柄上的特定控制代码在本地放置机会锁。

这对于利用TOCTOU漏洞非常有用,因为你可以通过锁定进程试图打开的文件或目录轻松赢得与进程的竞争。当然,它也有一些限制,比如你不能只允许一个访问(所有等待中的访问将在锁被解除后同时发生),并且它不适用于所有类型的访问。TOCTOU是time-of-check-to-time-of-use的缩写; TOCTTOU可发音为TOCK too。TOCTOU是指计算机系统的资料与权限等状态的检查与使用之间,因为某特定状态在这段时间已改变所产生的软件漏洞。

SetOpLock工具允许你创建这些锁,如果用命令“SET OPLOCK”锁定了一个本地用户,则所有的本地用户都会被锁定,并阻止对文件或目录的访问,直到你按Enter键释放锁为止。

James再次将这种技术与之前的技术相结合,创建了一个强大的原语,可以缓解对某些TOCTOU漏洞的利用:通过设置伪符号链接(如前所述),并在最终文件(符号链接的目标)上放置一个机会锁,我们可以在打开目标文件时更改符号链接(即使目标文件被锁定,符号链接也没有),让它指向另一个目标文件。

9.png

在上面显示的设置中,对文件C:\Dir\file.txt的第一次访问将打开C:\One\foo.xxx,第二次访问将打开C:\Two\bar.yyy。

BaitAndSwitch工具使用独占的机会锁实现此技术,如果你需要读取或写入锁,则可以使用SetOpLock和CreateSymlink。

以产品X为例,具体说明一下漏洞的利用

产品X具有以下行为:

1.在C:\ProgramData\Product\Logs(具有默认/继承访问权限的目录)中创建日志文件;

2.日志文件由特权(系统)和非特权(用户)进程创建/写入;

3.创建日志文件的过程设置了一个明显的ACL,这样每个人都可以写入文件;

11.jpg

这些行为足以导致了一个漏洞,黑客们可以利用这个漏洞创建包含任意内容的任意文件。

如果我们删除现有的日志文件, 并将日志目录转换为与C:\Windows\System32进行交叉,则产品X的特权的过程将创建System32系统的日志目录(多亏了从C:\ProgramData继承的访问权限)。

5.png

我们也可以使用符号链接技术来劫持一个特定的日志文件(如some.log),以创建一个具有攻击者选择的名称的任意文件,例如,程序目录中的DLL。

7.png

因为特权进程还在日志文件上设置了一个许可的ACL,所以我们还可以根据自己的喜好更改文件的内容。

以下是在几个产品上发现的同一个漏洞,可能是因为它是一个普通需求的简单实现(所有组件都可以写的日志文件——用户和系统组件,所有组件的公共日志代码)。在过去的一年里,我们看到了该漏洞存在于以下几个产品中:

· 在Rylance Hanson发现的Cylance产品中;

· 在Ben Turner发现的Symantec / Altiris代理

· McAfee Endpoint Security中(已修补)

· 在Mark Barnes发现的NVIDIA GeForce ExperienceIntel Driver&Support Assistant

· Pulse Secure VPN客户端(未打补丁)中;

在具有任意文件写入特权的进程上下文中获取代码执行

在具有任意文件写入特权的进程上下文中获取代码执行的两种常用技术是:

1.DLL劫持:在特权进程将加载DLL的位置(在应用程序的目录、System32、Windows或系统的%PATH%上的其他目录中)创建DLL。它需要一种方法(重新)启动特权进程来加载有效载荷,,以及DLL将在被劫持之前加载的位置。

2.覆盖:替换将让我们代码执行的现有二进制/脚本/配置文件/等,除了要求(重新)启动进程外,它还需要文件写入操作以允许覆盖现有文件(另外目标文件不应被锁定),并且通常只特定于给定的服务/应用程序。

4.png

至少有两种鲜为人知的技术:

1. 使用C:\Windows\System32\ Wow64Log.dll在特权32位进程中加载64位DLL。默认情况下,此DLL不存在(至少在消费者版本上),并且在所有32位进程中被加载。但是,DLL不能一直使用来自Kernel32的导入,因此它必须只使用NTDLL API,当然,这只有在你有一个有趣的(特权的)32位进程注入时才有效,这个技巧是由George Nicolaou发现的。

2.使用“诊断中心标准收集器服务(Diagnostics Hub Standard Collector Service)”:这项技术是由James Forshaw发现的,他在这篇博客文章中详细解释了这项技术,并发表了一个利用该漏洞的示例。简而言之,该技术可以让DiagHub服务(作为系统运行)以DLL的形式加载System32中的任何扩展名的文件。因此,如果你可以在System32中创建一个带有有效载荷的test.log文件(当然,文件的内容必须是一个可用的DLL),只需使用此技术将该DLL加载到特权服务中。然而,这项技术会在即将发布的Windows 10中被限制。

5.png

控制内容

以上这些技术需要对创建的文件的内容进行控制,因为如果你可以将文件的创建劫持到任意位置,但是无法控制文件中的内容,那么它的攻击性则非常有限。在本文的漏洞示例中,我们有一个由特权程序在生成的文件上设置的很好的ACL,但是如果没有这条件怎么办?

我们可以尝试针对其他操作,在本文的日志示例中,假设日志功能在日志达到特定大小时轮换日志,则特权进程可能会移动或重命名日志文件(例如,从abc.log改变为abc.old.log)。然后我们可以使用符号链接滥用此操作:

1.通过经过伪造的符号链接将重命名/移动操作中的源文件替换为我们的有效载荷(sh.dll);

2.通过经过伪造的符号链接将目标文件替换为我们要创建或替换的文件(本文用的是target.dll);

因此,重命名操作发生时的布局如下所示:

12.jpg

当特权进程试图将abc.log移动或重命名为abc.old.log时,它实际上会将用户拥有的文件sh.dll移动或重命名为target.dll,以将我们的有效载荷放在要执行的正确位置。

因此,我们可以控制的特权文件移动/重命名/复制操作都是非常有趣的原语:

1.受控移动或重命名为我们提供任意文件写入;

2.完全受控(源和目标)副本也是如此;

3.在复制操作中,我们控制源文件,但不控制目标文件,这样就可以读取任意的文件(如果目标位置是用户可读的);

4.一个我们控制源文件而不是目标文件的移动/重命名操作,该操作可以让我们删除任意一个文件;

注意:

1.覆盖目标的能力取决于执行操作的进程所使用的选项;

2.如果目标文件已经存在,我们还可以使用硬链接代替伪符号链接;

3.滥用任意文件读取的常见方法是获取SAM,SECURITY和SYSTEM配置单元以转储SAM数据库和缓存凭据;

利用权限升级删除任意文件

以上我们讨论了利用权限升级可以写入任意文件,那么可以删除任意文件吗?除了具有明显的DoS潜力外,我们有时可以滥用EoP的任意文件删除漏洞删除以下文件来:

1.是否位于可以写入的位置,即使不能覆盖现有的位置,比如C:\ProgramData;

2.稍后将用于特权进程的读取或写入操作(无论是我们用于删除的相同进程还是不同的进程);

例如,如果我们知道如何触发从C:\ ProgramData \ Product \ foo到C:\ ProgramData \ Product \ bar的移动/重命名,但这些文件已经存在且我们没有写入权限,那么我们就可以使用任意文件删除漏洞来删除foo和bar,并自己重新创建它们。我们可以使用以前的技术来滥用写入操作(如果产品目录现在是空的,则使用伪符号链接,否则使用硬链接)并完成链接。

利用滥用 Windows 特权文件操作的漏洞来绕过杀毒软件的检测

绕过杀毒软件的检测是这类漏洞的主要目标,因为具有高度特权的软件是不受这类漏洞限制的。它们一旦躲开追踪,就可以操纵一切文件了,包括用户拥有的文件。执行扫描、删除和恢复的特权进程有时会欺骗我们执行有趣的文件操作,从而将防御组件变成摆设。

利用杀毒软件的隔离和恢复功能

隔离和恢复功能特别有趣,尤其是当它们可以由非特权用户触发时(有时不在UI中,但可以通过COM劫持访问),触发隔离(或删除)的最简单方法当然是将已知的检测到的文件(如EICAR)放入文件系统中。

有趣的是,有些杀毒软件会在删除检测到的文件前执行特权操作,例如:

1.在同一目录中创建/删除临时文件;

2.将受感染的文件复制或移动到用户可写的位置;

3.将受感染的文件复制或移动到用户可读的隔离位置(如果你利用此文件,请注意不要破坏SAM文件);

临时和隔离文件有时会进行编码或填充,如果你想查看算法(读取结果文件),那么在启动IDA/Ghidra之前最好检查Hexacorn的DeXRAY

如果非特权用户可以触发恢复过程,则恢复过程是另一个示例或特权文件写漏洞。要控制内容,要么在删除或恢复期间查找潜在的TOCTOU,要么将载荷设置为“恶意”,以便首先对其进行隔离。

移动文件删除/隔离

如果杀毒软件在检测和删除/隔离(TOCTOU)期间没有锁定(或以其他方式阻止访问)被检测到的文件,则我们可以使用一个有趣的交叉技巧来替换它的父目录(检测之后,删除之前)。如果我们想要删除一些一些我们无权访问的文件(例如,C:\ Windows \ System32 \ license.rtf),我们可以这样做:

1.在我们创建的目录中删除EICAR(或任何可检测文件),其名称与目标文件相同,例如C:\Temp\Test\ lic.rtf;

2.等待杀毒软件检测到它;

3.删除或重命名父目录C:\Temp\Test;

4.用交叉把它换成C:\Windows\System32;

5.杀毒软件删除已经被解析成C:\Windows\System32\licence.rtf的C:\Temp\Test\licence.rtf;

正确的方法是使用机会锁,但是在实践中并不总是那么容易实现,因为文件在被删除之前可以被访问很多次。一种快速且简单的方法是简单地在目录旁边创建交叉,并进行一个循环,不断地交换这两个目录。根据杀毒软件检索删除路径和删除文件的方式,我们可能有一个删除该交叉的机会。目前,该方法已经在几款杀毒软件产品上起了作用。

滥用Windows特权文件操作的漏洞已经被发现

去年年初,在常见的软件产品中发现了以下这样的漏洞,目前它们硬件被报告给各自的供应商,其中一些供应商已经发布了补丁。下表总结了我们发现的漏洞及其当前的状态。

13.jpg

注意:缺少的产品名称和额外的细节将在修复时发布,这样做是为了避免漏洞发生野外利用。

目前,以下3个漏洞的详细信息已经被公开了:

· Pulse Secure VPN client 

· McAfee Endpoint Security 

· F-Secure SAFE 

本文介绍了如何滥用Windows上的特权进程执行文件,来实现本地权限升级(从用户升级到管理员/系统权限)。除此之外,我还介绍了利用这类漏洞的可用技术、工具和具体过程。

特权文件操作漏洞

以高权限运行的进程会对所有进程中执行的文件执行操作,这意味着,当高权限进程在没有足够预防措施的情况下,可以访问用户控制的所有文件或目录。因此,从理论上说,这就是一个安全漏洞,因为恶意攻击者有可能会滥用该特权进程执行的操作,使特权文件做一些不应该做的事情。对于许多特权访问用户控制的资源的情况都是如此,文件只是一个简单的目标。

在渗透测试中,大家熟知的示例包括用户可写的服务可执行文件和DLL劫持漏洞,如果你对特权服务将执行的文件具有写入权限,或者对它将在其中查找DLL的目录具有写入权限,那么你可以在这个特权进程中执行有效载荷。不过,以上举例的这个漏洞已经众所周知了,除了偶尔的配置漏洞发生之外,一般的防护软件都可以对它进行预防了。

然而,其他文件系统操作的潜在滥用似乎不那么为人所知了,但同样和以上所说的漏洞一样危险。如果你可以让一个特权进程为你创建、复制、移动或删除任意文件,那么使用system函数来调用shell脚本的漏洞就离你不远了。

此外,由于这些都是逻辑漏洞,它们通常非常稳定(不涉及内存损坏),通常能够在代码重构中存活(只要文件操作逻辑不变),并且无论处理器体系结构如何,它们都以完全相同的方式被滥用。对攻击者来说,这些功能非常有价值。

漏洞的寻找过程

用户可写的位置

虽然大多数特权程序不会直接操作一些非特权用户的文件(有些例外,如AV),但许多程序会对可能位于用户可以操作的某个位置的文件执行操作。非特权用户在以下一些位置,是具有某种形式的写入权限的:

1.用户自己的文件和目录,包括其AppData和Temp文件夹,如果你足够幸运或运行AV,某些特权进程可能会使用;

2.公众用户的文件和目录:idem;

3.在C:\中创建的目录具有默认ACL(访问控制列表):默认情况下,在分区根目录中创建的目录具有允许用户写入的许可ACL;

4.具有默认ACL的C:\ ProgramData子目录:默认情况下,用户可以创建文件和目录,但不能修改现有文件和目录,这通常是第一个看的位置;

5. C:\ Windows \ Temp的子目录:默认情况下,用户可以创建文件和目录,但不能修改现有文件和目录,也不能读取其他用户创建的文件/访问目录。有意查看安装程序和准时运行的其他特权软件和脚本,而不检查预先存在的文件和目录;

你可以使用特定的工具和命令(例如SysInternals的AccessChk,icacls或PowerShell的Get-Acl)检查文件权限,也就可以使用浏览器的“安全”选项卡来检查文件权限,“高级”表单具有“有效访问”选项卡,允许列出特定帐户或组对其的访问权限该文件/目录(如AccessChk在命令行上执行)。下面的屏幕截图显示了在C:\ProgramData目录上授予用户组的(默认)访问权限:

1.jpg

寻找特权文件操作

要查找特权进程执行的文件操作的示例,我们可以简单地使用SysInternals的ProcMon,Procmon是微软出品用于监视Windows系统里程序的运行情况,监视内容包括该程序对注册表的写入、对文件的写入、网络的连接、进程和线程的调用情况,procmon是一款超强的系统监视软件。Procmon为感兴趣的进程过滤文件事件,当我们看到它访问用户可控制的文件和目录时,就可以检查该进程是否使用模拟客户端来实现这一点。

2.jpg

漏洞利用技术与工具

一旦我们发现在用户/用户可控制的文件和目录上,可以执行对一些文件的操作,我们就需要一种方法来劫持这些操作,进而实施攻击。

值得庆幸的是,James Forshaw @tiraniddo )通过他在NTFS文件系统和Windows内部的开创性工作完成了所有繁重工作,他在众多文章中发表了其中的技术细节。他提出了几种滥用Windows文件系统和路径解析功能的技术(以下我会详细介绍),并在开源的symboliclink-test -tools toolkit和NtApiDotNet库中实现了这些技术。他的技术和工具包为许多测试人员(包括我自己)打开了一扇寻找这种类型的漏洞的门,让这些漏洞的利用成为可能。

NTFS 交叉

 交叉是一个NTFS功能,它允许将目录设置为文件系统的安装点(mount point),就像Unix中的安装点一样,但是也可以设置为解析到另一个目录(在同一个或另一个文件系统上)。在本文在,我们可以把它们看作是一种只包含目录的符号交叉。

3.png

有趣的是,在大多数情况下,路径解析将遵循 交叉规则(除非明确设置参数以防止这种情况),因此在上面的设置中,尝试打开C:\ Dir \ file.txt的程序实际上将打开C:\ Other \ file.txt。

连接可以由非特权用户创建,由于它们可以跨卷工作,因此你也可以将C:\Dir“重定向”到D:\OtherDir。如果你具有对现有目录的写入权,则可以将其转换为 交叉,但必须为空。

NTFS交叉是用重解析点(reparse points)实现的,虽然内置工具不允许这样做,但是可以通过设置自定义重解析点的实现将它们解析为任意路径。CreateMountPoint工具允许你完成重解析点实现,对于常规 交叉,你还可以将mklink和PowerShell的New-Item与-Type Junction参数一起使用。

NTFS重解析点(Reparse Points)

随Windows 2000发布的NTFS版本5里最有趣的一个属性是引入了一些特殊的文件系统功能,并应用于特定的文件或目录上。这些特殊功能使NTFS文件系统更加强大和有扩展性。这个特性的实现基础叫做重解析点(reparse points)。

重解析点的使用源于一些应用程序想把一些特殊数据存储到特殊的地方——重解析点,然后由应用程序做上特殊的标记,只允许它使用。为此文件系统引入了一个应用程序相关的特殊过滤器(application-specific filter),并与重解析点的标记关联起来。多个应用程序可以把不同的数据存储到同一个重解析点文件里,只要使用不同的标记。微软保留了几个标记为自己使用。

现在我们假设用户打算访问一个有标记的重解析点文件。当文件系统打开文件时,发现有重解析点关联到这个文件,于是“重解析”这个打开文件请求,发现与应用程序相关联的可用过滤器,并与这个重解析点进行匹配,通过后就可以把重解析点的数据传送给这个过滤器了。过滤器于是可以把这些数据用于任何途径,依赖于应用程序最初的定义。这是一个非常灵活的系统:应用程序不需关心重解析点是如何工作的,重解析点的实现细节对于用户是完全透明的。你只需简单的放入和拿出数据,其余的事情都是自动完成,这使文件系统的功能大大增强了。

微软使用重解析点在Windows 2000里实现了如下的功能:

1. 符号链接(Symbolic Links):符号链接允许你创建一个指向其他地方某个文件的指针。NTFS并没有像UNIX文件系统那样实现“真正”的文件符号链接,但是从功能上重解析点完全可以模拟得一模一样。本质上,NTFS的符号链接就是一个重解析点,把对一个文件的访问转移到另一个文件身上。

2. 交叉点(Junction Points):交叉点和符号链接类似,只不过对象是目录而不是文件。

3.卷装载点(Volume Mount Points):卷装载点和前2者类似,只是更进一层:它能创建对整个卷的链接。比如,你可以为可移动硬盘或其他存储介质创建卷装载点,或者让本地的不同分区(C:,D:,E:等等)看起来就像在一个卷里一样。这对于那些大型的CD-ROM服务器非常有用,如果没有卷装载点,它们就只能为每张磁盘人工维护一个分区字母。

4.远程存储服务器(RSS:Remote Storage Server):Windows 2000的这个特性能利用一些规则来移除NTFS卷上不常用的文件,放到存档介质里(比如CD-RW或磁带)。当它把文件移出到“下线”或“半下线”的存储介质上时,RSS自动创建指向这个存档文件的重解析点,以备日后使用。

硬链接 (hard link)

我们都知道文件都有文件名与数据,这在 Linux 上被分成两个部分:用户数据 (user data) 与元数据 (metadata)。用户数据,即文件数据块 (data block),数据块是记录文件真实内容的地方;而元数据则是文件的附加属性,如文件大小、创建时间、所有者等信息。在 Linux 中,元数据中的 inode 号(inode 是文件元数据的一部分但其并不包含文件名,inode 号即索引节点号)才是文件的唯一标识而非文件名。文件名仅是为了方便人们的记忆和使用,系统或程序通过 inode 号寻找正确的文件数据块。

为解决文件的共享使用,Linux 系统引入了两种链接:硬链接 (hard link) 与软链接(又称符号链接,即 soft link 或 symbolic link)。链接为 Linux 系统解决了文件的共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若一个 inode 号对应多个文件名,则称这些文件为硬链接。换言之,硬链接就是同一个文件使用了多个别名(见 图 2.hard link 就是 file 的一个别名,他们有共同的 inode)。硬链接可由命令 link 或 ln 创建。如下是对文件 oldfile 创建硬链接。

由于硬链接是有着相同 inode 号仅文件名不同的文件,因此硬链接存在以下几点特性:

· 文件有相同的 inode 及 data block;

· 只能对已存在的文件进行创建;

· 不能交叉文件系统进行硬链接的创建;

· 不能对目录进行创建,只可对文件创建;

删除一个硬链接文件并不影响其他有相同 inode 号的文件。

所以,非特权用户还可以创建硬链接,与Unix对应的硬链接一样,将作为现有文件的附加路径。它不能在目录上工作,也不能跨卷工作(对于硬链接来说没有意义)。

8.png

此外,内置工具不允许你创建到没有写入权限的文件的硬链接,但是实际的系统调用允许你使用打开供读取的文件来创建硬链接。使用symboliclink-test -tools中的CreateHardLink工具(或Ruben Boonen编写的这个PowerShell脚本)创建指向你没有写入权限的文件的硬链接。

注意,如果没有对文件的写入权限,就不能删除创建的链接。另外,这项技术在即将发布的Windows 10中得到了缓解。

本文我们对特权文件操作的原理和可能发生漏洞的地方,进行了理论上的分析。下文我们接着将对象管理器(ObjectManager)符号链接以及漏洞利用的示例和思路。