Files
Domito/docs/ci-validate-file-object.md
T

11 KiB

CiValidateFileObject: validation flag 0x00000020

Issue #3 is documented; kernel runtime verification is pending. The probe builds and its plumbing is covered by host tests, but those tests do not execute Windows Code Integrity. Do not close the runtime-testing portion on that basis.

Meaning supported by the evidence

DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE is Domito's descriptive name for 0x00000020 in the second argument, ValidationFlags. It is not a recovered Microsoft macro name. On the analyzed ci.dll 10.0.26100.8328 x64, the flag causes CipGetAcceptedRootKeys to OR 0xA4FF into its accepted-root mask in the branch where the global at RVA 0x45A88 is zero. Other global state, flags, and the requested signing level also affect the result.

The assembly at preferred image base 0x180000000 establishes the operation:

0x180089e40: MOV EDX, 0xa4ff
0x180089e4c: BT ECX, EDI          ; ECX = 0x8c in this branch; EDI = signing level
0x180089e4f: JC 0x180089e57
0x180089e51: TEST SIL, 0x20      ; saved ValidationFlags
0x180089e55: JZ 0x180089e59
0x180089e57: OR EBX, EDX         ; broaden accepted-root mask

For requested level 0, flags 0, the branch above, and all other optional global additions disabled, the mask is 0x80AA; setting only 0x20 makes it 0xA4FF (additional bits 0x2455). This is an arithmetic witness, not a measurement of the running OS's initialized policy. Levels 2, 3, and 7 already trigger the OR through 0x8C. When the global at RVA 0x45A88 is nonzero, the helper returns 0xA4FF directly, so the flag need not change its result.

Later processing can still remove bit 0x8 when the helper's third argument is nonzero and validation flag 0x00400000 is present. The surrounding CipDisallowEmbeddedPicrootIfTrusted path can add that latter flag itself. Thus 0xA4FF is not a promise about every final policy result.

The widened mask includes categories associated with test roots and unresolved chains as well as other roots. It must not be named "trust any Authenticode signer", "require Microsoft", or "enable TESTSIGNING". It changes the acceptance policy for this operation; it does not change the system boot setting. Further signature, chain, signing-level, and policy checks still matter. A successful call alone does not establish equivalence with WinVerifyTrust or the caller's intended publisher trust policy.

CipGetHashAlgorithmForLegacyScenario does not test 0x20: the flag is not a fixed SHA-1/SHA-256 selector. Also distinguish this input flag from the unrelated MINCRYPT_POLICY_INFO.PolicyBits output bit 0x20, associated with the built-in DMD root category in this build.

Historical generation differences

The same numeric flag does not imply the same internal root mask across Windows versions. Static analysis of the extracted x64 corpus adds these bounded facts:

  • On sampled 10.0.10240.16463 (1507), 10.0.10586.0 (1511), 10.0.14393.2189 (1607) and 10.0.15063.0 (1703), the recovered CipGetAcceptedRootKeys bodies have no direct flag & 0x20 broadening branch. They still test larger masks containing that bit, so this is not evidence that the flag has no effect anywhere in those verifiers.
  • On sampled 10.0.16299.125 (1709) and 10.0.17134.1130 (1803), when the helper's main global mode is zero, flag & 0x20 selects mask 0x224FF (assignment), rather than the modern mask |= 0xA4FF operation. A signing-level predicate can select the same mask. Other global additions occur afterward. The other global mode returns 0x204FF directly.

The observed helper masks include branch-specific diagnostics/categories and must not be interpreted as an official stable root enum. The descriptive Domito constant names the intended broadening use without promising a single mask or identical behavior on every build.

The ISO-derived 10.0.19041.1 x64 helper provides direct static evidence for the reported build family: in its main global mode it uses mask |= 0x24FF when flag 0x20 or the signing-level predicate applies; the alternate mode returns 0x24FF. With requested level zero and optional global additions disabled, 0xAA becomes 0x24FF (added bits 0x2455). This is an arithmetic witness, not a measurement of initialized OS policy. The exact CI SHA-256 is 2955710d34db26e352e62b20861ba338f0216cc36b378d128057be955e184282. CodeIntegrity-RE retains matched-PDB pseudocode and assembly in historical/ci-19041.1-contracts.json and historical/ci-19041.1-assembly.json, with batch evidence ID ev_13f89502777ee38ea98410f7a07f078bb934e78d792b662ab242f7b11d27e747. Other 19041/19045 servicing revisions and the TESTSIGNING-off runtime workaround still require their own evidence.

Historical records in CodeIntegrity-RE: historical/ci-14393-contracts.json ev_e63a4c4b7613e231795051b784b9571605b9bf81258b5dcd04d32ddc681abb41, historical/ci-15063-contracts.json ev_338658fb82559fa6daa8ae5487b90fe84b8e408ea7578feed6d8dff5d590e114, historical/ci-16299-contracts.json ev_fe5731175f0726f0d46af2f78ca615d284f8598939296815e26b5589db3001cf, and historical/ci-17134-contracts.json ev_0c3e3359b8b7486318006afa6b95eb2257a18abeec9041a86a32db487bcbe996.

Evidence and compatibility limits

The upstream report describes success with flags 0x20, signing level 0, and TESTSIGNING off on Windows builds 19041 and 19045. That is third-party runtime evidence, not a reproduction by this project. Its behavior is consistent with broader root acceptance; it does not establish identical internals across those builds.

Our static evidence uses REA 6.8.0 / Ghidra 12.1.3 and the binary SHA-256 c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b. Public-symbol mapping uses ci.pdb GUID 1CAE574B-CF1F-3564-5F74-49567A128629, age 1. The companion CodeIntegrity-RE repository retains:

  • ci-flag20-assembly-evidence.json: assembly evidence ev_ea763b9be7643a6241b0c805eae5f735a4b4a0a82df1383b1e68da4207f85fb4.
  • evidence/CipGetAcceptedRootKeys.c: decompilation ev_c905f03db9470c8b2d89a6295080143f25d5780ca0909de0ca554e40d078a3f3.
  • evidence/CipGetHashAlgorithmForLegacyScenario.c: ev_addf8f1bc18da58db80db54eeab0dd78ec5ab88b219ece3e082ebd9a8e50b257.
  • evidence/CipDisallowEmbeddedPicrootIfTrusted.c: ev_2475a5804fc87683a54b06acebd3785e5e8eb31f4f542f699fc2ff414c1ab7fe.
  • evidence/CipValidateFileObjectByFileOrPageHash.c: capacity contract ev_3897aa76c17cc64a30f60e72b994e695b88c8eda2ae6f246766bdc9314f06cca.

No complete enumeration of downstream uses of the flag, historical Microsoft name, or cross-version/architecture guarantee is claimed. The public declaration keeps the existing parameter widths. Names now describe the known inputs; on 26100 x64 the third argument is consumed as a byte. Narrowing the ABI everywhere requires separate evidence.

Kernel experiment prepared for the VM

Add tests/ci-file-object-probe.cpp to an existing signed test driver and add include/ to its include path. Link the existing CI imports (ci.lib). Include ntifs.h, Domito.MinCrypt.h, and tests/ci-file-object-probe.h at the call site. Invoke DomitoProbeCiValidateFileObject at PASSIVE_LEVEL with a referenced file object for the fixture and an array of DOMITO_CI_PROBE_RESULT_COUNT results. Keep the file bytes stable and hold the object reference for the entire probe. The surrounding driver owns opening/referencing/closing the file and exporting the result records. The probe has no DriverEntry or installation side effects.

It makes six calls, always at requested level 0:

  1. Flags 0, then 0x20, then 0x20, then 0, with 64-byte capacity each time.
  2. Flags 0, then 0x20, with declared capacity 63 (64-byte backing storage).

Each call uses fresh policies, hash capacity, algorithm, and signing-time storage. Both policy objects are freed after capture, including on failure. The records capture status, both policy statuses/bits, chain presence, signing time, digest algorithm/length/bytes, and a trailing buffer guard. The digest is a file hash, not a certificate thumbprint. Failed-call outputs are raw diagnostics, not valid verification results. Chain internals are deliberately not dereferenced.

The repeated order helps reveal cache/order effects but does not eliminate them. Also run the reversed initial order in a fresh VM snapshot when comparing policy. For the analyzed 26100 helper, both 63-byte-capacity calls should return STATUS_BUFFER_TOO_SMALL and set length 64, regardless of the eventual algorithm. Treat older-build differences as observations to investigate, not silently as test success. Every guard must remain intact; successful hash sizes must be at most 64. Inspect non-success statuses rather than treating all failures as invalid signatures (I/O and resource errors are possible).

Run on Windows 10 19041 and 19045 and a current Windows 11 build. The decisive reproduction uses a driver loadable with TESTSIGNING off. Record that setting, Secure Boot, HVCI/WDAC policy, OS build, CI version/hash, driver identity, fixture SHA-256, and each complete result. Keep policy constant between paired calls. Use these fixtures:

  • A known third-party RSA Authenticode executable (ideally the reported fixture).
  • A Microsoft-signed executable and a catalog-signed file as controls.
  • An unsigned executable and a copy of the signed file with a byte changed in a hashed section, leaving its certificate table unchanged.
  • A self-signed/test-root fixture to characterize the broader acceptance policy.
  • If available, timestamped, expired, revoked, and ECC variants, with expectations recorded in advance and revocation/configuration differences retained.

Compare with signtool verify /pa /v as a separate user-mode policy observation, not an equality oracle. The report is reproduced only when the same valid third-party fixture changes from rejection at 0 to success at 0x20, under the reported environment, with usable outputs. Unchanged success can mean the existing policy already accepts it. Unchanged failure is a result to investigate. Unsigned and tampered fixtures must not become accepted as valid signatures. Self-signed acceptance must be reported explicitly, not confused with public-CA publisher trust. Do not use this flag as a new production default based only on one positive fixture.

Checks available without the VM

From an x64 Visual Studio developer shell:

python tests\run-ci-probe-tests.py

This compiles the actual probe body with mock CI calls and constants extracted from the public header. It verifies call order, input forwarding, fresh capacity and outputs, result capture, and freeing both policies after success and failure. It does not emulate the flag's trust semantics. Kernel compilation checks the real headers separately; neither compilation nor mock tests substitute for the VM run above.