免责声明:本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!
前言 本篇文章通过一个案例介绍JS逆向过程中抠JS之后的补环境工作。
逆向过程 目标网址:aHR0cDovL3d3dy5pd2VuY2FpLmNvbS91bmlmaWVkd2FwL3Jlc3VsdD93PSVFNyVCQiVCRiVFOCU4OSVCMiVFNyU5NCVCNSVFNSU4QSU5QiVFNiVBNiU4MiVFNSVCRiVCNSZxdWVyeXR5cGU9c3RvY2s=
逆向目标:cookie中的v值
Hook Cookie 对于处理Cookie种某一个键值对生成这类问题第一反应应该是想到采用Hook的方式。这里介绍2种Hook的方式,一种是通过FD编程猫插件,一种是通过油猴插件。
使用油猴插件
油猴插件的使用参照:JS逆向之Tampermonkey工具篇
插件内容为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 (function ( ) { 'use strict' ; var cookie_cache = document .cookie ; Object .defineProperty (document , 'cookie' , { get : function ( ) { console .log ('Getting cookie' ); return cookie_cache; }, set : function (val ) { console .log ('Setting cookie' , val); if (val.indexOf ("v=" ) != -1 ) { debugger ; } var cookie = val.split (";" )[0 ]; var ncookie = cookie.split ("=" ); var flag = false ; var cache = cookie_cache.split ("; " ); cache = cache.map (function (a ){ if (a.split ("=" )[0 ] === ncookie[0 ]) { flag = true ; return cookie; } return a; }); cookie_cache = cache.join ("; " ); if (!flag) { cookie_cache += cookie + "; " ; } this ._value = val; return cookie_cache; } }) })();
使用FD编程猫插件
FD编程猫插件用法参照:JS逆向之Fiddler编程猫插件使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 (function ( ) { 'use strict' ; var cookieTemp = "" ; Object .defineProperty (document , 'cookie' , { set : function (val ) { if (val.indexOf ("v=" )) { debugger ; } console .log ('Hook捕获到cookie设置->' , val); cookieTemp = val; return val; }, get : function ( ) { return cookieTemp; } }); })();
逆向分析 无论是采用哪一种方式,都可以成功断住,如下图:
image-20220412203406372
跟栈,进到o方法,发现第二个参数t就是需要的v的值。
image-20220412213516531
继续跟栈,进到D方法,setCookie就是上边的o方法,第二个参数n就是上边的t也就是Cookie v的值。而且n是由rt.update生成的。
image-20220412213620698
我们看下rt对象:
image-20220412214401314
一个Init方法应该是对算法进行初始化,一个update方法则是生成v。
我们断进去update方法,update方法就是这个D方法,D方法又调用了O方法,我们跟进去。
image-20220413133045178
我们单步执行到S.toBuffer();可以看到S是一个l对象,并且包含一个base_fields属性。如下图:
image-20220413133536272
那我们点进去S.toBuffer看看能不能找到l对象的原型,qn返回了l也就是前面的S,我们住需要抠出qn就可以得到l了,注意的是qn本来就执行了,返回一个逗号表达式,逗号表达式的最后是l,所以生成l对象的正确写法是new qn(xxx)
。
image-20220413152301870
l对象在初始化的时候需要传一个r参数,我们全局搜索一下new qn看看是否能找到一个实例,看看这个r传的是啥。搜索结果如下:
image-20220413153204291
我们断住这个地方,调试发现a固定为4,n固定为1,e固定为3,t固定为2。知道了如何抠出S,也知道了如果初始化S,下面我们进行检验。我们新建一个snippet然后拷贝全部的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 var _qn;var TOKEN_SERVER_TIME = 1649765993.630 ;!function (n, t ) { !function ( ) { var r, e, a; var qn = function ( ) { var n, t, r; n = t = r = a; var e, o, i; e = o = i = s; var u = o[15 ] , c = o[102 ] , f = e[103 ]; function l (r ) { var a = o[102 ] , i = e[103 ]; this [n[76 ]] = r; for (var u = t[52 ], c = r[a + g + i]; u < c; u++) this [u] = t[52 ] } return l[e[104 ]][w + m + I + u] = function ( ) { for (var a = e[105 ], u = this [a + y], c = [], s = -e[0 ], v = o[2 ], f = u[r[56 ]]; v < f; v++) for (var l = this [v], p = u[v], d = s += p; c[d] = l & parseInt (t[77 ], n[78 ]), --p != r[52 ]; ) --d, l >>= parseInt (n[79 ], i[106 ]); return c } , l[v (t[80 ], t[81 ], b)][ot (i[107 ])] = function (n ) { for (var r = e[8 ], a = this [ot (e[108 ], e[109 ])], o = t[52 ], u = e[2 ], s = a[c + r + f]; u < s; u++) { var v = a[u] , l = i[2 ]; do { l = (l << t[82 ]) + n[o++] } while (--v > t[52 ]); this [u] = l >>> i[2 ] } } , l }(), zn; _qn = qn; }() }(["" , 9527 , "V587" ]);
定一个全局变量_qn,导出qn。运行文件,没有报错,然后我们生成一个S对象:
image-20220413154327048
这里我们就成功的生成了S,并且可以看到decodeBuffer方法和mm.toBuffer方法也都有。别忘了我们的最终目标是得到Cookie中的v,我们回到前面的O方法,看初始化并得到S之后,后续生成v的逻辑。我们看到Jn.serverTimeNow()
这一行代码,发现每次取出来的都是一个定值。
image-20220413155910667
其实这个获取的就是该JS文件最前面定义的TOKEN_SERVER_TIME,这个值可能会随着JS文件更新而发生变化,所以我们采取局部扣JS的方法可能会很麻烦,因为可能需要定期更新TOKEN_SERVER_TIME的这个值。那只能全扣了啊,问题是抠下来全部的JS,放在本地执行之后,去哪里拿这个Cookie呢?答案是从document中拿,但是本地没有document呀,所以接下来就是补环境了。
image-20220413161327925
补环境 按照JS逆向之vscode无环境联调 介绍的,把整个JS代码粘贴到VS Code中,然后开启DevTools,运行之后报document不存在:
image-20220413164041346
像这种拷贝整个JS然后补环境,需要补头补尾,中间的整个JS文件不能动,这样做的好处是中间的文件可以用一个占位符表示,以后每次JS更新了,只需要更新这个占位符的内容,这样更加通用,维护起来更加容易
我们补好window和document之后,再次运行,接着报错:
image-20220413165734237
这里r[51]是document,报错的一句r[51].getElementsByTagName(p + d)[r[52]]实际上是document.getElementsByTagName(‘head’)[0];继续补代码如下:
1 2 3 4 5 6 7 8 9 10 var window = this ;var Document = function ( ) {};Document .prototype .getElementsByTagName = function (x ) { if (x == 'head' ) { return [{}] } return [{}] }; var document = new Document ();
解释一下为什么这么补,补方法的时候关注3点,一是参数个数,二是返回值类型,三是对实际传入的参数进行特殊处理。因为getElementsByTagName只接受一个参数,所以只需要定义一个形参x。
image-20220413185332326
通过在原网站上调试知传入的实际参数为字符串head,且返回的是一个对象数组。所以上边也对head进行了特殊处理,而且方法的返回值是对象数组。
补好getElementsByTagName方法后,接着运行,接着报错。
image-20220413202853960
接着补createElement:
1 2 3 4 5 Document .prototype .createElement = function (x ) { if (x == "div" ) { return function onwheel ( ) {}; } };
为什么这么补?我们看下面一张图,作说明:
image-20220413203850845
首先很显然createElement要补到Document对象下,然后s[171]为div,并且调用createElement后要返回一个onwheel方法。
补好之后,运行接着报错,如图:
image-20220413204248014
n.attachElement没有定义,补一下:
1 2 3 Document .prototype .attachEvent = function (x, y ) {};
补好之后,依旧报错,嗯!逆向分析就是需要耐心:
image-20220413212157946
补navigator,可以看到navigator.plugins是一个对象数组,我们这里补一个空就行。
image-20220413213604130
1 2 3 Navigator = function ( ) {};Navigator .prototype .plugins = [];navigator = new Navigator ();
补好之后,运行接着报错:
image-20220413214446556
我们看看这个几个变量是什么,如图:
image-20220413214519986
我们找一下l的声明处,如图:
image-20220414235906611
可以看到l是取自window中的document,所以我们把document作为属性放到window对象下:
1 2 document = new Document ();var window = {"document" : document };
补好之后,再次运行,再次报错:
image-20220415000927626
这里就很奇怪了,因为原始网站这里应该是直接进去上边的if分支,而不是进到这里的else if判断。我们跟进去这个方法m,看看这个m是什么?
image-20220415001131494
可以看到o是localStorage,s[83]是window对象,这里的意思是判断window对象下是否存在localStorage,并判断这个属性是否为空。我们自己定义一个localStorage给到window。
1 2 3 LocalStorage = function ( ) {}localStorage = new LocalStorage ();window = {"document" : document , "localStorage" : localStorage };
补好之后运行,接着报错:
image-20220415001954038
这里的f是localStorage,缺少getItem,我们补一下:
1 2 3 4 5 6 LocalStorage .prototype .getItem = function (x ) { if (x == "hexin-v" ) { return null ; } return null ; }
我们通过原网站,看到该方法接受一个参数,并且只调用了一次,传的参数是hexin-v,返回null,我们照着补就行。
补好之后,接着运行:
image-20220415002404829
通过调用栈我们看看这个n是啥?n实际是上层函数传入的参数,即navigator.userAgent,原始网站有值,我们没有定义。
image-20220415003226879
我们补上userAgent:
1 Navigator .prototype .userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36" ;
补好之后,运行报错,如下图:
image-20220415003446318
通过原始网站知,javaEnabled方法返回false,那我们也给navigator对象加一个方法javaEnabled返回false即可:
1 Navigator .prototype .javaEnabled = function ( ) { return false };
补好之后运行报错:
image-20220415003756208
这里a[65]是window对象,a[175]是navigator对象,上面报错是说window.navigator为undefine,我们把navigator作为window的属性即可,修改上面补环境的代码:
1 var window = {"document" : document , "localStorage" : localStorage , "navigator" : navigator};
改好之后运行:
image-20220415004026612
提示location未定义,我们定义一个location对象:
1 2 Location = function ( ) {}location = new Location ();
接着运行,依然报错:
image-20220415004230766
这里的c[140]是href,location.href为空,我们查看原始网站知,location.href就是当前的页面地址,我们补上即可:
1 Location .prototype .href = "http://www.iwencai.com/unifiedwap/result?w=%E7%BB%BF%E8%89%B2%E7%94%B5%E5%8A%9B%E6%A6%82%E5%BF%B5&querytype=stock" ;
接着运行:
image-20220415004504041
提示location.hostname不存在,我们根据原网站取到location.hostname的值补上:
1 Location .prototype .hostname = "www.iwencai.com" ;
补好运行,报错:
image-20220415004845137
提示localStorage的setItem方法不存在,得嘞,补一个空方法:
1 2 3 LocalStorage .prototype .setItem = function (x, y ) {}
补好之后运行,没有报错!!!
此处应该有掌声表情包- 搜狗图片搜索
我们取一下cookie的值,也成功看到了v:
image-20220415005229606
上面补环境的代码比较零散,这里统一整理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 Document = function ( ) {};Document .prototype .getElementsByTagName = function (x ) { if (x == 'head' ) { return [{}] } return [{}] }; Document .prototype .createElement = function (x ) { if (x == "div" ) { return function onwheel ( ) {}; } else if (x == "canvas" ) { return {getContext : function (y ) { return undefined ; }} } }; Document .prototype .attachEvent = function (x, y ) {};Document .prototype .documentElement = { addBehavior : function ( ) {} }; document = new Document ();Navigator = function ( ) {};Navigator .prototype .javaEnabled = function ( ) { return false };Navigator .prototype .plugins = [];Navigator .prototype .userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36" ;navigator = new Navigator (); Location = function ( ) {};Location .prototype .href = "http://www.iwencai.com/unifiedwap/result?w=%E7%BB%BF%E8%89%B2%E7%94%B5%E5%8A%9B%E6%A6%82%E5%BF%B5&querytype=stock" ;Location .prototype .hostname = "www.iwencai.com" ;location = new Location (); LocalStorage = function ( ) {}LocalStorage .prototype .getItem = function (x ) { if (x == "hexin-v" ) { return null ; } return null ; } LocalStorage .prototype .setItem = function (x, y ) {} localStorage = new LocalStorage ();var window = {"document" : document , "localStorage" : localStorage , "navigator" : navigator};
总结 本文通过一个例子,介绍了手动补环境的过程,总结如下:拷贝整个JS然后补环境,需要补头补尾,原则上中间的整个JS文件一点也不能动,这样做的好处是中间的文件可以用一个占位符表示,以后每次JS更新了,只需要更新这个占位符的内容,这样更加通用,维护起来更加容易。
关于代码的获取 若需要代码,扫描加微信即可。