
PowerShell toolkit for AMSI/Defender detection-boundary analysis and static malware triage maps byte offsets to detection triggers, plus YARA, entropy, string, and PE/imphash analysis. Companion to OffsetScan.
A bounded-memory PowerShell toolkit for byte-offset inspection, source correlation, binary comparison, and defensive detection-boundary analysis.
OffsetInspect answers a practical analyst question:
What content is present at this byte offset, and what source or binary context surrounds it?
It also provides an OffsetInspect-native detection-boundary workflow inspired by the same analyst problem addressed by ThreatCheck, without bundling its source or binaries: it locates the earliest content prefix that AMSI or Microsoft Defender still detects, validates the boundary repeatedly, and feeds the resulting offset straight into the context inspector. On top of that core, it adds a red-team analysis and static-triage suite - multi-region discovery, corpus scanning, detection diffing, detection-trigger correlation, drift journaling, engagement reports, entropy analysis, string extraction, and PE/imphash parsing - all read-only, plus an authorized-use signature-robustness tester that perturbs samples only in memory, and without ever disabling or reconfiguring endpoint protection.
For corpus-scale static triage (PE parsing, entropy, strings, IOC) without PowerShell overhead, see OffsetScan
-IocJsonPath.-ContextLines.ProbeLog/ProbeCount) of every distinct provider invocation, streamed live to -Verbose, for a report-ready transcript of a scan's true provider cost.¹ These two commands have optional external dependencies: Invoke-OffsetYaraScan needs the YARA engine (winget install VirusTotal.YARA), and Invoke-OffsetClamScan needs ClamAV with signature databases (winget install Cisco.ClamAV, then freshclam). Every other command is self-contained. ClamAV is a single-file detector here, not a boundary-search engine - clamscan loads its full database per invocation, so bisection would require the clamd daemon.
² Add-OffsetDriftEntry journals cross-platform, but the Defender signature/engine version fields populate only on Windows (via Get-MpComputerStatus); elsewhere they record as null and the rest of the snapshot is still written.
The offset-inspection core and all static-triage helpers are cross-platform (Windows, Linux, macOS); the AMSI/Defender threat providers are Windows-only.
Install-Module OffsetInspect -Scope CurrentUser
Import-Module OffsetInspect
git clone https://github.com/warpedatom/OffsetInspect.git
cd OffsetInspect
Import-Module ./module/OffsetInspect/OffsetInspect.psd1 -Force
The repository also includes thin CLI wrappers:
./OffsetInspect.ps1 <file> <offset>
./OffsetThreatScan.ps1 <file> -Engine AMSI
Invoke-OffsetInspect ./sample.bin 0x200
$inspectParameters = @{
FilePaths = './script.ps1'
OffsetInputs = 128, 256, 512
ByteWindow = 64
ContextLines = 4
}
Invoke-OffsetInspect @inspectParameters
$inspectParameters = @{
FilePaths = './script.ps1'
OffsetInputs = 0x80, 0x100
PassThru = $true
}
$results = Invoke-OffsetInspect @inspectParameters
$results | Where-Object BytesDiffer
Invoke-OffsetInspect ./sample.bin 0x200 -Json
Invoke-OffsetInspect ./sample.bin 0x200 -Csv
Invoke-OffsetInspect ./sample.bin 0x200 -CsvPath ./artifacts/offsets.csv
JSON mode always emits an array, including for a single result.
$compareParameters = @{
FilePaths = './before.bin'
OffsetInputs = 0x200
CompareFile = './after.bin'
PassThru = $true
}
Invoke-OffsetInspect @compareParameters
| Input | Interpretation |
|---|---|
Numeric-only values without a prefix or suffix are intentionally treated as decimal.
The output reports both BytePositionInLine and CharacterPosition. This distinction matters when a source file contains multibyte characters.
Threat-provider analysis is Windows-only. The normal offset inspection command remains cross-platform.
$scanParameters = @{
FilePath = './script.ps1'
Engine = 'AMSI'
ScanMode = 'Text'
RepeatCount = 3
PassThru = $true
}
$result = Invoke-OffsetThreatScan @scanParameters
Text mode uses AmsiScanString, searches Unicode-scalar prefixes without splitting surrogate pairs, maps the detected prefix through the validated source encoding, and returns Unicode-scalar, UTF-16 code-unit, and source-file byte indexes. Embedded NUL characters are rejected in text mode; use raw-byte mode for those files.
Invoke-OffsetThreatScan ./content.bin -Engine AMSI -ScanMode RawBytes
$scanParameters = @{
FilePath = './sample.bin'
Engine = 'Defender'
RepeatCount = 3
TimeoutSeconds = 45
}
Invoke-OffsetThreatScan @scanParameters
The Defender provider:
MpCmdRun.exe platform path.-DisableRemediation.A result such as DetectionPrefixLength = 841 means:
It does not prove that byte 840 is the complete signature, the only contributing byte, or the full malicious range. Antivirus decisions may depend on tokenization, surrounding context, file type, provider state, and signature updates.
Scanning the same sample (PowerUp.ps1, a public red-team script, 445,954 bytes) with both providers shows what a boundary is and how far two engines can be trusted to agree. AMSI in text mode:
Threat boundary scan: C:\Ops\Samples\PowerUp.ps1
SHA-256: 7abc87d9620aef493617a4fc1f823850f32fb26ca9ae0f3befeadb04971e0246
Engine: AMSI
Scan mode: Text
Initial status: Detected
Scans performed: 25
Provider probes: 25 (see -Verbose or the ProbeLog property for the full audit trail)
Duration: 22771.419 ms
Known clean prefix: 445953
Detected prefix: 445954
Boundary offset: 445953 (0x6CE01)
Unicode scalar index: 445953
UTF-16 code-unit idx: 445953
Stable: True
Confidence: High
Line number: 4586
Byte in line: 46
Target byte: 0A (10)
--- Source Context ---
4585 | Set-Alias Get-CurrentUserTokenGroupSid Get-ProcessTokenGroup
4586 | Set-Alias Invoke-AllChecks Invoke-PrivescAudit
^
Microsoft Defender in raw-byte mode, same file:
Engine: Defender
Scan mode: RawBytes
Initial status: Detected
Scans performed: 25
Duration: 15370.562 ms
Known clean prefix: 445951
Detected prefix: 445952
Boundary offset: 445951 (0x6CDFF)
Stable: True
Confidence: High
Signature: Trojan:Win32/Kepavll!rfn
Line number: 4586
Byte in line: 44
Target byte: 69 (105)
--- Hex Dump ---
0006CDBF 74 2D 50 72 6F 63 65 73 73 54 6F 6B 65 6E 47 72 t-ProcessTokenGr
0006CDCF 6F 75 70 0A 53 65 74 2D 41 6C 69 61 73 20 49 6E oup.Set-Alias In
0006CDDF 76 6F 6B 65 2D 41 6C 6C 43 68 65 63 6B 73 20 49 voke-AllChecks I
0006CDEF 6E 76 6F 6B 65 2D 50 72 69 76 65 73 63 41 75 64 nvoke-PrivescAud
0006CDFF 69 74 0A it.
Both engines converge on line 4586 - Defender's boundary falls inside the trailing it of Invoke-PrivescAudit, AMSI's on the newline that terminates the same line, two bytes later. Neither offset is "the signature": they are the earliest prefix each provider still flagged, and the two-byte disagreement is exactly the tokenization/context effect described above. Defender additionally names what it matched (Trojan:Win32/Kepavll!rfn); AMSI reports no signature name, which is why Invoke-OffsetThreatScanRegion and Get-OffsetDetectionTrigger exist to characterize an AMSI hit.
Both scans cost 25 provider probes for a ~436 KiB file - the bisection is logarithmic in file size, and every probe is recorded in ProbeLog.
See Threat scanning design for the provider contract and interpretation guidance, provider interface for the scanner contract and how to add a provider without touching the search core, threat-scanning provenance for implementation boundaries and attribution, and output schema for the versioned object contract.
Export-OffsetThreatReport turns one or more scan results into a self-contained Markdown or HTML report - per-file summary, provider/signature/engine metadata, the full ProbeLog audit trail, and warnings - for attaching to an engagement writeup. It reads results only and never re-scans, so it runs cross-platform. Add -IncludeIoc to fold a hash/entropy/PE indicator panel (the same data as Get-OffsetIOC) into each report entry, and -IncludeTrigger to add a detection-trigger analysis (see below) for every result with a boundary.
Invoke-OffsetThreatScan ./sample.ps1 -Engine AMSI -ScanMode Text -PassThru |
Export-OffsetThreatReport -Path ./report.html -Format Html
# Aggregate many scans into one report, with an indicators panel and trigger analysis per file:
$results | Export-OffsetThreatReport -Path ./engagement.md -IncludeIoc -IncludeTrigger
For corpus-scale reports, -IncludeIoc re-scans every file in PowerShell, which is slow. The companion native engine OffsetScan emits schema-identical IOC JSON far faster; point the report at it with -IocJsonPath and it sources each panel from that JSON (falling back to a live Get-OffsetIOC only for files absent from it):
offsetscan ioc ./corpus --recurse > ./ioc.json
$results | Export-OffsetThreatReport -Path ./engagement.md -IocJsonPath ./ioc.json
Invoke-OffsetThreatScanBatch expands files, directories, and wildcards into a file list, scans each (continuing past per-file failures), and returns one result per file. -Summary returns a flattened detection matrix; the full results pipe straight into the report generator. Provider scanning is Windows-only.
Invoke-OffsetThreatScanBatch ./payloads -Recurse -Engine AMSI |
Export-OffsetThreatReport -Path ./engagement.html -Format Html
Invoke-OffsetThreatScanBatch ./samples -Summary |
Format-Table File, DetectionPrefixLength, Confidence, ProbeCount
Compare-OffsetThreatResult diffs two scan results - for example the same file before and after a signature-definition update - and classifies the change (NewlyDetected, NoLongerDetected, BoundaryEarlier, BoundaryLater, BoundaryUnchanged, BothClean) with the boundary delta and changed fields.
$before = Invoke-OffsetThreatScan ./sample.ps1 -Engine Defender -PassThru
# ... update Defender signature definitions ...
$after = Invoke-OffsetThreatScan ./sample.ps1 -Engine Defender -PassThru
Compare-OffsetThreatResult -Reference $before -Difference $after
The prefix search finds the first detection boundary. Invoke-OffsetThreatScanRegion finds multiple independently-detectable regions by splitting the file into segments and scanning each in isolation through AMSI entirely in memory - nothing detected is written to disk, so Defender real-time protection is never triggered or reconfigured. Each hit is bisected within its segment to map the exact triggering boundary to an absolute file offset.
Invoke-OffsetThreatScanRegion ./payload.bin -SegmentCount 16 |
Select-Object -ExpandProperty DetectedRegions |
Format-Table SegmentIndex, StartOffset, EndOffset, AbsoluteBoundaryOffset, SignatureName
This reports regions that trigger on their own; it can miss signatures that only fire in full-file context or that straddle a segment boundary, so treat the regions as leads to confirm with Invoke-OffsetThreatScan and manual validation. AMSI (in-memory) is the only engine supported here - Defender file scanning would require writing detected content to disk.
A boundary tells you where detection flips; Get-OffsetDetectionTrigger tells you what is there. Because a prefix boundary is the last byte of the earliest detected prefix, the triggering content is a run ending at that offset. The command reports the PE section the boundary falls in, the entropy of the run up to it (plaintext vs packed/encoded), and the extracted strings ending at or straddling it, ranked by proximity - the candidate signature content - with a one-line interpretation. It reads bytes only and never re-scans, so it runs cross-platform on saved results.
Invoke-OffsetThreatScan ./flagged.ps1 -Engine AMSI -PassThru | Get-OffsetDetectionTrigger
# Or point it at a file and a known boundary directly:
Get-OffsetDetectionTrigger -FilePath ./sample.bin -BoundaryOffset 0x4A1 |
Select-Object Interpretation, Section, PreBoundaryEntropy -ExpandProperty CandidateStrings
"It was detected before and now it isn't" has three very different causes: the file changed, the signatures changed, or the provider is non-deterministic. Add-OffsetDriftEntry records append-only NDJSON snapshots - file SHA-256, status, boundary, signature name, and the local Defender signature/engine versions - and Get-OffsetDrift reads that history and attributes each change to the right cause.
# Record a snapshot over time (from a scan result, or directly):
Invoke-OffsetThreatScan ./sample.ps1 -Engine AMSI -PassThru | Add-OffsetDriftEntry
Add-OffsetDriftEntry -FilePath ./sample.ps1 -Status Detected -Engine AMSI -SignatureName 'Trojan:PowerShell/X'
# Later, explain what changed:
Get-OffsetDrift -FilePath ./sample.ps1 | Select-Object -ExpandProperty Transitions
Each transition is labelled: a SHA-256 change reads as a file modification; a status change with the file unchanged but the Defender signature version moved reads as signature drift; a status change with neither reads as a non-deterministic provider result. The journal defaults to %LOCALAPPDATA%\OffsetInspect\drift.ndjson; override it with -JournalPath.
Invoke-OffsetMutationTest answers a detection-engineering question: is a signature a brittle exact-literal match, or is it robust to common obfuscation? Given a sample that AMSI currently detects, it applies standard perturbations - case inversion, string-literal concatenation, comment insertion, whitespace injection - and re-scans each variant to report which classes neutralize detection. Everything happens in memory via AMSI's in-process interface; no variant is written to disk, so no evasive artifacts are produced and Defender real-time protection is not involved. The command refuses to run without -AuthorizedEngagement, and is intended only for samples you are authorized to test.
Invoke-OffsetMutationTest -FilePath ./flagged.ps1 -AuthorizedEngagement |
Select-Object RobustnessSummary -ExpandProperty Results
A result of, say, "brittle: neutralized by StringConcatenation, CommentInsertion" tells a defender the signature keys on a contiguous literal and should be broadened; it tells an authorized operator the same thing about a control's coverage.
Detecting a boundary tells you what the engine sees; -CaptureTelemetry tells you what the defender sees. It snapshots each accessible Windows telemetry log's high-water mark before the scan, then reports whether the action raised an alert, with what context, and which sources were blind - the "assume visibility, then validate it" question, answered with evidence.
$r = Invoke-OffsetThreatScan ./flagged.ps1 -Engine AMSI -CaptureTelemetry -PassThru
$r.Telemetry | Format-List AlertGenerated, CorrelationConfidence, Findings
$r.Telemetry.Alert | Format-List ThreatName, SeverityName, SourceName, ProcessName, DetectionUser
The Telemetry property (OffsetInspect.TelemetryCorrelation) reports:
AlertGenerated / Alert - whether a Microsoft Defender detection (event 1116/1117) was logged for the scan, and its context (threat name, severity, detection source, process, user).CorrelationConfidence - High only when the detection's source matches the provider and its process matches the scanning host, so a coincidental concurrent detection is never claimed; Medium on source alone; Low on neither.SourcesAccessible / SourcesUnavailable - which telemetry logs were readable and which were blind (Sysmon absent, the Security log requiring elevation). A visibility gap is itself a finding.Findings - plain-language conclusions: an alert with full context, an alert lacking a threat name, no telemetry at all, or a missing source.The primary source is the Microsoft Defender Operational log, readable without elevation; correlation is by event RecordId (monotonic and timezone-independent). Windows-only, and inert unless -CaptureTelemetry is passed.
Three cross-platform static-analysis commands support malware triage and compose with the offset core:
Get-OffsetEntropy - per-window Shannon entropy (bits/byte) to locate packed or encrypted regions; cross-reference the flagged windows with Invoke-OffsetThreatScanRegion detections.Get-OffsetString - printable ASCII and UTF-16LE strings with byte offsets; pipe offsets into Invoke-OffsetInspect for context.Get-OffsetPEInfo - PE machine/bitness, entry point, section table, imports and imphash, appended-overlay detection, and resource size, with -Offset mapping a byte offset to its section (.text, .rsrc, ...). Imphash uses the standard library.function MD5 and is verified byte-identical to pefile/VirusTotal - including special-library ordinal resolution, so an ordinal imported from ws2_32/wsock32/oleaut32 resolves to its real function name; every other ordinal import renders , exactly as pefile does.Get-OffsetEntropy ./sample.bin -HighOnly | Select-Object -ExpandProperty Windows
Get-OffsetString ./sample.bin -MinimumLength 6 | Where-Object Value -match 'http|\.dll'
Get-OffsetPEInfo ./sample.exe | Select-Object Machine, EntryPointHex, ImpHash, ImportedDllCount, HasOverlay, OverlaySize
Get-OffsetIOC ./sample.exe | Format-List
Invoke-OffsetYaraScan runs analyst-authored YARA rules and returns each match with its byte offset - complementing the AMSI/Defender detection-boundary view with signatures you control, and needing no antivirus installed (only the YARA engine, e.g. winget install VirusTotal.YARA). Offsets feed straight into the inspector.
Invoke-OffsetYaraScan ./sample.bin -RulePath ./rules/malware.yar |
ForEach-Object { Invoke-OffsetInspect $_.File $_.Offset -ContextLines 2 }
Invoke-OffsetClamScan scans a file with the ClamAV on-demand engine and returns a normalized result (Clean / Detected / Error, plus the signature name). Because clamscan loads its full signature database on every call, it is a single-file detector, not a boundary-search engine (that would require the clamd daemon). It needs ClamAV installed and its signature databases downloaded - freshclam will not run until a config file exists:
# One-time setup: create the freshclam config (remove the sample's "Example" line), then fetch databases.
Copy-Item "$env:ProgramFiles\ClamAV\conf_examples\freshclam.conf.sample" "$env:ProgramFiles\ClamAV\freshclam.conf"
(Get-Content "$env:ProgramFiles\ClamAV\freshclam.conf") -notmatch '^\s*Example\s*$' |
Set-Content "$env:ProgramFiles\ClamAV\freshclam.conf" # requires admin to write under Program Files
& "$env:ProgramFiles\ClamAV\freshclam.exe"
Invoke-OffsetClamScan ./sample.bin
Use -DatabasePath to point at a signature directory in a writable (non-admin) location, and -ClamScanPath if clamscan is not on PATH.
Invoke-OffsetInspect -PassThru returns OffsetInspect.Result objects containing:
Invoke-OffsetThreatScan -PassThru returns OffsetInspect.ThreatScanResult objects containing:
ProbeLog audit trail of every distinct provider probe (surfaced as ProbeCount in CSV output, and exportable to a JSON transcript with -ProbeLogPath); see output schema.OffsetInspect.Result context at the mapped boundary.The v1-style implementation reread and decoded a complete file for every offset. Version 2 groups work by file:
Previous approach: approximately O(file size × offset count)
Version 2: approximately O(file bytes scanned once + requested windows)
Source mapping uses a streaming state machine and retains only the previous/following line descriptors required for requested offsets. Extremely long individual lines are displayed through a bounded preview controlled by -MaxLineBytes.
OffsetInspect.ps1 Thin offset-inspection CLI wrapper
OffsetThreatScan.ps1 Thin threat-scan CLI wrapper
module/OffsetInspect/ Complete Gallery package
OffsetInspect.psd1
OffsetInspect.psm1
OffsetInspect.Format.ps1xml
Public/
Private/
tests/ Pester tests
benchmarks/ Reproducible performance harness
build/ Validation, packaging, signing, publishing
.github/workflows/ CI, dependency review, release publishing
docs/ Architecture, schemas, provider design, release checklist
Install the pinned validation tools:
Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser
Install-Module PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser
Run the complete local gate:
./build/Test-Module.ps1
Run the deterministic benchmark harness:
./benchmarks/Measure-OffsetInspect.ps1 -FileSizeMiB 64 -OffsetCount 5000
Benchmark results vary by storage, host load, PowerShell edition, and file shape. Record those inputs when comparing commits.
Build a deterministic release archive and SHA-256 file:
./build/New-ReleasePackage.ps1
CI validates PowerShell 7 on Windows and Linux, Windows PowerShell 5.1, PSScriptAnalyzer, isolated module packaging, and the release archive. Release maintainers should also follow the release checklist.
OffsetInspect is intended for authorized defensive research, detection engineering, reverse engineering, malware analysis, and security testing. Threat-provider functions analyze content but do not disable, bypass, or reconfigure endpoint protections.
Invoke-OffsetMutationTest generates detection-evasion variants for signature-robustness assessment. It operates entirely in memory (no variant is written to disk), and refuses to run without the explicit -AuthorizedEngagement acknowledgement. Use it only against samples and controls you are authorized to test.
Review SECURITY.md before reporting a vulnerability. Do not submit sensitive samples through public GitHub issues.
OffsetInspect is released under the MIT License.
-CaptureTelemetry): whether a Microsoft Defender alert was raised, with what context, and which telemetry sources were blind - encoding the "assume visibility, then validate it" principle. Read-only, non-admin, Windows-only.Get-OffsetSignature): using the platform's real trust validation, reports whether a file is validly signed and trusted, who signed it, and whether it is embedded- or catalog-signed - a signer signal that complements imphash and the build-toolchain fingerprint (imports vs toolchain vs signer). Windows-only.| Command | Purpose | Platform |
|---|
Invoke-OffsetInspect | Map byte offsets to source/binary context, hex, and comparison | Cross-platform |
Invoke-OffsetThreatScan | AMSI/Defender detection-boundary search for one file | Windows |
Invoke-OffsetThreatScanBatch | Scan a corpus of files; -Summary returns a detection matrix | Windows |
Invoke-OffsetThreatScanRegion | Multi-region discovery via in-memory AMSI (no disk writes) | Windows |
Invoke-OffsetMutationTest | Signature-robustness testing: perturb a detected sample in memory, report which transforms evade (authorized use only) | Windows |
Compare-OffsetThreatResult | Diff two scan results (e.g. across signature-definition updates) | Cross-platform |
Get-OffsetDetectionTrigger | Correlate a detection boundary to the content that most likely triggered it | Cross-platform |
Add-OffsetDriftEntry | Record a detection snapshot (file hash + Defender signature version) to a journal | Cross-platform² |
Get-OffsetDrift | Explain how a file's detectability changed: file change vs signature update vs non-deterministic | Cross-platform |
Export-OffsetThreatReport | Render scan results into a Markdown/HTML engagement report | Cross-platform |
Invoke-OffsetYaraScan | Match a file against YARA rules; return hits with byte offsets | Cross-platform¹ |
Invoke-OffsetClamScan | Scan a file with the ClamAV engine; normalized detection result | Cross-platform¹ |
Get-OffsetEntropy | Per-window Shannon entropy to locate packed/encrypted regions | Cross-platform |
Get-OffsetString | Extract ASCII/UTF-16LE strings with byte offsets | Cross-platform |
Get-OffsetPEInfo | PE headers, sections, imports/imphash, overlay, offset→section | Cross-platform |
Get-OffsetIOC | Consolidated indicator panel: hashes, entropy, PE/imphash, strings | Cross-platform |
Get-OffsetSignature | Authenticode signature: is it validly signed and trusted, by whom, embedded vs catalog | Windows |
512 |
| Decimal 512 |
0x200 or 0X200 | Hexadecimal 0x200 |
200h | Hexadecimal 0x200 |
E1AB1 | Unprefixed hexadecimal because it contains A-F |
| Mode | Behavior |
|---|
Auto | Detects UTF-8/UTF-16 BOMs; otherwise uses UTF-8 |
Default | Uses the host operating system default encoding |
UTF8 | UTF-8 source mapping |
UTF16LE | Little-endian UTF-16 source mapping |
UTF16BE | Big-endian UTF-16 source mapping |
ASCII | ASCII source mapping |
ordNNNGet-OffsetIOC - one-shot indicator panel combining the above: MD5/SHA-1/SHA-256 (single-pass), overall entropy, printable-string count, and PE machine/imphash/overlay when applicable.