更新紀錄(僅初版,2026年08月28日 發布)
- 初次發布
從檔案總管啟動時讀得到組態檔,從工作排程器啟動卻說「找不到」。改成服務執行之後,儲存的密碼解不開了。手邊的瀏覽器明明已經登入,CI 上卻又回到登入畫面。
碰到這一類問題,比看程式碼更先要確認的是「以誰的身分、以什麼方式在執行」。因為同一台 PC 上的檔案和設定,換一個執行使用者未必就能照樣使用。
本文要回答的問題是:程式碼一行沒改,只是改成了排程工作、服務或 CI 作業,應用程式為什麼就壞了。想從症狀入手排查的話,請從下表進入對應的位置。
| 症狀 | 首先要比對的東西 | 閱讀位置 |
|---|---|---|
| 找不到組態檔或命令 | 執行使用者、AppData 的實際路徑、使用者環境變數 | AppData 與環境變數 |
| 明明寫進去的登錄檔值不見了 | HKCU 指向的那個使用者的 hive | HKCU |
| 檔案讀得到,機密資訊卻解不開 | DPAPI 的範圍與主要金鑰的歸屬 | DPAPI |
| 瀏覽器的登入狀態帶不過去 | 執行使用者、設定檔、受 DPAPI 保護的金鑰 | 瀏覽器設定檔 |
| 已儲存的認證資訊或憑證用不了 | 執行帳戶的保存庫與憑證存放區、工作的登入類型 | 認證資訊與憑證 |
| 以系統管理員身分執行就沒有 Z: 磁碟機 | 提升權限前後的權杖與登入工作階段 | UAC 權限提升 |
| 想在移轉前整體檢查一遍 | 各執行形態各自依賴什麼、需要怎樣的安裝設定 | 依執行形態的檢查清單、調查步驟 |
本文的前提
| 項目 | 內容 |
|---|---|
| 目標讀者 | 要把業務應用程式改成服務、排程工作或 CI 作業的開發者,以及調查「在我這邊明明能跑」的維運人員 |
| 前提環境 | Windows 10/11。驗證程式碼在 PowerShell 5.1 以上執行 |
| 難度 | 中級 |
1. 先說結論
劃分 Windows 執行環境的基本單位不是 PC,而是存取權杖與 SID,也就是「以誰的身分在執行」。AppData、HKCU、DPAPI 的金鑰、瀏覽器設定檔、認證資訊,都屬於那個使用者的環境。
你以互動式登入的身分備好的設定與登入狀態,不會自動被 SYSTEM 或別的服務帳戶繼承。放整台機器的資料的地方是 ProgramData 和 HKLM,就算要共用,也需要設計存取權限。
flowchart TB
accTitle: 同一台 PC 裡的兩個世界
accDescr: 同一台 PC 只共用 HKLM 與 ProgramData,你的 SID 的世界和另一個 SID 的世界各自擁有獨立的 AppData、登錄檔 hive 與 DPAPI 金鑰,隔著使用者邊界互相看不見
pc["同一台 PC"] --> shared["共用:HKLM・ProgramData"]
pc --> wa["你的 SID 的世界"]
pc --> wb["另一個 SID 的世界(SYSTEM 等)"]
wa --> ra["AppData・HKCU・DPAPI 金鑰"]
wb --> rb["另一套 AppData・另一個 hive・另一組金鑰"]
ra -.-|"使用者邊界:互相看不見"| rb
圖 1: 即使是同一台 PC,執行使用者不同,AppData、登錄檔和金鑰就是另外一套。
不過,SID 相同也未必就是同一個環境。工作的登入類型、IIS 是否載入設定檔、UAC 提升權限帶來的登入工作階段差異,也都要確認。同一帳戶提升權限並不會讓 HKCU 和保存庫變成別人的,把會變的邊界分開來考量才是關鍵。
以下的順序是:作為前提的執行使用者(第 2 章)、五道邊界(第 3~7 章)、依執行形態的檢查(第 8 章)、設計與調查(第 9 章)。
圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 33 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle
2. 前提:「以誰的身分執行」決定一切
2.1 要確認的不是使用者名稱,而是 SID 與權杖
Windows 用來識別使用者的 ID 不是使用者名稱,而是 SID(安全性識別碼)。處理程序持有存取權杖,其中包含執行使用者的 SID。在考量檔案的 ACL 判定、登錄檔的實體、加密金鑰時,這個執行主體都是出發點。
第一步用 whoami /user 確認。要看的不是你自己終端機裡的結果,而是出問題的那個執行環境中的結果。
> whoami /user
使用者資訊
----------------
使用者名稱 SID
=============== =============================================
desktop\you S-1-5-21-3623811015-3361044348-30300820-1001
C:\Users\you 就是與那個 SID 繫結的使用者設定檔的實體。設定檔裡有 AppData 等一組資料夾,還有使用者登錄檔 hive NTUSER.DAT。它是在首次登入時從預設設定檔複製出來的,所以別人的設定檔,是從與你調整好的環境不同的初始狀態開始的。1
flowchart TB
accTitle: 從啟動路徑到設定檔的流程
accDescr: 不論經由按兩下、工作排程器還是服務或 IIS 哪條路徑啟動,處理程序都帶著存取權杖裡的 SID,與該 SID 繫結的那整套使用者設定檔就是它的執行環境
e1["按兩下"] --> tok["處理程序的權杖(SID)"]
e2["工作排程器"] --> tok
e3["服務・IIS"] --> tok
tok --> prof["與 SID 繫結的整套設定檔"]
prof -.-> note["SID 不同就是處於初始狀態的另一套設定檔"]
圖 2: 由哪條路徑啟動,決定了背著哪個 SID 的設定檔執行。
2.2 服務用的帳戶,也各有各的環境
Windows 裡有一些不需要人登入也能執行的內建帳戶。它們各自擁有獨立的執行環境。2
| 帳戶 | SID | 設定檔與登錄檔的參照位置 |
|---|---|---|
| SYSTEM(LocalSystem) | S-1-5-18 |
設定檔在 C:\Windows\System32\config\systemprofile 下。HKCU 關聯到預設使用者3 |
| LocalService | S-1-5-19 |
在 C:\Windows\ServiceProfiles\LocalService 下。在 HKEY_USERS 裡有自己的子機碼4 |
| NetworkService | S-1-5-20 |
在 C:\Windows\ServiceProfiles\NetworkService 下。和 LocalService 一樣,有自己的設定檔和 hive4 |
IIS AppPool\<名稱> |
S-1-5-82-… |
應用程式集區專用的識別。預設不載入設定檔5 |
按兩下、工作排程器、服務、IIS 這些啟動路徑的差別,最後就是使用哪個執行主體的環境的差別。不要以為你在自己的互動式登入裡備好的設定、金鑰和認證資訊,在那個執行目標裡也一樣有。
2.3 確認完「同一個使用者」之後還要看的三個條件
| 要確認的條件 | 差異顯現的例子 |
|---|---|
| 登入類型 | 工作的 S4U 登入不儲存密碼,存取不了網路和 EFS6 |
| 設定檔的載入 | 在 IIS 中,loadUserProfile 決定了能否使用 AppData 和使用者 hive7 |
| 登入工作階段與提升權限 | 即使 SID 相同,提升權限後的處理程序也可能看不到網路磁碟機89 |
不要確認完 SID 就收手,用這三點來梳理「同一個帳戶裡到底哪裡不一樣」。工作在第 8 章、IIS 在第 4~5 章、提升權限在第 7 章詳述。
3. 邊界 1:AppData ── 同一個環境變數指向別的地方
典型的症狀是,一改成排程工作就找不到組態檔了。並不是路徑解析失敗,而是解析到了另一個使用者的正確路徑,而那裡沒有那個檔案。
3.1 AppData 的分區,以及執行使用者帶來的差別
AppData 是使用者設定檔下的資料夾。用途分為下面幾類。10
| 區域 | 主要用途 |
|---|---|
%APPDATA%(Roaming) |
希望跟著設定檔一起漫遊的使用者設定 |
%LOCALAPPDATA%(Local) |
機器本機的資料和快取 |
| AppData\LocalLow | 供低完整性等級的處理程序使用的資料 |
即使用同樣的程式碼開啟 %APPDATA%,參照位置也會隨執行使用者而變。
| 執行使用者 | 尋找組態檔的位置範例 |
|---|---|
| 使用者 A | C:\Users\a\AppData\Roaming\MyApp |
| SYSTEM | systemprofile 下的 AppData |
使用者 A 儲存的檔案,在 SYSTEM 那一側並不存在。光看「找不到組態檔」這條錯誤很難看明白,所以調查時要看的不是環境變數名稱,而是實際開啟的路徑。
flowchart TB
accTitle: %APPDATA% 的解析結果隨使用者而變
accDescr: 同樣的程式碼開啟 %APPDATA%,執行使用者是你時解析到 C:\Users 下,是 SYSTEM 時解析到 systemprofile 下,而後者並沒有你的組態檔
code["同樣的程式碼:開啟 %APPDATA%"] --> q{"執行使用者是誰?"}
q -->|"你"| a["C:\Users\you\AppData\Roaming"]
q -->|"SYSTEM"| b["systemprofile 下的 AppData"]
b -.-> miss["以為放好了的檔案並不在"]
圖 3: 環境變數不會說謊,但解析到哪裡由權杖決定。
3.2 PATH 也有依使用者區分的部分
系統環境變數是整台機器共用的,使用者環境變數則按使用者各自獨立。自己加進 PATH 的命令服務找不到時,也要確認這個差別。
儲存位置由「這份資料給誰讀」來決定。只屬於那個使用者的設定放進 AppData,全體使用者和服務共用的資料放到 %ProgramData% 下,後者要設計 ACL。詳細的選法請參閱「Windows應用程式資料儲存位置怎麼選 ── SQLite / JSON / 登錄檔 / Access 判斷表」。
flowchart TB
accTitle: 儲存位置由讀者決定
accDescr: 只有那個使用者會讀的資料放進 AppData 或 HKCU,要和全體使用者及服務共用的資料放進 ProgramData 或 HKLM,後者伴隨 ACL 的設計
q{"這份資料由誰來讀"} -->|"只有那個使用者"| f1["AppData・HKCU"]
q -->|"全體使用者・服務"| f2["ProgramData・HKLM"]
f1 -.-> w1["將來要改成服務的話得重新考量"]
f2 -.-> w2["設計寫入權限和 ACL"]
圖 4: 「一改成服務就讀不到了」,是設計時跳過了這個分支的結果。
4. 邊界 2:HKCU ──「目前的使用者」隨呼叫端而變
典型的症狀是,安裝程式寫入的授權資訊,服務卻找不到。寫入的地方和讀取的地方即使都叫 HKCU,也未必是同一個實體。
4.1 HKCU 是通往使用者 hive 的別名
HKEY_CURRENT_USER(HKCU)不是獨立的 hive,而是會按呼叫端使用者轉接到實體的別名。在一般的使用者處理程序中,它指向 HKEY_USERS 下那個 SID 的機碼。其內容就是登入時載入的 NTUSER.DAT。1
例外是 HKCU\Software\Classes,它的實體是另一個 hive 檔案 UsrClass.dat。這個檔案放在 %LOCALAPPDATA%\Microsoft\Windows 下。11
LocalSystem 的 HKCU 關聯到預設使用者(HKEY_USERS\.DEFAULT)。以系統管理員帳戶執行的安裝程式往 HKCU 寫,而 SYSTEM 的服務從 HKCU 讀,參照位置就對不上了。3
flowchart TB
accTitle: HKCU 這個別名的實體
accDescr: 應用程式開啟 HKCU 時,在你的處理程序中會轉接到 HKEY_USERS 下你的 SID 機碼,在 LocalSystem 的處理程序中則轉接到預設使用者的機碼,於是安裝程式寫下的值就看不見了
app["應用程式的程式碼:開啟 HKCU"] --> alias["HKCU 是通往實體的別名"]
alias -->|"你的處理程序"| ha["HKEY_USERS 下你的 SID"]
alias -->|"SYSTEM 的處理程序"| hd["HKEY_USERS\.DEFAULT"]
hd -.-> gone["以為寫進去的值並不存在"]
圖 5: 就算用 HKCU 這同一個名字,使用者一變,讀寫的實體也跟著變。
整台機器範圍的設定放進 HKLM。Microsoft 不建議從服務存取 HKCU。需要讀取使用者的設定時,要先模擬(impersonation)那個使用者,然後使用 RegOpenCurrentUser。12
4.2 還要確認設定檔有沒有被載入
除了執行帳戶之外,設定檔是否已載入有時也會成為問題。
IIS 的應用程式集區預設是不載入使用者設定檔就執行的。啟用 loadUserProfile 之後,才能使用設定檔下的 AppData 和 hive。7 這個設定也關係到下一章 DPAPI 和 ASP.NET Core Data Protection 的金鑰儲存位置。
工作的「不儲存密碼」設定和這個是兩回事,要當作登入類型的限制來確認。即使指定了同一個使用者,S4U 登入也用不了網路和 EFS。具體的設定差別整理在第 8 章。6
5. 邊界 3:DPAPI ── 加密是用「使用者的金鑰」上的鎖
AppData 和 HKCU 是「找的地方不對」的問題,而 DPAPI 帶來的問題是檔案讀得到,內容卻解不開。把用到已儲存密碼的應用程式改成服務後出現 CryptographicException,就是這種例子。
5.1 主要金鑰的主人不同就解不開
DPAPI(Data Protection API)是 Windows 提供給應用程式的加密功能。只要呼叫 CryptProtectData 或 .NET 的 ProtectedData.Protect,應用程式自己不用在程式碼裡帶著加密金鑰也能保護資料。13
不過,這並不表示金鑰就不需要了。它用的是由作業系統管理的主要金鑰。在 CurrentUser 範圍下,隨機產生的每個使用者的主要金鑰,會用從登入認證資訊衍生出的金鑰加以保護,並儲存在設定檔下。14
flowchart TB
accTitle: DPAPI 的金鑰鏈
accDescr: 隨機產生的使用者主要金鑰由從登入認證資訊衍生出的金鑰保護,而這把主要金鑰加密著應用程式的機密資訊,用別人的主要金鑰解不開同一段密文
pwd["登入認證資訊"] -->|"用衍生金鑰保護"| mk["你的主要金鑰"]
mk -->|"加密"| sec["應用程式的機密資訊(儲存的密碼等)"]
mk2["別的使用者的主要金鑰"] -.->|"解不開"| sec
mk -.-> loc["儲存位置在設定檔下"]
圖 6: 不是用認證資訊直接加密資料,而是用源自認證資訊的金鑰保護主要金鑰。
下面是一個最小範例:使用者 A 加密資料後放到共用位置,再換個使用者嘗試解密。就算共用了儲存位置,CurrentUser 的解密者也不會變。
# 在使用者 A 的工作階段中: 用 CurrentUser 範圍加密並儲存到共用位置
Add-Type -AssemblyName System.Security
$bytes = [Text.Encoding]::UTF8.GetBytes("secret")
$enc = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, "CurrentUser")
[Convert]::ToBase64String($enc) | Set-Content C:\ProgramData\demo.bin
# 換個使用者(例如用 PsExec 變成 SYSTEM)嘗試解密
$enc = [Convert]::FromBase64String((Get-Content C:\ProgramData\demo.bin))
[Security.Cryptography.ProtectedData]::Unprotect($enc, $null, "CurrentUser")
# → CryptographicException: 資料在目前的狀態下無效
5.2 範圍要從「應該由誰解密」來選
| 範圍 | 解密的單位 | 設計上的注意事項 |
|---|---|---|
CurrentUser |
加密者本人的主要金鑰 | 換到別的執行帳戶就解不開 |
LocalMachine |
同一台機器共用的金鑰 | 同一台機器上的任意處理程序都能解密,所以要用檔案 ACL 收緊能讀到密文的對象 |
服務和互動使用者都要讀的機密資訊,一開始就該把 LocalMachine 和檔案 ACL 組合起來設計,或者用加密者本人的帳戶執行。不要只為了消掉解密錯誤而改範圍,而要決定允許誰來解密。在共用終端上,還要注意按機器保護的範圍過寬所帶來的風險。15
flowchart TB
accTitle: DPAPI 範圍的選法
accDescr: 應該能解密的主體只有那個使用者就選 CurrentUser 範圍,是同一台 PC 上的多個執行主體就選 LocalMachine 範圍,後者用檔案 ACL 收緊讀者
q{"應該由誰來解密"} -->|"只有那個使用者"| cu["CurrentUser 範圍"]
q -->|"同一台 PC 的多個執行主體"| lm["LocalMachine 範圍"]
cu -.-> r1["換個使用者執行就解密失敗"]
lm -.-> r2["讀者用檔案 ACL 收緊"]
圖 7: 範圍不是挑「碰巧能跑的那個」,而是當作解密者的設計來選。
5.3 密碼的「變更」和「重設」不是一回事
保護主要金鑰的那把金鑰依賴登入認證資訊。使用者自己變更密碼時,主要金鑰會被源自新密碼的金鑰重新保護,解密能力得以延續。
另一方面,系統管理員重設本機帳戶的密碼時,可能導致過去的密文解不開。就算帳戶相同,也需要確認認證資訊是以哪種方式變更的。14
5.4 用 ASP.NET Core 時也要確認金鑰的儲存位置
即使沒有直接呼叫 DPAPI,也可能依賴這道邊界。ASP.NET Core Data Protection 會把用於保護 Cookie 驗證等的金鑰環,儲存到與環境相應的位置。16
| 環境 | 金鑰的存放位置與影響 |
|---|---|
| 使用者設定檔可用 | %LOCALAPPDATA%\ASP.NET\DataProtection-Keys。在 Windows 上用 DPAPI 加密 |
| 設定檔不可用,且裝載於 IIS 上 | 退回到為背景工作處理程序帳戶設定了 ACL 的 HKLM 登錄檔下 |
| 兩者都不符合 | 變成僅限處理程序內的暫時金鑰。一重新啟動就遺失金鑰,驗證 Cookie 等受保護的資料隨之失效 |
要把 loadUserProfile、setProfileEnvironment 和裝載形態成套確認,弄清金鑰會被放到哪裡的組態。重要的不只是「能啟動」,還有重新啟動之後能否繼續用同一把金鑰。
範圍的選擇以及與 Credential Manager 的分工,在「Windows 應用程式的機密資訊保存 - 用 DPAPI 避免明文設定」中有詳細討論。
6. 邊界 4:瀏覽器設定檔 ──「已登入」是那個使用者的私有物
手邊明明已經登入,在 CI 上啟動瀏覽器卻回到了登入畫面。把設定檔的儲存位置和加密金鑰分開看,這個問題就理清楚了。
6.1 儲存位置和金鑰,是兩道邊界
Chrome、Edge 等 Chromium 系瀏覽器的設定檔,預設位於 %LOCALAPPDATA% 下的 User Data 資料夾。瀏覽記錄、Cookie、擴充功能、儲存的密碼等,都屬於那個 Windows 使用者的環境。17
Cookie 和儲存的密碼由設定檔內的加密金鑰加密,而這把金鑰本身受 DPAPI 保護。因此就算把資料夾複製到別的使用者或別的機器,金鑰也對不上,解不開。
近年的 Chrome 還疊上了 App-Bound Encryption(應用程式繫結加密)。它把金鑰的解密改為經由 SYSTEM 權限的服務進行,不只驗證使用者,還驗證發出要求的應用程式的識別。18 這說的是 Chromium 系;擁有自家一套設定檔保護機制的 Firefox 等則另當別論。
| 邊界 | 在自動化那一側會發生什麼 |
|---|---|
| AppData 的邊界 | CI 代理程式或服務所用的使用者,沒有你平時用的那套設定檔 |
| DPAPI 的邊界 | 就算複製了資料夾,也解不開受保護的金鑰 |
flowchart TB
accTitle: 支撐瀏覽器登入狀態的兩道邊界
accDescr: 瀏覽器設定檔位於 LOCALAPPDATA 下屬於邊界 1,Cookie 的加密金鑰受 DPAPI 保護屬於邊界 3,所以設定檔和金鑰都不會被另一個使用者繼承
prof["瀏覽器設定檔"] --> loc["存放位置在 LOCALAPPDATA 下"]
prof --> key["Cookie 加密金鑰受 DPAPI 保護"]
loc -.-> ci["CI 的執行使用者拿到的是空的另一套設定檔"]
key -.-> copy["複製資料夾帶不走"]
圖 8: 「把已登入狀態帶過去」會同時被邊界 1 和邊界 3 擋住。
6.2 自動化中要明確寫出登入狀態是怎麼造出來的
Selenium 和 Playwright 預設會新建一個用完即丟的暫時設定檔來啟動。因此,即使是同一個使用者,平時瀏覽器的登入狀態也不會被自動拿來用。
就算指定了持續保留的設定檔目錄,一旦要挪到別的使用者的 CI 或服務上,上一節的儲存位置和金鑰問題依然在。對策不是複製已登入的設定檔,而是下面兩者之一。
- 把測試用帳戶的登入步驟寫成程式碼。
- 用自動化工具的 storage state 機制,明確地儲存和還原 Cookie 等。
別的使用者沒辦法只靠複製就用上 Cookie,這不是不方便,而是安全上的一道邊界。自動化的設計要以這道邊界的存在為前提。
7. 邊界 5:認證資訊與憑證 ── 保存庫按使用者各自獨立
用 cmdkey 儲存的認證資訊,工作執行時沒被用上,於是驗證出錯。這裡同樣不是看「儲存在這台 PC 上了」,而是要確認「是以哪個使用者儲存的」。
7.1 認證管理員是按執行使用者區分的保存庫
用 cmdkey /list 能查看的認證管理員,是按使用者區分的保存庫。19 儲存的認證資訊雖然在磁碟上,但受 DPAPI 保護,由以那個使用者身分執行的程式使用。20
保存庫裡會放著檔案伺服器和網路磁碟機的已儲存認證資訊、git-credential-manager 儲存的 Git 權杖、RDP 連線的已儲存密碼,以及使用 Credential API 的應用程式的機密資訊等。
即使在你互動式登入的保存庫裡有,服務或工作的執行帳戶的保存庫裡也沒有。需要的認證資訊,要準備一套以執行帳戶自身的內容寫入的安裝步驟。只有手邊的 git pull 能成功時,同樣要確認用的是哪個保存庫。
不過,S4U 設定的工作,光往裡塞認證資訊是解決不了的。要先確認登入類型,再考慮改成儲存密碼的設定或切換到服務帳戶。步驟的先後順序在第 8 章整理。
flowchart TB
accTitle: 認證資訊的保存庫按使用者各自獨立
accDescr: 你的保存庫裡放著 git 的認證資訊、檔案伺服器的已儲存認證資訊和 RDP 的密碼,而服務執行使用者的保存庫在沒有寫入認證資訊之前是空的,這正是驗證出錯的真相
you["你的保存庫"] --> g["git 的認證資訊"]
you --> n["檔案伺服器的已儲存認證資訊"]
you --> r["RDP 的已儲存密碼"]
svc["服務執行使用者的保存庫"] -.-> empty["沒寫入就是空的=驗證出錯的真相"]
圖 9: 「手邊驗證過得去」只是「你的保存庫用得上」的簡寫。
7.2 憑證要把存放區和私密金鑰的權限分開確認
| 存放區 | 邊界與用途 |
|---|---|
Cert:\CurrentUser |
按使用者區分的存放區。放進這裡的用戶端憑證,其私密金鑰以使用者為單位受保護 |
Cert:\LocalMachine |
整台機器範圍的存放區。放服務用的憑證,並用私密金鑰的 ACL 允許服務帳戶讀取 |
服務要用的憑證,基本上要把 LocalMachine 存放區和私密金鑰的 ACL 成套設計。21 使用者存放區的實體在 HKCU\Software\Microsoft\SystemCertificates 下,所以它位於 HKCU 這道邊界的裡側。22
詳情請參閱「Windows 憑證存放區實務指南 ── 該放進使用者,還是電腦?」。
7.3 UAC 提升權限要把「同一帳戶」和「不同帳戶」分開看
在啟用了 UAC 的系統管理員使用者登入時,會建立兩個相互連結的權杖:權限受限的標準權杖,和完整的系統管理員權杖。8
網路磁碟機的對應是按登入工作階段儲存的。檔案總管裡看得到 Z:,以系統管理員身分執行的工具裡卻可能看不到。9
| 執行方式 | 會變的和不會變的 |
|---|---|
| 保持同一帳戶做 UAC 提升權限 | SID 相同,HKCU 和認證保存庫也照舊。按登入工作階段儲存的磁碟機對應可能就看不見了 |
| 用別的系統管理員帳戶的認證資訊提升權限/RunAs | SID 也變了。HKCU 和保存庫都變成那個系統管理員的。AppData、金鑰、瀏覽器狀態也都受另一個使用者邊界的影響 |
不要把「一提升權限就跑不動了」全都歸結成使用者變了,先確認是不是同一個帳戶。
flowchart TB
accTitle: UAC 提升權限造成的同一使用者內部的分裂
accDescr: 啟用 UAC 的系統管理員登入會造出標準權杖和提升權杖兩個權杖,在標準權杖那一側對應的網路磁碟機,從提升權杖的處理程序裡看不到
logon["系統管理員使用者的登入"] --> t1["標準權杖"]
logon --> t2["提升權杖"]
t1 --> d1["在這裡對應的 Z: 磁碟機"]
t2 -.->|"另一個登入工作階段"| d2["提升權限後的工具看不到 Z:"]
圖 10: 提升權限造出的是「同一個使用者的另一個世界」。邊界不只是 SID 的事。
8. 依執行形態的檢查清單
8.1 工作要先看執行帳戶,再看登入類型
工作排程器即使指定了同一個使用者,能用什麼也會隨登入類型而變。6
| 登入類型 | 要確認的事 |
|---|---|
| 互動式權杖(InteractiveToken) | 在已登入的工作階段中執行的設定 |
| 儲存密碼(Password) | 非互動也能使用認證資訊的設定 |
| S4U(不儲存密碼) | 不儲存密碼,存取不了網路資源和加密檔案(EFS) |
用不了已儲存認證資訊的工作,要先確認是不是 S4U 的限制。不要把「保持 S4U 再往保存庫裡加認證資訊」當成對策。先考慮儲存密碼的設定或切換到服務帳戶,在此之上如有必要,再寫入執行帳戶的保存庫。
設定的細節在「工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計」中討論。
flowchart TB
accTitle: 工作的登入類型這第二根軸
accDescr: 工作排程器的執行設定帶有互動式權杖、儲存密碼、S4U 這幾種登入類型,S4U 下不儲存密碼,存取不了網路和 EFS
task["工作的執行設定"] --> lt{"登入類型"}
lt -->|"互動式權杖"| it["在已登入的工作階段中執行"]
lt -->|"儲存密碼"| pw["非互動也能使用認證資訊"]
lt -->|"S4U(不儲存密碼)"| s4u["構不著網路和 EFS"]
圖 11: 就算是同一個執行使用者,登入的方式不同,能用的東西也不同。
8.2 更換執行目標之前的檢查表
| 執行形態 | 執行主體 | 特別要檢查的邊界與症狀 |
|---|---|---|
| 工作排程器 | 註冊時指定的帳戶 | 邊界 1・2・3・5。確認 AppData 和 HKCU 的參照位置、DPAPI 的解密、保存庫。S4U 下用不了網路和 EFS |
| Windows 服務 | SYSTEM、LocalService、NetworkService、服務帳戶 | 邊界 1~5。SYSTEM 用的是 systemprofile 和預設使用者的 HKCU,LocalService/NetworkService 用的是 ServiceProfiles 下各自的環境。開發者的金鑰、保存庫、瀏覽器狀態都不會被繼承 |
| IIS 應用程式集區 | IIS AppPool\<名稱> 等應用程式集區專有的識別 |
邊界 1・2・3・5。確認預設未載入設定檔、Data Protection 的金鑰存放處、對 CurrentUser 憑證私密金鑰的存取 |
| RunAs/UAC 提升權限 | 指定的使用者,或同一使用者的另一個權杖 | 同一帳戶的話 SID、HKCU、保存庫都相同,磁碟機對應則受工作階段差異影響。不同帳戶的話邊界 1~5 全都要檢查 |
| CI/CD 代理程式 | 代理程式的服務使用者。很多情況下從未互動式登入過 | 邊界 1~5。確認有沒有依賴瀏覽器的設定檔和登入狀態、Git 的認證資訊、開發者的 HKCU 設定或 DPAPI 保護的資料 |
| RDP/共用伺服器 | 同一使用者的多重工作階段,或多個使用者 | 同一使用者的多重工作階段共用 AppData 和 HKCU,所以要注意寫入衝突。不同使用者的話在邊界 1~5 上分開 |
最後一行是反方向的提醒。同一個使用者就算有多個 RDP 工作階段,AppData 和 HKCU 也不會按工作階段各自獨立。這裡的問題不是「看不見」,而是「共用同一份東西並往裡寫」。
9. 設計與疑難排解的指引
9.1 儲存位置和安裝步驟,由使用它的主體來決定
| 對象 | 設計的基本原則 |
|---|---|
| 使用者專屬的設定 | 放進 AppData、HKCU |
| 全體使用者和服務共用的資料與設定 | 放進 ProgramData、HKLM,並設計寫入權限和 ACL |
| 機密資訊 | DPAPI 的範圍從「應該由誰解密」來選。用 LocalMachine 時連檔案 ACL 一起設計 |
| 服務用的認證資訊與憑證 | 把認證資訊寫入執行帳戶的保存庫、把憑證放進 LocalMachine 憑證存放區並設定私密金鑰的 ACL,都要寫進安裝步驟 |
如果將來打算改成服務,那麼在決定儲存位置的階段,就要把那個執行主體也算進讀者裡。「湊巧開發者的環境裡有就拿來用」這種依賴,原則上不要帶進維運。
9.2 調查從執行主體出發,走向實際的參照位置
flowchart TB
accTitle: 使用者邊界問題的調查步驟
accDescr: 用 whoami 確認執行使用者,用 Process Explorer 看權杖,用 Process Monitor 找出實際讀取的路徑和登錄機碼,必要時用 psexec 從對方的世界裡重現
s1["用 whoami /all 確認執行使用者"] --> s2["用 Process Explorer 確認權杖"]
s2 --> s3["用 ProcMon 找出實際的路徑與機碼"]
s3 --> s4["用 psexec 從對方的世界重現"]
s3 -.-> hint["與預期不符的設定檔路徑就是線索"]
圖 12: 先確認執行使用者,看清實際的路徑和登錄機碼,再在目標執行主體上重現。
| 步驟 | 確認方法 | 要看的點 |
|---|---|---|
| 1. 確認執行使用者 | 啟動後立刻把 whoami /all 輸出到記錄 |
出問題的那個工作或服務的執行主體,是否和手邊相同 |
| 2. 確認權杖 | Process Explorer | 處理程序的使用者和工作階段。是另一個使用者,還是同一使用者的另一個執行內容 |
| 3. 看實際的參照位置 | Process Monitor | 開啟的檔案路徑和登錄機碼。PATH NOT FOUND 裡有沒有出現和預想不同的設定檔 |
| 4. 在目標執行主體上重現 | 是 SYSTEM 的話用 psexec -s -i cmd |
在 SYSTEM 的 shell 裡做同樣的操作,確認與自己的互動環境有什麼差別 |
設定找不到時回到 AppData 和 HKCU,檔案讀得到卻解不開時回到 DPAPI,只有驗證失敗時回到保存庫、憑證和登入類型。不要只憑錯誤訊息下判斷,把「誰、在哪裡、用哪把金鑰或哪份認證資訊」對齊才是捷徑。
10. 總結
「在我這邊能跑」的意思是「以那個使用者、那套設定檔、那些金鑰和那個保存庫能跑」。即使在同一台 PC 上啟動同一個 .exe,改成排程工作、服務或 CI 作業也必須當作執行環境的移轉來對待。
AppData 和使用者環境變數參照的是執行使用者的設定檔,HKCU 也朝著那個使用者的 hive。DPAPI 的 CurrentUser 範圍依賴使用者的主要金鑰,瀏覽器的登入狀態和認證管理員同樣受這道邊界的影響。
再往下,即使 SID 相同,登入類型、設定檔載入、UAC 提升權限帶來的工作階段差異依然存在。反過來,同一使用者的多個工作階段共用 AppData 和 HKCU 時,要考量寫入衝突。
設計審查時,請確認下面這個問題。
這段程式碼,不論以哪個使用者身分執行都是正確的嗎?
把需要的儲存位置、金鑰、認證資訊,明確地為實際的執行主體準備好。只要在移轉前確認這條原則,就能在設計階段減少「程式碼一行沒改卻壞了」這類事故。
相關文章
- Windows 應用程式的機密資訊保存 - 用 DPAPI 避免明文設定
- Windows應用程式資料儲存位置怎麼選 ── SQLite / JSON / 登錄檔 / Access 判斷表
- 工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計
- Windows 服務帳戶選定 ── LocalSystem、虛擬帳戶與 gMSA
- Windows 憑證存放區實務指南 ── 該放進使用者,還是電腦?
相關的諮詢領域
小村軟體有限公司承接業務應用程式改成服務、排程工作時的執行環境設計,「在我這邊明明能跑」這類問題的調查,以及 Windows 應用程式機密資訊管理的設計。
參考連結
-
Microsoft Learn, About User Profiles. 關於使用者設定檔在首次登入時建立,以及設定檔由登錄檔 hive NTUSER.DAT(登入時載入並對應到 HKEY_CURRENT_USER)和檔案系統上的一組設定檔資料夾構成等內容。 ↩ ↩2
-
Microsoft Learn, Local accounts. 關於 SYSTEM(S-1-5-18)、NETWORK SERVICE(S-1-5-20)、LOCAL SERVICE(S-1-5-19)是用於執行作業系統和服務的預設本機系統帳戶等內容。 ↩
-
Microsoft Learn, LocalSystem Account. 關於 LocalSystem 的權杖包含 NT AUTHORITY\SYSTEM、它不與任何已登入的使用者帳戶關聯、因而 HKEY_CURRENT_USER 關聯到預設使用者,以及要存取其他使用者的設定檔必須模擬那個使用者等內容。 ↩ ↩2
-
Microsoft Learn, LocalService Account. 關於 LocalService 帳戶在 HKEY_USERS 下擁有自己的子機碼,以及 HKEY_CURRENT_USER 關聯到 LocalService 帳戶等內容。NetworkService 同理(NetworkService Account)。 ↩ ↩2
-
Microsoft Learn, Application Pool Identities. 關於應用程式集區以集區專有的識別執行、IIS 預設不載入 Windows 使用者設定檔,以及把 LoadUserProfile 屬性設為 true 就能載入設定檔等內容。 ↩
-
Microsoft Learn, logonType Simple Type. 關於工作的登入類型有 S4U、Password、InteractiveToken,以及 S4U 登入不儲存密碼,網路和加密檔案都存取不了等內容。 ↩ ↩2 ↩3
-
Microsoft Learn, Process Model Settings for an Application Pool. 關於應用程式集區的 processModel 有 loadUserProfile 屬性和 setProfileEnvironment 屬性,用於控制背景工作處理程序是否載入使用者設定檔等內容。 ↩ ↩2
-
Microsoft Learn, How User Account Control works. 關於啟用 UAC 時,系統管理員使用者登入會建立標準使用者權杖和完整系統管理員存取權杖這兩個相互連結的權杖等內容。 ↩ ↩2
-
Microsoft Learn, Mapped drives are not available from an elevated prompt. 關於以標準權杖登入的工作階段中對應的網路磁碟機無法從提升權限後的處理程序使用,以及其背景是兩個相互連結的登入工作階段各自持有磁碟機對應等內容。 ↩ ↩2
-
Microsoft Learn, KNOWNFOLDERID. 關於 FOLDERID_RoamingAppData(%APPDATA%)、FOLDERID_LocalAppData(%LOCALAPPDATA%)、FOLDERID_LocalAppDataLow 被定義為按使用者區分的已知資料夾等內容。 ↩
-
Microsoft Learn, Error occurs during desktop setup and desktop location is unavailable. 關於使用者設定檔擁有 NTUSER.DAT 和 UsrClass.dat 兩個 hive 檔案,以及 UsrClass.dat 放在 AppData\Local\Microsoft\Windows 下等內容。 ↩
-
Microsoft Learn, Services and the Registry. 關於服務不應存取 HKEY_CURRENT_USER 或 HKEY_CLASSES_ROOT,以及模擬使用者時應使用 RegOpenCurrentUser 函式等內容。 ↩
-
Microsoft Learn, CryptProtectData function. 關於 CryptProtectData 通常用與已登入使用者關聯的工作階段金鑰保護資料、以同一使用者解密為前提,以及可用 CRYPTPROTECT_LOCAL_MACHINE 旗標切換成按機器保護等內容。 ↩
-
Microsoft Learn, Windows Data Protection. 關於 DPAPI 用從使用者密碼衍生的金鑰加密隨機產生的主要金鑰加以保護、主要金鑰儲存在使用者設定檔下,以及變更密碼時主要金鑰會被重新保護等內容。 ↩ ↩2
-
Microsoft Learn, ProtectedData Class. 關於 DataProtectionScope.CurrentUser 只允許保護資料的那個使用者解密,而 LocalMachine 允許同一台機器上的任意處理程序解密等內容。 ↩
-
Microsoft Learn, Data Protection key management and lifetime in ASP.NET Core. 關於使用者設定檔可用時金鑰儲存在 %LOCALAPPDATA%\ASP.NET\DataProtection-Keys 並在 Windows 上用 DPAPI 加密、以 IIS 裝載而設定檔不可用時退回到為背景工作處理程序帳戶設定了 ACL 的 HKLM 登錄檔、哪個條件都不滿足時金鑰隨處理程序結束而遺失導致受保護的內容無法解密,以及 setProfileEnvironment 屬性相關等內容。 ↩
-
Chromium project, User Data Directory. 關於 Windows 上 Chrome 的 User Data 目錄預設是 %LOCALAPPDATA%\Google\Chrome\User Data,以及設定檔(瀏覽記錄、書籤、Cookie 等)放在它下面等內容。 ↩
-
Google Security Blog, Improving the security of Chrome cookies on Windows. 關於 Chrome 在 Windows 上一直使用 DPAPI 加密 Cookie 等資料,以及藉助 App-Bound Encryption 改為經由 SYSTEM 權限的服務保護金鑰並驗證要求解密的應用程式的識別等內容。 ↩
-
Microsoft Learn, cmdkey. 關於用 cmdkey 命令可以列出、建立和刪除已儲存的使用者名稱和密碼(認證資訊)等內容。 ↩
-
Microsoft Learn, Cached and Stored Credentials Technical Overview. 關於儲存在認證管理員中的認證資訊放在磁碟上並受 DPAPI 保護,以及以那個使用者身分執行的程式可以存取這個存放區中的認證資訊等內容。 ↩
-
Microsoft Learn, Local Machine and Current User Certificate Stores. 關於憑證存放區有本機電腦存放區(整台機器範圍)和目前使用者存放區(按使用者區分)兩種等內容。 ↩
-
Microsoft Learn, System Store Locations. 關於 CERT_SYSTEM_STORE_CURRENT_USER 的系統存放區放在登錄檔的 HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates 下等內容。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
PowerShell 中安全處理認證資訊 ── 把明文密碼逐出腳本
本文整理將 PowerShell 腳本中的明文密碼遷移至安全保存方式的做法,說明 SecureString 的實際樣貌與限制、Export-Clixml 透過 DPAPI 保存的機制,以及 SecretManagement/SecretStore 的適用場合。
登錄檔的 32bit/64bit 重新導向與虛擬化的陷阱 ── Wow6432Node 與「明明寫入了卻讀不到值」的問題
整理 32bit 應用程式寫入 HKLM\Software 會被重新導向到 Wow6432Node 的機制、UAC 虛擬化在哪些條件下會轉送到 VirtualStore、COM 註冊依 bitness 分離所造成的實際損害,以及透過 RegistryView 或 reg.e...
Power Automate 與 PowerShell + 工作排程器的分工 ── 不混用自動化工具,各就各位地串接
本文整理 PowerShell + 工作排程器的夜間批次作業與 Power Automate 流程開始混雜在公司內部的中小企業資訊部門所需的判斷依據:兩者擅長領域的差異、該用哪一種來建置的判斷表、透過 SharePoint 進行鬆散耦合串接的協作模式,以及授權與維運上的注意事項。
PowerShell 的錯誤處理與重新執行設計 ── 從 try/catch 失效的陷阱到 exit code、重試的實務定石
本文從實務角度整理 PowerShell 終止錯誤與非終止錯誤的差異、try/catch 失效的陷阱與 -ErrorAction Stop 的實務定石、以 $LASTEXITCODE 判定成敗、exit code 設計,直到指數退避的重試機制。
Windows應用程式資料儲存位置怎麼選 ── SQLite / JSON / 登錄檔 / Access 判斷表
Windows桌面應用程式的資料該存在哪裡、用什麼格式儲存?本文整理AppData/ProgramData的使用區分,以及SQLite、JSON檔案、登錄檔、Access(.accdb)各自的優勢與陷阱,並附判斷表,從實務角度說明防止資料損毀與位元數問題等注意事項。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
常見問題
整理諮詢這個主題時常見的問題。
- 從檔案總管啟動能跑的應用程式,從工作排程器啟動卻找不到組態檔。這是為什麼?
- 因為 %APPDATA% 之類的環境變數會解析到「正在執行的那個使用者」的設定檔。把工作改成以 SYSTEM 或別的帳戶執行時,環境變數就指向另一個設定檔(SYSTEM 的話是 systemprofile 下),而你儲存的組態檔並不在那裡。請確認工作的執行帳戶,該共用的資料放到 ProgramData 下才是長久的做法。
- 用 ProtectedData.Protect 儲存的密碼,改成服務執行之後就解不開了。
- CurrentUser 範圍的 DPAPI 依賴加密者本人的主要金鑰。服務的執行帳戶不同,主要金鑰也就是另一把,解密會以 CryptographicException 失敗。服務和互動使用者都要讀的機密資訊,請改用 LocalMachine 範圍加上檔案 ACL 重新設計,或者用加密者本人的帳戶執行服務。
- 以 SYSTEM 執行的處理程序讀取 HKCU 會怎樣?
- 在 LocalSystem 的處理程序中,HKEY_CURRENT_USER 會關聯到預設使用者(HKEY_USERS\.DEFAULT),所以互動使用者寫進 HKCU 的值是看不到的。整台機器共用的設定請放到 HKLM;真的需要讀取使用者設定時,請先模擬那個使用者,然後使用 RegOpenCurrentUser。
- 能把已登入的 Chrome/Edge 設定檔複製到 CI 機器上使用嗎?
- 基本上不行。Chrome、Edge 等 Chromium 系瀏覽器的設定檔位於該使用者的 %LOCALAPPDATA% 下,Cookie 與已儲存密碼的加密金鑰受那個使用者的 DPAPI 保護。把資料夾複製到別的使用者或別的機器上,金鑰對不上,也就解不開(Firefox 等擁有自家一套設定檔保護機制的瀏覽器情況不同)。自動化中請把測試用帳戶的登入步驟寫成程式碼,或者使用自動化工具的 storage state 機制。
- 用 cmdkey 儲存的認證資訊,在工作排程器執行時沒有被用上。
- 因為認證管理員的保存庫按使用者各自獨立,而你存進去的是互動式登入的那個你自己的保存庫。此外,設定成「不儲存密碼」(S4U)的工作是在沒有網路認證資訊的情況下執行的,所以只要還是 S4U,往保存庫裡塞認證資訊也用不上。請先考慮改成「儲存密碼」的設定或切換到服務帳戶,在此之上如有必要,再把認證資訊寫入執行帳戶自身的保存庫。