Allureluoli 二月FEBRUARY

2026-10-30-Neighbour-lite

Neighbour-lite

题目来源:Sudocrypt v16.0

这比赛… 一言难尽!

附件只有一个加密 RAR,解开是个 Go 客户端,flag 只能跟服务器要。

顺序记一下:剥壳 → 看二进制 → 抓协议 → 把签名和 PoW 自己写出来 → 伪造”观测” → 最后让官方客户端替我交答案。

四步协议

第一步 · 连文件名都不给的 RAR

1
2
00000000: 5261 7221 1a07 0100 07e4 bf50 2104 0000  Rar!.......P!...
00000010: 010f 08d1 0ef2 08d0 7c17 c630 feca d8a1 ........|..0.....

1a07 0100 是 RAR5,紧接着的 ...04 是 HEAD_ENCRYPTION —— 头加密(-hp),所以 7z l 连文件列表都要密码。

1
2
3
4
rar2john Neighbour-lite.rar
# $rar5$16$08d10ef208d07c17c630fecad8a16e88$15$d76d82ec58bacd1f9ec5958bf7a5cc7b$8$cc91cd1fc4c72582

hashcat -m 13000 -a 0 n.hash rockyou.txt --backend-ignore-cuda -O -w 3
1
2
3
Speed.#1.........:    37791 H/s
Progress.........: 786432/14344385 (5.48%)
$rar5$16$...:nguyen

22 秒,密码 nguyen。解出来两个同源文件:neighbor-agent.exe(7.3 MB)、neighbor-agent-linux(7.1 MB)。

第二步 · 二进制里不用逆也能看到的东西

1
2
3
$ ./neighbor-agent.exe
[*] Solving Proof of Work...
[-] Failed: bad observation

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
2
3
4
5
6
7
POST /register      {"pubkey":"<64 hex>"}
POST /run/start {}
POST /gate/1/submit {"observation":{"gateway_mac":"02:00:00:00:51:00",
"nic_ipv6_ll":"fe80::2b5:a39c:3e46:82a6",
"nic_mac":"00:15:5d:ba:e8:d2",
"nonce":"<64 hex>","rtt_ns":1598400},
"pow_nonce":"5875","run_id":"<uuid>"}

每个请求带三个头:X-Agent-Pubkey(每次运行新生成的 ed25519 公钥)、X-Client-Auth: Neighbor-Go-Client-v3、X-Signature。

想要真服务器的响应,就再套一层 HTTP→HTTPS 转发代理:客户端以为在说明文,代理帮我转给真服务,两头的请求/响应都落盘。真服务器回的是:

1
2
3
4
5
POST /run/start -> {"nonce":"44a3be45...","ok":true,
"pow_challenge":"04da53b8b46cdda9920c402fb5db701a",
"run_id":"55c4a4db-4d58-48cd-9bba-b28811950b02"}

POST /gate/1/submit -> 403 {"err":"bad observation","ok":false}

第四步 · 签名和 PoW 自己复现

签名缺了会回 {"err":"auth"},说明必须带。被签的串不用猜,拿一条真实的 (pubkey, path, body, signature) 在本地用公钥枚举验签就行,两分钟就中:

1
sign( "POST /run/start " + sha256(body).hexdigest() )
1
2
msg = f"{method} {path} {sha256(body_bytes).hexdigest()}".encode()
sig = sk.sign(msg) # -> X-Signature

PoW 同理,用抓到的 (challenge=04da53b8..., pow_nonce=5875) 反推构造:

1
sha256(raw_challenge + uint64_le(nonce)) = 0000ebe864762e62...   # 前 16 bit 为 0
1
2
3
4
5
6
def solve_pow(challenge_hex, bits=16):
raw, nb, n = bytes.fromhex(challenge_hex), bits // 8, 0
while True:
if sha256(raw + struct.pack("<Q", n)).digest()[:nb] == b"\x00" * nb:
return str(n)
n += 1

另外 /run/start 的 body 永远是 {},签名里又没有时间戳/随机数 —— 抓一次就能无限重放。等于白送签名能力。

到这儿我已经是个”合法客户端”了,body 想写什么写什么。

第五步 · 服务器只认真的邻居

服务器只会回 bad observation,不告诉你哪里错,所以每次换一项,一次一个新 run(换 run + 解 PoW 大约 1 秒):

观测矩阵

结论三条:

  1. nic_mac 必须是全球管理单播 MAC —— 本地管理位(首字节 0x02)和 Hyper-V 的 00:15:5d 全挂;
  2. nic_ipv6_ll 必须是 nic_mac 推出来的 EUI-64:mac[0]^0x02 + mac[1:3] + ff:fe + mac[3:];
  3. gateway_mac 同样要全球管理,虚拟网关 02:00:00:00:51:00 不行;rtt_ns 合理即可,nonce 必须用下发的值。

第 2 条是题眼 —— 题目叫 Neighbour,考的就是”你到底有没有在看局域网的 IPv6 邻居”。虚拟机里的网卡是虚拟 OUI、link-local 是隐私地址,所以官方客户端在本机跑一万次也是 403。

1
2
3
4
5
def eui64(mac):                    # "00:1a:2b:3c:4d:5e" -> "fe80::21a:2bff:fe3c:4d5e"
b = bytes.fromhex(mac.replace(":", ""))
b = bytes([b[0] ^ 0x02]) + b[1:]
h = (b[:3] + b"\xff\xfe" + b[3:]).hex()
return "fe80::" + h[0:4] + ":" + h[4:8] + ":" + h[8:12] + ":" + h[12:16]

通过的那一次:

1
2
3
4
{"ok":true,
"enc_flag":"7355fed1d2d437ca12255cb57cff8f2c5f64ef8ce1f9088e77324c",
"offset":12647653,
"session_key":"00009ae191a66eba465e318611cffd55"}

第六步 · 先吃一个诱饵

enc_flag 27 字节,session_key 16 字节,按 session_key 循环异或:

1
2
pt = bytes(c ^ sk[i % len(sk)] for i, c in enumerate(enc_flag))
# sUd0CrYpT{m3m0ry_dump_f41l}

排版完美、格式全对,但语义是 “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
2
3
4
/gate/2/verify
{"final_flag":"sUd0CrYpT{l0c4l_n3tw0rk_n1nj4}",
"memory_hash":"d99b4954d0076b42ee803ea4dca7718330564582c39bf772b1e4ae5894cd9898",
"run_id":"..."}

为了确认这个值不是”客户端随手编的”,再做一次对照:把 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 改服务端下发而不是本地常量。

评论COMMENTS