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.
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:
An anonymous IIFE containing:
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.UDS
into a new variable - AET
. This variable is then used as a function a couple of lines below.BTX
variable is declared and a line later it is used as a function.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.
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());
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.
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.
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:
#billing[firstname]
, #billing[lastname]
, #billing[email]
, and #address
.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.
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.
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.