260 lines
20 KiB
Plaintext
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.
|