本文由 简悦 SimpRead转码,原文地址 knight.sc
我最近在查看 MITRE ATT&CK™ 知识库,看到了有关流程注入的页面......。
最近,我在查看MITRE ATT&CK™ 知识库时,看到了关于权限升级的进程注入技术 页面。对于那些不了解MITRE ATT&CK™ 知识库的人来说,它是一组涵盖常见对手战术和技术的文档和定义。MacOS 和 Linux 的进程注入部分被混在一起,而且不是很详细。在某些情况下,这些信息对 macOS 来说似乎并不准确。本文介绍了适用于 macOS 的常见进程注入技术。
这是 macOS 上最知名、最常见的代码注入技术之一。通过将 DYLD_INSERT_LIBRARIES
环境变量设置为自己选择的 dylib,然后启动应用程序,攻击者就可以在启动的进程中运行 dylib 代码。在旧版本的 macOS 中,这可用于将 dylib 注入具有更高权限的苹果平台应用程序。这样,被注入的 dylib 也能获得这些额外权限。自 macOS 10.12 中添加 SIP 后,苹果平台二进制文件就不能再使用这种技术了。从 macOS 10.14 开始,第三方开发人员也可以为自己的应用程序选择加固运行时。这也可以防止使用此技术注入 dylibs。
以下是 MacOS 上 DYLD_INSERT_LIBRARIES 运行方式的几个示例:
thomasfinch.me/blog/2015/0…
blog.timac.org/2012/1218-s…
Thread Injection
如果查找 Windows 上的代码注入技术,线程注入是最常见的技术之一。有了像 CreateRemoteThread
这样的 API,整个过程就相当简单,不需要太多代码。如果你尝试在 macOS 上搜索同样的内容,你会发现资源要少得多。幸运的是,MacOS and iOS Internals 系列丛书的作者 Jonathan Levin 在他的网站上提供了一个很好的例子。
该示例使用了马赫的 thread_create_running
API。由于 macOS 具有双重性格,既有低级的 Mach API,也有 BSD API,因此存在两套用于处理线程的 API。一个是 Mach API,另一个是 pthread
API。遗憾的是,macOS 的某些内部组件希望每个线程都已通过 BSD API 正确创建,并已正确设置了所有 Mach 线程结构和 pthread
结构。为了处理这个问题,上面的 inject.c 示例试图首先调用注入代码中的 _pthread_set_self
,以使线程进入工作状态。
这种方法在 macOS 10.14 之前一直运行良好,因为在该版本中,某些 pthread
内部代码发生了变化。我想在 10.14 及更高版本上获得此示例的工作版本,因此决定研究一下 pthread
代码。在 macOS 10.14 之前,_pthread_set_self
代码的作用如下:
libpthread-301.50.1/src/pthread.c
PTHREAD_NOINLINE
void
_pthread_set_self(pthread_t p)
{
return _pthread_set_self_internal(p, true);
}
PTHREAD_ALWAYS_INLINE
static inline void
_pthread_set_self_internal(pthread_t p, bool needs_tsd_base_set)
{
if (p == NULL) {
p = &_thread;
}
uint64_t tid = __thread_selfid();
if (tid == -1ull) {
PTHREAD_ABORT("failed to set thread_id");
}
p->tsd[_PTHREAD_TSD_SLOT_PTHREAD_SELF] = p;
p->tsd[_PTHREAD_TSD_SLOT_ERRNO] = &p->err_no;
p->thread_id = tid;
if (needs_tsd_base_set) {
_thread_set_tsd_base(&p->tsd[0]);
}
}
这段代码允许我们将 NULL
传递给 _pthread_set_self
调用,反过来,它将根据应用程序的主线程设置一些内部 pthread
结构。在注入情况下,这种方法非常理想,因为我们是从一个裸马赫线程开始的,没有设置任何 pthread
结构,也没有对任何其他线程的引用。在 macOS 10.14 及更高版本中,此代码已发生变化,不能再向 _pthread_set_self
传递 NULL
libpthread-330.201.1/src/pthread.c
PTHREAD_NOINLINE
void
_pthread_set_self(pthread_t p)
{
#if VARIANT_DYLD
if (os_likely(!p)) {
return _pthread_set_self_dyld();
}
#endif // VARIANT_DYLD
_pthread_set_self_internal(p, true);
}
#if VARIANT_DYLD
// _pthread_set_self_dyld is noinline+noexport to allow the option for
// static libsyscall to adopt this as the entry point from mach_init if
// desired
PTHREAD_NOINLINE PTHREAD_NOEXPORT
void
_pthread_set_self_dyld(void)
{
pthread_t p = main_thread();
p->thread_id = __thread_selfid();
if (os_unlikely(p->thread_id == -1ull)) {
PTHREAD_INTERNAL_CRASH(0, "failed to set thread_id");
}
// <rdar://problem/40930651> pthread self and the errno address are the
// bare minimium TSD setup that dyld needs to actually function. Without
// this, TSD access will fail and crash if it uses bits of Libc prior to
// library initialization. __pthread_init will finish the initialization
// during library init.
p->tsd[_PTHREAD_TSD_SLOT_PTHREAD_SELF] = p;
p->tsd[_PTHREAD_TSD_SLOT_ERRNO] = &p->err_no;
_thread_set_tsd_base(&p->tsd[0]);
}
#endif // VARIANT_DYLD
PTHREAD_ALWAYS_INLINE
static inline void
_pthread_set_self_internal(pthread_t p, bool needs_tsd_base_set)
{
p->thread_id = __thread_selfid();
if (os_unlikely(p->thread_id == -1ull)) {
PTHREAD_INTERNAL_CRASH(0, "failed to set thread_id");
}
if (needs_tsd_base_set) {
_thread_set_tsd_base(&p->tsd[0]);
}
}
内部实现分为用户空间 libpthread
库中无法访问的 dyld 专用实现和另一个内部实现,后者需要传递一个有效的线程。事实上,如果传入 null,_pthread_set_self_internal
就会崩溃,因为它希望参数是存在的。
我决定继续查看 pthread
源代码,寻找另一个可以帮助将裸马赫线程引导到正确设置的 pthread
的函数。我最终发现了 pthread_create_from_mach_thread
函数。这个函数从 macOS 10.12 开始就存在了,所以应该能在 10.12 及以上版本上运行。它调用内部的 _pthread_create
实现,并将 true
传递给 from_mach_thread
参数。我只能在系统中找到一个实际使用此 API 的二进制文件:Xcode DVTInstrumentsFoundation.framework
中的 RemoteInjectionAgent
。
我们的想法是注入一个裸马赫线程作为引导线程,然后使用pthread_create_from_mach_thread
创建第二个完全配置好的合法pthread
。下面是 Jonathan Levin 示例中修改后的 injectedCode
代码。
_injectedCode:
00000001000020d0 push rbp ; DATA XREF=_inject+576, _inject+1014
00000001000020d1 mov rbp, rsp
00000001000020d4 sub rsp, 0x10
00000001000020d8 lea rdi, qword [rbp-8]
00000001000020dc xor eax, eax
00000001000020de mov ecx, eax
00000001000020e0 lea rdx, qword [_injectedCode+56] ; 0x100002108
00000001000020e7 mov rsi, rcx
00000001000020ea movabs rax, 0x5452434452485450 ; PTHRDCRT
00000001000020f4 call rax
00000001000020f6 mov dword [rbp-0xc], eax
00000001000020f9 add rsp, 0x10
00000001000020fd pop rbp
00000001000020fe mov rax, 0xd13
0000000100002105 jmp _injectedCode+53 ; CODE XREF=_injectedCode+53
0000000100002107 ret
0000000100002108 push rbp ; DATA XREF=_injectedCode+16
0000000100002109 mov rbp, rsp
000000010000210c sub rsp, 0x10
0000000100002110 mov esi, 0x1
0000000100002115 mov qword [rbp-8], rdi
0000000100002119 lea rdi, qword [aLiblibliblib] ; "LIBLIBLIBLIB"
0000000100002120 movabs rax, 0x5f5f4e45504f4c44 ; DLOPEN__
000000010000212a call rax
000000010000212c xor esi, esi
000000010000212e mov edi, esi
0000000100002130 mov qword [rbp-0x10], rax
0000000100002134 mov rax, rdi
0000000100002137 add rsp, 0x10
000000010000213b pop rbp
000000010000213c ret
aLiblibliblib:
000000010000213d db "LIBLIBLIBLIB", 0
您可以从以下链接下载此代码的完整更新工作示例:
关于这项技术有几点说明。首先,它依赖于调用 task_for_pid
来获取受害者进程的 Mach 任务端口。您只能以 root 身份执行此操作,而且就像 dylib 注入一样,由于 macOS 10.12 及更高版本上的 SIP,您无法在苹果平台二进制文件上使用 task_for_pid
。因此,尽管这仍然是一种有趣的技术,但它在权限升级方面并不那么有用。这种技术过去曾在 iOS 漏洞利用中使用过,当时另一个漏洞利用程序允许任务端口泄露给攻击进程。
Thread Hijacking
MacOS 上另一种可能的技术是线程劫持。我们不在远程进程中创建线程,而是检索现有线程,强迫它运行我们想要的内容。苹果一直在锁定 "task_for_pid "以及任何需要任务端口的马赫 API,以防止滥用泄漏的任务端口。因此,线程劫持成为一种更有趣的技术。布兰登-阿扎德(Brandon Azad)围绕这一技术撰写了一篇精彩的文章,在此我就不详细介绍了。我强烈建议你阅读以下内容:
我简单研究了一下这种技术,并尝试劫持一个线程,运行代码,然后将线程恢复到初始状态。看起来我们能用 thread_get_state
保存的并不是所有状态,而且线程经常会崩溃。不过,如果你只是想在特权应用程序的上下文中执行代码,这在其他用途上已经足够好了,但如果你想在不经意间控制另一个进程,这就不够好了。您可以在这里查看我的代码示例:
如果你对这项技术感兴趣,我强烈推荐你阅读 Brandon Azad 的 threadexec 库代码。它详细介绍了这一技术,并与他的上述文章相辅相成。不幸的是,他似乎和我得出了类似的结论,即试图保存和恢复线程状态并不那么可靠。
ptrace?
如果您阅读了 ATT&CK 页面,您可能会认为在 Linux 和 macOS 上,"跟踪 "API 可用于代码注入。但 macOS 上的情况并非如此。虽然 macOS 上确实存在 ptrace
系统调用,但它并未完全实现。例如,PTRACE_PEEKTEXT
、PTRACE_POKETEXT
、PTRACE_GETREGS
、PTRACE_SETREGS
调用都不存在。
我认为还可能存在其他尚未探索的技术。由于 libdispatch
是使应用程序能够并行工作的核心库之一,这似乎是一个尚未被充分探索的领域。我的想法是,可以向远程进程注入有效调度块格式的代码,然后将该调度块提交到工作队列中。或者,也有可能找到一个已排队但当前未运行的程序块,并劫持该程序块指向的代码。我还没有时间深入研究这个问题,但我认为这绝对是一个有趣的研究领域。
希望你能看到,在 macOS 上有多种不同的代码注入技术。我希望通过更多类似的文章,让更多人了解代码注入技术。在下一篇文章中,我将介绍检测这些技术的不同方法。