要点解析
访问 https://geetest.com/demo,可以看到很多的demo,这些模式混淆方式大多大差不差,因此这里我们选择最简单的滑块slide-bind模式来进行测试,这也是b站采用的方式(*现已更换为点选)
验证码逆向和加速乐这类的waf参数逆向过程不一样,需要事先注意一下提交的参数是否为本地生成,不要一上来就直接到源码里搜参数,然后分析半天,最后发现参数在xhr的请求结果里。验证码一般在起始阶段,会获取一个被称为challenge
的东西,这个一般就是网络请求返回的验证码题目标识,我们需要直接发送请求,先得到对应的数据,再去定位剩余加密参数。如图,这里的challenge和gt就是由之前的/register-slide
得到的数据,因此我们在这里只需要重点关注w参数
常用的查找加密参数的方式有搜索、跟栈、hook,我们该如何开始入手呢?这就不得不提到验证码有个特性了,目前遇到的大多数验证码都会利用jsonp来实现跨域请求,jsonp会携带请求参数请求接口,与普通的http请求不同的是,浏览器会自动拿返回的数据,执行指定的回调函数,因此我们可以通过直接hook jsonp的回调,来查看核心的执行逻辑。
为什么会使用jsonp呢?因为浏览器由于同源策略,无法直接请求与host不同的域名,因此,各大验证码平台,为了非侵入式直接嵌入,只能通过jsonp来请求自家的服务器,否则就需要通过后端来转发数据,这样会增加和项目的耦合性,也不利于在线升级安全策略
如何分辨jsonp请求呢?目前jsonp有几个硬指标:
- 响应类型是script
- get请求,只有get方法才会被浏览器允许跨域
- 请求的负载中含有callback参数,响应的内容中是callback函数和参数
上图就是一个经典的jsonp案例,对于jsonp来说,是通过修改script元素的src实现的代码注入,因此,我们可以通过hook script元素的src属性,获取到对应的url,这样我们就能找到这个网络请求参数是在哪个位置生成的了
hook思路
注意,需要hook原型链上的内容,否则会被检测。为了方便,我们可以hook HTMLScriptElement,这里我提供一套解决方案,注意,这里常规情况下,还需要对原型链上的toString方法进行hook,以防止格式化检测:
(function() {
const originalSetAttribute = HTMLScriptElement.prototype.setAttribute;
// 重写原型链上的 setAttribute 方法,这里也可以hook Document对象
HTMLScriptElement.prototype.setAttribute = function(name, value) {
if (name === 'src') {
debugger; // 在这里打断点
console.log(`src被更改: ${value}`);
}
return originalSetAttribute.apply(this, arguments);
};
// 使用 Object.defineProperty 拦截 src 属性的赋值操作
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
set: function(value) {
debugger; // 在这里打断点
console.log(`src被更改: ${value}`);
this.setAttribute('src', value);
},
get: function() {
return this.getAttribute('src');
},
configurable: true
});
})();
这里有同学可能会问,为什么除了我们平常常用的Object.defineProperty
来拦截setter,还要重写HTMLScriptElement.prototype.setAttribute
因为使用 Object.defineProperty
只能拦截 src
属性的赋值操作,如果有心人通过 setAttribute
方法设置 src
属性时,就无法拦截到了,采用我们上述的方案这样可以确保在所有情况下都能拦截对 script
标签 src
属性的赋值操作,并执行自定义逻辑。
这里一下就hook到了具体的位置,n[e]很明显是script标签的src属性,而这里的e对象是又一个混淆后的方法生成的,这里$_BFIEk
是一个字符串解密函数,返回$_BCBk
,那么这里实际上就是var e=this.$_BCBK
但是这里有一个问题,this获取的是闭包指向的外部对象,而外部对象的属性名全部都是混淆过后的,这里如果不使用ast将字符串解密并回填的话,分析将十分痛苦。不过在平常ast的经验中可以得出,字符串加密时,通常使用的是同一个字符串大数组,可以大胆猜想,420所代表的字符串是一致的。果然,我们通过对420的搜索搜索到了另一处内容
对其进行插桩日志查询,发现了非常振奋人心的事实,这个插桩点正是我们要寻找的内容,这里不仅输出了jsonp的参数,还输出了手势轨迹,说明这里确实是关键点
对此处进行插入条件断点 t.w !== undefined
,等到断点断出含w的t后,一路跟栈,这里有手就行省略一万字,得出w生成是由h和u拼接而成的。(定睛一看,原来这里直接把w unicode化了,早知道直接搜索\u0077
了😭😭😭)
u参数
看一下u的生成方式,这里单步进入即可,一行一行向下走,直到return,可以发现,这段函数里面有大量的死代码用于增加分析难度,实际并没有作用。有用的执行内容仅为 var e = new U()[$_CBFJN(392)](this[$_CBGAK(744)](t));
,其中this[$_CBGAK(744)](t)
为加密函数,而this[$_CBGAK(744)](t)
返回的则是一段固定值,在这里屡次调用加密函数,每次都能得出不同的结果。这时候就要警觉了,这不是标准rsa算法的特征吗?
跟进去查看一下,内容看起来还是比较长的,我们着重去找字符串解密的地方,看看能不能寻找到蛛丝马迹
将字符串解密回填,发现调用了this.doPublic
,那么说明调用了RSA库,this是一个rsa对象,那么这时候,我们从控制台找到this对象,然后从原型上找到setPublic方法,点击进去打断点,就可以找到公钥和幂模了,这里公钥就是这一长串,模是16进制的10001,也就是10进制的65537
l参数
可以知道,l 参数的结果是将 gt["stringify"](o)
和 r["$_CCEc"]()
加密后得到的,先来分析 r["$_CCEc"]()
,选中后跟进进去,跳转到了熟悉的位置,就是之前的 16 位随机字符串。 gt["stringify"](o)
返回的是 JSON 格式的数据,由 o 参数生成,而o参数则是轨迹以及环境信息生成。分析完 V[$_CAIAt(353)]
即完成,跟进打下断点,可得到初始向量 iv ,值为 “0000000000000000”,由此可知此为AES加密
引库复现
function aesV(o_text, random_str) {
var key = CryptoJS.enc.Utf8.parse(random_str);
var iv = CryptoJS.enc.Utf8.parse("0000000000000000");
var srcs = CryptoJS.enc.Utf8.parse(o_text);
var encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
for (var r = encrypted, o = r.ciphertext.words, i = r.ciphertext.sigBytes, s = [], a = 0; a < i; a++) {
var c = o[a >>> 2] >>> 24 - a % 4 * 8 & 255;
s.push(c);
}
return s;
};
h参数
跟进 m[$_CAIAt(782)]
,为 e 中两个 value 值相加:
[](htt)
t 为传入的 l 参数,跟进到 this[$_GFJn(264)]
中,扣下来即可复现,校验结果一致,至此w参数还原完毕
大佬,最后一张图片没有上传上来,看不到啊
最后一张没啥内容,就是两个参数的简单相加