.NET CLR Profiling: how to find values for ldstr while profiling powershell

PARTH GUPTA 20 Reputation points
2025-10-23T07:30:57.9233333+00:00

Hi,

I am exploring how to profile .NET CLR to monitor powershell execution (like an AV product). I want to monitor all commands entered to powershell before they get executed (without using AMSI). My plan is to use the profiling API and use functions like JITCompilationStarted to extract IL code of the entered command. Following is my general method:

  1. Set environment variables in a cmd window (as required for profiling)
  2. Implement JITCompilationStarted in profiler DLL and get FunctionID
  3. Use that FunctionID with GetFunctionInfo and GetMethodName to get className and methodName
  4. Use GetILFunctionBody to get pointer to IL of the function
  5. Parse the IL to look for opcode 0x72 (ldstr)
  6. Run GetUserString from IMetaDataImport on the string token to get the string.

How ever, when i test it using a simple command, like

  1. powershell.exe -c "Write-Host 'HelloWorld'"
  2. powershell.exe -c "ping example.com"

I see a lot of IL code (which contain some LDSTR) but no ldstr containing the target string HelloWorld or example.com.

BTW I am implementing ICorProfilerCallback3 and ICorProfilerInfo3

Could you please guide me what i am doing wrong? I have attached the output and code here in a single file.

output.txt

Developer technologies | .NET | .NET CLI
0 comments No comments
{count} votes

Answer accepted by question author
  1. Varsha Dundigalla(INFOSYS LIMITED) 2,700 Reputation points Microsoft External Staff
    2025-10-23T12:13:45.36+00:00

    Thank you for reaching out.

    The absence of the command text (e.g., "ping google.com") in your IL disassembly is expected because of how PowerShell processes scripts. When you run powershell.exe -c "ping google.com", the command string is parsed by PowerShell and converted into an Abstract Syntax Tree (AST) before execution. This means:

    • The script text is not embedded as a literal in the JIT-compiled IL for methods like FuncCallInstruction.Run or ActionCallInstruction.Run.
    • IL instructions such as ldstr only represent hardcoded string literals inside the method body. Dynamic content like user-entered commands is passed as arguments at runtime, not compiled into IL.

    Your IL dump shows dynamic invocation logic (callvirt, ldfld, Invoke) and DLR (Dynamic Language Runtime) operations, which handle execution dynamically rather than storing the script text in IL.

    Why IL Parsing Alone Cannot Capture PowerShell Commands

    The CLR Profiling API approach you implemented (GetILFunctionBody + scanning for 0x72 (ldstr)) works for static literals but fails for dynamic scenarios like PowerShell scripts. The command string is consumed by methods such as:

    • System.Management.Automation.PowerShell.AddScript(string script)
    • System.Management.Automation.PowerShell.AddCommand(string command)
    • System.Management.Automation.PowerShell.Invoke()

    These methods receive the script text as arguments, not as IL constants. Therefore, ResolveStringToken will never return "ping google.com" because the token refers only to compile-time literals.

    Workaround: Capture Arguments Instead of IL

    To monitor PowerShell commands effectively, you need to inspect method arguments at runtime rather than relying on IL parsing. The CLR Profiling API provides a mechanism for this:

    Enable Enter/Leave Callbacks
    Use COR_PRF_MONITOR_ENTERLEAVE and implement FunctionEnter3WithInfo to intercept method calls.

    Retrieve Argument Values
    Use GetFunctionEnter3Info to access argument metadata and object IDs. For string arguments, extract the actual text using GetStringLayout2 or by dereferencing the object ID.

    Target Specific Methods
    Focus on PowerShell APIs where script text is passed:

    • PowerShell.AddScript
      • PowerShell.AddCommand
      • PowerShell.Invoke

    This approach captures the raw command text before execution, similar to AMSI but without relying on AMSI.

    By intercepting method entry and reading arguments, you capture the script text before execution, regardless of whether the script is inline (-c) or loaded from a file. This avoids parsing complex IL that does not contain the command text.

    Please let us know if you require any further assistance, we’re happy to help.

    If you found this information useful, kindly mark this as "Accept Answer".


0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.