文章总结: 本文详细介绍了ELFGOTHook技术在逆向工程中的实战应用,作者通过分析一个受OLLVM混淆和字符串加密保护的Android游戏外挂,展示了如何通过hooksendto函数在数据发送前动态替换卡密验证的AppID。关键发现包括:利用GOT表可读写特性绕过复杂混淆,通过init_array注入shellcode,以及处理OLLVM的GOT地址混淆。可操作建议涉及使用开源工具elf-got-patcher进行二进制补丁,并提供了IDA定位GOT地址的具体方法。 综合评分: 92 文章分类: 逆向分析,二进制安全,红队,漏洞分析,安全工具
ELF GOT Hook 实战
LeoChen.. LeoChen..
看雪学苑
2026年6月22日 17:59 上海
在小说阅读器读本章
去阅读
最近沉迷某游戏,排位连跪,手机正好有 root,于是开始研究内核G。找到一个好用的G,但卡密高达 一天 8 元。本着「能动手就别动钱包」的原则,决定研究如何破解卡密验证。
初步分析
第一步:识别文件格式
拿到目标文件(一个 .sh 脚本),直接拖入 IDA Pro,提示不是 ELF 格式。
用十六进制编辑器打开
发现文件头部特征是经典的 gzexe 压缩加密 —— 这是一种 Linux 下常见的可执行文件压缩方式,本质上是把 ELF 用 gzip 压缩后包裹在一个 shell 脚本里。
第二步:解压还原
gzexe -d MG
解压后得到真正的 ELF 二进制,重新拖入 IDA Pro。
第三步:面对混淆
IDA 分析结果显示:
-
严重的 OLLVM 混淆
—— 控制流平坦化,大量虚假分支
-
字符串加密
—— 所有关键字符串(URL、密钥等)在二进制中不可见,运行时动态解密
这意味着静态分析几乎无法直接定位关键逻辑。
抓包定位卡密验证
既然静态分析困难,转向动态分析。手机端抓包发现卡密验证请求指向:
http://8i0LqloF.不方便展示.cn/api/?id=kmlogon&app=51642
关键参数:app=51642 —— 这是该挂在卡密验证平台上的 App ID。
验证平台分析
该挂使用的是微卡验证平台。恰好我也有这个平台的账号,可以自建后台、自定义配置。
经过大量动态调试(Frida + IDA 远程调试),成功还原了目标挂的后台配置数据,并将其复制到了自己的微卡验证后台。
本文主题是 GOT Hook 静态补丁技术,动态调试过程不在此展开。重点是为大家提供一个面对 OLLVM 混淆 + 字符串加密的 ELF 时的解题思路。
核心思路
现在问题变得很简单:
把二进制中发送的 App ID 从
51642改成我自己的 ID,卡密验证就会走我自己的后台。
但是,由于 OLLVM 字符串加密,51642 这个字符串在二进制中根本找不到 —— 它是运行时解密后才出现在内存中的。
传统的十六进制补丁?不行。
修改解密逻辑?OLLVM 混淆下太复杂。
那怎么办?
答案:GOT Hook
不需要理解混淆逻辑,不需要解密字符串 —— 在数据发送出去的那一刻拦截并修改。
程序最终要把数据发送到服务器,必然要调用网络发送函数。通过分析,该程序使用 sendto() 发送 UDP 数据报。我们只需要:
-
Hook
sendto()函数—— 在数据发出前拦截
-
扫描发送缓冲区
—— 搜索原始 App ID
51642 -
替换为自己的 ID
—— 原地修改,长度不变
-
正常发送
—— 调用真实
sendto(),程序无感知
这就是 GOT Hook 的威力:绕过所有混淆,直接在数据出口动手。
什么是 GOT Hook?
GOT(Global Offset Table,全局偏移表)
ELF 动态链接二进制调用外部函数(如 sendto())时,不会直接跳转到 libc,而是通过 GOT 表间接调用:
正常调用链:
代码 → 读取 GOT[sendto] → 跳转到 libc sendto()
Hook 后:
代码 → 读取 GOT[sendto] → 跳转到我们的 hook 函数 → 修改数据 → 调用真实 sendto()
关键点:GOT 表位于 rw-(可读写) 内存段,可以在运行时被修改。
为什么选择 Hook sendto?
sendto() 是发送 UDP 数据报的系统调用封装,签名如下:
ssize_tsendto(int fd, constvoid *buf, size_t len, int flags,
conststruct sockaddr *dest_addr, socklen_t addrlen);
-
buf(第 2 个参数)—— 发送缓冲区,包含即将发出的数据
-
len(第 3 个参数)—— 数据长度
我们的 hook 只需要在 buf 中搜索 App ID 并替换,然后原样调用真实 sendto() 即可。
不只是
sendto—— 根据目标程序的实际情况,你也可以 hooksend()、write()、SSL_write()等任何通过 GOT 调用的函数。核心思路完全一致。
本工具的 Hook 流程
┌──────────────────────────────────────────────────────────┐
│ ELF 二进制(patch 前) │
│ │
│ .init_array[N] ──RELA──▶ original_init() │
│ GOT[sendto] ────────▶ libc sendto(可能已混淆) │
│ code cave ────────▶ 0x00 0x00 ... (全零填充) │
└──────────────────────────────────────────────────────────┘
▼ patcher.exe + config.json ▼
┌──────────────────────────────────────────────────────────┐
│ ELF 二进制(patch 后) │
│ │
│ .init_array[N] ──RELA──▶ init_wrapper() ◄── code cave │
│ │ │
│ ├─ 调用 original_init() │
│ ├─ 保存真实 sendto 到 BSS 槽 │
│ └─ GOT[sendto] = hook_sendto │
│ │
│ hook_sendto(): │
│ ├─ 扫描 buf 搜索 "51642" │
│ ├─ 替换为我的 App ID │
│ └─ 尾调用真实 sendto() │
└──────────────────────────────────────────────────────────┘
运行时执行时序
程序启动
│
├─ 动态链接器解析 .init_array
│ └─ RELA addend 指向 code cave → 调用 init_wrapper()
│ │
│ ├─ ① 调用原始 init 函数(保持程序正常初始化)
│ ├─ ② 读取 GOT[sendto],减去混淆常量,得到真实地址
│ ├─ ③ 将真实 sendto 地址存入 BSS 空闲槽
│ └─ ④ 将 hook_sendto 地址(加混淆常量)写入 GOT
│
├─ 程序正常运行,卡密验证逻辑解密字符串、构造请求 ...
│
└─ 调用 sendto(fd, "...app=51642...", len, ...)
└─ 实际跳转到 hook_sendto()
├─ 扫描 buf,找到 "51642"
├─ 原地替换为自己的 App ID
└─ 尾调用真实 sendto() → 数据发往服务器
└─ 服务器收到的是修改后的 App ID ✓
OLLVM GOT 混淆处理
这个二进制受 OLLVM 保护,GOT 中存储的不是真实函数地址,而是混淆后的值:
GOT[sendto] = 真实地址 + 混淆常量
真实地址 = GOT[sendto] - 混淆常量
我们的工具通过 got_addend 配置项处理这种情况,安装 hook 时保持混淆方式一致,程序完全无感知。
实战:如何获取 GOT 地址
玩过 SimpleHook 的应该都知道,SimpleHook 可以自定义类名和方法名来 hook Java 层函数。sendto 在这里就相当于「方法名」—— 只不过这是 native 层的函数,运行在 ARM64 ELF 中,我们需要获取它在 GOT 表中的实际地址。
用 IDA Pro 定位 got_sendto 和 got_addend
步骤:
- 在 IDA 中找到
sendto方法名 - 按住方法名 → 右键 → Xrefs graph to…
- 在交叉引用图中可以看到两个关键地址:
| 你看到的位置 | 对应配置字段 | 含义 |
| — | — | — |
| data 段中引用 sendto 的位置 | got_sendto | GOT 表项的虚拟地址 |
| MOVZ+MOVK×3 指令中的 64 位常量 | got_addend | GOT 混淆常量(OLLVM 特有) |
got_sendto 就是 GOT 表中存储 sendto 函数指针的那个槽位地址。got_addend 是 OLLVM 用来混淆 GOT 值的 64 位常量(如果目标没有 OLLVM 混淆,填 0 即可)。
sendto 的本质 —— 理解 buf 参数
ssize_tsendto(int fd, constvoid *buf, size_t len, int flags,
conststruct sockaddr *dest_addr, socklen_t addrlen);
-
buf(第 2 个参数)就是请求体 —— 程序即将发送的数据
-
我们通过 hook
sendto,在发送前扫描并修改buf的内容,就能实现动态替换字符串
这和 SimpleHook 修改方法参数是同一个思路,只不过这里是在 native 层、二进制级别操作。
开源项目:elf-got-patcher
知道了这些基本信息之后,就需要自己写 GOT Hook 了。为了方便大家理解和使用,我开源了一个工具:
elf-got-patcher:https://github.com/LeoChen-CoreMind/elf-got-patcher
这个工具的核心思想:纯 C 编写 shellcode,编译成裸 ARM64 二进制,通过 patcher 注入到 ELF 的 code cave 中。
关键代码解析
1. init_wrapper —— 如何注入进 ELF
ELF 有一个 .init_array 段,里面存放程序启动时自动调用的初始化函数。我们的注入方式:
- 找到
.init_array中最后一个 RELA 条目 - 修改它的 addend,让它指向我们的 code cave
- 程序启动时就会自动调用我们的
init_wrapper
// init_wrapper 的核心逻辑:
voidinit_wrapper(void){
// 计算 ASLR 基址偏移
uint64_t base = (uint64_t)&g_config - g_config.self_va;
// ① 先调用原始 init 函数(不破坏程序原有逻辑)
void (*orig)(void) = (void (*)(void))(base + g_config.orig_init);
orig();
// ② 从 GOT 表读取 sendto 地址,减去混淆常量,得到真实地址
uint64_t *got = (uint64_t *)(base + g_config.got_sendto);
uint64_t real = *got - g_config.got_addend;
// ③ 保存真实 sendto 到 BSS 槽位(后面 hook 要用)
*(uint64_t *)(base + g_config.saved_sendto) = real;
// ④ 把 hook 函数地址写入 GOT(加回混淆常量保持一致)
*got = (uint64_t)&hook_sendto + g_config.got_addend;
}
原理图:
修改前:.init_array RELA addend → original_init()
修改后:.init_array RELA addend → init_wrapper()(在 code cave 中)
│
├─ 调用 original_init() ← 保持原有逻辑
└─ 篡改 GOT[sendto] ← 安装 hook
2. hook_sendto —— 拦截并修改请求体
ssize_thook_sendto(int fd, constvoid *buf, size_t len, int flags,
conststruct sockaddr *dst, socklen_t addrlen){
// 在 buf 中搜索所有配置的 needle,找到就替换
if (buf && g_config.pair_count)
scan_and_replace((uint8_t *)buf, len);
// 调用真实 sendto,发送修改后的数据
uint64_t base = (uint64_t)&g_config - g_config.self_va;
sendto_fn real = *(sendto_fn *)(base + g_config.saved_sendto);
return real(fd, buf, len, flags, dst, addrlen);
}
scan_and_replace 会遍历配置中的所有 needle/replace 对,在 buf 中逐字节搜索匹配,找到就原地替换:
// 简化后的核心逻辑:
for (每个 needle/replace 对) {
for (buf 中的每个位置) {
if (当前位置开始的 N 字节 == needle) {
memcpy(当前位置, replace, N); // 原地替换
break; // 每对只替换一次
}
}
}
3. 哨兵配置池 —— 同一份 shellcode 适配不同目标
shellcode 中所有地址都用 0xCAFEBABE 开头的哨兵值占位:
struct config_pool g_config = {
.self_va = 0xCAFEBABE00000001, // patcher 填入实际 VA
.orig_init = 0xCAFEBABE00000002, // patcher 填入原始 init VA
.got_sendto = 0xCAFEBABE00000003, // patcher 填入 GOT 地址
.got_addend = 0xCAFEBABE00000004, // patcher 填入混淆常量
// ...
};
Patcher 在注入时扫描这些哨兵,替换为实际地址。这样同一份 hook.bin 可以适配任何目标,只需要更换 JSON 配置。
使用方法
1. 编译
# 设置 Android NDK 路径
$env:ANDROID_NDK = "C:\path\to\ndk"
# 一键编译(shellcode + patcher)
.\build.bat
2. 编写 JSON 配置
{
// 目标 ELF 文件路径
"input":"../MG_orig",
"output":"../MG_patched",
"payload":"shellcode/hook.bin",
// IDA 中获取的地址
"got_sendto":"0x29A0B8",// GOT 表项地址(Xrefs graph → data 位置)
"got_addend":"0x25B41D7A03EB0111",// 混淆常量(MOVZ+MOVK×3)
// 替换规则:原始 App ID → 我的 App ID
"needle":"51642",
"replace":"35029",
// 可选:开启调试日志
"debug_flag":"1",
"debug_path":"/data/local/tmp/mg_sendto.log"
}
cave_va、cave_off、rela_addend_off、orig_init、saved_sendto均支持自动检测,大多数情况下不需要手动填写。
3. 执行 Patch
.\patcher.exe configs\my_config.json
输出示例:
[+] payload shellcode/hook.bin : 970 bytes
[+] input ..\MG_orig : 2681320 bytes
[*] auto-detecting code cave ...
[+] auto-cave: off=0x7d340 va=0x7d340 (1016 bytes avail)
[*] auto-detecting .init_array RELA ...
[+] auto-rela: 4 entries, using last: addend_off=0x1ae8 orig_init=0xef320
[*] auto-detecting BSS slot ...
[+] auto-bss: saved_sendto VA=0x29a5f0
[+] g_config patched at payload off 0x248 (self_va=0x7d588)
pair[0] "51642" -> "35029"
debug ON -> /data/local/tmp/mg_sendto.log
=========================================
PATCH OK : ..\MG_orig -> ..\MG_patched
payload @ file 0x7d340 (VA 0x7d340) 970 bytes
RELA addend @ 0x1ae8 : 0xef320 -> 0x7d340
=========================================
4. 推送到手机
adb push MG_patched /data/local/tmp/
adb shell "su -c 'cp /data/local/tmp/MG_patched /target/path/MG; chmod 755 /target/path/MG'"
5. 验证效果
如果开启了 debug_flag,可以导出日志查看修改后的请求:
adb pull /data/local/tmp/mg_sendto.log
用 Python 解析日志:
import struct, sys
d = open(sys.argv[1], 'rb').read(); o = 0
while o + 8 <= len(d):
n = struct.unpack('<Q', d[o:o+8])[0]; o += 8
print(d[o:o+n]); o += n + 1
如果看到请求中 app=35029(你的 ID)而不是 app=51642(原始 ID),说明 hook 成功 ✓
6. 实战验证:先用 Debug 模式跑一遍
在正式修改之前,建议先开启 debug 模式观察原始流量,确认目标字符串确实存在于 sendto 的发送缓冲区中。
配置中设置:
"debug_flag":"1",
"debug_path":"/data/local/tmp/mg_sendto.log"
查看日志内容,发现确实捕获到了 35029 这个 App ID —— 这就是程序运行时解密后、即将发送的真实请求体。字符串加密在发送那一刻已经被解密了,全部暴露在 buf 中。
确认目标字符串存在后,配置 needle/replace 进行替换,把 35029 改成自己的 App ID,重新 patch 即可。
验证修改成功后,关闭调试模式(debug_flag 设为 0 或删除该字段),去掉日志开销,即完成最终部署。 
实际场景提醒:本文以
sendto+ App ID 替换为例,但现实中每个目标的情况都不一样 —— 可能用的是send()、write()、甚至自定义的加密通信函数;要替换的可能是 URL、Token、设备指纹等任何字段。核心方法论是通用的,具体操作需要根据你的实际逆向分析结果随机应变。
总结
面对 OLLVM 混淆 + 字符串加密的 ELF 二进制,传统的静态补丁几乎不可能。本文的思路是:
-
不与混淆正面交锋
—— 不去解密字符串、不去理解控制流平坦化
-
抓住数据出口
—— 程序无论怎么混淆,最终都要调用系统函数发送数据
-
GOT Hook 拦截
—— 在 GOT 表层面替换函数指针,在数据发送前修改内容
-
纯静态补丁
—— 不需要 Frida、不需要 root 注入框架,直接修改 ELF 文件
这个思路不仅适用于卡密验证绕过,也适用于任何需要修改 native 层网络请求的场景甚至于修改方法参数里的各种字符串实现你想实现的效果。希望对大家有帮助。
工具开源地址:elf-got-patcher
https://github.com/LeoChen-CoreMind/elf-got-patcher
本文仅用于安全研究与技术学习,请勿用于任何违法用途。
#
看雪ID:LeoChen..
https://bbs.kanxue.com/user-home-1069137.htm
*本文为看雪论坛优秀文章,由 LeoChen.. 原创,转载请注明来自看雪社区
第十届安全开发者峰会【议题征集】-欢迎投稿
往期推荐
基于 Seccomp-notify 的 Binder 事务级无 Hook 代理机制研究
AI 辅助分析|CyberGame 高分 Web 题:缓存投毒攻击链路拆解
UDS协议安全CTF挑战分析
把 .o 变成 .ko:一次 ELF 格式的奇妙之旅
基于eBPF的Android ART运行时DEX采集与方法字节码回填
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 LeoChen.. LeoChen..《ELF GOT Hook 实战》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论