Hikari Source Analysis — AntiHook
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Hikari Source Analysis — AntiHook
The third — and likely final — installment of the Hikari source-analysis series, which has covered AntiClassDump, AntiDebug, and now AntiHook, all focused on security hardening. Source: https://github.com/61bcdefg/Hikari-LLVM15-Core/blob/main/AntiHooking.cpp
Implementation principle
The pass offers three main capabilities: inline-hook detection, runtime protection, and defense against symbol-rebinding attacks:
-
Detect and disrupt potential inline hooks. By analyzing instruction sequences in the target program, the pass identifies possible hook code — code an attacker may have planted to control program behavior. Once it detects such an instruction pattern, it inserts additional checks; on an illegal operation it diverts to an error-handling flow, protecting the program from further tampering.
-
Protect the Objective-C runtime. On Objective-C environments like iOS and macOS, method implementations can be replaced or hooked at runtime — a common dynamic-modification technique. The AntiHook pass inserts specific checks to ensure Objective-C methods haven’t been illegally replaced or modified when they execute.
-
Counter dynamic-library symbol rebinding. Rebinding symbols in dynamic libraries (e.g., via fishhook) is a common way attackers hijack normal calls. The pass analyzes external function calls and rewrites them to point at safe stubs or alternative implementations, blocking this attack class.
Code analysis
The source is relatively small — about three hundred lines — so I’ll walk through it directly.
0. config
First, the opcode signatures for the ARM A64 instruction set — the binary encodings of specific instructions. These come from the instruction-encoding section of the ARM architecture reference; every instruction in the ARM A64 A-profile has a specific bit pattern.
// Arm A64 Instruction Set for A-profile architecture 2022-12, Page 56
#define AARCH64_SIGNATURE_B 0b000101
// Arm A64 Instruction Set for A-profile architecture 2022-12, Page 75
#define AARCH64_SIGNATURE_BR 0b1101011000011111000000
// Arm A64 Instruction Set for A-profile architecture 2022-12, Page 79
#define AARCH64_SIGNATURE_BRK 0b11010100001
AARCH64_SIGNATURE_Bis the opcode signature of theBinstruction (unconditional branch). It’s0b000101in the A64 reference (0bprefix = binary).AARCH64_SIGNATURE_BRis the opcode of theBRinstruction (branch to register), which jumps to an address held in a register. Its binary pattern is0b1101011000011111000000.AARCH64_SIGNATURE_BRKis the opcode of theBRKinstruction (breakpoint/interrupt), which raises a synchronous exception, usually for debugging.0b11010100001is its unique identifier.
These opcodes define the basic instruction types; compilers/assemblers use the signatures to construct correct binary instructions. Here they’re used to recognize specific binary instruction patterns — mainly for inline-hook detection later.
The config options are self-explanatory from their descriptions:
static cl::opt<std::string>
PreCompiledIRPath("adhexrirpath",
cl::desc("External Path Pointing To Pre-compiled Anti "
"Hooking Handler IR"),
cl::value_desc("filename"), cl::init(""));
static cl::opt<bool> CheckInlineHook("ah_inline", cl::init(true), cl::NotHidden,
cl::desc("Check Inline Hook for AArch64"));
static bool CheckInlineHookTemp = true;
static cl::opt<bool>
CheckObjectiveCRuntimeHook("ah_objcruntime", cl::init(true), cl::NotHidden,
cl::desc("Check Objective-C Runtime Hook"));
static bool CheckObjectiveCRuntimeHookTemp = true;
static cl::opt<bool> AntiRebindSymbol("ah_antirebind", cl::init(false),
cl::NotHidden,
cl::desc("Make fishhook unavailable"));
static bool AntiRebindSymbolTemp = false;
1. initialize
1. Get target architecture info. Reads the module’s Triple (architecture, OS, etc.).
this->triple = Triple(M.getTargetTriple());
2. Build a default precompiled-IR path. If PreCompiledIRPath isn’t set, it builds a default path under the user’s home directory, embedding the architecture and OS names.
if (PreCompiledIRPath.empty()) {
SmallString<32> Path;
if (sys::path::home_directory(Path)) {
sys::path::append(Path, "Hikari");
sys::path::append(Path, "PrecompiledAntiHooking-" +
Triple::getArchTypeName(triple.getArch()) + "-" +
Triple::getOSTypeName(triple.getOS()) + ".bc");
PreCompiledIRPath = Path.str();
}
}
3. Check/parse/link the precompiled IR. Uses std::ifstream to check the file exists and is readable, then parses it into a Module via parseIRFile and links it with Linker::linkModules.
std::ifstream f(PreCompiledIRPath);
if (f.good()) {
errs() << "Linking PreCompiled AntiHooking IR From:" << PreCompiledIRPath
<< "\n";
SMDiagnostic SMD;
std::unique_ptr<Module> ADBM(
parseIRFile(StringRef(PreCompiledIRPath), SMD, M.getContext()));
Linker::linkModules(M, std::move(ADBM), Linker::Flags::OverrideFromSrc);
} else {
errs() << "Failed To Link PreCompiled AntiHooking IR From:"
<< PreCompiledIRPath << "\n";
}
4. Determine opaque-pointer support.
this->opaquepointers = !M.getContext().supportsTypedPointers();
5. Insert Objective-C function declarations. If the target is Apple-provided and struct._objc_method is defined in the context, it inserts declarations for objc_getClass, sel_registerName, and method_getImplementation — needed for the Objective-C runtime hook checks.
if (triple.getVendor() == Triple::VendorType::Apple &&
StructType::getTypeByName(M.getContext(), "struct._objc_method")) {
Type *Int8PtrTy = Type::getInt8PtrTy(M.getContext());
M.getOrInsertFunction("objc_getClass",
FunctionType::get(Int8PtrTy, {Int8PtrTy}, false));
M.getOrInsertFunction("sel_registerName",
FunctionType::get(Int8PtrTy, {Int8PtrTy}, false));
FunctionType *IMPType =
FunctionType::get(Int8PtrTy, {Int8PtrTy, Int8PtrTy}, true);
PointerType *IMPPointerType = PointerType::getUnqual(IMPType);
M.getOrInsertFunction(
"method_getImplementation",
FunctionType::get(IMPPointerType,
{PointerType::getUnqual(StructType::getTypeByName(
M.getContext(), "struct._objc_method"))},
false));
// ...
The initializer sets up the hook-checking mechanism, ensures the precompiled IR file is found and linked, and inserts any missing ObjC runtime function declarations for later safety checks.
2. runOnModule
Flow. runOnModule checks and handles inline hooks, prevents symbol rebinding (fishhook-style attacks), and handles Objective-C runtime hooks. Its rough flow:
- Iterate all functions in the module.
- For each, check whether it should undergo “antihook” — based on compile-time flags and function attributes.
- If
initializedisfalse, callinitialize. - Read
F’sah_inlineattribute to decide whether to check inline hooks; fall back to the globalCheckInlineHookif unset. - If AArch64 and
CheckInlineHookTempis true, runHandleInlineHookAArch64. - Read
F’sah_antirebindattribute; fall back toAntiRebindSymbol. - If
AntiRebindSymbolTempis true, iterateF’s instructions; for eachCall/Invokewhose target is an external-linkage declaration, create a new global and rewrite the call target. - Read
F’sah_objcruntimeattribute; fall back toCheckObjectiveCRuntimeHook. - If
CheckObjectiveCRuntimeHookTempis false, skip this function. - Iterate
F’s users to find an Objective-C method struct (struct._objc_method); if found, follow its users to locate instance/class method-list globals. - If
methodListGVandmethodStructare found, get the selector and class names, then callHandleObjcRuntimeHook. - Return
true.
Key code.
// Iterate all functions in the module
for (Function &F : M) {
// Check whether antihook should apply to this function
if (toObfuscate(flag, &F, "antihook")) {
errs() << "Running AntiHooking On " << F.getName() << "\n";
// Initialize if not already
if (!this->initialized)
initialize(M);
// Decide whether to check inline hooks from the function attribute
if (!toObfuscateBoolOption(&F, "ah_inline", &CheckInlineHookTemp))
CheckInlineHookTemp = CheckInlineHook;
// AArch64 + inline-hook check → handle
if (triple.isAArch64() && CheckInlineHookTemp) {
HandleInlineHookAArch64(&F);
}
// Decide whether anti-rebind is active
if (!toObfuscateBoolOption(&F, "ah_antirebind", &AntiRebindSymbolTemp))
AntiRebindSymbolTemp = AntiRebindSymbol;
// Anti-rebind symbol check
if (AntiRebindSymbolTemp) {
for (Instruction &I : instructions(F)) {
if (isa<CallInst>(&I) || isa<InvokeInst>(&I)) {
CallSite CS(&I);
Function *Called = CS.getCalledFunction();
if (!Called)
Called = dyn_cast<Function>(CS.getCalledValue()->stripPointerCasts());
// External-linkage function declaration
if (Called && Called->isDeclaration() && Called->isExternalLinkage(Called->getLinkage()) && !Called->isIntrinsic() && !Called->getName().startswith("clang.")) {
// Create a new global
GlobalVariable *GV = cast<GlobalVariable>(M.getOrInsertGlobal(("AntiRebindSymbol_" + Called->getName()).str(), Called->getType()));
if (!GV->hasInitializer()) {
GV->setConstant(true);
GV->setInitializer(Called);
GV->setLinkage(GlobalValue::LinkageTypes::PrivateLinkage);
}
appendToCompilerUsed(M, {GV});
Value *Load = new LoadInst(GV->getValueType(), GV, Called->getName(), &I);
Value *BitCasted = BitCastInst::CreateBitOrPointerCast(Load, CS.getCalledValue()->getType(), "", &I);
CS.setCalledFunction(BitCasted);
}
}
}
}
// Decide whether to check ObjC runtime hooks
if (!toObfuscateBoolOption(&F, "ah_objcruntime", &CheckObjectiveCRuntimeHookTemp))
CheckObjectiveCRuntimeHookTemp = CheckObjectiveCRuntimeHook;
// Skip if ObjC runtime hook check is off
if (!CheckObjectiveCRuntimeHookTemp)
continue;
// Locate the ObjC method struct and its class/instance methods
GlobalVariable *methodListGV = nullptr;
ConstantStruct *methodStruct = nullptr;
// ... elided: iterating the function's users and their users ...
// Found an ObjC method list and method struct
if (methodListGV && methodStruct) {
// Get the selector and class names
GlobalVariable *SELNameGV = cast<GlobalVariable>(methodStruct->getOperand(0)->stripPointerCasts());
ConstantDataSequential *SELNameCDS = cast<ConstantDataSequential>(SELNameGV->getInitializer());
bool classmethod = methodListGV->getName().startswith("_OBJC_$_CLASS_METHODS");
std::string classname = methodListGV->getName().substr(strlen(classmethod ? "_OBJC_$_CLASS_METHODS_" : "_OBJC_$_INSTANCE_METHODS_")).str();
std::string selname = SELNameCDS->getAsCString().str();
// Handle the ObjC runtime hook
HandleObjcRuntimeHook(&F, classname, selname, classmethod);
}
}
}
return true;
A closer look at AntiRebindSymbol: it prevents symbols from being rebound during dynamic linking, defeating frameworks like fishhook. Key steps:
- Find external-linkage function calls. Iterate all instructions looking for
CallInst/InvokeInst. - Check whether it’s an external declaration. External functions aren’t defined in this module and are resolved at runtime via dynamic linking.
- Create and initialize a global. For each such function, create a new global initialized to point at it, with private linkage so it’s invisible outside the module.
- Replace the original call. Load the function address from the new global via a
LoadInst, correct the type with aBitCastInst, and substitute it as the call target.
Effectively it works by creating, at compile time, a private global pointing at the original function. Since it’s private and initialized only at compile time, a runtime attacker can’t easily modify the address — preventing hook-by-symbol-address attacks.
3. HandleInlineHookAArch64
Detects and handles inline hooks on AArch64.
1. Get and split the entry block. The entry block A is split at the first non-PHI/non-debug/non-lifetime instruction, creating block C (which holds most of the original entry block’s instructions).
BasicBlock *A = &(F->getEntryBlock()); // Entry block A
BasicBlock *C = A->splitBasicBlock(A->getFirstNonPHIOrDbgOrLifetime()); // Split A at the first non-PHI/Dbg/Lifetime instruction → block C
2. Create the handler and detection blocks. B is the handler run when a hook is detected; Detect and Detect2 hold the detection logic.
BasicBlock *B = BasicBlock::Create(F->getContext(), "HookDetectedHandler", F); // Handler block B
BasicBlock *Detect = BasicBlock::Create(F->getContext(), "", F); // First detection block
BasicBlock *Detect2 = BasicBlock::Create(F->getContext(), "", F); // Second detection block
3. Rewire the entry block. Delete A’s terminator and branch to Detect.
A->getTerminator()->eraseFromParent(); // Remove A's original terminator
BranchInst::Create(Detect, A); // Add a branch to Detect at the end of A
4. First-stage detection. In Detect, check whether F’s first instruction matches AARCH64_SIGNATURE_B or AARCH64_SIGNATURE_BRK. If so, jump to handler B; otherwise continue to Detect2.
IRBuilder<> IRBDetect(Detect); // IRBuilder for Detect
// ... build and configure the detection IR ...
Value *Load = IRBDetect.CreateLoad(Int32Ty, IRBDetect.CreateBitCast(F, Int32PtrTy));
Value *LS2 = IRBDetect.CreateLShr(Load, ConstantInt::get(Int32Ty, 26));
Value *ICmpEQ2 = IRBDetect.CreateICmpEQ(
LS2, ConstantInt::get(Int32Ty, AARCH64_SIGNATURE_B));
Value *LS3 = IRBDetect.CreateLShr(Load, ConstantInt::get(Int32Ty, 21));
Value *ICmpEQ3 = IRBDetect.CreateICmpEQ(
LS3, ConstantInt::get(Int32Ty, AARCH64_SIGNATURE_BRK));
Value *Or = IRBDetect.CreateOr(ICmpEQ2, ICmpEQ3);
// Branch to B (hook handler) or Detect2 (further detection)
IRBDetect.CreateCondBr(Or, B, Detect2);
5. Second-stage detection. In Detect2, check whether the instruction(s) following the detection logic match AARCH64_SIGNATURE_BR. If so, jump to B; otherwise continue to C.
IRBuilder<> IRBDetect2(Detect2); // IRBuilder for Detect2
// ... build further detection IR ...
Value *PTI = IRBDetect2.CreatePtrToInt(F, Int64Ty);
Value *AddFour = IRBDetect2.CreateAdd(PTI, ConstantInt::get(Int64Ty, 4));
Value *ITP = IRBDetect2.CreateIntToPtr(AddFour, Int32PtrTy);
Value *Load2 = IRBDetect2.CreateLoad(Int32Ty, ITP);
Value *LS4 = IRBDetect2.CreateLShr(Load2, ConstantInt::get(Int32Ty, 10));
Value *ICmpEQ4 = IRBDetect2.CreateICmpEQ(
LS4, ConstantInt::get(Int32Ty, AARCH64_SIGNATURE_BR));
Value *AddEight = IRBDetect2.CreateAdd(PTI, ConstantInt::get(Int64Ty, 8));
Value *ITP2 = IRBDetect2.CreateIntToPtr(AddEight, Int32PtrTy);
Value *Load3 = IRBDetect2.CreateLoad(Int32Ty, ITP2);
Value *LS5 = IRBDetect2.CreateLShr(Load3, ConstantInt::get(Int32Ty, 10));
Value *ICmpEQ5 = IRBDetect2.CreateICmpEQ(
LS5, ConstantInt::get(Int32Ty, AARCH64_SIGNATURE_BR));
Value *Or2 = IRBDetect2.CreateOr(ICmpEQ4, ICmpEQ5);
// Branch to B (hook handler) or C (normal continuation)
IRBDetect2.CreateCondBr(Or2, B, C);
6. Handler logic. In B, call CreateCallbackAndJumpBack (analyzed below).
IRBuilder<> IRBB(B); // IRBuilder for B
// ... add the handler callback and the jump-back ...
CreateCallbackAndJumpBack(&IRBB, C); // Invoke the callback on hook detection, then jump back to C
4. HandleObjcRuntimeHook
Detects whether an Objective-C method’s implementation (IMP) has been hooked, by comparing the runtime-resolved implementation against the expected one.
void HandleObjcRuntimeHook(Function *ObjcMethodImp, std::string classname,
std::string selname, bool classmethod)
Parameters:
ObjcMethodImp: function pointer to the Objective-C method implementation;classname: the class name string;selname: the selector name string;classmethod: whether the method is a class method.
1. Get the module and split the entry block. Creates three blocks: A (runtime hook detection), B (handler), and C (original continuation).
Module *M = ObjcMethodImp->getParent();
BasicBlock *A = &(ObjcMethodImp->getEntryBlock());
BasicBlock *C = A->splitBasicBlock(A->getFirstNonPHIOrDbgOrLifetime());
BasicBlock *B = BasicBlock::Create(A->getContext(), "HookDetectedHandler", ObjcMethodImp, C);
A->getTerminator()->eraseFromParent(); // Remove A's terminator
2. Build instructions in A and B.
IRBuilder<> IRBA(A);
IRBuilder<> IRBB(B);
3. Decide whether the ObjC method is hooked:
-
Get the class object and selector:
Value *GetClass = IRBA.CreateCall(M->getFunction("objc_getClass"), {IRBA.CreateGlobalStringPtr(classname)}); Value *GetSelector = IRBA.CreateCall(M->getFunction("sel_registerName"), {IRBA.CreateGlobalStringPtr(selname)}); -
Get the method object depending on class/instance method:
Value *GetMethod = IRBA.CreateCall(M->getFunction(classmethod ? "class_getClassMethod" : "class_getInstanceMethod"), {GetClass, GetSelector}); -
Get the implementation:
Value *GetMethodImp = IRBA.CreateCall(M->getFunction("method_getImplementation"), {GetMethod}); -
Compare the resolved implementation against the original:
Value *IcmpEq = IRBA.CreateICmpEQ(IRBA.CreateBitCast(GetMethodImp, Int8PtrTy), ConstantExpr::getBitCast(ObjcMethodImp, Int8PtrTy));
4. Branch on the comparison.
IRBA.CreateCondBr(IcmpEq, C, B); // Equal → C, otherwise → B
5. On hook detection, invoke the callback and jump back to C.
CreateCallbackAndJumpBack(&IRBB, C);
In short, it fetches the runtime method implementation via the Objective-C runtime and compares it to the current implementation pointer. A mismatch means the method is likely hooked, diverting control to handler block B, which calls CreateCallbackAndJumpBack to run the callback and jump back to C. If they match, execution continues directly at C.
5. CreateCallbackAndJumpBack
Runs exception-handling/cleanup on detection, then ensures execution continues. Depending on the environment it applies different strategies, but always jumps back to C afterward.
// CreateCallbackAndJumpBack: on illegal operation (e.g., inline hook), invoke a
// callback and jump back to the normal execution path.
void CreateCallbackAndJumpBack(IRBuilder<> *IRBB, BasicBlock *C) {
// Get the module of the current block
Module *M = C->getModule();
// Try to get "AHCallBack" — a possibly predefined security-check callback
Function *AHCallBack = M->getFunction("AHCallBack");
// If found, emit a call to it
if (AHCallBack) {
IRBB->CreateCall(AHCallBack);
} else {
// Otherwise, on Darwin AArch64 (iOS/macOS)...
if (triple.isOSDarwin() && triple.isAArch64()) {
// ...build an inline-asm exit syscall
std::string exitsvcasm = "mov w16, #1\n"; // Syscall number 1 (exit)
exitsvcasm +=
"svc #" + std::to_string(cryptoutils->get_range(65536)) + "\n"; // Random svc immediate
// Create the inline-asm call
InlineAsm *IA =
InlineAsm::get(FunctionType::get(IRBB->getVoidTy(), false),
exitsvcasm, "", true, false);
IRBB->CreateCall(IA);
} else {
// Otherwise, call abort()
FunctionType *ABFT =
FunctionType::get(Type::getVoidTy(M->getContext()), false);
Function *abort_declare =
cast<Function>(M->getOrInsertFunction("abort", ABFT).getCallee());
abort_declare->addFnAttr(Attribute::AttrKind::NoReturn); // Mark abort as noreturn
IRBB->CreateCall(abort_declare);
}
}
// Finally, jump back to C regardless of whether a callback ran
IRBB->CreateBr(C);
}
Summary
This pass is a carefully designed hardening strategy — highly selective and configurable. Through architecture adaptation and dedicated defenses against hooking and rebinding attacks, it strengthens the code’s resistance. It also accounts for the Objective-C runtime, recognizing and handling class/instance method hooking. Its configurability makes it adaptable to many compilation scenarios while still meeting specific project/developer needs. With some recent downtime, this Hikari source-analysis series wraps up here; if other passes raise questions, feel free to reach out.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch