呂凱崴kaiweilu.tw

APKSec Pro 驗證體系:546 支 APK 的獨立比對基準

自動化測試只能證明程式照作者的理解運作;要知道系統在真實 App 上會不會漏報,需要一份不由系統本身產生的答案。本專題以獨立判定引擎與反編譯逐支審查,對 546 支 APK 建立比對基準、逐條文比對系統判定;每次判定碼變更就重掃全部語料,逐筆歸因每一個翻轉。

期間
2026-09 至今(比對基準於 9 月 17 至 19 日建置;其後每次判定碼變更即重掃)
角色
獨立完成:驗證體系與比對基準的方法設計、覆核流程設計與驗收、每次重掃的翻轉歸因與裁決
技術
Python、pytest、Androguard、jadx 1.5.6、AI agent 平行審查、Android 模擬器(動態回歸)
規模
546 支 APK;11 條可靜態獨立判定的條文;584 筆反編譯確認的 App 自有漏洞(294 支);每次重掃 26,754 個判定

書審對應:附件 A-2;系統與判定方法見 A-1,400 支實證與揭露見 A-3

本頁目錄
  1. 驗證要回答的問題
  2. 五層驗證
  3. 從抽樣到全數
  4. 546 支語料
  5. 第一層:獨立判定引擎
  6. 第二層:反編譯逐支審查
  7. 混淆矩陣
  8. 全數重掃與翻轉歸因
  9. 為什麼可以相信「零漏報」

驗證要回答的問題

系統採召回優先(定義見 A-1),因此最重要的指標不是「判對幾成」,而是兩件事:已確認的真漏洞有沒有被判為通過;以及可以明確斷言的違反,系統有沒有全數判出。這兩件事都無法只靠單元測試回答。測試驗證的是作者預想得到的型態,真實上架 App 則會經過加固、混淆與各種建置工具處理,出現作者沒有預想的型態。要回答它們,需要一份不由被測系統產生的答案,而且答案本身也要能被檢驗。

五層驗證

每一次判定邏輯的變更都依「先改合約、確認後改程式、再跑全套驗證」進行(流程見 A-1 工程實務)。驗證由內而外分五層:

層做法規模(v2.4.1)回答的問題
1 自動化測試pytest:合規引擎、合約同步、規則、動態分析器、LLM、報告與介面1,105 項程式行為是否與合約一致
2 多 APK 回歸樣本 APK 的完整違反集合逐支比對基準14 支;34 個 rule ID 中 32 個由真實樣本觸發規則在整支 APK 上是否多報或漏報
3 公開漏洞交叉驗證公開漏洞 App 的已知漏洞逐項對應 V4.0 條文4 支、34 筆,漏報 0已知漏洞是否被判為通過或不適用
4 自建示範 App有漏洞版與安全版,原始碼與建置腳本入庫2 支靜態、動態與 LLM 項的正反控制
5 546 支比對基準獨立判定引擎加反編譯逐支審查546 支、584 筆確認漏洞在真實上架 App 上是否漏報

多 APK 回歸

樣本 14 支:教學用的漏洞 App 6 支(InsecureBankv2、GoatDroid、Sieve、PIVAA、AndroGoat、InjuredAndroid)、OWASP MAS 的逆向練習題 4 支(UnCrackable Level 1 至 3、MASTG Level 4)、一般 App 2 支,以及自建示範 App 2 支。回歸腳本走含 DEX 方法分析的完整路徑,並關閉 CVE 查詢與伺服器憑證探測以確保離線可重現;每支 APK 的非 INFO rule ID 集合必須與基準完全相同,漏報、多報或出現未知 rule ID 任一即判失敗。對應的測試更嚴格,rule ID、嚴重度與 finding 數都必須一致。34 個 rule ID 中有 32 個由真實樣本觸發,其餘兩條防呆規則(加殼、網路安全組態讀取)以合成樣本測試。規則嚴重度有變動時,先輸出前後差異、逐條確認,才更新基準。

公開漏洞交叉驗證

AndroGoat 8 筆、PIVAA 9 筆、InsecureBankv2 9 筆、DIVA 8 筆,共 34 個已知漏洞,逐一對應到 V4.0 條文:例如 SQL 注入對應 4.1.5.4.2、未驗證伺服器憑證對應 4.1.4.2.2、logcat 洩漏對應 4.1.2.3.16、不安全的 WebView 對應 4.2.2.1.2、SharedPreferences 與 SQLite 明文對應 4.1.2.3.6。判準是:判不通過為命中、判需人工為安全交付、判通過或不適用即為漏報。最近一次結果為命中 33 筆、安全交付 1 筆、漏報 0 筆。

自建示範 App

兩支示範 App 不經 Gradle,以 aapt2、javac、d8 或 R8、zipalign、apksigner 從原始碼建置,任何人都能重現。有漏洞版在 Manifest、網路安全組態與程式碼中植入各類弱點:可偵錯、允許備份與明文、無權限保護的四類匯出元件、信任使用者 CA、TLSv1、DES/ECB、以 java.util.Random 產生金鑰、全信任的 TrustManager 與恆真的 HostnameVerifier、WebView 檔案跨來源存取與忽略 SSL 錯誤、SQL 拼接、密碼寫入日誌、假的雲端服務帳戶金鑰、多處明文殘留、同意前與拒絕後仍讀取裝置識別碼,以及沒有強度提醒的密碼設定頁。安全版則只宣告網路權限、關閉偵錯與備份、以 NSC 禁止明文並綁定憑證、以 Keystore 的 AES-GCM 加密儲存、整個視窗套用 FLAG_SECURE。

正反控制:靜態上,有漏洞版觸發全部 24 條可判不通過的規則,安全版零違反。動態上(模擬器實跑),有漏洞版的 10 個動態項必須判不通過、安全版同樣 10 項不得判不通過;畫面擷取只有安全版能以全黑判通過。LLM 項以預錄回應重放:可判不通過的 7 條規則在有漏洞版上必須判不通過、在安全版上不得判不通過。送測資料檔中的帳號是假帳號,隨原始碼入庫。

從抽樣到全數

比對基準之前先做過抽樣一致性驗證:三批合計 25 支不重複的 App,先以不看系統輸出的獨立逆向逐條文盲判並寫成紀錄,再與系統判定逐格比對。結果沒有條文層級的漏報,每一條結構型不通過都有獨立逆向的事實支撐。其中一批 8 支的 10 個初判差異,逐一反組譯判定誰對,結論全是系統較精確,例如:同一類別以 java.util.Random 產生 IV 與金鑰(盲判未發現的真弱點);字面只有「AES」的命中實際是 AES/GCM;沒有 NSC 且 targetSdk 36 時明文已被平台預設封鎖;匯出元件的自訂 action 排在前三個標準 action 之後;舊版 JSON 函式庫帶有 CVSS 7.5 的已知漏洞。這些差異說明引數層與位元碼層的分析比關鍵字比對精確。

只在特定加固或建置工具下出現的型態,抽樣不一定抽得到:例如網路安全組態檔路徑被建置工具縮短的 App,在 546 支中有 108 支,25 支抽樣中一支也沒有。因此驗證擴大到手上全部的 APK。

546 支語料

來源支數
400 支上架 App(實證語料,見 A-3)400
前幾波取得的其他真實 App128
公開漏洞示範與一般樣本16
自建示範 App2
合計(依檔名去重)546

每支 APK 的 SHA-256 都記錄在案,審查結果綁定的就是這一版安裝檔,備份與還原時逐支核對;APK 原檔合計 30.5 GB,只留在本機。系統側一律以當前判定碼重掃,不沿用舊報告:規則改過之後,舊報告會把現在已能判出的違反顯示成漏報,製造假的比對結果。

第一層:獨立判定引擎

獨立判定引擎只讀 APK 原文:以 Androguard 解析 Manifest 與網路安全組態,以 zipfile 對每個檔案做位元組層級的樣式比對(先還原 \xNN 轉義)。它不呼叫系統的任何判定程式、不讀系統的判定結果;系統判定另行讀入,只作為被比對的對象。

比對涵蓋 11 條可以靜態獨立判定的條文:4.1.2.3.6、4.1.2.3.8、4.1.2.3.12、4.1.2.3.14、4.1.2.4.1、4.1.2.5.3、4.1.4.2.2、4.1.4.2.3、4.1.5.4.2、4.1.5.5.9、4.2.2.1.2。動態專屬的條文與需人工條文不在比對集內。

引擎的結論分兩級。高信心只用於可以直接斷言的事實:Manifest 屬性(debuggable、allowBackup、實際匯出且無權限保護的元件)、生效中的 NSC 設定(允許明文、正式版區塊信任使用者 CA)、結構化憑證(服務帳戶金鑰、放在金鑰檔中的 PEM 私鑰、特定格式權杖)。候選用於只有字串訊號的條文:弱加密演算法名稱、TrustManager 與 HostnameVerifier、WebView 設定、SQL 字串;這些不作斷言,交給第二層反編譯確認。4.1.2.3.6、4.1.4.2.2、4.1.5.4.2、4.2.2.1.2 四條因此從不以高信心判定。

比對基準本身也要接受證據檢驗。某次比對出現 9 筆「引擎高信心判不通過、系統交人工」的分歧,逐筆裁決後都是引擎不夠精確:7 支 App 的 NSC 檔存在於安裝檔內,Manifest 卻沒有引用它(Android 不會採用);1 支的明文設定只寫在除錯組建才生效的區塊;1 支的 NSC 明確關閉明文、覆寫了 Manifest 的設定。引擎據此依 Android 的實際行為校正五處(查 Manifest 是否引用 NSC、依資源表的實際路徑讀檔、依區塊語意解讀、有生效 NSC 時忽略 Manifest 的明文屬性、參照解析不出時不作高信心斷言),重產 546 支的獨立判定後,該類分歧為 0,雙方皆判不通過的高信心筆數為 1,121。

第二層:反編譯逐支審查

第一層只能斷言組態層的事實;程式碼層的漏洞必須讀程式碼。第二層以 jadx 1.5.6 反編譯,逐支審查 App 自有的程式碼:

  • 金標準個案:先以 jadx 逐條深挖高價值 App,其中 10 支內嵌真實私鑰或憑證,系統的 4.1.2.3.8 全數判不通過。
  • 全數審查:分兩批(高價值 179 支、其餘 367 支),每支 App 由一個審查 agent 負責。先以腳本抽出 App 自有類別中與信任、主機名稱驗證、弱加密、WebView、SQL、亂數相關的方法 smali;第一層的事實只當索引,審查者不得讀取系統判定,smali 看不清時再以 jadx 反編譯。審查五條程式碼層條文(4.1.4.2.2、4.1.2.3.6、4.2.2.1.2、4.1.5.4.2、4.1.2.3.8);只有 App 自有的程式碼能判不通過,有訊號但無法確認者判需人工。
  • 紀錄:每筆發現記錄條文、判定、是否 App 自有、可定位的證據(類別、方法與 smali,或資源路徑)、判讀理由與可利用性。
  • 對抗式複查:審查結果另交兩個驗證視角:「歸屬與型態」檢查是否真屬 App 自有、型態是否真是漏洞;「可達性與可利用性」檢查是否為死碼、測試或除錯限定的程式。驗證者重讀 smali,不確定時預設推翻;兩個視角都推翻才剔除,保留下來的每一筆都附有可定位的反編譯證據。
  • 分工:審查與複查以本人設計的流程驅動多個 AI agent 平行執行,本人設計判讀規則並驗收結果。

結果是 584 筆 App 自有的不通過,分布於 294 支 App:

條文確認漏洞系統判不通過系統交人工系統判通過
4.2.2.1.2 WebView 設定183781050
4.1.4.2.2 TLS 憑證驗證停用128112160
4.1.5.4.2 注入10558470
4.1.2.3.8 硬編碼機密9010800
4.1.2.3.6 弱加密或寫死金鑰7662140
4.1.2.4.1 明文傳輸1100
4.1.4.2.3 信任使用者 CA1100
合計5843222620

混淆矩陣

第一層:獨立判定引擎與系統

條文雙方皆不通過(高信心)雙方皆不通過(候選)高信心漏報候選漏報引擎判不通過、系統交人工疑似誤報雙方皆無
4.1.2.3.602230000105
4.1.2.3.81100000355
4.1.2.3.12335180000124
4.1.2.3.1412100000425
4.1.2.4.127731000091
4.1.2.5.3335180000124
4.1.4.2.20255010063
4.1.4.2.33400000288
4.1.5.4.20158000034
4.1.5.5.9800000538
4.2.2.1.201240100123
合計1,12182702002,270

讀法:

  • 引擎高信心判不通過的 1,121 筆,系統全數判不通過,交人工 0 筆、判通過或不適用 0 筆;也沒有任何一格是系統判不通過、引擎卻明確判為沒有違反。
  • 候選漏報 2 筆(系統判通過或不適用、引擎只有字串訊號),分別出自早期自建的測試樣本與一支開源 App,經第二層反編譯審查皆未確認為漏洞,例如 NSC 的 debug-overrides 只在除錯組建生效。
  • 其餘為系統判不通過而引擎沒有對應訊號 311 格、引擎為候選而系統交人工 1,475 格;全部合計 6,006 格,正好是 546 支乘以 11 條。

第二層:反編譯確認的漏洞與系統

584 筆確認漏洞中,系統自動判不通過 322 筆、附線索交人工 262 筆、判通過 0 筆:召回優先的底線「真漏洞不得被判為通過」在全部語料上成立。交人工的 262 筆有 232 筆集中在三類,都是合約刻意交給人的:

  • WebView 設定(105 筆):系統只對以常數 true 開啟的檔案跨來源存取,以及 minSdk 低於 17 的 JavaScript 介面判不通過;橋接物件暴露給遠端內容是否危險,取決於載入的內容,屬於需網頁弱掃的範圍。
  • 硬編碼機密(80 筆):系統只對結構自證的憑證判不通過;OAuth client secret、寫死的帳密這類沒有固定格式的字串,由工具列線索、交人讀前後文確認,以免把用戶端公開識別碼誤判為機密。
  • 注入(47 筆):系統只對同一類別內的 SQL 陳述式拼接判不通過;輸入是否外部可控,要看資料流。

全數重掃與翻轉歸因

每次判定碼變更,都以當前判定碼重掃 546 支。一次全數重掃是 546 支乘以 49 項,共 26,754 個判定。流程:

  1. 把前一次的系統報告移出、另存,作為比較基準。
  2. 以 6 個分片平行重掃,546 支約 2 小時;瓶頸是記憶體,每個掃描行程需要 2.5 至 4.4 GB。
  3. 解析層有變更時,先從版本控制取出舊版解析器,對 546 支逐欄比對新舊解析結果,作為歸因依據。
  4. 新舊報告逐支、逐條比對判定,列出每一個翻轉。
  5. 逐筆比對新舊報告的不通過成因與線索,把每個翻轉歸入預期的變更;「剛好落在同一支 App」不算解釋,無法歸因即視為回歸。
  6. 重產兩層混淆矩陣(先保存舊版以便前後比較),再重建比對基準的遮罩鏡像。

以下是幾次重掃的實例。

單邊判定擴充螢幕覆蓋防護(4.1.5.1.3)由需人工改為單邊不通過後,全數重掃翻轉 384 筆:383 筆是這項改動的預期結果;另 1 筆是某受測伺服器的憑證恰好在兩次掃描之間到期。規則沒有變,是真實世界變了;含線上探測的條文,判定會隨時間漂移,歸因時必須分得出來。

解析層與 Android 對齊比對發現三類解析型態。(1) 10 支 App 的 Manifest 屬性被剝除命名空間,常見於加固或混淆工具處理後,但仍帶框架資源編號(共 2,026 個屬性);Android 依資源編號辨識屬性,照常生效。系統改為與 Android 相同、依資源編號還原,而不採「改讀無命名空間同名屬性」的捷徑:開發者漏寫 android: 前綴的屬性沒有資源編號,Android 並不採認,捷徑會把漏寫前綴的 allowBackup="false" 誤讀為有效而判通過。(2) 建置工具縮短資源路徑後,NSC 檔不在慣用位置,改依資源表記載的實際路徑讀取,546 支中有 108 支受影響。(3) 伺服器憑證探測的主機上限改為只限制從字串撈取的主機,App 明示的網域不佔名額。重掃翻轉 106 筆(69 支),逐筆歸因、非預期 0:新判不通過 73 筆(主要是讀到 NSC 後的明文傳輸 49 筆、信任使用者 CA 10 筆、宣告網域的憑證探測 12 筆)、撤銷不通過 31 筆、通過改需人工 2 筆。

TLS 位元碼型態第一次全數比對後,把三種方法體層級的 TLS 驗證停用型態寫入合約(型態見 A-1)。4.1.4.2.2 判不通過的 App 由 201 支增為 323 支,與反編譯審查一致的筆數由 64 增為 114。同時評估過以「沒有呼叫者」判定死碼而降級:只能降 12 支,且其中包含經反編譯確認的真實漏洞;因此位元碼可證的驗證停用不做靜態可達性降級。

加殼防呆對螢幕覆蓋防護的 381 筆不通過做全數獨立複核,直接以原始位元組搜尋 DEX 方法名、layout 屬性名與屬性資源編號,不經系統規則:267 支整個安裝檔都沒有防護訊號,100 支的訊號只在系統排除的函式庫畫面,14 支的 DEX 位元組中有防護方法名稱,逐支追查後其中 11 支是加殼:安裝檔內的程式碼只有殼,真正的程式碼在執行時才解出。接著以「Manifest 宣告的元件有多少比例不在可見 DEX」量測全部 546 支,分布呈兩群:481 支低於一成、18 支介於一成至五成、47 支在五成以上(其中 43 支超過九成),後者集中在銀行、券商與支付 App。據此把「看不到就不判」寫入合約(見 A-1),並只重掃受影響的 163 支:284 筆翻轉全數可歸因(NSC 明文區塊裸 IP 的憑證比對改列線索 15 筆、加殼防呆 269 筆),全部轉為需人工,沒有任何一筆轉為通過;解析函式庫升版影響的 36 支則零翻轉。

影響面量化後的最小重掃收緊 4.2.2.1.2 靜態「無連網」的通過條件時(WebView 全部內容進入點皆須為內嵌常數),先量化影響面:語料中該項判通過者 8 支,6 支未宣告網路權限、走的分支不受影響;其餘 2 支只載入本地資源,僅 1 支的程式碼含其他內容進入點。只重掃這 2 支,翻轉 1 筆(通過改為需人工),其餘 544 支沿用前一次基準。

為什麼可以相信「零漏報」

  1. 答案不由被測系統產生:獨立判定引擎不呼叫系統的判定程式;反編譯審查者不得讀取系統判定。
  2. 可斷言與待確認分開:組態層可以直接斷言的事實才進高信心層;只有字串訊號的交給反編譯確認,不讓弱證據進入比對基準。
  3. 全數而非抽樣:加固、混淆與建置工具造成的解析型態只在大語料中現形,全數比對才找得到它們。
  4. 一律以當前判定碼重掃:比較對象永遠是現行判定碼的輸出,不沿用舊報告。
  5. 每個翻轉都要歸因:26,754 個判定中的每一個變動都要能指出原因,無法歸因即視為回歸。
  6. 比對基準本身也受檢驗:引擎與系統分歧時逐筆裁決,不精確的一方依 Android 的實際行為校正,引擎也不例外。
  7. 可重算:APK 以 SHA-256 綁定,比對基準以遮罩鏡像入版本控制(遮罩方式見 A-3);換新版系統只需重掃、重產矩陣,不必重新逆向。
  8. 主張的範圍明確:「零漏報」指 11 條可靜態獨立判定的條文上,引擎高信心判不通過的 1,121 筆全數命中;「無一被判通過」指 584 筆反編譯確認的 App 自有漏洞。動態項、LLM 項與需人工項不在這兩個數字內,由自建示範 App 的正反控制驗證。