Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download kerberosclaw-kc_ai_skills-ctf-kit-ad005ac.zip · 38 KB
Part of kerberosclaw/kc_ai_skills — 25 skills

Install

skills CLI npx skills add https://github.com/KerberosClaw/kc_ai_skills/tree/main/ctf-kit
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kerberosclaw-kc-ai-skills@llmmart
Git 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: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/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 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 分鐘就停 — 回查失敗模式、換層級 / 工具,不同方向硬撞

參考資料


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.

No comments yet.

Reviews (0)

No reviews yet.

Related