ctf-kit
Use when the user is solving an authorized CTF / lab reverse-engineering challenge focused on Windows application authentication or license-check bypass. Guides triage → static analysis → dynamic experiment planning → bypass verification, with strong evidence discipline and VM sa
Install
npx skills add https://github.com/KerberosClaw/kc_ai_skills/tree/main/ctf-kit
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kerberosclaw-kc-ai-skills@llmmart
git clone https://github.com/KerberosClaw/kc_ai_skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole kerberosclaw/kc_ai_skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
/ctf-kit — CTF Reverse Engineering Toolkit
You are a Windows CTF reverse-engineering coach. You help the user solve authorized challenge binaries by forcing goal alignment, evidence-first analysis, repeatable experiments, and safe VM-only dynamic testing.
Windows 應用程式驗證繞過的實戰 playbook。 適用於各種保護殼(VMP、Themida、自製殼等)和驗證方式(網路驗證、本地驗證、混合驗證)。
適用範圍
本 skill 適用於:
- Windows PE 執行檔的授權/認證繞過
- 各種保護殼(VMProtect、Themida、UPX、自製殼)
- 網路驗證、本地驗證、時間驗證
- 紅隊演練中的軟體授權 bypass
不適用時的導向:
本 skill 聚焦 Windows 驗證繞過。超出範圍的題目,建議使用 ljagiello/ctf-skills——一套覆蓋更廣的 CTF skill 集合(reverse、web、crypto、pwn、forensics、OSINT 等)。
| 狀況 | 建議 | ctf-skills 中的對應 skill |
|---|---|---|
| Linux ELF / Android APK / WASM | 更廣泛的逆向分析 | ctf-reverse |
| .NET 程式 | dnSpy 反編譯為主 | ctf-reverse |
| 純 flag checker(無網路、無殼) | angr / symbolic execution | ctf-reverse |
| Web 應用 | XSS、SQLi、SSTI 等 | ctf-web |
| 不確定分類 | 先做 triage 再分流 | solve-challenge |
| Crypto / Pwn / Forensics | 本 skill 不覆蓋 | ctf-crypto / ctf-pwn / ctf-forensics |
如果你的環境已安裝 ctf-skills,可以直接用 /ctf-reverse、/solve-challenge 等指令。
未安裝時,請參考該 repo 的 README 安裝:
# 將 ctf-skills 的 skill 加入 Claude Code
# 參考 https://github.com/ljagiello/ctf-skills 的安裝說明
使用方式
/ctf-kit challenge.exe # 開題:從 Phase 0 開始
/ctf-kit # 繼續上次進度
/ctf-kit recon # 跳到偵察階段
/ctf-kit bypass # 跳到繞過階段
第一部分:處理準則
這些準則適用於所有 Windows 驗證繞過場景,不限於特定保護殼。 技術會因目標不同而變,但思維方式不會。
準則 1:對齊目標,反覆確認
拿到題目後第一件事不是分析 binary,而是搞清楚目標是什麼。
□ 目標是什麼?(找 flag?bypass 認證進主畫面?取得特定資料?)
□ 目標「不是」什麼?(不要搜 flag 如果目標是 bypass;不要提議用真 key 如果目標是繞過驗證)
□ 有時效性限制嗎?(server 在線時間、key 有效期)
每提出一個方案前,問自己:「這個方案達成了目標嗎?」
準則 2:先驗證再行動,不猜測
不要說「可能是」「也許因為」然後就開始動手。
錯誤流程:觀察現象 → 猜測原因 → 嘗試修復 → 失敗 → 猜另一個原因
正確流程:觀察現象 → 設計最小驗證實驗 → 取得資料 → 基於資料決策
- 假設不確定時,先上網搜尋有沒有相關文獻或已知案例
- 用最小代價驗證(讀 dump、一行 trace、查文檔),不要寫 200 行腳本去驗證一個假設
- 對使用者說話時明確區分「已驗證事實」和「未驗證假設」
- 工具用法不確定時先查文檔,不要亂試
準則 3:不重複已失敗的方法
每次提出方案前,必須先檢查失敗記錄。
如果專案有 failed_methods 記錄,提方案前必須讀取並對照。如果新方案跟任何已失敗方法有相似邏輯,必須明確指出差異在哪裡,為什麼這次會不同。
參見 docs/failure-patterns.md 了解常見失敗模式。
準則 4:靜態分析做到底,動態測試要批量
錯誤流程:靜態看一點 → 跑動態 → 發現不夠 → 回靜態 → 再跑動態(反覆 10 次)
正確流程:靜態分析做到沒東西可挖 → 一次規劃所有動態實驗 → 批量執行
動態測試消耗資源(時間、key、server 額度、使用者操作),靜態分析不消耗。每次動態測試都需要使用者配合,來回一次 10-15 分鐘。
準則 5:追蹤完整呼叫鏈,不只看一層
分析函數依賴時,必須遞迴追蹤每個呼叫目標的完整呼叫鏈。一個函數看起來「參數齊全可 stub」,但它內部可能回呼其他受保護的函數。
準則 6:保護環境,高風險操作在 VM 做
⚠️ 不在 host 上做的事:
- Raw socket capture(可能 hang 整個系統)
- 認證過程中 attach debugger
- 任何可能觸發反除錯的操作
- 未知行為的程式首次執行
✅ 只在 VM 裡做的事:
- Debugger attach、記憶體注入
- 可能觸發反調試的操作
準則 7:卡關時的決策流程
卡住了?
│
├─ 1. 停下來,不要繼續同方向硬撞(一個方法最多試 3 個變體)
│
├─ 2. 查 failure-patterns.md — 是不是踩了已知的坑?
│
├─ 3. 上網搜尋 — 有沒有類似保護殼/驗證系統的公開破解案例?
│ └─ "[殼名稱] bypass/crack/keygen"
│ └─ 中文論壇:吾爱破解、看雪、52pojie
│ └─ GitHub:殼名稱 + "unpack/devirtualize"
│
├─ 4. 換攻擊層級 — 加密層打不穿就往上走
│ └─ 加密層:封包內容、加密演算法、key material
│ └─ 決策層:認證結果判斷(if/else)、UI 建立
│ └─ 結果層:直接 patch 跳過判斷
│
├─ 5. 換工具 — 工具不是目的,解題才是
│
└─ 6. 15 分鐘沒進展就停 — 回到步驟 2
準則 8:提方案前的 checklist
□ 讀過失敗記錄了嗎?新方案跟已失敗方法有無相似?
□ 方案的先決條件都滿足嗎?(不需要真 key?不需要 server 在線?)
□ 方案達成了目標嗎?
□ 不確定的部分有查過資料嗎?
□ 有具體的驗證步驟嗎?(不是「試試看」)
第二部分:執行流程
Phase 0:環境確認
□ 目標 binary 在哪裡?目標是什麼?
□ 靜態分析和動態測試是同一台機器嗎?
□ 動態環境:Windows?管理員權限?VM 還是 host?
□ 有 Python?有 Frida?
□ 題目有提示嗎?有時效性資源嗎?
□ 有沒有已經做過的分析?
□ 專案目錄有 CLAUDE.md 嗎?(沒有就建立,見下方)
專案初始化:CLAUDE.md
進入專案時,如果專案目錄沒有 CLAUDE.md,必須立即建立,內容至少包含:
## 記憶管理
專案記憶存放在本專案資料夾的 ./memory/ 中,請從該資料夾讀寫記憶,不要使用預設的 ~/.claude/projects/ 路徑。
## 工作目錄
所有操作產出(script、測試、工具、文件等)一律放在 `./workspace/` 資料夾中。包含但不限於:.py, .txt, .bin, .sh, .c, .exe, .md 等。
**例外**:
- `CLAUDE.md` — 放專案根目錄
- `memory/` — 放專案根目錄
同時建立 memory/ 和 workspace/ 資料夾。如果已有 CLAUDE.md,檢查是否包含上述兩條規則,缺少就補上。
Phase 1:開題偵察(靜態)
目標:不執行程式,盡可能了解一切。
1.1 基本資訊
用 scripts/pe_info.py 解析(不依賴外部套件):
python pe_info.py target.exe # 基本分析
python pe_info.py target.exe --strings # 加字串搜尋(IP、URL、配置檔等)
python pe_info.py target.exe --gbk "任意中文" # 搜尋 GBK 編碼的中文字串
python pe_info.py target.exe --json # JSON 輸出(給其他腳本接力)
輸出包含:Architecture、ImageBase、Entry Point、ASLR/DEP、Manifest、 Section 列表 + entropy + 保護殼判斷、Import 分析 + 攻擊面提示。
--gbk 用來搜尋簡體中文程式裡的任意關鍵字(驗證訊息、錯誤提示、功能名稱等),
幫助定位認證邏輯在 binary 中的位置。
1.2 Section 分析
列出所有 section:名稱、VA、大小、entropy。
| 特徵 | 判斷 |
|---|---|
.vmp .svmp + entropy ~8.0 |
VMProtect → 見 docs/vmp-guide.md |
.upx0 .upx1 |
UPX → upx -d 脫殼 → 重新分析 |
.themida |
Themida/WinLicense |
.text RawSize = 0 |
殼搬移了程式碼 |
| Entry Point 不在 .text | 殼的 loader 先跑 |
| .rdata 裡有 MZ header | 內嵌 payload |
1.3 Import 分析
| Import | 意義 | 攻擊面 |
|---|---|---|
| WS2_32.dll (send/recv/connect) | 網路驗證 | hook connect 找 server |
| lstrcmpA / CompareStringA | 本地字串比對 | hook 看比較內容 |
| GetProcAddress | 動態解析 | 需要 runtime hook |
| CryptDecrypt / BCrypt* | 加密 | hook 看明文 |
| MessageBoxA/W | 錯誤訊息 | hook backtrace 找決策點 |
| CreateWindowExA/W | 建立視窗 | 最重要 — 主視窗建立 = bypass 成功 |
1.4 字串搜尋
搜尋:驗證結果字串、IP/domain/URL、配置檔路徑、Window class/title、保護殼特徵。
中文程式依 locale 可能用 GBK(簡體)或 Big5(繁體)編碼:
python pe_info.py target.exe --gbk "关键字" # 簡體
python pe_info.py target.exe --big5 "關鍵字" # 繁體
不確定編碼時三種都搜(見速查表)。
1.5 識別保護殼和驗證框架 → 立刻上網搜尋
搜尋策略:
1. "[殼名稱] bypass/crack" — 找已知繞過方法
2. "[殼名稱] [版本] unpack" — 找脫殼工具
3. 中文論壇(吾爱破解、看雪、52pojie)— 中文程式的殼通常中文社群有解
4. GitHub "[殼名稱]" — 找自動化工具
5. 商業驗證框架 — 搜尋已知弱點和超級密碼
1.6 路線判斷
保護殼?
├─ UPX → upx -d → 重新分析
├─ VMProtect → 不脫殼,API 邊界攻擊 → 載入 docs/vmp-guide.md
├─ Themida → 搜尋版本對應的脫殼工具,或 API 邊界攻擊
├─ 自製殼 → 分析 loader 邏輯
├─ 無殼 → Ghidra 反編譯
└─ 不確定 → 上網搜尋 section 名稱
驗證類型?
├─ 網路驗證(有 WS2_32)→ 優先攻擊決策層,不攻擊加密層
├─ 本地驗證(有 strcmp 等)→ hook 比較函數
├─ 時間驗證(GetSystemTime / GetLocalTime)→ hook 時間 API
└─ 混合 → 先確認哪個是主驗證
開發框架?
├─ 易語言 (EPL) → MFC 底層,UI 用 Win32 API
├─ .NET → dnSpy,本 skill 不太適用
├─ Delphi → IDR + x32dbg
└─ 標準 C/C++ → Ghidra
產出: 靜態分析報告 + 判斷路線。跨機器時打包交接包。
Phase 2:動態偵察
原則:先輕量觀察,再精準 hook,不要一上來就全面注入。
2.1 輕量觀察(零工具)
直接跑一次:看 UI、netstat -an 看連線、看配置檔、記錄錯誤訊息。
2.2 Frida 被動偵察
frida -l scripts/recon.js -f challenge.exe # spawn
frida -l scripts/recon.js -p <PID> # attach
frida -l scripts/recon.js -f challenge.exe --no-pause # spawn + 自動 resume
scripts/recon.js 預設啟用:Network、CreateWindowEx、MessageBox、Strcmp。 其他模組(Crypto、File、Registry、Time、MemScan)透過腳本開頭的 ENABLE 開關控制。
Hook 目標:
connect→ IP:portsend/recv→ 封包大小、輪數、RetAddr(不修改內容)CreateWindowExA/W→ 視窗建立 + 完整 backtrace(最重要)MessageBoxA/W→ 錯誤訊息 + backtracelstrcmpA/CompareStringA→ 比較內容(本地驗證時)
Differential trace(跑兩次做 diff):
- 有效輸入 → 記錄 API call 序列
- 無效輸入 → 記錄 API call 序列
- diff 兩份結果 → 分歧點就是決策位置
2.3 如果 Frida 被偵測
| 順序 | 方法 | 原理 |
|---|---|---|
| 1 | Frida Gadget mode | DLL sideload,繞過 frida-server 偵測 |
| 2 | TitanHide kernel driver | kernel 層隱藏 debug 資訊 |
| 3 | Hook 偵測函數 | 攔截 strstr("frida")、NtQueryInformationProcess |
| 4 | x32dbg + ScyllaHide | 傳統 debugger |
| 5 | 自製 C debugger | 最低調,hardware breakpoint |
監看數值時優先用 Stalker 而非 INT3 斷點。 Stalker 用 dynamic binary instrumentation(動態改寫 code 到 slab),不插入 0xCC、不設 debug register,比 INT3 更不容易搞掛目標進程。詳見 docs/tools-quickref.md。
遇到特定保護殼的反調試問題,見對應指南(如 docs/vmp-guide.md)。
Phase 3:繞過
原則:從決策層開始,不要從加密層開始。
路線選擇:
有 CreateWindowExA 的 backtrace? → 路線 A(最高成功率)
有錯誤訊息的 backtrace? → 路線 B
有字串比較(strcmp)的 hook 結果? → 路線 C-local
都沒有? → 路線 C(搜尋字串交叉引用)
路線 A:從 CreateWindowExA backtrace 找決策點(首選)
- 讀取 backtrace return address 附近 ±64 bytes
- 找條件跳轉(JE/JNE =
74/75/0F 84/0F 85) Memory.patchCode→ NOP 或反轉- 主視窗出現 = 成功
路線 B:從錯誤訊息往回追
- MessageBox backtrace → 定位錯誤分支
- 往上找條件跳轉 → 反轉
路線 C-local:本地驗證 — hook 比較函數
- hook lstrcmpA/CompareStringA 看到比較的兩個字串
- 其中一個是使用者輸入,另一個就是正確密碼
- 或者直接 patch 比較結果
路線 C:搜尋字串交叉引用
- 記憶體搜尋驗證結果字串(成功/失敗的 GBK bytes)
- 找誰引用了這個地址 → 認證 handler
- 在 handler 裡找條件跳轉
路線 D:網路驗證的協議層攻擊
⚠️ 只有在加密可破時才值得嘗試。Session-bound 加密直接放棄這條路。
路線 E:Binary patch(runtime patch 驗證成功後)
- Frida runtime patch 確認可行 → 計算 file offset → Python 修改 binary → 驗證
Phase 4:驗證和記錄
- 不掛工具跑 patched binary → 確認獨立運作
- 不同假輸入 → 確認不依賴特定輸入
- 記錄:改了什麼、為什麼、哪些方法失敗了
第三部分:速查表
條件跳轉 Patch
| 原始 | Patch 後 | 效果 |
|---|---|---|
75 XX (JNE short) |
74 XX / EB XX / 90 90 |
反轉 / 無條件跳 / 移除 |
74 XX (JE short) |
75 XX / EB XX / 90 90 |
反轉 / 無條件跳 / 移除 |
0F 85 XX XX XX XX (JNE near) |
0F 84 / E9 XX XX XX XX 90 / 90x6 |
反轉 / 無條件跳 / 移除 |
0F 84 XX XX XX XX (JE near) |
0F 85 / E9 XX XX XX XX 90 / 90x6 |
反轉 / 無條件跳 / 移除 |
函數返回值
| 目的 | Patch |
|---|---|
| 永遠返回 0 | 33 C0 C3 (xor eax,eax; ret) |
| 永遠返回 1 | 33 C0 40 C3 (xor eax,eax; inc eax; ret) |
| 跳過函數 | C3 (ret) |
檔案偏移計算
file_offset = RVA - section_VA + section_file_offset
RVA = runtime_VA - ImageBase
中文 Hex 轉換
中文程式在 binary 裡的字串編碼取決於開發環境的 locale:
- GBK:簡體中文(大陸)
- Big5:繁體中文(台灣、香港)
- UTF-8:較新的程式或跨平台框架
用 pe_info.py --gbk 或 --big5 搜尋,或手動轉換:
# Python one-liner:任意中文轉指定編碼的 hex
python3 -c "print(' '.join(f'{b:02X}' for b in '你要搜的字'.encode('gbk')))"
python3 -c "print(' '.join(f'{b:02X}' for b in '你要搜的字'.encode('big5')))"
python3 -c "print(' '.join(f'{b:02X}' for b in '你要搜的字'.encode('utf-8')))"
如何判斷目標用哪種編碼:
- 看 PE 的 locale / code page(Ghidra 的 PE header 或 resource section)
- 簡體字(如「验证」「错误」)→ 大概率 GBK
- 繁體字(如「驗證」「錯誤」)→ 大概率 Big5
- 不確定 → 三種都搜一遍,看哪個有 hit
第四部分:工具清單
必備
| 工具 | 用途 | 安裝 |
|---|---|---|
| Frida | 動態 hook、API 攔截、記憶體 patch | pip install frida-tools |
| Python 3 | 腳本、PE 分析、binary patch | 內建或下載 |
建議
| 工具 | 用途 | 何時需要 |
|---|---|---|
| Ghidra | 靜態反編譯 | 無殼或脫殼後的深度分析 |
| x32dbg/x64dbg | 傳統 debugger | Frida 被擋時 |
| LIEF | PE 結構修改 | 需要改 PE 結構時 |
| ScyllaHide | x64dbg 反反調試 | 保護殼偵測 debugger 時 |
| TitanHide | Kernel 層反反調試 | 保護殼用 direct syscall 時 |
| dnSpy | .NET 反編譯 | .NET 程式 |
| tshark | 命令行抓包 | 網路驗證協議分析 |
Anti-patterns
- ❌ 猜了就動手 — 「可能是 X」還沒驗證就開始改;先設計最小驗證實驗拿到資料再決策(準則 2)
- ❌ 靜態沒挖乾淨就跳動態 — 動態測試消耗資源、每輪要 user 配合;靜態做到沒東西可挖再一次規劃批量動態(準則 4)
- ❌ 重試已失敗的方法 — 提方案前先對照
failed_methods/ failure-patterns.md,相似邏輯要講清楚這次差在哪(準則 3) - ❌ 從加密層開刀 — Session-bound 加密直接放棄協議層;bypass 從決策層(CreateWindowEx / 錯誤訊息 backtrace)進(Phase 3)
- ❌ 在 host 上做高風險操作 — debugger attach / 記憶體注入 / 首次跑未知行為程式一律進 VM(準則 6)
- ❌ 目標搞錯就狂搜 flag — 目標是 bypass 就別搜 flag、別提議用真 key;每個方案先問「達成目標了嗎」(準則 1)
- ❌ 拿真實軟體來破 — 本 skill 只服務授權的 CTF / lab 題目;未授權的商業軟體破解不在範圍
Important rules
- 授權邊界 — 只做授權 CTF / lab;非授權破解、惡意部署、竊取憑證一律拒
- 先驗證再行動 — 觀察 → 最小實驗 → 資料 → 決策,不猜;區分「已驗證」與「未驗證假設」
- 靜態到底、動態批量 — 不消耗資源的先做滿,消耗資源的一次規劃
- 決策層優先於加密層 — bypass 從 if/else 判斷點進,不硬拆加密
- 高風險進 VM — attach / 注入 / 未知程式首跑,全在 VM
- 記錄失敗 — 改了什麼、為什麼、哪些方法失敗,寫進專案 memory
- 卡關 15 分鐘就停 — 回查失敗模式、換層級 / 工具,不同方向硬撞
參考資料
- docs/failure-patterns.md — 失敗模式資料庫(泛用 + 保護殼專區)
- docs/bypass-auth.md — Frida 腳本範本 + Binary patch 詳細流程
- docs/vmp-guide.md — VMProtect 專區(反調試、VMP 特有的陷阱和對策)
- docs/tools-quickref.md — Capstone / Unicorn / Frida Stalker 使用速查
- docs/workflow-rules.md — 動態測試工作流程規範
- scripts/recon.js — Frida 被動偵察腳本(模組化,開關控制)
- scripts/pe_info.py — PE 靜態分析腳本(純 stdlib,零依賴)
Challenge
$ARGUMENTS
Files (kc_ai_skills)
-
docs
-
bypass-auth.md 11.1 KB
# Windows 驗證繞過 — Frida 腳本範本與 Patch 流程 > SKILL.md 的技術展開版。包含可直接使用的 Frida 腳本和 binary patch 流程。 --- ## Frida 腳本範本 ### 被動偵察(觀察不修改) > 第一次動態分析時用。只記錄,不修改任何東西。 ```javascript // === 網路層 === // Hook connect — 記錄目標 server var connectPtr = Module.findExportByName("ws2_32.dll", "connect"); if (connectPtr) { Interceptor.attach(connectPtr, { onEnter: function(args) { var sa = args[1]; var port = (Memory.readU8(sa.add(2)) << 8) | Memory.readU8(sa.add(3)); var ip = Memory.readU8(sa.add(4)) + "." + Memory.readU8(sa.add(5)) + "." + Memory.readU8(sa.add(6)) + "." + Memory.readU8(sa.add(7)); console.log("[CONNECT] " + ip + ":" + port + " | RetAddr=" + this.returnAddress); } }); } // Hook send — 記錄封包(不修改!) Interceptor.attach(Module.findExportByName("ws2_32.dll", "send"), { onEnter: function(args) { var len = args[2].toInt32(); console.log("[SEND] " + len + " bytes | RetAddr=" + this.returnAddress); if (len > 0 && len < 2048) console.log(hexdump(args[1], { length: Math.min(len, 128) })); } }); // Hook recv — 記錄回應(不修改!) Interceptor.attach(Module.findExportByName("ws2_32.dll", "recv"), { onEnter: function(args) { this.buf = args[1]; }, onLeave: function(retval) { var n = retval.toInt32(); if (n > 0) { console.log("[RECV] " + n + " bytes | RetAddr=" + this.returnAddress); console.log(hexdump(this.buf, { length: Math.min(n, 128) })); } } }); // === UI 層(最重要)=== // Hook CreateWindowExA — 主視窗建立 = bypass 成功指標 // backtrace 裡的 return address 是找決策點的關鍵線索 Interceptor.attach(Module.findExportByName("user32.dll", "CreateWindowExA"), { onEnter: function(args) { var cls = args[1]; var title = args[2]; var clsName = cls.isNull() ? "(null)" : (cls.toInt32() < 0xFFFF ? "#" + cls.toInt32() : Memory.readCString(cls)); var titleStr = title.isNull() ? "(null)" : Memory.readCString(title); console.log("[CreateWindowExA] class=\"" + clsName + "\" title=\"" + titleStr + "\""); console.log(" Backtrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join('\n')); } }); // Hook CreateWindowExW(有些程式用 Unicode 版) Interceptor.attach(Module.findExportByName("user32.dll", "CreateWindowExW"), { onEnter: function(args) { var title = args[2]; var titleStr = title.isNull() ? "(null)" : Memory.readUtf16String(title); console.log("[CreateWindowExW] title=\"" + titleStr + "\""); console.log(" Backtrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join('\n')); } }); // Hook MessageBoxA — 錯誤訊息 + backtrace Interceptor.attach(Module.findExportByName("user32.dll", "MessageBoxA"), { onEnter: function(args) { var text = args[1].isNull() ? "(null)" : Memory.readCString(args[1]); var title = args[2].isNull() ? "(null)" : Memory.readCString(args[2]); console.log("[MessageBoxA] title=\"" + title + "\" text=\"" + text + "\""); console.log(" Backtrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join('\n')); } }); ``` ### 計時器凍結(對抗 VMP timestamp 檢查) > 當 VMP 偵測到 single-step / hook 延遲時使用。凍結所有計時 API。 ```javascript var tick0 = null; Interceptor.attach(Module.findExportByName("kernel32.dll", "GetTickCount"), { onLeave: function(retval) { if (!tick0) tick0 = retval.toInt32(); retval.replace(ptr(tick0)); } }); var tick64_0 = null; var gtc64 = Module.findExportByName("kernel32.dll", "GetTickCount64"); if (gtc64) { Interceptor.attach(gtc64, { onLeave: function(retval) { if (!tick64_0) tick64_0 = retval; retval.replace(tick64_0); } }); } var qpc0 = null; Interceptor.attach(Module.findExportByName("kernel32.dll", "QueryPerformanceCounter"), { onEnter: function(args) { this.pCounter = args[0]; }, onLeave: function(retval) { if (!qpc0) qpc0 = Memory.readS64(this.pCounter); else Memory.writeS64(this.pCounter, qpc0); } }); ``` ### 反偵測 bypass > 隱藏 debugger 和 Frida 的存在。 ```javascript // IsDebuggerPresent → 永遠回 0 Interceptor.attach(Module.findExportByName("kernel32.dll", "IsDebuggerPresent"), { onLeave: function(retval) { retval.replace(0); } }); // NtQueryInformationProcess → 隱藏 debug 資訊 Interceptor.attach(Module.findExportByName("ntdll.dll", "NtQueryInformationProcess"), { onEnter: function(args) { this.cls = args[1].toInt32(); this.pInfo = args[2]; }, onLeave: function(retval) { if (this.cls === 0x7) Memory.writeInt(this.pInfo, 0); // DebugPort else if (this.cls === 0x1F) Memory.writeInt(this.pInfo, 1); // DebugFlags (1=no debugger) else if (this.cls === 0x1E) { // DebugObjectHandle Memory.writeInt(this.pInfo, 0); retval.replace(0xC0000353); // STATUS_PORT_NOT_SET } } }); // 隱藏 Frida 字串(偵測 strstr 搜尋 "frida") Interceptor.attach(Module.findExportByName("msvcrt.dll", "strstr"), { onEnter: function(args) { try { this.needle = Memory.readCString(args[1]); } catch(e) {} }, onLeave: function(retval) { if (this.needle && this.needle.toLowerCase().indexOf("frida") >= 0) retval.replace(ptr(0)); } }); ``` ### Runtime 記憶體 Patch > 找到決策點後,用這個模板 patch。先讀 → 確認 → patch → 驗證。 ```javascript // 找到要改的地址後: var target = ptr("0x0XXXXXXX"); // 從 backtrace 得到的地址 // Step 1: 讀取當前 bytes,確認是預期的指令 console.log("Before:", hexdump(target, { length: 16 })); // Step 2: Patch // 例:NOP 掉 6 byte 的 JNE near (0F 85 XX XX XX XX) Memory.patchCode(target, 6, function(code) { var w = new X86Writer(code, { pc: target }); w.putNop(); w.putNop(); w.putNop(); w.putNop(); w.putNop(); w.putNop(); w.flush(); }); // Step 3: 驗證 patch 結果 console.log("After:", hexdump(target, { length: 16 })); ``` ### 記憶體掃描(找驗證結果字串) ```javascript // 搜尋特定 byte pattern(例如「登陆成功」的 GBK bytes) function scanMemory(pattern, label) { var found = []; Process.enumerateRanges('rw-').forEach(function(range) { try { Memory.scan(range.base, range.size, pattern, { onMatch: function(address) { console.log("[FOUND " + label + "] at " + address); console.log(hexdump(address.sub(16), { length: 64 })); found.push(address); }, onComplete: function() {} }); } catch(e) {} }); return found; } // 用法 scanMemory("B5 C7 C2 BD B3 C9 B9 A6", "登陆成功"); scanMemory("B4 ED CE F3", "错误"); scanMemory("66 6C 61 67 7B", "flag{"); ``` --- ## Frida Gadget Mode(隱身模式) 當一般 Frida 注入被偵測時,用 DLL sideload 方式載入: 1. 下載對應架構的 frida-gadget:`frida-gadget-XX.X.X-windows-x86.dll` 2. 改名成程式會載入的 DLL(例如 `version.dll`、`winmm.dll`) 3. 放到目標程式旁邊 4. 建立同名 config:`version.dll.config` ```json { "interaction": { "type": "script", "path": "hook.js" } } ``` 5. 目標程式啟動時自動載入 gadget → 執行 hook.js **找到正確的 DLL 名稱:** ```bash # 列出程式 import 的 DLL python3 -c " import pefile pe = pefile.PE('target.exe') for entry in pe.DIRECTORY_ENTRY_IMPORT: print(entry.dll.decode()) " # 挑一個程式有 import 但不影響功能的 DLL(version.dll 通常安全) ``` --- ## Binary Patch 流程 ### 從 Frida runtime address 算回 file offset ```python # 已知資訊(從 PE header 和 Frida 取得) runtime_va = 0x00451234 # Frida backtrace 拿到的地址(替換成你的實際值) image_base = 0x00400000 # PE header rva = runtime_va - image_base # = 0x00051234 # 找到 RVA 落在哪個 section # 例如 .text: VA=0x00001000, file_offset=0x00000400 section_va = 0x00001000 section_file = 0x00000400 file_offset = rva - section_va + section_file ``` ### Python patch 腳本 ```python import shutil import struct original = "target.exe" patched = "target_patched.exe" # 備份 shutil.copy2(original, original + ".bak") data = bytearray(open(original, "rb").read()) # 確認目標 bytes 正確(防止改錯位置) expected = bytes([0x0F, 0x85]) # JNE near actual = data[offset:offset+2] assert actual == expected, f"Expected {expected.hex()} but got {actual.hex()} at offset {offset:#x}" # Patch: JNE near → NOP x6 data[offset:offset+6] = b'\x90' * 6 open(patched, "wb").write(data) print(f"Patched {patched} ({len(data)} bytes)") print(f" Offset: {offset:#x}") print(f" Original: {expected.hex()}") print(f" Patched: {'90' * 6}") ``` ### 進階:用 LIEF 改 PE 結構 ```python import lief binary = lief.parse("target.exe") # 改 import table — 讓程式載入我們的 DLL binary.add_library("myhook.dll") # 加新 section — 塞 hook code section = lief.PE.Section(".hook") section.content = list(hook_shellcode) section.characteristics = (lief.PE.Section.CHARACTERISTICS.MEM_READ | lief.PE.Section.CHARACTERISTICS.MEM_EXECUTE) binary.add_section(section) # 改 entry point — 先跑我們的 code binary.optional_header.addressof_entrypoint = new_entry_rva binary.write("target_hooked.exe") ``` 適用場景:需要永久注入自訂 code、改 import table 載入 hook DLL、或加新 section 時。 簡單改幾個 byte 用上面的 Python 腳本就好,不需要 LIEF。 --- ## Differential Trace(API 比較法) > 比較成功/失敗兩次的 API call 序列,找到分歧點。 > 這是替代「記憶體 dump 比較」的正確方法。 ```javascript // 記錄所有 Win32 API 呼叫到檔案 var logFile = new File("C:\\CTF\\api_trace.log", "w"); var callCount = 0; function traceAPI(dll, func) { var p = Module.findExportByName(dll, func); if (!p) return; Interceptor.attach(p, { onEnter: function(args) { callCount++; var line = callCount + "|" + func + "|" + this.returnAddress + "\n"; logFile.write(line); } }); } // Hook 關鍵 API ["CreateWindowExA", "CreateWindowExW", "ShowWindow", "UpdateWindow", "MessageBoxA", "MessageBoxW", "DestroyWindow"].forEach(function(f) { traceAPI("user32.dll", f); }); ["connect", "send", "recv", "closesocket"].forEach(function(f) { traceAPI("ws2_32.dll", f); }); // 程式結束時 flush logFile.flush(); ``` 跑兩次(成功/失敗),然後 diff 兩份 log: ```bash diff api_trace_success.log api_trace_fail.log ``` 分歧點的 RetAddr 就是決策位置附近。 -
failure-patterns.md 6 KB
# 失敗模式資料庫 > 遇到瓶頸時查這裡,避免在已知死路上浪費時間。 > 每次遇到新的失敗模式就加進來。 > > **每次提出新方案前,掃一遍這份文件。** 如果新方案跟以下任何模式有相似邏輯,必須先回答:「這次跟上次的差異在哪裡?為什麼這次會成功?」 --- ## 一、泛用失敗模式(適用所有 Windows 驗證繞過) ### 1.1 在加密層死磕 **症狀:** 花大量時間分析加密演算法、搜尋 key、嘗試解密,但完全不需要理解加密就能 bypass。 **正確做法:** 加密是手段不是目的。如果目標是 bypass 認證,找到「通過/不通過」的**判斷點**然後改掉就好。先嘗試決策層攻擊,打不通再考慮加密層。 ### 1.2 提方案時不檢查先決條件 **症狀:** 提出方案,但先決條件(如「需要有效 key」「需要 server 在線」)跟當前目標矛盾。 **正確做法:** 提方案前問:(1) 先決條件都滿足嗎?(2) 這達成了目標嗎? ### 1.3 一個方法反覆重試 **症狀:** 方法 A 失敗 → 微調 A 再試 → 再微調... 花 2 小時在同一條死路上。 **正確做法:** 一個方法**最多試 3 個變體**。都失敗就記錄原因,切換到完全不同的路線。 ### 1.4 沒搜尋就下結論 **症狀:** 「這個殼不能破」「這個加密很強」——但沒上網查過。 **正確做法:** 任何關於保護殼、加密、驗證框架的假設,先搜尋: - 中文:吾爱破解、看雪、52pojie - 英文:GitHub、Stack Overflow、r/ReverseEngineering ### 1.5 NUL 掉本地錯誤字串無效 **症狀:** 把 binary 裡的「錯誤」字串 NUL 掉,程式還是顯示錯誤訊息。 **原因:** 網路驗證程式的錯誤訊息幾乎都是 server 回傳的,不是本地字串。 ### 1.6 Patch binary 後 crash **可能原因:** 改到了 PE header / 破壞了 code integrity check / file offset 算錯。 **預防:** 先用 Frida runtime patch 驗證正確性,確認有效再固化到 binary。 ### 1.7 angr / symbolic execution 跑不出結果 **判斷時機:** 10 分鐘沒結果就放棄。angr 只適合無殼、邏輯簡單的 flag checker。加殼/重度混淆的 control flow 讓 symbolic execution 狀態爆炸。 --- ## 二、保護殼反調試(通用 + 殼特定) ### 2.1 軟體斷點被偵測(INT3)`[通用]` **症狀:** 設 breakpoint 後程式彈錯誤或退出。 **原因:** 保護殼掃描 code section 找 0xCC 或做 CRC 校驗。 **正確做法:** 硬體斷點(DR0-DR3)或 Frida Interceptor(不修改 code byte)。 ### 2.2 硬體斷點觸發後無限迴圈 `[通用]` **症狀:** 硬體斷點觸發一次後不斷重複觸發。 **原因:** DR6 暫存器未清除。處理後必須寫 `context.Dr6 = 0`。 ### 2.3 Hook ExitProcess/NtTerminateProcess 無效 `[VMP/Themida]` **症狀:** hook 了退出函數,程式還是能自己結束。 **原因:** 保護殼用 direct syscall 繞過 user-mode hook。 **正確做法:** 不攔截退出,攔截退出的**原因**。或用 TitanHide kernel driver。 ### 2.4 x32dbg + ScyllaHide 被偵測 `[VMP 3.x]` **原因:** VMP 3.x 用 direct syscall,ScyllaHide user-mode hook 無效。 **正確做法:** TitanHide 或 Frida。 ### 2.5 認證過程中 attach 工具 `[通用]` **症狀:** 認證過程中 attach Frida/debugger,程式偵測到退出。 **正確做法:** 在程式啟動後、使用者操作前 attach。 ### 2.6 顯示隱藏視窗觸發退出 `[VMP]` **原因:** 保護殼監控視窗狀態,外部 ShowWindow = 干預 = 退出。 ### 2.7 硬體斷點被 SEH 清除 `[VMP]` **原因:** 通過 exception handler 清除 DR 暫存器。用 Frida 替代。 更多 VMP 特有問題見 [docs/vmp-guide.md](vmp-guide.md)。 --- ## 三、加密與協議層 ### 3.1 Replay 成功封包到新 session `[通用]` **症狀:** 重播舊 session 的封包,程式報錯。 **判斷方式:** 比較兩次 session 同一 round 封包——無共同 byte → per-session key。 **正確做法:** 不攻擊加密層,攻擊決策層。 ### 3.2 偽造 recv 回應 `[通用]` **原因:** 程式解密假資料得到亂碼,完整性校驗失敗。 ### 3.3 假伺服器回覆正確大小但錯誤內容 `[通用]` **原因:** 不只看大小,也看內容。加密回應有完整性校驗。 ### 3.4 單 byte XOR / 已知明文攻擊 `[通用]` **判斷方式:** 多組同一 round 封包,body 長度同但內容不同 → per-session key + 串流加密。 ### 3.5 S-box 搜尋找加密 key `[VMP]` **原因:** VMP 把 S-box 放在 virtual registers 裡,不暴露到一般記憶體。 --- ## 四、記憶體分析 ### 4.1 全記憶體 dump 比對 `[VMP]` **原因:** VMP 虛擬機器狀態隨機化,diff 全是噪音。 **正確做法:** 比較 API call 序列(differential trace)。 ### 4.2 跨環境記憶體注入 `[通用]` **原因:** 記憶體中的指標是特定環境的虛擬地址,跨機器/跨 session 不匹配。 ### 4.3 改 result code 期望自動解密 `[VMP]` **原因:** Section 解密有多重 state check,改一個 flag 不夠。 --- ## 五、工具使用陷阱 ### 5.1 Frida Stalker follow 全線程 `[通用]` **症狀:** 程式 hang → 網路超時。Stalker overhead 太大。 **正確做法:** 用 Interceptor 精準 hook 目標 API,不做全面 trace。 ### 5.2 EnumWindows 找不到視窗 `[通用]` **原因:** 管理員權限程式的視窗,一般權限看不到。用 `tasklist` 找 PID。 ### 5.3 Shell redirect 覆蓋輸出 `[通用]` **正確做法:** 腳本內部用 Python `open()` / Frida `File` API 寫檔。 --- ## 六、環境與操作 ### 6.1 Host 抓包導致系統 hang `[通用]` **後果:** 遠端連入無法操作,需系統還原,所有環境設定回滾。 **正確做法:** 網路抓包在 VM 裡做。Host 最多用 `netstat`。 ### 6.2 系統還原後環境消失 `[通用]` **正確做法:** 記錄環境設定步驟。或用 VM snapshot。 -
tools-quickref.md 16.5 KB
# 工具使用速查 > Capstone、Unicorn、Frida Stalker 的實戰用法。 > 不是完整文檔,只收錄 bypass 場景最常用的 pattern。 --- ## Frida Stalker > **為什麼用 Stalker 而不是 INT3 斷點?** > Stalker 用 dynamic binary instrumentation(動態改寫 code 到 slab 執行), > 不插入 0xCC、不設 debug register、不觸發 exception。 > 對抗 code integrity check 和反調試比 INT3 穩定得多。 > > **代價:** 效能 overhead 比 Interceptor 大。不要 follow 太多線程。 ### 基本概念 ``` Stalker.follow(threadId, options) — 開始追蹤指定線程 Stalker.unfollow(threadId) — 停止追蹤 Stalker.flush() — 強制輸出緩衝區 options.events — 要記錄的事件類型 options.transform — 逐 block 改寫指令(最強大的功能) options.onReceive / onCallSummary — 接收事件資料的 callback ``` ### 場景 1:監看特定地址的暫存器值 > 最常用。等於「在某地址設斷點看 register」但不用 INT3。 ```javascript var moduleBase = Module.findBaseAddress("target.exe"); var watchAddr = moduleBase.add(0x1234); // 要觀察的地址 Stalker.follow(Process.getCurrentThreadId(), { transform: function(iterator) { var instruction; while ((instruction = iterator.next()) !== null) { // 在目標地址插入 callback if (instruction.address.equals(watchAddr)) { iterator.putCallout(function(context) { console.log("[HIT] addr=" + watchAddr); console.log(" EAX=" + context.eax); console.log(" EBX=" + context.ebx); console.log(" ECX=" + context.ecx); console.log(" EDX=" + context.edx); console.log(" [ESP]=" + Memory.readPointer(context.esp)); // 讀記憶體 // var val = Memory.readU32(context.eax); }); } iterator.keep(); // 保留原始指令 } } }); ``` ### 場景 2:監看特定地址範圍(縮小 overhead) > **關鍵技巧:** 只在目標範圍內做 transform,其他地址直接 keep。 > 這是避免 Stalker hang 的核心——限制 instrumentation 範圍。 ```javascript var rangeStart = moduleBase.add(0x1000); var rangeEnd = moduleBase.add(0x2000); Stalker.follow(targetThreadId, { transform: function(iterator) { var instruction; var blockStart = null; while ((instruction = iterator.next()) !== null) { if (!blockStart) blockStart = instruction.address; // 只在目標範圍內插入 callout if (instruction.address.compare(rangeStart) >= 0 && instruction.address.compare(rangeEnd) < 0) { // 只攔截條件跳轉(找決策點) if (instruction.mnemonic === "je" || instruction.mnemonic === "jne" || instruction.mnemonic === "jz" || instruction.mnemonic === "jnz") { var addr = instruction.address; iterator.putCallout(function(context) { // EFLAGS 的 ZF bit(bit 6) var zf = (context.eflags >>> 6) & 1; console.log("[BRANCH] " + addr + " ZF=" + zf); }); } } iterator.keep(); } } }); ``` ### 場景 3:追蹤 API call 序列(輕量版 differential trace) > 比 Interceptor 多一個好處:能看到 call 指令的**來源地址**和目標。 ```javascript var logFile = new File("C:\\CTF\\stalker_trace.log", "w"); var targetModule = Process.findModuleByName("target.exe"); Stalker.follow(targetThreadId, { events: { call: true }, // 只記錄 call 事件 onReceive: function(events) { var parsed = Stalker.parse(events, { stringify: false, annotate: false }); for (var i = 0; i < parsed.length; i++) { var ev = parsed[i]; // ev = [event_type, from_addr, to_addr, ...] var from = ptr(ev[1]); var to = ptr(ev[2]); // 只記錄從目標模組出發的 call if (targetModule.base.compare(from) <= 0 && from.compare(targetModule.base.add(targetModule.size)) < 0) { var sym = DebugSymbol.fromAddress(to); logFile.write(from + " -> " + to + " " + sym + "\n"); } } logFile.flush(); } }); ``` ### 場景 4:修改執行流程(不改 binary) > 在 transform 裡可以**替換指令**,等於 runtime patch 但不改原始 byte。 > 比 Memory.patchCode 更隱蔽——原始 code 完全不動。 ```javascript var patchAddr = moduleBase.add(0x5678); Stalker.follow(targetThreadId, { transform: function(iterator) { var instruction; while ((instruction = iterator.next()) !== null) { if (instruction.address.equals(patchAddr)) { // 原始指令是 JNE(跳過成功路徑) // 替換成 NOP,讓它 fall through 到成功路徑 iterator.putNop(); // 不 keep() → 原始指令被丟棄 } else { iterator.keep(); } } } }); ``` ### Stalker 使用注意事項 | 問題 | 對策 | |------|------| | Follow 太多線程 → 程式 hang | **只 follow 1-2 個目標線程**,用 `Process.enumerateThreads()` 找到正確的 | | 長時間 follow → 記憶體暴漲 | 拿到需要的資料後立刻 `Stalker.unfollow()` | | Transform callback 太重 → 程式變慢 | 限制 callout 只在目標地址範圍內觸發 | | Follow 主線程但看不到認證邏輯 | 認證可能在 worker thread,試 follow 其他線程 | | Stalker + 網路驗證 → 超時 | 網路封包處理路徑不要 follow,或凍結計時 API | | `iterator.keep()` 忘了寫 → crash | 每個 instruction 都要 keep()(除非刻意替換)| ### 找到正確的線程 ```javascript // 列出所有線程 Process.enumerateThreads().forEach(function(t) { console.log("Thread " + t.id + " state=" + t.state); }); // 通常策略: // 1. hook 目標 API(如 recv),在 onEnter 記錄 Process.getCurrentThreadId() // 2. 用記錄到的 threadId 做 Stalker.follow ``` ### Stalker vs Interceptor vs INT3 — 選擇指南 | | Stalker | Interceptor | INT3 (debugger) | |---|---------|-------------|-----------------| | 原理 | 動態改寫 code 到 slab | Hook 函數入口(改前幾 byte)| 插入 0xCC 觸發 exception | | 觸發反調試? | 不觸發 debug flag | 可能觸發 code integrity | 觸發 INT3 偵測 | | 效能 | 中-重(取決於範圍)| 輕 | 輕 | | 粒度 | **逐指令** | 函數入口/出口 | 任意地址 | | 適合場景 | 監看暫存器值、追蹤分支走向 | Hook API、看參數/返回值 | 無反調試的程式 | | 修改能力 | 替換指令 | 改參數/返回值 | 改 context | | VMP 相容性 | ✅ 高(不改原始 byte)| ⚠️ 中(改函數入口可能被偵測)| ❌ 低 | **經驗法則:** - 想看 API 參數 → Interceptor - 想看特定地址的暫存器/分支走向 → Stalker - 想 patch 但不動原始 byte → Stalker transform - 無反調試 → 隨便,INT3 最直觀 --- ## Capstone > 在 Frida 或 Python 裡做即時反組譯。用來確認地址附近是什麼指令。 ### Python 用法 ```python from capstone import Cs, CS_ARCH_X86, CS_MODE_32 # 或 CS_MODE_64 cs = Cs(CS_ARCH_X86, CS_MODE_32) cs.detail = True # 啟用指令細節(operand、group 等) # 從 binary 讀取 bytes with open("target.exe", "rb") as f: f.seek(file_offset) code = f.read(64) # 反組譯 for insn in cs.disasm(code, runtime_va): print(f"0x{insn.address:08x} {insn.mnemonic:8s} {insn.op_str}") ``` ### 場景 1:找目標地址附近的條件跳轉 ```python from capstone import Cs, CS_ARCH_X86, CS_MODE_32, CS_GRP_JUMP cs = Cs(CS_ARCH_X86, CS_MODE_32) cs.detail = True # code = 從 binary 或 memory dump 讀取的 bytes # base_addr = 這段 code 的起始 VA for insn in cs.disasm(code, base_addr): # 找條件跳轉 if insn.group(CS_GRP_JUMP) and insn.mnemonic != "jmp": print(f"[COND JUMP] 0x{insn.address:08x} {insn.mnemonic} {insn.op_str}") print(f" bytes: {insn.bytes.hex()}") print(f" offset in file: 計算 file_offset...") ``` ### 場景 2:遞迴追蹤 CALL 鏈 ```python from capstone import Cs, CS_ARCH_X86, CS_MODE_32 cs = Cs(CS_ARCH_X86, CS_MODE_32) def find_calls(code, base_addr): """找出這段 code 裡的所有 CALL 目標""" calls = [] for insn in cs.disasm(code, base_addr): if insn.mnemonic == "call": # 直接 call(非 register call) try: target = int(insn.op_str, 16) calls.append((insn.address, target)) except ValueError: # register call (call eax 等),記錄但無法靜態解析 calls.append((insn.address, f"REG:{insn.op_str}")) return calls # 用法 calls = find_calls(code_bytes, 0x00401000) for src, dst in calls: print(f" 0x{src:08x} -> {dst if isinstance(dst, str) else f'0x{dst:08x}'}") ``` ### 場景 3:在 Frida 裡用 Capstone(透過 Instruction 物件) ```javascript // Frida 內建 Instruction.parse(),不需要額外安裝 Capstone var addr = ptr("0x00401234"); for (var i = 0; i < 20; i++) { var insn = Instruction.parse(addr); console.log(addr + " " + insn.mnemonic + " " + insn.opStr); addr = insn.next; // 下一條指令的地址 } ``` ### 安裝 ```bash pip install capstone # 或 pip install capstone-engine ``` --- ## Unicorn Engine > CPU 模擬器。載入 binary code 到虛擬記憶體,逐步執行,完全控制 register 和 memory。 > 用途:模擬保護殼的解密邏輯、在沒有 Windows 環境時跑小段 x86 code。 ### Python 用法 ```python from unicorn import Uc, UC_ARCH_X86, UC_MODE_32 from unicorn.x86_const import * # 建立模擬器 mu = Uc(UC_ARCH_X86, UC_MODE_32) # 映射記憶體(必須 page-aligned,0x1000 的倍數) CODE_ADDR = 0x00400000 STACK_ADDR = 0x00100000 mu.mem_map(CODE_ADDR, 0x10000) # code mu.mem_map(STACK_ADDR, 0x10000) # stack # 寫入 code code = b"\x33\xc0\x40\xc3" # xor eax,eax; inc eax; ret mu.mem_write(CODE_ADDR, code) # 設定暫存器 mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + 0x8000) mu.reg_write(UC_X86_REG_EBP, STACK_ADDR + 0x8000) # 執行 mu.emu_start(CODE_ADDR, CODE_ADDR + len(code)) # 讀取結果 eax = mu.reg_read(UC_X86_REG_EAX) print(f"EAX = {eax}") # → 1 ``` ### 場景 1:模擬解密函數 ```python from unicorn import Uc, UC_ARCH_X86, UC_MODE_32 from unicorn.x86_const import * mu = Uc(UC_ARCH_X86, UC_MODE_32) # 從 binary dump 載入 sections # 通常需要:.text, .rdata, .data(至少包含目標函數和它引用的資料) with open("target.exe", "rb") as f: # 假設已知 section 的 file offset 和 VA sections = [ # (file_offset, va, size) (0x1000, 0x00401000, 0x5000), # .text (0x6000, 0x00406000, 0x2000), # .rdata ] for foff, va, size in sections: mu.mem_map(va & ~0xFFF, (size + 0xFFF) & ~0xFFF) f.seek(foff) mu.mem_write(va, f.read(size)) # Stack mu.mem_map(0x00100000, 0x10000) mu.reg_write(UC_X86_REG_ESP, 0x00108000) # 設定函數參數(cdecl:推到 stack) # decrypt(buffer, length, key) BUF_ADDR = 0x00200000 mu.mem_map(BUF_ADDR, 0x1000) mu.mem_write(BUF_ADDR, encrypted_data) esp = 0x00108000 mu.mem_write(esp + 4, BUF_ADDR.to_bytes(4, 'little')) # arg1: buffer mu.mem_write(esp + 8, len(encrypted_data).to_bytes(4, 'little')) # arg2: length mu.mem_write(esp + 12, key_value.to_bytes(4, 'little')) # arg3: key # 執行 FUNC_ADDR = 0x00401234 mu.emu_start(FUNC_ADDR, 0x00401234 + 0x100, timeout=5000000) # 5 秒 timeout # 讀取解密結果 result = mu.mem_read(BUF_ADDR, len(encrypted_data)) print(result) ``` ### 場景 2:逐步執行 + trace ```python from unicorn import Uc, UC_ARCH_X86, UC_MODE_32, UC_HOOK_CODE from unicorn.x86_const import * from capstone import Cs, CS_ARCH_X86, CS_MODE_32 cs = Cs(CS_ARCH_X86, CS_MODE_32) def hook_code(mu, address, size, user_data): """每條指令執行前觸發""" code = mu.mem_read(address, size) for insn in cs.disasm(bytes(code), address): eax = mu.reg_read(UC_X86_REG_EAX) print(f"0x{address:08x} {insn.mnemonic:8s} {insn.op_str:20s} EAX=0x{eax:08x}") mu = Uc(UC_ARCH_X86, UC_MODE_32) # ... 載入 code 和設定 ... # 加 hook(可限制範圍) mu.hook_add(UC_HOOK_CODE, hook_code, begin=0x00401000, end=0x00402000) mu.emu_start(start_addr, end_addr) ``` ### 場景 3:從記憶體 dump 載入 > 如果你有 Frida dump 出來的記憶體區域,可以直接載入 Unicorn 模擬。 ```python import json from unicorn import Uc, UC_ARCH_X86, UC_MODE_32 mu = Uc(UC_ARCH_X86, UC_MODE_32) # 假設 Frida dump 出來的格式是 [{base, size, file}, ...] dump_regions = json.load(open("dump_regions.json")) for region in dump_regions: base = region["base"] size = region["size"] aligned_base = base & ~0xFFF aligned_size = ((base + size + 0xFFF) & ~0xFFF) - aligned_base try: mu.mem_map(aligned_base, aligned_size) except Exception: pass # 可能已映射(regions 重疊) data = open(region["file"], "rb").read() mu.mem_write(base, data) ``` ### Unicorn 常見問題 | 問題 | 對策 | |------|------| | `UC_ERR_FETCH_UNMAPPED` | Code 訪問了未映射的記憶體。加 `mu.mem_map()` | | `UC_ERR_READ_UNMAPPED` / `WRITE` | 資料讀寫未映射區域。檢查指標和 stack | | 模擬 Windows API call | Unicorn 不模擬 OS。需要自己 hook API 地址回傳假值 | | 超時 | 可能進了無限迴圈。加 instruction count 限制 | | VMP bytecode 模擬 | 需要載入所有 .svmp section + 正確的初始 register state | ### Hook Windows API(Unicorn 不提供 OS 模擬) ```python # 在 API 的 IAT 地址設 hook,模擬返回值 def hook_api(mu, address, size, user_data): if address == 0x004B0020: # recv 的 IAT 地址(替換成你的實際值) # 模擬 recv 返回 100 bytes mu.reg_write(UC_X86_REG_EAX, 100) # 跳過 call,直接到下一條指令 ret_addr = mu.mem_read(mu.reg_read(UC_X86_REG_ESP), 4) mu.reg_write(UC_X86_REG_EIP, int.from_bytes(ret_addr, 'little')) mu.reg_write(UC_X86_REG_ESP, mu.reg_read(UC_X86_REG_ESP) + 4) mu.hook_add(UC_HOOK_CODE, hook_api, begin=0x004B0000, end=0x004B0040) ``` ### 安裝 ```bash pip install unicorn ``` --- ## 三工具組合使用場景 ### 流程:Frida Stalker 找決策點 → Capstone 確認指令 → Unicorn 驗證 patch ``` 1. Frida Stalker follow 目標線程 → 在可疑區域加 callout,記錄條件跳轉的 ZF 值 → 找到「成功走 A 分支、失敗走 B 分支」的地址 2. Capstone 反組譯該地址附近 → 確認是 JNE/JE + 跳轉目標 → 計算需要 patch 的 bytes 3.(可選)Unicorn 模擬驗證 → 載入函數 code,設定 patch 後的 bytes → 確認 patch 後 control flow 走向正確路徑 4. Frida Stalker transform 做 runtime patch → 不改原始 byte,在 slab 裡替換指令 → 驗證成功 → 固化成 binary patch ``` ### 範例:完整的「找決策點 + patch」腳本 ```javascript // Step 1: Stalker 找條件跳轉 var target = Process.findModuleByName("target.exe"); var suspect = target.base.add(0x1234); // 從 backtrace 得到的可疑區域 var found = {}; Stalker.follow(threadId, { transform: function(iterator) { var insn; while ((insn = iterator.next()) !== null) { // 監看可疑區域 ±0x100 範圍的條件跳轉 var offset = insn.address.sub(suspect).toInt32(); if (Math.abs(offset) < 0x100) { if (insn.mnemonic === "je" || insn.mnemonic === "jne" || insn.mnemonic === "jz" || insn.mnemonic === "jnz") { var a = insn.address; var m = insn.mnemonic; iterator.putCallout(function(ctx) { var zf = (ctx.eflags >>> 6) & 1; console.log("[BRANCH] " + a + " " + m + " ZF=" + zf + " EAX=" + ctx.eax); found[a.toString()] = { mnemonic: m, zf: zf }; }); } } iterator.keep(); } } }); // Step 2: 使用者觸發認證(成功或失敗) // Step 3: 分析 found{} 裡哪個分支決定了成功/失敗 // Step 4: 用 Stalker transform 的 iterator.putNop() 做 runtime patch 驗證 ``` -
vmp-guide.md 5 KB
# VMProtect 專區 > 本文件收錄 VMProtect 保護殼特有的行為、陷阱和對策。 > 從 67+ 次實戰失敗中歸納。其他保護殼不一定適用。 --- ## VMP 核心特性 | 特性 | 影響 | |------|------| | Code virtualization | 原始 x86 指令被轉成 VMP bytecode,無法直接反組譯 | | Direct syscall | 用 sysenter/syscall 繞過 user-mode hook(ExitProcess、NtQueryInformationProcess 等)| | Code integrity check | 偵測 .svmp section 的任何修改(INT3、patch)| | Timestamp check | 在加密路徑上檢查經過時間,偵測 single-step | | SEH-based DR clearing | 通過 exception handler 清除硬體斷點暫存器 | | VM state randomization | 每次執行虛擬暫存器和堆疊佈局不同 | | Section encryption | 部分 section 只在認證成功後才解密 | --- ## 對 VMP 程式的攻擊原則 ### 1. 不要脫殼 完整反虛擬化(devirtualization)對 VMP 3.x 不實際。在 API 邊界攻擊就夠了。 ### 2. 不要在 VMP code 裡設斷點 VMP 對自己的 bytecode section 做 integrity check。任何 INT3 或 byte 修改都會被偵測。 **只在 system DLL 的 API entry point 設斷點**(那些 VMP 不校驗)。 ### 3. 不要做記憶體 dump 比較 VMP 的虛擬機器狀態每次執行都不同。成功/失敗的 diff 全是 VM 內部隨機狀態的噪音。 **改用 API call 序列比較**(differential trace)。 ### 4. 不要在 recv 路徑 single-step VMP 在 recv → 解密 → 判斷的路徑上有 timestamp check。Single-step 太慢會觸發超時。 **凍結計時 API 或直接 hook 決策結果端**。 ### 5. User-mode hook 擋不住 VMP 的反調試 VMP 3.x 用 direct syscall 呼叫 NtTerminateProcess、NtQueryInformationProcess。 ScyllaHide 等 user-mode 方案無效。需要 **TitanHide kernel driver** 或 **Frida**(不觸發 debug flag)。 --- ## VMP 反調試繞過方案 | 問題 | 方案 | 備註 | |------|------|------| | INT3 被偵測 | 硬體斷點(DR0-DR3)或 Frida Interceptor | 不修改 code byte | | 硬體斷點被 SEH 清除 | Frida(不用 DR)| 或在 SEH handler 裡重設 DR | | 硬體斷點無限迴圈 | 處理後清 DR6(`context.Dr6 = 0`)| | | ScyllaHide 被繞過 | TitanHide kernel driver | kernel 層攔截 syscall | | Frida 被偵測 | Gadget mode(DLL sideload)| 改名成 version.dll | | Timestamp 偵測 | 凍結 GetTickCount / QPC | 見 bypass-auth.md 腳本 | | ExitProcess hook 無效 | 不攔截退出,攔截退出的**原因** | hook 決策點而非結果 | | 顯示隱藏視窗觸發退出 | 不要 ShowWindow | VMP 監控視窗狀態 | --- ## VMP 程式上已證實無效的方法 > 以下方法在 VMP 3.x 上反覆測試失敗。不要嘗試。 | # | 方法 | 死因 | |---|------|------| | 1 | 全記憶體 dump 比對 | VM 狀態隨機化,82 個 diff block 全是噪音 | | 2 | recv 後 single-step | 封包 timestamp 偵測 | | 3 | Hook ExitProcess/NtTerminateProcess | Direct syscall 繞過 | | 4 | Replay/偽造加密封包 | Session-bound key | | 5 | VMP context transplant | 跨 session 指標不匹配 | | 6 | NUL 掉本地錯誤字串 | 訊息來自 server | | 7 | Stalker follow 全線程 | Overhead 太大,程式 hang | | 8 | Stalker 只 follow 主線程 | 認證可能不在主線程 | | 9 | INT3 在 .svmp section | Code integrity check | | 10 | 顯示隱藏視窗 | 觸發退出機制 | | 11 | .svmp section 靜態 patch | Bytecode 加密/隨機化 | | 12 | S-box 搜尋 | S-box 在 virtual registers 裡 | | 13 | VMP State Clone / Live Patch | 寫入執行中 bytecode → crash | | 14 | 跨環境記憶體注入 | 指標跨機器不匹配 | | 15 | 改 result code 期望自動解密 | Section 解密有多重 state check | | 16 | NtTerminateProcess 時掃記憶體 | 記憶體已釋放,太晚 | | 17 | angr / symbolic execution | VMP control flow 導致狀態爆炸 | --- ## VMP 程式的推薦攻擊流程 ``` Phase 1(靜態) 識別 .svmp section → 確認 VMP 版本 分析 import(在 VMP 外的 API call) 搜尋字串(驗證結果字串用 GBK hex 搜尋) 上網搜尋 VMP 版本 + 破解案例 Phase 2(動態) Frida hook Win32 API(不 hook VMP 內部) 重點:CreateWindowExA backtrace 如果被偵測:Gadget mode → TitanHide 凍結計時 API(如需要) Phase 3(繞過) 從 backtrace 找決策點 → patch 條件跳轉 如果是網路驗證:不攻擊加密層,攻擊決策層 Runtime patch 驗證 → Binary patch 固化 ``` --- ## Frida 計時凍結腳本 ```javascript var tick0 = null; Interceptor.attach(Module.findExportByName("kernel32.dll", "GetTickCount"), { onLeave: function(retval) { if (!tick0) tick0 = retval.toInt32(); retval.replace(ptr(tick0)); } }); // 同理 hook GetTickCount64, QueryPerformanceCounter // 完整腳本見 docs/bypass-auth.md ``` --- ## 延伸閱讀 - [failure-patterns.md](failure-patterns.md) — 第二節「保護殼反調試」和第三節「加密協議層」有更多 VMP 相關模式 - [bypass-auth.md](bypass-auth.md) — Frida 反偵測腳本、Gadget mode 設定、Differential trace -
workflow-rules.md 3.7 KB
# 動態測試工作流程規範 > 從實戰中反覆糾正後歸納的流程。每一條都是踩過坑才加上的。 --- ## 測試前環境清理(每次必做) 在每次動態測試前,必須完成以下步驟: ```powershell # 1. 殺殘留進程 taskkill /F /IM target.exe # 2. 確認無殘留 TCP 連線(TIME_WAIT 可忽略) netstat -an | findstr <PORT> # 3. 清除配置檔(如有) del /f C:\path\to\config.ini ``` 三項全部完成才能告訴使用者「環境已清理,可以開程式」。 --- ## Frida Attach 時機 ``` 正確流程: 1. 使用者開程式 2. 登入視窗出現 3. 使用者不按登入 4. 使用者說「開了」 5. tasklist 找 PID 6. frida -p <PID> -l script.js 7. 使用者操作(按登入等) 錯誤流程(會被 VMP 偵測): - 認證過程中 attach Frida - 程式啟動前就 spawn(某些 VMP 版本偵測) - 自動輪詢 attach(等使用者說才動手) ``` **找 PID 用 `tasklist`,不用 `EnumWindows`。** 管理員權限的程式視窗在一般權限下 EnumWindows 看不到。 --- ## 腳本設計原則 ### 輸出方式 ```python # ✅ 正確:腳本內部用 Python open() 寫檔 with open("output.txt", "w") as f: f.write(result) # ❌ 錯誤:依賴 shell redirect # python script.py > output.txt ← 背景模式可能覆蓋或丟失 # ❌ 錯誤:依賴 && cat # python script.py && cat output.txt ← 多餘 ``` ### Frida 腳本輸出 ```javascript // ✅ 正確:用 Frida 的 File API 直接寫檔 var f = new File("C:\\CTF\\trace.log", "w"); f.write(data); f.flush(); // ❌ 錯誤:用 send() 傳大量資料回 Python // send(hugeBuffer); ← 資料量大時會丟失或超時 ``` ### 錯誤處理 ```javascript // ✅ 正確:失敗的 region 跳過,不 crash 整個流程 Process.enumerateRanges('rw-').forEach(function(range) { try { // ... 操作 } catch(e) { // 跳過這個 range,繼續下一個 } }); // ❌ 錯誤:一個 exception 炸掉整個腳本 ``` --- ## 執行方式 ```bash # 標準執行 python script.py <PID> # timeout 設 180 秒(認證流程可能要等 server 回應) # 跑完後讀取結果 # 用 Read 工具讀 output.txt,不要 cat ``` --- ## 安全邊界 ### 只在 VM 裡做的事 - Debugger attach(x32dbg、自製 debugger) - 記憶體注入(WriteProcessMemory、VirtualAllocEx) - 任何可能觸發 VMP 反除錯的操作 - 未知行為的程式首次執行 - Raw socket capture ### 可以在 Host 上做的事 - Frida attach(小心時機) - `netstat` 觀察連線 - `tasklist` 找 PID - 讀取配置檔 - 靜態分析(Python PE parser、strings) ### 絕對不做的事 - 在 host 上做 raw socket capture(曾導致系統 hang → 系統還原) - 認證過程中 attach 工具(VMP 偵測) - 顯示程式的隱藏視窗(VMP 退出機制) - 自動輪詢 attach(等使用者指示) --- ## 靜態分析優先 在跑任何動態測試之前: 1. 用 Ghidra / Python 靜態分析完所有 CALL 鏈 2. 不要每發現一個函數就停下來跑測試 3. 一次看完所有需要分析的部分 4. 規劃好所有要驗證的假設 5. 一次 attach 驗證多個假設 **為什麼:** 每次動態測試需要使用者配合(開程式、attach、操作 UI),來回一次 10-15 分鐘。如果靜態能確認的事情就不要浪費動態測試的機會。 --- ## 提方案前的 checklist ``` □ 讀過 failed_methods 了嗎?新方案跟已失敗方法有無相似? □ 方案的先決條件都滿足嗎? □ 方案達成了目標嗎?(bypass ≠ 用真 key 登入) □ 不確定的部分有查過資料嗎? □ 明確區分了「已驗證事實」和「未驗證假設」嗎? □ 有具體的驗證步驟嗎?(不是「試試看」) ```
-
-
scripts
-
pe_info.py 18.3 KB
#!/usr/bin/env python3 """ pe_info.py — PE 靜態偵察腳本 用途:不依賴外部工具,用純 Python 解析 PE header, 輸出 bypass 流程需要的所有靜態資訊。 使用方式: python pe_info.py target.exe python pe_info.py target.exe --json # JSON 輸出(給其他腳本用) python pe_info.py target.exe --strings # 額外做字串搜尋 python pe_info.py target.exe --gbk "关键字" # 搜尋 GBK 編碼(簡體中文) python pe_info.py target.exe --big5 "關鍵字" # 搜尋 Big5 編碼(繁體中文) python pe_info.py target.exe --utf8 "關鍵字" # 搜尋 UTF-8 編碼 python pe_info.py target.exe --encoding euc-kr "키워드" # 任意編碼 輸出: - 基本資訊(arch, ImageBase, EP, ASLR, DEP, manifest) - Section 列表 + entropy + 保護殼判斷 - Import 分析 + 攻擊面提示 - 可疑字串(IP, URL, 配置檔路徑, 視窗標題) 不需要安裝任何第三方套件。 """ import struct import sys import math import json import os import re from collections import OrderedDict # ============================================================= # PE Parser(純 stdlib,不依賴 pefile) # ============================================================= def read_pe(data): """解析 PE header,回傳結構化資訊""" info = OrderedDict() # DOS header if data[:2] != b"MZ": return {"error": "Not a PE file (no MZ header)"} pe_offset = struct.unpack_from("<I", data, 0x3C)[0] if data[pe_offset:pe_offset+4] != b"PE\x00\x00": return {"error": "Not a PE file (no PE signature)"} # COFF header coff = pe_offset + 4 machine = struct.unpack_from("<H", data, coff)[0] num_sections = struct.unpack_from("<H", data, coff + 2)[0] timestamp = struct.unpack_from("<I", data, coff + 4)[0] optional_size = struct.unpack_from("<H", data, coff + 16)[0] characteristics = struct.unpack_from("<H", data, coff + 18)[0] machines = {0x14c: "x86 (32-bit)", 0x8664: "x64 (64-bit)", 0x1c0: "ARM", 0xaa64: "ARM64"} info["architecture"] = machines.get(machine, f"Unknown (0x{machine:04x})") info["is_32bit"] = machine == 0x14c info["num_sections"] = num_sections info["is_dll"] = bool(characteristics & 0x2000) # Optional header opt = coff + 20 magic = struct.unpack_from("<H", data, opt)[0] is_pe32plus = magic == 0x20b if is_pe32plus: info["image_base"] = f"0x{struct.unpack_from('<Q', data, opt + 24)[0]:016x}" entry_rva = struct.unpack_from("<I", data, opt + 16)[0] dll_chars = struct.unpack_from("<H", data, opt + 70)[0] subsystem = struct.unpack_from("<H", data, opt + 68)[0] num_data_dirs = struct.unpack_from("<I", data, opt + 108)[0] data_dirs_offset = opt + 112 else: info["image_base"] = f"0x{struct.unpack_from('<I', data, opt + 28)[0]:08x}" entry_rva = struct.unpack_from("<I", data, opt + 16)[0] dll_chars = struct.unpack_from("<H", data, opt + 70)[0] subsystem = struct.unpack_from("<H", data, opt + 68)[0] num_data_dirs = struct.unpack_from("<I", data, opt + 96)[0] data_dirs_offset = opt + 96 + 4 info["entry_point_rva"] = f"0x{entry_rva:08x}" subsystems = {1: "Native", 2: "GUI", 3: "CLI (Console)", 9: "WinCE"} info["subsystem"] = subsystems.get(subsystem, f"Unknown ({subsystem})") info["aslr"] = bool(dll_chars & 0x0040) info["dep"] = bool(dll_chars & 0x0100) info["high_entropy_aslr"] = bool(dll_chars & 0x0020) # Sections section_table = opt + optional_size sections = [] ep_section = None for i in range(num_sections): s_off = section_table + i * 40 name_bytes = data[s_off:s_off+8] name = name_bytes.split(b"\x00")[0].decode("ascii", errors="replace") virtual_size = struct.unpack_from("<I", data, s_off + 8)[0] virtual_addr = struct.unpack_from("<I", data, s_off + 12)[0] raw_size = struct.unpack_from("<I", data, s_off + 16)[0] raw_offset = struct.unpack_from("<I", data, s_off + 20)[0] chars = struct.unpack_from("<I", data, s_off + 36)[0] # Entropy ent = 0.0 if raw_size > 0 and raw_offset + raw_size <= len(data): ent = calc_entropy(data[raw_offset:raw_offset + raw_size]) # Flags flags = [] if chars & 0x20000000: flags.append("X") # execute if chars & 0x40000000: flags.append("R") # read if chars & 0x80000000: flags.append("W") # write section = OrderedDict([ ("name", name), ("virtual_addr", f"0x{virtual_addr:08x}"), ("virtual_size", f"0x{virtual_size:08x}"), ("raw_offset", f"0x{raw_offset:08x}"), ("raw_size", f"0x{raw_size:08x}"), ("entropy", round(ent, 4)), ("flags", "".join(flags)), ]) # 判斷保護殼 packer_hint = detect_packer_section(name, ent, raw_size, virtual_size) if packer_hint: section["packer_hint"] = packer_hint sections.append(section) # EP 落在哪個 section if virtual_addr <= entry_rva < virtual_addr + virtual_size: ep_section = name info["entry_point_section"] = ep_section or "(unknown)" if ep_section and ep_section != ".text": info["ep_warning"] = f"Entry point not in .text (in {ep_section}) — likely packed" info["sections"] = sections # Imports imports = parse_imports(data, data_dirs_offset, num_data_dirs, sections, is_pe32plus) if imports: info["imports"] = imports # Manifest(搜尋 requireAdministrator) manifest = extract_manifest_hint(data) if manifest: info["manifest"] = manifest return info def calc_entropy(data): """計算 Shannon entropy (0.0 - 8.0)""" if not data: return 0.0 freq = [0] * 256 for b in data: freq[b] += 1 length = len(data) entropy = 0.0 for f in freq: if f > 0: p = f / length entropy -= p * math.log2(p) return entropy def detect_packer_section(name, entropy, raw_size, virtual_size): """根據 section 名稱和特徵判斷保護殼""" name_lower = name.lower() if name_lower.startswith(".vmp") or name_lower.startswith(".svmp"): return "VMProtect" if name_lower in (".upx0", ".upx1", "upx0", "upx1"): return "UPX (try: upx -d)" if name_lower == ".themida" or name_lower == ".winlice": return "Themida / WinLicense" if name_lower == ".aspack": return "ASPack" if name_lower == ".adata": return "ASProtect (possible)" if name_lower in (".nsp0", ".nsp1", ".nsp2"): return "NsPack" if name_lower == ".perplex": return "Perplex PE Protector" # 高 entropy + 可執行 = 可能加殼 if entropy > 7.5 and raw_size > 0x1000: return f"High entropy ({entropy:.2f}) — possibly packed/encrypted" # .text raw_size = 0 = 殼搬移了 code if name_lower == ".text" and raw_size == 0 and virtual_size > 0: return "Empty .text section — code relocated by packer" return None def parse_imports(data, data_dirs_offset, num_data_dirs, sections, is_pe32plus): """解析 import table""" if num_data_dirs < 2: return None entry_size = 8 # each data dir entry = 8 bytes (RVA + Size) import_rva = struct.unpack_from("<I", data, data_dirs_offset + entry_size)[0] import_size = struct.unpack_from("<I", data, data_dirs_offset + entry_size + 4)[0] if import_rva == 0: return None import_offset = rva_to_offset(import_rva, sections) if import_offset is None: return None imports = OrderedDict() pos = import_offset while pos + 20 <= len(data): ilt_rva = struct.unpack_from("<I", data, pos)[0] name_rva = struct.unpack_from("<I", data, pos + 12)[0] if name_rva == 0: break name_offset = rva_to_offset(name_rva, sections) if name_offset and name_offset < len(data): dll_name = read_cstring(data, name_offset) else: dll_name = f"(RVA 0x{name_rva:08x})" # 解析函數名 funcs = [] if ilt_rva != 0: ilt_offset = rva_to_offset(ilt_rva, sections) if ilt_offset: entry_sz = 8 if is_pe32plus else 4 fpos = ilt_offset count = 0 while fpos + entry_sz <= len(data) and count < 200: if is_pe32plus: entry_val = struct.unpack_from("<Q", data, fpos)[0] ordinal_flag = 1 << 63 else: entry_val = struct.unpack_from("<I", data, fpos)[0] ordinal_flag = 1 << 31 if entry_val == 0: break if entry_val & ordinal_flag: funcs.append(f"Ordinal #{entry_val & 0xFFFF}") else: hint_offset = rva_to_offset(entry_val & 0x7FFFFFFF, sections) if hint_offset and hint_offset + 2 < len(data): func_name = read_cstring(data, hint_offset + 2) funcs.append(func_name) fpos += entry_sz count += 1 attack_surface = analyze_import_surface(dll_name, funcs) entry = OrderedDict([("functions", funcs)]) if attack_surface: entry["attack_surface"] = attack_surface imports[dll_name] = entry pos += 20 return imports def analyze_import_surface(dll_name, funcs): """根據 import 判斷攻擊面""" dll_lower = dll_name.lower() hints = [] if "ws2_32" in dll_lower or "wsock32" in dll_lower: hints.append("Network verification — hook connect/send/recv") if "winhttp" in dll_lower or "wininet" in dll_lower: hints.append("HTTP-based verification — hook at HTTP layer") func_set = set(f.lower() for f in funcs) if func_set & {"lstrcmpa", "lstrcmpw", "comparestringa", "comparestringw"}: hints.append("String comparison — possible local key check") if func_set & {"cryptdecrypt", "cryptencrypt", "bcryptdecrypt", "bcryptencrypt"}: hints.append("Crypto API — hook to see plaintext") if func_set & {"getprocaddress"}: hints.append("Dynamic resolve — static IAT hook may be insufficient") if func_set & {"createprocessa", "createprocessw"}: hints.append("Spawns subprocess — check if loader") if func_set & {"messageboxw", "messageboxa"}: hints.append("MessageBox — hook for error messages + backtrace") if func_set & {"createwindowexa", "createwindowexw"}: hints.append("CreateWindowEx — KEY hook target (main window = bypass success)") if func_set & {"regopenkeyexa", "regopenkeyexw"}: hints.append("Registry access — may store license info") if func_set & {"getsystemtime", "getlocaltime"}: hints.append("Time API — possible time-based license check") return hints if hints else None def rva_to_offset(rva, sections): """RVA → file offset""" for s in sections: va = int(s["virtual_addr"], 16) vs = int(s["virtual_size"], 16) ro = int(s["raw_offset"], 16) rs = int(s["raw_size"], 16) if va <= rva < va + max(vs, rs): return rva - va + ro return None def read_cstring(data, offset, max_len=256): """讀取 null-terminated string""" end = data.find(b"\x00", offset, offset + max_len) if end == -1: end = offset + max_len return data[offset:end].decode("ascii", errors="replace") def extract_manifest_hint(data): """搜尋 manifest 中的執行等級""" hints = {} # requireAdministrator if b"requireAdministrator" in data: hints["execution_level"] = "requireAdministrator" elif b"highestAvailable" in data: hints["execution_level"] = "highestAvailable" elif b"asInvoker" in data: hints["execution_level"] = "asInvoker" return hints if hints else None # ============================================================= # 字串搜尋 # ============================================================= def search_strings(data, min_len=6): """搜尋可疑字串""" results = OrderedDict() # ASCII strings ascii_strings = [] for m in re.finditer(rb"[\x20-\x7e]{%d,}" % min_len, data): s = m.group().decode("ascii") offset = m.start() ascii_strings.append((offset, s)) # IP addresses ips = [(off, s) for off, s in ascii_strings if re.search(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", s)] if ips: results["ip_addresses"] = [{"offset": f"0x{o:08x}", "value": s} for o, s in ips[:20]] # URLs urls = [(off, s) for off, s in ascii_strings if re.search(r"https?://|ftp://|wss?://", s, re.IGNORECASE)] if urls: results["urls"] = [{"offset": f"0x{o:08x}", "value": s} for o, s in urls[:20]] # Config file paths configs = [(off, s) for off, s in ascii_strings if re.search(r"\.(ini|cfg|json|xml|conf|dat|key|lic)\b", s, re.IGNORECASE)] if configs: results["config_files"] = [{"offset": f"0x{o:08x}", "value": s} for o, s in configs[:20]] # Window class / registration wnd = [(off, s) for off, s in ascii_strings if any(k in s for k in ("Window", "Dialog", "MainWnd", "FormClass"))] if wnd: results["window_hints"] = [{"offset": f"0x{o:08x}", "value": s} for o, s in wnd[:10]] return results if results else None def search_encoded_string(data, text, encoding): """搜尋指定編碼的字串(GBK、Big5、UTF-8 等)""" try: pattern = text.encode(encoding) except (UnicodeEncodeError, LookupError): return [] results = [] start = 0 while True: idx = data.find(pattern, start) if idx == -1: break results.append(f"0x{idx:08x}") start = idx + 1 return results # ============================================================= # Main # ============================================================= def main(): if len(sys.argv) < 2: print("Usage: python pe_info.py <target.exe> [--json] [--strings]") print(" [--gbk <text>] [--big5 <text>] [--utf8 <text>] [--encoding <enc> <text>]") sys.exit(1) target = sys.argv[1] use_json = "--json" in sys.argv do_strings = "--strings" in sys.argv # 收集所有編碼搜尋請求:[(encoding, text), ...] encoded_queries = [] encoding_flags = {"--gbk": "gbk", "--big5": "big5", "--utf8": "utf-8"} i = 0 while i < len(sys.argv): arg = sys.argv[i] if arg in encoding_flags and i + 1 < len(sys.argv): encoded_queries.append((encoding_flags[arg], sys.argv[i + 1])) i += 2 elif arg == "--encoding" and i + 2 < len(sys.argv): encoded_queries.append((sys.argv[i + 1], sys.argv[i + 2])) i += 3 else: i += 1 if not os.path.isfile(target): print(f"Error: {target} not found") sys.exit(1) with open(target, "rb") as f: data = f.read() info = read_pe(data) info["file"] = target info["file_size"] = f"0x{len(data):x} ({len(data):,} bytes)" if do_strings: strings = search_strings(data) if strings: info["strings"] = strings if encoded_queries: enc_results = OrderedDict() for encoding, q in encoded_queries: hits = search_encoded_string(data, q, encoding) key = f"[{encoding}] {q}" enc_results[key] = hits if hits else ["(not found)"] info["encoded_search"] = enc_results if use_json: print(json.dumps(info, indent=2, ensure_ascii=False)) else: print_report(info) def print_report(info): """人類可讀的報告""" print("=" * 60) print(f" PE Analysis: {info.get('file', '?')}") print(f" Size: {info.get('file_size', '?')}") print("=" * 60) if "error" in info: print(f"\n ERROR: {info['error']}") return print(f"\n Architecture: {info['architecture']}") print(f" ImageBase: {info['image_base']}") print(f" Entry Point: {info['entry_point_rva']} (in {info['entry_point_section']})") print(f" Subsystem: {info['subsystem']}") print(f" ASLR: {'Yes' if info['aslr'] else 'No'}") print(f" DEP: {'Yes' if info['dep'] else 'No'}") print(f" DLL: {'Yes' if info['is_dll'] else 'No'}") if "ep_warning" in info: print(f" ** WARNING: {info['ep_warning']}") if "manifest" in info: for k, v in info["manifest"].items(): print(f" Manifest: {k} = {v}") # Sections print(f"\n Sections ({info['num_sections']}):") print(f" {'Name':<12} {'VirtAddr':<12} {'VirtSize':<12} {'RawSize':<12} {'Entropy':<10} {'Flags':<6} Hint") print(" " + "-" * 80) for s in info.get("sections", []): hint = s.get("packer_hint", "") print(f" {s['name']:<12} {s['virtual_addr']:<12} {s['virtual_size']:<12} " f"{s['raw_size']:<12} {s['entropy']:<10} {s['flags']:<6} {hint}") # Imports if "imports" in info: print(f"\n Imports:") for dll, entry in info["imports"].items(): funcs = entry.get("functions", []) surface = entry.get("attack_surface", []) print(f"\n {dll} ({len(funcs)} functions)") if surface: for hint in surface: print(f" >> {hint}") for f in funcs[:15]: # 只顯示前 15 個 print(f" - {f}") if len(funcs) > 15: print(f" ... and {len(funcs) - 15} more") # Strings if "strings" in info: print(f"\n Suspicious Strings:") for category, items in info["strings"].items(): print(f"\n [{category}]") for item in items: print(f" {item['offset']} {item['value'][:80]}") # Encoded string search if "encoded_search" in info: print(f"\n Encoded String Search:") for query, hits in info["encoded_search"].items(): print(f" {query}: {', '.join(hits)}") print() if __name__ == "__main__": main() -
recon.js 13.8 KB
/** * recon.js — Frida 被動偵察腳本 * * 用途:第一次動態分析時掛上去,觀察目標程式的行為。 * 只記錄,不修改任何東西。 * * 使用方式: * frida -l recon.js -f target.exe # spawn(程式啟動前 hook) * frida -l recon.js -p <PID> # attach(程式已在跑) * frida -l recon.js -f target.exe --no-pause # spawn + 自動 resume * * 輸出: * 所有 hook 結果印到 console。 * 如需寫檔,取消註解底部的 File output 區塊。 * * 自訂: * 根據靜態分析結果,啟用/停用對應的 hook 區塊。 * 每個區塊開頭都有 ENABLE 開關。 */ "use strict"; // ============================================================ // 設定區 — 根據目標程式調整 // ============================================================ var ENABLE_NETWORK = true; // WS2_32: connect, send, recv var ENABLE_WINDOW = true; // CreateWindowExA/W(最重要:找決策點) var ENABLE_MESSAGEBOX = true; // MessageBoxA/W(錯誤訊息 + backtrace) var ENABLE_STRCMP = true; // lstrcmpA, CompareStringA(本地驗證) var ENABLE_CRYPTO = false; // CryptDecrypt, BCryptDecrypt var ENABLE_FILE = false; // CreateFileA/W, ReadFile(配置檔讀取) var ENABLE_PROCESS = false; // CreateProcessA/W(loader 偵測) var ENABLE_REGISTRY = false; // RegOpenKeyExA/W, RegQueryValueExA/W var ENABLE_TIME = false; // GetSystemTime, GetLocalTime(時間驗證偵測) var ENABLE_MEMORY_SCAN = false; // 定時掃描記憶體找特定 pattern // 記憶體掃描 pattern — 根據目標程式的靜態分析結果填入 // 用 pe_info.py --strings 或 --gbk 找到關鍵字後,把 hex 填在這裡 // GBK 轉換:python3 -c "print(' '.join(f'{b:02X}' for b in '關鍵字'.encode('gbk')))" var SCAN_PATTERNS = [ // { pattern: "xx xx xx xx", label: "驗證成功訊息" }, // { pattern: "xx xx xx xx", label: "錯誤訊息" }, // { pattern: "66 6C 61 67 7B", label: "flag{" }, ]; // ============================================================ // 工具函數 // ============================================================ function safeReadCString(p) { if (p.isNull()) return "(null)"; try { return Memory.readCString(p); } catch (e) { return "(unreadable)"; } } function safeReadUtf16(p) { if (p.isNull()) return "(null)"; try { return Memory.readUtf16String(p); } catch (e) { return "(unreadable)"; } } function backtrace(ctx) { return " Backtrace:\n" + Thread.backtrace(ctx, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join("\n"); } function tryHook(dll, func, callbacks) { var p = Module.findExportByName(dll, func); if (p) { Interceptor.attach(p, callbacks); return true; } return false; } // ============================================================ // 網路層 — connect / send / recv // ============================================================ if (ENABLE_NETWORK) { tryHook("ws2_32.dll", "connect", { onEnter: function (args) { var sa = args[1]; var family = Memory.readU16(sa); if (family === 2) { // AF_INET var port = (Memory.readU8(sa.add(2)) << 8) | Memory.readU8(sa.add(3)); var ip = Memory.readU8(sa.add(4)) + "." + Memory.readU8(sa.add(5)) + "." + Memory.readU8(sa.add(6)) + "." + Memory.readU8(sa.add(7)); console.log("[CONNECT] " + ip + ":" + port + " | RetAddr=" + this.returnAddress); } } }); tryHook("ws2_32.dll", "send", { onEnter: function (args) { var len = args[2].toInt32(); console.log("[SEND] " + len + " bytes | RetAddr=" + this.returnAddress); if (len > 0 && len < 2048) { console.log(hexdump(args[1], { length: Math.min(len, 128) })); } } }); tryHook("ws2_32.dll", "recv", { onEnter: function (args) { this.buf = args[1]; }, onLeave: function (retval) { var n = retval.toInt32(); if (n > 0) { console.log("[RECV] " + n + " bytes | RetAddr=" + this.returnAddress); console.log(hexdump(this.buf, { length: Math.min(n, 128) })); } } }); // WSAConnect(某些程式用這個而非 connect) tryHook("ws2_32.dll", "WSAConnect", { onEnter: function (args) { var sa = args[1]; var family = Memory.readU16(sa); if (family === 2) { var port = (Memory.readU8(sa.add(2)) << 8) | Memory.readU8(sa.add(3)); var ip = Memory.readU8(sa.add(4)) + "." + Memory.readU8(sa.add(5)) + "." + Memory.readU8(sa.add(6)) + "." + Memory.readU8(sa.add(7)); console.log("[WSA_CONNECT] " + ip + ":" + port + " | RetAddr=" + this.returnAddress); } } }); } // ============================================================ // UI 層 — CreateWindowEx / MessageBox // ============================================================ if (ENABLE_WINDOW) { tryHook("user32.dll", "CreateWindowExA", { onEnter: function (args) { var cls = args[1]; var clsName = cls.isNull() ? "(null)" : (cls.toInt32() > 0 && cls.toInt32() < 0xFFFF) ? ("#" + cls.toInt32()) : safeReadCString(cls); var title = safeReadCString(args[2]); console.log("[CreateWindowExA] class=\"" + clsName + "\" title=\"" + title + "\""); console.log(backtrace(this.context)); } }); tryHook("user32.dll", "CreateWindowExW", { onEnter: function (args) { var title = safeReadUtf16(args[2]); console.log("[CreateWindowExW] title=\"" + title + "\""); console.log(backtrace(this.context)); } }); tryHook("user32.dll", "ShowWindow", { onEnter: function (args) { var hwnd = args[0]; var cmd = args[1].toInt32(); // SW_SHOW=5, SW_HIDE=0, SW_MAXIMIZE=3 console.log("[ShowWindow] hwnd=" + hwnd + " cmd=" + cmd + " | RetAddr=" + this.returnAddress); } }); } if (ENABLE_MESSAGEBOX) { tryHook("user32.dll", "MessageBoxA", { onEnter: function (args) { var text = safeReadCString(args[1]); var title = safeReadCString(args[2]); console.log("[MessageBoxA] title=\"" + title + "\" text=\"" + text + "\""); console.log(backtrace(this.context)); } }); tryHook("user32.dll", "MessageBoxW", { onEnter: function (args) { var text = safeReadUtf16(args[1]); var title = safeReadUtf16(args[2]); console.log("[MessageBoxW] title=\"" + title + "\" text=\"" + text + "\""); console.log(backtrace(this.context)); } }); } // ============================================================ // 字串比較 — lstrcmpA / CompareStringA(本地驗證) // ============================================================ if (ENABLE_STRCMP) { tryHook("kernel32.dll", "lstrcmpA", { onEnter: function (args) { var s1 = safeReadCString(args[0]); var s2 = safeReadCString(args[1]); console.log("[lstrcmpA] \"" + s1 + "\" vs \"" + s2 + "\" | RetAddr=" + this.returnAddress); } }); tryHook("kernel32.dll", "lstrcmpiA", { onEnter: function (args) { var s1 = safeReadCString(args[0]); var s2 = safeReadCString(args[1]); console.log("[lstrcmpiA] \"" + s1 + "\" vs \"" + s2 + "\" | RetAddr=" + this.returnAddress); } }); tryHook("kernel32.dll", "CompareStringA", { onEnter: function (args) { var s1 = safeReadCString(args[2]); var s2 = safeReadCString(args[4]); console.log("[CompareStringA] \"" + s1 + "\" vs \"" + s2 + "\" | RetAddr=" + this.returnAddress); } }); } // ============================================================ // 加密 API // ============================================================ if (ENABLE_CRYPTO) { // WinCrypt tryHook("advapi32.dll", "CryptDecrypt", { onEnter: function (args) { this.pbData = args[3]; this.pdwDataLen = args[4]; }, onLeave: function (retval) { if (retval.toInt32() !== 0) { var len = Memory.readU32(this.pdwDataLen); console.log("[CryptDecrypt] " + len + " bytes | RetAddr=" + this.returnAddress); if (len > 0 && len < 2048) { console.log(hexdump(this.pbData, { length: Math.min(len, 128) })); } } } }); // BCrypt tryHook("bcrypt.dll", "BCryptDecrypt", { onEnter: function (args) { this.pbOutput = args[4]; this.pcbResult = args[6]; console.log("[BCryptDecrypt] called | RetAddr=" + this.returnAddress); }, onLeave: function (retval) { if (retval.toInt32() === 0 && !this.pcbResult.isNull()) { var len = Memory.readU32(this.pcbResult); console.log("[BCryptDecrypt] output " + len + " bytes"); if (len > 0 && len < 2048) { console.log(hexdump(this.pbOutput, { length: Math.min(len, 128) })); } } } }); } // ============================================================ // 檔案操作(配置檔偵測) // ============================================================ if (ENABLE_FILE) { tryHook("kernel32.dll", "CreateFileA", { onEnter: function (args) { var path = safeReadCString(args[0]); var access = args[1].toInt32(); var rw = (access & 0x80000000) ? "R" : ""; rw += (access & 0x40000000) ? "W" : ""; console.log("[CreateFileA] " + rw + " \"" + path + "\" | RetAddr=" + this.returnAddress); } }); tryHook("kernel32.dll", "CreateFileW", { onEnter: function (args) { var path = safeReadUtf16(args[0]); console.log("[CreateFileW] \"" + path + "\" | RetAddr=" + this.returnAddress); } }); } // ============================================================ // 子進程(loader 偵測) // ============================================================ if (ENABLE_PROCESS) { tryHook("kernel32.dll", "CreateProcessA", { onEnter: function (args) { var app = safeReadCString(args[0]); var cmd = safeReadCString(args[1]); console.log("[CreateProcessA] app=\"" + app + "\" cmd=\"" + cmd + "\""); console.log(backtrace(this.context)); } }); tryHook("kernel32.dll", "CreateProcessW", { onEnter: function (args) { var app = safeReadUtf16(args[0]); var cmd = safeReadUtf16(args[1]); console.log("[CreateProcessW] app=\"" + app + "\" cmd=\"" + cmd + "\""); } }); } // ============================================================ // Registry(授權資訊偵測) // ============================================================ if (ENABLE_REGISTRY) { tryHook("advapi32.dll", "RegOpenKeyExA", { onEnter: function (args) { var subkey = safeReadCString(args[1]); console.log("[RegOpenKeyExA] \"" + subkey + "\" | RetAddr=" + this.returnAddress); } }); tryHook("advapi32.dll", "RegQueryValueExA", { onEnter: function (args) { var name = safeReadCString(args[1]); console.log("[RegQueryValueExA] \"" + name + "\" | RetAddr=" + this.returnAddress); } }); } // ============================================================ // 時間 API(時間驗證偵測) // ============================================================ if (ENABLE_TIME) { tryHook("kernel32.dll", "GetSystemTime", { onEnter: function (args) { console.log("[GetSystemTime] called | RetAddr=" + this.returnAddress); } }); tryHook("kernel32.dll", "GetLocalTime", { onEnter: function (args) { console.log("[GetLocalTime] called | RetAddr=" + this.returnAddress); } }); } // ============================================================ // 記憶體掃描(定時) // ============================================================ if (ENABLE_MEMORY_SCAN && SCAN_PATTERNS.length > 0) { setInterval(function () { SCAN_PATTERNS.forEach(function (entry) { Process.enumerateRanges("rw-").forEach(function (range) { try { Memory.scan(range.base, range.size, entry.pattern, { onMatch: function (address) { console.log("[SCAN] Found \"" + entry.label + "\" at " + address); console.log(hexdump(address.sub(16), { length: 64 })); }, onComplete: function () {} }); } catch (e) {} }); }); }, 3000); // 每 3 秒掃一次 } // ============================================================ // File output(取消註解以寫檔) // ============================================================ // var logFile = new File("C:\\CTF\\recon_log.txt", "w"); // 在各 hook 的 console.log 後加上: // logFile.write("[CONNECT] " + ip + ":" + port + "\n"); // logFile.flush(); console.log("[recon.js] Loaded. Hooks active:"); console.log(" Network=" + ENABLE_NETWORK + " Window=" + ENABLE_WINDOW + " MessageBox=" + ENABLE_MESSAGEBOX + " Strcmp=" + ENABLE_STRCMP); console.log(" Crypto=" + ENABLE_CRYPTO + " File=" + ENABLE_FILE + " Process=" + ENABLE_PROCESS + " Registry=" + ENABLE_REGISTRY + " Time=" + ENABLE_TIME + " MemScan=" + ENABLE_MEMORY_SCAN);
-
-
SKILL.md 19.8 KB
--- name: ctf-kit description: "Use when the user is solving an authorized CTF / lab reverse-engineering challenge focused on Windows application authentication or license-check bypass. Guides triage → static analysis → dynamic experiment planning → bypass verification, with strong evidence discipline and VM safety boundaries. NOT for real-world unauthorized software cracking, malware deployment, credential theft, or non-Windows CTF domains better handled by a broader CTF skill." version: 0.4.0 status: mvp triggers: - "ctf" - "reverse" - "bypass" - "crack" - "逆向" - "繞過驗證" - "破解" license: MIT compatibility: "Requires filesystem-based agent (Claude Code or similar) with bash, Python 3. Windows dynamic analysis environment (physical or VM) recommended." allowed-tools: Bash Read Write Edit Glob Grep Task WebFetch WebSearch Skill metadata: user-invocable: "true" argument-hint: "[challenge-file] [phase]" --- # /ctf-kit — CTF Reverse Engineering Toolkit You are a Windows CTF reverse-engineering coach. You help the user solve authorized challenge binaries by forcing goal alignment, evidence-first analysis, repeatable experiments, and safe VM-only dynamic testing. Windows 應用程式驗證繞過的實戰 playbook。 適用於各種保護殼(VMP、Themida、自製殼等)和驗證方式(網路驗證、本地驗證、混合驗證)。 ## 適用範圍 本 skill 適用於: - Windows PE 執行檔的授權/認證繞過 - 各種保護殼(VMProtect、Themida、UPX、自製殼) - 網路驗證、本地驗證、時間驗證 - 紅隊演練中的軟體授權 bypass **不適用時的導向:** 本 skill 聚焦 Windows 驗證繞過。超出範圍的題目,建議使用 [ljagiello/ctf-skills](https://github.com/ljagiello/ctf-skills)——一套覆蓋更廣的 CTF skill 集合(reverse、web、crypto、pwn、forensics、OSINT 等)。 | 狀況 | 建議 | ctf-skills 中的對應 skill | |------|------|--------------------------| | Linux ELF / Android APK / WASM | 更廣泛的逆向分析 | `ctf-reverse` | | .NET 程式 | dnSpy 反編譯為主 | `ctf-reverse` | | 純 flag checker(無網路、無殼)| angr / symbolic execution | `ctf-reverse` | | Web 應用 | XSS、SQLi、SSTI 等 | `ctf-web` | | 不確定分類 | 先做 triage 再分流 | `solve-challenge` | | Crypto / Pwn / Forensics | 本 skill 不覆蓋 | `ctf-crypto` / `ctf-pwn` / `ctf-forensics` | 如果你的環境已安裝 ctf-skills,可以直接用 `/ctf-reverse`、`/solve-challenge` 等指令。 未安裝時,請參考該 repo 的 README 安裝: ```bash # 將 ctf-skills 的 skill 加入 Claude Code # 參考 https://github.com/ljagiello/ctf-skills 的安裝說明 ``` ## 使用方式 ``` /ctf-kit challenge.exe # 開題:從 Phase 0 開始 /ctf-kit # 繼續上次進度 /ctf-kit recon # 跳到偵察階段 /ctf-kit bypass # 跳到繞過階段 ``` --- ## 第一部分:處理準則 > 這些準則適用於所有 Windows 驗證繞過場景,不限於特定保護殼。 > 技術會因目標不同而變,但思維方式不會。 ### 準則 1:對齊目標,反覆確認 拿到題目後第一件事不是分析 binary,而是**搞清楚目標是什麼**。 ``` □ 目標是什麼?(找 flag?bypass 認證進主畫面?取得特定資料?) □ 目標「不是」什麼?(不要搜 flag 如果目標是 bypass;不要提議用真 key 如果目標是繞過驗證) □ 有時效性限制嗎?(server 在線時間、key 有效期) ``` 每提出一個方案前,問自己:「這個方案達成了目標嗎?」 ### 準則 2:先驗證再行動,不猜測 **不要說「可能是」「也許因為」然後就開始動手。** ``` 錯誤流程:觀察現象 → 猜測原因 → 嘗試修復 → 失敗 → 猜另一個原因 正確流程:觀察現象 → 設計最小驗證實驗 → 取得資料 → 基於資料決策 ``` - 假設不確定時,先**上網搜尋**有沒有相關文獻或已知案例 - 用最小代價驗證(讀 dump、一行 trace、查文檔),不要寫 200 行腳本去驗證一個假設 - 對使用者說話時明確區分「已驗證事實」和「未驗證假設」 - 工具用法不確定時**先查文檔**,不要亂試 ### 準則 3:不重複已失敗的方法 **每次提出方案前,必須先檢查失敗記錄。** 如果專案有 `failed_methods` 記錄,提方案前必須讀取並對照。如果新方案跟任何已失敗方法有相似邏輯,必須明確指出差異在哪裡,為什麼這次會不同。 參見 [docs/failure-patterns.md](docs/failure-patterns.md) 了解常見失敗模式。 ### 準則 4:靜態分析做到底,動態測試要批量 ``` 錯誤流程:靜態看一點 → 跑動態 → 發現不夠 → 回靜態 → 再跑動態(反覆 10 次) 正確流程:靜態分析做到沒東西可挖 → 一次規劃所有動態實驗 → 批量執行 ``` 動態測試消耗資源(時間、key、server 額度、使用者操作),靜態分析不消耗。每次動態測試都需要使用者配合,來回一次 10-15 分鐘。 ### 準則 5:追蹤完整呼叫鏈,不只看一層 分析函數依賴時,**必須遞迴追蹤每個呼叫目標的完整呼叫鏈**。一個函數看起來「參數齊全可 stub」,但它內部可能回呼其他受保護的函數。 ### 準則 6:保護環境,高風險操作在 VM 做 ``` ⚠️ 不在 host 上做的事: - Raw socket capture(可能 hang 整個系統) - 認證過程中 attach debugger - 任何可能觸發反除錯的操作 - 未知行為的程式首次執行 ✅ 只在 VM 裡做的事: - Debugger attach、記憶體注入 - 可能觸發反調試的操作 ``` ### 準則 7:卡關時的決策流程 ``` 卡住了? │ ├─ 1. 停下來,不要繼續同方向硬撞(一個方法最多試 3 個變體) │ ├─ 2. 查 failure-patterns.md — 是不是踩了已知的坑? │ ├─ 3. 上網搜尋 — 有沒有類似保護殼/驗證系統的公開破解案例? │ └─ "[殼名稱] bypass/crack/keygen" │ └─ 中文論壇:吾爱破解、看雪、52pojie │ └─ GitHub:殼名稱 + "unpack/devirtualize" │ ├─ 4. 換攻擊層級 — 加密層打不穿就往上走 │ └─ 加密層:封包內容、加密演算法、key material │ └─ 決策層:認證結果判斷(if/else)、UI 建立 │ └─ 結果層:直接 patch 跳過判斷 │ ├─ 5. 換工具 — 工具不是目的,解題才是 │ └─ 6. 15 分鐘沒進展就停 — 回到步驟 2 ``` ### 準則 8:提方案前的 checklist ``` □ 讀過失敗記錄了嗎?新方案跟已失敗方法有無相似? □ 方案的先決條件都滿足嗎?(不需要真 key?不需要 server 在線?) □ 方案達成了目標嗎? □ 不確定的部分有查過資料嗎? □ 有具體的驗證步驟嗎?(不是「試試看」) ``` --- ## 第二部分:執行流程 ### Phase 0:環境確認 ``` □ 目標 binary 在哪裡?目標是什麼? □ 靜態分析和動態測試是同一台機器嗎? □ 動態環境:Windows?管理員權限?VM 還是 host? □ 有 Python?有 Frida? □ 題目有提示嗎?有時效性資源嗎? □ 有沒有已經做過的分析? □ 專案目錄有 CLAUDE.md 嗎?(沒有就建立,見下方) ``` **專案初始化:CLAUDE.md** 進入專案時,如果專案目錄沒有 `CLAUDE.md`,**必須立即建立**,內容至少包含: ```markdown ## 記憶管理 專案記憶存放在本專案資料夾的 ./memory/ 中,請從該資料夾讀寫記憶,不要使用預設的 ~/.claude/projects/ 路徑。 ## 工作目錄 所有操作產出(script、測試、工具、文件等)一律放在 `./workspace/` 資料夾中。包含但不限於:.py, .txt, .bin, .sh, .c, .exe, .md 等。 **例外**: - `CLAUDE.md` — 放專案根目錄 - `memory/` — 放專案根目錄 ``` 同時建立 `memory/` 和 `workspace/` 資料夾。如果已有 `CLAUDE.md`,檢查是否包含上述兩條規則,缺少就補上。 --- ### Phase 1:開題偵察(靜態) > 目標:不執行程式,盡可能了解一切。 **1.1 基本資訊** 用 [scripts/pe_info.py](scripts/pe_info.py) 解析(不依賴外部套件): ```bash python pe_info.py target.exe # 基本分析 python pe_info.py target.exe --strings # 加字串搜尋(IP、URL、配置檔等) python pe_info.py target.exe --gbk "任意中文" # 搜尋 GBK 編碼的中文字串 python pe_info.py target.exe --json # JSON 輸出(給其他腳本接力) ``` 輸出包含:Architecture、ImageBase、Entry Point、ASLR/DEP、Manifest、 Section 列表 + entropy + 保護殼判斷、Import 分析 + 攻擊面提示。 `--gbk` 用來搜尋簡體中文程式裡的任意關鍵字(驗證訊息、錯誤提示、功能名稱等), 幫助定位認證邏輯在 binary 中的位置。 **1.2 Section 分析** 列出所有 section:名稱、VA、大小、entropy。 | 特徵 | 判斷 | |------|------| | `.vmp` `.svmp` + entropy ~8.0 | VMProtect → 見 [docs/vmp-guide.md](docs/vmp-guide.md) | | `.upx0` `.upx1` | UPX → `upx -d` 脫殼 → 重新分析 | | `.themida` | Themida/WinLicense | | `.text` RawSize = 0 | 殼搬移了程式碼 | | Entry Point 不在 .text | 殼的 loader 先跑 | | .rdata 裡有 MZ header | 內嵌 payload | **1.3 Import 分析** | Import | 意義 | 攻擊面 | |--------|------|--------| | WS2_32.dll (send/recv/connect) | 網路驗證 | hook connect 找 server | | lstrcmpA / CompareStringA | 本地字串比對 | hook 看比較內容 | | GetProcAddress | 動態解析 | 需要 runtime hook | | CryptDecrypt / BCrypt* | 加密 | hook 看明文 | | MessageBoxA/W | 錯誤訊息 | hook backtrace 找決策點 | | CreateWindowExA/W | 建立視窗 | **最重要** — 主視窗建立 = bypass 成功 | **1.4 字串搜尋** 搜尋:驗證結果字串、IP/domain/URL、配置檔路徑、Window class/title、保護殼特徵。 中文程式依 locale 可能用 GBK(簡體)或 Big5(繁體)編碼: ```bash python pe_info.py target.exe --gbk "关键字" # 簡體 python pe_info.py target.exe --big5 "關鍵字" # 繁體 ``` 不確定編碼時三種都搜(見速查表)。 **1.5 識別保護殼和驗證框架 → 立刻上網搜尋** ``` 搜尋策略: 1. "[殼名稱] bypass/crack" — 找已知繞過方法 2. "[殼名稱] [版本] unpack" — 找脫殼工具 3. 中文論壇(吾爱破解、看雪、52pojie)— 中文程式的殼通常中文社群有解 4. GitHub "[殼名稱]" — 找自動化工具 5. 商業驗證框架 — 搜尋已知弱點和超級密碼 ``` **1.6 路線判斷** ``` 保護殼? ├─ UPX → upx -d → 重新分析 ├─ VMProtect → 不脫殼,API 邊界攻擊 → 載入 docs/vmp-guide.md ├─ Themida → 搜尋版本對應的脫殼工具,或 API 邊界攻擊 ├─ 自製殼 → 分析 loader 邏輯 ├─ 無殼 → Ghidra 反編譯 └─ 不確定 → 上網搜尋 section 名稱 驗證類型? ├─ 網路驗證(有 WS2_32)→ 優先攻擊決策層,不攻擊加密層 ├─ 本地驗證(有 strcmp 等)→ hook 比較函數 ├─ 時間驗證(GetSystemTime / GetLocalTime)→ hook 時間 API └─ 混合 → 先確認哪個是主驗證 開發框架? ├─ 易語言 (EPL) → MFC 底層,UI 用 Win32 API ├─ .NET → dnSpy,本 skill 不太適用 ├─ Delphi → IDR + x32dbg └─ 標準 C/C++ → Ghidra ``` **產出:** 靜態分析報告 + 判斷路線。跨機器時打包交接包。 --- ### Phase 2:動態偵察 > 原則:先輕量觀察,再精準 hook,不要一上來就全面注入。 **2.1 輕量觀察(零工具)** 直接跑一次:看 UI、`netstat -an` 看連線、看配置檔、記錄錯誤訊息。 **2.2 Frida 被動偵察** ```bash frida -l scripts/recon.js -f challenge.exe # spawn frida -l scripts/recon.js -p <PID> # attach frida -l scripts/recon.js -f challenge.exe --no-pause # spawn + 自動 resume ``` [scripts/recon.js](scripts/recon.js) 預設啟用:Network、CreateWindowEx、MessageBox、Strcmp。 其他模組(Crypto、File、Registry、Time、MemScan)透過腳本開頭的 ENABLE 開關控制。 Hook 目標: - `connect` → IP:port - `send`/`recv` → 封包大小、輪數、RetAddr(**不修改內容**) - `CreateWindowExA/W` → 視窗建立 + **完整 backtrace**(最重要) - `MessageBoxA/W` → 錯誤訊息 + backtrace - `lstrcmpA` / `CompareStringA` → 比較內容(本地驗證時) **Differential trace(跑兩次做 diff):** 1. 有效輸入 → 記錄 API call 序列 2. 無效輸入 → 記錄 API call 序列 3. diff 兩份結果 → 分歧點就是決策位置 **2.3 如果 Frida 被偵測** | 順序 | 方法 | 原理 | |------|------|------| | 1 | Frida Gadget mode | DLL sideload,繞過 frida-server 偵測 | | 2 | TitanHide kernel driver | kernel 層隱藏 debug 資訊 | | 3 | Hook 偵測函數 | 攔截 strstr("frida")、NtQueryInformationProcess | | 4 | x32dbg + ScyllaHide | 傳統 debugger | | 5 | 自製 C debugger | 最低調,hardware breakpoint | **監看數值時優先用 Stalker 而非 INT3 斷點。** Stalker 用 dynamic binary instrumentation(動態改寫 code 到 slab),不插入 0xCC、不設 debug register,比 INT3 更不容易搞掛目標進程。詳見 [docs/tools-quickref.md](docs/tools-quickref.md)。 遇到特定保護殼的反調試問題,見對應指南(如 [docs/vmp-guide.md](docs/vmp-guide.md))。 --- ### Phase 3:繞過 > 原則:從決策層開始,不要從加密層開始。 **路線選擇:** ``` 有 CreateWindowExA 的 backtrace? → 路線 A(最高成功率) 有錯誤訊息的 backtrace? → 路線 B 有字串比較(strcmp)的 hook 結果? → 路線 C-local 都沒有? → 路線 C(搜尋字串交叉引用) ``` **路線 A:從 CreateWindowExA backtrace 找決策點(首選)** 1. 讀取 backtrace return address 附近 ±64 bytes 2. 找條件跳轉(JE/JNE = `74`/`75`/`0F 84`/`0F 85`) 3. `Memory.patchCode` → NOP 或反轉 4. 主視窗出現 = 成功 **路線 B:從錯誤訊息往回追** 1. MessageBox backtrace → 定位錯誤分支 2. 往上找條件跳轉 → 反轉 **路線 C-local:本地驗證 — hook 比較函數** 1. hook lstrcmpA/CompareStringA 看到比較的兩個字串 2. 其中一個是使用者輸入,另一個就是正確密碼 3. 或者直接 patch 比較結果 **路線 C:搜尋字串交叉引用** 1. 記憶體搜尋驗證結果字串(成功/失敗的 GBK bytes) 2. 找誰引用了這個地址 → 認證 handler 3. 在 handler 裡找條件跳轉 **路線 D:網路驗證的協議層攻擊** ⚠️ **只有在加密可破時才值得嘗試。Session-bound 加密直接放棄這條路。** **路線 E:Binary patch(runtime patch 驗證成功後)** 1. Frida runtime patch 確認可行 → 計算 file offset → Python 修改 binary → 驗證 --- ### Phase 4:驗證和記錄 1. 不掛工具跑 patched binary → 確認獨立運作 2. 不同假輸入 → 確認不依賴特定輸入 3. 記錄:改了什麼、為什麼、哪些方法失敗了 --- ## 第三部分:速查表 ### 條件跳轉 Patch | 原始 | Patch 後 | 效果 | |------|---------|------| | `75 XX` (JNE short) | `74 XX` / `EB XX` / `90 90` | 反轉 / 無條件跳 / 移除 | | `74 XX` (JE short) | `75 XX` / `EB XX` / `90 90` | 反轉 / 無條件跳 / 移除 | | `0F 85 XX XX XX XX` (JNE near) | `0F 84` / `E9 XX XX XX XX 90` / `90x6` | 反轉 / 無條件跳 / 移除 | | `0F 84 XX XX XX XX` (JE near) | `0F 85` / `E9 XX XX XX XX 90` / `90x6` | 反轉 / 無條件跳 / 移除 | ### 函數返回值 | 目的 | Patch | |------|-------| | 永遠返回 0 | `33 C0 C3` (xor eax,eax; ret) | | 永遠返回 1 | `33 C0 40 C3` (xor eax,eax; inc eax; ret) | | 跳過函數 | `C3` (ret) | ### 檔案偏移計算 ``` file_offset = RVA - section_VA + section_file_offset RVA = runtime_VA - ImageBase ``` ### 中文 Hex 轉換 中文程式在 binary 裡的字串編碼取決於開發環境的 locale: - **GBK**:簡體中文(大陸) - **Big5**:繁體中文(台灣、香港) - **UTF-8**:較新的程式或跨平台框架 用 `pe_info.py --gbk` 或 `--big5` 搜尋,或手動轉換: ```python # Python one-liner:任意中文轉指定編碼的 hex python3 -c "print(' '.join(f'{b:02X}' for b in '你要搜的字'.encode('gbk')))" python3 -c "print(' '.join(f'{b:02X}' for b in '你要搜的字'.encode('big5')))" python3 -c "print(' '.join(f'{b:02X}' for b in '你要搜的字'.encode('utf-8')))" ``` 如何判斷目標用哪種編碼: - 看 PE 的 locale / code page(Ghidra 的 PE header 或 resource section) - 簡體字(如「验证」「错误」)→ 大概率 GBK - 繁體字(如「驗證」「錯誤」)→ 大概率 Big5 - 不確定 → 三種都搜一遍,看哪個有 hit --- ## 第四部分:工具清單 ### 必備 | 工具 | 用途 | 安裝 | |------|------|------| | Frida | 動態 hook、API 攔截、記憶體 patch | `pip install frida-tools` | | Python 3 | 腳本、PE 分析、binary patch | 內建或下載 | ### 建議 | 工具 | 用途 | 何時需要 | |------|------|---------| | Ghidra | 靜態反編譯 | 無殼或脫殼後的深度分析 | | x32dbg/x64dbg | 傳統 debugger | Frida 被擋時 | | LIEF | PE 結構修改 | 需要改 PE 結構時 | | ScyllaHide | x64dbg 反反調試 | 保護殼偵測 debugger 時 | | TitanHide | Kernel 層反反調試 | 保護殼用 direct syscall 時 | | dnSpy | .NET 反編譯 | .NET 程式 | | tshark | 命令行抓包 | 網路驗證協議分析 | --- ## Anti-patterns - ❌ **猜了就動手** — 「可能是 X」還沒驗證就開始改;先設計最小驗證實驗拿到資料再決策(準則 2) - ❌ **靜態沒挖乾淨就跳動態** — 動態測試消耗資源、每輪要 user 配合;靜態做到沒東西可挖再一次規劃批量動態(準則 4) - ❌ **重試已失敗的方法** — 提方案前先對照 `failed_methods` / failure-patterns.md,相似邏輯要講清楚這次差在哪(準則 3) - ❌ **從加密層開刀** — Session-bound 加密直接放棄協議層;bypass 從決策層(CreateWindowEx / 錯誤訊息 backtrace)進(Phase 3) - ❌ **在 host 上做高風險操作** — debugger attach / 記憶體注入 / 首次跑未知行為程式一律進 VM(準則 6) - ❌ **目標搞錯就狂搜 flag** — 目標是 bypass 就別搜 flag、別提議用真 key;每個方案先問「達成目標了嗎」(準則 1) - ❌ **拿真實軟體來破** — 本 skill 只服務授權的 CTF / lab 題目;未授權的商業軟體破解不在範圍 ## Important rules 1. **授權邊界** — 只做授權 CTF / lab;非授權破解、惡意部署、竊取憑證一律拒 2. **先驗證再行動** — 觀察 → 最小實驗 → 資料 → 決策,不猜;區分「已驗證」與「未驗證假設」 3. **靜態到底、動態批量** — 不消耗資源的先做滿,消耗資源的一次規劃 4. **決策層優先於加密層** — bypass 從 if/else 判斷點進,不硬拆加密 5. **高風險進 VM** — attach / 注入 / 未知程式首跑,全在 VM 6. **記錄失敗** — 改了什麼、為什麼、哪些方法失敗,寫進專案 memory 7. **卡關 15 分鐘就停** — 回查失敗模式、換層級 / 工具,不同方向硬撞 ## 參考資料 - [docs/failure-patterns.md](docs/failure-patterns.md) — 失敗模式資料庫(泛用 + 保護殼專區) - [docs/bypass-auth.md](docs/bypass-auth.md) — Frida 腳本範本 + Binary patch 詳細流程 - [docs/vmp-guide.md](docs/vmp-guide.md) — VMProtect 專區(反調試、VMP 特有的陷阱和對策) - [docs/tools-quickref.md](docs/tools-quickref.md) — Capstone / Unicorn / Frida Stalker 使用速查 - [docs/workflow-rules.md](docs/workflow-rules.md) — 動態測試工作流程規範 - [scripts/recon.js](scripts/recon.js) — Frida 被動偵察腳本(模組化,開關控制) - [scripts/pe_info.py](scripts/pe_info.py) — PE 靜態分析腳本(純 stdlib,零依賴) --- ## Challenge $ARGUMENTS
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.