Banking / FinTech¶
Protect transaction-signing keys, certificate pins, and authentication flows against on-device analysis. The goal isn't perfect secrecy — that belongs server-side — it's raising attacker cost above the economic threshold for fraud-scale automation.
Threat model¶
| Asset | Adversary | Capability |
|---|---|---|
| HMAC / signing keys | Mobile malware, rooted-device fraud rings | Static extraction, memory dump |
| Certificate pin list | MITM proxies (mitmproxy, Charles, Burp) | Static string search, runtime patching |
| Anti-fraud heuristics | Reverse engineers building bypass kits | Decompile + write a generator that mimics legitimate traffic |
| Session token handling | Frida / Substrate scripts | Hook the request signer to log tokens |
See Security Model for what Kagura can and cannot do in this scenario. Spoiler: this is defense in depth — Kagura is one layer.
Policy file¶
{
"profile": "STRONG",
"passes": {
"str_aes": true,
"wstr": true,
"mvo": true,
"pe": true,
"co": true,
"fla": true,
"bcf": true,
"sub": true,
"bbcheck": true,
"tamper": true,
"anti_debug": true,
"ci": true,
"sv": true,
"honey": true
},
"tuning": {
"bcf_prob": 60,
"seed": 0
}
}
Why these choices:
str_aes+wstr— encrypt all string literals (SwiftString, Objective-C@"", Kotlin / Java string constants pulled into JNI) sostringsreturns nothing usefulmvo+pe— on-stack key buffers and pointers stay XOR-encrypted at every store/load, even after the function uses themco+sub— break MBA pattern matchers (de4dot, FLOSS) on inline constantsfla+bcfwithbcf_prob: 60— defeats Ghidra's CFG view in decompiler-resistant buildstamper— catches binary patching by hashing the loaded text sectionbbcheck— emits a per-basic-block checksum call site. It only detects anything once you supply your ownkagura_bb_check; the shipped one is a weak always-pass stub. See Anti-Analysis Passes.anti_debug— drops the obvious "Cheat Engine attached as debugger" attacksci— hides imports from IDA's Import Viewsv— strips non-public symbols so the dynamic symtab doesn't leak internal nameshoney— injects decoy globals (g_api_key_v2, etc.) and fake stubs
Build¶
# Per-release key rotation: a key extracted from v1 doesn't decrypt v2 strings
clang -fpass-plugin=KaguraObfuscator.dylib \
-mllvm -kagura-config=kagura-bank.json \
-mllvm -kagura-build-id=$(git rev-parse HEAD) \
-mllvm -kagura-symmap \
-mllvm -kagura-symmap-out=bank-symmap-$(git describe).json \
-O2 -c bank_core.c -o bank_core.o
Keep the bank-symmap-<release>.json files off-device — they let you
symbolicate crash reports without shipping debug symbols.
Source-side annotations¶
Mark the highest-value functions for VM virtualization:
#include "kagura/runtime.h"
// Sign a transaction request. Worth the 10–50× VM overhead — called once per
// payment, but a 100% guarantee that this code is never readable as
// native instructions.
__attribute__((annotate("kagura_vm")))
int sign_transaction(const uint8_t *payload, size_t len, uint8_t out[64]) {
// ... HMAC-SHA256 implementation
}
// Hot path — keep BALANCED-equivalent. Don't VM this.
__attribute__((annotate("kagura_nofla")))
int validate_amount(int64_t amount) { return amount > 0 && amount < 1e9; }
Runtime hardening¶
In your application's startup path:
#include "kagura/runtime.h"
int app_init(void) {
// 1. Refuse to run on a tampered binary.
// kagura_macho_tampered() is the predicate. Its response-shaped sibling
// kagura_self_check() returns void and calls the tamper hook itself,
// which is the opposite of what we want here: we want to soft-respond
// so the detection point isn't a tombstone an attacker can grep for in
// /var/mobile/Library/Logs.
if (kagura_macho_tampered()) {
return -1;
}
// 2. Refuse to run with Frida / Substrate loaded
if (kagura_suspicious_lib_loaded()) {
return -1;
}
// 3. Refuse to run on jailbroken / rooted devices for payment flows.
// (Use feature-gated UX; do not crash the app for non-payment flows.)
#if defined(__APPLE__)
if (kagura_jailbreak_detected()) {
#elif defined(__ANDROID__)
if (kagura_magisk_present() || kagura_xposed_present()) {
#else
if (0) {
#endif
// Disable payment UI; show a "not supported on this device" screen.
}
return 0;
}
Every name above comes from kagura/runtime.h. Do not hand-write extern
declarations for them: a misspelling then links against nothing (or, if you
mark it weak, silently evaluates to false) and the guard disappears without a
word. runtime/ios/device_attest.c carries a comment about the time that
happened to four checks at once.
Verification¶
Before shipping, run all of:
# 1. No plaintext API keys in the binary
strings YourApp.app/YourApp | grep -iE "api_key|hmac|secret"
# Should return nothing.
# 2. No readable signing logic
ghidra YourApp.app/YourApp # then run the Ghidra eval suite
cd tests/decompiler_eval && python3 run_ghidra_eval.py \
--binary YourApp.app/YourApp --ghidra /path/to/ghidra
# 3. Frida resistance
cd tests/frida_resistance
for s in probes/F*.js; do frida -l "$s" -f YourApp.app/YourApp; done
# 4. App Store review risk assessment
./scripts/cli/review-risk-assessment.sh YourApp.app/YourApp --platform ios
# 5. Confirm the release build actually obfuscated what you asked for
./scripts/cli/kagura-diff.py baseline.dylib release.dylib --html report.html
What's still on you¶
Kagura cannot:
- Protect a key that must survive a memory dump — put it in the platform keystore (Keychain / Android Keystore / SE / StrongBox)
- Make rooted / jailbroken devices safe for high-value transactions — combine with Apple DeviceCheck / App Attest or Android Play Integrity
- Replace server-side fraud detection — Kagura raises client-side cost; the server still needs to detect anomalous patterns
See Security Model for the full boundary.