JS解混淆 最近在整理之前和一些同伴的分享資料,發現時間已經過了好久,特此整理一些有價值的分享記錄。 JS混淆 學習js混淆可以逆向分析混淆和加密過程,實戰可用於爬蟲和滲透信息獲取 本文檔用於初步介紹js混淆的基礎概念以及如何解混淆、調試,便於幹掉反爬蟲和滲透信息收集思路拓展 概念解釋 混淆/加密 ...
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解混淆的使用者出發,需要瞭解和積累解混淆的經驗,才能應對更多的反爬蟲,收集到更多的爬蟲信息