一次使用Hxd修改EXE安装包硬编码IP地址的实战经历,踩过八进制前导零的坑后,反向利用inet_addr的八进制解析规则完成精确替换
源码丢了,我在 EXE 里改了 3 个字节
场景:一个尘封多年的 EXE 安装包,源码早已遗失,但服务器迁移后 IP 地址必须修改。
工具:Hxd(十六进制编辑器)
战果:不仅成功完成了字节码级别的 IP 替换,还在踩坑之后反向利用了
inet_addr()的八进制解析规则——把陷阱变成了解法。
一、那个下午,客户说"必须得用"
事情是这样的——
客户有一套老旧的业务系统,运行了很多年。最近服务器要整体迁移,从旧的 10.30.127.132 迁移到新的 10.244.27.90。数据库迁完了、配置文件改了、防火墙规则也调好了,一切看起来都在掌控之中。
然后客户抛过来一个 EXE 安装包:"这个也要改。"
"源码呢?"
"早不记得放哪儿了,就剩这一个 EXE 了。"
这就有了今天这篇文章——在没有源码的情况下,如何通过 Hxd 十六进制编辑器直接修改二进制文件中的硬编码 IP 地址。
二、Hxd 上阵:十六进制里的"手术刀"
Hxd[1] 是一款轻量但功能强大的十六进制编辑器,免费、免安装,在逆向工程和二进制分析领域有着极佳的口碑。
拿到 EXE 后,操作思路很直接:
用 Hxd 打开 EXE,搜索源 IP 的十六进制表示 找到目标位置,替换为新 IP 保存,测试运行
搜索源 IP 10.30.127.132 的 ASCII 十六进制:
31 30 2E 33 30 2E 31 32 37 2E 31 33 32 1 0 . 3 0 . 1 2 7 . 1 3 2Hxd 强大的搜索功能很快在文件中定位到了这个字符串——它被硬编码在程序的网络连接初始化逻辑里,可能是一个 connect() 调用、也可能是一个配置文件路径、也可能是一段内置的默认服务器地址。
不管哪种情况,替换它。
三、位数不对:一个不起眼的大问题
目标 IP 是 10.244.27.90,十六进制表示为:
31 30 2E 32 34 34 2E 32 37 2E 39 30 1 0 . 2 4 4 . 2 7 . 9 0问题来了——长度不一致。
10.30.127.132 | ||
10.244.27.90 |
在二进制文件中,字符串通常以 00(NULL)结尾。如果直接替换,目标 IP 会比源 IP 少一个字节。这个多出来的字节如果处理不当——
可能会破坏后续数据的对齐 可能会留下一个残留的 00导致字符串被截断或者更糟,程序在解析时读到预期之外的数据
直观的"聪明"做法:在 27 前面补一个 0,变成 027。
10.244.027.90 ← 正好 13 个字符,长度完美对齐!替换后的十六进制:
31 30 2E 32 34 34 2E 30 32 37 2E 39 30 1 0 . 2 4 4 . 0 2 7 . 9 0看起来天衣无缝。保存,运行——炸了。
四、八进制陷阱:前导零的隐秘杀机
程序启动后,连接的不是 10.244.27.90,而是一个完全陌生的地址。排查了一圈,最终把目光锁定在那个"聪明"的补零操作上。
027,在 C 语言里,是八进制数。
这不是玩笑。在 C 语言及大量派生语言中,以 0 开头的数字字面量会被解析为八进制:
int a = 027; // 八进制 27 = 十进制 23int b = 27; // 十进制 27int c = 010; // 八进制 10 = 十进制 8而很多网络库——特别是基于 C 语言标准库 inet_addr() 或旧版 Windows API 的 inet_aton() 实现——在解析 IP 地址的每一段时,会用 strtol() 或类似的函数。这些函数默认遵循 C 语言的字面量规则:
以 0x开头 → 十六进制以 0开头 → 八进制其他 → 十进制
所以,当程序读到 027 时:
027(八进制) = 2×8¹ + 7×8⁰ = 16 + 7 = 23(十进制)程序实际连接的地址变成了 10.244.23.90,而不是期望的 10.244.27.90。
这个特性从 UNIX 诞生之初就存在了。Ken Thompson 和 Dennis Ritchie 在设计 C 语言时沿用了 B 语言的八进制约定,而这个约定又来自更早的 PDP-7 汇编习惯。半个世纪后,它仍然静静躺在每一行
inet_addr()的源码里,等着给粗心的程序员上一课。
五、不只是 inet_addr:哪些场景会踩这个坑?
这个问题影响面比想象中广。任何调用以下函数的程序,传入带前导零的 IP 分段时,都可能触发八进制解析:
inet_addr() | ||
inet_aton() | ||
inet_pton() | 严格十进制,不会误解析 | |
gethostbyname() | inet_addr() | |
getaddrinfo() |
特别需要警惕的是 Windows 平台的老旧程序。Windows Sockets 的 inet_addr() 实现明确遵循 BSD 规范,支持八进制和十六进制表示法:
127.0.0.1 → 标准点分十进制0177.0.0.1 → 八进制,等同于 127.0.0.10x7F.0.0.1 → 十六进制,等同于 127.0.0.1甚至支持混合表示法:0177.0.0.0x1 —— 每一段独立解析,各用各的进制。极度灵活,也极度危险。
六、反败为胜:既然你认八进制,那我就写八进制
当我把 027 会被解析为 23 这件事搞清楚之后,脑子里突然闪过一个念头——
既然 inet_addr() 认八进制,那我能不能反过来利用它?
我需要的是一个字符串,它写在二进制里恰好 13 个字符(和源 IP 等长),但被 inet_addr() 解析后,第三段的值是 27。
换句话说:找一个值 X,使得 X 的八进制写法写在 IP 字符串里,但解析出来的十进制值等于 27。
X(八进制) = 27(十进制)X = 3×8 + 3 = 24 + 3 = 27所以 X = 33(八进制) → 写作 "033"验证一下:
inet_addr("10.244.033.90")// 第三段 "033" → 八进制解析 → 3×8 + 3 = 27 ✓// 结果:10.244.27.90 ✓而且长度呢?
10.30.127.132 | ||
10.244.033.90 |
完全对齐!
这是一个巧妙的逆向思维——陷阱本身变成了解决方案。那个让你掉进去的坑,换个角度看,恰好是一把为你量身定做的钥匙。
最终替换
在 Hxd 中,将源 IP 的十六进制:
31 30 2E 33 30 2E 31 32 37 2E 31 33 32 1 0 . 3 0 . 1 2 7 . 1 3 2替换为:
31 30 2E 32 34 34 2E 30 33 33 2E 39 30 1 0 . 2 4 4 . 0 3 3 . 9 0保存、运行——通了。程序正确连接到了 10.244.27.90。
那个下午最妙的一刻,不是问题被解决,而是意识到:让你栽跟头的东西,换个姿态就能为你所用。八进制没有好坏,全看你怎么用它。
七、换个思路:如果不用八进制还有哪些解法
虽然最终方案靠八进制反杀,但这个过程中也探索过其他可能的路径,作为知识储备也值得记录:
备选方案一:NULL 字节填充
如果源 IP 字符串后面紧跟着一个 00(NULL 终止符),而 00 后面还有一些填充字节,可以直接写入目标 IP,把多出的位置也填成 00。
原始:10.30.127.132\0 [padding]替换:10.244.27.90\0\0 [padding]但这条路的风险在于,不确定多写一个 NULL 是否会破坏后续数据结构。需要完整的二进制分析才能确认。
备选方案二:重新编译
用 inet_pton() 替代 inet_addr(),前者严格解析十进制,不会有八进制歧义。但这需要源码——在这件事里,这条路走不通。
为什么八进制方案是最优解
对比下来,033 方案有三个无可替代的优势:
字节完全对齐:13 个字符精确替换 13 个字符,零副作用 不依赖文件结构:不需要分析 NULL 之后有什么,不需要了解 PE 布局 符合程序原有行为: inet_addr()天生就认八进制,这不是 hack,这是它设计的一部分
八、经验总结
这次经历看似是一个小插曲,但背后折射出几个值得深思的问题:
1. 字节码修改永远优先考虑"安全替换",而非"直觉替换"
补 0 的思路在直觉上完全合理——位置刚好、长度一样、看起来就是一个无辜的占位符。但二进制文件没有"直觉",它只认字节,以及字节背后整个工具链的解析规则。任何看似无害的修改,都要从底层协议的视角审视一遍。
2. 陷阱和方案,有时是同一枚硬币的两面
027 是陷阱——它让 IP 解析错误,程序连不上服务器。033 是答案——它利用同样的八进制规则,把 27 正确地送进了 inet_addr() 的解析结果里。
同一个规则、同一个函数、同一个文件位置。唯一的区别是:你是在无意识地触发它,还是在有意识地驾驭它。
3. C 语言的八进制遗产比你想象的更"长寿"
0 开头的数字是八进制——这可能是 C 语言最古老的特性之一。但因为它深植于 POSIX 标准和无数网络库的实现中,直到今天仍在发挥作用。写代码时尽量避免前导零,读代码时务必意识到前导零的存在。
4. Hxd 是二进制分析的利器,但用它的人才是关键
Hxd 提供了精准的十六进制查看和编辑能力,但它不会告诉你 027 是八进制,也不会帮你算出 033(八进制) = 27(十进制)。工具解决"怎么做",领域知识解决"为什么能这样做"。
5. 保留源码,保留源码,保留源码
重要的事情说三遍。这次能救回来纯属运气——IP 地址恰好是可读的 ASCII 字符串、恰好 inet_addr() 的八进制规则可以被反向利用。如果碰到的是编译进代码的混淆常量、或者加密过的配置段,结果可能完全不同。
九、写在最后
那台老服务器最终成功迁移了。客户那边一切正常,仿佛什么都没发生过。
但我们知道,在那个下午,一个 EXE 文件的某个偏移处,30 32 37 被改成了 30 33 33——三个字节之差,承载的却是对一个半世纪前诞生的八进制约定的理解、对 inet_addr() 解析规则的逆向思考,以及一次从"掉进坑里"到"踩着坑爬上去"的认知跃迁。
二进制不会骗人。规则也不会偏袒谁——它只是静静地在那里。你用错了,它是 bug;你用对了,它是 feature。
如果你也曾有过类似的经历——用十六进制编辑器改过 DLL、修过 PE 头、或者在二进制文件里留下过自己的"签名"——欢迎在评论区聊聊。
本文基于真实项目经历撰写,敏感信息已做脱敏处理。
引用链接
[1]Hxd: https://mh-nexus.de/en/hxd/
夜雨聆风