在 Day22,我們透過 Python 的 subprocess 模組,成功使用 SIGTERM 終止 Kali Linux 中的 sleep 60 測試程序。
這代表 BehaviorGuard 已經具備基本的程序終止能力,不再只是顯示 Response 建議。
不過,當時的終止功能仍然很簡單,只要傳入 authorized=True,並符合指定的測試指令,就可以執行終止。
這樣的設計如果直接用在真正的自動回應系統中,可能產生安全問題。
例如,Detection Engine 誤判正常程序,或是原本的程序已經結束,PID 被其他程序重複使用,就可能導致錯誤的終止操作。
因此,Day23 我們要建立 Response Policy(回應安全策略),並加入程序保護清單與身分驗證機制。
今天的重點不是增加偵測規則,而是讓程序終止功能在執行前,先確認目標是否符合安全條件。
Response Policy 可以理解成事件回應的安全規則。
例如,當系統收到一筆要求終止程序的請求時,不應該立即執行 SIGTERM,而是先檢查:
只有所有條件都通過,才允許執行終止操作。
我們今天採用的設計是:
Process Termination Request
|
v
Response Policy
|
v
授權檢查
|
v
白名單檢查
|
v
程序身分驗證
|
v
ALLOW / DENY
| |
DENY ALLOW
| |
拒絕執行 SIGTERM
|
v
驗證程序退出
這樣可以讓 Response Engine 與安全策略分開,未來要增加其他檢查時,也不需要一直修改程序終止的主要邏輯。
之前我們主要透過 PID 與 Command 識別程序。
但 PID 並不是永久不變的識別碼。
例如,某個程序的 PID 是 1234,當它結束後,Linux 未來可能把相同 PID 分配給另一個程序。
如果系統只記住 PID,就可能無法分辨現在的 PID 1234 是否還是原本的程序。
因此,這次我們要取得更多程序資訊。
Linux 提供 /proc 虛擬檔案系統,可以讀取程序相關資料。
| Linux 路徑 | 用途 |
|---|---|
/proc/[pid]/comm |
取得程序名稱 |
/proc/[pid]/cmdline |
取得程序命令列參數 |
/proc/[pid]/exe |
取得執行檔的符號連結 |
/proc/[pid]/stat |
取得程序狀態與啟動時間等資訊 |
其中比較重要的是 /proc/[pid]/stat。
它的第 22 個欄位 starttime,記錄程序在系統開機後經過多少 clock ticks 才啟動。
如果我們同時記錄 PID 與 Start Time,就能降低只使用 PID 造成的身分混淆風險。
另外,Executable 可以幫助我們確認程序實際執行的檔案位置,而不只是相信程序名稱。
不過,Executable 路徑比對也不是完整的檔案完整性驗證,這次尚未檢查檔案雜湊值。
首先進入專案:
cd ~/behaviorguard
編輯:
nano response/policy.py
我們在這個檔案中建立 get_process_identity() 函式,專門取得 Linux 程序身分資訊。
def get_process_identity(pid):
try:
base = f"/proc/{pid}"
with open(f"{base}/comm", "r") as f:
name = f.read().strip()
with open(f"{base}/cmdline", "rb") as f:
command = [
x.decode(errors="replace")
for x in f.read().split(b"\x00")
if x
]
executable = os.readlink(f"{base}/exe")
with open(f"{base}/stat", "r") as f:
stat = f.read().strip()
end = stat.rfind(")")
if end == -1:
return None
fields = stat[end + 2:].split()
start_time = int(fields[19])
return {
"pid": pid,
"name": name,
"command": command,
"executable": executable,
"start_time": start_time
}
except (OSError, ValueError, IndexError):
return None
1. 讀取 Command
Linux 的 /proc/[pid]/cmdline 使用 Null Byte(\x00)分隔命令列參數。
因此,我們使用:
f.read().split(b"\x00")
將原始資料拆成不同參數,再解碼成文字。
例如:
["sleep", "60"]
2. 取得 Executable
executable = os.readlink(f"{base}/exe")
/proc/[pid]/exe 是指向程序執行檔的符號連結。
透過 os.readlink(),可以取得它指向的路徑。
3. 取得 Start Time
end = stat.rfind(")")
fields = stat[end + 2:].split()
start_time = int(fields[19])
由於 /proc/[pid]/stat 的第二個欄位 comm 被括號包住,而且可能包含空格,所以不能直接對整行進行簡單的空格切割。
我們先找到括號結束的位置,再解析後面的欄位。
移除前兩個欄位後,fields[0] 對應原本的第 3 欄,因此 fields[19] 就是原本的第 22 欄 Start Time。
這次取得的程序資料包含 PID、Name、Command、Executable 和 Start Time,供後續 Policy 比對使用。
接著,我們建立受保護程序清單:
PROTECTED_PROCESSES = {
"systemd",
"sshd",
"NetworkManager",
"dbus-daemon",
"cron",
"crond"
}
這裡的白名單代表受到保護的程序。
如果程序名稱出現在清單中,Response Policy 就會拒絕終止請求。
另外,今天仍然限制只有指定測試指令可以被終止:
ALLOWED_TEST_COMMAND = ["sleep", "60"]
因此,我們目前的 Policy 並不是允許終止所有非白名單程序,而是只允許符合測試條件的程序。
除了授權、PID 與白名單檢查,我們也加入:
keys = [
"pid",
"name",
"command",
"executable",
"start_time"
]
for key in keys:
if current[key] != expected_identity.get(key):
return False, f"Process identity mismatch: {key}"
return True, "Policy and identity verified"
這裡使用兩份資料:
expected_identity:事先保存的程序身分。current:執行終止前,重新從 Linux /proc 取得的程序身分。如果兩份資料有任何欄位不一致,就拒絕執行。
例如,Start Time 不同,代表目前觀察到的程序身分與之前記錄的不一致。
這種不符合條件就拒絕的設計,符合 Fail Closed(無法確認安全條件時預設拒絕)的概念。
完成 Policy 後,我們修改 response/engine.py。
原本 Day20 的 handle_alert() 仍然保留 Dry Run,不會自動終止可疑程序。
今天修改的是 Day22 建立的 terminate_test_process()。
主要邏輯如下:
def terminate_test_process(
proc,
authorized=False,
expected_identity=None
):
print("\n========== BehaviorGuard Termination ==========")
allowed, reason = check_termination_policy(
proc,
authorized=authorized,
expected_identity=expected_identity
)
if not allowed:
print(f"[DENIED] {reason}")
print("Executed : False")
return False
print(f"[POLICY] ALLOW: {reason}")
try:
proc.terminate()
proc.wait(timeout=3)
print("Executed : True")
print("Result : Process exited")
return True
except subprocess.TimeoutExpired:
print("[FAIL] Process did not exit in time.")
return False
except OSError as e:
print(f"[ERROR] {e}")
return False
這次最重要的改變是:
Response Engine 必須先取得 Response Policy 的 ALLOW 結果,才能執行 proc.terminate()。
如果沒有授權、程序身分不一致或程序受到保護,就直接回傳 False。
這也讓我們可以把安全檢查與實際處置分成不同模組。
今天建立了 test_day23_advanced.py,分別測試五種情況。
測試使用 Python subprocess.Popen() 建立真正的 sleep 60 子程序,再由 get_process_identity() 取得程序資訊。
測試結束後,也會清理自己建立的程序,避免殘留背景程序。
執行:
PYTHONPATH=. python3 test_day23_advanced.py
第一個測試沒有提供授權:
[DENIED] Authorization required
Executed : False
[PASS] Unauthorized request denied.
結果符合預期,Response Engine 沒有執行終止操作。
第二個測試將 sleep 暫時加入受保護清單。
即使提供 authorized=True,Policy 仍然應該拒絕。
實際結果:
[DENIED] Protected process: sleep
Executed : False
[PASS] Whitelist protection worked.
這證明程序保護清單有實際參與 Policy 判斷。
測試結束後,程式會移除暫時加入的 sleep 保護設定。
第三個測試修改預期身分中的 Start Time:
fake_identity = identity.copy()
fake_identity["start_time"] += 1
這次不是實際製造 PID 重複使用,而是透過修改測試資料,模擬程序啟動時間不一致。
實際結果:
[DENIED] Process identity mismatch: start_time
Executed : False
[PASS] Start time mismatch denied.
Policy 成功發現 Start Time 不符合預期並拒絕終止。
第四個測試將預期執行檔路徑改成:
fake_identity["executable"] = "/tmp/fake_sleep"
實際結果:
[DENIED] Process identity mismatch: executable
Executed : False
[PASS] Executable mismatch denied.
這代表 Executable 路徑比對確實有效,當目前執行檔與預期資料不一致時,系統會拒絕處置。
最後建立一個符合條件的測試程序,使用正確的身分資料與授權參數。
實際執行結果:
========== BehaviorGuard Termination ==========
[POLICY] ALLOW: Policy and identity verified
PID : 2551678
Action : Process Termination
Mode : CONTROLLED TEST
Executed : True
Result : Process exited
[PASS] Authorized process terminated.
這次 Response Policy 成功允許處置,而 Response Engine 也成功向測試程序發送 SIGTERM,並確認程序結束。
| 測試項目 | Policy 結果 | 測試結果 |
|---|---|---|
| 未授權請求 | DENY | PASS |
| 白名單保護 | DENY | PASS |
| Start Time 不一致 | DENY | PASS |
| Executable 不一致 | DENY | PASS |
| 符合所有安全條件 | ALLOW | PASS |
今天共完成五項進階測試,全部通過。
與 Day22 相比,這次的進步不只是能終止程序,而是加入了執行終止前的安全條件檢查。
雖然今天增加了程序身分驗證,但目前仍然只是初階的 Response Policy。
首先,authorized=True 只是函式參數,不是真正的使用者身分驗證或授權系統。
其次,今天使用 PID、Start Time 和 Executable 來比對程序身分,可以降低身分混淆風險,但不能完全消除檢查與執行之間的競態條件。
這涉及 TOCTOU(Time-of-Check to Time-of-Use)問題。
例如,系統完成程序檢查後,到真正發出終止訊號之間,程序狀態仍可能發生改變。
Linux 有提供 pidfd 相關機制,可以在未來研究如何更可靠地參照特定程序。
另外,目前的白名單主要根據程序名稱判斷,這不是完整的程序可信度驗證機制。
最後,Response Policy 仍然只應用在受控測試程序終止流程,還沒有讓真正的 Detection Alert 自動觸發 SIGTERM。
因此,今天不能宣稱已經完成企業級自動化防禦或完全解決誤判問題。
Day23 讓我更了解,真正的事件回應並不是簡單地找到 PID,然後直接執行終止指令。
在 Day22,我們已經成功使用 SIGTERM 終止測試程序,但當時的安全檢查相對簡單。
今天透過 Response Policy,加入了程序白名單、指令限制、啟動時間與執行檔路徑比對,讓終止操作在執行之前必須先經過更多檢查。
其中我認為最重要的收穫,是了解 PID 並不能單獨作為程序身分的可靠依據。
過去我只知道 Linux 可以透過 PID 管理程序,但這次才進一步學到 /proc/[pid]/stat 中的 Start Time,以及 /proc/[pid]/exe 提供的執行檔資訊,可以協助判斷程序是否符合預期。
另外,Fail Closed 也是今天很重要的觀念。
如果系統無法確認程序身分,就應該拒絕執行可能影響系統正常運作的動作,而不是在資訊不完整的情況下繼續處置。
不過,目前我們的保護仍然有限,還沒有完整的身分授權、處置稽核與競態防護機制。
下一篇 Day24,我們預計將 Detection 與受控 Process Termination 進一步整合,測試符合明確安全條件的事件,能否經過 Response Policy 後自動觸發程序終止。
我們會先限制在自己建立的測試程序,不會直接讓所有 HIGH Alert 都自動終止 Linux 程序。