Context of this Entry

The context comes from Szabolcs Schmidt Tweet, where he shared a zip file named setup.pkg, that contains three files AsusMouseDriver.sys, Inx, kg.cmd: samples-list


Things to Setup : kg.cmd

stg1-kg-cmd

Turns out Inx is a legit command line RAR utility that’ll extract contents from AsusMouseDriver.sys rar archive file in C:\Users\Public\WindowsSecurity folder.

Then gonna setup an AutoRun Key named UpdateService with command line set to execute ntoskrnl.exe with arguments being .\Lib\image, dcal143

Followed by running it immediately and displaying a decoy PDF created by thatgirlpossesed4773: the-decoy-pdf

AsusMouseDriver.sys RAR archive contains legit files for Python3.10.11 environment like renamed python executable to ntoskrnl.exe and supported DLLs, asus-rar-contents with only impostor Lib\image python file being ran with argument dcal143.


The Loader: Lib\image

stg2-python-loader

(On the Left) we see image being python file, executes marshaled code-object after some b85 decode and bz2, zlib decompression on a huge buffer.

(On the Right) we’ll make a PYC file by prepending Python3.10 16-byte magic header to the marshaled code-object to further make sense of it,

The huge 74MB size, shows indication of obfuscation by junk insertion.

Trying to decompile the PYC file using pycdc or pylingual fails due to heavy variable name obfuscation and intentional use of opcodes that hinder decompilation, likely some protector is used: stg2-pycdc-failed-output

Going through the pycdas disassembly output, after replacing the python co_varnames, co_names to placeholders, we can already make some guesses.

stg2-co-varnames-pyc-1 Interesting variable and function names like try_request_server, get_bytecodes, decrypt_code, rc4, base64, bz2, zlib, requests somewhat hints the behavior of this stage.

stg2-dis-import-pyc-1 Starting off with enough imports, it imports libraries like sys, ctypes, marshal, zlib, base64, bz2, inspect, requests.

stg2-dis-hide-pyc-1

  • (684-692): id = sys.argv[1], means dcal143 is an id
  • (694-714): ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0), this will hide the console window

stg2-dis-request-server-pyc-1

  • (2734-2736): declares the RC4 decryption KEY = b'phuongmai2005'
  • (2738-2750): creates a function try_request_server() that takes an argument as ip_server.

stg2-dis-exec-pyc-1

  • (4446-4460): exec(marshal.loads(get_bytecodes(b64code)), execute the loaded marhsal code-object

TL;DR This stage will try to execute a marshal code-object fetched from remote URL, after some Base64 decoding, Zlib decompression, and RC4 decryption.

As part of obfuscation, string constants are constructed dynamically with lots of lambda expression, which makes it kinda time taking to know the requested URL .

But some mock runs gives the payload URL cause python will complain when ran with disabled network access: sstg2-mock-run-show-link

Going to the URL shows an active campaign, which they’re constantly updating with scripts, as of writing, most recent addition is dcal143v2.txt: opendir-link

stg2-dcal143-decryption

(On the Left-Bottom) We see marshal bytes, then we do same as previous ie. prepend magic header to form a new PYC file, then dump to further analyse.


The Injector : dcal143 Python Marshal-Object

Again this marshal code-object is obfuscated, and performs anti-decompilation measure.

Going through the variable and function names hints possible process hollowing behavior: stg3-co-var-pyc-2

Walking the disassembly, we see it defines many helper functions: stg3-dis-functions-pyc-2

  • (8214-8222): defines NtWriteVirtualMemory function with argument and return types, for later use:
NtWriteVirtualMemory = ntdll.NtWriteVirtualMemory
 
NtWriteVirtualMemory.argtypes = [ wt.HANDLE, LPVOID, c_void_p, c_size_t, POINTER(c_size_t) ]
NtWriteVirtualMemory.restype = c_int
  • (8224-8254) : defines function like killprocessbyid, runpe, shc_loader and the names are self explanatory

stg3-dis-rc4-key-pyc-2

  • (before-8366): base64_encrypted_pe payload is constructed
  • (8368-8370): declares RC4 decryption KEY = b'buiphuonglinh'

Summarizing the following ‘cause the disassembly was too long: It’ll take a look at the current running processes then store them to process_list variable, following is the manual reconstruction of disassembly snippet:

process_list = subprocess.run(
    ['tasklist'],     # string constructed dynamically
    stdout=PIPE,
    text=True,
    creationflags=CREATE_NO_WINDOW
).stdout

then will eventually compare’em against many antivirus process strings, if found then exits immediately.

stg3-dis-run-pe-call-pyc-2

  • (8478-8482): calls run_pe(base64_encrypted_pe, KEY) that’ll inject the next payload to a remote process
  • (8488-8492): then sleeps for a while time.sleep(20)

stg3-dis-shc-call-pyc-2

  • (8634-8638) : shc_loader(base64_encrypted_shc, KEY) same goes to shellcode loader

TL;DR This stage will eventually inject next stage to a remote process, plus inject and run the shellcode to the same process.

We can get hold of Base64 encrypted shellcode and PE by performing some python injection using a tool called PyInjector: stg3-python-injection-on-pyc-2 This will write the contents of base64_encrypted_pe and base64_encrypted_shc variables the was constructed dynamically to external files.

run_pe : PE Injection into cvtres.exe

Starts system utility cvtres.exe in this case, cross-check its architecture, then uses combination of CreateProcessA, GetThreadContext, NtUnmapViewOfSection, VirtualAllocEx, NtWriteVirtualMemory, SetThreadContext API calls to perform injection: stg3-pe-injection-cvtres

Now when decoded and RC4 decrypted the content of base64_encrypted_pe, we get to the next stage which is .NET Reactor protected: stg3-dotnet-decryption

shc_loader: Injection to same process

This function uses VirtualAlloc, CreateThread, RtlMoveMemory, WaitForSingleObject, ResumeThread API calls to inject the shellcode in the same process, the x64dbg->Dump1 in following snap shows the shellcode being ran: stg3-shc-injection

Again when decode and decrypted, we see this shellcode is generated using an open source tool donut: stg3-shc-decryption


The Donut Shellcode: Being Position-Independent

Taking a quick triage look with capa, we see this shellcode uses some aPLib data decompression, chaskey encryption and PEB access, all smells like donut innit? : stg3-shc-capa-out

The following routine from the shellcode is a part of Donut’s chaskey decryption routine that decrypts the next stage for in-memory execution as AppDomain: stg3-decomp-shc-1

stg3-decomp-shc-2 Above snippet bypasses Antimalware Scan Interface (AMSI) and Event Tracing for Windows (ETW) by patching related API like AmsiScanBuffer, AmsiScanString, EtwEventWrite, EtwEventUnregister, shows that the shellcode was generated with option -b (bypass) enabled (refer to donut github repo for more).


The Loader Again : Yxolp.exe The .Net Assembly

stg4-dnspy-main stg4-dnspy-main-2 This stage will invoke mlBlFRODqNj9jxsZ5wv.LrNv6COPJYQa5wBkwqU -> KU8OtPPFaB() method from .NET assembly that is retrieved from the resource section: stg4-dnspy-target-rsrc

The TripleDES key and IV are Base64 encoded: stg4-dnspy-key-iv

The method being invoked: stg4-dnspy-method-invoke

Following emit will TripleDES decrypt and Zlib decompress the Hxyez resource from this assembly, we get to last stage that is again .NET Reactor protected DLL file: stg4-rsrc-decryption


The PureRAT Ft. ProtoBuf : Mvgnd.dll

We’ll use NETReactorSlayer to get comparatively cleaner version, I’ll go through some of the workings in particular: stg5-dnspy-proto-stuff-1 This stage take use of Protocol Buffers (protobuf-net) for deserialization. One such buffer when deserialized reveals C2 server 151.242.170[.228] and ports 56001, 56002, 56003, 56004, 56005, 56007, along with another Base64 encoded buffer blob that decodes to an X509 certificate used for TLS pinning, followed by some other values (see right bottom of following snap): stg5-dnspy-proto-stuff-2

Most of the strings are not decrypted by .NETSlayer, but we get the spirit of these snipppets: stg5-dnspy-socket-setup above code performs socket setup with TLS pinning, for encrypted C2 communication,

stg5-dnspy-mutex-create Mutex creation used to make sure only one instance is running,

stg5-dnspy-registry-set does persistence via registry key addition with binary value,

stg5-dnspy-wmi-query WMI query prolly checking any antivirus product presence,

stg5-dnspy-screen-capture capture full screenshot then does some conversion to JPEG to store them as an array, to eventually send them to C2 servers,

stg5-dnspy-restart above snippet tries to keep itself running, no matter the exceptions occurred.

Also contains many more features like fingerprinting via GUID and system resources, capability to execute other plugins received from C2 server, looks for Crypto Wallet credentials from file system paths (%APPDATA%), and registry keys and so on.


The frequent use of deserialzed ProtoBuf object, along with other characteristics resembles of this being PureRAT/PXA Stealer (similarity found in other incidents). Many variable from python loader/injectors are in Vietnamese, but the C2 server is located in Singapore, It is possible that they’re using remote servers.

See ya in another post, till then have a nice time :)


IOC(s)

  • source : https://x.com/smica83/status/1976718339314516476
  • setup.pkg : 45f9b2a451141d50faf17513f0d78064716dc673a976e77da0bdf7fe27719106
  • https[:]//104.194.153[.]193/homepage/links?id=dcal143
  • Yxolp.exe .NET Assembly: 3de860c805c65fadae42459736cfebd92e4636c1964d8c21049b990d1c5f90f0
  • donut shellcode: 40a6d7433ca98739f07fd0658cf3ecdd4c8119d601308122fc78dce8a2e47812
  • Mvgnd.dll : 380b32e1c665993dcc4691bc1161ae5c44f94e7aeefe31adcd9acd83bc8313c2
  • C2 server : 151.242.170[.]228