kagura — Xcode Integration Guide¶
This document explains how to integrate the kagura LLVM obfuscator into an Xcode project using the provided xcconfig and shell scripts.
Table of Contents¶
- Prerequisites
- Quick Start
- xcconfig Reference
- Manual xcconfig Snippet
- Per-File Selective Obfuscation
- Build Phase Script
- Swift and Objective-C Compatibility
- Bitcode
- Debug vs Release
- Code Signing
- Performance Tuning
- Troubleshooting
Prerequisites¶
| Requirement | Minimum | Recommended |
|---|---|---|
| LLVM / clang | 17.0 | 22.0 |
| Xcode | 14.0 | 16.0+ |
| macOS SDK | 13.0 | 15.0+ |
| CMake | 3.20 | latest |
Build kagura first. The Xcode integration references the compiled plugin at
build/lib/Transforms/KaguraObfuscator.dylib. From the kagura repository root:
For a non-Homebrew LLVM installation, specify the LLVM CMake directory explicitly:
cmake -B build \
-DLLVM_DIR=/path/to/llvm/lib/cmake/llvm \
-DCMAKE_C_COMPILER=/path/to/llvm/bin/clang \
-DCMAKE_CXX_COMPILER=/path/to/llvm/bin/clang++ \
.
cmake --build build -- -j$(sysctl -n hw.logicalcpu)
Verify the build succeeded:
Quick Start¶
-
Add
kagura.xcconfigto your Xcode project. Dragintegration/xcode/kagura.xcconfiginto the Project Navigator. Xcode will prompt you to add it; choose your app target. -
Assign the xcconfig to the Release configuration. Select the project in the Project Navigator → Info tab → Configurations → Release → expand your target → set "Based on Configuration File" to
kagura. -
Set
KAGURA_PLUGIN_PATH. In Build Settings, add a User-Defined Setting:
| Key | Value |
|---|---|
KAGURA_PLUGIN_PATH |
$(SRCROOT)/../kagura/build/lib/Transforms/KaguraObfuscator.dylib |
Adjust the relative path to match the location of the kagura checkout
relative to your .xcodeproj file.
-
Add the validation build phase (optional but recommended). See Build Phase Script.
-
Archive or build for Release. kagura flags are active only when the KAGURA_* toggles are set to
1in the xcconfig.
xcconfig Reference¶
The following settings are available in kagura.xcconfig.
Plugin Path¶
Override this in a machine-local .xcconfig that you do not commit to source
control, or via a User-Defined build setting in the target.
Feature Toggles¶
| Setting | Default | Pass |
|---|---|---|
KAGURA_ENABLE_STR |
1 |
StringEncryption — XOR-encrypts string literals |
KAGURA_ENABLE_STR_AES |
0 |
StringEncryptionAES — AES-128-CTR string encryption (requires runtime) |
KAGURA_ENABLE_WSTR |
1 |
WideStringEncryption — encrypts wide strings and CFString buffers |
KAGURA_ENABLE_FLA |
1 |
ControlFlowFlattening — switch-based state machine |
KAGURA_ENABLE_BCF |
1 |
BogusControlFlow — MBA opaque predicates |
KAGURA_ENABLE_SUB |
1 |
Substitution — MBA arithmetic replacements |
KAGURA_ENABLE_CO |
0 |
ConstantObfuscation — replaces integer constants |
KAGURA_ENABLE_GENC |
0 |
GlobalEncryption — XOR-encrypts private integer globals |
KAGURA_ENABLE_MVO |
0 |
MemoryValueObfuscation — XOR-encrypts alloca'd integer locals |
KAGURA_ENABLE_OBJC |
1 |
ObjCObfuscation — selector/class name obfuscation |
KAGURA_ENABLE_HONEY |
0 |
HoneyValue — decoy globals and fake security-stub functions |
KAGURA_ENABLE_ANTIDEB |
1 |
AntiDebug — ptrace/Frida/port-27042 checks |
KAGURA_ENABLE_TAMPER |
1 |
AntiTamper — FNV-1a integrity + jailbreak detection |
KAGURA_ENABLE_METRICS |
0 |
ObfuscationMetrics — stderr diagnostic output |
KAGURA_ENABLE_SYMMAP |
0 |
SymbolMap — emit JSON symbol map for crash symbolication |
KAGURA_DWARF |
keep |
DWARF handling: keep / strip / obfuscate |
Tuning Parameters¶
| Setting | Default | Description |
|---|---|---|
KAGURA_BCF_PROB |
30 |
Probability (0–100) of inserting bogus CF per BB |
KAGURA_BCF_ITER |
1 |
Number of BCF iterations |
KAGURA_SUB_ITER |
1 |
Number of substitution iterations |
KAGURA_SEED |
(unset) | PRNG seed — set for reproducible obfuscation |
Manual xcconfig Snippet¶
If you prefer not to use the provided kagura.xcconfig file you can paste the
following into your own xcconfig. Replace $(KAGURA_PLUGIN_PATH) with the
literal path to KaguraObfuscator.dylib if the variable expansion does not
work in your environment.
// ── kagura obfuscation flags ──────────────────────────────────────────────
// Load the pass plugin
// -fpass-plugin tells clang to load kagura into the New Pass Manager.
KAGURA_PLUGIN_FLAG = -fpass-plugin=$(KAGURA_PLUGIN_PATH)
// String encryption: XOR-encrypts all string literals at compile time.
// Strings are decrypted lazily on first access via a constructor stub.
KAGURA_STR_FLAG = -mllvm -kagura-str
// Control-flow flattening: replaces the CFG with a switch-based state machine.
// Best combined with -O1 or higher so dead code is eliminated first.
KAGURA_FLA_FLAG = -mllvm -kagura-fla
// Bogus control flow: injects dead basic blocks guarded by opaque predicates.
// Increase -kagura-bcf-prob to add more bogus branches (costs binary size).
KAGURA_BCF_FLAG = -mllvm -kagura-bcf -mllvm -kagura-bcf-prob=30 -mllvm -kagura-bcf-iter=1
// Instruction substitution: replaces arithmetic/bitwise ops with MBA equivalents.
KAGURA_SUB_FLAG = -mllvm -kagura-sub -mllvm -kagura-sub-iter=1
// Constant obfuscation: replaces integer constants with MBA expressions.
// Disabled by default — significantly increases binary size.
// KAGURA_CO_FLAG = -mllvm -kagura-co
// ObjC obfuscation: rewrites selector and class name metadata in LLVM IR.
// Does not affect Swift-only code; safe to enable for mixed targets.
KAGURA_OBJC_FLAG = -mllvm -kagura-objc
// Anti-debug / Anti-Frida: injects ptrace(PT_DENY_ATTACH), port-27042 probe,
// and /proc/maps detection. Requires kagura_runtime to be linked.
KAGURA_ANTIDEB_FLAG = -mllvm -kagura-anti-debug
// Assemble all active flags
OTHER_CFLAGS = $(inherited) $(KAGURA_PLUGIN_FLAG) $(KAGURA_STR_FLAG) $(KAGURA_FLA_FLAG) $(KAGURA_BCF_FLAG) $(KAGURA_SUB_FLAG) $(KAGURA_OBJC_FLAG) $(KAGURA_ANTIDEB_FLAG)
OTHER_CPLUSPLUSFLAGS = $(inherited) $(KAGURA_PLUGIN_FLAG) $(KAGURA_STR_FLAG) $(KAGURA_FLA_FLAG) $(KAGURA_BCF_FLAG) $(KAGURA_SUB_FLAG) $(KAGURA_OBJC_FLAG) $(KAGURA_ANTIDEB_FLAG)
Per-File Selective Obfuscation¶
Some files (parsers, hot loops, generated code) may not benefit from or may be
harmed by obfuscation. Xcode lets you override OTHER_CFLAGS on a per-file
basis in the "Compile Sources" build phase.
Disabling obfuscation for a single file¶
- Select the target → Build Phases → Compile Sources.
- Find the source file, click the compiler flags column.
- Enter the flags you want to remove, prefixed with
-Xclang -remove-option— however, Xcode does not support removing inherited flags at the file level. The practical approach is to set a replacement flag set that omits kagura:
Because -fno-pass-plugin is not a real flag, a simpler pattern is to compile
sensitive files without the plugin by assigning an explicit empty override:
The recommended approach for large projects is to keep a secondary xcconfig that enables only lightweight passes and assign it to files via a custom build rule.
Enabling extra passes for a single file¶
To apply the VM pass only to a specific .c or .m file, add these per-file
flags in Compile Sources:
Note: -kagura-vm requires libkagura_runtime.a to be linked into the binary.
Using source annotations (preferred)¶
For finer granularity, annotate individual functions in source code. kagura
respects __attribute__((annotate("kagura.*"))) markers:
// Apply VM obfuscation to this function only
__attribute__((annotate("kagura.vm")))
int verify_license(const char *key) { ... }
// Exclude this function from all kagura passes
__attribute__((annotate("kagura.skip")))
void hot_render_loop(void) { ... }
Supported annotation strings:
| Annotation | Effect |
|---|---|
kagura.vm |
Force VMObfuscation on this function |
kagura.fla |
Force ControlFlowFlattening on this function |
kagura.bcf |
Force BogusControlFlow on this function |
kagura.skip |
Exclude function from all kagura passes |
Build Phase Script¶
Add kagura-build-phase.sh as a Run Script phase before the Compile Sources
phase. This script validates the plugin exists and prints the active
configuration to the build log.
- Target → Build Phases →
+→ New Run Script Phase. - Drag the new phase above "Compile Sources".
- Set Shell to
/bin/bash. - Paste the script body:
Or, if you prefer to embed the full path:
- Uncheck "Based on dependency analysis" so the script runs on every build.
What the script does:
- Resolves
KAGURA_PLUGIN_PATH(falls back to the relative default). - Exits with code
0(non-fatal) if the plugin is missing, printing a warning. This allows Debug builds (which typically skip the xcconfig) to succeed without the plugin being present. - Prints a formatted summary of all active kagura settings to the build log for auditing purposes.
Swift and Objective-C Compatibility¶
Objective-C and Objective-C++¶
kagura is fully compatible with Objective-C and Objective-C++. The
-kagura-objc pass additionally rewrites selector strings and class names
embedded in LLVM IR metadata, providing protection beyond what the C/C++ passes
offer.
Flags are passed via OTHER_CFLAGS / OTHER_CPLUSPLUSFLAGS which apply to
.m / .mm files automatically.
Swift¶
Swift source is compiled by swiftc, not clang, so OTHER_CFLAGS does not
apply. The OTHER_SWIFT_FLAGS setting in kagura.xcconfig forwards the
-fpass-plugin flag to swiftc; however:
-mllvmflags are not forwarded to Swift's LLVM backend in the same way.-kagura-objcdoes not apply to Swift-generated LLVM IR.- String literal protection via
-kagura-strhas limited effectiveness on Swift strings because Swift stores many strings in the Swift runtime, not as C@llvm.globalobjectconstants.
For Swift targets the recommended approach is: 1. Implement security-sensitive logic in Objective-C or C. 2. Call that logic from Swift via a bridging header. 3. Apply the full kagura flag set to the C/Objective-C translation units.
Bitcode¶
kagura requires bitcode to be disabled.
Bitcode (ENABLE_BITCODE = YES) causes Xcode to embed LLVM bitcode in the
binary and re-optimize it server-side (App Store re-compilation). This
re-compilation step does not load the kagura plugin and would strip or undo
the obfuscation.
Ensure your xcconfig or build settings include:
As of Xcode 14, bitcode submission is deprecated and disabled by default for
new projects. If you are upgrading an older project, verify this setting is
explicitly set to NO in the Release configuration.
Debug vs Release¶
It is strongly recommended to enable kagura passes only in Release builds.
- Obfuscation significantly increases compile time (typically 2–5× depending on the passes and iteration counts selected).
- Obfuscated binaries are harder to debug with LLDB because variable names, inline frames, and source locations may be obscured.
- The
kagura.xcconfigfile is intended to be assigned only to the Release configuration (see Quick Start).
To ensure kagura is inactive in Debug builds, confirm the xcconfig is not assigned to the Debug configuration:
Project → Info → Configurations → Debug → <your target> → None
Project → Info → Configurations → Release → <your target> → kagura
For CI pipelines that run tests against Release builds, consider using
KAGURA_ENABLE_FLA=0 KAGURA_ENABLE_BCF=0 to disable the most
compile-time-expensive passes while keeping string encryption active.
Code Signing¶
kagura does not affect Mach-O load commands or code signature structures directly, but the passes do modify function bodies, which changes the code hash that codesign measures.
- Always sign after compilation (the default Xcode behaviour).
- Do not attempt to sign a binary, apply kagura post-link, then re-sign manually — use the standard Xcode archive workflow instead.
- If you use a custom post-link script that strips or modifies the binary, ensure it runs before the codesign step.
ENABLE_HARDENED_RUNTIME = YESis compatible with kagura. The AntiDebug pass injectsptrace(PT_DENY_ATTACH, 0, 0, 0)which is distinct from the Hardened Runtime debugger restriction and they do not conflict.
Performance Tuning¶
The table below shows recommended flag combinations for three common scenarios.
Alternatively, use the JSON config DSL (-mllvm -kagura-config=kagura.json) with a built-in profile:
Available profiles: FAST, BALANCED, STRONG.
For manual tuning, the table below shows recommended flag combinations:
| Pass flag | FAST | BALANCED | STRONG |
|---|---|---|---|
-kagura-str |
YES | YES | YES |
-kagura-wstr |
NO | YES | YES |
-kagura-fla |
NO | YES | YES |
-kagura-bcf |
NO | YES | YES |
-kagura-bcf-prob |
— | 30 | 60 |
-kagura-bcf-iter |
— | 1 | 2 |
-kagura-sub |
NO | YES | YES |
-kagura-sub-iter |
— | 1 | 2 |
-kagura-co |
NO | NO | YES |
-kagura-genc |
NO | YES | YES |
-kagura-mvo |
NO | YES | YES |
-kagura-objc |
YES | YES | YES |
-kagura-anti-debug |
NO | YES | YES |
-kagura-tamper |
NO | YES | YES |
-kagura-honey |
NO | NO | YES |
-kagura-vm |
NO | NO | selective |
-kagura-ibr |
NO | NO | YES |
-kagura-lt |
NO | YES | YES |
| Estimated compile overhead | ~1.1× | ~2×–3× | ~5×–10× |
Notes:
- "selective" for -kagura-vm means applying it only to the highest-value
functions via __attribute__((annotate("kagura.vm"))).
- VM obfuscation produces the largest binary size increase. Virtualizing an
entire app is not recommended.
- Setting -kagura-seed=<N> with a fixed value makes builds reproducible,
which is useful for CI caching and binary diffing.
Troubleshooting¶
error: unknown argument '-kagura-fla'¶
Cause: clang is not picking up the kagura plugin. The -mllvm flags
are only valid after -fpass-plugin loads the plugin that registers them.
Fix: Confirm KAGURA_PLUGIN_PATH points to the correct .dylib and that
the file exists. Run the build phase script manually:
The output should start with -fpass-plugin=....
error: unknown command line argument '-kagura-fla' (wrong LLVM path)¶
Cause: The system clang (Apple LLVM) is being used instead of the LLVM
clang that understands the New Pass Manager plugin interface. Apple's clang
ships with a different LLVM version and does not support third-party pass
plugins loaded via -fpass-plugin.
Fix: Set CC and CXX to the Homebrew (or custom) LLVM clang in your
build settings:
On Intel Macs the Homebrew prefix is /usr/local rather than /opt/homebrew.
fatal error: pass plugin not loaded: KaguraObfuscator.dylib¶
Cause: The dynamic library cannot be found or loaded. This is typically a missing dependency (LLVM dylibs) or a quarantine flag from macOS Gatekeeper.
Fix 1 — Remove Gatekeeper quarantine:
Fix 2 — Verify LLVM dylib linkage:
All @rpath entries must resolve. If they do not, set DYLD_LIBRARY_PATH
to include the LLVM lib directory, or rebuild kagura with
-DCMAKE_BUILD_RPATH=/opt/homebrew/opt/llvm/lib.
Crash in obfuscated code at runtime¶
Cause: Non-deterministic obfuscation (default KAGURA_SEED=0) can, in rare
cases involving complex CFG structures, produce code that crashes due to an
ordering bug in a pass.
Fix 1 — Set a fixed seed to reproduce the crash:
Once the crash is reproducible with a fixed seed, bisect which pass is responsible by disabling passes one at a time.
Fix 2 — Exclude the crashing function:
Add __attribute__((annotate("kagura.skip"))) to the function that crashes and
file a bug report with the function's LLVM IR (-emit-llvm -S).
Fix 3 — Reduce iteration counts:
Higher iteration counts increase the probability of exposing edge cases.
Build is extremely slow¶
Cause: -kagura-fla and -kagura-bcf are O(n) in the number of basic
blocks and can be quadratic for very large functions (>1000 BB).
Fix: Annotate hot / large functions with kagura.skip, or reduce
KAGURA_BCF_PROB to 10–15. See the Performance Tuning
table for a "Fastest build" configuration that still provides meaningful
protection.