商标0401版某数VM分析及纯算法还原介绍
1 前言
1.1 前情回顾
2022年4月1日某数推出了新版反爬方案,用在了商标和万海两个网站上,逆向难度可以说是全方面的升级。具体的难度增加有三点:
1. 控制流平坦化的部分加了很多虚假分支及三目运算符,反混淆难度大大增加了。
2. 增加了一个十分成熟的VM保护,调试难度又比单纯的控制流平坦化增加了很多。
3. 检测流程极大改变了,虽然一些设计还是和原来相似,但是即使熟悉之前版本的逆向套路,也对本版本的逆向帮助不大。
出于对新版本的好奇,我对这两个网站的代码进行了还原,然后扣了下商标的算法。最后感慨一句,以后还是无脑补环境吧,比起纯算要省力太多了。。
在这个基础上,和大家分享一下新版的逻辑。逻辑到了这种的复杂程度,还有VM的阻隔,只是我纸面的文章其实已经不能为大家讲全逆出成品的方方面面了,但是应该可以在破除陌生感和实战时大量减少试错成本两方面给大家一些帮助。
本次主要分享cookie的纯算法逆向过程,为了直观,这次还是用反混淆、反编译后的代码去介绍。
1.2 目标网址
商标全站的某数都是一套逻辑,所以我们找个好验证的链接来说,该链接第一次请求获得412响应状态码,第二次携带正确生成的cookie请求可得200状态码,如果生成的cookie错误会返回400状态码,这方便我们验证生成的cookie是否正确。
链接(需要用BASE64解码):aHR0cDovL3dzZ2cuc2JqLmNuaXBhLmdvdi5jbjo5MDgwL3RtYW5uL2FubkluZm9WaWV3L2hvbWVQYWdlLmh0bWw=
1.3 另外,插条广告
顺便夹带点私货,最近要看下新的工作机会,不过对目前求职行情不太了解,所以希望有内推渠道的大佬帮推一把。
期望城市:北京,目前更擅长Web逆向,App逆向要弱一些。
有内推渠道的佬私我要下简历,py一波,,感激不尽!哈哈哈。
下面开始正式的逆向介绍吧
2 某数VM介绍
2.1 前置知识
前段时间我已经借助企鹅VM写过一篇很详细的介绍VM的文章了,所以重复的内容此处就不提了,留个链接大家没看过的先看下:
《VM防护介绍及企鹅滑块分析》:https://mp.weixin.qq.com/s/C8gB-D6EUliPXoMgjk0Bag
这次主要是介绍一下某数的VM整体设计以及之前文章没介绍到的部分:作用域管理和寄存器。
2.2 某数VM整体设计
2.2.1 定位VM函数
在eval
里的js
中搜一下字符串r2mKa0
,就可以定位到这个VM函数,而r2mKa0
及其后跟的那一堆字符串,后续会解析出VM使用的指令数组。
2.2.2 某数VM函数的参数介绍
每次请求,变量名数组会变,可解析出指令数组的字符串、字符串常量和数值常量都是固定的,也就是说这个VM其实是比较静态的,检测逻辑每次不变,各检测部分的排列顺序也每次不变,这和我上篇文章介绍的企鹅VM有很大的不同。
所以逆向过程中,在参数部分只需要关注变量名数组(作用:根据名称匹配某些值)和全局变量数据(作用:通过全局变量数组跨越各VM函数的作用域)即可。
2.2.3 某数VM函数各组成部分
2.2.3.1 VM全局初始化部分
首先看到的是简单的几行代码及三个函数,函数的作用见上图注释,其中_Format1922
函数继续进行全局初始化
如上图所示,进入`_Format1922`函数中,看下VM全局初始化部分的详细步骤。
上图为以反编译的视角看_Format1922
函数。
2.2.3.2 子函数初始化部分
_Format2941
函数为子函数初始化部分,详细分析继续见上图中注释。
还有上图中_Format2942
、_Format2942
这两个函数,简单提一下
2.3 寄存器
2.4 栈帧及作用域管理
2.4.1 什么是作用域栈和栈帧
从2.2.3.2的图中可以看出每个子函数都有一个空数组作为自己的作用域,每个子函数还有一个包含父函数作用域的作用域栈,作用域栈中的每个元素都是栈帧。
2.4.2 作用域栈和栈帧的作用是什么
步骤2.3
中介绍的寄存器是每个函数都独立的,只能存储临时变量供自身函数内使用。如果想将临时变量传递给子函数使用,就需要作用域栈了。
我们举例说一下,子函数看到的作用域栈如上图所示。其中父函数作用域中有三个元素,是父函数执行过程中保存的。那么在执行到调用子函数的逻辑时,进入子函数后,子函数就可以通过作用域栈.slice(-2)[0]
来获取父作用域的变量,然后进行后续运算。
比如子函数通过执行作用域栈.slice(-2)[0][1]+子函数作用域[0]
这样的语句,就可以实现父函数的中间变量2+子函数的中间变量1
这个逻辑了。
3 COOKIE部分纯算法逆向逻辑分析
3.1 整体概览
3.1.1 html部分
只有对meta标签和$_ts变量初始化的逻辑
3.1.2 单独请求的那个js部分
由上面三张截图可见,和旧版的某数一样,该js文件的逻辑就是拼出eval用到的js字符串、构造出$_ts对象。
还是搜索call(
定位到进入eval
的逻辑。
这部分比较简单,就不详细说了。
3.1.3 eval中js部分
如上图,最开始定义了一堆变量,还有一些没用的花指令(有||
的那些)。
填充了6个数组,构造了下全局数组,然后就直接进VM中了,说明主流程全都在VM里。
上图在2.2.3.1
中已经展示过了,就是VM初始化逻辑,将一些子函数压入全局数组中,供VM外的函数调用。
然后回退到上一步,进入_Format747(157)
函数中看下,如上图所示,发现要继续进_$ce()
中。
如上图,进入_$ce()
后可以看到一个7位数组和一个12位数组concat
出了一个19位数组。
这19位数组的每个元素都是对象,且_$ce
属性都为函数,然后循环执行这19个函数。
观察上图可发现那个12位数组中除了_$ce
属性外,还统一有很多其他的属性,这里先提一句,后面这12位数组还会使用,_$ce
函数的调用只是这12个元素的初始化过程,后面还会循环执行各元素的另一个属性。
而那个7位数组也很重要:
比如七位数组第1个元素的_$ce
函数,是初始化了上图所示的一个35位的数组,这个数组由$_ts.cd
属性转换得来,这个数组中的数组元素可以被一些函数转成字符串,时间戳之类的,纯算法还原的过程中的很多变量都来自这个数组。
又比如七位数组第3个元素的_$ce
函数,是将首页html
中meta
标签的content
属性解析成了上图所示的_Format858
数组,这个数组中的某个元素经过转化就得到了后缀。
了解逻辑的过程中,大概跟一下7位元素的数组,做到心中有数就行,12位元素的那个数组后面用到哪个再具体跟。
看完19个函数循环执行后,我们再进入上图中var1595
表示的函数,这个函数里就开始正式生成cookie
了,我们继续进入。
激动人心的时候到了!可以看到,这个函数里生成了一个四位数组,然后将这个四位数组传入一个函数中,就生成我们想要的cookie了。
所以,我们最终最终的目标,其实就是搞清楚这个4位数组的每个数都是怎么得来的就大功告成了。
下面,我们就来仔细分析下这个四位数组的组成。
3.3 四位数组组成分析
随便从浏览器中复制一个四位数组出来当例子:
[
[98,110,84,121,98,107,45,21,0], [3,14,1,0,33,128,130,69,112,40,5,87,105,110,51,50,10,19,1,1,98,110,84,121,154,21,224,200,0,2,167,252,55,4,54,184,4,7,12,1,0,0,0,0,0,0,0,23,12,219,196,0,1,0,2,4,240,103,180,100,9,2,8,0,6,15,1,0,0,0,0,23,245,247,142,70,98,2,0,0,0,13,1,0], [155,221,202,57,17,200,34,98,12,201,17,4,52,111,242,42,73,64,6,62,43], 1
]
3.3.1 第一个数组
很简单,就是取了个当前时间戳,然后从3.1.3
中说的那个35位数组中取出一个元素转成了时间戳,然后做了下运算,然后就产生了第一个数组。
3.3.2 第二个数组
可以看到它生成逻辑是又循环了那个12位数组中的函数,然后每个函数都会返回一个数组。最后把12个数组拼起来就是第二个数组了。
因为这12个数组中有的函数执行是会返回空数组的,有的函数还会返回不固定长度的数组,所以第二个数组的组成设计的很有规律,规律如下:
push(id)
push(id对应的函数返回数组的长度)
push进实际的数组
我们举个例子:
步骤3.3开头的那个4位数组,它第二位数组开头的 `3,14,1,0,33,128,130,69,112,40,5,87,105,110,51,50`,组成过程如下:
3:函数的id
14:该函数产生的指纹数组长度
后面14个元素:就是该函数实际返回的。
下面说一下,只关注生成cookie的逻辑时,需要看的函数id和id产生的数组中,每一位元素的来源吧
id:3,
length:14,
1,:chrome.runtime
0,:?
33,:eval.toString()
128,:对error的检测?
130,69,112,40,:对ua的计算
5,87,105,110,51,50,:navigator.platform
id:10,
length:19,
1, 第一次赋值:默认0,第二次赋值 0|1
1, 程序里写死的
98,110,84,121, _Format590((_Format747_745()+tm1)-tm2)
154,21,224,200, _Format590(_Format601(19))
0,2,167,252,55,4,54,184,: 时间戳、常量数字数组、随机数进行少量运算
4, :_Format601(_Format764[39]);
id:7,
length:12,
1,0,0,0, 多个部位的|运算,暂时写死16777216
0,0,0,0, 初始化0,后未被修改
23,12, 数字常量数组中几个元素组成一个数组,做了些步骤较多的运算
219,196, 第2个控制流数组所在的函数tostring+58个函数列表里动态取一个函数tostring做了少量运算
id:0,
length:1,
0, 默认值0,没走其他逻辑
id:2,
length:4,
240,103,180,100, 熟悉的20取4
id:9,
length:2,
8, 第一次赋值0,第二次赋值:0|数字常量数组[12]
0, 第一次赋值0
id:6,
length:15,
1,第一次赋值:0,第二次赋值0|1
0,0,初始化0
0,0,NaN计算
23,245,247,142,70,98,2,0,:那个解析出的数字数组做的一系列运算
0,0, 初始化0
id:13,
length:1,
0 : 初始化0
上面的笔记只是生成cookie时的,很多值还是初始化状态。刷新cookie或生成后缀时,这里面的某些值会因为已经赋值了而发生变动。
至于具体的逻辑,限于篇幅,就需要大家慢慢跟一下了,或者遇见哪个问题来群里讨论即可。
3.3.3 第三个数组
如上图所示,就是老某数版本最后那个把一个数组每个元素都异或的逻辑。单步跟一下就好了。
3.3.2 第4个元素
第4个元素1,是程序里直接写死的。
4 结尾
4.1 小结
经过上面的介绍,相信大家已经对新版某数的逻辑有了较详细的了解了,下一步,实战起来就完事了。
至于后缀的生成逻辑,我们后面的文章再约~~
4.2 callback
文章看完了,但我的广告别忘了啊!北京有内推的佬私我要下简历,帮我内推一波啊!哈哈