Executable code protection and obfuscation in MacOS

If you, like me, want to distribute a binary executable program for macOS and introduce at least a minimal level of protection—such as hiding text and literals in the binary or obfuscating the code—you may notice that, as of now (end of 2024), few tools are available for this purpose. Unlike Windows, which has numerous EXE protection software options, macOS developers often find themselves at a disadvantage due to the lack of such tools.

This information may be helpful if you are developing in C or C++ and compiling your program using GCC or Clang.

I found “Hikari”, a custom Clang toolchain: https://github.com/HikariObfuscator/Hikari

Although its page says that it is already deprecated as of 2024, it is still usable and has at least a few actively maintained forks.

Hikari is a modified version of LLVM that incorporates several advanced obfuscation techniques to protect software from reverse engineering. Obfuscation in Hikari essentially transforms the code so that it is functionally the same but is much harder for a reverse engineer to analyze and understand. Here’s an overview of the main concepts and techniques involved in Hikari obfuscation:

1. Purpose of Obfuscation

Obfuscation aims to protect intellectual property, software algorithms, and proprietary logic from being extracted or understood through reverse engineering. By making the code harder to read or follow, obfuscation raises the difficulty of tasks like decompiling, debugging, or reconstructing code logic. It can increase the time, resources, and expertise needed to analyze a binary, deterring attackers or making reverse engineering impractical.

2. Techniques Used in Hikari Obfuscation

Hikari employs a variety of techniques, which can be enabled individually or as a package. Here are some of the core methods:

A. Control Flow Flattening

  • Concept: Control flow flattening reorders and reorganizes the control flow in a way that hides the logical structure of the code.
  • Implementation: Hikari uses a “dispatcher loop” that handles all branching within a function. Instead of straightforward conditional branches, a central dispatcher routes the program flow based on values, making it challenging to reconstruct the original branching structure.
  • Effect: This approach confuses tools that attempt to follow the program’s execution path, making it much harder to understand the logic and purpose of functions.

B. Bogus Control Flow Insertion

  • Concept: Bogus control flow adds fake or meaningless code paths that appear as potential execution paths but are never used.
  • Implementation: Hikari inserts fake branches and conditions that look valid but do not affect the actual program outcome. These branches are interwoven with the real ones.
  • Effect: By adding these paths, Hikari increases the complexity of the program’s control flow, making static analysis and decompilation much harder. The extraneous paths mislead reverse engineers, wasting their time on non-functional code paths.

C. Function Call Obfuscation

  • Concept: Function call obfuscation hides or complicates direct calls to functions, making it harder to trace function calls and understand inter-function relationships.
  • Implementation: Hikari can replace direct function calls with indirect calls or use lookup tables, where a function is called by referencing its address through an intermediary step.
  • Effect: This obfuscation disrupts typical static analysis methods that rely on direct calls, making it harder to map out function usage and data flow.

D. String Obfuscation

  • Concept: String obfuscation encrypts or encodes strings so that they are unreadable in the binary.
  • Implementation: Hikari encodes string literals and only decodes them at runtime, often through an indirect function or algorithm. This approach may include embedding the string in a different data format or using XOR encryption.
  • Effect: With strings encrypted in the binary, a reverse engineer can no longer see readable text, which is typically helpful for understanding software functionality, messages, or commands.

E. Instruction Substitution and Split

  • Concept: Instruction substitution replaces certain instructions with equivalent but more complex sequences, while instruction splitting divides basic operations into smaller, scattered chunks.
  • Implementation: Hikari replaces common instructions with less common ones or sequences of instructions that accomplish the same task but look different in disassembly. Splitting moves parts of a single operation across multiple code sections.
  • Effect: By transforming simple instructions into complex equivalents and splitting them, it is harder for an analyst to determine the program’s exact operations at any given point.

With some dancing around, I could compile and use it under Mac OS 14.1 Sonoma. It compiles and works well with Inter and ARM targets.