Files

260 lines
20 KiB
Plaintext

OTCHE WINDOWS SOURCE SETUP / WORKER CONTRACT
Prerequisites and trust
- Windows 10/11 with Windows PowerShell Desktop 5.1, NTFS C:, QEMU Guest Agent,
working display driver, Task Scheduler, active Defender, and an existing dedicated
local non-RID500 administrator account with its existing password. Operator selects
it explicitly with Get-Credential. Installer neither creates nor modifies accounts.
- UAC must already be enabled. Limited and Highest tasks use the same account's
split interactive token. The user task is never elevated; admin is never silently
downgraded. Actual account SID, console session and TokenElevation are checked.
'user' means the filtered (Limited) token of this prepared local administrator,
NOT a separate standard account. An account such as OtcheUser may exist harmlessly
but is not used by the runner. True standard-user semantics require a separate
source/profile whose console account is a standard user; this installer does not
support that model. With the default ConsentPromptBehaviorAdmin=5, eligible Windows
components can auto-elevate; Limited is not a guarantee against UAC bypasses. The
installer records policy but never changes UAC or other protection settings.
- Restricted blocks ALL script files, including signed installer and signed runner.
First obtain an independently approved organizational script-execution policy.
Recommended: AllSigned, with every supplied .ps1 and .psm1 signed by an approved
code-signing certificate trusted by LocalMachine and the selected account. Verify
signatures with Get-AuthenticodeSignature. Sign only after reviewing final files.
RemoteSigned requires valid signatures on downloaded/Internet-zoned scripts.
No policy switch, Unblock-File, EncodedCommand, paste-to-evade-policy, or Bypass is
supported. Interactive pasting does not solve Restricted blocking scheduled files.
AppLocker/WDAC/ConstrainedLanguage must independently permit the reviewed runner,
native Add-Type helpers and selected execution hosts. Protection stays enabled.
- Run elevated: .\Install-OtcheSource.ps1 -Credential (Get-Credential
"$env:COMPUTERNAME\LabRunner") -EnableAutoLogon
This command is one line. Omitting EnableAutoLogon leaves login to the operator.
The password is validated, never reset; opt-in autologon stores only an LSA private
secret. It refuses existing autologon/DefaultPassword configuration rather than
overwriting it. LSA secrets remain recoverable by SYSTEM/admins: protect master,
clones, exports and backups. Installation failures require operator inspection;
installer does not destructively rollback accounts, registry or existing evidence.
- Maintenance (run elevated under the existing approved policy; no credential):
.\Install-OtcheSource.ps1 -Verify
Read-only JSON {ok,errors[]} checks directory ACL shape, local account SID,
all four task principals/actions/working directories, on-demand/enabled state,
Winlogon autologon/account values, and installed/supplied runner signatures and
SHA256 equality. Both copies must have Valid trusted Authenticode signatures for
AllSigned compatibility, even if the current policy is RemoteSigned. Without
configured autologon, DefaultUserName is not required. This cannot verify the
password stored in LSA or replace the real desktop/clone qualification check.
.\Install-OtcheSource.ps1 -UpdateRunner
Use the reviewed, newly signed installer bundle beside the six runner files on a
source explicitly taken out of publication for maintenance. Stops on source/ACL/
task/autologon drift or a busy task, before copying anything. Requires Valid
trusted signatures for all six replacement files; stages all as .new, checks
their hashes, then uses atomic per-file replacement preserving existing ACLs.
ISO ReadOnly attributes are removed only from owned installed/staged copies,
never from the source media; other attribute bits and ACLs are preserved. Updates
clear a legacy target's ReadOnly bit only after all staged files pass validation,
restoring original target attributes if replacement fails. A stale .new is reused
only if it is a regular, non-reparse file whose hash matches the signed bundle;
unrecognized staging files are left untouched for inspection. Cleanup failures
are warnings when another failure is already pending, preserving its real cause.
No task, account, registry, autologon secret or directory ACL is changed. The JSON
output is the NEW Get-OtcheBaseline, not qualification. Do not dispatch during
maintenance. Replacement is atomic per file, not a six-file transaction: if an
I/O failure interrupts replacement, correct the failure and rerun -UpdateRunner,
then -Verify. Existing installed-baseline.json is installation evidence, not
rewritten by an update. Re-sign changed files, reboot/check the desktop as needed,
and requalify disposable clones before republishing the source revision.
- No disk is initialized or formatted and no evidence disk is assumed. Artifacts
use existing NTFS C:. The full source remains an ordinary stopped VM template=0.
Operator reboots and verifies a real active console desktop before stopping it.
Complete first-login privacy/setup through the real UI. An active WTS session alone
is insufficient: the input desktop must be unlocked Default, with the expected
account's real Explorer shell and visible taskbar, and no visible setup/sign-in host.
Worker clones CURRENT disks with full=1, never a snapshot name. Never alter master.
For a source without CD, provision an empty CD on the stopped CLONE, not the source.
- These files do not establish network isolation. Worker must remove clone NICs
BEFORE media delivery in offline mode and prove fail-closed isolation for online.
Sample delivery follows recorder writable-sink + full-frame readiness only.
Worker commands (native System32 WindowsPowerShell\v1.0\powershell.exe):
-NoProfile -NonInteractive -File C:\ProgramData\Otche\runner\Test-OtcheReady.ps1 -Privilege user
-NoProfile -NonInteractive -File C:\ProgramData\Otche\runner\Start-OtcheCommand.ps1 -ManifestPath C:\ProgramData\Otche\control\<command UUID>.json
Invoke-Otche.ps1 -ManifestPath is the installed interactive runner interface. QGA
never invokes the sample or submits a generic shell command. The fixed scheduler
starts prepared InteractiveToken tasks (Limited/Highest), plus a separate SYSTEM
Defender collector. All commands and control JSON must be short/bounded; use ISO
for samples and bounded QGA file-open/read offsets for binary artifacts. Mutable
JSON uses the worker's fixed read-only PowerShell snapshot with ReadWrite|Delete
sharing, closed before output, never file contents as code or a policy bypass.
No repeated file-write overwrites, credential, password or PVE token in a manifest.
Readiness uses a fixed Otche-DesktopReady InteractiveToken/Limited task running the
signed Test-OtcheReady.ps1 -Privilege user -DesktopProbe with no command/input API.
Each request has a new nonce and 60-second deadline; bounded response must match the
nonce, boot ID, configured account and active console session. The task is limited
to 90 seconds and runs without opening a console window. UserOOBEBroker residency
and arbitrary WWAHost applications are not treated as unfinished setup.
This bounded startup budget includes normal Authenticode/native-helper initialization
when an allowlist blocks certificate-network lookups; signature checks are not disabled.
The worker waits for the accepted QGA PID to exit and never overlaps readiness scripts.
Approved -UpdateRunner maintenance migrates the previous 15-second task budget.
Deadline timestamps are parsed with invariant RoundtripKind and normalized to UTC;
the guest may use any Windows time zone (UTC is no longer a prerequisite). The clock
still must be correct. Probe responses use results\desktop-probe-<nonce>.json; the
caller deletes its consumed response and checks at most 128 probe files per request
for stale responses older than 60 seconds. Session/task/desktop failures return
ready=false with error and baseline=null, without performing a full baseline. Only
a successful desktop probe pays for one baseline; -BaselineOnly still collects it
without probing sample-owned UI.
The normal readiness gate runs before media/dispatch; the actual runner repeats
desktop/token checks once before launch. During observation, -BootOnly returns only
the boot ID without launching a desktop probe. Post-run -BaselineOnly collects the
full source/Defender baseline without reinterpreting sample-owned GUI as setup.
Manifest (JSON data, max 1 MiB): command_id, attempt_id, job_id (canonical UUIDs),
iso_label OT<first 14 uppercase hexadecimal job UUID characters> (16 ASCII characters, identical ISO9660/Joliet labels), sha256 (lowercase),
filename (execution_filename selected ONCE by worker per Job), settings from API.
Settings require architecture auto|x86|x64, wsh_host cscript|wscript, msi_ui
full|quiet|passive, plus existing fields. args is a JSON string array; only
{sample.path}/{sample.name} are expanded. ISO root job.json contains job_id,
sha256,filename,iso_label; payload is sample/<filename>, unchanged original bytes.
Optional root qualification='benign'|'eicar'. EICAR is delivery-only, never started.
Fixture generation is a separate explicit disposable-only script, never installer.
Its short-lived machine-bound qualification marker is placed only on a clone.
Results: C:\ProgramData\Otche\results\<command UUID>\
- receipt.json: command_id,attempt_id,job_id,state accepted|running,accepted_at,
started_at,deadline_at,session_id,user,privilege,pid,boot_id.
- status.json refreshed about every 3 seconds, result.json atomically terminal:
command_id,attempt_id,job_id,phase,outcome,findings,telemetry,started_at,deadline_at,
finished_at,error,report,artifacts[{path,filename,kind,content_type}]. report matches
docs/API.md. Artifacts are bounded JSON/log files; preserve them if collection fails.
- receipt CreateNew precedes scheduling; dispatch.json CreateNew precedes runner
activity. One clone accepts exactly one command. Existing receipt => duplicate,
NEVER rerun after failure/crash. accepted receipt has null started_at/deadline_at;
these become real timestamps only after selected sample host Process.Start succeeds.
Preparation failure/quarantine never invents startup/duration/PID. Worker enforces
a preparation deadline and then external runtime from started_at/deadline_at.
- Full observation uses Stopwatch and continues when the parent exits; descendants
are not guessed from installed applications. Worker stops clone after collection.
Unexpected reboot/disconnect is externally interrupted, never resumed/reset.
- Test-OtcheReady returns ready,session_id,user,privilege,boot_id,baseline
{fingerprint,qualified,errors},defender,environment,error. qualified here means
readable active prerequisites, NOT a published qualified revision. Worker compares
observed fingerprint with externally qualified revision; drift or unreadable
required telemetry is unqualified. Baseline fingerprints include full OS build,
runner hashes/signatures, PS policy, Defender versions/preferences and security
state. environment.security contains uac.{EnableLUA,ConsentPromptBehaviorAdmin,
PromptOnSecureDesktop,FilterAdministratorToken}, smart_app_control (CI\Policy
VerifiedAndReputablePolicyState), secure_boot (SecureBoot\State
UEFISecureBootEnabled), and device_guard.{EnableVirtualizationBasedSecurity,
RequirePlatformSecurityFeatures,HVCIEnabled}. Absent registry values are null;
EnableLUA other than 1 makes the baseline unqualified. Secure Boot uses the
user-readable registry value, never an elevated-only Confirm-SecureBootUEFI call.
The Win32_DeviceGuard CIM snapshot is baseline.device_guard_runtime (null if
unavailable): VirtualizationBasedSecurityStatus, SecurityServicesConfigured,
SecurityServicesRunning, CodeIntegrityPolicyEnforcementStatus and
UsermodeCodeIntegrityPolicyEnforcementStatus. This token-dependent diagnostic is
NOT fingerprinted; registry DeviceGuard/HVCI configuration is. Thus these new
fingerprint fields do not differ solely because a token is Limited vs Highest.
Intelligence or security-state updates can change fingerprints; requalify rather
than silently accept drift.
Dispatch and evidence semantics
- EXE/SCR/PE COM direct; DOS16/non-PE COM explicitly incompatible. PE x86/x64 chooses
matching hosts; unsupported machines/architecture mismatch fail before dispatch.
- DLL regsvr32 requires actual DllRegisterServer export. rundll32 requires explicitly
requested actual named/ordinal export, never guessed. Existence does not establish
a compatible ABI; operator must choose a rundll32-compatible export. Commas in DLL
paths are rejected. Native CRT quoting is separate from rundll32's module syntax.
- PS1 uses -File and no execution-policy bypass. WSH uses requested cscript/wscript.
CMD/BAT uses fixed cmd /d /s /v:off /c with per-process environment transport and
one nonrecursive percent substitution, not concatenation of user text into shell
source. Delayed expansion remains OFF. Reports retain resolved original argument
values, never internal environment-variable aliases.
Actual CMD and BAT fixtures preserve spaces, Unicode, a&b, (parentheses), literal%,
literal %PATH%, !bang!, caret^value, comma/semicolon/equal, empty strings, trailing
backslashes, and balanced literal quotes such as say"hello". A terminal value a"b
is also supported. These are measured cases, not a claim about every batch script's
own argument processing (a script can itself reinterpret values through CALL/etc.).
Native batch parameters have no general CRT-style quote escape. An unmatched quote
in a NONFINAL value consumes following arguments: requesting [a"b, tail] yields
first value a"b" "tail and no second value in the direct native control. The runner
rejects that case. Inner quotes exposing spaces/tabs/comma/semicolon/equal also
split native parameters: say "two words" is not one exact literal parameter.
These specific boundaries, NUL/CR/LF, and the bounded cmd line limit fail explicitly;
there is no blanket percent/metacharacter/quote rejection. A batch script needing
otherwise unrepresentable text needs its own explicit data-file interface, not
silently changed argument bytes. No generic shell API is added.
- MSI keeps /i <selected sample>, required /L*V <command MSI log>, and selected UI.
Explicit custom install modifiers /norestart, /promptrestart, /forcerestart,
/m <SMS MIF basename <=8 characters>, /n <instance ProductCode GUID> are accepted,
alongside PROPERTY=value entries. /promptrestart with quiet UI is natively invalid.
Operation/package replacements (/x, another /i, /a, /j, /p, /y, /z, etc.), custom
log/UI switches and ACTION/UILEVEL properties cannot replace controlled invariants.
Property values use Windows Installer syntax, not CRT backslash escaping:
COMPANY=Acme "Widgets" becomes COMPANY="Acme ""Widgets"""; empty values and trailing
backslashes remain literal. /m requires the environment's real SMS ISMIF32.DLL;
no successful status-file generation is claimed from command construction alone.
No implicit installed-application launch. Reboots are suppressed ONLY if explicitly
requested by the submitted arguments; actual reboot is externally interrupted.
Exit 3010 means reboot required; 1641 means reboot initiated, not Defender blocking.
Microsoft command syntax references:
https://learn.microsoft.com/en-us/windows/win32/msi/command-line-options
https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options
- SHA256 checks original ISO and isolated NTFS copy. ZoneId=3 is written only when on;
off never removes a stream or unblocks. Missing file without correlated detection
remains unknown/delivery_error. There is no retry after early quarantine.
- SYSTEM collector records Defender state/preferences before and after, correlated
detections and Defender/AppLocker/CodeIntegrity events by time and sample path.
Ordinary operational/audit events are not declared blocks. No SmartScreen result
is invented from a dialog/exit. Crash/status exit differs from policy evidence.
Collector publishes telemetry.json with initial=true immediately after the first
successful Defender snapshot (after=before, empty detections), before querying
detections/event logs or taking a second snapshot. This removes those queries
from the start gate (normally targeting 1-3 seconds, not a timing guarantee).
Invoke-Otche permits up to 90 seconds instead of 30 and still fails closed on
missing/inactive/error telemetry. Subsequent snapshots set initial=false.
Event channels use independent EventRecordID cursors plus the original UTC time
filter, oldest-first bounded batches, avoiding repeated XML work. Threat detection
queries still run every loop; threat-name metadata is queried only when a new
correlated detection needs it. Collection loops sleep three seconds after work.
- Detection lists cap 64 (4 resources x 1 KiB), event lists 128, queries 200/channel,
Defender queries 256, event XML 4 KiB,
control JSON 1 MiB, output JSON 2 MiB, stdout/stderr 1 MiB, exported MSI log 4 MiB.
Raw MSI logging can grow during observation; disposable disk must have capacity.
Truncated/unreadable evidence is partial, not clean. Sample processes share their
runner token and admin samples can alter guest evidence; external video and worker
timestamps remain independent. Guest artifacts are observations, not tamper-proof.
Verification boundary
Local AST/harmless quoting/native helper smoke checks do not qualify a guest image.
Only real disposable-clone readiness, recorded benign controls and EICAR delivery,
with identical before/after fingerprint and collected evidence, qualify a revision.
The reproducible harmless regression is Test-OtcheArguments.ps1 (run normally under
the existing approved policy). It compiles a temporary benign environment reader,
executes both .cmd/.bat fixtures, prints the native unmatched-quote counterexample,
checks literal resolved argument values and MSI construction, and deletes its files.
Run local tests only under an approved existing policy; they do not authorize source or protection changes.
Grub optional file snapshots
- settings.grub_paths is an immutable bounded literal array: max32 paths, 1024 UTF8
bytes/path, 8192 total. Exact local absolute DOS paths only; no environment/sample
expansion. Percent/braces are ordinary filename characters. No directories/globs,
UNC/devices/ADS/traversal, hard links or reparse points. Open handles must resolve
to the verified actual local fixed NTFS volume, not merely a checked drive letter.
- Invoke-Otche captures after the observation deadline on its existing selected
interactive token. It shares reads/writes (not delete), pins ancestor handles,
captures at most open length and compares a second bounded read plus size/write
metadata. Concurrent readable logs work; detected changes retain explicit
best_effort snapshots, never assert atomic consistency. Unknown size/hash are null;
zero bytes is a real captured empty file. Per-file failures do not change Defender.
- grub_limits is controller-only manifest data: per-file <=8MiB, total <=32MiB,
further restricted by max_artifact_bytes/2 and collection deadline. No credentials.
Numbered results/grub-NNN.bin are transient selected-user snapshots. SYSTEM only
safe-opens those fixed files, verifies size/hash, copies into protected
control/grub-<command>-NNN.bin and publishes a bounded export-ready receipt.
Requested file paths are never read by SYSTEM or on the host. Raw staged copies
are not individual artifacts; controller streams one ZIP with safe numbered
members and a per-file manifest, commits it before allowed clone cleanup.
- Test-OtcheGrub.ps1 is a harmless local helper regression (not source qualification):
actual live writer/read sharing, identical basenames, empty/missing/denied files,
literal percent/braces, byte limits and device/network/traversal/reparse rejection.