Using the Mach-O module in YARA-X

December 18, 2024 by Jacob Latonis4 minutes

Introduction

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.

Usage

Importing the macho module

To begin using the macho module inside a YARA rule, add the following at the top of your rule like so:

import "macho"

Myriad structures inside a Mach-O binary

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.

Symbol Table

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"
        )
}

Imports

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")
}

Exports

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

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

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")
}

Entitlements

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")
}

Binary Similarity

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.

Dylib Hashing

import "macho"

rule dylib_hash_example {
    condition:
        macho.dylib_hash() == "c92070ad210458d5b3e8f048b1578e6d"
}

Import Hashing

import "macho"

rule import_hash_example {
    condition:
        macho.import_hash() == "35ea3b116d319851d93e26f7392e876e"
}

Export Hashing

import "macho"

rule export_hash_example {
    condition:
        macho.export_hash() == "6bfc6e935c71039e6e6abf097830dceb"
}

Entitlement Hashing

import "macho"

rule entitlement_hash_example {
  condition:
    macho.entitlement_hash() == "cc9486efb0ce73ba411715273658da80"
}

Conclusion

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.