May 2, 2020

Enabling LSFileQuarantineEnabled on cli binaries

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 [email protected] to the ls command:

~/Code/sandboxie ❯ ls [email protected]
-rw-r--r--   1 maximeg  staff   127B May  2 01:13 safe
[email protected]  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:

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 [email protected] test.txt
[email protected] 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!

Copyright © 2020 vulnerable.af