JS解混淆
最近在整理之前和一些同伴的分享资料,发现时间已经过了好久,特此整理一些有价值的分享记录。
JS混淆
学习js混淆可以逆向分析混淆和加密过程,实战可用于爬虫和渗透信息获取
本文档用于初步介绍js混淆的基础概念以及如何解混淆、调试,便于干掉反爬虫和渗透信息收集思路拓展
概念解释
混淆/加密
降低代码可读性加强安全性,防止被人任意查看,在一定程度保护资源
理想的混淆或加密应该具备如下特点
1、没有确定的破解模式;
2、很难编制自动破解程序(只能手工破解);
3、破解过程繁琐、耗时;
4、"混淆|加密"后的代码,比原始代码长度增加少;
代码里诸如此类就是经过了混淆的结果,可以通过console+断点打出来看看值
js混淆和eval加密
前端虽然开源, 但是由于前端代码量很多,也有一些特殊的保护代码的方法
其中Eval、js混淆是常用的方式,但是在大的互联网产品上用得很少,因为前端加密(RSA、AES、MD5等)是为了保证数据传输中的安全性,而非要让人难以模仿数据传输请求
而前端中的js混淆、eval对于专业的人来说形同虚设,所以也没必要做混淆和eval,并且对于代码维护是及其不利的
eval加密
js中的eval()方法就是一个js语言的执行器
它能把其中的参数按照JavaScript语法进行解析并执行
简单来说就是把原本的js代码变成了eval的参数,变成参数后代码就成了字符串,其中的一些字符就会被按照特定格式"编码"
是最早JS出现的混淆加密,据说第一天就被破解,修改一下代码,alert一下就可以破解了
#eval加密
源代码:
var showmsg="粘贴要加密/解密的javascript代码到这里";
if(1==1){
alert(showmsg);
}
加密后的样子:
eval(function(p,a,c,k,e,d){e=function(c)
{return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?
String.fromCharCode(c+29):c.toString(36))};
if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return
d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new
RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('5 4="粘贴要加密/解密的3代码到这里";2(0==0){
1(4);}',62,6,'1|alert|if|javascript|showmsg|var'.split('|'),0,{}))
eval()语句还有一个重要用途:在反调试中可以使用该语句来进行一些函数值赋空从而跳出debugger的函数
JS混淆
把其中的变量、方法位置顺序打乱,但是又用一些无关的变量或者方法来保证执行顺序
常见手段
1、去除缩进、空行、换行、注释
2、变量名替换(缩短/改乱)
3、通过自定义变量名引用JS关键字
4、添加大段空白,增加代码前后间隔,干扰阅读
5、混眼法(通过利用[]和["、']及变量定义语句来添加与代码功能无关的字符/增添与代码功能无关的运算语句)
6、对源代码进行加密,同时附上解密的代码(运行时先解密,然后通过document.write()或eval()或innerHTML把代码释放出来执行)
其他混淆类型
hash类型
压缩类型
常用工具
混淆
这里是从使用工具加密信息方出发,具体工具的使用可以自行学习。
- webassembly
- esprima
针对JavaScript
- JavaScript Obfuscator
具体使用参考:7.8k Star!一个强大的 JS 代码混淆工具 - 掘金 (juejin.cn)
- terser
- uglify-js
- uglify-es
- Google Closure Compiler
- YUI Compressor
针对CSS
-
PostCSS
-
clean-css
-
CSSO
-
YUI Compressor
针对HTML
- html-minifier
混淆示例
此处使用JavaScript Obfuscator Tool,由JavaScript Obfuscator作者搭建的一个在线混淆网站,直接输入需要混淆的代码输出混淆结果即可
以下面的一个简单hello world为例
##源代码
function hi() {
console.log("Hello World!");
}
hi();
经过混淆之后
##混淆后的代码
(function(_0x1522cf,_0x263348){var _0x2bf84c=_0x42bb,_0x47bae4=_0x1522cf();while(!![]){try{var _0x301f10=parseInt(_0x2bf84c(0x11b))/0x1*(-parseInt(_0x2bf84c(0x10f))/0x2)+-parseInt(_0x2bf84c(0x114))/0x3+parseInt(_0x2bf84c(0x112))/0x4*(-parseInt(_0x2bf84c(0x117))/0x5)+-parseInt(_0x2bf84c(0x110))/0x6+parseInt(_0x2bf84c(0x115))/0x7*(parseInt(_0x2bf84c(0x118))/0x8)+parseInt(_0x2bf84c(0x119))/0x9*(parseInt(_0x2bf84c(0x116))/0xa)+parseInt(_0x2bf84c(0x11a))/0xb*(parseInt(_0x2bf84c(0x113))/0xc);if(_0x301f10===_0x263348)break;else _0x47bae4['push'](_0x47bae4['shift']());}catch(_0x2af3c3){_0x47bae4['push'](_0x47bae4['shift']());}}}(_0x22dc,0x1e93e));function hi(){var _0xfdbe99=_0x42bb;console[_0xfdbe99(0x111)]('Hello\x20World!');}hi();function _0x42bb(_0x4a56bb,_0x17e1ee){var _0x22dca2=_0x22dc();return _0x42bb=function(_0x42bb1c,_0x597cba){_0x42bb1c=_0x42bb1c-0x10f;var _0x2ad529=_0x22dca2[_0x42bb1c];return _0x2ad529;},_0x42bb(_0x4a56bb,_0x17e1ee);}function _0x22dc(){var _0x1ca681=['937926xGdCzf','log','344SUuAGG','1124988WMYeGw','111081MLZhWo','35SqOFWp','670aFpiLz','12820fkuEha','108152xzQqbd','15975Prsnjz','44YZHRMa','1oaFebR','44836HvkwgV'];_0x22dc=function(){return _0x1ca681;};return _0x22dc();}
##为了展示直观,经过代码美化处理结果如下
(function(_0x1522cf, _0x263348) {
var _0x2bf84c = _0x42bb,
_0x47bae4 = _0x1522cf();
while (!![]) {
try {
var _0x301f10 = parseInt(_0x2bf84c(0x11b)) / 0x1 * (-parseInt(_0x2bf84c(0x10f)) / 0x2) + -parseInt(_0x2bf84c(0x114)) / 0x3 + parseInt(_0x2bf84c(0x112)) / 0x4 * (-parseInt(_0x2bf84c(0x117)) / 0x5) + -parseInt(_0x2bf84c(0x110)) / 0x6 + parseInt(_0x2bf84c(0x115)) / 0x7 * (parseInt(_0x2bf84c(0x118)) / 0x8) + parseInt(_0x2bf84c(0x119)) / 0x9 * (parseInt(_0x2bf84c(0x116)) / 0xa) + parseInt(_0x2bf84c(0x11a)) / 0xb * (parseInt(_0x2bf84c(0x113)) / 0xc);
if (_0x301f10 === _0x263348) break;
else _0x47bae4['push'](_0x47bae4['shift']());
} catch (_0x2af3c3) {
_0x47bae4['push'](_0x47bae4['shift']());
}
}
}(_0x22dc, 0x1e93e));
function hi() {
var _0xfdbe99 = _0x42bb;
console[_0xfdbe99(0x111)]('Hello\x20World!');
}
hi();
function _0x42bb(_0x4a56bb, _0x17e1ee) {
var _0x22dca2 = _0x22dc();
return _0x42bb = function(_0x42bb1c, _0x597cba) {
_0x42bb1c = _0x42bb1c - 0x10f;
var _0x2ad529 = _0x22dca2[_0x42bb1c];
return _0x2ad529;
}, _0x42bb(_0x4a56bb, _0x17e1ee);
}
function _0x22dc() {
var _0x1ca681 = ['937926xGdCzf', 'log', '344SUuAGG', '1124988WMYeGw', '111081MLZhWo', '35SqOFWp', '670aFpiLz', '12820fkuEha', '108152xzQqbd', '15975Prsnjz', '44YZHRMa', '1oaFebR', '44836HvkwgV'];
_0x22dc = function() {
return _0x1ca681;
};
return _0x22dc();
}
可以发现代码混淆有几个比较固定的特征,一些变量的命名会赋随机值,而后通过一个数组去进行存储。同时使用一个while-try-catch的结构。
再看一下实际环境中经过混淆的代码
// 此处也是经过格式美化,源代码只有一行
eval(function(p, a, c, k, e, d) {
e = function(c) {
return (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
};
if (!''.replace(/^/, String)) {
while (c--) d[e(c)] = k[c] || e(c);
k = [function(e) {
return d[e]
}];
e = function() {
return '\\w+'
};
c = 1;
};
while (c--)
if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
return p;
}('4 3(1){2 0=5 8();7 0.6(1)}', 9, 9, 'b|tksl|var|dswejwehxt|function|new|decode|return|Base64'.split('|'), 0, {}));
// respond
eval(function(p, a, c, k, e, d) {
e = function(c) {
return (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
};
if (!''.replace(/^/, String)) {
while (c--) d[e(c)] = k[c] || e(c);
k = [function(e) {
return d[e]
}];
e = function() {
return '\\w+'
};
c = 1;
};
while (c--)
if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
return p;
}('(c(w){"2O 2E";8 7={};w.7=7;7.21=c(){};8 V=[],1T=(c(){8 16=1E;2t{16=1l w.2s()}2u(e){16=1l w.2w("2v.2o")}l c(){l 16}})(),14=c(1t,22){8 p=1T();5(!p){l}p.2n("2p",1t,1f);p.2r=c(){5(p.1Q!==4||p.2a!==2q&&p.2a!==2D){l}22(p.2C)};5(p.1Q===4){l}p.2G(1b)},1J=c(25){l 25.U(7.f.2g,\'\').I(7.f.1R)};7.14=14;7.2F=V;7.2y=1J;7.f={b:/@b[^\\{]+\\{([^\\{\\}]*\\{[^\\}\\{]*\\})+/17,1j:/@(?:\\-(?:o|2x|2z)\\-)?1j[^\\{]+\\{(?:[^\\{\\}]*\\{[^\\}\\{]*\\})+[^\\}]*\\}/17,2f:/\\/\\*[^*]*\\*+([^/][^*]*\\*+)*\\//17,20:/(1t\\()[\'"]?([^\\/\\)\'"][^:\\)\'"]+)[\'"]?(\\))/g,1U:/@b *([^\\{]+)\\{([\\S\\s]+?)$/,Y:/(Y\\s+)?([a-2e-Z]+)\\s?/,12:/\\(\\s*v\\-19\\s*:\\s*(\\s*[0-9\\.]+)(1o|E)\\s*\\)/,15:/\\(\\s*u\\-19\\s*:\\s*(\\s*[0-9\\.]+)(1o|E)\\s*\\)/,2g:/\\(\\s*m(1h|2B)\\-(2A|19)\\s*:\\s*(\\s*[0-9\\.]+)(1o|E)\\s*\\)/17,1R:/\\([^\\)]*\\)/g};7.2b=w.1s&&w.1s("Y 1M")!==1b&&w.1s("Y 1M").2k;5(7.2b){l}8 h=w.2j,t=h.2m,X=[],F=[],B=[],1i={},1w=30,G=h.1H("G")[0]||t,1z=h.1H("1z")[0],W=G.1H("2l"),1a,1p,1c,T=c(){8 Q,H=h.1k(\'H\'),d=h.d,29=t.q.J,1n=d&&d.q.J,1d=1E;H.q.26="2i:2H;34-33:1Z;19:1Z";5(!d){d=1d=h.1k("d");d.q.36="35"}t.q.J="1S%";d.q.J="1S%";d.27(H);5(1d){t.23(d,t.2Z)}Q=H.2Y;5(1d){t.1r(d)}K{d.1r(H)}t.q.J=29;5(1n){d.q.J=1n}Q=1c=10(Q);l Q},18=c(1X){8 1C="32",1m=t[1C],1u=h.31==="3c"&&1m||h.d[1C]||1m,C={},28=W[W.y-1],1v=(1l 3a()).37();5(1X&&1a&&1v-1a<1w){w.38(1p);1p=w.2d(18,1w);l}K{1a=1v}N(8 i 1h X){5(X.1q(i)){8 z=X[i],v=z.12,u=z.15,1y=v===1b,1x=u===1b,E="E";5(!!v){v=10(v)*(v.1D(E)>-1?(1c||T()):1)}5(!!u){u=10(u)*(u.1D(E)>-1?(1c||T()):1)}5(!z.1Y||(!1y||!1x)&&(1y||1u>=v)&&(1x||1u<=u)){5(!C[z.b]){C[z.b]=[]}C[z.b].M(F[z.F])}}}N(8 j 1h B){5(B.1q(j)){5(B[j]&&B[j].2N===G){G.1r(B[j])}}}B.y=0;N(8 k 1h C){5(C.1q(k)){8 A=h.1k("q"),L=C[k].2P("\\n");A.2J="2I/L";A.b=k;G.23(A,28.2K);5(A.R){A.R.26=L}K{A.27(h.2V(L))}B.M(A)}}},1B=c(P,6,b){8 11=P.U(7.f.2f,\'\').U(7.f.1j,\'\').I(7.f.b),1e=11&&11.y||0;6=6.1O(0,6.2U("/"));8 1N=c(L){l L.U(7.f.20,"$1"+6+"$2$3")},1L=!1e&&b;5(6.y){6+="/"}5(1L){1e=1}N(8 i=0;i<1e;i++){8 1g,D,13,1K;5(1L){1g=b;F.M(1N(P))}K{1g=11[i].I(7.f.1U)&&r.$1;F.M(r.$2&&1N(r.$2))}13=1g.1A(",");1K=13.y;N(8 j=0;j<1K;j++){D=13[j];5(1J(D)){2W}X.M({b:D.1A("(")[0].I(7.f.Y)&&r.$2||"1M",F:F.y-1,1Y:D.1D("(")>-1,12:D.I(7.f.12)&&10(r.$1)+(r.$2||""),15:D.I(7.f.15)&&10(r.$1)+(r.$2||"")})}}18()},1I=c(){5(V.y){8 O=V.2R();14(O.6,c(P){1B(P,O.6,O.b);1i[O.6]=1f;w.2d(c(){1I()},0)})}},1G=c(){N(8 i=0;i<W.y;i++){8 x=W[i],6=x.6,b=x.b,2h=x.24&&x.24.2T()==="2S";5(!!6&&2h&&!1i[6]){5(x.R&&x.R.2c){1B(x.R.2c,6,b);1i[6]=1f}K{5((!/^([a-2e-Z:]*\\/\\/)/.2Q(6)&&!1z)||6.U(r.$1,"").1A("/")[0]===w.1P.2X){5(6.1O(0,2)==="//"){6=w.1P.2L+6}V.M({6:6,b:b})}}}}1I()};1G();7.21=1G;7.T=T;c 1F(){18(1f)}5(w.1V){w.1V("2M",1F,1E)}K 5(w.1W){w.1W("39",1F)}})(3b);', 62, 199, '|||||if|href|respond|var|||media|function|body||regex||doc||||return||||req|style|RegExp||docElem|max|min||sheet|length|thisstyle|ss|appendedEls|styleBlocks|thisq|em|rules|head|div|match|fontSize|else|css|push|for|thisRequest|styles|ret|styleSheet||getEmValue|replace|requestQueue|links|mediastyles|only||parseFloat|qs|minw|eachq|ajax|maxw|xmlhttpmethod|gi|applyMedia|width|lastCall|null|eminpx|fakeUsed|ql|true|fullq|in|parsedSheets|keyframes|createElement|new|docElemProp|originalBodyFontSize|px|resizeDefer|hasOwnProperty|removeChild|matchMedia|url|currWidth|now|resizeThrottle|maxnull|minnull|base|split|translate|name|indexOf|false|callMedia|ripCSS|getElementsByTagName|makeRequests|isUnsupportedMediaQuery|eql|useMedia|all|repUrls|substring|location|readyState|other|100|xmlHttp|findStyles|addEventListener|attachEvent|fromResize|hasquery|1em|urls|update|callback|insertBefore|rel|query|cssText|appendChild|lastLink|originalHTMLFontSize|status|mediaQueriesSupported|rawCssText|setTimeout|zA|comments|minmaxwh|isCSS|position|document|matches|link|documentElement|open|XMLHTTP|GET|200|onreadystatechange|XMLHttpRequest|try|catch|Microsoft|ActiveXObject|moz|unsupportedmq|webkit|height|ax|responseText|304|strict|queue|send|absolute|text|type|nextSibling|protocol|resize|parentNode|use|join|test|shift|stylesheet|toLowerCase|lastIndexOf|createTextNode|continue|host|offsetWidth|firstChild||compatMode|clientWidth|size|font|none|background|getTime|clearTimeout|onresize|Date|this|CSS1Compat'.split('|'), 0, {}));
可以从里面发现一些规律,例如一大段字符的split替换、eval(function)的声明。
或者类似如下的大段以单个字母进行的随机命名
!function() {
var t = document
, e = 0;
(window.isLogin || "object" == typeof OP_CONFIG && OP_CONFIG.userInfo && OP_CONFIG.userInfo.uid) && (e = 1);
var d = 1646064e6
, o = 16487424e5
, i = 16461396e5
, a = 16471872e5
, c = 0
, n = null
, u = 1e4
, r = "//www.imooc.com"
, s = "//www.imooc.com/static/moco/v1.0/images/redrain2"
, l = "20220301";
location.href.indexOf("guoyuchen") > -1 && (r = "//www-xiongwenhui.imooc.com",
s = "/static/moco/v1.0/images/redrain2");
var f = [s + "/ready.png?t=" + l, s + "/go.png?t=" + l, s + "/close-btn.png?t=" + l, s + "/redpacket.png?t=" + l, s + "/boom.png?t=" + l, s + "/result-bg1.png?t=" + l, s + "/use-btn.png?t=" + l, s + "/result-bg2.png?t=" + l, s + "/more-btn.png?t=" + l, s + "/more-btn2.png?t=" + l, s + "/result-bg3.png?t=" + l, s + "/halfAd1.jpeg?t=" + l, s + "/halfAd2.jpeg?t=" + l, s + "/coupon-bg2.png?t=" + l, s + "/coupon-btn2.gif?t=" + l, "//www.imooc.com/static/moco/v1.0/images/march2022/big-ad2.png?t=" + l, "//www.imooc.com/static/moco/v1.0/images/march2022/big-ad2-btn.png?t=" + l]
, m = {
modal: '<div class="redRain-modal" id="redRainModal"></div>',
coupon: '<div class="coupon-wrap center"> <div class="coupon-btn js-startCoupon"></div> <div class="close-btn js-closeCoupon couponCloseBtn618"></div> </div>',
halfScreenAd: '<div class="half-wrap center"> <div class="close-adv imv2-close js-closeHalfScreenAd"></div> <a href="//www.imooc.com/act/march2022?utm_source=imooc&utm_campaign=half" target="_blank"><img src="$img" /></a> </div>',
gameStart: '<div id="march2022" class="red-rain"> <a class="activity-center" data-type="2" target="_blank" style="background-image: url(//www.imooc.com/static/moco/v1.0/images/march2022/big-activity2.png?t=3)"> <span class="close imv2-add_circle_o js-close-activity"></span> <button class="activity-center-btn js-start-game" style="background-image: url(//www.imooc.com/static/moco/v1.0/images/march2022/big-activity2-btn.gif?t=3)"></button> </a> </div>',
loading: '<div class="loading center"></div>',
readyGo: '<div class="readyGo center"> <img src="' + s + "/ready.png?t=" + l + '" alt="" class="ready"> <img src="' + s + "/go.png?t=" + l + '" alt="" class="go hide"> </div>',
gameMain: '<div class="gameMain-wrap"> <div class="rain-wrap"> <div class="rain-box js-rain-box"></div> </div> <div class="line"></div> <div class="rainInfo-wrap"> <div class="clickNum">Combo X <span class="js-rain-clickNum">0</span></div> <div class="interval">剩余时间 <span class="js-rain-restTime">15</span>s</div> </div> </div>',
result1: '<div class="result-wrap1 center"> <div class="redpacket-price offset">¥$redpacketPrice</div> <div class="to-use-btn js-rainToActive offset"></div> <p class="tip offset">红包将在 <span class="js-redpacket-lefttime">3天</span> 后失效哦</p> <p class="tip offset">下单自动结算,可与优惠券叠加使用</p> <div class="close-btn js-closeRedRain"></div> </div>',
result2: '<div class="result-wrap2 center"> <div class="time">$nextTime</div> <div class="to-use-btn js-rainToActive"></div> <div class="close-btn js-closeRedRain"></div> </div>',
result3: '<div class="result-wrap3 center"> <div class="to-use-btn js-rainToActive"></div> <div class="close-btn js-closeRedRain"></div> </div>',
rightFloat: '<div id="rightFloat20201111" class="js-rainToActive"> <div class="redpacket"> <div class="redpacketContent"> $content </div> </div> <div class="bottomTitle"></div> </div>',
rightFloat2: '<div id="rightFloat20201111" class="js-rainToActive double11"></div>'
}
反混淆
反混淆的工具是依据混淆原理生成代码,实际需要不断观察分析及调整,比较考验人的耐性
需要具备将问题划分为N个子问题的能力
同时还需要具备js的基础知识,以及还原后如何重构代码(什么工具打包的webpack)
继续深入的话还需要了解JS语法解释器、AST抽象语法树、编程语言实现模式
- jspacker -> 针对eval
- unjsa -> 针对JSA
- crack.js -> 针对javascript-obfuscator
- jsnice -> 针对UnuglifyJS
本文档针对js解混淆初步入手,如何调试和如何定位进行说明
调试
在网页的调试过程中,需要借助一些工具去"投巧"(^_^)
- Fiddler/Reres:替换发包和请求内容
-
WT-JS_DEBUG:可以直接调试或美化js代码,同时附带多种解密
调试
alert调试
联网刚刚起步的时代,网页前端还主要以内容展示为主,浏览器脚本还只能为页面提供非常简单的辅助功能
那个时候,网页主要运行在以IE6为主的浏览器中,JS的调试功能还非常弱,只能通过内置于Window对象中的alert方法来调试
另一方面,alert的调试信息,必须在程序逻辑中添加类似"alert(xxxxx)"这样的语句,才能正常工作,并且alert会阻碍页面的继续渲染
这就意味着开发人员调试完成后,必须手动清除这些调试代码
console调试
新一代的浏览器Firefox、Chrome,包括IE,都相继推出了JS调试控制台,支持使用类似"console.log(xxxx)"的形式,在控制台打印调试信息,而不直接影响页面显示
如果在使用console对象之前先进性存在性验证,其实不删除也不会对业务逻辑造成破坏
为了代码整洁,在调试完成后,还是应尽可能删除这些与业务逻辑无关的调试代码
Chrome开发团队为Chrome浏览器拓展了更丰富的功能,具体操作可以使用Chrome浏览器
断点调试
JS断点调试,即是在浏览器开发者工具中为JS代码添加断点,让JS执行到某一特定位置停住,方便开发者对该处代码段的分析与逻辑处理。为了能够观察到断点调试的效果
给一段代码添加断点的流程是"F12(Ctrl + Shift + I)打开开发工具"------"点击Sources菜单"------"左侧树中找到相应文件"------"点击行号列"即完成在当前行添加/删除断点操作
当断点添加完毕后,刷新页面JS执行到断点位置停住,在Sources界面会看到当前作用域中所有变量和值,只需对每个值进行验证
此处选中第五行,再次刷新页面即将执行到此处
刷新之后的效果
可以发现右侧有这样一行工具栏
工具栏从左到右各图标的功能分别如下:
Pause/Resume script execution:F8 暂停/恢复脚本执行(程序执行到下一断点停止)
Step over next function call: F10 执行到下一步的函数调用(跳到下一行)
Step into next function call: F11 进入当前函数
Step out of current function:Shift+F11 跳出当前执行函数
Step: F9 同F11,将跨国异步函数进入下一行
Deactive/Active all breakpoints:Ctrl+F8 关闭/开启所有断点(不会取消)
Pause on exceptions:异常情况自动断点设置
Debugger断点
在开发中偶尔会遇到异步加载html片段(包含内嵌JS代码)的情况,而这部分JS代码在Sources树种无法找到
因此无法直接在开发工具中直接添加断点,那么如果想给异步加载的脚本添加断点,此时"debugger;"就发挥作用了
通过在代码中添加"debugger;"语句,当代码执行到该语句的时候就会自动断点
接下去的操作就跟在Sources面板添加断点调试几乎一模一样,唯一的区别在于调试完后需要删除该语句
DOM断点调试
在DOM元素上添加断点,进而达到调试的目的
代码展开
如果页面源代码显示单行,可以点击左下角的大括号展开,更为直观的浏览代码
结果如下图
搜索关键字
在页面内通过ctrl+F,可以出现搜索框
反调试
一些网站会通过监控网页窗口的长宽高以此监视是否开启调试模式以此来进行反调试,对此需要将devtool独立出来
具体请点击右上角的三个点,选择第一行的左边第一个按键,即可将调试窗口独立
实验
普通解混淆
以某东的登录为例子
随便输入数据找到post
看一下payload这里可以发现密码nloginpwd是经过加密的,还有一个pubKey和sa_token。那么需要解决的加密方式就是这三个
全局搜索nloginpwd,判断一下可能的位置,定断点刷新一下,定过来了
可以看到这里有个data,console打出来看看能和我们抓到的Post匹配上,可以发现这里的pubKey和sa_token是写死的
可以看到这里对于nloginpwd有个getEntryPwd函数,应该是对此进行了加密,跟进去看一下。这里可以打印一下getEntryPwd的赋值,可以发现是我们输入的原密码
根据名字看一下,这里赋值pubKey,同时进行一个JSEncrypt的操作,跟进去看一下
可以发现这段特别长,直接将整段copy出来尝试运行
可以发现这个代码是经过加密的,首行直接说明了。这种情况建议copy下来通过直接WT-JS_DEBUG尝试运行
代码copy过来发现有一些变量没有赋值,这里在首行直接赋空值,保证代码顺利运行就行
赋值之后重新运行可以看到加载成功
回溯源码理一下整个加密流程,将其串起来结合copy的加密算法写个解密过程。发现和抓到的密码是不一样的,而且每次运行结果都不一样,怀疑该加密跟时间有关
在某东的登录再用同样的密码登录几次尝试,发现每次加密后的密码也不一样
反调试解混淆
一些网站可能通过监视屏幕的宽高比,判断是否开启开发者工具而禁止调试,或者直接禁用F12。
这种情况以PM2.5实时查询|PM2.5历史数据查询|PM2.5全国城市排名|PM2.5雾霾地图|中国空气质量在线监测分析平台|真气网 (aqistudy.cn)为例举例说明如何反调试跳出debugger()函数
尝试F12开启调试窗口,直接被弹窗禁止了。这样就需要手动通过更多工具-开发者人员工具调出调试台。同时将其分离成独立窗口
可以看见因为开始调试直接进入debugger反调试
可以发现这个网页因为反调试网络啥信息都没有了(Φ皿Φ),这个时候可以通过窗口旁边的调用堆栈看看这个debugger是从哪里弹出来的
发现是一个txsdefwsw和c,进去看一下
追溯c的源码过去看看没有发现什么有用的信息,感觉程序栈不完整没有捕获到相关函数。重新刷新页面一下,发现新内容
(^_^)发现了反调试的代码,发现首页的源码。这里还包含一些其他反调试的检测。那么就是在这里触发的debug。首页当检测到非法调试之后,触发txsdefwsw()函数,全局搜索一下这个函数。
发现这里是通过eval()函数去执行一个function,(eval函数可以执行表达式,具体深入可以自行google)根据注释可以发现这里有一个debug的检测,那么看一下这两个eval里的function是什么。
控制台通过var打印一下这两个function的返回值,可以发现两个eval分别执行endebug和txsdefwsw两个函数。
思路就是通过替换这个eval函数的执行函数,让他执行一个空值的函数从而跳过这里的debug函数。可以用工具reres去替换这个js链接为本地经过改写的js文件。这样网页执行时,调用同名的空函数则不会触发debug。
可以发现替换后源码的js变成了这样
再次刷新还是debug,(╯▔皿▔)╯跟过去发现这里还有一层反调试
console打印一下,发现这里再次调用了首页的检测逻辑
但这里经过多次刷新后发现,此处的eval()表达式执行的函数名是随机变换的。因此前步涉及到的直接替换函数在这里就不起作用了。这个时候就需要跟进函数,发现这个随即名称的函数是针对随机输入的固定base64加密,最后输出debug函数
那么此处只要单步调试,打断点定在程序执行加密前,将输入赋空值,这样输出必定为空,则可以绕过debug
其他
从慕课网的源码里也发现了一些信息,诸如内网ip或者一些网站设置的一些弱密码。(此类信息一般很好分析,大多源码里会被加上注释╮(╯▽╰)╭)
同时JS混淆还包括多种加密,加密方法需要视具体源码所定,其加密可能是传统加密亦或者编写者进行过一些调整。这些都需要调试者去尝试判断
具体分析依旧回归源码,诸如实验一的某东JSencode加密
防护
以IPS特征库登录界面登录逻辑为例
首先看一下一个成功登录的流量
再看一下失败登录的流量,可以发现没有调试的情况下流量不会向相关js文件发送请求
走正常的登录流程,可以发现,只是向登录接口发送了一个post请求
分析payload发现发送Username和Password,本例没有加密(常规会对此加密),为作说明同样全局搜索Password,寻找可疑处打断点
当给源码打上断点后,再次刷新登录,可以发现向断点处接口jquery.min.map也发送了请求。根据源码注释也也可以发现,我们打断点的位置正好对应了请求的url
防护
针对此类调试情况,可以根据流量是否向项目结构内js网页发送请求判断是否正在js调试。此类防护IPS规则不好写受用户自定义的命名限制,建议自定义添加规则,针对隐私/重要的js的url地址提取content进行JS调试的防护
结合上文提到的反调试,即跳出eval赋值表达式函数。替换网页js源码。
防护
针对反调试函数赋空可以总结eval(function xxx{})此类格式,发现替换js页面请求里会没有这个url
尝试替换ace-extra.min.js
替换页面,没有该url
无替换,出现
尝试通过浏览器的覆盖功能,可以出现修改的请求,但流量内依旧没有改写后的字符
因此,对于替换类的解混淆,无法提取规则。需要依据上文提到的特殊url访问去进行js解混淆的防护
但在调试时,response会返回经过混淆的代码。可以根据混淆代码的一些固定格式去提取特征,进行防护
总结
JS混淆一般被用于反爬虫和信息保护,不过只要善用工具和足够的耐心,结合一些工具就可以从源码里收获很多的有用信息
对于攻击者进行js调试时,网站的代码需要考虑从如何反调试的角度去思考
作为js解混淆的使用者出发,需要了解和积累解混淆的经验,才能应对更多的反爬虫,收集到更多的爬虫信息