Last week, I’ve looked at various security features offered by macOS, how they are enabled, and especially if they can be enabled on binary files without creating .app
bundles. Introduced in OS X 10.5, the quarantine is enabled with an extended flag attribute (xattr
) added to downloaded files, assuming the application respects this convention. The flag used by macOS is com.apple.quarantine
, and Gatekeeper relies on it to only verify files from untrusted origin. Yup, you read right - files coming from untrusted sources will bypass Gatekeeper if they don’t have this flag set.
Extended flag attributes are listed when you pass -@
to the ls
command:
~/Code/sandboxie ❯ ls -lh@
-rw-r--r-- 1 maximeg staff 127B May 2 01:13 safe
-rw-r--r--@ 1 maximeg staff 60B May 2 01:45 test.txt
com.apple.metadata:kMDItemWhereFroms 100B
com.apple.quarantine 57B
The xattr
command can be used to list, modify, delete, or clear the extended flag attributes.
# List the xattrs:
~/Code/sandboxie ❯ xattr -r test.txt
test.txt: com.apple.metadata:kMDItemWhereFroms
test.txt: com.apple.quarantine
# Read the xattr values:
~/Code/sandboxie ❯ xattr -l test.txt
com.apple.metadata:kMDItemWhereFroms:
00000000 62 70 6C 69 73 74 30 30 A2 01 02 5F 10 1C 68 74 |bplist00..._..ht|
00000010 74 70 3A 2F 2F 30 2E 30 2E 30 2E 30 3A 38 30 30 |tp://0.0.0.0:800|
00000020 30 2F 74 65 73 74 2E 74 78 74 5F 10 14 68 74 74 |0/test.txt_..htt|
00000030 70 3A 2F 2F 30 2E 30 2E 30 2E 30 3A 38 30 30 30 |p://0.0.0.0:8000|
00000040 2F 08 0B 2A 00 00 00 00 00 00 01 01 00 00 00 00 |/..*............|
00000050 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000060 00 00 00 41 |...A|
00000064
com.apple.quarantine: 0081;5eacc2c7;Chrome;76BAB094-6D07-4515-8D54-83AA485EDB54
# Remove a xattr:
~/Code/sandboxie ❯ xattr -d com.apple.quarantine test.txt
~/Code/sandboxie ❯ xattr -r test.txt
test.txt: com.apple.metadata:kMDItemWhereFroms
# Clear all the xattrs:
~/Code/sandboxie ❯ xattr -c test.txt
~/Code/sandboxie ❯ xattr -r test.txt
~/Code/sandboxie ❯ # nothing
The two xattrs are enough to know I downloaded the file using Chrome
at the URL http://0.0.0.0:8000/test.txt
.
Command line applications can of course invoke xattr
to set the quarantine flag, but this process is prone to errors, the flag can change at any point, and it requires identifying and modifying every place in the code that creates files.
Applications can enable LSFileQuarantineEnabled
in their Info.plist
to automatically quarantine all the files they create. This option is documented on Apple’s Documentation:
A Boolean value indicating whether the files this app creates are quarantined by default.
If we add this option to an App, files we create will gain the quarantine flag, as expected - and we don’t have to use a specific API to create files.
import AVFoundation
let downloadsDirectoyy = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
let filename = downloadsDirectoyy.appendingPathComponent("quarantine-me.txt")
let content = "EICAR, anyone?"
try! content.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
Now, if we create a Command Line Tool, inject the Info.plist
file and use the same code … the quarantine flag will NOT be added to files we create. Good thing we double check and write tests, right? :-)
Enabling the App Sandbox entitlement fixes this, but comes with its own disadvantages, the biggest one being that our app now lives in a sandboxed Container and can’t interact freely with the file system - unless we use Open file popup, wich we don’t.
Why the difference?
After hours of Google/GitHub searches, trying numerous tools (and asking for help!), we learn a few things. Let’s use dtrace
and dtruce
and look for calls to sandbox or quarantine functions.
Using dtruss
, we trace our App bundle and identify calls to _LSSetProcessQuarantineProperties
and libquarantine.dylib
that would match what we’re looking for.
~/Code/sandboxie ❯ sudo dtruss -fas ./Quarantineo.app/Contents/MacOS/Quarantineo
[snip]
7480/0x460f4: 20492 26 23 __mac_syscall(0x7FFF6CBE1E11, 0x57, 0x7FFEECC402B0) = 0 0
libsystem_kernel.dylib`__mac_syscall+0xa
libquarantine.dylib`_qtn_proc_apply_to_self+0x155
LaunchServices`_LSSetProcessQuarantineProperties+0x1b6
LaunchServices`_LSRegisterSelf+0x1e3
LaunchServices`_LSApplicationCheckIn+0x1ee1
HIServices`_RegisterApplication+0x1794
HIServices`GetCurrentProcess+0x17
HIToolbox`MenuBarInstance::GetAggregateUIMode(unsigned int*, unsigned int*)+0x3f
HIToolbox`MenuBarInstance::IsVisible()+0x33
AppKit`_NSInitializeAppContext+0x23
AppKit`-[NSApplication init]+0x1b6
AppKit`+[NSApplication sharedApplication]+0x7b
AppKit`NSApplicationMain+0x16e
Quarantineo`main+0xd
libdyld.dylib`start+0x1
Quarantineo`0x1
[snip]
The stack trace starts with AppKit, the library used to create graphical interfaces in macOS apps. Our command line binary doesn’t have a GUI, so it doesn’t invoke AppKit - that would explain why we can’t use LSFileQuarantineEnabled
.
We also learn the quarantine is managed by __mac_syscall
, which is a syscall defined in the XNU Kernel to control the MAC Framework . Usermode applications invoke it using __sandbox_ms
.
We can list the dynamic libraries using these methods using grep:
~/Code/sandboxie ❯ grep -R __mac_syscall /usr/lib/system
Binary file /usr/lib/system/libsystem_kernel.dylib matches
~/Code/sandboxie ❯ grep -R __sandbox_ms /usr/lib/system
Binary file /usr/lib/system/libquarantine.dylib matches
Binary file /usr/lib/system/libsystem_secinit.dylib matches
Binary file /usr/lib/system/libsystem_kernel.dylib matches
Binary file /usr/lib/system/libsystem_sandbox.dylib matches
Let’s catch calls to __mac_syscall
using dtrace
on our command-line tool, to figure out the impact of enabling the App Sandbox. We also print the first argument passed to __mac_syscall
, the target.
# App Sandbox disabled
~/Code/sandboxie ❯ sudo dtrace -n 'pid$target:*:*__mac_syscall*:entry { printf("%s", copyinstr(arg0)); }' -W main-without-app-sandbox
Waiting for main-without-app-sandbox, hit Ctrl-C to stop waiting...
dtrace: pid 9508 has exited
CPU ID FUNCTION:NAME ARG0
5 369170 __mac_syscall:entry Sandbox
0 369170 __mac_syscall:entry Sandbox
0 369170 __mac_syscall:entry Sandbox
# App Sandbox enabled
~/Code/sandboxie ❯ sudo dtrace -n 'pid$target:*:*__mac_syscall*:entry { printf("%s", copyinstr(arg0)); }' -W main-with-app-sandbox
Waiting for main-with-app-sandbox, hit Ctrl-C to stop waiting...
dtrace: pid 9495 has exited
CPU ID FUNCTION:NAME ARG0
0 388356 __mac_syscall:entry Sandbox
6 388356 __mac_syscall:entry Sandbox
2 388356 __mac_syscall:entry Sandbox
2 388356 __mac_syscall:entry Quarantine <--- yay!
2 388356 __mac_syscall:entry Sandbox
2 388356 __mac_syscall:entry Sandbox
0 388356 __mac_syscall:entry Sandbox
0 388356 __mac_syscall:entry Sandbox
0 388356 __mac_syscall:entry Sandbox
When the App Sandbox is enabled, __mac_syscall:entry
is invoked more times and the fourth call defines the quarantine policy. Perfect!
Using lldb
, we can get the thread backtrace for this fourth call. Full log
~/Code/sandboxie ❯ lldb
(lldb) target create main-with-app-sandbox
Current executable set to '/Users/maximeg/Code/sandboxie/main-with-app-sandbox' (x86_64).
(lldb) br se -n __sandbox_ms
Breakpoint 1: where = libsystem_kernel.dylib`__mac_syscall, address = 0x00000000000022fc
(lldb) run
(lldb) thread continue
(lldb) thread continue
(lldb) thread continue
Resuming thread 0x6e20f in process 9667
Process 9667 resuming
Process 9667 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00007fff6ccf02fc libsystem_kernel.dylib`__mac_syscall
libsystem_kernel.dylib`__mac_syscall:
-> 0x7fff6ccf02fc <+0>: movl $0x200017d, %eax ; imm = 0x200017D
0x7fff6ccf0301 <+5>: movq %rcx, %r10
0x7fff6ccf0304 <+8>: syscall
0x7fff6ccf0306 <+10>: jae 0x7fff6ccf0310 ; <+20>
Target 0: (main-with-app-sandbox) stopped.
(lldb) register read
General Purpose Registers:
rax = 0x0000000000000000
rbx = 0x0000000000000012
rcx = 0x0000000000003e4c
rdx = 0x00007ffeefbfb660
rdi = 0x00007fff6cdbed42 "Quarantine" # <--- we're looking at the right call
rsi = 0x0000000000000057
rbp = 0x00007ffeefbfbe90
rsp = 0x00007ffeefbfb618
r8 = 0x0000000000000000
r9 = 0x0000000000000380
r10 = 0x00000000e3ffffff
r11 = 0x0000000000000008
r12 = 0x000000010080dce4
r13 = 0x0000000100209eb0
r14 = 0x00007ffeefbfb640
r15 = 0x000000010080dc5c
rip = 0x00007fff6ccf02fc libsystem_kernel.dylib`__mac_syscall
rflags = 0x0000000000000246
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
(lldb) thread backtrace -c 40 * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00007fff6ccf02fc libsystem_kernel.dylib`__mac_syscall
frame #1: 0x00007fff6cdbd66d libsystem_secinit.dylib`_libsecinit_appsandbox + 1253
frame #2: 0x00007fff6cdbd147 libsystem_secinit.dylib`_libsecinit_initializer + 35
frame #3: 0x00007fff69b947c1 libSystem.B.dylib`libSystem_initializer + 268
frame #4: 0x00000001000251e3 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
frame #5: 0x00000001000255ee dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #6: 0x000000010002000b dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 493
frame #7: 0x000000010001ff76 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #8: 0x000000010001ff76 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #9: 0x000000010001ff76 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #10: 0x000000010001ff76 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #11: 0x000000010001e014 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #12: 0x000000010001e0b4 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #13: 0x000000010000c5e6 dyld`dyld::initializeMainExecutable() + 199
frame #14: 0x0000000100011af8 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
frame #15: 0x000000010000b227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #16: 0x000000010000b025 dyld`_dyld_start + 37
(lldb)
The App Sandbox is configured by libsystem_secinit.dylib, which is consistent with the information we can find online.
A few resources were helpful in digging further, I recommend you check them if you’d like to learn more about the App Sandbox:
- Hack in the (sand)Box
- m6m wiki
- 0xbf00/libsecinit
- steven-michaud/SandboxMirror
- A Whirlwind Tour of the Apple Sandbox
We now know why GUI apps support LSFileQuarantineEnabled
but command-line apps don’t, but our original question is still unanswered: can we enable this feature on a binary without the App Sandbox?
Invoking libquarantine directly
The quarantine is managed by /usr/lib/system/libquarantine.dylib
, which provides a few methods that we can list using nm
:
~/Code/sandboxie ❯ nm -gU /usr/lib/system/libquarantine.dylib
... snip ...
0000000000001ab3 T __qtn_proc_set_flags
0000000000001cd7 T __qtn_proc_set_identifier
00000000000023ad T __qtn_proc_set_path_exclusion_pattern
... snip ...
These functions are used in at least two open sourced Apple projects ntpd and Webkit.
#include "quarantine.h"
[snip...]
qtn_proc_t qp = qtn_proc_alloc();
qtn_proc_set_identifier(qp, "org.ntp.ntpd");
qtn_proc_set_flags(qp, QTN_FLAG_SANDBOX | QTN_FLAG_HARD);
qtn_proc_apply_to_self(qp);
qtn_proc_free(qp);
quarantine.h
is part of the private Apple SDK, so we can’t include it directly - but we can craft our own header file using public knowledge; and let’s create a small proof-of-concept to verify we can use this library, and that it works as we expect it.
You can find all the files used in this PoC (and hopefully reproduce its results) in this Gist
// main.c
#include <stdio.h>
#include <stdlib.h>
#include "quarantine.h"
int main() {
/* Set Quarantine */
qtn_proc_t qp = qtn_proc_alloc();
qtn_proc_set_identifier(qp, "com.punkeel.ohno");
qtn_proc_set_flags(qp, QTN_FLAG_SANDBOX | QTN_FLAG_HARD);
qtn_proc_apply_to_self(qp);
qtn_proc_free(qp);
FILE *fp;
fp = fopen("test.txt", "w+");
fprintf(fp, "This is testing for fprintf...\n");
fputs("This is testing for fputs...\n", fp);
fclose(fp);
return 0;
}
~/Code/sandboxie/c-quarantine ❯ make
./main
ls -lah@ test.txt
-rw-r--r--@ 1 maximeg staff 60B May 2 02:14 test.txt
com.apple.quarantine 33B
xattr -l test.txt
com.apple.quarantine: 0083;5eacbb6f;com.punkeel.ohno;
Done! We have successfully enabled LSFileQuarantineEnabled
for our binary, without creating a full App bundle or restricting ourselves to what the App Sandbox allows!
As a heavy Homebrew user myself, I wish more CLI tools would benefit from the security features introduced by Apple. Some are easy to enable, others like this one may have some quirks and require use of deprecated or non-public headers - but over time I’m sure we’ll be able to figure it out!
That’s all, folks!