MaliciousContentRules.psm1

[System.Collections.Generic.List[String]] $suspiciousTokenElements = @(
    ## Code compilation
    "Add-Type", "DllImport",

    ## Doing dynamic assembly building / method indirection
    "DefineDynamicAssembly", "DefineDynamicModule", "DefineType", "DefineConstructor", "CreateType",
    "DefineLiteral", "DefineEnum", "DefineField", "ILGenerator", "Emit", "UnverifiableCodeAttribute",
    "DefinePInvokeMethod", "GetTypes", "GetAssemblies", "Methods", "Properties",

    ## Suspicious methods / properties on "Type"
    "GetConstructor", "GetConstructors", "GetDefaultMembers", "GetEvent", "GetEvents", "GetField",
    "GetFields", "GetInterface", "GetInterfaceMap", "GetInterfaces", "GetMember", "GetMembers",
    "GetMethod", "GetMethods", "GetNestedType", "GetNestedTypes", "GetProperties", "GetProperty",
    "InvokeMember", "MakeArrayType", "MakeByRefType", "MakeGenericType", "MakePointerType",
    "DeclaringMethod", "DeclaringType", "ReflectedType", "TypeHandle", "TypeInitializer",
    "UnderlyingSystemType",

    ## Doing things with System.Runtime.InteropServices
    "InteropServices", "Marshal", "AllocHGlobal", "PtrToStructure", "StructureToPtr",
    "FreeHGlobal", "IntPtr",

    ## General Obfuscation
    "MemoryStream", "DeflateStream", "FromBase64String", "EncodedCommand", "Bypass", "ToBase64String",
    "ExpandString", "GetPowerShell",

    ## Suspicious Win32 API calls
    "OpenProcess", "VirtualAlloc", "VirtualFree", "WriteProcessMemory", "CreateUserThread", "CloseHandle",
    "GetDelegateForFunctionPointer", "kernel32", "CreateThread", "memcpy", "LoadLibrary", "GetModuleHandle",
    "GetProcAddress", "VirtualProtect", "FreeLibrary", "ReadProcessMemory", "CreateRemoteThread", 
    "AdjustTokenPrivileges", "WriteByte", "WriteInt32", "OpenThreadToken", "PtrToString",
    "FreeHGlobal", "ZeroFreeGlobalAllocUnicode", "OpenProcessToken", "GetTokenInformation", "SetThreadToken",
    "ImpersonateLoggedOnUser", "RevertToSelf", "GetLogonSessionData", "CreateProcessWithToken",
    "DuplicateTokenEx", "OpenWindowStation", "OpenDesktop", "MiniDumpWriteDump", "AddSecurityPackage",
    "EnumerateSecurityPackages", "GetProcessHandle", "DangerousGetHandle",

    ## Crypto - ransomware, etc.
    "CryptoServiceProvider", "Cryptography", "RijndaelManaged", "SHA1Managed", "CryptoStream",
    "CreateEncryptor", "CreateDecryptor", "TransformFinalBlock", "DeviceIoControl", "SetInformationProcess",
    "PasswordDeriveBytes", 

    ## Keylogging
    "GetAsyncKeyState", "GetKeyboardState", "GetForegroundWindow",

    ## Using internal types
    "BindingFlags", "NonPublic",

    ## Changing logging settings
    "ScriptBlockLogging", "LogPipelineExecutionDetails", "ProtectedEventLogging"
)
[System.Collections.Generic.HashSet[String]] $suspiciousTokens = 
    New-Object 'System.Collections.Generic.HashSet[String]' $suspiciousTokenElements, ([StringComparer]::OrdinalIgnoreCase)


######################################################################
##
## Rules
##
######################################################################

<#
.DESCRIPTION
    Finds instances of dynamic method invocation, which can be used to hide
    or obscure the name of a .NET method being invoked.
#>

function Measure-DynamicMethodInvocation
{
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst]
        $ScriptBlockAst
    )

    ## Finds MemberExpressionAst nodes that don't invoke a constant expression
    [ScriptBlock] $predicate = {
        param ([System.Management.Automation.Language.Ast] $Ast)

        $targetAst = $Ast -as [System.Management.Automation.Language.InvokeMemberExpressionAst]
        if($targetAst)
        {
            if(-not ($targetAst.Member -is [System.Management.Automation.Language.ConstantExpressionAst]))
            {
                return $true
            }
        }
    }

    $foundNode = $ScriptBlockAst.Find($predicate, $true)
    if($foundNode)
    {
        [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{
            "Message"  = "Possible method obfuscation found via dynamic method invocation: " + $foundNode.Extent
            "Extent"   = $ScriptBlockAst.Extent
            "RuleName" = "MaliciousContent.DynamicMethodInvocation"
            "Severity" = "Warning" }
    }
}

<#
.DESCRIPTION
    Finds instances of access to use of the [Type] class, which can be used to hide
    or obscure access to unsafe types.
#>

function Measure-DynamicTypeUsage
{
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst]
        $ScriptBlockAst
    )

    ## Finds TypeExpressionAst that accesses the [Type] class
    [ScriptBlock] $predicate = {
        param ([System.Management.Automation.Language.Ast] $Ast)

        $targetAst = $Ast -as [System.Management.Automation.Language.TypeExpressionAst]
        if($targetAst)
        {
            if([Type] -eq $targetAst.TypeName.GetReflectionType())
            {
                return $true
            }
        }
    }

    $foundNode = $ScriptBlockAst.Find($predicate, $true)
    if($foundNode)
    {
        [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{
            "Message"  = "Possible type obfuscation found via type cast: " + $foundNode.Extent
            "Extent"   = $ScriptBlockAst.Extent
            "RuleName" = "MaliciousContent.DynamicTypeCast"
            "Severity" = "Warning" }
    }
}

<#
.DESCRIPTION
    Finds instances of conversions to the [Type] class, which can be used to hide
    or obscure access to unsafe types.
#>

function Measure-DynamicTypeConversion
{
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst]
        $ScriptBlockAst
    )

    ## Finds ConvertExpressionAst that converts to the [Type] class
    [ScriptBlock] $predicate = {
        param ([System.Management.Automation.Language.Ast] $Ast)

        $targetAst = $Ast -as [System.Management.Automation.Language.ConvertExpressionAst]
        if($targetAst)
        {
            if([Type] -eq $targetAst.Type.TypeName.GetReflectionType())
            {
                return $true
            }
        }
    }

    $foundNode = $ScriptBlockAst.Find($predicate, $true)
    if($foundNode)
    {
        [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{
            "Message"  = "Possible type obfuscation found via type conversion: " + $foundNode.Extent
            "Extent"   = $ScriptBlockAst.Extent
            "RuleName" = "MaliciousContent.DynamicTypeConversion"
            "Severity" = "Warning" }
    }
}

<#
.DESCRIPTION
    Finds instances of dynamic member access, which can be used to hide
    or obscure the name of a .NET member (i.e.: property) being accessed.
#>

function Measure-DynamicMemberAccess
{
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst]
        $ScriptBlockAst
    )

    ## Finds MemberExpressionAst that uses a non-constant member
    [ScriptBlock] $predicate = {
        param ([System.Management.Automation.Language.Ast] $Ast)

        $targetAst = $Ast -as [System.Management.Automation.Language.MemberExpressionAst]
        $methodAst = $Ast -as [System.Management.Automation.Language.InvokeMemberExpressionAst]
        if($targetAst -and (-not $methodAst))
        {
            if($targetAst.Member -is [System.Management.Automation.Language.ConstantExpressionAst])
            {
                ## See if this is static member access on a variable
                $typeAst = $targetAst.Expression -as [System.Management.Automation.Language.TypeExpressionAst]
                if($targetAst.Static -and (-not $typeAst))
                {
                    return $true
                }

                ## Otherwise, this is just constant access and will be validated by
                ## content matching
                return $false
            }
            else
            {
                ## This is not constant access, therefore suspicious
                return $true
            }
        }
    }

    $foundNode = $ScriptBlockAst.Find($predicate, $true)
    if($foundNode)
    {
        [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{
            "Message"  = "Possible member obfuscation found via dynamic member access: " + $foundNode.Extent
            "Extent"   = $ScriptBlockAst.Extent
            "RuleName" = "MaliciousContent.DynamicMemberAccess"
            "Severity" = "Warning" }
    }
}

<#
.DESCRIPTION
    Finds instances of suspicious strings or tokens.member access, which can be used to hide
    or obscure the name of a .NET member (i.e.: property) being accessed.
#>

function Measure-SuspiciousContent
{
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst]
        $ScriptBlockAst
    )

    ## Get all of the sub-ASTs, and sort by length (so that we can get the specific matches).
    $allAsts = $ScriptBlockAst.FindAll( { $true }, $true) | Sort { $_.Extent.Text.Length }

    ## A list of ASTs that we've reported on, so that we don't need to report on its
    ## parent.
    $reportedAsts = @()

    foreach($ast in $allAsts)
    {
        $foundSuspiciousContent = $false
        $suspiciousContent = ""

        ## Quick test to see if the token text has an exact match
        if($suspiciousTokens.Contains($ast.Extent.Text))
        {
            $foundSuspiciousContent = $true
            $suspiciousContent = $ast.Extent.Text
        }
        else
        {
            ## Otherwise, substring search
            foreach($suspiciousToken in $suspiciousTokens)
            {
                if($ast.Extent.Text.IndexOf($suspiciousToken, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)
                {
                    $foundSuspiciousContent = $true
                    $suspiciousContent = $suspiciousToken
                }
            }
        }

        if($foundSuspiciousContent)
        {
            ## If this AST is a parent of an already reported AST,
            ## don't report the parent.
            foreach($reportedAst in $reportedAsts)
            {
                $reportedAstParent = $reportedAst
                do
                {
                    if($reportedAstParent -eq $ast)
                    {
                        $foundSuspiciousContent = $false
                    }

                    $reportedAstParent = $reportedAstParent.Parent
                } while($reportedAstParent -and $foundSuspiciousContent)

                if(-not $foundSuspiciousContent)
                {
                    break
                }
            }
        }

        if($foundSuspiciousContent)
        {
            $reportedAsts += $ast

            [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord] @{
                "Message"  = "Possible malicious script content: " + $suspiciousContent
                "Extent"   = $ast.Extent
                "RuleName" = "MaliciousContent.SuspiciousContentText"
                "Severity" = "Warning" }
        }
    }
}