AMFI Syscall | Offensive Security

Blog

Research & Tutorials

Jun 9, 2020

AMFI syscall

Csaba Fitzl covers the dyld restriction decision process in macOS and a previously undiscussed or undocumented AMFI (AppleMobileFileIntegrity) system call.

25 min read

The mysterious syscall of AMFI

On macOS, one popular technique to inject code into other applications is leveraging the [ccie]DYLD_INSERT_LIBRARIES[/ccie] environment variable, which I wrote about in 2019 DYLD_INSERT_LIBRARIES DYLIB injection in macOS / OSX. This variable can store a colon-separated list of dynamic libraries to load before the ones specified in the target process.

Several limitations apply to when this injection technique can be used and when it cannot, which I also discussed. I revisited this topic, not only because things might have changed since then but also to ensure that I didn’t miss anything. It turned out to be a wise move and a useful exercise.

The restrictions around the use of environment variables are implemented in dyld2.cpp, specifically in the [ccie]configureProcessRestrictions[/ccie] function. When I analyzed the function in more detail, it turned out I had overlooked an important point previously.

In this post, we will cover how the [ccie]dyld[/ccie] dynamic linker restriction decision process is different on newer versions of the OS. With that, we will uncover a previously undiscussed or undocumented AppleMobileFileIntegrity (AMFI) system call.

The following code snippet shows the entire [ccie]configureProcessRestrictions[/ccie] function from the source code.
[cce lang=”c”] 1 static void configureProcessRestrictions(const macho_header* mainExecutableMH, const char* envp[])
2 {
3 uint64_t amfiInputFlags = 0;
4 #if TARGET_OS_SIMULATOR
5 amfiInputFlags |= AMFI_DYLD_INPUT_PROC_IN_SIMULATOR;
6 #elif __MAC_OS_X_VERSION_MIN_REQUIRED
7 if ( hasRestrictedSegment(mainExecutableMH) )
8 amfiInputFlags |= AMFI_DYLD_INPUT_PROC_HAS_RESTRICT_SEG;
9 #elif __IPHONE_OS_VERSION_MIN_REQUIRED
10 if ( isFairPlayEncrypted(mainExecutableMH) )
11 amfiInputFlags |= AMFI_DYLD_INPUT_PROC_IS_ENCRYPTED;
12 #endif
13 uint64_t amfiOutputFlags = 0;
14 const char* amfiFake = nullptr;
15 if ( dyld3::internalInstall() && dyld3::BootArgs::enableDyldTestMode() ) {
16 amfiFake = _simple_getenv(envp, “DYLD_AMFI_FAKE”);
17 }
18 if ( amfiFake != nullptr ) {
19 amfiOutputFlags = hexToUInt64(amfiFake, nullptr);
20 }
21 if ( (amfiFake != nullptr) || (amfi_check_dyld_policy_self(amfiInputFlags, &amfiOutputFlags) == 0) ) {
22 gLinkContext.allowAtPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_AT_PATH);
23 gLinkContext.allowEnvVarsPrint = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS);
24 gLinkContext.allowEnvVarsPath = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS);
25 gLinkContext.allowEnvVarsSharedCache = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE);
26 gLinkContext.allowClassicFallbackPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS);
27 gLinkContext.allowInsertFailures = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION);
28 }
29 else {
30 #if __MAC_OS_X_VERSION_MIN_REQUIRED
31 // support chrooting from old kernel
32 bool isRestricted = false;
33 bool libraryValidation = false;
34 // any processes with setuid or setgid bit set or with __RESTRICT segment is restricted
35 if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {
36 isRestricted = true;
37 }
38 bool usingSIP = (csr_check(CSR_ALLOW_TASK_FOR_PID) != 0);
39 uint32_t flags;
40 if ( csops(0, CS_OPS_STATUS, &flags, sizeof(flags)) != -1 ) {
41 // On OS X CS_RESTRICT means the program was signed with entitlements
42 if ( ((flags & CS_RESTRICT) == CS_RESTRICT) && usingSIP ) {
43 isRestricted = true;
44 }
45 // Library Validation loosens searching but requires everything to be code signed
46 if ( flags & CS_REQUIRE_LV ) {
47 isRestricted = false;
48 libraryValidation = true;
49 }
50 }
51 gLinkContext.allowAtPaths = !isRestricted;
52 gLinkContext.allowEnvVarsPrint = !isRestricted;
53 gLinkContext.allowEnvVarsPath = !isRestricted;
54 gLinkContext.allowEnvVarsSharedCache = !libraryValidation || !usingSIP;
55 gLinkContext.allowClassicFallbackPaths = !isRestricted;
56 gLinkContext.allowInsertFailures = false;
57 #else
58 halt(“amfi_check_dyld_policy_self() failed\n”);
59 #endif
60 }
61 }
[/cce]

<p id="l:amfi_1"style="text-align: center; font-size:14px; color: #444;">Listing – The configureProcessRestrictions function from dyld2

The global variables [ccie]gLinkContext.allowEnvVarsPath[/ccie] and [ccie]gLinkContext.allowEnvVarsPrint[/ccie] control whether environment variables will be processed or ignored by the dynamic linker. Therefore, they affect our injection mechanism. These two global variables are set by negating the [ccie]isRestricted[/ccie] local variable (line 52-53) in the [ccie]else[/ccie] branch at the end of the function (line 29-60). The logic of this branch is similar to older implementations of [ccie]dyld[/ccie] and it can be summarized as follows:

The [ccie]isRestricted[/ccie] variable is set to true (and consequently injection is blocked) if:

  1. The main executable has a restricted segment (line 35-37)
  2. suid / guid bits are set (line 35-37)
  3. System Integrity Protection (SIP) is enabled and the program has the [ccie]CS_RESTRICT[/ccie] flag (line 38-44) set

If the [ccie]CS_REQUIRE_LV[/ccie] flag is set, it will lift the restrictions (line 46-49). This flag, however, indicates that library validation is active. Therefore, even if restrictions around environment variables are loosened, library validation policy will stop our injection attempts because our dylb won’t match the target process code signature.

In analyzing the [ccie]else[/ccie] branch conditions, we can make a few considerations.

The code doesn’t deal with situations where:

  1. the binary is signed with hardened runtime and,
  2. unrestrictive entitlements like [ccie]com.apple.security.cs.allow-dyld-environment-variables[/ccie] are set. This would enable environment variables regardless of hardened runtime being set.

Additionally, if SIP (System Integrity Protection) is enabled, which is the default on latest versions of macOS, this [ccie]else[/ccie] branch will never be hit by the call flow.

So what will set the restrictions around injection in these cases, then? The answer is AppleMobileFileIntegrity (AMFI).

If we go back to the source code, we can see that there is a significant part at the beginning that refers to AMFI, including constants, variables, and a function call (line 3-28).

AMFI is a kernel extension, which was originally introduced in iOS. In version 10.10 it was also added to macOS. It extends the MACF (Mandatory Access Control Framework), just like the Sandbox, and it has a key role in enforcing SIP and code signing.

Let’s repeat that code segment here.
[cce lang=”c”] uint64_t amfiInputFlags = 0;
#if TARGET_OS_SIMULATOR
amfiInputFlags |= AMFI_DYLD_INPUT_PROC_IN_SIMULATOR;
#elif __MAC_OS_X_VERSION_MIN_REQUIRED
if ( hasRestrictedSegment(mainExecutableMH) )
amfiInputFlags |= AMFI_DYLD_INPUT_PROC_HAS_RESTRICT_SEG;
#elif __IPHONE_OS_VERSION_MIN_REQUIRED
if ( isFairPlayEncrypted(mainExecutableMH) )
amfiInputFlags |= AMFI_DYLD_INPUT_PROC_IS_ENCRYPTED;
#endif
uint64_t amfiOutputFlags = 0;
const char* amfiFake = nullptr;
if ( dyld3::internalInstall() &&
dyld3::BootArgs::enableDyldTestMode() ) {
amfiFake = _simple_getenv(envp, “DYLD_AMFI_FAKE”);
}
if ( amfiFake != nullptr ) {
amfiOutputFlags = hexToUInt64(amfiFake, nullptr);
}
if ( (amfiFake != nullptr) ||
(amfi_check_dyld_policy_self(amfiInputFlags, &amfiOutputFlags) == 0) ) {
gLinkContext.allowAtPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_AT_PATH);
gLinkContext.allowEnvVarsPrint = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS);
gLinkContext.allowEnvVarsPath = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS);
gLinkContext.allowEnvVarsSharedCache = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE);
gLinkContext.allowClassicFallbackPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS);
gLinkContext.allowInsertFailures = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION);
[/cce]

<p id="l:amfi_2"style="text-align: center; font-size:14px; color: #444;">Listing – The AMFI related code in configureProcessRestrictions

For macOS, we can ignore the [ccie]#if[/ccie] code blocks that are related to iOS or a simulator. These will not compile into the macOS binary, so we can remove them from the code for easier reading.
[cce lang=”c”] 1 uint64_t amfiInputFlags = 0;
2 if ( hasRestrictedSegment(mainExecutableMH) )
3 amfiInputFlags |= AMFI_DYLD_INPUT_PROC_HAS_RESTRICT_SEG;
4 uint64_t amfiOutputFlags = 0;
5 const char* amfiFake = nullptr;
6 if ( dyld3::internalInstall() && dyld3::BootArgs::enableDyldTestMode() ) {
7 amfiFake = _simple_getenv(envp, “DYLD_AMFI_FAKE”);
8 }
9 if ( amfiFake != nullptr ) {
10 amfiOutputFlags = hexToUInt64(amfiFake, nullptr);
11 }
12 if ( (amfiFake != nullptr) || (amfi_check_dyld_policy_self(amfiInputFlags, &amfiOutputFlags) == 0) ) {
13 gLinkContext.allowAtPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_AT_PATH);
14 gLinkContext.allowEnvVarsPrint = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS);
15 gLinkContext.allowEnvVarsPath = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS);
16 gLinkContext.allowEnvVarsSharedCache = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE);
17 gLinkContext.allowClassicFallbackPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS);
18 gLinkContext.allowInsertFailures = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION);
19 }
[/cce]

<p id="l:amfi_3"style="text-align: center; font-size:14px; color: #444;">Listing – The cleaned up AMFI related code in configureProcessRestrictions

The code will start by checking whether the binary has a [ccie]RESTRICTED[/ccie] segment. If this is the case, it changes the [ccie]amfiInputFlags[/ccie] variable, (line 2-3) which will be passed to the [ccie]amfi_check_dyld_policy_self[/ccie] function call later (line 12).

The [ccie]internalInstall[/ccie] function called at line 6 in the listing above can be found in [ccie]dyld-732.8/dyld3/Loading.cpp[/ccie]. This function will check if the SIP-related flag [ccie]CSR_ALLOW_APPLE_INTERNAL[/ccie] is set or not by calling the [ccie]csr_check[/ccie] function. On default installations, this is not set. [ccie]csr_check[/ccie] will return zero if the flag is set. If it’s not, it will return a different value, which will cause [ccie]internalInstall[/ccie] function to return FALSE, as the equal ([ccie]==[/ccie]) condition won’t be met.
[cce lang=”c”]bool internalInstall()
{
return ( csr_check(CSR_ALLOW_APPLE_INTERNAL) == 0 );
}
[/cce]

<p id="l:amfi_4"style="text-align: center; font-size:14px; color: #444;">Listing – The internalInstall function in Loading.cpp

With that, the following branch in [ccie]configureProcessRestrictions[/ccie] will be never called, as the first part of the [ccie]&&[/ccie] operation will be FALSE.
[cce lang=”c”]6 if ( dyld3::internalInstall() && dyld3::BootArgs::enableDyldTestMode() ) {
7 amfiFake = _simple_getenv(envp, “DYLD_AMFI_FAKE”);
8 }
[/cce]

<p id="l:amfi_5"style="text-align: center; font-size:14px; color: #444;">Listing – The if branch in configureProcessRestrictions will not be taken if internalInstall returns false.

[ccie]amfiFake[/ccie] would be only set by the previously skipped branch, thus the next [ccie]if[/ccie] branch will be also skipped.
[cce lang=”c”] 9 if ( amfiFake != nullptr ) {
10 amfiOutputFlags = hexToUInt64(amfiFake, nullptr);
11 }
[/cce]

<p id="l:amfi_6"style="text-align: center; font-size:14px; color: #444;">Listing – The next if branch in configureProcessRestrictions will not be taken

At this point, the code will call [ccie]amfi_check_dyld_policy_self[/ccie].
[cce lang=”c”]12 if ( (amfiFake != nullptr) || (amfi_check_dyld_policy_self(amfiInputFlags, &amfiOutputFlags) == 0) ) {
13 gLinkContext.allowAtPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_AT_PATH);
14 gLinkContext.allowEnvVarsPrint = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS);
15 gLinkContext.allowEnvVarsPath = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS);
16 gLinkContext.allowEnvVarsSharedCache = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE);
17 gLinkContext.allowClassicFallbackPaths = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS);
18 gLinkContext.allowInsertFailures = (amfiOutputFlags & AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION);
19 }
[/cce]

<p id="l:amfi_7"style="text-align: center; font-size:14px; color: #444;">Listing – The call to amfi_check_dyld_policy_self and the following code segment in configureProcessRestrictions

But what does [ccie]amfi_check_dyld_policy_self[/ccie] do? A Google search only reveals the source code from Apple, and nothing else. Let’s investigate the source code to see if we can find out what happens when this function is called.

There is a definition in [ccie]dyld2.cpp[/ccie], but it only indicates that it’s an external function. We might know more about it if we had the [ccie]libamfi.h[/ccie] header file, but unfortunately it’s not available from Apple.
[cce lang=”c”]#include <libamfi.h>
extern “C” int amfi_check_dyld_policy_self(uint64_t input_flags,
uint64_t* output_flags);
[/cce]

<p id="l:amfi_8"style="text-align: center; font-size:14px; color: #444;">Listing – The definition of amfi_check_dyld_policy_self in dyld2.cpp

There is, however, a function with this name in [ccie]dyld-732.8/src/glue.c[/ccie].
[cce lang=”c”]uint64_t amfi_check_dyld_policy_self(uint64_t inFlags, uint64_t* outFlags)
{
if ( gSyscallHelpers->version >= 10 )
return gSyscallHelpers->amfi_check_dyld_policy_self(inFlags, outFlags);
outFlags = 0x3F; // on old kernel, simulator process get all flags
return 0;
}
[/cce]

<p id="l:amfi_9"style="text-align: center; font-size:14px; color: #444;">Listing – amfi_check_dyld_policy_self function in glue.c

Upon inspecting the code, we find a check on the [ccie]gSyscallHelpers[/ccie] structure. If we follow [ccie]gSyscallHelpers[/ccie], we arrive at another header file, [ccie]dyld-732.8/src/dyldSyscallInterface.h[/ccie]. We can see that [ccie]gSyscallHelpers[/ccie] is a structure with plenty of function pointers, and one of them is the one we are looking for.
[cce lang=”c”]// This file contains the table of function pointers the host dyld supplies
// to the iOS simulator dyld.
//
struct SyscallHelpers
{
(…)
int (amfi_check_dyld_policy_self)(uint64_t input_flags, uint64_t
output_flags);
(…)
};
extern const struct SyscallHelpers
gSyscallHelpers;
[/cce]

<p id="l:amfi_10"style="text-align: center; font-size:14px; color: #444;">Listing – Part of the gSyscallHelpers structure in dyldSyscallInterface.h

Comments on both of the last two code segments indicate that they are related to an iOS simulator. The name of the structure suggests that this is a system call. Again, in a quick Google search for AMFI syscall, we can’t find anything that would reveal what this system call does, or if AMFI has any system calls in general.

If we go ahead and reverse engineer the binary, we’ll see what happens at that point. We’ll use Hopper for static analysis and [ccie]lldb[/ccie] for dynamic.

We will start by doing static analysis with Hopper. If we open [ccie]/usr/lib/dyld[/ccie] and look for the [ccie]configureProcessRestrictions[/ccie], the first thing we will notice is that it’s not there. After looking into the decompiled code, we find that this function is actually compiled inline in [ccie]dyld_main[/ccie]. Below is an excerpt from the decompiled main function showing the key parts of [ccie]configureProcessRestrictions[/ccie]. We added references to the first source code outline as comments.
[cce lang=”c”]int __ZN4dyld5_mainEPK12macho_headermiPPKcS5_S5_Pm(void * arg0, long
arg1, int arg2, int * * arg3, int * * arg4, int * * arg5, long * arg6) {
(…)
rbx = dyld::hasRestrictedSegment(r13); //line 7
rax = dyld3::internalInstall(); //line 15-17
var_8A8 = r15;
if ((rax == 0x0) || (dyld3::BootArgs::enableDyldTestMode() == 0x0)) goto loc_65fd;

loc_6589: //line 16-18
rax = __simple_getenv(r12, “DYLD_AMFI_FAKE”);
if (rax == 0x0) goto loc_65fd;
(…)

loc_65fd: //line 21
var_450 = 0x0;
rax = _amfi_check_dyld_policy_self((rbx & 0xff) + (rbx & 0xff), &var_450);
if (rax == 0x0) goto loc_66cf;

loc_6617: //line 35-38
if ((_issetugid() == 0x0) && (dyld::hasRestrictedSegment(r13) == 0x0)) {
r15 = 0x0;
}
else {
r15 = 0x1;
}
r14 = _csr_check(0x4);
[/cce]

<p id="l:amfi_11"style="text-align: center; font-size:14px; color: #444;">Listing – The decompiled code of dyldmain</p> <p>We can see the [ccie]else[/ccie] branch starting at [ccie]loc_6617[/ccie] (read: offset 0x6617). This is the part that won&#8217;t be taken on new versions of macOS with SIP enabled. [ccie]loc_65fd[/ccie] is where the call to AMFI will happen. Fortunately, we can disassemble or decompile [ccie]_amfi_check_dyld_policy_self[/ccie] here, which reveals that it is a wrapper around [ccie]___sandbox_ms[/ccie].<br /> [cce lang=&#8221;c&#8221;]int _amfi_check_dyld_policy_self(int arg0, int arg1) {<br /> rsi = arg1; //amfiOutputFlags address<br /> rdi = arg0; //amfiInputFlags<br /> if (rsi != 0x0) {<br /> stack[-8] = rbp;<br /> rbp = &amp;stack[-8];<br /> stack[-16] = rbx;<br /> rsp = rsp &#8211; 0x28;<br /> rbx = rsi; //amfiOutputFlags address<br /> *rsi = 0x0;<br /> *(rbp &#8211; 0x10) = 0xaaaaaaaaaaaaaaaa; //amfiOutputFlags value<br /> *(rbp &#8211; 0x20) = rdi; //amfiInputFlags<br /> *((rbp &#8211; 0x20) + 0x8) = rbp &#8211; 0x10;<br /> if (___sandbox_ms(&#8220;AMFI&#8221;, 0x5a, rbp &#8211; 0x20, rbp &#8211; 0x10) !=<br /> 0x0) {<br /> rax = ___error();<br /> rax = *(int32_t *)rax;<br /> }<br /> else {<br /> rax = 0x0;<br /> }<br /> *rbx = *(rbp &#8211; 0x10); //amfiOutputFlags value<br /> }<br /> else {<br /> rax = 0x16;<br /> }<br /> return rax;<br /> }<br /> [/cce]</p> <p id="l:amfi_12"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; The decompiled code of amfi_check_dyld_policy_self</p> <p>But what is [ccie]___sandbox_ms[/ccie]? It&#8217;s basically a wrapper for the [ccie]__mac_syscall[/ccie] system call. We can go into [ccie]___sandbox_ms[/ccie], and verify that there is indeed a syscall call in this function. The specified syscall number is [ccie]0x17d[/ccie] which corresponds to [ccie]__mac_syscall[/ccie]<a href="https://opensource.apple.com/source/xnu/xnu-6153.11.26/bsd/kern/syscalls.master.auto.html">syscalls.master</a>. The [ccie]0x2[/ccie] in [ccie]0x200017d[/ccie] specifies the class of the syscall, a Unix/BSD-style syscall in this case.<br /> [cce]___sandbox_ms:<br /> 0000000000056108 mov eax, 0x200017d<br /> 000000000005610d mov r10, rcx<br /> 0000000000056110 syscall<br /> 0000000000056112 jae loc_5611c<br /> 0000000000056114 mov rdi, rax<br /> 0000000000056117 jmp _cerror_nocancel<br /> loc_5611c:<br /> 000000000005611c ret<br /> [/cce]</p> <p id="l:amfi_13"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; Disassembly of ___sandbox_ms</p> <p>[cce lang=&#8221;c&#8221;]__mac_syscall[/ccie] allows us to create an [ccie]ioctl[/ccie] type system call for one of the policy modules registered in the MACF (Mandatory Access Control Framework). The definition can be found in [ccie]xnu-6153.11.26/security/mac.h[/ccie]<br /> [cce]int __mac_syscall(const char *_policyname, int _call, void *_arg);<br /> [/cce]</p> <p id="l:amfi_14"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; __mac_syscall definition in mac.h</p> <p>While the [ccie]policyname[/ccie] defines which MACF module we want to call, the [ccie]_call[/ccie] acts as an [ccie]ioctl[/ccie] number to specify which function to call inside the module. Finally, [ccie]*_arg[/ccie] are variable number of arguments to pass to the MACF function.</p> <p>If we look back at the code in [ccie]amfi_check_dyld_policy_self[/ccie], the policy selector is [ccie]AMFI[/ccie], the [ccie]ioctl[/ccie] code is [ccie]0x5a[/ccie], and the input/output flags are passed in as arguments.<br /> [cce lang=&#8221;c&#8221;]*(rbp &#8211; 0x10) = 0xaaaaaaaaaaaaaaaa; //amfiOutputFlags value<br /> *(rbp &#8211; 0x20) = rdi; //amfiInputFlags<br /> *((rbp &#8211; 0x20) + 0x8) = rbp &#8211; 0x10;<br /> if (___sandbox_ms(&#8220;AMFI&#8221;, 0x5a, rbp &#8211; 0x20, rbp &#8211; 0x10) !=<br /> 0x0) {<br /> rax = ___error();<br /> rax = *(int32_t *)rax;<br /> }</p> <p>[/cce]</p> <p id="l:amfi_13a"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; __mac_syscall parameters in amfi_check_dyld_policy_self</p> <p>When the function returns, the flags will be saved in the originally provided memory location. ([ccie]*rbx = *(rbp &#8211; 0x10); //amfiOutputFlags value[/ccie])</p> <p>Let&#8217;s verify this with dynamic analysis. Our debugger of choice for this action is [ccie]lldb[/ccie]. We simply use a dummy Hello World C code, which only prints out one line, and we didn&#8217;t sign our compiled binary.<br /> [cce]% lldb ./hello<br /> (lldb) target create &#8220;./hello&#8221;<br /> Current executable set to &#8216;/Users/user/hello&#8217; (x86_64).<br /> [/cce]</p> <p id="l:amfi_15"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; Starting lldb and specifying target executable</p> <p>We will set a couple of breakpoints:</p> <ol> <li>dyld main function</li> <li>_amfi_check_dyld_policy_self</li> </ol> <p>[cce](lldb) b dylddyld::_main
Breakpoint 1: where = dylddyld::_main(macho_header const*, unsigned<br /> long, int, char const**, char const**, char const**, unsigned long*),<br /> address = 0x00000000000060ed</p> <p>(lldb) b dyldamfi_check_dyld_policy_self
Breakpoint 2: where = dyldamfi_check_dyld_policy_self, address =<br /> 0x000000000004a7f6<br /> [/cce]</p> <p id="l:amfi_16"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; Setting breakpoint in lldb</p> <p>Next, we will start our process with the [ccie]run[/ccie] command and break at the dyld&#8217;s main entry.<br /> [cce](lldb) r<br /> Process 28240 launched: &#8216;/Users/user/hello&#8217; (x86_64)<br /> Process 28240 stopped<br /> * thread #1, stop reason = breakpoint 1.1<br /> frame #0: 0x000000010000a0ed dylddyld::_main(macho_header const*,
unsigned long, int, char const**, char const**, char const**, unsigned
long*)
dylddyld::_main:<br /> -&gt; 0x10000a0ed &lt;+0&gt;: push rbp<br /> 0x10000a0ee &lt;+1&gt;: mov rbp, rsp<br /> 0x10000a0f1 &lt;+4&gt;: push r15<br /> 0x10000a0f3 &lt;+6&gt;: push r14<br /> Target 0: (hello) stopped.<br /> [/cce]</p> <p id="l:amfi_17"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; Starting the process, and hitting our first breakpoint</p> <p>We can continue with the [ccie]continue[/ccie] command, hitting the AMFI call.<br /> [cce](lldb) con<br /> Process 28240 resuming<br /> Process 28240 stopped<br /> * thread #1, stop reason = breakpoint 2.1<br /> frame #0: 0x000000010004e7f6 dyldamfi_check_dyld_policy_self
dyldamfi_check_dyld_policy_self:<br /> -&gt; 0x10004e7f6 &lt;+0&gt;: test rsi, rsi<br /> 0x10004e7f9 &lt;+3&gt;: je 0x10004e848 ; &lt;+82&gt;<br /> 0x10004e7fb &lt;+5&gt;: push rbp<br /> 0x10004e7fc &lt;+6&gt;: mov rbp, rsp<br /> Target 0: (hello) stopped.<br /> [/cce]</p> <p id="l:amfi_18"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; Hitting the breakpoint at dyldamfi_check_dyld_policy_self

From here, we can single-step with the [ccie]step[/ccie] instruction, and we finally hit the syscall.
cce
Process 28240 stopped
* thread #1, stop reason = instruction step into
frame #0: 0x000000010005a110 dyld__sandbox_ms + 8<br /> dyld__sandbox_ms:
-> 0x10005a110 <+8>: syscall
0x10005a112 <+10>: jae 0x10005a11c ; <+20>
0x10005a114 <+12>: mov rdi, rax
0x10005a117 <+15>: jmp 0x1000583e8 ; cerror_nocancel
Target 0: (hello) stopped.
(lldb)
Process 28240 stopped
* thread #1, stop reason = instruction step into
frame #0: 0x000000010005a112 dyld__sandbox_ms + 10<br /> dyld__sandbox_ms:
-> 0x10005a112 <+10>: jae 0x10005a11c ; <+20>
0x10005a114 <+12>: mov rdi, rax
0x10005a117 <+15>: jmp 0x1000583e8 ; cerror_nocancel
0x10005a11c <+20>: ret
Target 0: (hello) stopped.
[/cce]

<p id="l:amfi_19"style="text-align: center; font-size:14px; color: #444;">Listing – Arriving to the syscall with single steps

If we single-step onward (out of the function calls), we will get to the point where the output value is saved. Recall this line from our decompiled code, where the [ccie]amfiOutputFlags[/ccie] are saved by dereferencing a stack pointer and stored at a memory location pointed by [ccie]rbx[/ccie]:
[cce]*rbx = *(rbp – 0x10); //amfiOutputFlags value
[/cce]

<p id="l:amfi_20"style="text-align: center; font-size:14px; color: #444;">Listing – Setting of amfiOutputFlags in the decompiler

In the debugger, the following listing shows the same point:
cce lang=”c” step
Process 28240 stopped
* thread #1, stop reason = instruction step into
frame #0: 0x000000010004e854 dyldamfi_check_dyld_policy_self + 94<br /> dyldamfi_check_dyld_policy_self:
-> 0x10004e854 <+94>: mov qword ptr [rbx], rcx
0x10004e857 <+97>: add rsp, 0x18
0x10004e85b <+101>: pop rbx
0x10004e85c <+102>: pop rbp
Target 0: (hello) stopped.
(lldb) register read rcx
rcx = 0x000000000000005f
[/cce]

<p id="l:amfi_21"style="text-align: center; font-size:14px; color: #444;">Listing – Setting of amfiOutputFlags in the debugger

Here the flags are stored in [ccie]rcx[/ccie] and their value is [ccie]0x5f[/ccie]. If we continue single-stepping, we will arrive back at the location where our initial [ccie]else[/ccie] branch in [ccie]configureProcessRestrictions[/ccie] would start. Stepping forward, we will see that the branch is not taken, as we expected, and the [ccie]issetugid[/ccie] and other calls will be skipped:
cce lang=”c” step
Process 28240 stopped
* thread #1, stop reason = instruction step into
frame #0: 0x000000010000a611 dylddyld::_main(macho_header const*,<br /> unsigned long, int, char const**, char const**, char const**, unsigned<br /> long*) + 1316<br /> dylddyld::_main:
-> 0x10000a611 <+1316>: je 0x10000a6cf ; <+1506>
0x10000a617 <+1322>: call 0x10005a4e0 ; issetugid
0x10000a61c <+1327>: test eax, eax
0x10000a61e <+1329>: jne 0x10000a630 ; <+1347>

(lldb) step
Process 28240 stopped
* thread #1, stop reason = instruction step into
frame #0: 0x000000010000a6cf dylddyld::_main(macho_header const*,<br /> unsigned long, int, char const**, char const**, char const**, unsigned<br /> long*) + 1506<br /> dylddyld::_main:
-> 0x10000a6cf <+1506>: mov rcx, qword ptr [rbp – 0x450]
0x10000a6d6 <+1513>: mov eax, ecx
0x10000a6d8 <+1515>: and al, 0x1
0x10000a6da <+1517>: mov byte ptr [rip + 0x90d15], al ;
dyld::gLinkContext + 397
Target 0: (hello) stopped.
[/cce]

<p id="l:amfi_22"style="text-align: center; font-size:14px; color: #444;">Listing – Verifying in the debugger that the else branch is not taken

If we check the flag value of [ccie]0x5f[/ccie] it translates to binary [ccie]0101 1111[/ccie], which means that all of the flags are set (see below for reference).
[cce lang=”c”] enum amfi_dyld_policy_output_flag_set {
AMFI_DYLD_OUTPUT_ALLOW_AT_PATH = (1 << 0),
AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS = (1 << 1),
AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE = (1 << 2),
AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS = (1 << 3),
AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS = (1 << 4),
AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION = (1 << 5),
};

if ( amfi_check_dyld_policy_self(amfiInputFlags, &amfiOutputFlags) == 0 ) {
gLinkContext.allowAtPaths = (amfiOutputFlags &
AMFI_DYLD_OUTPUT_ALLOW_AT_PATH);
gLinkContext.allowEnvVarsPrint = (amfiOutputFlags &
AMFI_DYLD_OUTPUT_ALLOW_PRINT_VARS);
gLinkContext.allowEnvVarsPath = (amfiOutputFlags &
AMFI_DYLD_OUTPUT_ALLOW_PATH_VARS);
gLinkContext.allowEnvVarsSharedCache = (amfiOutputFlags &
AMFI_DYLD_OUTPUT_ALLOW_CUSTOM_SHARED_CACHE);
gLinkContext.allowClassicFallbackPaths = (amfiOutputFlags &
AMFI_DYLD_OUTPUT_ALLOW_FALLBACK_PATHS);
gLinkContext.allowInsertFailures = (amfiOutputFlags &
AMFI_DYLD_OUTPUT_ALLOW_FAILED_LIBRARY_INSERTION);
}
[/cce]

<p id="l:amfi_23"style="text-align: center; font-size:14px; color: #444;">Listing – AMFI output flags related code from dyld2.cpp

If we run the same exercise for a binary that has a restricted segment, for example, we will get a result value of [ccie]0x40[/ccie]. This translates to none of the above flags set, which means the environment variables leveraged for injections won’t be allowed. This further confirms that this system call will properly control whether environmental variables should be ignored or not by the dynamic linker on newer versions of macOS.

The only remaining part to cover is what happens in the kernel. The AMFI kext that we need to inspect can be found at [ccie]/System/Library/Extensions/AppleMobileFileIntegrity.kext/Contents/MacOS/AppleMobileFileIntegrity[/ccie]. We will not debug the syscall in kernel mode, only review the code via static analysis in Hopper.

The AMFI MAC (Mandatory Access Control) registration happens at the [ccie]_amfi_register_mac_policy[/ccie] function call. The following listing shows the code for this function where we notice a call to [ccie]_initializeAppleMobileFileIntegrity[/ccie]:
[cce lang=”c”]int _amfi_register_mac_policy() {
_initializeAppleMobileFileIntegrity();
return 0x0;
}
[/cce]

<p id="l:amfi_24"style="text-align: center; font-size:14px; color: #444;">Listing – The amfi_register_mac_policy function

If we check the [ccie]_initializeAppleMobileFileIntegrity[/ccie] function implementation, we notice that it uses the string “AMFI” for the [ccie]mac_policy[/ccie] argument. This makes sense, as we saw the same string passed as an argument to the syscall in userspace to select the MACF module we wanted to call.
[cce lang=”c”]mac_policy = “AMFI”;
(…)
rax = _mac_policy_register(mac_policy, amfiPolicyHandle, 0x0);
[/cce]

<p id="l:amfi_25"style="text-align: center; font-size:14px; color: #444;">Listing – The AMFI policy registration with the kernel

The actual syscall handler is implemented in [ccie]policy_syscall[/ccie] which receives three arguments:
[cce lang=”c”]int __ZL15_policy_syscallP4prociy(void * arg0, int arg1, unsigned long
long arg2)
[/cce]

<p id="l:amfi_26"style="text-align: center; font-size:14px; color: #444;">Listing – The AMFI syscall handler function, policy_syscall

We can determine the type of these arguments by looking at the implementation of [ccie]__mac_syscall[/ccie] in kernel mode in [ccie]xnu-6153.11.26/security/mac_base.c[/ccie]:
[cce lang=”c”]/

* __mac_syscall: Perform a MAC policy system call
*
* Parameters: p Process calling this routine
* uap User argument descriptor (see below)
* retv (Unused)
*
* Indirect: uap->policy Name of target MAC policy
* uap->call MAC policy-specific system call to perform
* uap->arg MAC policy-specific system call arguments
*
* Returns: 0 Success
* !0 Not success
*
*/
int
__mac_syscall(proc_t p, struct __mac_syscall_args *uap, int *retv __unused)
{
(…)
for (i = 0; i < mac_policy_list.staticmax; i++) {
mpc = mac_policy_list.entries[i].mpc;
if (mpc == NULL) {
continue;
}

if (strcmp(mpc->mpc_name, target) == 0 &&
mpc->mpc_ops->mpo_policy_syscall != NULL) {
error = mpc->mpc_ops->mpo_policy_syscall(p,
uap->call, uap->arg);
goto done;
}
}
(…)
[/cce]

<p id="l:amfi_27"style="text-align: center; font-size:14px; color: #444;">Listing – Parts of __mac_syscall from mac_base.c

If we map [ccie]mpo_policy_syscall(p,uap->call,uap->arg)[/ccie] to the comment section, we can determine that the first argument is the calling process, the second argument is the MAC policy-specific system call to perform, and the third argument refers to the MAC policy-specific system call arguments.

We can confirm this by looking at the same function ([ccie]policy_syscall[/ccie]) in Hopper. [ccie]rsi[/ccie] takes the 2nd argument ([ccie]arg1[/ccie]). This should be the policy-specific syscall value (~ioctl value), which in our case was [ccie]0x5a[/ccie]. If we inspect what happens in this function, we can see that this is indeed one of the two values that is being checked for (the other is [ccie]0x5b[/ccie]). From there, we will have a call to [ccie]check_dyld_policy_internal[/ccie].
[cce lang=”c”]int __ZL15_policy_syscallP4prociy(void * arg0, int arg1, unsigned long
long arg2) {
rsi = arg1;
rdi = arg0;
rax = arg2;
if (rsi != 0x5b) {
rbx = 0x4e;
if (rsi == 0x5a) {
if (rax != 0x0) {
var_30 = 0xaaaaaaaaaaaaaaaa;
r14 = rdi;
rax = _copyin(rax, &var_30, 0x10, 0x0);
rbx = rax;
if (rax == 0x0) {
var_18 = 0x0;
_check_dyld_policy_internal(r14,
0xaaaaaaaaaaaaaaaa, &var_18);
rax = _copyout(&var_18,
0xaaaaaaaaaaaaaaaa, 0x8);
rbx = rax;
}
}
else {
rbx = 0x16;
}
}
}
[/cce]

<p id="l:amfi_28"style="text-align: center; font-size:14px; color: #444;">Listing – Part of the decompiled code of AMFIpolicy_syscall function</p> <p>If we check the [ccie]check_dyld_policy_internal[/ccie] call, the further function names are descriptive. AMFI will do various verifications against the calling process, and essentially determine the flags.<br /> [cce lang=&#8221;c&#8221;]int __ZL27_check_dyld_policy_internalP4procyPy(void * arg0, unsigned<br /> long long arg1, unsigned long long * arg2) {<br /> var_3C = 0xffffffffaaaa0000;<br /> macos_dyld_policy_collect_state(arg0, arg1, &amp;var_3C);<br /> rax = macos_dyld_policy_at_path(arg0, &amp;var_3C);<br /> var_30 = rax;<br /> rax = macos_dyld_policy_env_vars(arg0, &amp;var_3C);<br /> r13 = rax;<br /> rax = macos_dyld_policy_fallback_paths(arg0, &amp;var_3C);<br /> r14 = rax;<br /> rax = macos_dyld_policy_library_interposing(arg0, &amp;var_3C);<br /> rbx = rax;<br /> rcx = (_cs_require_lv(arg0) != 0x0 ? 0x1 : 0x0) &lt;&lt; 0x5;<br /> rax = arg2;<br /> *rax = rbx | r14 | r13 | var_30 | rcx;<br /> return rax;<br /> }<br /> [/cce]</p> <p id="l:amfi_29"style="text-align: center; font-size:14px; color: #444;">Listing &#8211; Part of the decompiled code of AMFIcheck_dyld_policy_internal function

We will not analyze each of these one by one, as it would be excessive, but one function is worth a look: [ccie]macos_dyld_policy_collect_state[/ccie]. In reviewing it, we can see that it collects a lot of information for the calling process. It verifies many of the code signing properties of the process, including the entitlements. It’s beyond the scope of this post to analyze this in detail, but by looking through the function names and strings, we can see that it does what we expect. This is the point where all of the entitlements are also being taken into consideration, which is what we were missing originally when we were looking at [ccie]configureProcessRestrictions[/ccie].
[cce lang=”c”]int
__Z31macos_dyld_policy_collect_stateP4procyP24amfi_dyld_policy_state_t(void
* arg0, unsigned long long arg1, void * arg2) {
r13 = arg2;
r14 = arg1;
r15 = arg0;
*(int16_t *)r13 = *(int32_t *)r13 & 0xfffffffe | (_csr_check(0x2,
arg1, arg2) != 0x0 ? 0x1 : 0x0);
*(int16_t )r13 = (_cs_restricted(r15) & 0x1) * 0x2 + ((int32_t
*)r13 & 0xfffffff9) + (r14 & 0x2) * 0x2;
*(int16_t )r13 = ((int32_t *)r13 & 0xfffffff7) +
(_proc_issetugid(r15) & 0x1) * 0x8;
*(int16_t *)r13 = *(int32_t *)r13 & 0xffffffef | _cs_require_lv(r15)
<< 0x4 & 0x10;
*(int16_t *)r13 = *(int32_t *)r13 & 0xffffffdf |
_csproc_forced_lv(r15) << 0x5 & 0x20;
*(int16_t *)r13 = *(int32_t *)r13 & 0xffffffbf |
_csproc_get_platform_binary(r15) << 0x6 & 0x40;
(…)
*(int16_t *)r13 = 0xfffffffffffffeff & *(int32_t *)r13 |
(proc_has_entitlement(r15,
“com.apple.security.cs.allow-relative-library-loads”) & 0xff) << 0x8;
*(int16_t *)r13 = 0xfffffffffffffdff & *(int32_t *)r13 |
(proc_has_entitlement(r15,
“com.apple.security.cs.allow-dyld-environment-variables”) & 0xff) << 0x9;
*(int16_t *)r13 = r14 << 0xb & 0x800 | (proc_has_entitlement(r15,
“com.apple.security.get-task-allow”) & 0xff) << 0xa | 0xfffffffffffff3ff
& *(int32_t *)r13;
*(int16_t *)r13 = r14 << 0xb & 0x2000 | 0xffffffffffffcfff &
*(int32_t *)r13 | (_csr_check(0x10) == 0x0 ? 0x1 : 0x0) << 0xc;
rbx = 0x4000;
if (proc_has_entitlement(r15, “com.apple.security.app-sandbox”) ==
0x0) {
rbx = 0x4000;
if (proc_has_entitlement(r15,
“com.apple.security.app-sandbox.optional”) == 0x0) {
rbx = (proc_has_entitlement(r15,
“com.apple.security.app-protection”) & 0xff) << 0xe;
[/cce]

<p id="l:amfi_30"style="text-align: center; font-size:14px; color: #444;">Listing – Part of the decompiled code of AMFI`macos_dyld_policy_collect_state function

This function also reveals an undocumented entitlement [ccie]com.apple.security.cs.allow-relative-library-loads[/ccie].

To sum up, we saw that [ccie]dyld[/ccie] uses AMFI to determine the process restrictions around injection on newer version of macOS. We verified that with both static and dynamic analysis. During our investigation, we uncovered a previously undocumented AMFI MACF policy system call.


About the Author

Csaba Fitzl has worked for 6 years as a network engineer and 8 years as a blue/red teamer in a large enterprise focusing on malware analysis, threat hunting, exploitation, and defense evasion. Currently, he is focusing on macOS research and working at OffSec as a content developer. He gives talks and workshops at various international IT security conferences, including Hacktivity, hack.lu, Troopers, SecurityFest, DEFCON, and Objective By The Sea. @theevilbit