A SQLite engine, database, and interface contained in a single PowerShell file using NTFS Alternate Data Streams.
⚠️ WARNING: This is for educational purposes only. It should not be considered production-ready, best-practice, etc. You should fully understand code before you run it on your system, and you should have authorization to run code on your system. The contents of this post and associated repository may trigger endpoint protection and antivirus, though the contents as published are not malicious. This little experiment uses SQLite, which is not affliated with me nor this project, and can be obtained from their website.
- Download
build.ps1 - Update
$exePathto reflect the location of your sqlite3.exe binary - Update
$targetScriptto reflect where you'd like themain.ps1script to be output - Run
.\build.ps1to build the script.
.\main.ps1 -Query "INSERT INTO TableName (Column1, Column2) VALUES ('Value1', 'Value2');"Doing this will also automatically create the table specified with the columns specified if the table does not already exist.
No output is provided upon successful insertion.
.\main.ps1 -Query "SELECT * FROM TableName"The output is a DatabaseRecord object (or array of DatabaseRecord objects, if multiple items are returned). These objects support normal PowerShell operations and pipeline functionality.
Example output:
ID Col1 Col2 CreationTime
-- ---- ---- ------------
1 Value1 Value2 12/31/2025 6:53:05 PM
These are the only things that stay on the disk permanently after the script finishes.
Script File (main.ps1):
Main stream: Contains the PowerShell code and the Base64-encoded SQLite engine.
Data stream: (main.ps1:Data): A hidden NTFS Alternate Data Stream containing the actual binary SQLite database file. This grows as you add rows.
Win32Ghost Type: Once the script runs in a session, the Win32Ghost .NET type is stored in the PowerShell process's memory (AppDomain).
When you strike Enter on a -Query command, the following artifacts appear. They are deleted when the query finishes.
Temporary Engine ($env:TEMP\sqlite_[GUID].exe):
A unique, randomly named executable. This is the binary that handles the SQL logic. It only lives for the duration of the Invoke-SQLiteQuery function.
Session Database ($env:TEMP\db_[GUID].db):
A temporary working copy of the database. SQLite requires a physical file to perform locking and writes. We extract the binary data from the ADS to this file, modify it, and then push the results back to the ADS.
If you check Task Manager or Get-Process at the exact right millisecond, you will see a process named sqlite_[GUID].exe running as a child of the PowerShell host.
Beyond just files, the system registers the following activities:
- Process Creation Events: If you have Audit Process Creation (Event ID 4688) or Sysmon enabled, the OS will log the start and stop of the GUID-named executable.
- Antimalware Scan Interface (AMSI): Windows Defender will scan the Base64 string as it is decoded in memory and scan the temporary .exe as it is written to the
$env:TEMPfolder. - NTFS Log Transactions: The filesystem logs the creation of the temp files and the update to the named data stream on the script.
| Artifact | Location | Persistence | Purpose |
|---|---|---|---|
| SQL Database | main.ps1:Data | Permanent | Reliable relational storage inside the script |
| Engine Binary | $env:TEMP\sqlite_*.exe | Ephemeral | Active processing of SQL commands |
| Working DB | $env:TEMP\db_*.db | Ephemeral | Required for SQLite's ACID compliance |
| Win32 Logic | Memory (RAM) | Session-based | High-speed I/O bridge to bypass PS overhead |
Please ⭐ star this repository if it is helpful. Constructive feedback is always welcome, as are pull requests. Feel free to open an issue on the repository if needed.
