In this section

0.3 How PowerShell Is Built

Module 0

Introduction

Lesson 0.1 showed that PowerShell passes objects instead of text. This lesson looks at what those objects are and where they come from, because almost every surprise a newcomer has with PowerShell, a sort that comes out wrong, a filter that finds nothing after a table, a date that will not compare, comes from not knowing what kind of thing is in hand.

PowerShell is built in layers. Each layer is a separate piece of software with its own job, and they are put together differently in different places, which is why PowerShell can run in a console, inside VS Code, inside another program, or with no window at all. Underneath is .NET, Microsoft's programming platform, which supplies every type of value.

On top of it is the PowerShell engine: the language, the pipeline, the commands. Around the engine is a host, the program you type into. And at the very end of every command, a formatting step turns objects into text for a person to read. Each layer explains something an analyst needs.

None of this is theory for its own sake. Each layer shows up in evidence and in mistakes. The .NET layer is why an attacker's script can call the network and the Windows API without any extra tools.

The host layer is why some commands work in your console and fail in a scheduled task. The formatting layer is why a report written with Format-Table loses half its data. Knowing which layer you are dealing with makes each of those quick to diagnose.

How PowerShell is built .NET types, methods, the runtime The PowerShell engine language, pipeline, commands, providers A host console, VS Code, a JEA endpoint Formatting text, only at the end Objects all the way through; text only when a person needs to read the result.

By the end of the lesson you can name the .NET type of a value and say why it matters, explain the difference between the engine and the host, use an object's properties and methods, avoid the errors that come from comparing values of the wrong type, calculate with dates, keep data as objects until the end of a pipeline, and use providers to reach things that are not files as if they were.

The evidence is the same as in Lesson 0.1: NE-SHARMA-LT's processes at 13:00 on 9 March, as PowerShell exported them and as tasklist printed them. Everything runs live.

1

Built on .NET

every value has a type

Every value in PowerShell, from a single number to a whole process record, is a .NET object of some type. GetType() says which. For the user-owned rundll32 process from the evidence, and three of its properties:

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> $x = $p | Where-Object ProcessId -eq 7488
PS C:\Evidence> $x.GetType().FullName
System.Management.Automation.PSCustomObject
PS C:\Evidence> $x.ProcessId.GetType().FullName
System.Int64
PS C:\Evidence> $x.CreationDate.GetType().FullName
System.DateTime
PS C:\Evidence> $x.CommandLine.GetType().FullName
System.String

The process is a PSCustomObject, PowerShell's general-purpose object, which ConvertFrom-Json builds from each JSON record.

PSCustomObjects are also what scripts build with [pscustomobject]@{...}, as every lesson in Module 11 did, so a result you make and a record you read are the same kind of thing. Its ProcessId is a System.Int64, a 64-bit whole number. Its CreationDate is a System.DateTime, because ConvertFrom-Json recognized the timestamp format and converted it. Its CommandLine is a System.String, the type of all text.

Those types are not decoration. A number compares and sorts as a number, a date subtracts to give a duration, and a string has methods for searching and cutting text. Knowing the type of a value tells you what you can do with it, and many of PowerShell's apparent oddities turn out to be a value of one type being treated as another.

Types also depend on where the data came from. The same process, read live on Windows with Get-CimInstance, arrives as a CIM instance with CreationDate as a DateTime already. Read from tasklist's CSV, every field is a string. Read from this JSON export, PowerShell made the date a DateTime because it recognized the format. The investigation's first job with any new source is finding out which of those it is.

.NET is also what makes PowerShell able to reach almost anything on Windows. Any .NET type can be used directly from PowerShell, with its full name in square brackets, as Module 11 did for cryptography and Module 10 for web requests. Most of the time cmdlets hide this, but it is always there underneath, and it is why there is rarely something PowerShell cannot do.

Attackers know this too. A PowerShell script can create a web client, decode Base64, load a DLL into memory or call Windows functions directly, all through .NET, with no tool to drop on disk. Module 7 reads scripts that do exactly that, and Constrained Language Mode, which Lesson 11.7 tested, works by cutting off most of this .NET access for code the machine does not trust.

2

The Engine and the Host

one language, many windows

The PowerShell engine runs the language: it parses what you type, runs commands, passes objects along the pipeline. The host is the program that gives the engine a place to receive input and show output. The automatic variable $Host describes it:

PS C:\Evidence> $Host.Name
ConsoleHost
PS C:\Evidence> $Host.Version.ToString()
7.6.6
PS C:\Evidence> $PSVersionTable.PSVersion.ToString()
7.6.6

This build runs the engine in ConsoleHost, the ordinary console host of pwsh, version 7.6.6. The host's version matches the engine's here because the console host ships with pwsh; a third-party host can report its own version, which is why $PSVersionTable, not $Host, is the reliable way to learn the engine's version.

In Visual Studio Code's integrated console the same engine reports a different host name, and a JEA endpoint, from Module 9, is a remote host with no console at all.

The language and the commands are the same in each. So is the evidence each produces: a command run in VS Code and one run in the console leave the same kind of script block log, distinguished only by the host recorded with it.

The distinction matters in two places.

Some features belong to the host rather than the engine: command-line editing and history come from the PSReadLine module the console loads, and they do not exist in a remote session or a scheduled script. And evidence records the host: PowerShell's own logs, which Module 7 reads, include a host name and application, which is often how an analyst tells an interactive session from a script launched by something else.

Lesson 0.4 looks at the hosts an analyst actually uses, the console, Windows Terminal and Visual Studio Code, and how to set each one up for this course. Lesson 0.8 then builds an analyst workstation around them.

3

Properties and Methods

what an object has and what it can do

Objects have properties, which hold data, and methods, which do something with it. The command line of the user-owned rundll32 is a string, so it has a string's properties and methods:

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> $x = $p | Where-Object ProcessId -eq 7488
PS C:\Evidence> $x.CommandLine.Length
85
PS C:\Evidence> $x.CommandLine.Split(' ')[-1]
C:\Users\p.sharma\AppData\Local\Temp\upd.dll,Start
PS C:\Evidence> $x.CommandLine.ToUpper().Contains('TEMP')
True
PS C:\Evidence> ($x | Get-Member -MemberType NoteProperty).Count
10

The command line is 85 characters long, a property every string has. Split(' ') cut it at spaces and [-1] took the last piece, the DLL path and its entry point. ToUpper().Contains('TEMP') tested whether it mentions Temp in any letter case, chaining two methods. The process itself carries ten data properties, which Get-Member listed.

Methods are often the shortest way to a precise answer, and they need no module or tool beyond PowerShell itself. Contains, StartsWith, EndsWith, Replace, Split and Substring cover most of what an analyst does to a string, and they behave exactly the same on a command line, a file path or a registry value.

Lesson 0.6 shows how to list every property and method an object has with Get-Member, which is how you discover them in the first place.

Methods and properties are reached with a dot, and they chain: each one returns another object with its own members. That is why a line like $x.CommandLine.ToUpper().Contains('TEMP') reads left to right as a series of small steps, each acting on the result of the one before.

String methods in .NET are case-sensitive by default, which is why the example upper-cased the command line before testing for TEMP. PowerShell's own operators, -like, -match and -eq, are case-insensitive by default, which suits Windows, where paths and names ignore case. Knowing which you are using avoids a search that misses Temp because it looked for TEMP.

4

Typed Values

when text pretends to be numbers

Values from text sources arrive as strings, even when they look like numbers. Import-Csv, which Lesson 0.1 used for tasklist's output, gives every column as a string, including PID. Sorting by it:

First trystrings
PS> Import-Csv ./tasklist-sharma.csv | Sort-Object PID | Select-Object -First 5
PIDs 0, 112, 1192, 1404, 1928: 112 before 4, because "1" sorts before "4".
Import-Csv gives every value as a string, so Sort-Object compares characters, not numbers. Nothing errors, and the list looks sorted.
Refinedcast to a number
PS> Import-Csv ./tasklist-sharma.csv | Sort-Object { [int]$_.PID } | Select-Object -First 5
PIDs 0, 4, 112, 388, 540, in numeric order.

The comparison underneath, and the fix in full:

PS C:\Evidence> '10' -lt '9'
True
PS C:\Evidence> 10 -lt 9
False
PS C:\Evidence> (Import-Csv ./tasklist-sharma.csv | Sort-Object { [int]$_.PID } | Select-Object -First 5).PID -join ', '
0, 4, 112, 388, 540

As strings, "10" is less than "9", because the comparison goes character by character and "1" comes before "9". As numbers, 10 is not less than 9. Casting each PID to [int] in the sort's script block put the processes in true numeric order: 0, 4, 112, 388, 540.

The first five are now the System Idle Process, System, Registry, smss.exe and the first csrss.exe, which is the order a Windows analyst expects, since the lowest IDs belong to the processes that start first.

This is the most common quiet error in evidence work. It is also one of the hardest to spot by eye, because the wrong order looks plausibly sorted at a glance, and the rows that are out of place are often the ones you were looking for.

Event IDs, process IDs, ports, byte counts and timestamps all arrive as text from CSV files, log lines and some APIs. Filters like -gt 1000 and sorts on those columns are wrong until the values are converted, and nothing warns you. When a value comes from text, convert it to its real type before comparing it.

The fix in the refined command used a script block in Sort-Object, {[int]$_.PID}, which computes the sort key for each object without changing the object itself. The rows still carry PID as text, so the tasklist record stays exactly as collected, while the sort uses the number. That pattern, compute the comparison, keep the evidence, recurs throughout the course.

PowerShell does convert automatically in one direction: when the left side of a comparison is a number, the right side is converted to a number. 10 -lt '9' is False, as numbers. The trouble comes when the left side is a string, as every value from Import-Csv is, and the comparison is done as text.

5

Dates Are Values

arithmetic, not parsing

Timestamps are the backbone of every investigation, and in PowerShell a DateTime is a value you can calculate with. Two rundll32 processes started on NE-SHARMA-LT; how far apart?

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> $x = $p | Where-Object ProcessId -eq 7488
PS C:\Evidence> $y = $p | Where-Object ProcessId -eq 8012
PS C:\Evidence> ($y.CreationDate - $x.CreationDate).TotalSeconds
35
PS C:\Evidence> $x.CreationDate.ToString('yyyy-MM-dd HH:mm:ss')
2026-03-09 10:31:23

Subtracting one DateTime from another gave a TimeSpan, and its TotalSeconds property gave 35. A TimeSpan has TotalMinutes, TotalHours and TotalDays as well, so the same subtraction answers questions at any scale, from seconds between two processes to days between a phishing email and its first use.

ToString with a format string wrote the date in the order an analyst wants in a report, year first. No text was parsed at any step.

Thirty-five seconds between the user's copy and the SYSTEM copy is a small fact with a large meaning, which Module 6 develops: the SYSTEM copy did not start on its own; something created a way for it to run with more rights, moments after the first. Timelines in this course are built from exactly this arithmetic, sorting and subtracting DateTime values from every source.

DateTime also sorts correctly, compares with -lt and -gt, and offers methods such as AddHours and AddDays for building time windows, the "one hour either side" that starts most searches. Module 2 builds those windows for sign-in logs and Module 6 for event logs, always with DateTime values rather than strings that merely look like times.

Time zones are the one thing DateTime arithmetic will not decide for you. A DateTime knows whether it is UTC, local or unspecified, and comparing timestamps from sources in different zones without converting them first produces timelines that are hours wrong. This course keeps every timestamp in UTC, and Module 6 shows how to check which kind a source gives you.

6

Objects Until the End

and formatting last

Every command in a pipeline receives the objects the previous one produced, one at a time, as they are produced. Filtering, sorting and selecting all pass the same kind of object along. Formatting does not:

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> ($p | Where-Object Owner -like '*SYSTEM' | Sort-Object ProcessId | Get-Member).TypeName | Sort-Object -Unique
System.Management.Automation.PSCustomObject
PS C:\Evidence> ($p | Format-Table Name | Get-Member).TypeName | Sort-Object -Unique
Microsoft.PowerShell.Commands.Internal.Format.FormatEndData
Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
Microsoft.PowerShell.Commands.Internal.Format.GroupEndData
Microsoft.PowerShell.Commands.Internal.Format.GroupStartData

After Where-Object and Sort-Object, the pipeline still held process objects, PSCustomObjects with all their properties. Get-Member reports the type name of whatever reaches it, which makes it the quickest way to see what a pipeline is carrying at any point: put it after any command to check.

After Format-Table, it held five kinds of formatting record: start, group start, entry, group end and end. Those are internal instructions for drawing a table on screen, not processes, and any command after them sees no ProcessId, no Owner, nothing to filter.

That is the formatting layer at work. When a pipeline ends, PowerShell formats whatever objects are left and writes them as text to the host, which is why you see tables and lists on screen.

Format-Table and Format-List do that formatting early, on purpose, and their output is only good for display. Put them last, or not at all; never in the middle of a pipeline whose result you want to keep.

Select-Object is the command to use when you want fewer properties but still want objects. It returns new objects carrying only the properties you name, which sort, filter and export normally. Lesson 11.5 showed its one catch: anything after it can see only what it kept, so select late, after the commands that need the other properties have run.

The same reasoning explains why exporting works: Export-Csv and ConvertTo-Json receive objects and write every property. Formatted output sent to a file gives you a drawing of a table, with columns cut to the window's width and long values truncated with an ellipsis, which is exactly what you do not want in evidence.

7

Providers

drives that are not disks

PowerShell presents several stores of data as drives, through providers, so the same commands work on all of them. The providers and non-file drives in this build:

PS C:\Evidence> (Get-PSProvider).Name -join ', '
Alias, Environment, FileSystem, Function, Variable
PS C:\Evidence> (Get-PSDrive | Where-Object Provider -notlike '*FileSystem').Name -join ', '
Alias, Env, Function, Variable
PS C:\Evidence> Test-Path Env:PATH
True
PS C:\Evidence> Get-ChildItem Alias:gci | ForEach-Object Definition
Get-ChildItem

Five providers here, and the Alias, Env, Function and Variable drives, each browsable with Get-ChildItem like a folder: Test-Path found the PATH variable on the Env: drive, and the Alias: drive said gci stands for Get-ChildItem. On Windows there are two more that analysts use constantly: the Registry provider, which gives the HKLM: and HKCU: drives, and the Certificate provider, which gives Cert:, where Lesson 11.7 found the code-signing certificate.

The registry as a drive is the provider that matters most for investigations. Persistence, configuration, installed software, recently used files and much of what Module 5 and Module 8 look for live there, and all of it is reachable with the commands this course teaches for folders.

Get-ChildItem and Get-ItemProperty read run keys, services and installed software exactly as they read files and folders, which Module 5 uses throughout. The commands are the ones you already know; only the path changes.

Providers are also why paths in PowerShell sometimes look unusual to someone coming from other tools. HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run is a path, in the same sense as C:\Users, and commands like Test-Path, Get-Item and Get-ChildItem accept it. Evidence collected from the registry by PowerShell records those paths, which is how Module 5's exports and the Project's registry file name their keys.

The Env: drive is also how scripts read environment variables, such as $env:USERNAME and $env:APPDATA, which attacker scripts use constantly to build paths that work on any user's machine. The Project's second-stage script does exactly that, and recognizing the pattern is part of reading it.

8

The Layers, Checked

five facts from this lesson

The facts of this lesson, as checks: the process is a PSCustomObject, its CreationDate a DateTime, its ProcessId a 64-bit integer, strings compare as text while numbers compare as numbers, and the Environment provider is present:

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> $x = $p | Where-Object ProcessId -eq 7488
PS C:\Evidence> @(
>>     $x -is [System.Management.Automation.PSCustomObject]
>>     $x.CreationDate -is [datetime]
>>     $x.ProcessId -is [long]
>>     ("10" -lt "9") -and -not (10 -lt 9)
>>     (Get-PSProvider).Name -contains "Environment"
>> )
True
True
True
True
True

Five True. Each check is a single comparison that returns True or False, so the five lines together cost almost nothing to run and fail loudly the moment an assumption stops holding.

The -is operator tests a value's type directly, which is the cheapest way to confirm that data is what a script assumes before the script relies on it. A collection script that checks the types of what it collected catches a format change before it turns into a wrong answer.

Types are also what make the course's evidence files work across lessons. A process export written in Module 4, read in Module 11, gives back objects with the same property names and, where JSON allows, the same types. The investigation can pick up where it left off weeks later, on a different machine, without anything being retyped or reparsed.

None of this needs memorizing, and nobody types GetType() as often at the end of a course as at the start. The habit it builds is one question asked often: what type is this? GetType(), Get-Member and -is answer it in a moment, and most of the rest of the course becomes easier for having asked.

Treating PowerShell as a text shell
sort values read from text
cast them to the right type first"112" sorts before "4"
format, then filter
filter and sort, then format lastFormat-* emit formatting records, not data
parse a date out of a string
use the DateTime and its methodsarithmetic and formatting come with the type
Know the type of what you hold, keep objects through the pipeline, and format only for the reader.

Knowing the layers explains PowerShell's behavior; the next lesson sets up the place you will actually type it: the console, Windows Terminal and Visual Studio Code.

Practice

Do this Work with the types you hold
  1. Ask the type. Use GetType(), Get-Member and -is before relying on a value.
  2. Convert text. Cast values from CSV, logs and text to numbers or dates before comparing or sorting.
  3. Keep objects. Filter, sort and select first; format only at the end, and never format before exporting.
  4. Use providers. Read the registry, certificates and environment with the same commands you use for files.
What you should end up with: pipelines that compare, sort and calculate correctly on any evidence.

Then, in PowerShell on your own machine, pick any command, pipe it to Get-Member, and find one method you did not know the object had.

Next: the console, Windows Terminal and Visual Studio Code, and setting each one up.