December 18, 2024 by Jacob Latonis4 minutes
Detecting things in Mach-O binaries used to be quite an effort in the original YARA; it would involve magic byte validation, guessing offsets, counting occurrences, and a lot more.
With the advent of YARA-X, the new and improved Mach-O module can be leveraged in various ways. This blog post will detail particular use-cases and show examples for those and more.
If you’re interested in seeing a more in-depth look at some of the features mentioned below, you can watch the talk linked below, given by Greg Lesnewich and myself, which breaks down the motivations behind the Mach-O module in YARA-X and features examples of how it can be utilized. If you prefer a written summary, keep reading.
macho
moduleTo begin using the macho
module inside a YARA rule, add the following at the
top of your rule like so:
import "macho"
A Mach-O binary can feature a lot of different information and structures. To
see all of what YARA-X and the macho
module can potentially parse, you can
see everything documented in the macho
documentation
for YARA-X.
This section will cover commonly used structures for detections in YARA rules.
If you want to detect around something in the symbol table, we can leverage
the macho.symtab
structure, where each string is located in entries
.
import "macho"
rule swift_bin {
condition:
for any symbol in macho.symtab.entries: (
symbol == "_swift_getObjCClassMetadata"
)
}
The macho
module has multiple ways to explore imports in a Mach-O binary.
To iterate over the imports defined via load commands in the Mach-O binary, one
can use the array of imports located at macho.imports
:
import "macho"
rule macho_imports {
condition:
for any i in macho.imports: (
i contains "_harmony_"
)
}
Use has_import
to detect whether an import is present in a given binary.
import "macho"
rule macho_imports {
condition:
macho.has_import("_NSEventTrackingRunLoopMode")
}
Exported symbols from the Mach-O binary are parsed in YARA-X and can be used queried against and enumerated.
To iterate over the exports found in a Mach-O binary, the macho
module
contains the list of exports as a string found at macho.exports
.
import "macho"
rule macho_exports {
condition:
for any e in macho.exports: (
e contains "execute_header"
)
}
To check if a Mach-O binary contains a specific export, one can leverage the
has_export()
function like so:
import "macho"
rule macho_exports_query {
condition:
macho.has_export("suspicious_export_identifier")
}
Remote paths are used to tell the Mach-O binary where it can search for the libraries it depends on. These load commands are parsed via YARA-X and can be leveraged in YARA rules.
To iterate through the rpaths
used in load commands in the Mach-O binary, one
can leverage the rpaths
array from the macho
module:
import "macho"
rule rpath_iter {
condition:
for any rpath in macho.rpaths: (
rpath contains "lib/swift/macosx"
)
}
To detect if a specific rpath
is present in the load commands, one can use
the has_rpath()
function:
import "macho"
rule rpath_query {
condition:
macho.has_rpath("@loader_path/../Frameworks")
}
Dylibs are shared libraries leveraged by the Mach-O binary. To iterate through the dylibs loaded in the Mach-O binary, one can iterate through the dylib structures parsed by YARA-X:
import "macho"
rule library_dylib_location {
condition:
for any d in macho.dylibs: (
d.name contains "/Library/"
)
}
To detect if a certain dylib is loaded via a load command in the binary, the
has_dylib()
function is available for use.
import "macho"
rule libsystem_use {
condition:
macho.has_dylib("/usr/lib/libSystem.B.dylib")
}
Mach-O binaries can feature entitlements, which are XML properties for
requesting
certain permissions from the user/device that it is being executed on. These are
parsed out into strings which are able to be queried via the macho
module.
These entitlements can be leveraged in multiple ways.
One can iterate over the entitlements like so:
import "macho"
rule entitlements_example {
condition:
for any e in macho.entitlements: (
e contains "com.apple.security"
)
}
To detect a specific entitlement, one can also use the has_entitlement()
function.
import "macho"
rule entitlements_example {
condition:
macho.has_entitlement("com.apple.security.device.microphone")
}
If you wish to detect if a Mach-O binary is using a certain set of dylibs,
imports, exports, entitlements, or more, you can leverage the respective
_hash()
function for each structure.
The hash algorithm is an MD5 hash of the deduplicated, sorted, and lowercased
entries joined via a comma (md5("dylib_1,dylib_2,dylib_n")
) found in the
binary for each category.
import "macho"
rule dylib_hash_example {
condition:
macho.dylib_hash() == "c92070ad210458d5b3e8f048b1578e6d"
}
import "macho"
rule import_hash_example {
condition:
macho.import_hash() == "35ea3b116d319851d93e26f7392e876e"
}
import "macho"
rule export_hash_example {
condition:
macho.export_hash() == "6bfc6e935c71039e6e6abf097830dceb"
}
import "macho"
rule entitlement_hash_example {
condition:
macho.entitlement_hash() == "cc9486efb0ce73ba411715273658da80"
}
There are many features and structures parsed with the macho
module in YARA-X,
and only a subset of them were covered in this blog post. To fully explore what
is possible with the macho
module, please consult the documentation.