Runtime Library¶
Some passes emit calls to symbols they do not define. Those targets require
linking libkagura_runtime.a (built at build/runtime/libkagura_runtime.a):
| Pass | Flag | Symbols it calls but does not define |
|---|---|---|
| StringEncryptionAES | -kagura-str-aes |
kagura_aes128_ctr_decrypt, kagura_check_blob_integrity |
| VMObfuscation | -kagura-vm |
kagura_vm_execute |
| AntiDebug | -kagura-anti-debug |
kagura_check_tracer_pid, kagura_check_inline_hooks, kagura_check_got_hooks, kagura_check_sw_breakpoints, kagura_check_hw_breakpoints, kagura_check_emulator |
| AntiTamper | -kagura-tamper |
kagura_self_check, kagura_tamper_detected, kagura_runtime_hash_check |
| BasicBlockChecksum | -kagura-bbcheck |
kagura_bb_check, kagura_on_tamper_detected |
| Telemetry | -kagura-telemetry |
kagura_telemetry_event |
| CallIndirection | -kagura-ci |
POSIX: kagura_rtld_default_handle + dlsym; Windows: kagura_windows_resolve_symbol |
| PointerAuth | -kagura-pac |
kagura_random_u64 |
| JNIObfuscation | -kagura-jni |
kagura_jni_get_env, kagura_jni_find_class, kagura_jni_register_native |
| ObjCObfuscation | -kagura-objc |
kagura_objc_register_remap |
Two names that look like they belong in that table do not, because the pass
defines them rather than importing them: kagura_anti_debug_init (the
module constructor AntiDebug builds) and kagura_on_tamper_detected (AntiDebug
emits a weak default that calls abort(); override it to choose your own
response). Every other pass — the string, control-flow and data passes —
generates self-contained code and needs no runtime at all.
Directly callable API¶
include/kagura/runtime.h is the public subset of the runtime: the detectors
you can call yourself, the hooks you can override, and the protected value
types.
#include "kagura/runtime.h"
if (kagura_suspicious_lib_loaded()) { /* Frida gadget, Substrate, … */ }
if (kagura_check_tracer_pid()) { /* ptrace / debugger attached */ }
if (kagura_check_sw_breakpoints()) { /* int3 / BRK patched into code */ }
if (kagura_jailbreak_detected()) { /* Apple targets */ }
if (kagura_magisk_present()) { /* Android targets */ }
Use these from your main() (mobile apps) or your DLL DllMain (Windows) to
get the same defense without going through pass-injected init code — useful in
projects where you want explicit control over when checks fire.
Predicates versus responses¶
Two shapes exist and they are not interchangeable:
| Shape | Returns | Behaviour |
|---|---|---|
int kagura_check_*(void) |
nonzero == detected | Reports. Does nothing else. |
void kagura_*_check(void) |
nothing | Runs the predicates and invokes the tamper hook on a hit. |
kagura_self_check(), kagura_check_hooks() and kagura_check_breakpoints()
are in the second group despite their names, so if (kagura_self_check() != 0)
does not compile against this header. It used to appear to work when callers
hand-wrote their own extern int declaration — and then branched on a garbage
register. runtime/ios/device_attest.c documents the same failure hitting four
symbols at once, which is the reason this header exists.
Always #include "kagura/runtime.h" instead of declaring these by hand.
Platform attestation API¶
Thin C bindings for the major platform attestation services. The C side generates nonces and runs fast local pre-screens; the async signed-token round-trip is wired up from your Swift / Kotlin code.
Apple — DeviceCheck / App Attest (runtime/ios/device_attest.c)¶
int kagura_devicecheck_available(void); // iOS 11+, macOS 10.15+
int kagura_appattest_available(void); // iOS 14+, A10+ hardware
int kagura_appattest_nonce(uint8_t *out, size_t len);
int kagura_appattest_local_check(void); // fast (<5ms) env screen
Swift bridge example:
import DeviceCheck
let service = DCAppAttestService.shared
if service.isSupported && kagura_appattest_local_check() == 1 {
var nonce = Data(count: 32)
_ = nonce.withUnsafeMutableBytes { kagura_appattest_nonce($0.baseAddress, 32) }
service.generateKey { keyId, err in /* server-side verification */ }
}
Android — Play Integrity (runtime/android/play_integrity.c)¶
void kagura_play_integrity_nonce(char *out_hex32, size_t len);
int kagura_play_integrity_verdict_ok(const char *jwt_payload_b64url);
int kagura_play_integrity_local_check(void);
The full JWT signature must be verified server-side — verdict_ok is a
local fast-path, not a security boundary. See the file header comment
for the Kotlin caller skeleton.
Windows — ETW analysis-tool detection (runtime/windows/etw_detection.c)¶
int kagura_etw_provider_present(const wchar_t *provider_guid);
int kagura_etw_analysis_tool_check(void); // checks Cheat Engine / Procmon / etc.
This module ships as a stub by default. Build with -DKAGURA_ETW_FULL=1
and link tdh.lib to enable the real TdhEnumerateProviders-based
enumeration — see the file's header comment for the implementation outline.
Source layout¶
runtime/
├── core/ AES, VM interpreter, crash symbolication, device key
├── anti_debug/ Cross-platform POSIX anti-debug / anti-Frida
├── android/ Root detection, attestation, /proc, syscall probes (Android + Linux)
├── ios/ Jailbreak detection, Mach-O integrity (iOS + macOS)
├── windows/ IsDebuggerPresent, NtQueryInformationProcess, PE integrity
└── game/ Anti-cheat, IL2CPP protection, telemetry
Windows pass helpers¶
AntiDebug's six entry points use Windows debugger APIs, API-entry hook and breakpoint probes, normal IAT validation, current-thread debug registers on x86/x64, and known emulator indicators. They are bounded heuristics, not an exhaustive detector. IAT checking skips delay imports and bound imports without an original name table. Hardware breakpoint checking returns no detection on unsupported architectures.
CallIndirection resolves dllimport functions from already loaded modules with
GetProcAddress; it does not load DLLs. Ordinary external declarations,
including static CRT and other translation-unit definitions, remain direct
calls because their names need not be exported. Libraries must remain loaded
while cached pointers are used. Missing symbols invoke the tamper-response hook.
Windows inline-hook probes recognize x64 relative/indirect entry jumps; software
breakpoint probes cover x86/x64 INT3 and ARM64 BRK at selected API entries.
The original import operand retains the DLL dependency and identifies its exporting module, preventing collisions between DLLs with the same symbol name. Ordinal-only exports and import-library aliases retain the loader address if a named lookup is unavailable. The PE import table remains visible.