GO语言开发的 DxF外挂验证分析从零拆解,看完你也会!
这个是一名用户扔给我的程序文件夹开始,我一步步摸清它的验证机制,直到最后把它完全攻破。
并写出了这份文章 ,主要是靠经验,纯属捡漏!!
准备工具:
DBG X64
go解析工具 :GoReSym
开始下载:解压得出一个文件夹
下载安装后,od报错,也不知道怎么分析,然后查看pe
【tu1.png】
看不到有用的 只知道是64位后面才知道是GO语言写的,用的是 前端用的Wails 框架
然后开始先抓包
【tu2.png】
然后知道了
- 它连的是
ip:4002,全程明文 HTTP!
- 登录时发出的请求长这样(就是这次抓到的原文):
POST /heartbeat HTTP/1.1
host: ip:4002
content-type: application/json
{"content":"Var=2&card=00&mac=None&soft=%E9%9A%94%E7%A6%BB%E7%99%BD%E5%9B%BE&pid=5824"}
AAAAAAAAAAAAAAAApnLBr6BD+K1BSoPcBuc6tN0rs1Fwtw+Xq+uwwdRe3XX7X*****
前期没什么有用的东西,不过后面dbg调试的时候 看到一些特征,感觉是 go语言,那如果是go语言
Go编译器通常会把所有函数名压在二进制里(pclntab 表),而且是“明文”
也就是说,哪怕没有源码,函数名全在文件里躺着,而且也没有什么加密,然后请求返回Base64 乱码,说明请求不加密、响应加密。想伪造响应,必须先搞明白这段 Base64 的加密方式。
第二阶段:解密base64
我们把那段 Base64 存下来(比如存成 resp.bin),先猜:
# 看前 12 个字节
[Convert]::FromBase64String((Get-Content resp.bin))[0..11]
得到的结果:解码后**前 12 个字节全是 00。
【tu3.png】
这条结果说明什么呢:这是 AES-GCM 加密的典型结构——前12字节 = nonce(随机数)+ 后面 = 密文和校验标签。正常软件 nonce 是随机的,这里全零——作者图省事,固定了 nonce。
说明响应用的是"标准 AES-GCM",就差一把钥匙。
第三阶段:回到x64 dbg,把"钥匙"找出来
我们既然知道是GO语言开发的,然后用到一个解析工具 :GoReSym
作者开源地址:怕违规,你们自己去git搜索一下
现在知道了要干什么:找到 AES 密钥是怎么来的。这时候第一阶段发现的"Go 函数名全在"就成了知识库咯。
.1 从函数名里找线索
Go 程序的函数名藏在一种叫 pclntab 的符号表里——注意:x64dbg 自带的“符号”功能不认这种格式(它只认微软的 PDB),所以直接在里面搜是搜不到的,要用Go语言专门的解析工具:
然后我们拿到一个函数清单的文件,搜关键词:
- 搜
Generate → 出现 GenerateKey、GenerateRequestParams
- 搜
Aes → 出现 AesGcmDecrypt
- 搜
Machine → 出现 GetMachineCode
【tu4.png】
| 函数名 |
一眼看出的作用 |
GenerateKey |
生成密钥(就是它!) |
AesGcmDecrypt |
AES-GCM 解密(解密服务器响应) |
GenerateRequestParams |
组装请求参数 |
GetMachineCode |
取机器码 |
然后都取出来了,就不一一截图了!!
这些关键词说明:验证流程的画布已经完整了——生成密钥 → 组装请求 → 发心跳 → 解密响应。现在只需要钻进 GenerateKey 看密钥怎么算。因为名字已被解析出来,偏移已换算好,直接拿去用
我们打开调试器
- 在 x64dbg 里按
Ctrl+G,输入函数地址(这里是 0x1403AE000 的相对位置,回车。
【tu5.png】
- 然后跑起程序断点,等他停下来,然后断到达函数开头后往下看,到这段代码
00000001403AE000 | 4C:8D6424 E0 | lea r12,qword ptr ss:[rsp-0x20] |
00000001403AE005 | 4D:3B66 10 | cmp r12,qword ptr ds:[r14+0x10] |
00000001403AE009 | 0F86 0B010000 | jbe 棱镜工作台.1403AE11A |
00000001403AE00F | 55 | push rbp |
00000001403AE010 | 48:89E5 | mov rbp,rsp |
00000001403AE013 | 48:81EC 98000000 | sub rsp,0x98 |
00000001403AE01A | 48:898424 A8000000 | mov qword ptr ss:[rsp+0xA8],rax |
00000001403AE022 | 48:89CA | mov rdx,rcx |
00000001403AE025 | 48:C1F9 3F | sar rcx,0x3F |
00000001403AE029 | 48:89C6 | mov rsi,rax |
00000001403AE02C | 48:B8 057CF36AE259D148 | mov rax,0x48D159E26AF37C05 |
00000001403AE036 | 48:F7EA | imul rdx |
00000001403AE039 | 48:C1FA 0A | sar rdx,0xA |
00000001403AE03D | 48:29CA | sub rdx,rcx |
00000001403AE040 | 48:69CA 100E0000 | imul rcx,rdx,0xE10 |
00000001403AE047 | 48:894C24 60 | mov qword ptr ss:[rsp+0x60],rcx |
00000001403AE04C | 44:0F117C24 70 | movups xmmword ptr ss:[rsp+0x70],xmm15 |
00000001403AE052 | 44:0F11BC24 80000000 | movups xmmword ptr ss:[rsp+0x80],xmm15 |
00000001403AE05B | 48:89F0 | mov rax,rsi |
00000001403AE05E | 66:90 | nop |
00000001403AE060 | E8 7BBACBFF | call <棱镜工作台.sub_140069AE0> |
00000001403AE065 | 48:8D0D 94902500 | lea rcx,qword ptr ds:[0x140607100] | 解密
我们解读这段代码 :栈检测,Go运行时代码,业务还没开始……中间一大段:sub rsp、mov、imul、算术运算……
00000001403AE060 call sub_140069AE0 ;前面执行完SHA256或者其他前置逻辑
00000001403AE065 lea rcx,ds:[0x140607100]; ←真正加载加密常量slice头,解密相关
所以我们在下面看到了他
00000001403AE065 | 48:8D0D 94902500 | lea rcx,qword ptr ds:[0x140607100] | 解密
- 我们直接跑过去看内存 ,ascii看到了以下内容
【tu6.png】
your-32-byte-secret-key-1234567890
嘿嘿嘿,AES 密钥的种子被硬编码在程序里。这样任何拿到这个 exe 的人,都能算出密钥
3.1 继续跟完整的密钥公式
然后光有种子不够,AES 密钥还得经过处理。但是继续看 GenerateKey 的逻辑(单步几条),注意到:
1.它调用了一次"获取时间"(时间戳,请求过 /timestamp 那个)
- 时间戳经过一步运算(除以 3600 —— 也就是"取小时")之后,和种子拼接
3.拼接结果过一遍 SHA-256,得到 32 字节的 AES 密钥
【tu7.png】
然后得到的结论(先记为公式 A):
密钥 = SHA256( "your-32-byte-secret-key-1234567890" + 时间戳的小时形式 )
注意:这里拼接的是小时取整后的时间戳**(如 1789045200),不是原始秒、也不是小时数,因为踩了坑过来的!
这条我们总结:密钥是"种子 + 时间"算出来的,每小时变一次,但每个人都能算。到这一步,"解密"已经没有门槛了。
期待的小手,可以写出注册机了
第四阶段:AI写脚本验证 —— 真的能解密
把已知的的公式结果,交给豆包写,不吹不黑,豆包2.1的turbo 还是可以用的
4.1 写解密脚本
import base64, hashlib
from Crypto.Cipher import AES
SEED = "your-32-byte-secret-key-1234567890"
def decrypt(b64_body, timestamp):
key = hashlib.sha256((SEED + str(timestamp // 3600 * 3600)).encode()).digest()
raw = base64.b64decode(b64_body)
cipher = AES.new(key, AES.MODE_GCM, nonce=raw[:12], mac_len=16)
return cipher.decrypt_and_verify(raw[12:-16], raw[-16:])
# 用第二阶段抓到的真实响应 + 当时的服务器时间戳
print(decrypt(open("resp.bin").read(), 1789046645))
我的踩坑记录:第一次没解开,因为在"时间戳怎么拼"上猜错了一版(我让ai先试了"小时数"但是不行GG,应该用"小时取整后的时间戳")。改了拼接方式后:
得到的答案:解密成功,明文是:
{"code": 1006, "expiration_time": "卡密不存在.", "remaining_time": "", "file_modified_time": "", "data": null}
这个时候我们有了 看懂响应 伪造响应的全部能力。code=1006 是"授权失败",跟界面上弹出的提示完全对上了
第五阶段:移花接木,欺骗客户端
首先 既然密钥能算,那造一个 code=2000(授权成功)的响应是分分钟的事。但是唯一的问题:怎么把这个假响应送到客户端手里?不可能一直开着抓包把?而且 客户端写死了连 IP:4002(硬编码 IP,不查域名——所以改 hosts 没用)
那就玩一个"地址占位":把这个 IP 绑到本机,客户端以为在连服务器,其实连的是我们,不用带端口:
# 需要管理员。把目标 IP 绑到本机的 Loopback 接口(物理网卡完全不受影响)
netsh interface ip add address name="Loopback Pseudo-Interface 1" ip 255.255.255.255
顺便写个端口返回器:浏览器打开 **:4002/timestamp——如果此时本机服务器已经起来、你看到的是自己服务器返回的 {"timestamp":...},说明流量已经"截胡"成功。
【tu8.png】
然后写一个 ,写个几十行的小服务,监听 4002,对客户端的一切请求都回"我们造的响应":
1 收到 GET /timestamp → 返回当前时间戳
2 收到 POST /heartbeat → 用 上面保留的 的公式造一个 code=2000 的加密响应返回
def cmd_demo(_args) -> int:
hr("DEMO · 伪造服务器 + 模拟请求客户端 ")
state = {"i": 0}
srv = start_server(0, state, days=3650, data={}) # 端口0随机的
port = srv.server_address[1]
print(c(" [1] 客户端 GET /timestamp (明文 HTTP, V-02)", "1;36"))
import urllib.request
ts_body = urllib.request.urlopen(f"http://127.0.0.1:{port}/timestamp", timeout=5).read()
server_ts = json.loads(ts_body)["timestamp"]
print(f" <- {{\"timestamp\":{server_ts}}}")
print(c(" [2] 客户端 POST /heartbeat body={{\"content\":\"Var=2&card=00&mac=None&...\"}}", "1;36"))
req = urllib.request.Request(
f"http://127.0.0.1:{port}/heartbeat",
data=json.dumps({"content": "Var=2&card=00&mac=None&soft=%E9%9A%94%E7%A6%BB%E7%99%BD"
"%E5%9B%BE&pid=8052"}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
body = urllib.request.urlopen(req, timeout=5).read().decode()
print(f" <- {body[:60]}... ({len(body)} B Base64, nonce=全零x12)")
然后我们跑一下,看看是否登录成功
【tu9.png】
完美 正常进入,因为我不会 DNF 我不知道如何用 就不测试了 ,就只是简单复现
总结
为什么能成功破解 首先我 不是攻破了什么高深加密,而是作者把每一环都做"省"了——种子密钥写死、时间可预测、流量明文、响应无条件信任。加密算法再标准,钥匙插在锁上就等于没有锁。
建议修复思路(能防住我这种菜鸟):
1.首先流量上 TLS
2.授权改成服务器签名、客户端只留公钥
3.功能层加真实会话。
4.能加V,能加壳只管上
5.什么恶心都只管上,反正我后面不会再看了 hhh
最后如果对你有学习帮助 请给我 点赞+评分 谢谢!
另外如果有困惑的程序 可以留言,我下课后 如果能玩的动
我会积极的写出一篇教程文章,努力活跃论坛气氛
练手程序下载地址:链接:https://share.weiyun.com/s6PECfJw 密码:isgkxa
调试请在虚拟机哦!!!