Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# ShellKnight Changelog

## [v2026.09.26.001] - 2026-09-26

- **State directory hardened against local privilege escalation (critical):** `C:\ProgramData\ShellKnight` holds `config.json` (read at startup), `run.ps1` (the native `ShellKnight` scheduled task executes it as SYSTEM every 8 hours) and the `Logs`, `JSON` and `Intel` folders. By default ProgramData lets `BUILTIN\Users` create files and folders in its subfolders, and `CREATOR OWNER` gets full control of what they create. So on an endpoint where ShellKnight had never run, a standard user could pre-create `C:\ProgramData\ShellKnight` (or `run.ps1`, or `config.json`) and own it. Owning `run.ps1` lets them choose the code SYSTEM runs on the next scheduled run - a local privilege escalation. Owning `config.json` lets them set `BattlefieldURL` and `BattlefieldApiKey`, redirecting the run report and the tenant API key to a URL of their own. Once SYSTEM has created a file a user cannot modify it, but if the user owns the folder they can still delete and replace its files.
- **What changed:** before `config.json` is read or the scheduled task is trusted, a new guard (running during config load, ahead of `Initialize-Logging`) creates or repairs the folder with an explicit ACL and re-checks the sensitive files:
- **Explicit ACL, set by SID:** `SYSTEM` (`S-1-5-18`) and `Administrators` (`S-1-5-32-544`) full control, `Users` (`S-1-5-32-545`) read and execute, all inheritable to files and subfolders; owner set to Administrators; inheritance removed (`icacls /inheritance:r`) so ProgramData's `Users`-create ACEs no longer apply. SIDs are used throughout, never localized names, because the built-in groups are `Administratoren` / `Benutzer` on a German box but the SIDs are the same everywhere. Removing the parent's inherited ACEs and setting new inheritable ones propagates to existing `Logs` / `JSON` / `Intel` children that still inherit, so no recursive `/T` sweep runs on every check-in.
- **A folder a user already owns is rebuilt:** if the folder exists but `SYSTEM` or `Administrators` does not own it, it is removed with its contents and recreated. A user-owned tree has no run history worth keeping and may carry access-control entries the guard cannot enumerate.
- **`config.json` and `run.ps1` are owner-checked:** either one not owned by `SYSTEM` or `Administrators` is deleted, so a planted `config.json` is never read and a planted `run.ps1` is never executed. `run.ps1` is rewritten by the self-schedule block later in the run; a removed `config.json` means built-in defaults and environment variables stand in for that run.
- **Model:** owner is read with `(Get-Acl -LiteralPath ...).GetOwner([System.Security.Principal.SecurityIdentifier]).Value`, the same non-throwing pattern the Intel cache trust check uses (v2026.09.25.004); an owner that cannot be read is treated as untrusted. The whole guard is wrapped so a hardening failure logs a warning and never stops the run.
- **New payload fields:** the report's `health` object gains `state_dir_repaired` (the folder was found user-owned and rebuilt) and `state_dir_files_removed` (count of untrusted `config.json` / `run.ps1` deleted, 0/1/2), so Battlefield can flag an endpoint that showed signs of a local tampering attempt. Battlefield stores the whole report (ADR 0002), so it accepts the fields unchanged; nothing displays them yet.
- **Regression test:** new `tests/Test-StateDirGuard.ps1` runs the extracted guard verbatim under `Set-StrictMode -Version 2`, with `Get-Acl` and `icacls` mocked and real temp directories for the filesystem operations. It covers a fresh box (folder created), the steady state (trusted owner, folder and its contents kept), a user-owned folder (removed and recreated), an unreadable owner (treated as untrusted), and trusted vs. user-owned vs. unknown-owner `config.json` and `run.ps1`. It asserts the `icacls` arguments carry the three SIDs with the `*` prefix and no localized principal, that `/inheritance:r` and `/setowner` are issued, and - from the script's own source order - that the guard runs before the `config.json` read. It does not replace a real Windows run: `Get-Acl`, `icacls` and NTFS inheritance are Windows behaviours this test mocks.
- **Not yet run on real Windows.** The ACL, the ownership reclaim and the owner checks need one real SYSTEM run on a Windows 10/11 endpoint before this reaches `main`, per the repo rule.

## [v2026.09.25.004] - 2026-09-25

- **The Intel Engine loads threat intel for the first time (critical):** since v1.002 the engine's `Invoke-SafeBlock` read `$Script:Config.IntelEngine_PrimarySource`, which `$Script:Config` did not have; only `$SK_IntelEngine_PrimarySource` existed. Under `Set-StrictMode -Version 2` that threw in the `$consolidated` literal, before any download, cache write or `IntelSource`, and with no cache written the next run took the same path. **Every device on every run reported `intel_source: "Hardcoded fallback"` and 0 hash, filename and C2 IOCs** (Battlefield backtest, 2026-07-03 to 2026-09-25), so the detection engines ran on their hard-coded lists only. The only trace was one INFO line in the log: `Intel Engine skipped - The property 'IntelEngine_PrimarySource' cannot be found on this object. Verify that the property exists.` The property is now in `$Script:Config`.
Expand Down
Loading
Loading