JS解混淆

JICEY發表於2024-04-12

JS解混淆

最近在整理之前和一些同伴的分享資料,發現時間已經過了好久,特此整理一些有價值的分享記錄。

JS混淆

學習js混淆可以逆向分析混淆和加密過程,實戰可用於爬蟲和滲透資訊獲取

本文件用於初步介紹js混淆的基礎概念以及如何解混淆、除錯,便於幹掉反爬蟲和滲透資訊收集思路擴充

概念解釋

混淆/加密

降低程式碼可讀性加強安全性,防止被人任意檢視,在一定程度保護資源

理想的混淆或加密應該具備如下特點

1、沒有確定的破解模式;

2、很難編制自動破解程式(只能手工破解);

3、破解過程繁瑣、耗時;

4、“混淆|加密”後的程式碼,比原始程式碼長度增加少;

程式碼裡諸如此類就是經過了混淆的結果,可以透過console+斷點打出來看看值

image-20220519164011809

image-20220519164047706

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:替換髮包和請求內容

image-20220526144505490

  • WT-JS_DEBUG:可以直接除錯或美化js程式碼,同時附帶多種解密

    image-20220526144522616

除錯

alert除錯

聯網剛剛起步的時代,網頁前端還主要以內容展示為主,瀏覽器指令碼還只能為頁面提供非常簡單的輔助功能

那個時候,網頁主要執行在以IE6為主的瀏覽器中,JS的除錯功能還非常弱,只能透過內建於Window物件中的alert方法來除錯

另一方面,alert的除錯資訊,必須在程式邏輯中新增類似"alert(xxxxx)"這樣的語句,才能正常工作,並且alert會阻礙頁面的繼續渲染

這就意味著開發人員除錯完成後,必須手動清除這些除錯程式碼

console除錯

新一代的瀏覽器Firefox、Chrome,包括IE,都相繼推出了JS除錯控制檯,支援使用類似"console.log(xxxx)"的形式,在控制檯列印除錯資訊,而不直接影響頁面顯示

image-20220519155900045

如果在使用console物件之前先進性存在性驗證,其實不刪除也不會對業務邏輯造成破壞

為了程式碼整潔,在除錯完成後,還是應儘可能刪除這些與業務邏輯無關的除錯程式碼

Chrome開發團隊為Chrome瀏覽器擴充了更豐富的功能,具體操作可以使用Chrome瀏覽器

斷點除錯

JS斷點除錯,即是在瀏覽器開發者工具中為JS程式碼新增斷點,讓JS執行到某一特定位置停住,方便開發者對該處程式碼段的分析與邏輯處理。為了能夠觀察到斷點除錯的效果

給一段程式碼新增斷點的流程是"F12(Ctrl + Shift + I)開啟開發工具"——"點選Sources選單"——"左側樹中找到相應檔案"——"點選行號列"即完成在當前行新增/刪除斷點操作

當斷點新增完畢後,重新整理頁面JS執行到斷點位置停住,在Sources介面會看到當前作用域中所有變數和值,只需對每個值進行驗證

此處選中第五行,再次重新整理頁面即將執行到此處

image-20220519160502399

重新整理之後的效果

image-20220519160715970

可以發現右側有這樣一行工具欄

image-20220519160739806

工具欄從左到右各圖示的功能分別如下:

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元素上新增斷點,進而達到除錯的目的

程式碼展開

如果頁面原始碼顯示單行,可以點選左下角的大括號展開,更為直觀的瀏覽程式碼

image-20220519163056019

結果如下圖

image-20220519163118540

搜尋關鍵字

在頁面內透過ctrl+F,可以出現搜尋框

image-20220519163158080

反除錯

一些網站會透過監控網頁視窗的長寬高以此監視是否開啟除錯模式以此來進行反除錯,對此需要將devtool獨立出來

image-20220523161109942

具體請點選右上角的三個點,選擇第一行的左邊第一個按鍵,即可將除錯視窗獨立

image-20220523161100341

實驗

普通解混淆

以某東的登入為例子

image-20220523175721059

隨便輸入資料找到post

image-20220523175637616

看一下payload這裡可以發現密碼nloginpwd是經過加密的,還有一個pubKey和sa_token。那麼需要解決的加密方式就是這三個

image-20220523175711706

全域性搜尋nloginpwd,判斷一下可能的位置,定斷點重新整理一下,定過來了

image-20220523175817583

可以看到這裡有個data,console打出來看看能和我們抓到的Post匹配上,可以發現這裡的pubKey和sa_token是寫死的

image-20220523175841152

可以看到這裡對於nloginpwd有個getEntryPwd函式,應該是對此進行了加密,跟進去看一下。這裡可以列印一下getEntryPwd的賦值,可以發現是我們輸入的原密碼

image-20220523180022647

image-20220523180142991

根據名字看一下,這裡賦值pubKey,同時進行一個JSEncrypt的操作,跟進去看一下

可以發現這段特別長,直接將整段copy出來嘗試執行

image-20220523180419152

可以發現這個程式碼是經過加密的,首行直接說明了。這種情況建議copy下來透過直接WT-JS_DEBUG嘗試執行

image-20220523180622563

程式碼copy過來發現有一些變數沒有賦值,這裡在首行直接賦空值,保證程式碼順利執行就行

image-20220526103318093

賦值之後重新執行可以看到載入成功

image-20220526103345375

回溯原始碼理一下整個加密流程,將其串起來結合copy的加密演算法寫個解密過程。發現和抓到的密碼是不一樣的,而且每次執行結果都不一樣,懷疑該加密跟時間有關

image-20220526104621935

在某東的登入再用同樣的密碼登入幾次嘗試,發現每次加密後的密碼也不一樣

image-20220526104654874

image-20220526104705667

反除錯解混淆

一些網站可能透過監視螢幕的寬高比,判斷是否開啟開發者工具而禁止除錯,或者直接禁用F12。

這種情況以PM2.5實時查詢|PM2.5歷史資料查詢|PM2.5全國城市排名|PM2.5霧霾地圖|中國空氣質量線上監測分析平臺|真氣網 (aqistudy.cn)為例舉例說明如何反除錯跳出debugger()函式

image-20220526110805867

嘗試F12開啟除錯視窗,直接被彈窗禁止了。這樣就需要手動透過更多工具-開發者人員工具調出除錯臺。同時將其分離成獨立視窗

image-20220526110818945

可以看見因為開始除錯直接進入debugger反除錯

image-20220526110955108

可以發現這個網頁因為反除錯網路啥資訊都沒有了(Φ皿Φ),這個時候可以透過視窗旁邊的呼叫堆疊看看這個debugger是從哪裡彈出來的

image-20220526111759296

發現是一個txsdefwsw和c,進去看一下

image-20220526111950633

追溯c的原始碼過去看看沒有發現什麼有用的資訊,感覺程式棧不完整沒有捕獲到相關函式。重新重新整理頁面一下,發現新內容

image-20220526131827671

(_)發現了反除錯的程式碼,發現首頁的原始碼。這裡還包含一些其他反除錯的檢測。那麼就是在這裡觸發的debug。首頁當檢測到非法除錯之後,觸發txsdefwsw()函式,全域性搜尋一下這個函式。

image-20220526131851285

發現這裡是透過eval()函式去執行一個function,(eval函式可以執行表示式,具體深入可以自行google)根據註釋可以發現這裡有一個debug的檢測,那麼看一下這兩個eval裡的function是什麼。

image-20220526132427283

控制檯透過var列印一下這兩個function的返回值,可以發現兩個eval分別執行endebug和txsdefwsw兩個函式。

image-20220526133402582

image-20220526143514050

image-20220526143557326

思路就是透過替換這個eval函式的執行函式,讓他執行一個空值的函式從而跳過這裡的debug函式。可以用工具reres去替換這個js連結為本地經過改寫的js檔案。這樣網頁執行時,呼叫同名的空函式則不會觸發debug。

image-20220526143611477

image-20220526143626478

可以發現替換後原始碼的js變成了這樣

image-20220527140449510

再次重新整理還是debug,(╯▔皿▔)╯跟過去發現這裡還有一層反除錯

image-20220527142212473

console列印一下,發現這裡再次呼叫了首頁的檢測邏輯

image-20220527142357030

但這裡經過多次重新整理後發現,此處的eval()表示式執行的函式名是隨機變換的。因此前步涉及到的直接替換函式在這裡就不起作用了。這個時候就需要跟進函式,發現這個隨即名稱的函式是針對隨機輸入的固定base64加密,最後輸出debug函式

那麼此處只要單步除錯,打斷點定在程式執行加密前,將輸入賦空值,這樣輸出必定為空,則可以繞過debug

其他

從慕課網的原始碼裡也發現了一些資訊,諸如內網ip或者一些網站設定的一些弱密碼。(此類資訊一般很好分析,大多原始碼裡會被加上註釋╮(╯▽╰)╭)

image-20220526144753813

同時JS混淆還包括多種加密,加密方法需要視具體原始碼所定,其加密可能是傳統加密亦或者編寫者進行過一些調整。這些都需要除錯者去嘗試判斷

具體分析依舊迴歸原始碼,諸如實驗一的某東JSencode加密

防護

以IPS特徵庫登入介面登入邏輯為例

image-20220527113411921

首先看一下一個成功登入的流量

image-20220602091141109

再看一下失敗登入的流量,可以發現沒有除錯的情況下流量不會向相關js檔案傳送請求

image-20220527164925675

走正常的登入流程,可以發現,只是向登入介面傳送了一個post請求

image-20220602091237436

分析payload發現傳送Username和Password,本例沒有加密(常規會對此加密),為作說明同樣全域性搜尋Password,尋找可疑處打斷點

image-20220527115855569

當給原始碼打上斷點後,再次重新整理登入,可以發現向斷點處介面jquery.min.map也傳送了請求。根據原始碼註釋也也可以發現,我們打斷點的位置正好對應了請求的url

image-20220527113714521

image-20220602091334186

防護

針對此類除錯情況,可以根據流量是否向專案結構內js網頁傳送請求判斷是否正在js除錯。此類防護IPS規則不好寫受使用者自定義的命名限制,建議自定義新增規則,針對隱私/重要的js的url地址提取content進行JS除錯的防護

結合上文提到的反除錯,即跳出eval賦值表示式函式。替換網頁js原始碼。

防護

針對反除錯函式賦空可以總結eval(function xxx{})此類格式,發現替換js頁面請求裡會沒有這個url

嘗試替換ace-extra.min.js

替換頁面,沒有該url

image-20220602091421892

無替換,出現

image-20220602091458230

嘗試透過瀏覽器的覆蓋功能,可以出現修改的請求,但流量內依舊沒有改寫後的字元

image-20220527145645472

image-20220527145655393

因此,對於替換類的解混淆,無法提取規則。需要依據上文提到的特殊url訪問去進行js解混淆的防護

但在除錯時,response會返回經過混淆的程式碼。可以根據混淆程式碼的一些固定格式去提取特徵,進行防護

總結

JS混淆一般被用於反爬蟲和資訊保護,不過只要善用工具和足夠的耐心,結合一些工具就可以從原始碼裡收穫很多的有用資訊

對於攻擊者進行js除錯時,網站的程式碼需要考慮從如何反除錯的角度去思考

作為js解混淆的使用者出發,需要了解和積累解混淆的經驗,才能應對更多的反爬蟲,收集到更多的爬蟲資訊

相關文章