elf-got-patcher

April 27, 2026 · View on GitHub

elf-got-patcher

Static ELF Patcher for ARM64 GOT Hook Injection

License: GPL v3 Platform Language

English | 中文文档


What is elf-got-patcher?

elf-got-patcher is an open-source, config-driven static binary patcher for ARM64 (AArch64) ELF executables. It injects a pure-C shellcode payload into an existing code cave, hijacks .init_array via RELA addend modification, and installs a GOT hook — all without modifying the original code or data segments.

The entire hook logic is written in pure C (no hand-assembled hex), compiled with Android NDK clang, and extracted as a flat binary. A sentinel-based configuration pool allows the same shellcode to be reused across different target binaries by simply changing a JSON config file.

Use Cases

  • Security Research — Analyze network protocols of obfuscated binaries by intercepting sendto / send calls
  • CTF / Reverse Engineering — Practice ELF patching, GOT hooking, and OLLVM deobfuscation techniques
  • Protocol Analysis — Replace identifiers (e.g., App IDs) in outbound traffic to redirect API calls
  • Binary Instrumentation Education — Learn how .init_array hijacking, GOT hooking, and code caves work in practice
  • Anti-cheat / License Research — Study how software protection mechanisms can be bypassed (for authorized testing only)

Understanding GOT Hooking

What is the GOT?

The Global Offset Table (GOT) is a data structure used by ELF binaries to resolve addresses of external (dynamically linked) functions at runtime. When a program calls an external function, it doesn't jump directly to libc — instead it reads the real address from the GOT and branches there.

Normal call flow:
  code → GOT[target_func] → libc target_func()

After GOT hook:
  code → GOT[target_func] → hook_func() → scan/replace buffer → libc target_func()

Key insight: The GOT resides in a rw- (read-write) memory segment, which means we can modify function pointers at runtime to hijack any external function call.

Choosing a Hook Target

This tool can hook any GOT-visible external function. Choose the target based on your reverse engineering analysis:

Target FunctionUse Casebuf arglen arg
sendto(fd, buf, len, ...)UDP networkingarg1arg2
send(fd, buf, len, flags)TCP networkingarg1arg2
write(fd, buf, len)General I/Oarg1arg2
SSL_write(ssl, buf, len)TLS encrypted trafficarg1arg2
custom_func(ctx, buf, ...)Custom protocol functionsvariesvaries

By hooking these functions, you can:

  1. Inspect outbound payloads before they leave the process
  2. Modify specific byte patterns (e.g., replace an App ID, version string) in-flight
  3. Log traffic for protocol reverse engineering
  4. Redirect requests by altering parameters

Default: hook.c ships with a sendto hook. To hook a different function, search for [CUSTOMIZE] comments in the source. Functions sharing the same (fd, buf, len, ...) layout (like write, send) only require changing the GOT address in the JSON config — no code changes needed.

How It Works (Diagram)

┌──────────────────────────────────────────────────────────┐
│  ELF Binary (before patch)                               │
│                                                          │
│  .init_array[N] ──RELA──▶ original_init()               │
│  GOT[target]    ────────▶ libc target_func (obfuscated)  │
│  code cave      ────────▶ 0x00 0x00 0x00 ... (zeros)    │
└──────────────────────────────────────────────────────────┘

             ▼  patcher.exe + config.json  ▼

┌──────────────────────────────────────────────────────────┐
│  ELF Binary (after patch)                                │
│                                                          │
│  .init_array[N] ──RELA──▶ init_wrapper() ◄── code cave  │
│                           │                              │
│                           ├─ call original_init()        │
│                           ├─ save real func addr to BSS  │
│                           └─ GOT[target] = hook_func     │
│                                                          │
│  hook_func():                                            │
│    ├─ scan buffer for needle                             │
│    ├─ replace with target string                         │
│    └─ tail-call real target_func                         │
└──────────────────────────────────────────────────────────┘

GOT Obfuscation (OLLVM)

Some binaries (especially those protected by OLLVM) don't store the real function address in the GOT. Instead, they store an obfuscated value:

GOT[target] = real_addr + CONSTANT    (obfuscated)
real_addr   = GOT[target] - CONSTANT  (de-obfuscate)

elf-got-patcher handles this via the got_addend config field. The shellcode:

  1. Reads the obfuscated GOT value
  2. Subtracts the constant to get the real function address
  3. Saves the real address for later use
  4. Writes hook_func + CONSTANT back to the GOT (preserving the obfuscation scheme)

How It Works

┌─────────────┐    ┌──────────────┐    ┌─────────────────┐
│  config.json │───▶│  patcher.exe │───▶│  patched ELF    │
│  (addresses) │    │  (host x64)  │    │  (target ARM64) │
└─────────────┘    └──────┬───────┘    └─────────────────┘

                   ┌──────┴───────┐
                   │   hook.bin   │
                   │ (ARM64 flat) │
                   └──────────────┘
  1. Shellcode (hook.c) is compiled to a flat ARM64 binary (hook.bin) using NDK clang with -mcmodel=tiny (pure PC-relative, no ADRP page alignment issues)
  2. Patcher (main.c) reads a JSON config, locates sentinel markers (0xCAFEBABE...) in the shellcode, fills in runtime addresses, verifies the code cave, writes the payload, and patches the RELA addend
  3. At runtime: .init_array calls init_wrapper → saves real function address → installs hook in GOT → every call to the target function flows through the hook for string replacement

Project Structure

elf-got-patcher/
├── shellcode/
│   ├── hook.c              Pure C shellcode (init_wrapper + hook_sendto + scan_and_replace)
│   ├── hook.ld             Linker script (controls section layout)
│   └── Makefile            NDK clang build → hook.bin
├── patcher/
│   └── main.c              ELF parser + sentinel patcher + cave writer + RELA modifier
├── configs/
│   └── mg_35029_to_51642.json   Example config (with // comments)
├── build.bat               One-click build script (Windows)
├── LICENSE                  GPL-3.0
└── README.md

Quick Start

Prerequisites

  • Android NDK r21+ (for shellcode compilation: aarch64-linux-android clang)
  • Any C99 compiler (for patcher: MSVC cl.exe, MinGW gcc, or Linux gcc/clang)

Build

# Set NDK path
export ANDROID_NDK="/path/to/ndk"   # Linux/macOS
$env:ANDROID_NDK = "C:\path\to\ndk" # Windows PowerShell

# Build everything
./build.bat                # Windows
# or: make -C shellcode && gcc -O2 -o patcher patcher/main.c  # Linux

Run

./patcher.exe configs/mg_35029_to_51642.json

Output:

[+] payload shellcode/hook.bin : 504 bytes
[+] g_config patched at payload off 0x180 (self_va=0x7d4c0)
[+] cave is 504 bytes of zero, fits.
[+] RELA addend verified = 0xef320
=========================================
PATCH OK : input -> output
  payload @ file 0x7d340 (VA 0x7d340) 504 bytes
  RELA addend @ 0x1ae8 : 0xef320 -> 0x7d340
=========================================

Deploy to Device

adb push output_elf /data/local/tmp/
adb shell "su -c 'cp /data/local/tmp/output_elf /target/path; chmod 755 /target/path'"

Adapting to a New Target

Create a new JSON config. Use readelf and IDA/Ghidra to extract:

FieldDescriptionHow to Find
cave_va / cave_offCode cave addressreadelf -l → find zero-filled region in r-x PT_LOAD
rela_addend_offRELA addend file offsetreadelf -rR_AARCH64_RELATIVE entry in .init_array
orig_initOriginal init function VAThe RELA addend's original value
got_sendtoGOT entry VA for target functionIDA → trace call wrapper to LDR from GOT
got_addendGOT obfuscation constantIDA → MOVZ+MOVK×3 in wrapper (0 if no obfuscation)
saved_sendtoWritable BSS slot VAreadelf -l → end of rw- PT_LOAD BSS region
needle / replaceSearch / replace stringsApplication-specific (must be equal length)

Hooking a Different Function

The default hook targets sendto, but you can hook any GOT-visible function by editing hook.c. Look for [CUSTOMIZE] comments:

  1. Change the typedef & function signature to match your target (e.g., write, send, or a custom function)
  2. Adjust which arguments are the buffer pointer and length in the scan_and_replace call
  3. Set got_sendto in the JSON config to your target function's GOT entry VA

For functions with the same (fd, buf, len, ...) layout (like write, send), no code change is needed — just update the GOT address in the config.

Design Highlights

  1. Pure C shellcode — readable, modifiable, no hand-assembled ARM64 hex
  2. -mcmodel=tiny — all addressing is pure PC-relative (ADR), no ADRP page-alignment issues at arbitrary code cave locations
  3. Sentinel-based config — patcher locates g_config by scanning for 0xCAFEBABE markers, fills addresses dynamically
  4. ASLR-safe — runtime base computed via &g_config - self_va, all VA fields adjusted by base offset
  5. Zero-invasive — only writes to zero-filled cave + modifies 8-byte RELA addend; original code/data untouched
  6. Config-driven — same hook.bin works for any target; just change JSON addresses

Disclaimer

This tool is provided for educational and authorized security research purposes only. The authors are not responsible for any misuse. Users must comply with all applicable laws and obtain proper authorization before modifying any software.

License

This project is licensed under the GNU General Public License v3.0.

Contributing

Issues and Pull Requests are welcome. Please follow the existing code style and include comments in your submissions.

Acknowledgments

  • Android NDK — ARM64 cross-compilation toolchain
  • LLVM/Clang — Compiler infrastructure
  • The reverse engineering community for sharing knowledge on ELF internals, GOT hooking, and OLLVM analysis