Neighbour-lite
题目来源:Sudocrypt v16.0
这比赛… 一言难尽!
附件只有一个加密 RAR,解开是个 Go 客户端,flag 只能跟服务器要。
顺序记一下:剥壳 → 看二进制 → 抓协议 → 把签名和 PoW 自己写出来 → 伪造”观测” → 最后让官方客户端替我交答案。

第一步 · 连文件名都不给的 RAR
1 | 00000000: 5261 7221 1a07 0100 07e4 bf50 2104 0000 Rar!.......P!... |
1a07 0100 是 RAR5,紧接着的 ...04 是 HEAD_ENCRYPTION —— 头加密(-hp),所以 7z l 连文件列表都要密码。
1 | rar2john Neighbour-lite.rar |
1 | Speed.#1.........: 37791 H/s |
22 秒,密码 nguyen。解出来两个同源文件:neighbor-agent.exe(7.3 MB)、neighbor-agent-linux(7.1 MB)。
第二步 · 二进制里不用逆也能看到的东西
1 | $ ./neighbor-agent.exe |
Go 1.27.3 编译,-ldflags="-s -w" 去了符号,但 Go 自带的 pclntab 还在,函数名一抓一大把:
1 | main.main main.solvePoW main.measureRTT main.observeLAN main.postSigned main.isDebuggerPresent |
连源码路径都留在里面:D:/chall/agent/main_windows.go。
再加上字符串里的 JSON tag —— pow_challenge、observation{gateway_mac,nic_ipv6_ll,nic_mac,rtt_ns}、enc_flag、session_key、final_flag、memory_hash、X-Signature —— 这题在干什么基本就清楚了:客户端观测本机所在局域网,签名后交给服务器判定。
顺手记一下:GoReSym 跑这个 exe 直接 no valid pclntab found(它只认到 1.2x),所以别在符号恢复上花时间,strings + 抓包更快。
第三步 · 让它连我的假服务器
客户端有 -server 参数,先丢给本地 8080 的假服务器,让它把请求原样打出来:
1 | POST /register {"pubkey":"<64 hex>"} |
每个请求带三个头:X-Agent-Pubkey(每次运行新生成的 ed25519 公钥)、X-Client-Auth: Neighbor-Go-Client-v3、X-Signature。
想要真服务器的响应,就再套一层 HTTP→HTTPS 转发代理:客户端以为在说明文,代理帮我转给真服务,两头的请求/响应都落盘。真服务器回的是:
1 | POST /run/start -> {"nonce":"44a3be45...","ok":true, |
第四步 · 签名和 PoW 自己复现
签名缺了会回 {"err":"auth"},说明必须带。被签的串不用猜,拿一条真实的 (pubkey, path, body, signature) 在本地用公钥枚举验签就行,两分钟就中:
1 | sign( "POST /run/start " + sha256(body).hexdigest() ) |
1 | msg = f"{method} {path} {sha256(body_bytes).hexdigest()}".encode() |
PoW 同理,用抓到的 (challenge=04da53b8..., pow_nonce=5875) 反推构造:
1 | sha256(raw_challenge + uint64_le(nonce)) = 0000ebe864762e62... # 前 16 bit 为 0 |
1 | def solve_pow(challenge_hex, bits=16): |
另外 /run/start 的 body 永远是 {},签名里又没有时间戳/随机数 —— 抓一次就能无限重放。等于白送签名能力。
到这儿我已经是个”合法客户端”了,body 想写什么写什么。
第五步 · 服务器只认真的邻居
服务器只会回 bad observation,不告诉你哪里错,所以每次换一项,一次一个新 run(换 run + 解 PoW 大约 1 秒):

结论三条:
nic_mac必须是全球管理单播 MAC —— 本地管理位(首字节 0x02)和 Hyper-V 的00:15:5d全挂;nic_ipv6_ll必须是nic_mac推出来的 EUI-64:mac[0]^0x02+mac[1:3]+ff:fe+mac[3:];gateway_mac同样要全球管理,虚拟网关02:00:00:00:51:00不行;rtt_ns合理即可,nonce必须用下发的值。
第 2 条是题眼 —— 题目叫 Neighbour,考的就是”你到底有没有在看局域网的 IPv6 邻居”。虚拟机里的网卡是虚拟 OUI、link-local 是隐私地址,所以官方客户端在本机跑一万次也是 403。
1 | def eui64(mac): # "00:1a:2b:3c:4d:5e" -> "fe80::21a:2bff:fe3c:4d5e" |
通过的那一次:
1 | {"ok":true, |
第六步 · 先吃一个诱饵
enc_flag 27 字节,session_key 16 字节,按 session_key 循环异或:
1 | pt = bytes(c ^ sk[i % len(sk)] for i, c in enumerate(enc_flag)) |
排版完美、格式全对,但语义是 “memory dump fail” —— 这是留给”想直接从内存里抠 flag”的人的假答案,交上去就是错的。
第七步 · 让官方客户端自己交答案
客户端在 /gate/1/submit 之后还有一步:[+] Syncing state with server (computing memory proof)...,算完 memory_hash 就往 /gate/2/verify 提交 final_flag。
memory_hash 我没打算逆(它是跟 offset 绑的内存证明),于是换个思路:写个假服务器,把真服务器那条 200 原样回给它。客户端校验、解密、计算全都自己做,最后把结果塞进自己的请求里,我只管打日志:
1 | /gate/2/verify |
为了确认这个值不是”客户端随手编的”,再做一次对照:把 enc_flag、session_key 全置 0,offset 改成 0 ——
final_flag一个字都没变 → 它是客户端里硬编码的(而且混淆过,strings搜sUd0CrYpT/l0c4l都搜不到);memory_hash变了(d99b4954…→6dbf91dc…)→ 它确实是跟着offset走的证明。
flag:
1 | sUd0CrYpT{l0c4l_n3tw0rk_n1nj4} |
踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
| RAR 头加密 | 7z l 就要密码,文件名都看不到 |
rar2john + hashcat -m 13000,rockyou 前 6% 命中 |
| WSL 里的 hashcat 全挂 | CUDA PTX .version 9.0 ... current 8.5,pocl shared.cl build failed |
换 Windows 版 hashcat,加 --backend-ignore-cuda 走 OpenCL |
| GoReSym 失效 | no valid pclntab found |
Go 1.27.3 太新,工具只到 1.2x;直接 strings + 抓包 |
| 签名猜不中 | body、path+body、method+path+body 全 401 |
离线拿公钥枚举验签,命中 "METHOD path sha256hex(body)" |
| 官方客户端永远 403 | 本机是 Hyper-V 网卡 + 随机 link-local | 服务器要全球管理 MAC + EUI-64,自己构造观测 |
| 诱饵 flag | m3m0ry_dump_f41l |
真答案在客户端 /gate/2/verify 的 final_flag 里 |
一句话总结
这题的防守做得很讲究:PoW 挡脚本、ed25519 签名挡改包、观测校验挡虚拟机 —— 但三层都能从”客户端自己”身上绕过去,最后一层干脆让客户端替你把答案写出来。真要加固的话:签名串加时间戳和随机数(现在可无限重放),final_flag 改服务端下发而不是本地常量。
本文作者 Allureluoli
文字版权归作者所有,转载请注明出处;商业用途请先联系二月。