feat(v1.1+v1.2): context-aware MCP decompile/disasm (fold/branch/summary), seep_r2_xrefs paginator, MANUAL anti-debug/unpacking/poc-validation SOP, 7-section verifier
This commit is contained in:
1 parent
55afe5e613
commit
8f3c49c84b
6 files changed
+732
-9
No files matched your search
@@ -0,0 +1,190 @@
|
||||
# MANUAL — 反调试对抗与强反调试绕过 (Anti-Debug Bypass SOP)
|
||||
|
||||
> 本文件是当 Agent 在 G0/G1 阶段检测到目标存在**反调试或强对抗机制**时的标准化处置手册。
|
||||
> 覆盖 Windows 用户态全量反调试技法、通用补丁模板与代理 DLL 框架接入指引。
|
||||
>
|
||||
> 读取顺序:先通过 `seep_auto_triage` 识别壳类型 → 与本文档对比签名 → 选择对应打桩策略。
|
||||
|
||||
---
|
||||
|
||||
## 一、 快速诊断表:常见反调试信号 → 处置路由
|
||||
|
||||
| 检测到的信号 | 最可能机制 | 首选处置策略 |
|
||||
|---|---|---|
|
||||
| `IsDebuggerPresent` 出现在导入表 | PEB.BeingDebugged | NOP / 硬编码返回 0 |
|
||||
| `NtQueryInformationProcess` + 0x7 | ProcessDebugPort | Syscall Stub 替换 |
|
||||
| `CheckRemoteDebuggerPresent` | 同上 | Hook 返回 FALSE |
|
||||
| `GetTickCount` / `QueryPerformanceCounter` 频繁调用 | 时序反调试 | 钩子固定返回值 |
|
||||
| `CreateToolhelp32Snapshot` + 进程名比对 | 调试器进程名检测 | Hook 过滤快照列表 |
|
||||
| `OutputDebugString` 后检测错误码 | ODS 陷阱 | 无操作(正常运行不受影响) |
|
||||
| `NtSetInformationThread` 0x11 | ThreadHideFromDebugger | Hook 丢弃该调用 |
|
||||
| 注册表 `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug` | AeDebug 检测 | 值打补丁 / Hook RegQueryValueEx |
|
||||
| 断点扫描 / 自校验 CRC | 内存完整性自检 | 修正 CRC 或 NOP 校验逻辑 |
|
||||
| 驱动 `\Device\DebuggerDetect` / ObRegisterCallbacks | 内核级反调试 | → 参考第五节:内核层对抗 |
|
||||
|
||||
---
|
||||
|
||||
## 二、 用户态 PEB 系列反调试 —— 最常见(通用补丁)
|
||||
|
||||
### 2.1 `IsDebuggerPresent` → 返回值强制为 0
|
||||
|
||||
**原理**:`kernel32!IsDebuggerPresent` 本质只读 `PEB.BeingDebugged (offset 0x2)`。
|
||||
|
||||
**代理 DLL / Frida 修补(推荐)**:
|
||||
|
||||
```javascript
|
||||
// Frida 方案
|
||||
Interceptor.attach(Module.getExportByName('kernel32.dll', 'IsDebuggerPresent'), {
|
||||
onLeave: function(retval) { retval.replace(0); }
|
||||
});
|
||||
```
|
||||
|
||||
```cpp
|
||||
// 内联汇编补丁(静态,适用于代理 DLL)
|
||||
// 定位 IsDebuggerPresent 首地址,写入:
|
||||
// XOR EAX, EAX
|
||||
// RET
|
||||
// 原始字节(x64)通常是: 65 48 8B 04 25 60 00 00 00 → 替换前 3 字节
|
||||
BYTE patch[] = { 0x33, 0xC0, 0xC3 }; // xor eax,eax; ret
|
||||
WriteProcessMemory(hProc, pIsDebuggerPresent, patch, sizeof(patch), nullptr);
|
||||
```
|
||||
|
||||
### 2.2 `NtQueryInformationProcess` 信息类 7 (ProcessDebugPort) / 30 (ProcessDebugObjectHandle)
|
||||
|
||||
**原理**:成功连接调试器时 Port != 0。
|
||||
|
||||
```javascript
|
||||
// Frida 方案
|
||||
const NtQIP = Module.getExportByName('ntdll.dll', 'NtQueryInformationProcess');
|
||||
Interceptor.attach(NtQIP, {
|
||||
onEnter(args) { this.cls = args[1].toInt32(); this.out = args[2]; },
|
||||
onLeave(retval) {
|
||||
if (this.cls === 7 || this.cls === 30) {
|
||||
this.out.writePointer(ptr(0)); // 清零 DebugPort / DebugObject
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 `NtSetInformationThread` → ThreadHideFromDebugger (0x11)
|
||||
|
||||
调用此系统调用后,调试器事件将对该线程不可见,导致断点失效。
|
||||
|
||||
```javascript
|
||||
const NtSIT = Module.getExportByName('ntdll.dll', 'NtSetInformationThread');
|
||||
Interceptor.attach(NtSIT, {
|
||||
onEnter(args) {
|
||||
if (args[1].toInt32() === 0x11) {
|
||||
args[1] = ptr(0); // 替换为无害的 ThreadBasicInformation
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、 时序反调试 —— 高频坑(修复固定返回值)
|
||||
|
||||
调试状态下,`GetTickCount` / `QueryPerformanceCounter` 的推进速度远慢于正常运行,常被用来识别单步调试。
|
||||
|
||||
```javascript
|
||||
// Frida 方案:固定时钟
|
||||
let baseTick = Date.now();
|
||||
Interceptor.replace(Module.getExportByName('kernel32.dll', 'GetTickCount'),
|
||||
new NativeCallback(() => {
|
||||
baseTick += 15; // 模拟正常 15ms 系统时钟步进
|
||||
return baseTick;
|
||||
}, 'uint32', [])
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、 进程名 / 窗口名 / 文件路径特征检测 —— 改名或 Hook 快照
|
||||
|
||||
目标程序扫描进程快照(`CreateToolhelp32Snapshot + Process32Next`)比对调试器进程名:
|
||||
`x64dbg.exe`、`ollydbg.exe`、`ida.exe`、`ida64.exe`、`cheatengine.exe` 等。
|
||||
|
||||
**最简单处置**:把调试器改名为 `explorer2.exe`、`chrome_x.exe` 等。
|
||||
|
||||
**进阶 Hook 方案**:
|
||||
|
||||
```javascript
|
||||
// 过滤 Process32Next 快照,抹去调试器进程条目
|
||||
const blacklist = ['x64dbg.exe', 'ida64.exe', 'ollydbg.exe', 'wireshark.exe'];
|
||||
const Process32Next = Module.getExportByName('kernel32.dll', 'Process32NextW');
|
||||
Interceptor.attach(Process32Next, {
|
||||
onLeave(retval) {
|
||||
if (retval.toInt32() !== 0) {
|
||||
const entry = this.context.rdx; // PROCESSENTRY32W 结构
|
||||
const exeName = entry.add(44).readUtf16String();
|
||||
if (blacklist.some(b => exeName && exeName.toLowerCase().includes(b))) {
|
||||
// 递归调用直到返回非黑名单进程(简化处理:置为无害进程名)
|
||||
entry.add(44).writeUtf16String('System');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、 内核级反调试对抗(Ring0 / 驱动层)
|
||||
|
||||
> **适用场景**:游戏反作弊(ACE/EAC/BattlEye)、强保护商业软件、带 Ring0 驱动的许可校验模块。
|
||||
> **注意**:内核对抗超出 Agent 全自动范畴,以下为指导性 SOP,需结合 WinDbg + 内核调试器手工处理。
|
||||
|
||||
### G0 阶段主动识别信号:
|
||||
- `triage` 输出中存在 `.sys` 驱动签名或服务注册(HKLM\SYSTEM\CurrentControlSet\Services)
|
||||
- 导入表出现 `NtLoadDriver`、`ZwCreateSection` 等内核接口
|
||||
- 运行时观察到 `ObRegisterCallbacks` / `PsSetCreateProcessNotifyRoutine` 回调注册
|
||||
|
||||
### 处置策略(按难度升序):
|
||||
| 方案 | 适用条件 | 操作要点 |
|
||||
|---|---|---|
|
||||
| **Kernel Patch Guard 绕过** | 测试用 VM / Hyper-V | 关闭 Driver Signature Enforcement,加载调试补丁驱动 |
|
||||
| **VirtualBox + DKOM** | 沙箱分析 | 修改 `EPROCESS.DebugPort = NULL` 绕过内核调试器检测 |
|
||||
| **全量内存 Dump + 用户态重放** | 驱动已运行 | 在内核加载完毕后 dump 进程内存,在用户态 Loader 中重放 |
|
||||
| **Emulation (Unidbg/QEMU)** | 需脱离真实 OS | 在模拟环境中重跑关键算法,完全规避内核对抗 |
|
||||
|
||||
---
|
||||
|
||||
## 六、 代理 DLL 注入框架模板(统一打桩方式)
|
||||
|
||||
> 适用于不能直接修改宿主 EXE(如有数字签名)的场景。
|
||||
|
||||
所有上述 Hook 均可集成在以下代理 DLL 框架中,加载即自动注入,宿主签名完好:
|
||||
|
||||
```cpp
|
||||
// anti_debug_patch.cpp — 代理 DLL 打桩框架(以 version.dll 为例)
|
||||
#include <Windows.h>
|
||||
|
||||
// 实际 version.dll 导出函数转发(保留原功能)
|
||||
#pragma comment(linker, "/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA")
|
||||
// ... 其他导出
|
||||
|
||||
void ApplyAntiDebugPatches() {
|
||||
// 1. 清除 PEB.BeingDebugged
|
||||
__asm {
|
||||
mov eax, fs:[30h] // PEB
|
||||
mov byte ptr [eax+2], 0 // BeingDebugged = 0
|
||||
}
|
||||
// 2. Hook NtQueryInformationProcess (如上 Frida 方案的 C++ 等效)
|
||||
// ...
|
||||
}
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpvReserved) {
|
||||
if (reason == DLL_PROCESS_ATTACH) ApplyAntiDebugPatches();
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、 两击熔断纪律 —— 反调试对抗的止损规则
|
||||
|
||||
| 情形 | 触发条件 | 处置 |
|
||||
|---|---|---|
|
||||
| 用户态 Hook 无效(程序仍检测到调试器)| 同构打桩 2 次失败 | 停止,升级为内核层分析 |
|
||||
| 内核层方案部署失败 | 同构 2 次失败 | 停止,改用 Emulation(Unidbg/QEMU)路线 |
|
||||
| Emulation 行为无法覆盖 | 无法还原算法 | 输出"该目标超出当前工具链全自动范畴",转人工分析 |
|
||||
@@ -0,0 +1,171 @@
|
||||
# MANUAL — PoC 闭环自动化验证 SOP (Automated PoC Validation & Self-Healing)
|
||||
|
||||
> 本文件定义了从 Agent 生成 PoC(Frida 脚本 / Smali 补丁 / 代理 DLL)到
|
||||
> **自动化执行、捕获回显、报错自愈** 的完整端到端验证流程。
|
||||
>
|
||||
> 目标:让 Agent 交付的 PoC 从"推测可行的代码"进化为"**100% 执行验证过的有效 PoC**"。
|
||||
|
||||
---
|
||||
|
||||
## 一、 三阶段闭环验证模型
|
||||
|
||||
```
|
||||
G4/G5:生成 PoC
|
||||
│
|
||||
▼
|
||||
[阶段 1] 静态自检 —— 语法解析、类名/方法签名格式校验
|
||||
│ 通过
|
||||
▼
|
||||
[阶段 2] 沙箱执行 —— 注入目标进程 / 模拟器,捕获 stdout/stderr
|
||||
│ 通过 失败
|
||||
▼ ▼
|
||||
[阶段 3] [自愈循环(最多 2 次)]
|
||||
结论确认 解析报错 → Agent 定位错误类型 → 修正代码 → 重跑
|
||||
输出验证 若 2 次自愈失败 → 标注"需人工校准",输出 diff 报告
|
||||
闭环报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、 Frida 脚本静态自检 → 自动执行 → 报错自愈(P0 优先实现)
|
||||
|
||||
### 2.1 Agent 执行序列(写入 AGENTS.md 执行纪律)
|
||||
|
||||
在 Agent 生成 Frida 脚本(`hook_verify.js`)后,必须立即执行以下序列,不得跳过直接进报告:
|
||||
|
||||
```powershell
|
||||
# Step 1:Node.js 语法快检(不启动 Frida,仅校验 JS 语法)
|
||||
node --check hook_verify.js
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "[自愈 Step 1] JS 语法错误,Agent 自动修正后重跑"
|
||||
# → 回传报错给 Agent,Agent 修正后重新生成并再次检查
|
||||
}
|
||||
|
||||
# Step 2:连接模拟器/测试机 (adb),注入目标包并捕获输出
|
||||
$pkg = "com.example.target" # 从 seep_apk_info 获取
|
||||
frida -U -f $pkg -l hook_verify.js --no-pause 2>&1 | Tee-Object -FilePath frida_run.log &
|
||||
Start-Sleep -Seconds 10 # 给 Frida 10 秒采集输出
|
||||
Stop-Process -Name frida -ErrorAction SilentlyContinue
|
||||
|
||||
# Step 3:分析 frida_run.log,由 Agent 读取并判定结果
|
||||
Get-Content frida_run.log
|
||||
```
|
||||
|
||||
### 2.2 常见报错 → 自愈映射表
|
||||
|
||||
| 报错特征 | 根因 | 自愈操作 |
|
||||
|---|---|---|
|
||||
| `Java.ClassNotFoundException: com.x.y.Auth` | 类名拼写错误或混淆 | 重新调 `seep_apk_smali_search` 找正确类路径 |
|
||||
| `Error: expected a pointer` | 参数类型声明错误 | 修正 `Java.use` 链中的类型签名 |
|
||||
| `Process not found` | 目标 App 未前台运行 | 先启动目标 App 再 attach:改 `-f` 为 `-n` |
|
||||
| `Error: unable to find module` | so 库未加载 | 加 `waitForModule()` 等待动态加载后 hook |
|
||||
| `Script terminated` 无任何 console 输出 | onEnter/onLeave 挂钩点在 Dalvik 解释模式 | 改用 `Java.deoptimizeEverything()` 强制解释执行 |
|
||||
| `Access violation` / 程序崩溃 | 内存写越界或返回类型错误 | 检查 `retval.replace()` 的参数类型一致性 |
|
||||
|
||||
---
|
||||
|
||||
## 三、 Windows Smali / Binary Patch 本地加载器验证
|
||||
|
||||
### 3.1 Smali 补丁验证流程
|
||||
|
||||
```powershell
|
||||
# 前置:已用 apktool d 解包并修改 smali,现在重打包
|
||||
cd task_sandbox\<task_name>
|
||||
|
||||
# 重打包
|
||||
.\Tool\mcp\Tool\safe\apktool\apktool.bat b smali_patched -o target_repacked.apk
|
||||
|
||||
# Zipalign 4 字节对齐(必须,否则 ART 拒绝加载)
|
||||
zipalign -v 4 target_repacked.apk target_aligned.apk
|
||||
|
||||
# 签名(测试用 debug 密钥)
|
||||
apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android `
|
||||
--out target_signed.apk target_aligned.apk
|
||||
|
||||
# 安装到测试机并捕获 logcat
|
||||
adb install -r target_signed.apk
|
||||
adb logcat -c && adb logcat | Select-String "VIP|Premium|License|granted|unlocked|ERROR"
|
||||
```
|
||||
|
||||
### 3.2 Windows 代理 DLL 本地加载验证
|
||||
|
||||
```powershell
|
||||
# 在隔离沙箱副本中加载代理 DLL,捕获函数返回值
|
||||
# 前提:已编译生成 anti_debug_patch.dll
|
||||
|
||||
# 创建进程副本(绝对不要在生产环境直接测试)
|
||||
$targetCopy = ".\sandbox\target_copy.exe"
|
||||
Copy-Item "target.exe" $targetCopy
|
||||
|
||||
# 将代理 DLL 放置在 target_copy.exe 同目录(version.dll 劫持)
|
||||
Copy-Item ".\patches\version.dll" ".\sandbox\version.dll"
|
||||
|
||||
# 启动进程并监控退出码与关键窗口
|
||||
$proc = Start-Process $targetCopy -PassThru
|
||||
Start-Sleep -Seconds 5
|
||||
if (-not $proc.HasExited) {
|
||||
Write-Host "[PASS] 进程正常运行,代理 DLL 注入无崩溃"
|
||||
# 截图或 spy++ 检查目标窗口状态(标题栏是否出现"专业版"字样)
|
||||
} else {
|
||||
Write-Host "[FAIL] 进程崩溃,退出码 $($proc.ExitCode)"
|
||||
# → 回传报错,Agent 检查 DLL 导出表是否与 system32\version.dll 一致
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、 离线确证(断网铁证 — CWE-602 终极验证)
|
||||
|
||||
PoC 执行成功后,**必须在飞行模式 / 断网状态下补做终极确证**,否则无法区分本地决策与服务端下发:
|
||||
|
||||
```text
|
||||
验证步骤:
|
||||
1. 关闭所有网络接口(或开飞行模式)
|
||||
2. 重启目标 App(清除任何本地缓存)
|
||||
3. 在断网状态下重新触发受限功能
|
||||
4. 观察是否依然解锁/生效
|
||||
|
||||
结论判定:
|
||||
✓ 断网后受限功能依然生效 → 确认为 CWE-602 本地决策脆弱性,PoC 有效
|
||||
✗ 断网后功能失效 → 服务端权威强依赖,本地 PoC 无法构成完整绕过,报告需标注"需服务端协同"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、 PoC 自愈循环上限与熔断规则
|
||||
|
||||
| 轮次 | 操作 | 若失败 |
|
||||
|---|---|---|
|
||||
| 第 1 次 | 生成 PoC → 静态自检 → 沙箱执行 | 解析报错 → 进入自愈 |
|
||||
| 第 2 次 | Agent 分析报错 → 修正代码 → 重跑 | 解析报错 → 再试一次 |
|
||||
| 第 3 次 | 最终修正 → 重跑 | **触发熔断:停止自动化,输出 diff 报告,标注"需人工校准"** |
|
||||
|
||||
熔断后 Agent 必须输出:
|
||||
```markdown
|
||||
## PoC 自动化验证受阻通知
|
||||
|
||||
- **PoC 文件**:`task_sandbox/<name>/hook_verify.js`
|
||||
- **失败原因**:[具体报错摘要]
|
||||
- **自愈尝试次数**:3 次(已达上限)
|
||||
- **建议**:将 `frida_run.log` 与当前 PoC 提交人工校准,或改用 Unidbg 模拟路线。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、 验证成功后的三段式报告触发
|
||||
|
||||
PoC 通过所有验证后,调用 `seep_gen_security_report` 自动生成结构化交付报告:
|
||||
|
||||
```python
|
||||
# Agent 内部调用
|
||||
seep_gen_security_report(
|
||||
task_name="项目X_CWE602_验证",
|
||||
vuln_title="客户端本地鉴权旁路 (CWE-602)",
|
||||
cwe_id="CWE-602",
|
||||
severity="HIGH",
|
||||
affected_component="LicenseManager.checkPremium()",
|
||||
root_cause="受限功能完全由本地布尔标志位控制,服务端无二次校验",
|
||||
poc_summary="Frida Hook 拦截 checkPremium() 返回值,强制返回 true,断网状态下受限功能正常解锁,确认为本地决策脆弱性。",
|
||||
remediation="将特权决策迁移至服务端,客户端仅持有短期、加密的会话票据,每次解锁必须通过服务端签名验证。"
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
# MANUAL — 通用脱壳前置分析 SOP (Unpacking & Deobfuscation)
|
||||
|
||||
> 本文件适用于 G0/G1 阶段识别出加壳保护后的标准化处置流程。
|
||||
> 覆盖:MPRESS / UPX / Themida / VMP 3.x / .NET Reactor / 自定义混淆。
|
||||
> 原则:**能动态 dump 就不静态逆壳**,优先内存 Dump 重建,效率最高。
|
||||
|
||||
---
|
||||
|
||||
## 一、 壳类型速查与工具路由
|
||||
|
||||
| 壳特征(triage 输出信号)| 壳类型 | 首选脱壳路线 |
|
||||
|---|---|---|
|
||||
| `MPRESS1` / `MPRESS2` 节区节名 | MPRESS | 等待 OEP 后内存 dump(Scylla) |
|
||||
| `UPX0` / `UPX1` 节名,入口在末节 | UPX | `upx -d target.exe` 直接命令行解压 |
|
||||
| `themida`/`.Boot` 节名,大量无效导入 | Themida/WinLicense | OEP 断点 + Scylla IAT Rebuild |
|
||||
| `.vmp0` / `.vmp1` 节名,VM bytecode | VMP 3.x | 内存 dump + 动态 trace 提取算法(见第四节)|
|
||||
| 熵值 > 7.2(整个文件),无可读字符串 | 自定义壳 / 强加密 | rabin2 entropy + x64dbg 手动定位 OEP |
|
||||
| `.text` 区段全 0、代码在 .rsrc 内 | 资源型 Loader | 提取资源区段 → 分析 Loader 落盘路径 |
|
||||
| `_CorExeMain` 导入(.NET)| .NET 壳 | de4dot + dnSpy 优先,再看 VMP 字节码 |
|
||||
|
||||
---
|
||||
|
||||
## 二、 通用 OEP 定位 + 内存 Dump 流程(适用所有用户态壳)
|
||||
|
||||
### 步骤一:定位 OEP(原始入口点)
|
||||
|
||||
```text
|
||||
策略 A(ESP 定律 — 通用于 MPRESS/UPX/简单 PE 壳):
|
||||
1. 在 x64dbg 中运行程序,在首个用户态断点暂停
|
||||
2. 在 ESP(栈顶指针)处设置内存写入断点(Hardware BP on Write)
|
||||
3. 运行至断点触发,此时 EIP 即为解压后的 OEP
|
||||
4. 验证:OEP 处通常为 "push ebp; mov ebp, esp" 或 .NET CRT init 代码
|
||||
|
||||
策略 B(节区执行监控 — 通用于 Themida/自定义壳):
|
||||
1. 用 x64dbg 打开,在"内存布局"中对原始代码节区(.text)设置执行断点
|
||||
2. 运行程序,壳在向 .text 写完解密代码后会 jmp 到 OEP,此时断点触发
|
||||
3. 记录此时的 EIP 地址
|
||||
|
||||
策略 C(API 断点反向追踪 — 通用于所有壳):
|
||||
1. 在 GetCommandLineA / GetModuleHandleA 等 CRT 初始化必调 API 上设断点
|
||||
2. 程序调用此 API 时已完成壳初始化,此时在调用栈中回溯到 OEP
|
||||
```
|
||||
|
||||
### 步骤二:内存 Dump 与 IAT 重建
|
||||
|
||||
使用 **Scylla**(x64dbg 内置插件):
|
||||
|
||||
```text
|
||||
1. 在 OEP 处暂停后打开 Scylla(Plugins → Scylla)
|
||||
2. OEP 自动填入,点击 "IAT Autosearch" 扫描导入表
|
||||
3. 点击 "Get Imports" 获取完整 IAT
|
||||
4. 对所有标记为无效(红色)的条目,点击 "Fix Dump" 尝试自动修复
|
||||
5. 点击 "Dump" 导出内存映像
|
||||
6. 点击 "Fix Dump" 将 IAT 写入 Dump 文件
|
||||
7. 用 CFF Explorer 或 PE-bear 验证修复后的 PE 头完整性
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、 UPX 脱壳(最简单,一行命令)
|
||||
|
||||
```powershell
|
||||
# 直接解压(破坏性操作,先备份!)
|
||||
Copy-Item target.exe target_backup.exe
|
||||
upx -d target.exe
|
||||
|
||||
# 验证解压成功
|
||||
.\Tool\mcp\Tool\safe\radare2\bin\rabin2.exe -I target.exe | Select-String "class|bits|type"
|
||||
```
|
||||
|
||||
若 UPX 头被抹除(修改魔数以对抗 `upx -d`):
|
||||
|
||||
```python
|
||||
# 修复被抹除的 UPX 魔数
|
||||
with open("target.exe", "r+b") as f:
|
||||
data = f.read()
|
||||
# 定位 UPX 压缩数据特征并还原魔数
|
||||
idx = data.find(b"\x55\x58\x50\x00") # UPX! 魔数附近
|
||||
if idx > 0:
|
||||
f.seek(idx - 4)
|
||||
f.write(b"UPX!")
|
||||
print("Header patched, retry: upx -d target.exe")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、 VMP 3.x 脱壳与算法提取(最复杂,动态 trace 优先)
|
||||
|
||||
> VMP 将原始 x86/x64 指令编译为自定义 VM 字节码,**不可能静态完整还原**。
|
||||
> 正确策略:**不试图还原 VM 本身,而是在 VM 执行过程中 trace 算法结果**。
|
||||
|
||||
### 4.1 G0 阶段止损判定
|
||||
|
||||
```text
|
||||
seep_auto_triage 检测到 .vmp0/.vmp1 节名后,Agent 应立即输出:
|
||||
"⚠️ 目标存在 VMP 3.x 级虚拟机保护,超出全自动符号反编译范畴。
|
||||
将切换为动态 trace 策略提取关键算法输出,而非还原完整伪代码。"
|
||||
```
|
||||
|
||||
### 4.2 动态 Trace 策略(提取算法的输入/输出)
|
||||
|
||||
```javascript
|
||||
// Frida 方案:监控 VMP 保护函数的入口参数与出口返回值
|
||||
// 需先通过 auto_triage 确定被 VMP 保护的关键导出函数 RVA
|
||||
const baseAddr = Process.enumerateModules()[0].base;
|
||||
const vmProtectedFunc = baseAddr.add(0xDEAD00); // 替换为实际 RVA
|
||||
|
||||
Interceptor.attach(vmProtectedFunc, {
|
||||
onEnter(args) {
|
||||
console.log("[VMP] Func entered, args:");
|
||||
for (let i = 0; i < 4; i++) {
|
||||
try { console.log(` arg[${i}] = ${args[i]} = "${args[i].readUtf8String()}"`); }
|
||||
catch(e) { console.log(` arg[${i}] = ${args[i]}`); }
|
||||
}
|
||||
},
|
||||
onLeave(retval) {
|
||||
console.log(`[VMP] Func returned: ${retval} (0x${retval.toString(16)})`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 Unidbg 模拟执行(无需真机,黑盒调用)
|
||||
|
||||
```bash
|
||||
# 适用于 Android SO 中的 VMP 保护(Java_* 导出函数)
|
||||
# 在 Unidbg 中直接调用目标 JNI 函数,忽略内部 VM 实现,只捕获结果
|
||||
java -jar unidbg-android.jar \
|
||||
--lib target.so \
|
||||
--func Java_com_example_MainActivity_verifyLicense \
|
||||
--args "test_user_id,TEST-SERIAL-1234"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、 .NET 壳脱壳(.NET Reactor / ConfuserEx / Eazfuscator)
|
||||
|
||||
### 5.1 通用流程
|
||||
|
||||
```powershell
|
||||
# 第一步:用 de4dot 自动识别并去混淆
|
||||
.\de4dot.exe target.exe -o target_clean.exe
|
||||
|
||||
# 第二步:用 dnSpy 打开清理后的程序集检查
|
||||
# 若 de4dot 无法识别,尝试:
|
||||
.\de4dot.exe --dont-rename target.exe -o target_clean.exe
|
||||
```
|
||||
|
||||
### 5.2 运行时 dump(对抗字符串/资源加密)
|
||||
|
||||
```javascript
|
||||
// Frida 方案:在 .NET CLR System.Reflection.Assembly.Load 时捕获解密后的程序集
|
||||
const Load = Module.getExportByName('clr.dll', '_CorExeMain');
|
||||
// 等待 CLR 完成解密,dump AppDomain 中加载的所有程序集
|
||||
Process.enumerateModules().forEach(m => {
|
||||
if (m.name.endsWith('.dll') || m.name.endsWith('.exe')) {
|
||||
console.log(`[CLR Module] ${m.name} @ ${m.base} size=${m.size}`);
|
||||
// 用 Memory.readByteArray 导出并 PE-fix
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、 脱壳后完整性验证
|
||||
|
||||
```powershell
|
||||
# 快速验证脱壳后 PE 是否可正常分析
|
||||
$r2 = ".\Tool\mcp\Tool\safe\radare2\bin\rabin2.exe"
|
||||
|
||||
# 检查 PE 头完整性
|
||||
& $r2 -I target_unpacked.exe | Select-String "class|type|bits|os|subsys"
|
||||
|
||||
# 检查导入表是否正常恢复
|
||||
& $r2 -i target_unpacked.exe | head -20
|
||||
|
||||
# 检查节区属性(脱壳后 .text 应为 RX)
|
||||
& $r2 -S target_unpacked.exe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、 两击熔断纪律 —— 脱壳的止损规则
|
||||
|
||||
| 情形 | 触发 | 处置 |
|
||||
|---|---|---|
|
||||
| ESP 定律 2 次均找不到 OEP | 壳可能有 TLS 回调或多层包装 | 改用节区执行监控策略 |
|
||||
| 节区执行监控 2 次失败 | 壳对内存断点有检测 | 改用 API 断点反向追踪 |
|
||||
| 所有 Dump 策略失败 | 可能有内核级保护 | 声明"当前工具链无法完全脱壳",切换 Unidbg Emulation 提取算法关键结果 |
|
||||
+142
-7
@@ -277,18 +277,47 @@ def seep_r2_functions(binary_path: str, filter_name: Optional[str] = None, limit
|
||||
return "\n".join(res)
|
||||
|
||||
@server.tool()
|
||||
def seep_r2_disasm(binary_path: str, target: str = "main", lines: int = 60) -> str:
|
||||
"""反汇编指定函数或地址,输出包含交叉引用 (XREF)、局部变量与源码行号提示的汇编代码。
|
||||
def seep_r2_disasm(
|
||||
binary_path: str,
|
||||
target: str = "main",
|
||||
lines: int = 60,
|
||||
output_mode: str = "full",
|
||||
limit_tokens: int = 0,
|
||||
) -> str:
|
||||
"""反汇编指定函数或地址,支持三种输出模式以控制上下文消耗(Context Budget Control)。
|
||||
|
||||
Args:
|
||||
binary_path: 目标二进制文件路径
|
||||
target: 目标函数名、符号或虚拟地址(例如 'main', 'sym.imp.puts', '0x1013ef0')
|
||||
lines: 当以地址为目标时反汇编的最大指令行数,默认 60
|
||||
output_mode: 输出模式:
|
||||
'full' = 完整原始反汇编(默认,适合精细分析)
|
||||
'branch' = 仅保留条件跳转与调用指令(cmp/test/jcc/call/ret),聚焦控制流骨架
|
||||
'summary'= 不输出指令,只统计:函数大小、块数、调用链、导入函数列表(最省 Token)
|
||||
limit_tokens: 最大输出字符数(0=不限制),超出时尾部截断并附注 Truncated 标记
|
||||
"""
|
||||
if not os.path.isfile(binary_path):
|
||||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||||
|
||||
# 优先尝试函数级反汇编 pdf,若不是已知函数则按指令行数 pd 反汇编
|
||||
if output_mode == "summary":
|
||||
# 仅抽取函数级摘要统计,不输出原始指令
|
||||
info_cmd = f"aaa; afi @ {target}"
|
||||
xref_cmd = f"axt @ {target}"
|
||||
call_cmd = f"axf @ {target}"
|
||||
info_out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", info_cmd, os.path.abspath(binary_path)], timeout=60)
|
||||
xref_out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", xref_cmd, os.path.abspath(binary_path)], timeout=60)
|
||||
call_out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", call_cmd, os.path.abspath(binary_path)], timeout=60)
|
||||
summary = (
|
||||
f"[DISASM SUMMARY MODE — 上下文降噪 Token 节省模式]\n"
|
||||
f"Target: {target}\n"
|
||||
f"--- Function Info (afi) ---\n{_clean_r2_output(info_out)[:800]}\n"
|
||||
f"--- Callers (axt / xrefs-to) ---\n{_clean_r2_output(xref_out)[:600]}\n"
|
||||
f"--- Callees (axf / xrefs-from) ---\n{_clean_r2_output(call_out)[:600]}\n"
|
||||
f"提示: 使用 output_mode='branch' 或 'full' 获取指令级详情。"
|
||||
)
|
||||
return summary
|
||||
|
||||
# 获取完整反汇编
|
||||
cmd = f"aaa; pdf @ {target}"
|
||||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmd, os.path.abspath(binary_path)], timeout=60)
|
||||
cleaned = _clean_r2_output(out)
|
||||
@@ -300,24 +329,93 @@ def seep_r2_disasm(binary_path: str, target: str = "main", lines: int = 60) -> s
|
||||
cleaned = _clean_r2_output(out)
|
||||
output_lines = [l for l in cleaned.splitlines() if not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||||
|
||||
return "\n".join(output_lines) or "[Disassembly empty or symbol not found]"
|
||||
if output_mode == "branch":
|
||||
# 过滤:仅保留含有控制流关键词的行(条件跳转、调用、返回)
|
||||
branch_keywords = ("cmp ", "test ", "je ", "jne ", "jz ", "jnz ", "jb ", "jbe ",
|
||||
"ja ", "jae ", "jl ", "jle ", "jg ", "jge ", "jmp ", "call ",
|
||||
"ret", "jcc", "cbz ", "cbnz ", "bl ", "bne ", "beq ")
|
||||
filtered = [l for l in output_lines if any(kw in l.lower() for kw in branch_keywords)]
|
||||
total = len(output_lines)
|
||||
kept = len(filtered)
|
||||
header = (
|
||||
f"[DISASM BRANCH MODE — 控制流骨架截面,已过滤 {total - kept}/{total} 行纯数据指令]\n"
|
||||
f"Target: {target} | 保留行: {kept} | 完整输出请用 output_mode='full'\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
)
|
||||
output_lines = filtered
|
||||
result = header + "\n".join(output_lines)
|
||||
else:
|
||||
result = "\n".join(output_lines)
|
||||
|
||||
if limit_tokens > 0 and len(result) > limit_tokens:
|
||||
result = result[:limit_tokens] + f"\n... [TRUNCATED at {limit_tokens} chars — use offset/target narrowing for full output]"
|
||||
|
||||
return result or "[Disassembly empty or symbol not found]"
|
||||
|
||||
@server.tool()
|
||||
def seep_r2_decompile(binary_path: str, target: str = "main") -> str:
|
||||
"""将目标函数反编译为类 C 伪代码(使用 radare2 内置反编译引擎 pdc)。
|
||||
def seep_r2_decompile(
|
||||
binary_path: str,
|
||||
target: str = "main",
|
||||
output_mode: str = "full",
|
||||
limit_tokens: int = 8000,
|
||||
) -> str:
|
||||
"""将目标函数反编译为类 C 伪代码,支持三档输出模式以控制 Token 消耗(Context Budget Control)。
|
||||
|
||||
Args:
|
||||
binary_path: 目标二进制文件路径
|
||||
target: 目标函数名称或虚拟地址(例如 'main', 'sym.verify_key')
|
||||
output_mode: 输出控制模式(默认 full):
|
||||
'full' = 完整伪代码(适合深度分析,默认)
|
||||
'fold' = 折叠纯计算/赋值语句,仅保留 if/else/switch/return/API调用行(减少 ~60% Token)
|
||||
'summary' = 不输出伪代码,仅返回:函数签名、入参、出参、直接调用的 API/函数名列表(最省 Token)
|
||||
limit_tokens: 最大输出字符数(默认 8000),超出则截断附 Truncated 标记;设 0 表示不限制
|
||||
"""
|
||||
if not os.path.isfile(binary_path):
|
||||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||||
|
||||
if output_mode == "summary":
|
||||
# 获取函数摘要信息(不输出函数体)
|
||||
cmds = f"aaa; afi @ {target}; axf @ {target}"
|
||||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmds, os.path.abspath(binary_path)], timeout=60)
|
||||
clean = _clean_r2_output(out)
|
||||
return (
|
||||
f"[DECOMPILE SUMMARY MODE — 最省 Token 函数摘要]\n"
|
||||
f"Target: {target}\n"
|
||||
f"{clean[:1200]}\n"
|
||||
f"提示: 使用 output_mode='fold' 或 'full' 获取伪代码详情。"
|
||||
)
|
||||
|
||||
cmd = f"aaa; pdc @ {target}"
|
||||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", cmd, os.path.abspath(binary_path)], timeout=60)
|
||||
cleaned = _clean_r2_output(out)
|
||||
output_lines = [l for l in cleaned.splitlines() if not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||||
return "\n".join(output_lines) or "[Decompile output empty]"
|
||||
|
||||
if output_mode == "fold":
|
||||
# 折叠模式:去除纯赋值/加减乘除/临时变量行,仅保留控制流与 API 调用
|
||||
def _is_control_or_call(line: str) -> bool:
|
||||
stripped = line.strip()
|
||||
keywords = ("if ", "else", "switch", "case ", "return", "while ", "for ",
|
||||
"do {", "goto ", "break", "continue",
|
||||
"call ", "(", "->")
|
||||
# 包含括号 = 可能是函数调用,包含关键词 = 控制流
|
||||
return any(k in stripped for k in keywords) or stripped.startswith("//")
|
||||
|
||||
folded = [l for l in output_lines if _is_control_or_call(l)]
|
||||
total = len(output_lines)
|
||||
kept = len(folded)
|
||||
header = (
|
||||
f"[DECOMPILE FOLD MODE — 已折叠 {total - kept}/{total} 行纯赋值/计算语句,上下文降噪约 {int((total-kept)/max(total,1)*100)}%]\n"
|
||||
f"Target: {target} | 保留行: {kept} | 完整输出请用 output_mode='full'\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
)
|
||||
result = header + "\n".join(folded)
|
||||
else:
|
||||
result = "\n".join(output_lines)
|
||||
|
||||
if limit_tokens > 0 and len(result) > limit_tokens:
|
||||
result = result[:limit_tokens] + f"\n... [TRUNCATED at {limit_tokens} chars — 使用 target 缩小范围或 output_mode='fold'/'summary' 降低消耗]"
|
||||
|
||||
return result or "[Decompile output empty]"
|
||||
|
||||
@server.tool()
|
||||
def seep_r2_diff(file_a: str, file_b: str, mode: str = "code") -> str:
|
||||
@@ -366,6 +464,43 @@ def seep_r2_asm(code_or_hex: str, arch: str = "x86", bits: int = 64, disasm: boo
|
||||
# 3. Android APK 逆向与动态 Hook 生成
|
||||
# ---------------------------------------------------------------------------
|
||||
@server.tool()
|
||||
def seep_r2_xrefs(
|
||||
binary_path: str,
|
||||
target: str,
|
||||
direction: str = "to",
|
||||
limit: int = 10,
|
||||
) -> str:
|
||||
"""获取指定地址/函数的交叉引用关系,内置分页与统计摘要避免大量 xref 结果内爆上下文。
|
||||
|
||||
Args:
|
||||
binary_path: 目标二进制文件路径
|
||||
target: 目标函数名或地址(如 'sym.check_license', '0x401234')
|
||||
direction: 方向— 'to'=谁引用了它(调用方), 'from'=它引用了谁(被调用方)
|
||||
limit: 最多返回多少条引用(默认 10),超出部分附加统计摘要
|
||||
"""
|
||||
if not os.path.isfile(binary_path):
|
||||
return f"[ERROR]: Target file does not exist: {binary_path}"
|
||||
|
||||
r2_cmd = f"aaa; {'axt' if direction == 'to' else 'axf'} @ {target}"
|
||||
out = _run_cmd([R2_EXE, "-q", "-e", "scr.color=0", "-c", r2_cmd, os.path.abspath(binary_path)], timeout=60)
|
||||
cleaned = _clean_r2_output(out)
|
||||
lines = [l for l in cleaned.splitlines() if l.strip() and not l.startswith("INFO:") and not l.startswith("WARN:")]
|
||||
|
||||
total = len(lines)
|
||||
shown = lines[:limit]
|
||||
result_lines = [
|
||||
f"[XREFS {direction.upper()} — {target} | 共 {total} 条引用,显示前 {min(limit, total)} 条]",
|
||||
f"注意: 若 total 远大于 limit,考虑缩小 limit 或先查 summary 模式确定目标函数。",
|
||||
"━" * 60,
|
||||
] + shown
|
||||
|
||||
if total > limit:
|
||||
result_lines.append(f"... [已截断,剩余 {total - limit} 条未展示 | 增大 limit 或用 filter_name 缩小目标范围]"
|
||||
f"\n模块来源分布小计: {dict(list({l.split()[2] if len(l.split())>2 else '?' for l in lines}))[:3]}"
|
||||
)
|
||||
|
||||
return "\n".join(result_lines)
|
||||
@server.tool()
|
||||
def seep_apk_info(apk_path: str) -> str:
|
||||
"""原生解析 APK 压缩包结构:统计 DEX 文件、Native .so 架构动态库、资源分布,并提取 AndroidManifest.xml 关键字符串(免 Java 环境)。
|
||||
|
||||
|
||||
@@ -241,6 +241,18 @@ metadata:
|
||||
| `references/license-validation.md` | 393 | 许可 / 激活 / 卡密 / 订阅类目标(16 节全量手册) |
|
||||
| `references/tooling.md` | 42 | 找现成脚本 / 工程模板时 |
|
||||
|
||||
### MANUAL/ 战术手册索引(按需查阅)
|
||||
|
||||
> MANUAL/ 存放深度专项 SOP,由 Agent 在触碰对应信号时主动加载。
|
||||
|
||||
| 手册 | 触发条件 | 核心内容 |
|
||||
| :--- | :--- | :--- |
|
||||
| `MANUAL/ANTI-DEBUG.md` | G0/G1 检测到 `IsDebuggerPresent`、`NtQueryInformationProcess`、驱动级检测、时序反调试等信号 | 全量反调试绕过字典、代理 DLL 打桩框架、内核层 SOP 与两击熔断规则 |
|
||||
| `MANUAL/UNPACKING.md` | G0/G1 检测到 `MPRESS/UPX/Themida/.vmp` 节名,或 `seep_auto_triage` 熵值 > 7.2 | 壳类型速查表、OEP 定位三策略、Scylla 内存 Dump 全流程、VMP 动态 trace 路线 |
|
||||
| `MANUAL/POC-VALIDATION.md` | G4/G5 生成 PoC 后(Frida 脚本 / Smali 补丁 / 代理 DLL)必读 | 三阶段闭环验证模型、Frida 自愈映射表、Smali/DLL 本地加载器、断网铁证流程 |
|
||||
| `MANUAL/IDA-PRO.md` | 用户询问 IDA Pro 接入或 ida MCP 无法启动时 | IDA 安装配置、install-ida.ps1、Radare2 降级路线 |
|
||||
| `MANUAL/PREREQUISITES.md` | 首次部署或环境报错时 | Python/Node/JDK/ADB 版本要求与修复指引 |
|
||||
|
||||
### 快速对应
|
||||
|
||||
| 你要做的事 | 加载 |
|
||||
|
||||
+29
-2
@@ -157,7 +157,7 @@ if (Test-Path $AgentDir) {
|
||||
# -----------------------------------------------------------------------------
|
||||
# 5. MCP 自动化服务层与底层工具 (MCP & Native Tools)
|
||||
# -----------------------------------------------------------------------------
|
||||
Write-Host "`n[5/6] 🔌 MCP 服务引擎与物理内置工具箱 (Native Tools)" -ForegroundColor White
|
||||
Write-Host "`n[5/7] 🔌 MCP 服务引擎与物理内置工具箱 (Native Tools)" -ForegroundColor White
|
||||
|
||||
Test-CheckItem "MCP" "核心服务端脚本 (seep_mcp_server.py 语法自洽)" {
|
||||
$sp = Join-Path $ToolDir 'mcp\seep_mcp_server.py'
|
||||
@@ -206,7 +206,7 @@ Test-CheckItem "依赖" "Playwright 浏览器自动化依赖已解压 (playwrigh
|
||||
# -----------------------------------------------------------------------------
|
||||
# 6. 环境运行时与第三方依赖配置
|
||||
# -----------------------------------------------------------------------------
|
||||
Write-Host "`n[6/6] ⚙️ 外部运行时与商业授权协同 (Runtime & Commercial)" -ForegroundColor White
|
||||
Write-Host "`n[6/7] ⚙️ 外部运行时与商业授权协同 (Runtime & Commercial)" -ForegroundColor White
|
||||
Test-CheckItem "运行时" "Python 解释器 (3.11+ 且可执行)" {
|
||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
||||
$py -ne $null
|
||||
@@ -235,6 +235,33 @@ Test-ManualItem "配置" "Claude Code 项目级 MCP 注册 (.mcp.json 在根目
|
||||
# -----------------------------------------------------------------------------
|
||||
# 汇总仪表盘
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 7. MANUAL/ 战术手册完备性校验 (Tactical Manuals)
|
||||
# -----------------------------------------------------------------------------
|
||||
Write-Host "`n[7/7] 📚 MANUAL/ 战术手册完备性校验" -ForegroundColor White
|
||||
$ManualDir = Join-Path $Root 'MANUAL'
|
||||
|
||||
Test-CheckItem "手册" "环境预要求指南 (MANUAL/PREREQUISITES.md)" {
|
||||
Test-Path (Join-Path $ManualDir 'PREREQUISITES.md')
|
||||
} "安装指南缺失,重新拉取仓库"
|
||||
|
||||
Test-CheckItem "手册" "IDA Pro 商业软件接入指南 (MANUAL/IDA-PRO.md)" {
|
||||
Test-Path (Join-Path $ManualDir 'IDA-PRO.md')
|
||||
} "IDA-PRO 指南缺失"
|
||||
|
||||
Test-CheckItem "手册" "反调试绕过战术手册 (MANUAL/ANTI-DEBUG.md)" {
|
||||
Test-Path (Join-Path $ManualDir 'ANTI-DEBUG.md')
|
||||
} "调试应对手册缺失,重新拉取仓库"
|
||||
|
||||
Test-CheckItem "手册" "通用脱壳前置分析 SOP (MANUAL/UNPACKING.md)" {
|
||||
Test-Path (Join-Path $ManualDir 'UNPACKING.md')
|
||||
} "脱壳手册缺失,重新拉取仓库"
|
||||
|
||||
Test-CheckItem "手册" "PoC 闭环自动化验证 SOP (MANUAL/POC-VALIDATION.md)" {
|
||||
Test-Path (Join-Path $ManualDir 'POC-VALIDATION.md')
|
||||
} "PoC 验证手册缺失,重新拉取仓库"
|
||||
|
||||
Write-Host "`n================================================================================" -ForegroundColor White
|
||||
Write-Host (" [体检报告] 核心检查通过: {0} 项" -f $script:OkCount) -ForegroundColor Green
|
||||
|
||||
|
||||
Reference in new issue
Block a user