XWorm Part 1 - Unraveling a Steganography-Based Downloader
Analyzing a multi-stage downloader that employs steganography to hide a .NET assembly.
Overview
Today, we’ll analyze a sample tagged as XWorm, sourced from Malware Bazaar, with a SHA-256 hash of 0fd706ebd884e6678f5d0c73c42d7ee05dcddd53963cf53542d5a8084ea82ad1
. This sample will be referred to as the first stage.
According to a recent AnyRun report1, XWorm is a remote access trojan (RAT) sold as a service, capable of exfiltrating files, stealing various application credentials, and maintaining remote access. It also states that XWorm is commonly delivered in multi-stage attacks, starting with phishing emails.
Technical Analysis
Stage 1
The first stage is a JScript file employing junk code, junk delimiter strings, and string concatenation for obfuscation. figure 1 - obfuscated stage 1 downloader
This can be trivially deobfuscated using a text editor like Sublime Text to replace the delimiter string defined on line 79
with an empty string, followed by replacing the below regular expression with an empty string to clean up string concatenation.
1
";[\s\n]+[a-z]+\s\+=\s"
After further deobfuscation and variable renaming, the below downloader is identified, which reaches out to a URL, checks for a status code of 200
, then executes the HTTP response as code through use of an immediately invoked function expression2 (IIFE). figure 2 - deobfuscated stage 1 downloader script
Stage 2
The next stage is further JScript employing obfuscation identical to stage 1. After repeating previous deobfuscation steps, we observe use of PowerShell to execute another stage as seen in figure 3. This is responsible for executing the PowerShell script seen in figure 4. figure 3 - stage 2 downloader script
figure 4 - PowerShell script invoked by stage 2 downloader script
After some code beautifying using CyberChef and variable renaming, I was able to better understand the execution flow. It begins by reaching out to an image hosted on well-known digital library site “Internet Archive”, where the image is loaded into memory as a byte array. figure 5 - PowerShell code responsible for loading image into memory
figure 6 - universe themed image hosted on Internet Archive
Once loaded into memory, the byte array is scanned for a hard-coded byte sequence that begins with 42 4D
, the magic bytes for the bitmap format3. Once found, the index position of the byte sequence start is stored in variable $bitmap_begin
. figure 7 - code responsible for locating bitmap embedded in image
Starting at $bitmap_begin
, the remaining image bytes are stored in a memory buffer, which are then used to construct a .NET Bitmap object. It then iterates through each pixel in the bitmap, reads in its RGB byte values, and adds them to a new byte list $byte_list
. figure 8 - code responsible for image manipulation
It then loads an embedded .NET assembly within the newly created byte list, where method VAI()
inside of ClassLibrary1.Home
is invoked. figure 9 - extraction and execution of .NET assembly embedded within bitmap
Dynamic PE Extration
Now having an understanding of its functionality, we can dump the assembly using a dynamic approach by replacing lines 55 and onwards with the following and executing the script.
1
[System.IO.File]::WriteAllBytes("assembly.mal", $dotnet_assembly);
Static PE Extraction
Alternatively, we can port functionality from the PowerShell script to Python, allowing us to perform static extraction of the .NET assembly from the image using the Pillow library4. The referenced blog on looping through pixel data with Python5 was helpful during development of this script.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import sys
import os
from PIL import Image
# take image as input
polyglot = sys.argv[1]
with open(polyglot, "rb") as f:
image_bytes = f.read()
# convert image to byte array
image_byte_array = bytearray(image_bytes)
size = len(image_byte_array)
# find bitmap start
bitmap_start = image_byte_array.find(b"\x42\x4D\x72\x6E\x37\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x64\x00\x00\x00\x4D\x2F\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x3C\x6E\x37\x00\xC4\x0E\x00\x00\xC4\x0E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
# extract bitmap based on start location
extracted_bitmap = image_byte_array[slice(bitmap_start, size)]
# temporarily write bitmap to disk
# (the Image module only handles file paths)
with open("bitmap.tmp", "wb") as f:
f.write(extracted_bitmap)
try:
img = Image.open("bitmap.tmp")
except FileNotFoundError:
print("Error: temporary bitmap file not found")
width, height = img.size # get dimensions
image = img.convert("RGB") # read image using RGB mode
byte_list = [] # define ouput byte list
# extract each pixel's RGB byte values
for y in range(height):
for x in range(width):
r, g, b = image.getpixel((x, y))
byte_list.append(r)
byte_list.append(g)
byte_list.append(b)
# bitmap cleanup
img.close()
image.close()
os.remove("bitmap.tmp")
# extract assembly
assembly_len = int.from_bytes(byte_list[:4], byteorder="little")
extracted_assembly = bytes(byte_list[4:assembly_len])
# write assembly to file
with open("extracted_assembly.mal", "wb") as f:
f.write(extracted_assembly)
Until next time, folks! See you in part two, where we will analyze the extracted .NET assembly. All hashes from the below IOC table will be available for download on MalShare.
IOCs
Type | IOC |
---|---|
Stage 1 Downloader SHA-256 | 0fd706ebd884e6678f5d0c73c42d7ee05dcddd53963cf53542d5a8084ea82ad1 |
Stage 1 Downloader User-Agent | MyCustomAgent/1.0 |
Stage 2 URL | hxxp[://]deadpoolstart[.]lovestoblog[.]com/arquivo_fb2497d842454850a250bf600d899709[.]txt |
Stage 2 Downloader SHA-256 | ad25fffedad9a82f6c55c70c62c391025e74c743a8698c08d45f716b154f86da |
Image SHA-256 | 89959ad7b1ac18bbd1e850f05ab0b5fce164596bce0f1f8aafb70ebd1bbcf900 |
Image URL | hxxps[://]archive[.]org/download/universe-1733359315202-8750/universe-1733359315202-8750[.]jpg |