HUMAN is Named a Leader and Earns Top Scores in Nine Criteria in the Forrester Wave™: Bot Management Software, Q3 2024
Tech & Engineering Blog

The Far Point of a Static Encounter

TL;DR: A breakdown of the Anti-VM skimmer and its variants from the earliest incarnation to the latest iteration served from staticounter.net.

Following @rootprivilege’s tweet of a skimmer served from the recently registered (2022-05-11) staticounter.]net, I decided to take a closer look. I’ll start with describing my process of deobfuscation, but if you’re more interested in the workings of the actual skimmer, you might want to skip ahead.

Unpacking The Deobfuscation

When I’m looking at new obfuscated code, I like to describe it to myself and make assumptions regarding the structures I’m noticing. I then try to either prove or disprove those assumptions about how the obfuscation process works. With that said, let's dig in.

@AffableKraut was kind enough to share the skimmer code which was injected at the end of the script. I’ve cropped it to just the skimmer itself.

Here’s the abridged version:

;var o1, o2, o3, o4/*, ... */;
(function () {
	var kjn = '', MXQ = 759 - 748;
	function UDS(b) {
		// ...
		return f.join('');
	}
	var ZhC = UDS('laessrrutnwmbnpoyhokgixzdoutrjtqccvcf').substr(0, MXQ);
	var LSm = 'f)C[fv.ogo=exs!)zo)-)r0uf"v7(5i7ahirklmnu...';
	var AET = UDS[ZhC];
	var JIX = '';
	var Uku = AET;
	var BTX = AET(JIX, UDS(LSm));
	var FpP = BTX(UDS('ih)c 2pni:%#DD5rn,|p26tca 0...'));
	var CuM = Uku(kjn, FpP);
	CuM(4372);
	return 9137;
})();

As a side note, I've found that the ;var at the beginning is often a good indication of an injection, since you might not be sure if the code you’re injecting to ends with a semicolon. If it doesn’t, it might have unexpected repercussions.

The structure of the obfuscation is pretty straightforward:

  1. Declaring many variables in the global scope.
  2. An anonymous IIFE containing:

    1. A function declaration - UDS. It ends with a .join, and I can see that there are a couple of calls to it with illegible strings as arguments, which makes me believe the function is a decoder.
    2. An assignment of a property of the function UDS into a new variable - AET. This variable is then used as a function a couple of lines below.
    3. The BTX variable is declared and a line later it is used as a function.
    4. Finally, almost at the end of the script, the Uku variable — another reference to AET — is called to create the CuM function, which is then executed. Since the execution of the CuM function does not save any output to any variable, I’m guessing it’s the main skimmer function.

The entire flow of building the script from obfuscated strings and then running it seems to fit that of a packer: build code from a long string, and then run it.

The Packer

Why do I think the UDS function is a decoder? Let’s examine it:

var ZhC = UDS('laessrrutnwmbnpoyhokgixzdoutrjtqccvcf').substr(0, MXQ);

Even without looking at how the UDS function works, we can tell it returns a string, since substr is chained to it. Running the function with the obfuscated string as input produces the string constructorabcdefghijklmnopqrstuvwxyz. I’m going to venture a guess and say that MXQ equals 11, which would leave us with just the word constructor. This answers the question of how the BTX and CuM functions are created: using the UDS function’s constructor method.

var ZhC = 'constructor';
var LSm = 'f)C[fv.ogo=exs!)zo)-)r0uf"v7(5i7ahirklmnu...';
var AET = UDS['constructor'];
var JIX = '';
var Uku = AET;
var BTX = AET(JIX, UDS(LSm));
var FpP = BTX(UDS('ih)c 2pni:%#DD5rn,|p26tca 0.....'));
var CuM = Uku(kjn, FpP);
CuM(4372);

A couple more declarations and replacements, and we end up with a second decoder function in BTX:

var BTX = function anonymous() {
	var m = 10, o = 59, v = 10;
	var r = "abcdefghijklmnopqrstuvwxyz";
	var j = [90, 65, 71, 94, 66, 75, 70, 82, 80, 74, 81, 89, 60, 88, 87, 86, 72, 79, 76, 85];
		var s = []; 
		for (var z = 0; z < j.length; z++)s[j[z]] = z + 1;
		var q = [];
		m += 23;
		o += 34;
		v += 86;
		for (var a = 0; a < arguments.length; a++) {
		var d = arguments[a].split(" "); 
		for (var e = d.length - 1; e >= 0; e--) {
			var w = null;
			var i = d[e];
			var x = null;
			var h = 0;
			var t = i.length;
			var g; 
			for (var n = 0; n < t; n++) {
				var l = i.charCodeAt(n);
				var u = s[l]; 
				if (u) { 
					w = (u - 1) * o + i.charCodeAt(n + 1) - m; g = n; n++; 
				} else if (l == v) { 
					w = o * (j.length - m + i.charCodeAt(n + 1)) + i.charCodeAt(n + 2) - m; g = n; n += 2; 
				} else { continue; } 
				if (x == null) x = [];
				if (g > h) x.push(i.substring(h, g));
				x.push(d[w + 1]); 
				h = n + 1; 
			} 
			if (x != null) {
				if (h < t) x.push(i.substring(h));
				d[e] = x.join(""); 
			} 
		} 
		q.push(d[0]); 
	} 
	var k = q.join("");
	var p = [32, 96, 39, 42, 92, 10].concat(j); 
	var c = String.fromCharCode(46); 
	for (var z = 0; z < p.length; z++) k = k.split(c + r.charAt(z)).join(String.fromCharCode(p[z]));
	return k.split(c + "!").join(c);
}

The next line is assigning to FpP the value of two decoder functions on a string:

var FpP = BTX(UDS('ih)c 2pni:%#DD5rn,|p26tca 0.....'));

Which will leave it holding the code for the function created in the next line.

Putting it all together, we get:

var ZhC = 'constructor';
var LSm = 'f)C[fv.ogo=exs!)zo)-)...';
var AET = UDS.constructor;
var JIX = '';
var Uku = UDS.constructor;
var BTX = UDS.constructor('', 'var m=10,o=59,v=10;var...');
var FpP = 'function _0x270ED(_0x26D23,_0x26A7C){var ...';
var CuM = UDS.constructor('', 'function _0x270ED(_0x26D23,_0x26A7C){var...');
CuM(4372);

This was the flow of the packer that builds the actual skimmer code. To extract the skimmer itself, simply look at the value of the FpP variable. To get a nice printout of the code, replace the execution of the CuM function with

console.log(CuM.toString());

 

The Obfuscated Skimmer

Just from skimming the code (pun intended), you can tell that the obfuscation’s main trick here is array replacement references as there are a lot - just shy of 600 - references to different indexes of the variable _0x26713. Here’s an example:

i71[_0x26713[99]][_0x26713[98]] = dN34[_0x26713[71]](f1)[_0x26713[77]];

When I searched for the variable’s declaration, I found it was assigned the result of a function call with a long nonsense string as its sole argument. Sound familiar?

var _0x26713 = (_0x270ED)("fisctlue-rtrornx.=oadan...");

Yup, it’s another decoder, which is widely used by skimmers I’ve examined. You can identify it by its join-split-join-split structure:

var _0x26713 = String.fromCharCode(127);
var _0x26C61 = '';
var _0x26B9F = '%';
var _0x26CC2 = '#1';
var _0x26897 = '%';
var _0x26A1B = '#0';
var _0x267D5 = '#';
return _0x269BA.join(_0x26C61).split(_0x26B9F).join(_0x26713).split(_0x26CC2).join(_0x26897).split(_0x26A1B).join(_0x267D5).split(_0x26713)

Running this decoder on the obfuscated string gives us an array of deobfuscated strings, all ready to be placed instead of the array references throughout the script. Some cleaning up and we end up with a cleaner version of the first example in this section:

i71.cd.nb = dN34.getElementById('number').value;

In the last couple of years, I’ve been building a deobfuscator — which I dubbed REstringer — that can handle many of these techniques by specifically targeting many of the obfuscation methods commonly used by skimmers. Here’s the skimmer after being deobfuscated using REstringer. More on this topic will be coming soon.

The (not so) Anti-VM Skimmer

In the same Twitter thread, Malwarebytes pointed out that this skimmer is linked to the “anti-VM” skimmer. Comparing the code of both skimmers confirms it, though the new sample dropped the “anti-VM” check ¯\(ツ)/¯. About a week later, Malwarebytes came out with a blog post describing the breadth of the infrastructure serving variants of the same skimmer.

The anti-VM code prevents the attack from activating if the skimmer detects it is running in a Virtual Machine: a sandbox environment used to observe and investigate threats.

After examining a few samples, both from the malicious domains they mentioned in their post, as well as others I’ve found (such as staticounter.]com, registered since 2020-12-19), it appears that the campaign targets a wide variety of payment providers, where each skimmer matches its techniques to the targeted framework and payment provider.

It’s not uncommon to find several variants of the same skimmer going around. Just like in other fields of software development, code sharing is prevalent, and attackers often take an existing skimmer that targets a specific framework and tweak it to run on a different version of the same framework or even on a completely different one.

This is the case here as well, with the different variants sharing most of their code with one another, save for a few key differences: how to target and get around different checkout or payment details forms’ implementations.

Since I already went through the deobfuscation process in detail earlier, and for clarity’s sake, I’m linking here to a version of the skimmer where almost everything has been renamed to help follow along with the flow.

Bypassing Stripe’s Payment Iframe

The latest variant, served from staticounter.]net, is targeting Magento version 1. It’s using the VarienForm function which was deprecated in Magento 2, along with Stripe’s payment form, evident by its reliance on the existence of elements in the page such as #stripe-payments-card-number, and #payment_method_stripe.

Once the elements mentioned above are confirmed to exist on the page, the attack starts by:

  1. Hiding the real payment form sandbox iframe and injecting its own accessible iframe.
  2. Replacing the real submit button with its own fake one.
    This prevents the site from submitting the form when the user clicks the button and allows the attacker to run first without racing against any of the site’s original functionality that might activate on the same click.
  3. Injecting a fake payment form into the new iframe, allowing the skimmer to monitor what is entered while making it look like the original form, complete with validity checks and a credit card brand icon which matches to the type of card number entered.
  4. Once the payment details have been entered in full, the skimmer collects any available personally identifiable information (PII) it manages to find by reading values from specific fields in the checkout form like #billing[firstname], #billing[lastname], #billing[email], and #address.
  5. Having collected all of the payment information and PII, it encrypts the data and exfiltrates it via jQuery’s Ajax POST request back to its server.
  6. The last step includes removing the fake iframe and restoring the original, and clicking the original submit button to elicit the default error message regarding an incomplete credit card number, as the real input fields remained empty.

The skimmer uses the creation of cookies in order to keep track of whether the attack was activated (cookie name form_key_id) or completed (cookie name _gld). This allows it to avoid redundantly re-injecting itself, or attacking a victim more than once, possibly leading to its early discovery.

By hiding the original payment iframe and injecting its own, the skimmer side-steps the protection the iframe provides. This also means the site may no longer be PCI compliant, exposing it to heavy fines and reputation degradation.

From the attacker’s point of view, the down side of such a replacement is that the payment isn’t really processed by the payment vendor, which requires the unsuspecting user to input their payment details more than once in order to complete the order. This makes the attack more noisy and prone to alert the end user of its existence.

While attack methods that succeeds in letting the payment pass on the first try without any visible errors have been observed before, it isn’t a scenario commonly seen.

By Hook or By Crook

The earliest sample I’ve seen, from June 2021, targeted Magento 2 using the same bait-and-switch technique of injecting another iframe with a fake form. We can determine this by comparing its targeted CSS selectors to the Magento source code. Here are a couple of examples: The old skimmer was searching for #stripe-card-element, while the newer is searching for #stripe-payments-card-number. The old skimmer looked for checkout in the url vs firecheckout. And the use of the populated class when reporting credit card number errors existed in version 1, but was removed in version 2. Another minor but interesting difference is that the newer version does not collect passwords, which the old one did by extracting the value of the field #billing:customer_password.

Other variants target more payment vendors and frameworks such as NMI’s USA ePay, Magento’s Authorize CIM Payment module, PayPal DPM, InstaSend, Adobe Commerce, and eWay, just to name a few.

Where payment input fields are behind iframes, the skimmer uses the method described above of injecting its own iframe. When the input fields are accessible directly, it might replace the input fields anyway to prevent the original site’s functions from running, or it might simply hook the submit button and collect the payment details values from the original fields, like in this example. As mentioned above, the skimmer ends the attack with clicking the original button to elicit an error message, but in some cases it even goes as far as displaying its own error message:

alert('Gateway error. You will be automatically redirected to the our website for payment processing after placing the order.');

Instead of using a cookie to mark the attack as completed or a fake form as injected, the skimmer might use localStorage keys like mage-cache-version or recently_viewed_product_session, as seen on this variant.

These aren’t major differences in implementation, but knowing about them may come in handy when checking for signs of the skimmer in a page. This can also help verify whether an attack took place during a session by checking out certain cookies, searching the localStorage, or looking for an unexpected element or a missing expected element.

Summary

Magecart attacks being more “covert”, along with skimmer names like “anti-VM”, do not represent a change in the attacks themselves, but in their supporting infrastructure; These evasion mechanisms are aimed to hinder us — the researchers and defenders — in finding and stopping the attacks before they commence. By requiring interaction with the page, requiring a valid referer header or checking the IP doesn’t belong to a known VPN service, the attackers attempt to increase the chances of us missing their skimmer with our automatic scans.

The anti-VM skimmer highlights the necessity of protection solutions which run in-session and can detect digital skimming attacks without having to rely on outside or sandbox scanning. HUMAN Code Defender is such a solution, giving you visibility into what code is running on your site, whether it is first- or third-party, static or dynamically loaded. The solution provides alerts when such attacks occur and the ability to block any script from performing unwanted actions.