Earlier this week I was messing with an old detection analytic designed to fire an alert when a suspicious applet was launched. In the case of macOS post-exploitation looking for adhoc signed binaries named "applet" can provide a decent tip-off. The platform I was using reports the code signing properties of executables in just a binary state: "it is" or "it is not". However, there were some inconsistent detections across test variations. Accurately representing the code signing properties has historically been difficult for security vendors to get "right" in many cases. Whether that be consistency of reporting or the lack of detail (in this case).
As it turns the sensor I was using reported adhoc signed binaries as truly "signed". While there's no inherent harm in doing this... it does deny defenders additional resolution and increase their time spent when hunting through event timelines. Instead when presented only with binary reporting options vendors should err on the side of caution and report adhoc binaries as unsigned. The remainder of this blog will focus on details of the tests and wrap up with a glimpse into the innerworkings of osacompile
.
The job of this analytic is to detect the execution of unsigned / adhoc signed applets. This detector's composition is based on process and code signing artifacts. Here we're looking for the execution of an application bundle: .app
with the Mach-O name of "applet". Applet is the default name of the thin Mach-O wrapper created when saving OSA code as an app bundle either through ”Script Editor.app” or /usr/bin/osascript
for example. Additionally, this has artifact has also been commonly seen in the XCSSET malware family.
Execution of the malicious applet
XProtect malware detection event
events:
- NEW_PROCESS
- EXISTING_PROCESS
op: and
rules:
- case sensitive: false
op: contains
path: event/FILE_PATH
value: app/Contents/MacOS/applet
- op: is
path: event/FILE_IS_SIGNED
value: 0
The primary artifact to point out here that caused confusion is event/FILE_IS_SIGNED
which reports: 0 if the file is completely unsigned and 1 if it’s anything adhoc or above.
Variation | CS flags | Detects? |
---|---|---|
/usr/bin/osacompile |
0x2 (adhoc) |
🔥NO |
"Script Editor.app"(don't sign) | Not signed at all. | ✅ YES |
"Script Editor.app"(locally sign) | 0x2 (adhoc) |
🔥 NO |
OSAKit API | N/A (depends on implementation) | N/A |
Using /usr/bin/codesign -d -v <FILE_PATH>
we can grab the signature of each file. Notice how the adhoc signed executables cause the sensor the report event/FILE_IS_SIGNED
. Unfortunately in the case of macOS many would not consider this intuitive.
What do you think? Should adhoc be considered "signed" when you can only report a true / false representation?
I would honestly expect this not to be the case. When a solution only has two states: signed or unsigned and there are many variations in-between we should err on the side of caution --especially in a security context. For a Mach-O to be truly "signed" (at least against the Catalina model) -- in my opinion we should see a X.509 certificate chain indicating the trust of a CA (see the following Firefox example — notice the certificate chain).
Additionally, there are a few major classes of code signatures on macOS (from an analysis perspective): Unsigned, Adhoc, Developer ID. App Store, and Platform. Each of these is helpful for identification of where the binary came from.
❯ codesign -d -vvv /Applications/Firefox.app
Executable=/Applications/Firefox.app/Contents/MacOS/firefox
Identifier=org.mozilla.firefox
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=863 flags=0x10000(runtime) hashes=18+5 location=embedded
...
CDHash=37271ba939e6813bc2a82cdb85e2c42c2c80ac6d
Authority=Developer ID Application: Mozilla Corporation (43AQ936H96)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Jun 19, 2023 at 3:01:26 AM
Notarization Ticket=stapled
TeamIdentifier=43AQ936H96