OtterSec 团队用内部工具和 AI Agent,把闭源 Solana 程序 GoonFi V2——一个 propAMM——成功反编译成了能编译出字节完全一致二进制的 Rust 源码,效果等同于直接开源。核心思路并不复杂:给模型一个明确的收敛条件,把编译器当作确定性反馈源,逐函数比对指令流并打分。缺失或多出的指令、顺序错误、寄存器不对、立即数偏差、栈偏移差一字节,各有各的权重。架构上则由一个 architect Agent 按顶层指令拆分调用图,Worker 并行还原,Judge 在每次提交后整体重新编译复评,同时禁止 asm!、black_box、#[no_mangle]、#[repr(C)] 这些取巧手段。还原过程还牵扯到一个容易被忽略的细节:需要对齐函数定义顺序,以及 panic Location 里嵌入的文件路径和行号。
拿到源码之后,审计 Agent 挖出了一个记账漏洞:程序直接用金库 SPL 余额推导库存偏移,任何人都能往金库转入代币人为压低价格,大约 610 万美元资金面临风险。
摘要
让我们有些意外的是,团队成功用内部工具把 GoonFi V2 反编译为 Rust 源代码,其生成的二进制文件与链上程序完全一致,实际上等于把它开源了。
借助这份"开源"代码,团队轻而易举地发现了一个使 $6.1M 面临风险的关键漏洞,并已负责任地向项目方披露。
2026 年,没有程序是闭源的
一位智者曾说过:

事实证明,到了 2026 年,你甚至不需要擅长逆向工程,一切默认都是开源的。不相信这个乌托邦?继续往下读。
逆向工程可能已被解决
我们之前已经展示过,在拥有合适工具时,AI 在逆向工程方面已经变得相当强大。
我们观察到一个现象:通过集成新工具和 AI 带来的提升,对于二进制文件而言效果更大。任何 Rust release 二进制文件都可能如此,因为源代码中呈现更高层级的构造,而且代码生成和优化方式不同于传统 C 编译器,标准反编译器的输出在这些场景下往往更差。
在最近一轮语言模型发布周期中,一个问题很自然地浮现在我们脑海里:逆向工程真的已经被解决了吗?我们能把模型推到多远?
逆向工程的圣杯
长期以来,模型已经能够用语言解释二进制文件的逻辑。而自"逆向工程"一词诞生以来,这基本上就是"做逆向工程"的定义。
那么,如果我们提高标准,给模型一个精确的停止条件,以认定对某个二进制文件的反编译已经"解决",会怎样?
一个自然的答案是持续进行,直到我们找到一组工具链、依赖和源代码的组合,其输出能生成我们正在反编译的那个完全相同的二进制文件。
如果你愿意,也可以称之为重新编译。
听起来很简单,对吧?
游戏与玩家
有了这个游戏的初始定义,剩下要做的就是构建完美的玩家。
为了证明这是可能的,先展示最终目标 GoonFi V2 的结果。你可以在我们的 Solana diff viewer 中于 ibrl.golf 上查看。你可以点击下方 widget,查看每个函数的映射和反编译。
而这一切最初就是下面这团混乱的二进制 blob,外加合约 IDL 和链上交易。
120: mov64 r0, r2
128: ldxdw r2, [r1 + 0x10]
130: ldxdw r3, [r1 + 0x0]
138: jeq r3, 0x0, +0x8 <.text+0x60>
140: ldxdw r4, [r1 + 0x8]
148: jeq r3, r4, +0x6 <.text+0x60>
150: sub64 r4, r3
158: rsh64 r4, 0x3
160: ja +0x9 <.text+0x90>
168: add64 r3, 0x8
170: add64 r4, -0x1
178: jne r4, 0x0, +0x6 <.text+0x90>
180: jeq r2, 0x0, +0xc <.text+0xc8>
188: ldxdw r1, [r1 + 0x18]
190: jeq r2, r1, +0xa <.text+0xc8>
198: sub64 r1, r2
1a0: rsh64 r1, 0x3
1a8: ja +0x8 <.text+0xd0>
1b0: ldxdw r5, [r3 + 0x0]
1b8: jgt r0, r5, -0xb <.text+0x48>
1c0: mov64 r0, r5
1c8: ja -0xd <.text+0x48>
1d0: add64 r2, 0x8
1d8: add64 r1, -0x1
1e0: jne r1, 0x0, +0x1 <.text+0xd0>
1e8: exit
1f0: ldxdw r3, [r2 + 0x0]
1f8: jgt r0, r3, -0x6 <.text+0xb0>
200: mov64 r0, r3
208: ja -0x8 <.text+0xb0>
210: mov64 r6, r1
218: jeq r3, 0x0, +0x30 <.text+0x280>
220: ldxb r4, [r2 + 0x0]
228: stxb [r10 - 0x59], r4
230: jlt r4, 0x2, +0x18 <.text+0x1d8>
238: lddw r1, 0x37a18
248: stxdw [r10 - 0x40], r1
250: mov64 r1, r10
258: add64 r1, -0x10
260: stxdw [r10 - 0x30], r1
268: lddw r1, 0x32558
278: stxdw [r10 - 0x8], r1
280: mov64 r1, r10
288: add64 r1, -0x59
290: stxdw [r10 - 0x10], r1
298: stdw [r10 - 0x20], 0x0
2a0: stdw [r10 - 0x38], 0x1
2a8: stdw [r10 - 0x28], 0x1
2b0: mov64 r7, r10
2b8: add64 r7, -0x58
2c0: mov64 r2, r10
2c8: add64 r2, -0x40
2d0: mov64 r1, r7
2d8: call 0x5bca
2e0: mov64 r1, r7
2e8: call 0x1a1
2f0: ja +0x18 <.text+0x298>
2f8: jlt r3, 0x9, +0x14 <.text+0x280>
300: mov64 r1, r3
308: add64 r1, -0x9
310: jlt r1, 0x8, +0x11 <.text+0x280>
318: mov64 r1, 0x0
320: ldxdw r0, [r2 + 0x9]
328: ldxdw r5, [r2 + 0x1]
330: jeq r3, 0x11, +0x5 <.text+0x240>
338: ldxb r2, [r2 + 0x11]
340: stxb [r10 - 0x59], r2
348: jeq r2, 0x0, +0x2 <.text+0x240>
350: jne r2, 0x1, +0x13 <.text+0x2d0>
358: mov64 r1, 0x1
360: mov64 r2, r5
368: rsh64 r2, 0x20
370: stxb [r6 + 0x10], r4
378: stxdw [r6 + 0x8], r0
380: stxw [r6 + 0x4], r2
388: stxw [r6 + 0x0], r5
390: mov64 r2, 0x18
398: ja +0x2c <.text+0x3e0>
3a0: lddw r1, 0x379e8
3b0: call 0x5a50
3b8: mov64 r1, r0
3c0: and64 r1, 0x3
3c8: jne r1, 0x1, +0x23 <.text+0x3c8>
3d0: ldxdw r1, [r0 + 0x7]
3d8: ldxdw r2, [r1 + 0x0]
3e0: jeq r2, 0x0, +0x20 <.text+0x3c8>
3e8: ja +0x1d <.text+0x3b8>
3f0: lddw r1, 0x37a18
400: stxdw [r10 - 0x40], r1
408: mov64 r1, r10
410: add64 r1, -0x10
418: stxdw [r10 - 0x30], r1
420: lddw r1, 0x32558
430: stxdw [r10 - 0x8], r1
438: mov64 r1, r10
440: add64 r1, -0x59
448: stxdw [r10 - 0x10], r1
450: stdw [r10 - 0x20], 0x0
458: stdw [r10 - 0x38], 0x1
460: stdw [r10 - 0x28], 0x1
468: mov64 r7, r10
470: add64 r7, -0x58
478: mov64 r2, r10
480: add64 r2, -0x40
488: mov64 r1, r7
490: call 0x5b93
498: mov64 r1, r7
4a0: call 0x16a
4a8: mov64 r1, r0
4b0: and64 r1, 0x3
4b8: jne r1, 0x1, +0x5 <.text+0x3c8>
4c0: ldxdw r1, [r0 + 0x7]
4c8: ldxdw r2, [r1 + 0x0]
4d0: jeq r2, 0x0, +0x2 <.text+0x3c8>
4d8: ldxdw r1, [r0 - 0x1]
4e0: callx r2
4e8: stw [r6 + 0x0], 0x2
4f0: mov64 r1, 0x2
4f8: mov64 r2, 0x10
500: add64 r6, r2
508: stxb [r6 + 0x0], r1
510: exit
518: mov64 r6, r1
520: ldxdw r1, [r6 + 0xc8]
528: jeq r1, 0xc8, +0x1c <.text+0x4f0>
530: jgt r1, 0xc8, +0x1e <.text+0x508>
538: mov64 r8, r6
540: add64 r8, r1
548: mov64 r9, 0xc8
550: sub64 r9, r1
558: mov64 r7, r9
560: jlt r9, r3, +0x1 <.text+0x450>
568: mov64 r7, r3
570: mov64 r1, r8
578: stxdw [r10 - 0x8], r3
580: mov64 r3, r7
588: call 0x6540
590: ldxdw r1, [r10 - 0x8]
598: jge r9, r1, +0x2 <.text+0x490>
5a0: add64 r8, r7
5a8: stb [r8 - 0x1], 0x40
5b0: ldxdw r3, [r6 + 0xc8]
5b8: mov64 r1, r3
5c0: add64 r1, r7
5c8: mov64 r2, 0x1
5d0: jlt r1, r3, +0x1 <.text+0x4c0>
5d8: mov64 r2, 0x0
5e0: and64 r2, 0x1
5e8: jne r2, 0x0, +0xb <.text+0x528>
5f0: stxdw [r6 + 0xc8], r1
5f8: jgt r1, 0xc8, +0x1 <.text+0x4e8>
600: ja +0x2 <.text+0x4f8>
608: stdw [r6 + 0xc8], 0xc8
610: stb [r6 + 0xc7], 0x40
618: mov64 r0, r6
620: exit
628: mov64 r2, 0xc8
630: lddw r3, 0x379a0
640: call 0x60a1
648: lddw r1, 0x37988
658: call 0x635a
660: mov64 r6, r1
668: jeq r3, 0x163, +0x1 <.text+0x558>
670: jgt r3, 0x51, +0x3 <.text+0x570>
678: lddw r1, -0x7ffffffffffffffd
688: ja +0x34 <.text+0x710>
690: stxdw [r10 - 0x148], r3
698: mov64 r1, r10
6a0: add64 r1, -0x58
6a8: stxdw [r10 - 0x150], r2
6b0: mov64 r3, 0x52
6b8: call 0x5894
6c0: ldxw r9, [r10 - 0x58]
6c8: jeq r9, 0x2, +0x21 <.text+0x6b8>
6d0: ldxdw r1, [r10 - 0x48]
6d8: stxdw [r10 - 0x68], r1
6e0: ldxdw r1, [r10 - 0x40]
6e8: stxdw [r10 - 0x60], r1
6f0: ldxdw r1, [r10 - 0x38]
6f8: stxdw [r10 - 0x90], r1
700: ldxdw r1, [r10 - 0x30]
708: stxdw [r10 - 0x88], r1
710: ldxb r1, [r10 - 0x28]
718: stxb [r10 - 0x80], r1
720: ldxdw r1, [r10 - 0x26]
728: stxdw [r10 - 0xb8], r1
730: ldxdw r1, [r10 - 0x1e]
738: stxdw [r10 - 0xb0], r1
740: ldxdw r1, [r10 - 0x16]
748: stxdw [r10 - 0xa8], r1
750: ldxdw r1, [r10 - 0xe]
758: stxdw [r10 - 0xa0], r1
760: ldxdw r1, [r10 - 0x8]
768: stxdw [r10 - 0x9a], r1
770: ldxdw r4, [r10 - 0x50]
778: ldxw r5, [r10 - 0x54]
780: ldxb r7, [r10 - 0x27]
788: ldxdw r1, [r10 - 0x68]
790: stxdw [r10 - 0x78], r1
798: ldxdw r1, [r10 - 0x60]
7a0: stxdw [r10 - 0x70], r1
7a8: mov64 r1, r7
7b0: and64 r1, 0x1
7b8: jne r1, 0x0, +0x11 <.text+0x728>
7c0: lddw r1, -0x7ffffffffffffff7
7d0: ja +0x5 <.text+0x6e0>
7d8: ldxdw r1, [r10 - 0x48]
7e0: stxdw [r10 - 0xe0], r1
7e8: ldxdw r1, [r10 - 0x40]
7f0: stxdw [r10 - 0xd8], r1
7f8: ldxdw r1, [r10 - 0x50]
800: ldxdw r2, [r10 - 0xd8]
808: stxdw [r10 - 0xe8], r2
810: ldxdw r3, [r10 - 0xe0]
818: stxdw [r10 - 0xf0], r3
820: stxdw [r6 + 0x18], r2
828: stxdw [r6 + 0x10], r3
830: stxdw [r6 + 0x8], r1
838: stw [r6 + 0x0], 0x2
840: exit
848: ldxdw r1, [r10 - 0x148]
850: add64 r1, -0x52
858: ldxdw r2, [r10 - 0x70]
860: stxdw [r10 - 0xd8], r2
868: ldxdw r2, [r10 - 0x78]
870: stxdw [r10 - 0xe0], r2
878: ldxdw r2, [r10 - 0x90]
880: stxdw [r10 - 0xd0], r2
888: ldxdw r2, [r10 - 0x88]
890: stxdw [r10 - 0xc8], r2
898: ldxb r2, [r10 - 0x80]
8a0: stxb [r10 - 0xc0], r2
8a8: ldxdw r2, [r10 - 0xb8]
8b0: stxdw [r10 - 0x140], r2
8b8: ldxdw r2, [r10 - 0xb0]
8c0: stxdw [r10 - 0x138], r2
8c8: ldxdw r2, [r10 - 0xa8]
8d0: stxdw [r10 - 0x130], r2
8d8: ldxdw r2, [r10 - 0xa0]
8e0: stxdw [r10 - 0x128], r2
8e8: ldxdw r2, [r10 - 0x9a]
8f0: stxdw [r10 - 0x122], r2
8f8: ldxb r2, [r10 - 0xc0]
900: stxb [r10 - 0x108], r2
908: ldxdw r2, [r10 - 0xc8]
910: stxdw [r10 - 0x110], r2
918: ldxdw r2, [r10 - 0xd0]
920: stxdw [r10 - 0x118], r2
928: ldxdw r2, [r10 - 0xd8]
930: stxdw [r10 - 0xe8], r2
938: stxdw [r10 - 0xf8], r2
940: ldxdw r2, [r10 - 0xe0]
948: stxdw [r10 - 0xf0], r2
950: stxdw [r10 - 0x100], r2
958: mov64 r3, 0x0
960: mov64 r2, 0x1
968: jeq r1, 0x0, +0x35 <.text+0x9f8>
970: jlt r1, 0x55, +0x4f <.text+0xad0>
978: stxdw [r10 - 0x158], r4
980: lddw r1, 0x300000000
990: ldxdw r1, [r1 + 0x0]
998: mov64 r3, r1
9a0: add64 r3, -0x53
9a8: mov64 r2, 0x0
9b0: mov64 r4, 0x1
9b8: jgt r3, r1, +0x1 <.text+0x8a8>
9c0: mov64 r4, 0x0
9c8: stxdw [r10 - 0x168], r7
9d0: ldxdw r8, [r10 - 0x150]
9d8: jne r4, 0x0, +0x1 <.text+0x8c8>
9e0: mov64 r2, r3
9e8: lddw r7, 0x300007fad
9f8: jeq r1, 0x0, +0x1 <.text+0x8e8>
a00: mov64 r7, r2
a08: stxdw [r10 - 0x160], r5
a10: lddw r1, 0x300000007
a20: jgt r7, r1, +0x5 <.text+0x930>
a28: mov64 r1, 0x1
a30: mov64 r2, 0x53
a38: lddw r3, 0x379b8
a48: call 0x5ac5
a50: add64 r8, 0x52
a58: lddw r1, 0x300000000
a68: stxdw [r1 + 0x0], r7
a70: mov64 r1, r7
a78: mov64 r2, 0x0
a80: mov64 r3, 0x53
a88: call 0x64a4
a90: mov64 r1, r8
a98: mov64 r2, r7
aa0: mov64 r3, 0x53
aa8: call 0x64a5
ab0: lsh64 r0, 0x20
ab8: rsh64 r0, 0x20
ac0: ldxdw r3, [r10 - 0x148]
ac8: ldxdw r4, [r10 - 0x158]
ad0: ldxdw r5, [r10 - 0x160]
ad8: ldxdw r7, [r10 - 0x168]
ae0: jne r0, 0x0, +0x21 <.text+0xad0>
ae8: ldxdw r1, [r10 - 0x150]
af0: ldxb r1, [r1 + 0xa5]
af8: jne r1, 0x1, +0x1e <.text+0xad0>
b00: ldxdw r2, [r10 - 0x150]
b08: add64 r2, 0xa6
b10: add64 r3, -0xa6
b18: ldxdw r1, [r10 - 0xf8]
b20: stxdw [r6 + 0x18], r1
b28: ldxdw r1, [r10 - 0x100]
b30: stxdw [r6 + 0x10], r1
b38: ldxdw r1, [r10 - 0x118]
b40: stxdw [r6 + 0x20], r1
b48: ldxdw r1, [r10 - 0x110]
b50: stxdw [r6 + 0x28], r1
b58: ldxb r1, [r10 - 0x108]
b60: stxb [r6 + 0x30], r1
b68: ldxdw r1, [r10 - 0x128]
b70: stxdw [r6 + 0x4a], r1
b78: ldxdw r1, [r10 - 0x130]
b80: stxdw [r6 + 0x42], r1
b88: ldxdw r1, [r10 - 0x138]
b90: stxdw [r6 + 0x3a], r1
b98: ldxdw r1, [r10 - 0x140]
ba0: stxdw [r6 + 0x32], r1
ba8: ldxdw r1, [r10 - 0x122]
bb0: stxw [r6 + 0x0], r9
bb8: stxw [r6 + 0x4], r5
bc0: stxb [r6 + 0x31], r7
bc8: stxdw [r6 + 0x60], r3
bd0: stxdw [r6 + 0x58], r2
bd8: stxdw [r6 + 0x8], r4
be0: stxdw [r6 + 0x50], r1
be8: ja -0x76 <.text+0x720>
bf0: lddw r1, -0x7ffffffffffffffd
c00: stxdw [r6 + 0x8], r1
c08: stdw [r6 + 0x18], 0x54
c10: stdw [r6 + 0x10], 0x53
c18: ja -0x7d <.text+0x718>
c20: ldxdw r3, [r2 + 0x0]
c28: ldxdw r4, [r3 + 0x50]
c30: jne r4, 0x52, +0x13 <.text+0xbb0>
c38: ldxdw r4, [r3 + 0x28]
c40: lddw r5, -0x6c5e9a281e0922fa
c50: jne r4, r5, +0xc <.text+0xb98>
c58: ldxdw r4, [r3 + 0x30]
c60: lddw r5, -0x53861431b91e3427
c70: jne r4, r5, +0x8 <.text+0xb98>
c78: ldxdw r4, [r3 + 0x38]
c80: lddw r5, -0x6ec8a4a0127a4be4
c90: jne r4, r5, +0x4 <.text+0xb98>
c98: ldxdw r3, [r3 + 0x40]
ca0: lddw r4, -0x56ff00817a0a73c6
cb0: jeq r3, r4, +0x6 <.text+0xbc8>
cb8: stdw [r1 + 0x0], 0x0
cc0: stw [r1 + 0x8], 0x16
cc8: ja +0x2 <.text+0xbc0>
cd0: stdw [r1 + 0x0], 0x0
cd8: stw [r1 + 0x8], 0x3
ce0: exit
ce8: mov64 r6, r1
cf0: mov64 r1, r10
cf8: add64 r1, -0x20
d00: call 0x5963
d08: ldxdw r1, [r10 - 0x20]
d10: jeq r1, 0x0, +0x6 <.text+0xc28>
d18: ldxdw r2, [r10 - 0x10]
d20: ldxb r3, [r10 - 0x8]
d28: stxb [r6 + 0x10], r3
d30: stxdw [r6 + 0x8], r2
d38: stxdw [r6 + 0x0], r1
d40: ja -0xd <.text+0xbc0>
d48: ldxdw r1, [r10 - 0x18]
d50: stxdw [r6 + 0x8], r1
d58: stdw [r6 + 0x0], 0x0
d60: ja -0x11 <.text+0xbc0>
d68: ldxdw r3, [r2 + 0x0]
d70: ldxdw r4, [r3 + 0x50]
d78: jne r4, 0xa5, +0x10 <.text+0xce0>
d80: ldxdw r4, [r3 + 0x28]
d88: lddw r5, -0x6c5e9a281e0922fa
d98: jne r4, r5, +0xc <.text+0xce0>
da0: ldxdw r4, [r3 + 0x30]
da8: lddw r5, -0x53861431b91e3427
db8: jne r4, r5, +0x8 <.text+0xce0>
dc0: ldxdw r4, [r3 + 0x38]
dc8: lddw r5, -0x6ec8a4a0127a4be4
dd8: jne r4, r5, +0x4 <.text+0xce0>
de0: ldxdw r3, [r3 + 0x40]
de8: lddw r4, -0x56ff00817a0a73c6
df8: jeq r3, r4, +0x3 <.text+0xcf8>
e00: stdw [r1 + 0x0], 0x0
e08: stw [r1 + 0x8], 0x3
e10: exit
e18: mov64 r6, r1
e20: mov64 r1, r10
e28: add64 r1, -0x20
e30: call 0x593d
e38: ldxdw r1, [r10 - 0x20]
e40: jeq r1, 0x0, +0x6 <.text+0xd58>
e48: ldxdw r2, [r10 - 0x10]
e50: ldxb r3, [r10 - 0x8]
e58: stxb [r6 + 0x10], r3
e60: stxdw [r6 + 0x8], r2
e68: stxdw [r6 + 0x0], r1
e70: ja -0xd <.text+0xcf0>
e78: ldxdw r1, [r10 - 0x18]
e80: stxdw [r6 + 0x8], r1
e88: stdw [r6 + 0x0], 0x0
e90: ja -0x11 <.text+0xcf0>
e98: ldxdw r3, [r2 + 0x0]
ea0: ldxdw r4, [r3 + 0x50]
ea8: jlt r4, 0xa5, +0x10 <.text+0xe10>
eb0: ldxdw r4, [r3 + 0x28]
eb8: lddw r5, -0x21708a111e0922fa
ec8: jne r4, r5, +0xc <.text+0xe10>
ed0: ldxdw r4, [r3 + 0x30]
ed8: lddw r5, -0x2532931b43a2bde8
ee8: jne r4, r5, +0x8 <.text+0xe10>
ef0: ldxdw r4, [r3 + 0x38]
ef8: lddw r5, 0x270db9834dfc1ab6
f08: jne r4, r5, +0x4 <.text+0xe10>
f10: ldxdw r3, [r3 + 0x40]
f18: lddw r4, -0x3745e27d7064202
f28: jeq r3, r4, +0x3 <.text+0xe28>
f30: stdw [r1 + 0x0], 0x0
f38: stw [r1 + 0x8], 0x3
f40: exit
f48: mov64 r6, r1
f50: mov64 r1, r10
f58: add64 r1, -0x20
f60: call 0x5917
f68: ldxdw r1, [r10 - 0x20]
f70: jeq r1, 0x0, +0xc <.text+0xeb8>
f78: ldxdw r2, [r10 - 0x18]
f80: jgt r2, 0xa4, +0x4 <.text+0xe88>
f88: mov64 r1, 0xa5
f90: lddw r3, 0x379d0
fa0: call 0x5f76
fa8: ldxb r2, [r10 - 0x8]
fb0: ldxdw r3, [r10 - 0x10]
fb8: stxb [r6 + 0x10], r2
fc0: stxdw [r6 + 0x8], r3
fc8: stxdw [r6 + 0x0], r1
fd0: ja -0x13 <.text+0xe20>
fd8: ldxdw r1, [r10 - 0x18]
fe0: stxdw [r6 + 0x8], r1
fe8: stdw [r6 + 0x0], 0x0
ff0: ja -0x17 <.text+0xe20>
ff8: lddw r2, 0x300000000
1008: ldxdw r3, [r2 + 0x0]
1010: mov64 r2, r3
1018: add64 r2, -0x18
1020: mov64 r4, 0x0
1028: mov64 r5, 0x1
1030: jgt r2, r3, +0x1 <.text+0xf20>
1038: mov64 r5, 0x0
1040: jne r5, 0x0, +0x1 <.text+0xf30>
1048: mov64 r4, r2
1050: lddw r2, 0x300007fe8
1060: jeq r3, 0x0, +0x2 <.text+0xf58>
1068: and64 r4, -0x8
1070: mov64 r2, r4
1078: lddw r3, 0x300000007
1088: jgt r2, r3, +0x3 <.text+0xf88>
1090: mov64 r1, 0x8
1098: mov64 r2, 0x18
10a0: call 0x59fe
10a8: lddw r3, 0x300000000
10b8: stxdw [r3 + 0x0], r2
10c0: ldxdw r3, [r1 + 0x10]
10c8: stxdw [r2 + 0x10], r3
10d0: ldxdw r3, [r1 + 0x8]
10d8: stxdw [r2 + 0x8], r3
10e0: ldxdw r1, [r1 + 0x0]
10e8: stxdw [r2 + 0x0], r1
10f0: mov64 r1, 0x14
10f8: lddw r3, 0x37a48
1108: call 0x591c
1110: exit
1118: lddw r1, 0x300000000
1128: ldxdw r2, [r1 + 0x0]
1130: mov64 r1, r2
1138: add64 r1, -0x12
1140: mov64 r3, 0x0
1148: mov64 r4, 0x1
1150: jgt r1, r2, +0x1 <.text+0x1040>
1158: mov64 r4, 0x0
1160: jne r4, 0x0, +0x1 <.text+0x1050>
1168: mov64 r3, r1
1170: lddw r1, 0x300007fee
1180: jeq r2, 0x0, +0x1 <.text+0x1070>
1188: mov64 r1, r3
1190: lddw r2, 0x300000007
11a0: jgt r1, r2, +0x5 <.text+0x10b0>
11a8: mov64 r1, 0x1
11b0: mov64 r2, 0x12
11b8: lddw r3, 0x37a00
11c8: call 0x59d5
11d0: lddw r2, 0x300000000
11e0: stxdw [r2 + 0x0], r1
11e8: lddw r3, 0x6572207365747962
11f8: stxdw [r1 + 0x8], r3
1200: lddw r3, 0x206c6c6120746f4e
1210: stxdw [r1 + 0x0], r3
1218: sth [r1 + 0x10], 0x6461
1220: ldxdw r3, [r2 + 0x0]
1228: mov64 r2, r3
1230: add64 r2, -0x18
1238: mov64 r4, 0x0
1240: mov64 r5, 0x1
1248: jgt r2, r3, +0x1 <.text+0x1138>
1250: mov64 r5, 0x0
1258: jne r5, 0x0, +0x1 <.text+0x1148>
1260: mov64 r4, r2
1268: lddw r2, 0x300007fe8
1278: jeq r3, 0x0, +0x2 <.text+0x1170>
1280: and64 r4, -0x8
1288: mov64 r2, r4
1290: lddw r3, 0x300000007
12a0: jgt r2, r3, +0x3 <.text+0x11a0>
12a8: mov64 r1, 0x8
12b0: mov64 r2, 0x18
12b8: call 0x59bb
12c0: lddw r3, 0x300000000
12d0: stxdw [r3 + 0x0], r2
12d8: stxdw [r2 + 0x8], r1
12e0: stdw [r2 + 0x10], 0x12
12e8: stdw [r2 + 0x0], 0x12
12f0: mov64 r1, 0x15
12f8: lddw r3, 0x37a48
1308: call 0x58dc
1310: exit
1318: mov64 r3, r2
1320: ldxdw r2, [r1 + 0x8]
1328: ldxdw r1, [r1 + 0x0]
1330: call 0x5eaa
1338: exit
1340: mov64 r7, r2
1348: mov64 r6, r1
1350: mov64 r1, r7
1358: lsh64 r1, 0x20
1360: rsh64 r1, 0x20
1368: jeq r1, 0x0, +0x89 <.text+0x1698>
1370: mov64 r3, 0x0
1378: mov64 r2, 0x1
1380: mov64 r1, r7
1388: and64 r1, 0x1
1390: jeq r1, 0x0, +0x9 <.text+0x12c0>
1398: mov64 r2, 0xa
13a0: mov64 r1, r7
13a8: lsh64 r1, 0x20
13b0: rsh64 r1, 0x20
13b8: jeq r1, 0x1, +0x1 <.text+0x12a8>
13c0: ja +0x3 <.text+0x12c0>
13c8: stxdw [r6 + 0x8], r2
13d0: stxdw [r6 + 0x10], r3
13d8: ja +0x7d <.text+0x16a8>
13e0: mov64 r1, r7
13e8: and64 r1, 0x2
13f0: jeq r1, 0x0, +0xc <.text+0x1338>
13f8: mov64 r1, r10
1400: add64 r1, -0x10
1408: mov64 r4, 0x64
1410: mov64 r5, 0x0
1418: call 0x68f1
1420: lddw r1, 0xfffffffe
1430: mov64 r4, r7
1438: and64 r4, r1
1440: ldxdw r3, [r10 - 0x8]
1448: ldxdw r2, [r10 - 0x10]
1450: jeq r4, 0x2, -0x12 <.text+0x12a8>
1458: mov64 r1, r7
1460: and64 r1, 0x4
1468: jeq r1, 0x0, +0xc <.text+0x13b0>
1470: mov64 r1, r10
1478: add64 r1, -0x20
1480: mov64 r4, 0x2710
1488: mov64 r5, 0x0
1490: call 0x68e2
1498: lddw r1, 0xfffffffc
14a8: mov64 r4, r7
14b0: and64 r4, r1
14b8: ldxdw r3, [r10 - 0x18]
14c0: ldxdw r2, [r10 - 0x20]
14c8: jeq r4, 0x4, -0x21 <.text+0x12a8>
14d0: mov64 r1, r7
14d8: and64 r1, 0x8
14e0: jeq r1, 0x0, +0xc <.text+0x1428>
14e8: mov64 r1, r10
14f0: add64 r1, -0x30
14f8: mov64 r4, 0x5f5e100
1500: mov64 r5, 0x0
1508: call 0x68d3
1510: lddw r1, 0xfffffff8
1520: mov64 r4, r7
1528: and64 r4, r1
1530: ldxdw r3, [r10 - 0x28]
1538: ldxdw r2, [r10 - 0x30]
1540: jeq r4, 0x8, -0x30 <.text+0x12a8>
1548: mov64 r1, r7
1550: and64 r1, 0x10
1558: jeq r1, 0x0, +0xd <.text+0x14a8>
1560: mov64 r1, r10
1568: add64 r1, -0x40
1570: lddw r4, 0x2386f26fc10000
1580: mov64 r5, 0x0
1588: call 0x68c3
1590: lddw r1, 0xfffffff0
15a0: mov64 r4, r7
15a8: and64 r4, r1
15b0: ldxdw r3, [r10 - 0x38]
15b8: ldxdw r2, [r10 - 0x40]
15c0: jeq r4, 0x10, -0x40 <.text+0x12a8>
15c8: mov64 r1, r7
15d0: and64 r1, 0x20
15d8: jeq r1, 0x0, +0x39 <.text+0x1688>
15e0: mov64 r1, r10
15e8: add64 r1, -0x70
15f0: mov64 r8, r2
15f8: mov64 r9, r3
1600: mov64 r3, 0x0
1608: lddw r4, 0x4ee2d6d415b
1618: mov64 r5, 0x0
1620: call 0x68b0
1628: mov64 r1, r10
1630: add64 r1, -0x60
1638: stxdw [r10 - 0x78], r9
1640: mov64 r2, r9
1648: mov64 r3, 0x0
1650: lddw r4, -0x7a53107f00000000
1660: mov64 r5, 0x0
1668: call 0x68a7
1670: mov64 r1, r10
1678: add64 r1, -0x50
1680: mov64 r2, r8
1688: mov64 r3, 0x0
1690: lddw r4, -0x7a53107f00000000
16a0: mov64 r5, 0x0
16a8: call 0x689f
16b0: mov64 r1, 0x1
16b8: ldxdw r3, [r10 - 0x68]
16c0: mov64 r2, 0x1
16c8: jne r3, 0x0, +0x1 <.text+0x15b8>
16d0: mov64 r2, 0x0
16d8: ldxdw r3, [r10 - 0x70]
16e0: ldxdw r5, [r10 - 0x60]
16e8: mov64 r4, 0x1
16f0: ldxdw r0, [r10 - 0x78]
16f8: jne r0, 0x0, +0x1 <.text+0x15e8>
1700: mov64 r4, 0x0
1708: add64 r5, r3
1710: ldxdw r3, [r10 - 0x58]
1718: mov64 r0, 0x1
1720: jne r3, 0x0, +0x1 <.text+0x1610>
1728: mov64 r0, 0x0
1730: ldxdw r8, [r10 - 0x48]
1738: mov64 r3, r8
1740: add64 r3, r5
1748: jlt r3, r8, +0x1 <.text+0x1638>
1750: mov64 r1, 0x0
1758: or64 r4, r0
1760: or64 r4, r2
1768: or64 r4, r1
1770: and64 r4, 0x1
1778: jne r4, 0x0, +0x5 <.text+0x1688>
1780: ldxdw r2, [r10 - 0x50]
1788: lddw r1, 0xffffffe0
1798: and64 r7, r1
17a0: jeq r7, 0x20, -0x7c <.text+0x12a8>
17a8: stdw [r6 + 0x0], 0x0
17b0: ja +0x3 <.text+0x16b0>
17b8: stdw [r6 + 0x10], 0x0
17c0: stdw [r6 + 0x8], 0x1
17c8: stdw [r6 + 0x0], 0x1
17d0: exit
17d8: and64 r2, 0xff
17e0: jgt r2, 0x7, +0xb <.text+0x1720>
17e8: mov64 r3, 0x1
17f0: lsh64 r3, r2
17f8: and64 r3, 0xff
1800: ldxb r2, [r1 + 0x0]
1808: add64 r2, r3
1810: mov64 r3, r2
1818: and64 r3, 0xff
1820: jne r3, r2, +0x0 <.text+0x1708>
1828: jne r3, r2, +0x5 <.text+0x1738>
1830: stxb [r1 + 0x0], r2
1838: exit
1840: lddw r1, 0x37ad0
1850: call 0x613c
1858: lddw r1, 0x37ae8
1868: call 0x6118
1870: ldxdw r2, [r1 + 0x0]
1878: jne r2, 0x0, +0x1 <.text+0x1768>
1880: exit
1888: ldxb r2, [r1 + 0x10]
1890: jgt r2, 0x7, +0xc <.text+0x17d8>
1898: ldxdw r1, [r1 + 0x8]
18a0: mov64 r3, 0x1
18a8: lsh64 r3, r2
18b0: and64 r3, 0xff
18b8: ldxb r2, [r1 + 0x0]
谁有时间读完所有这些?
对此的初步尝试可能看起来像这样:

你可能会惊讶于它已经完成了多少工作。如果再加上"代码必须是可编译 Rust"的约束,Agent 非常愿意从之前的二进制混乱中理出一些头绪。
从一些基本的 harness engineering 开始,在测试中我们发现,Claude Opus 5.5 作为 orchestrator 效果最好,它会生成 Astra subagents 来执行更小、更精准的逆向工程任务。
问题在于,单个 Agent 在如此巨大的二进制中很容易迷失:我们需要拆分工作,同时保持目标一致且可衡量。
分而治之
程序中每个函数的编译是相当独立的:在使用 Rust 时,调用者和被调用者相互影响的唯一方式是通过内联和 LLVM 的过程间 pass,比如 IPSCCP(常量和值范围传播)、死参数消除、-O3 下的参数提升、属性推断,这些影响相对可以忽略不计。
特别是在 GoonFi 上,200 多个函数中似乎只有 11 个被这些优化改变。
这意味着逐函数生成匹配的字节码能让我们非常接近目标。我们所需要的只是每个函数的源到二进制映射。
#[inline(always)]
fn execution_price_deviation(
limit: u64,
bid_decimals: u8,
amount_in: u64,
amount_out: u64,
oracle_price: u64,
is_user_bid: bool,
) -> bool {
if limit == 0 {
return false;
}
let base_unit = match 10u128.checked_pow(u32::from(bid_decimals)) {
Some(unit) => unit,
None => return true,
};
let (exec_num, exec_denom): (u128, u128) = if is_user_bid {
let num = match u128::from(amount_in).checked_mul(base_unit) {
Some(num) => num,
None => return true,
};
(num, u128::from(amount_out))
} else {
let num = match u128::from(amount_out).checked_mul(base_unit) {
Some(num) => num,
None => return true,
};
(num, u128::from(amount_in))
};
let oracle_times_denom = match u128::from(oracle_price).checked_mul(exec_denom) {
Some(value) => value,
None => return true,
};
let difference = if exec_num > oracle_times_denom {
exec_num.saturating_sub(oracle_times_denom)
} else {
oracle_times_denom.saturating_sub(exec_num)
};
let lhs = match difference.checked_mul(1_000_000) {
Some(value) => value,
None => return true,
};
let rhs = match u128::from(limit)
.checked_mul(u128::from(oracle_price))
.and_then(|value| value.checked_mul(exec_denom))
{
Some(value) => value,
None => return true,
};
lhs > rhs
}
这是 execution_price_deviation 的重建结果:每条指令都与目标对齐,因此得分为 0。
对于每个函数,我们通过将目标函数的指令与编译后的函数指令对齐并计算差异来计算得分:每条缺失或多出的指令 100 分,顺序错误 60 分,每个错误寄存器或立即数 5 分,栈偏移每错一个字节 1 分。
零分意味着完美匹配。任何错误都会增加给定反编译函数的代价。
另一方面,要连接到 Agents 的工具、反馈循环和 hooks,需要更多思考。
完成任务所需的工具
要反编译,你需要一个反编译器。这不是废话吗?
请查看我们之前的文章和这里使用的设置,以了解如何将其连接到 Agent:
renato-osec/patina-research
拥有能够把跨函数、需要大量上下文的复杂任务化简的工具也很重要。例如,我们可能想知道每条指令使用了哪些 PDA,以便把访问它们的不同函数分组。或者更重要的是,我们需要一种方法来知道程序中实际存在哪些指令。
这可以通过使用 Binary Ninja 和我们自己的 bn-ebpf-solana 这样的工具,配合一些静态分析和脚本来完成,得到类似这样的结果。
一般来说,除了把编译器作为确定性反馈源进行基础评分之外,我们还依赖 ad hoc 工具来获得超出二进制本身所能提供的速度和细节,例如查询链上信息或获取复杂的聚合数据,比如 rust crate 函数签名。为此我们用了一些现有工具,比如 Alchemy Solana Account Archive 和 Solrays。
作弊、过拟合、大量作弊
很好,现在我们有了一个让 Agent 可以运作的闭环,以及一些辅助工具。能出什么问题呢?
事实证明,问题很多:当我们将二进制相似度定义为循环的唯一目标时,我们只会得到一堆像这样过拟合的垃圾:
/// 根据已验证的曲线创建 market PDA 及其两个 token vault。
#[no_mangle]
pub fn create_market(
payer: &AccountInfo,
market: &AccountInfo,
bid_mint: &AccountInfo,
ask_mint: &AccountInfo,
bid_vault: &AccountInfo,
ask_vault: &AccountInfo,
oracle: &AccountInfo,
bid_program: &AccountInfo,
ask_program: &AccountInfo,
parameters: std::boxed::Box<InitializeMarketParameters>,
) -> ProgramResult {
use crate::{
extension::{self, AccountMeta, Instruction, Seed},
state::token,
utils::cpi::create_or_allocate_account,
};
use solana_program::sysvar::Sysvar;
let class_tiers = [
parameters.class_0,
parameters.class_1,
parameters.class_2,
parameters.class_3,
parameters.class_4,
parameters.class_5,
parameters.class_6,
parameters.class_7,
parameters.class_8,
parameters.class_9,
parameters.class_10,
parameters.class_11,
parameters.class_12,
parameters.class_13,
parameters.class_14,
parameters.class_15,
];
if class_tiers.iter().any(|&tier| tier > 6) {
return Err(ProgramError::Custom(13));
}
let initial_tiers = [parameters.initial_tier; 7];
let inventory_limit = parameters.inventory_limit;
let near_bid_denominator = parameters.near_bid_denominator;
let near_ask_denominator = parameters.near_ask_denominator;
let inventory_knee = parameters.inventory_knee;
let far_bid_denominator = parameters.far_bid_denominator;
let far_ask_denominator = parameters.far_ask_denominator;
let minimum_quote_threshold = parameters.minimum_quote_threshold;
let maximum_quote_age = parameters.maximum_quote_age;
let grace_slots = parameters.grace_slots;
let bid_rate_millionths = parameters.bid_rate_millionths;
let ask_rate_millionths = parameters.ask_rate_millionths;
let smoothing_period = parameters.smoothing_period;
let smoothing_interval = parameters.smoothing_interval;
let smoothing_mode = parameters.smoothing_mode;
let disable_staleness_adjustment = parameters.disable_staleness_adjustment;
let quote_flags = parameters.quote_flags;
let restart_slot = solana_program::sysvar::last_restart_slot::LastRestartSlot::get()
.map_err(|_| ProgramError::UnsupportedSysvar)?
.last_restart_slot;
let mut settings_words = [0u64; 217];
let settings_bytes = bytemuck::bytes_of_mut(&mut settings_words);
{
// PricingTier 仅包含字节数据,没有填充。
let tier_bytes = unsafe {
core::slice::from_raw_parts(initial_tiers.as_ptr().cast::<u8>(), 0x4d0)
};
settings_bytes[..0x4d0].copy_from_slice(tier_bytes);
settings_bytes[0x4d0..0x520].copy_from_slice(bytemuck::bytes_of(¶meters.bid_thresholds));
settings_bytes[0x520..0x570].copy_from_slice(bytemuck::bytes_of(¶meters.ask_thresholds));
settings_bytes[0x5a8] = maximum_quote_age;
settings_bytes[0x5a9] = grace_slots.min(50);
settings_bytes[0x5aa] = parameters.class_0;
settings_bytes[0x5ab] = parameters.class_1;
settings_bytes[0x5ac] = parameters.class_2;
settings_bytes[0x5ad] = parameters.class_3;
settings_bytes[0x5ae] = parameters.class_4;
settings_bytes[0x5af] = parameters.class_5;
settings_bytes[0x5b0] = parameters.class_6;
settings_bytes[0x5b1] = parameters.class_7;
settings_bytes[0x5b2] = parameters.class_8;
settings_bytes[0x5b3] = parameters.class_9;
settings_bytes[0x5b4] = parameters.class_10;
settings_bytes[0x5b5] = parameters.class_11;
settings_bytes[0x5b6] = parameters.class_12;
settings_bytes[0x5b7] = parameters.class_13;
settings_bytes[0x5d8] = parameters.class_14;
settings_bytes[0x5d9] = parameters.class_15;
settings_bytes[0x610] = smoothing_mode;
settings_bytes[0x611] = disable_staleness_adjustment;
settings_bytes[0x612] = quote_flags;
}
settings_bytes[0x570..0x578].copy_from_slice(&minimum_quote_threshold.to_le_bytes());
settings_bytes[0x578..0x580].copy_from_slice(&near_bid_denominator.to_le_bytes());
settings_bytes[0x580..0x588].copy_from_slice(&near_ask_denominator.to_le_bytes());
settings_bytes[0x588..0x590].copy_from_slice(&inventory_knee.to_le_bytes());
settings_bytes[0x590..0x598].copy_from_slice(&far_bid_denominator.to_le_bytes());
settings_bytes[0x598..0x5a0].copy_from_slice(&far_ask_denominator.to_le_bytes());
settings_bytes[0x5a0..0x5a8].copy_from_slice(&inventory_limit.to_le_bytes());
settings_bytes[0x5b8..0x5c0].copy_from_slice(&bid_rate_millionths.to_le_bytes());
settings_bytes[0x5d0..0x5d8].copy_from_slice(&ask_rate_millionths.to_le_bytes());
settings_bytes[0x5e8..0x5f0].copy_from_slice(&smoothing_period.to_le_bytes());
settings_bytes[0x5f0..0x5f8].copy_from_slice(&smoothing_interval.to_le_bytes());
settings_bytes[0x630..0x638].copy_from_slice(&restart_slot.to_le_bytes());
validate_initial_market_parameters(¶meters, &settings_words)?;
if pubkey::equals(bid_mint.key(), ask_mint.key()) {
return Err(ProgramError::Custom(2));
}
let bid_decimals = if pubkey::equals(bid_program.key(), &pubkey::TOKEN_PROGRAM_ID) {
if bid_mint.data_len() != token::Mint::LEN
|| !bid_mint.is_owned_by(&pubkey::TOKEN_PROGRAM_ID)
{
return Err(ProgramError::InvalidAccountData);
}
let data = bid_mint
.try_borrow_data()
.map_err(|_| ProgramError::InvalidAccountData)?;
data[44]
} else if pubkey::equals(bid_program.key(), &pubkey::TOKEN_2022_PROGRAM_ID) {
let data = bid_mint
.try_borrow_data()
.map_err(|_| ProgramError::InvalidAccountData)?;
spl_token_2022::extension::StateWithExtensions::<spl_token_2022::state::Mint>::unpack(&data)
.map_err(|_| ProgramError::InvalidAccountData)?
.base
.decimals
} else {
return Err(ProgramError::IncorrectProgramId);
};
let ask_decimals = if pubkey::equals(ask_program.key(), &pubkey::TOKEN_PROGRAM_ID) {
token::try_borrow_mint(ask_mint)
.map_err(|_| ProgramError::InvalidAccountData)?
.decimals()
} else if pubkey::equals(ask_program.key(), &pubkey::TOKEN_2022_PROGRAM_ID) {
let data = ask_mint
.try_borrow_data()
.map_err(|_| ProgramError::InvalidAccountData)?;
spl_token_2022::extension::StateWithExtensions::<spl_token_2022::state::Mint>::unpack(&data)
.map_err(|_| ProgramError::InvalidAccountData)?
.base
.decimals
} else {
return Err(ProgramError::IncorrectProgramId);
};
if bid_decimals != parameters.bid_decimals || ask_decimals != parameters.ask_decimals {
return Err(ProgramError::Custom(1));
}
let market_id = parameters.market_id.to_le_bytes();
let market_bump = [parameters.market_bump];
let market_seeds = [
Seed::from(&b"market_v2"[..]),
Seed::from(bid_mint.key()),
Seed::from(ask_mint.key()),
Seed::from(&market_id),
Seed::from(&market_bump),
];
create_or_allocate_account(market, &pubkey::ID, 0x800, &market_seeds, payer, None)?;
{
let mut data = market.try_borrow_mut_data()?;
data[..8].copy_from_slice(&0x9a325834352fbc30u64.to_le_bytes());
let state = MarketConfigurationState::market_account_from_bytes(&mut data[8..0x800]);
let body = bytemuck::bytes_of_mut(state);
let keys = bytemuck::from_bytes_mut::<[Pubkey; 6]>(&mut body[0x28..0xe8]);
keys[0] = *payer.key();
keys[1] = *bid_mint.key();
keys[2] = *ask_mint.key();
keys[3] = *bid_vault.key();
keys[4] = *ask_vault.key();
keys[5] = *oracle.key();
body[0x7b5] = ask_decimals;
body[0x7b4] = bid_decimals;
body[0xe8..0x7b0].copy_from_slice(bytemuck::bytes_of(&settings_words));
body[0x7b0] = parameters.market_bump;
body[..8].copy_from_slice(&market_id);
body[0x7b1] = parameters.ask_vault_bump;
body[0x7b2] = parameters.bid_vault_bump;
body[0x7b6] = 1;
body[0x7b8..0x7d8].copy_from_slice(bid_program.key());
body[0x7d8..0x7f8].copy_from_slice(ask_program.key());
}
let bid_space = token::token_account_space(bid_mint, bid_program.key())? as u64;
let bid_bump = [parameters.bid_vault_bump];
let bid_seeds = [
Seed::from(&b"vault"[..]),
Seed::from(market.key()),
Seed::from(bid_mint.key()),
Seed::from(&bid_bump),
];
create_or_allocate_account(
bid_vault,
bid_program.key(),
bid_space,
&bid_seeds,
payer,
None,
)?;
let mut initialize_data = [core::mem::MaybeUninit::<u8>::uninit(); 33];
{
let bid_metas = [
AccountMeta {
pubkey: bid_vault.key(),
is_writable: true,
is_signer: false,
},
AccountMeta {
pubkey: bid_mint.key(),
is_writable: false,
is_signer: false,
},
];
write_initialize_bytes(&mut initialize_data, &[18]);
write_initialize_owner!(initialize_data, market.key());
let bid_initialize_data = unsafe {
core::slice::from_raw_parts(initialize_data.as_ptr().cast::<u8>(), 33)
};
extension::invoke_signed(
&Instruction {
program_id: bid_program.key(),
data: bid_initialize_data,
accounts: &bid_metas,
},
&[bid_vault, bid_mint],
&[],
)?;
}
let ask_space = token::token_account_space(ask_mint, ask_program.key())? as u64;
let ask_bump = [parameters.ask_vault_bump];
let ask_seeds = [
Seed::from(&b"vault"[..]),
Seed::from(market.key()),
Seed::from(ask_mint.key()),
Seed::from(&ask_bump),
];
create_or_allocate_account(
ask_vault,
ask_program.key(),
ask_space,
&ask_seeds,
payer,
None,
)?;
let ask_metas = [
AccountMeta {
pubkey: ask_vault.key(),
is_writable: true,
is_signer: false,
},
AccountMeta {
pubkey: ask_mint.key(),
is_writable: false,
is_signer: false,
},
];
write_initialize_bytes(&mut initialize_data, &[18]);
write_initialize_owner!(initialize_data, market.key());
let ask_initialize_data = unsafe {
core::slice::from_raw_parts(initialize_data.as_ptr().cast::<u8>(), 33)
};
extension::invoke_signed(
&Instruction {
program_id: ask_program.key(),
data: ask_initialize_data,
accounts: &ask_metas,
},
&[ask_vault, ask_mint],
&[],
)
}
仅针对得分进行优化,你会得到过拟合的垃圾:一半的指令仍然对不上,而且源码读起来更像 C 而不是 Rust。
不仅如此,如果我们把这看作一个对抗场景,其中 AI 只是最大化自己的得分,我们可以相当合乎逻辑地想象,这里的最佳策略是一个非常 trivial 的策略:
// “奶酪”:手工伪造精确的字节,忽略它们的含义
#[no_mangle]
unsafe extern "C" fn create_market(/* ... */) -> u64 {
core::arch::asm!(
"stxdw [r10 - 0xdc0], r3",
"stxdw [r10 - 0xd50], r1",
"mov64 r1, 0",
"stxdw [r10 - 0xd48], r1",
"mov64 r9, 0xd",
// ... 1,254 more instructions, copied verbatim from the target ...
"exit",
options(noreturn),
)
}
完美无瑕,但不太有用。
需要说明的是,实际上没有 Agent 这样做,但如果我是这里的 Agent,我就会这么做……
这正是过去几年困扰这个问题的细微之处。在 Accelerate 2025 上,我们展示了早期的一次尝试遇到过同样的问题。

ChatGPT 4o 的美好旧时光。
那么,我们最终是如何防止作弊的?我们的第一个实验直接禁止了用于作弊的构造(在编译时使用 LLVM pass 插件),并大力抑制任何损害可读性的行为:
- 任何不自然的模式,比如
asm!、black_box、volatile 写入、transmute、类型转换之类的……虽然这些可能出现在程序的源码中,但它们极其罕见,而且通常不是正确的选择。
- 内部 helper 上的
#[no_mangle]。导出函数会阻止 LLVM 对其进行内联或特化,因此其函数体会作为独立函数保留下来,从而获得高分。但 Solana 程序通常只导出两个符号:entrypoint 和 custom_panic。
- 最后,对评分做一点修正:我们在最终源码计算中显式匹配并排除库函数,因为这些函数需要通过
Cargo.toml 指定,而不能通过源码重新定义来免费获得额外分数。
#[repr(C)],每当 Agent 必须符合诸如我们的 工具 中实现的确定性检查时,它经常被滥用。
这带来了更好的类型以及库函数和类型召回,这可能是处理 Rust 二进制最难的部分,也是作为副作用改善长期二进制保真度的最佳方法之一。
一个 judge 统领一切
Agents 仍然会很快迷失并重复彼此的工作,因此我们不是单纯独立并行,而是将程序拆分:一个 architect Agent 在一些静态分析的帮助下,按程序指令映射出函数,然后每个 Worker 获得自己的分支和调用图的一部分,从不匹配任何库函数的叶子节点开始。
这里一个非常好的启发式方法是按顶层指令拆分 Agents,这些指令很容易从指令数据第一个字节上的分支识别出来。
任何内容都必须通过 Judge 才能加入视图。Judge 结合了前述工具和评分:每次提交时,我们用原始工具链重新构建整个程序,并重新给每个函数评分。只有当总分上升、没有函数变差、且 diff 中没有出现作弊时,提交才会被合并。
Judge 的第一阶段只是一个确定性脚本,没有任何 LLM 参与。
反编译中更细微的方面留给一个 Reviewer Agent 来阅读每次合并,以及一个 Global Orchestrator,它旨在指出代码中的边缘情况和怪异之处,而这些在 GoonFi 中确实存在。
一个例子:GoonFi 运行一个除了燃烧 compute units 之外什么都不做的循环,而且只针对不在其白名单上的 signer。
// 定义在 swap_with_route.rs 中:
pub(crate) static COMPUTE_EXEMPT_SIGNERS: [Pubkey; 2] = [
PERMITTED_ROUTE_AUTHORITIES[3],
PERMITTED_ROUTE_AUTHORITIES[4],
];
if !COMPUTE_EXEMPT_SIGNERS.contains(user.key()) {
let mut i = 0u64;
while i < 7000 {
core::hint::black_box(i);
i += 1;
}
}
这显然是故意的,然而从二进制来看这一点根本不明显。但这正是当人类或 Agent 真正阅读重建代码时会立刻注意到的那类东西。
然后,我们还有 prologue 之谜。GoonFi 的所有顶层指令函数都以同样的东西开头:一连串将递增常量写入一个死栈槽的操作,之后再也不会被读回,而且这些值在链上每次构建时都会重新随机生成。
我们仍然不确定这是要做什么。而干净地复现它,同时不借助会被标记为作弊的编译器技巧,是整个过程中较难的部分之一。
macro_rules! prologue_padding {
(step:expr) => {{
let mut slot = 0u32;
let mut value: u32 = $first;
loop {
// SAFETY:`slot` 是一个存活且正确对齐的局部 `u32`。
unsafe { core::ptr::write_volatile(&mut slot, value) };
if value > 100 {
break;
}
value += $step;
}
}};
}
最后冲刺
那么,在每个函数都与原始源码完美匹配之后,还有什么需要修复?
即使每个函数都达到 100% 匹配,我们在一个 243k 字节的二进制上仍有整整 93k 字节的 diff。一个明显的来源是原始二进制和重建二进制之间那些匹配函数的顺序,以及由此产生的任何下游内容,比如 .data.rel.ro 中的指针表或 vtable。
函数顺序对编译很重要,因为 rustc 按用户定义它们的顺序发出函数:我们只需查看原始二进制,并决定如何放置内部符号和 crate imports。
你可以在 rustc 本身中看到这一点,在 items_in_deterministic_order 中:本地 item 按其定义索引排序,因此它们以源码顺序输出,而从其他 crate 引入的 item 则按符号名称排在它们前面。
pub fn items_in_deterministic_order(
&self,
tcx: TyCtxt<'tcx>,
) -> Vec<(MonoItem<'tcx>, MonoItemData)> {
// codegen 测试依赖于 item 按照与
// 它们在文件中出现的相同顺序处理,因此对于本地 item,我们首先按 node_id 排序
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct ItemSortKey<'tcx>(Option<usize>, SymbolName<'tcx>);
fn item_sort_key<'tcx>(tcx: TyCtxt<'tcx>, item: MonoItem<'tcx>) -> ItemSortKey<'tcx> {
ItemSortKey(
match item {
MonoItem::Fn(ref instance) => {
match instance.def {
// 我们只想将用户定义
// 实例的 HirIds 纳入考虑。其他实例对
// codegen 测试无关紧要,甚至可能使 item 顺序
// 不稳定。
InstanceKind::Item(def) => def.as_local().map(Idx::index),
InstanceKind::VTableShim(..)
| InstanceKind::ReifyShim(..)
| InstanceKind::Intrinsic(..)
| InstanceKind::FnPtrShim(..)
| InstanceKind::Virtual(..)
| InstanceKind::ClosureOnceShim { .. }
| InstanceKind::ConstructCoroutineInClosureShim { .. }
| InstanceKind::DropGlue(..)
| InstanceKind::CloneShim(..)
| InstanceKind::ThreadLocalShim(..)
| InstanceKind::FnPtrAddrShim(..)
| InstanceKind::AsyncDropGlueCtorShim(..) => None,
}
}
MonoItem::Static(def_id) => def_id.as_local().map(Idx::index),
MonoItem::GlobalAsm(item_id) => Some(item_id.owner_id.def_id.index()),
},
item.symbol_name(tcx),
)
}
let mut items: Vec<_> = self.items().iter().map(|(&i, &data)| (i, data)).collect();
items.sort_by_cached_key(|&(i, _)| item_sort_key(tcx, i));
items
}
一个意外且非常有用的差异来源在于程序使用的 panic 字符串:默认情况下它们包含源文件的相对路径和行号,这允许恢复项目布局,但也意味着我们必须遵守完全相同的布局才能得到匹配结果。
每个 panic 位置都会编译成一个 core::panic::Location:rustc 将文件路径、行和列填入这个 #[lang = "panic_location"] struct,然后将其作为 static 嵌入,供 panic handler 读回。
#[lang = "panic_location"]
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[stable(feature = "panic_hooks", since = "1.10.0")]
pub struct Location<'a> {
file: &'a str,
line: u32,
col: u32,
}
在解决这两个问题之后,我们得到了哈希完美的源码重新编译,这可以通过使用正确工具链编译来轻松验证。
这段代码甚至可以上传到像我们自己的 Solana Verifiable Build 这样的验证服务,以绑定到链上程序,只需对构建镜像做一些调整。
老生常谈的关键漏洞
现在我们有了源码,阅读和扫描它便是下一步合乎逻辑的操作。所有这些反编译和清理都是值得的,因为我们通过一个几乎没有监督的 Auditing Agent 发现了一个相当值得注意的漏洞。
我们发现的关键漏洞是一个简单的会计错误,仅靠经典静态分析很难发现:propAMM 信任了自己的 token 余额。
GoonFi 根据 inventory skew 为每种资产定价,并直接从 vault 的实时 SPL 余额推断它。但任何人都可以向 token account 发送 token:程序无法分辨真实 swap 和未经请求的捐赠。向 vault A 捐赠足够多,skew 就会将资产价格压到单个 raw unit,此时市场的另一边基本上就免费了。
let vault_amount =
goonfi_common::helpers::get_token_account_amount(bid_vault, &market.bid_token_program)
.map_err(|_| ProgramError::InvalidAccountData)?;
// ... vault_amount 直接流入报价,并在那里成为 inventory skew:
// state/market_v2.rs: let deviation = balance - inventory_limit;
这种通胀攻击,在拥有大量资金的情况下,可能已经耗尽或毁掉多个 GoonFi vault。
结论
我们希望将这里的工作转化为对整个社区有益的东西,并且我们相信整个 Solana 生态系统的开放未来。
请继续关注更新并欢呼吧。虽然逆向工程可能已经死了,但这意味着将有更多眼睛来保护 Solana 程序,也会有更多探索之手来防范风险。
当然,准备好你的 token,因为它们会被需要。
最后,我们邀请其他人审阅发布在这个 repo 中的完整反编译:
renato-osec/goonfi-v2-decompiled — 闭源 GoonFi V2 Solana 程序的字节级一致反编译 Rust 源码。
脚注
- 众多之一,因为编译绝不是单射的。
- 这个概念之前在像 decompetition.io 这样精彩的 CTF 中已经被探索过,不过那里有一个已经知道源码的 oracle。我们在这里处理的问题更难。
- 分叉自 decomp.me,它有着相同的目标,但针对的是主机游戏。