Exploring the Low-Level Implementation of FishHook
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.
Exploring the Low-Level Implementation of FishHook
I knew Fishhook as Facebook’s hooking framework that works by rewriting the pointers in the lazy-binding (GOT) table, but I’d never actually read the code. So I stepped through it under a debugger to understand the mechanics.
1. How Fishhook works
Rewrite the target address that an externally-referenced function points to in the (lazy) binding table, replacing it with the address of your custom code.
Implementation-wise it’s two steps:
-
From the segments defined in the Mach-O’s Load Commands, compute the start addresses of three tables: the Dynamic Symbol Table (Indirect Symbols), the Symbol Table, and the String Table.
-
Walk every function definition in the Lazy Symbol Pointers, looking for the function to hook (by string-comparing function names — which is why the String Table is needed; see note 1 for the lookup). If it’s the function to modify, rewrite its pointer in the (lazy) binding table (the GOT) to point at your own implementation.
2. Fishhook, step 1
Compute the three table addresses:

Here’s the Mach-O Load Commands layout:

Reading the fishhook logic top-down:
// Find base symbol/string table addresses
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
First it computes the __LINKEDIT base, because LC_SYMTAB and LC_DYSYMTAB offsets are relative to the __LINKEDIT base. Then it uses that base to compute the three tables’ start addresses.
From the Load Command:
linkedit_segment->vmaddr = 0x100010000linkedit_segment->fileoff = 0x10000
In the debugger, image list -o -f shows the in-memory base: slide = 0x2cb0000.
The dynamic-debugging values:
2.1 — From the code: linkedit_base = (0x4510000 + 0x100010000 - 0x10000) = 4367384576. The in-memory value is also 4367384576:

2.2 — symtab_cmd->symoff = 67160. From the code: symtab = (4367384576+67160).toString(16) = 0x104520658. The debugger shows symtab as 0x104520658 too:

2.3 — symtab_cmd->stroff = 70632. From the code: strtab = (4367384576 + 70632).toString(16) = 0x1045213e8. The debugger shows strtab = "".
2.4 — dysymtab_cmd->indirectsymoff = 70424. From the code: indirectsymoff = (4367384576 + 70424) = 0x104521318:

That yields the start addresses of the Symbol Table, Indirect Symbols, and String Table. Setup done — now the actual GOT hooking.
3. Fishhook, step 2
After computing those three values, fishhook walks the headers, looking in the Load Commands for S_LAZY_SYMBOL_POINTERS and S_NON_LAZY_SYMBOL_POINTERS under __DATA — the lazy and non-lazy binding sections. These sections hold the real pointers used at call time, and the whole point is to rewrite the pointer of the function you want to hook.
Once found, it enters the key perform_rebinding_with_section function, passing in the three table addresses, the ASLR slide, the (lazy) binding table address, and the rebindings list of functions to hook. It then searches the Indirect Symbols for the target function (by name); on a match, it rewrites the corresponding pointer in the (lazy) binding table.
Here’s the perform_rebinding_with_section implementation:

First it computes indirect_symbol_bindings — the in-memory start of the (lazy) symbol table: offset + (lazy) binding table addr. In the debugger: indirect_symbol_bindings = (0x4510000 + 4295000064) = 0x104518000, which is the (lazy) binding table’s in-memory base:

Then it loops over the (lazy) binding table — equivalent to looping the Indirect Symbols, since both tables index the same functions (see note 2). For each function in the (lazy) binding table, it reads the Indirect Symbols data value (uint32_t symtab_index = indirect_symbol_indices[i];), uses that to find the function’s String Table offset in the Symbol Table (uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;), and finally reads the function name from the String Table (char *symbol_name = strtab + strtab_offset;).
Next it loops the rebindings list; if a name matches the current function (strcmp() == 0), it rewrites that function’s GOT pointer to the replacement address (indirect_symbol_bindings[i] = cur->rebindings[j].replacement;):

Let’s step through one loop iteration, say i = 0: symtab_index = 181, indirect_symbol_indices[i] = 181. The code computes uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; — here strtab_offset = symtab[181].n_un.n_strx (see note 3 for the Symbol Table entry layout):
![strtab_offset for symtab[181]](/_astro/08_889798_FYX7WME2N8RRQBG.C9iiTVd8_Z1FtaHw.webp)
The figure shows strtab_offset = 0x22E, i.e. 558. So String Table base + 558 is this function’s name.
4. The GOT before and after fishhook
From the analysis above, replacing the GOT entry is exactly the line indirect_symbol_bindings[i] = cur->rebindings[j].replacement;. Break before and after it to see what changes for a hooked function — here, hooking NSLog.
The calling code:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"lalala111");
startHook();
char *s = "lalala";
NSLog(@"len : %ld",strlen(s));
}
void startHook() {
rebind_symbols((struct rebinding[1]){{"NSLog",myNSLog,NULL}},1);
}
The address NSLog points to in the GOT is ASLR + (offset in the lazy binding table): 0x1003f8000 + 0xc000 = 0x100404000. 0x100404000 holds NSLog’s function pointer; *(0x100404000) holds NSLog’s code:

The value at 0x100404000 is 0x01a97ff7f0; disassembling that address shows NSLog’s implementation:
disassemble --start-address 0x01a97ff7f0

Then, executing indirect_symbol_bindings[i] = cur->rebindings[j].replacement; rewrites the GOT pointer. Verify:

Now 0x100404000 holds 0x01003fd5ec, and *(0x01003fd5ec) is the replaced NSLog — my custom myNSLog:

Note 1
The logic for finding a function’s name by walking the (lazy) binding table (dynamically debugged in section 3): walk the lazy table, get the function’s index in the Indirect Symbol table, use that index to find the function’s entry in the Symbol Table, which stores the function’s offset into the String Table — and that offset gives the name:

Note 2
Walking the lazy symbol table and the Indirect Symbol table uses the same index, because both tables index the same functions. Verify: the Lazy Symbol Pointers:

and the Indirect Symbols:

Both list the same order: NSLog, NSStringFromClass, …
Note 3
The Symbol Table entry layout — n_strx is the function’s offset into the String Table:

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