Welcome back!
In Chapter 2: AST Transformation & Normalization, we learned how to clean up the raw data from PowerShell into a nice, tidy format. We can now easily read the command name and its arguments.
But here is the scary part: Knowing the command name is not enough.
This chapter introduces Security Pattern Detection. This is the layer that looks inside the arguments to find hidden dangers.
Imagine you have a security rule that allows the command Write-Host (which just prints text to the screen).
A malicious user sends this:
Write-Host "Current Date: $(Get-Date)"
This looks innocent. But look at $(...). That syntax tells PowerShell: "Stop what you are doing, execute the code inside these parentheses, and put the result here."
If the user writes:
Write-Host "Hacked: $(Invoke-Expression 'format C:')"
If we only look at the command name (Write-Host), we would allow this. But hidden inside the argument is a command that wipes the drive!
We need a system that acts like an Airport X-Ray Scanner.
We don't care what command is running. We care if the syntax uses specific "shapes" that allow code execution.
We look for specific structures in the AST that indicate complexity or danger.
$(...)This allows code to run inside strings.
SubExpression, ParenExpression.{ ... }A script block is a chunk of code wrapped in curly braces, passed as an argument.
Invoke-Command -ScriptBlock { Remove-Item C:\Important }
ScriptBlock.:: or .This happens when a user tries to access .NET functionality directly.
[System.Math]::Sqrt(64)
MemberInvocation.@paramsSplatting allows a user to store arguments in a variable and pass them all at once.
$params = @{ Path = "C:\"; Recurse = $true }
Get-ChildItem @params
$params.Variable (with isSplatted flag).Here is how our engine scans the command.
We don't need to write complex Regular Expressions to find these patterns. Because we used the AST Bridge (Chapter 1), PowerShell has already told us what everything is!
We just need to loop through the AST and fill out a checklist.
First, we define a simple interface representing our "X-Ray Results".
// parser.ts
type SecurityFlags = {
hasSubExpressions: boolean // $(...)
hasScriptBlocks: boolean // { ... }
hasSplatting: boolean // @params
hasMemberInvocations: boolean // [Math]::Sqrt()
// ... other flags
}
We write a function deriveSecurityFlags. It takes the parsed command and walks through every element.
// parser.ts
export function deriveSecurityFlags(parsed: ParsedPowerShellCommand): SecurityFlags {
// 1. Initialize all flags to false (Safe until proven otherwise)
const flags = {
hasSubExpressions: false,
hasScriptBlocks: false,
hasSplatting: false,
hasMemberInvocations: false,
// ...
}
// 2. Loop through every statement and command
for (const statement of parsed.statements) {
for (const cmd of statement.commands) {
// Check the elements inside this command
checkElements(cmd, flags)
}
}
return flags
}
Explanation: We start with a clean slate. We loop through every part of the command structure we built in Chapter 2.
Inside checkElements, we look at the elementTypes array we created during normalization.
// parser.ts (Simplified)
function checkElements(cmd: ParsedCommandElement, flags: SecurityFlags) {
// If we don't know the types, stop.
if (!cmd.elementTypes) return
for (const type of cmd.elementTypes) {
if (type === 'SubExpression') {
flags.hasSubExpressions = true
}
if (type === 'ScriptBlock') {
flags.hasScriptBlocks = true
}
if (type === 'MemberInvocation') {
flags.hasMemberInvocations = true
}
}
}
Explanation: This is the core logic. If the AST says an argument is a
ScriptBlock, we immediately raise thehasScriptBlocksflag. It is simple, fast, and accurate.
Let's see how this protects us.
Input Command:
Write-Host "Result: $(Get-Date)"
1. Normalization (Chapter 2 Output):
{
name: "Write-Host",
args: ["Result: ", "$(Get-Date)"], // Looks like strings
elementTypes: ["StringConstant", "SubExpression"] // Wait!
}
2. Pattern Detection (Chapter 3 Output):
const securityFlags = {
hasSubExpressions: true, // FLAGGED!
hasScriptBlocks: false,
hasMemberInvocations: false
}
3. The Decision:
Because hasSubExpressions is true, our security engine can say:
"This command is dynamic. I cannot predict what it will do statically. I must deny it (or ask the user for confirmation)."
In this chapter, we learned how to perform X-Ray scans on commands.
We moved beyond just reading the command name. We now analyze the structure of the arguments to detect potential code injection, obfuscation, or complex logic that makes static analysis impossible.
But wait! What if the user types Get-ChildItem C:\Windows? That is a static path.
What if they type Get-ChildItem $MyPath? That is a variable.
How do we tell the difference between a safe, static string and a variable? We need to know exactly what the arguments are.
In the next chapter, we will learn how to extract these static values safely.
Next: Chapter 4 - Static Prefix Extraction
Generated by Code IQ