Crack Slide | 某验 4 代分析笔记
前言
以官网demo为研究对象,仅限于安全研究,不公开具体源码。接下来我们就进入正题
正文
观察分析
首先是获取滑块,提交参数有这些,captcha_id固定值,challenge(uuid,可以自己生成,不是必需参数,不传也可以),callback(带时间戳的固定值),其余的也是固定值
返回参数,有这些
然后是验证码提交参数,除了上面返回的,和一些固定值,只有w值是未知的,接下来就来分析这个w参数
直接搜好像是搜不出来的,但是获取验证码,和验证码提交都用到了这个js,就很可疑
点进去后,嗯,混淆过的(怎么可能不混淆呢),啥也搜不出来
ast解混淆
首先是ast解混淆的代码,这里用大佬写好的
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");
// #######################################
// 还原需要用到的js源码
// #######################################
//里面内容太多了,字符限制,粘不出来,要提换成自己的
EtDyg.$_AC = function () {}();
//里面内容太多了,字符限制,粘不出来,要提换成自己的
EtDyg.$_Bj = function () {}();
EtDyg.$_CK = function () {
return typeof EtDyg.$_AC.$_EHFHu === "function" ? EtDyg.$_AC.$_EHFHu.apply(EtDyg.$_AC, arguments) : EtDyg.$_AC.$_EHFHu;
};
EtDyg.$_DB = function () {
return typeof EtDyg.$_Bj.$_EHGHy === "function" ? EtDyg.$_Bj.$_EHGHy.apply(EtDyg.$_Bj, arguments) : EtDyg.$_Bj.$_EHGHy;
};
function EtDyg() {}
// #######################################
// AST解析函数
// #######################################
// 删除节点中的extra属性(二进制、Unicode等编码 -> utf-8)
function replace_unicode(path) {
let node = path.node;
if (node.extra === undefined)
return;
delete node.extra;
}
// 定义一个全局变量,存放待替换变量名
let name_array = [];
function get_name_array(path) {
let {kind, declarations} = path.node
if (kind !== 'var'
|| declarations.length !== 3
|| declarations[0].init === null
|| declarations[0].init.property === undefined)
return;
//这个$_CK对应上面的EtDyg.$_CK
if (declarations[0].init.property.name !== "$_CK")
return;
// 获取待替换节点变量名
let name1 = declarations[0].id.name
// 获取待输出变量名
let name2 = declarations[2].id.name
// 将变量名存入数组
name_array.push(name1, name2)
// 删除下一个节点
path.getNextSibling().remove()
// 删除下一个节点
path.getNextSibling().remove()
// 删除path节点
path.remove()
}
function replace_name_array(path) {
let {callee, arguments} = path.node
if (callee === undefined || callee.name === undefined)
return;
// 不在name_array中的节点不做替换操作
if (name_array.indexOf(callee.name) === -1)
return;
// cvFBi.$_Cg函数获取结果
let value = EtDyg.$_CK(arguments[0].value);
// 创建节点并替换结果
let string_node = t.stringLiteral(value)
path.replaceWith(string_node)
}
function replace_$_Cg(path) {
let {arguments, callee} = path.node
// 解析arguments参数
if (arguments.length !== 1) return;
if (arguments[0].type !== 'NumericLiteral') return;
// 解析callee
if (callee.type !== 'MemberExpression') return;
let {object, property} = callee;
if (object.type !== 'Identifier' || property.type !== 'Identifier') return;
//这个$_CK对应上面的EtDyg.$_CK
if (property.name === '$_CK') {
// 计算值
let value = EtDyg.$_CK(arguments[0].value);
// 创建节点并替换
let string_node = t.stringLiteral(value)
path.replaceWith(string_node)
}
}
// 控制流平坦化
function replace_ForStatement(path) {
var node = path.node;
// 获取上一个节点,也就是VariableDeclaration
var PrevSibling = path.getPrevSibling();
// 判断上个节点的各个属性,防止报错
if (PrevSibling.type === undefined
|| PrevSibling.container === undefined
|| PrevSibling.container[0].declarations === undefined
|| PrevSibling.container[0].declarations[0].init === null
|| PrevSibling.container[0].declarations[0].init.object === undefined
|| PrevSibling.container[0].declarations[0].init.object.object === undefined)
return;
//这个$_CK对应上面的EtDyg.$_DB
if (PrevSibling.container[0].declarations[0].init.object.object.callee.property.name !== '$_DB')
return;
// SwitchStatement节点
var body = node.body.body;
// 判断当前节点的body[0]属性和body[0].discriminant是否存在
if (!t.isSwitchStatement(body[0]))
return;
if (!t.isIdentifier(body[0].discriminant))
return;
// 获取控制流的初始值
var argNode = PrevSibling.container[0].declarations[0].init;
var init_arg_f = argNode.object.property.value;
var init_arg_s = argNode.property.value;
var init_arg = EtDyg.$_DB()[init_arg_f][init_arg_s];
// 提取for节点中的if判断参数的value作为判断参数
var break_arg_f = node.test.right.object.property.value;
var break_arg_s = node.test.right.property.value;
var break_arg = EtDyg.$_DB()[break_arg_f][break_arg_s];
// 提取switch下所有的case
var case_list = body[0].cases;
var resultBody = [];
// 遍历全部的case
for (var i = 0; i < case_list.length; i++) {
for (; init_arg != break_arg;) {
// 提取并计算case后的条件判断的值
var case_arg_f = case_list[i].test.object.property.value;
var case_arg_s = case_list[i].test.property.value;
var case_init = EtDyg.$_DB()[case_arg_f][case_arg_s];
if (init_arg == case_init) {
//当前case下的所有节点
var targetBody = case_list[i].consequent;
// 删除break节点,和break节点的上一个节点的一些无用代码
if (t.isBreakStatement(targetBody[targetBody.length - 1])
&& t.isExpressionStatement(targetBody[targetBody.length - 2])
&& targetBody[targetBody.length - 2].expression.right.object.object.callee.object.name == "EtDyg") {
// 提取break节点的上一个节点AJgjJ.EMf()后面的两个索引值
var change_arg_f = targetBody[targetBody.length - 2].expression.right.object.property.value;
var change_arg_s = targetBody[targetBody.length - 2].expression.right.property.value;
// 修改控制流的初始值
init_arg = EtDyg.$_DB()[change_arg_f][change_arg_s];
targetBody.pop(); // 删除break
targetBody.pop(); // 删除break节点的上一个节点
}
//删除break
else if (t.isBreakStatement(targetBody[targetBody.length - 1])) {
targetBody.pop();
}
resultBody = resultBody.concat(targetBody);
break;
} else {
break;
}
}
}
//替换for节点,多个节点替换一个节点用replaceWithMultiple
path.replaceWithMultiple(resultBody);
//删除上一个节点
PrevSibling.remove();
}
// 删除无关函数
function delete_func(path) {
let {expression} = path.node
if (expression === undefined
|| expression.left === undefined
|| expression.left.property === undefined)
return;
//这些值都对应上面的EtDyg后面的,需要自己根据自己的源码替换
if (expression.left.property.name === '$_AC'
|| expression.left.property.name === '$_CK'
|| expression.left.property.name === '$_Bj'
|| expression.left.property.name === '$_DB'
) {
path.remove()
}
}
// #######################################
// AST还原流程
// #######################################
// 需要解码的文件位置
let encode_file = "gcaptcha4.js"
// 解码后的文件位置
let decode_file = "gcaptcha4_decode.js"
// 读取需要解码的js文件, 注意文件编码为utf-8格式
let jscode = fs.readFileSync(encode_file, {encoding: "utf-8"});
// 将js代码修转成AST语法树
let ast = parser.parse(jscode);
// AST结构修改逻辑
const visitor = {
StringLiteral: {
enter: [replace_unicode]
},
VariableDeclaration: {
enter: [get_name_array]
},
CallExpression: {
enter: [replace_name_array, replace_$_Cg]
},
ForStatement: {
enter: [replace_ForStatement]
},
ExpressionStatement: {
enter: [delete_func]
},
}
// 遍历语法树节点,调用修改函数
traverse(ast, visitor);
// 将ast转成js代码,{jsescOption: {"minimal": true}} unicode -> 中文
let {code} = generator(ast, opts = {jsescOption: {"minimal": true}});
// 将js代码保存到文件
fs.writeFile(decode_file, code, (err) => {
});
经过ast解混淆还原后的代码就清晰很多了,1.2w行的代码,还原后只剩6000多行,而且控制流也还原了,此处只想说一句牛逼,然后用reres插件,或者其他工具,替换网站原来的js文件,最后搜索"w"就能定位到了。由下面的代码看,w值就是变量r的值,r值又是在上面生成的,那就打上断点,滑动滑块,开始分析
源码分析
首先是参数e。
device_id:是根据浏览器生成的标识,可以写死
em:固定值
ep:固定值
geetest:固定值
lang:固定值
lot_number:最开始获取滑块的接口返回的
passtime:滑动滑块的时长,由下面track数组里的小数组的第三个参数相加得来
pow_msg(不是必传参数):由几个固定值加上滑块的captcha_id和lot_number和16位,16进制随机数
pow_sign(不是必传参数):将pow_msg,md5加密后生成的值
setLeft:滑块滑动的距离
track:滑动轨迹
userresponse:一个固定算法,下面再说
yizo:是无论键还是值,都是随机的,但是没用,写死就行
而l.default.stringify(e),就是个序列化的操作,同JSON.stringify(e)
先说下这两个不是必传的参数pow_msg和pow_sign
源码中搜下pow_msg就能找到,就在这里,具体逻辑在上面说了,有兴趣的话,可以自己跟进函数看看
setLeft,track和passtime,搜索了网上现成的生成代码
import ddddocr
import random
def generate_distance(slice_url, bg_url):
"""
带带弟弟ocr识别距离
:param bg_url: 背景图地址
:param slice_url: 滑块图地址
:return: distance: 距离
"""
slide = ddddocr.DdddOcr(det=False, ocr=False, show_ad=False)
slice_image = requests.get(slice_url).content
bg_image = requests.get(bg_url).content
result = slide.slide_match(slice_image, bg_image, simple_target=True)
return result['target'][0]
def generate_track(distance):
"""
轨迹生成(百度出来的)
:param distance: 滑动的距离
"""
def __ease_out_expo(step):
return 1 if step == 1 else 1 - pow(2, -10 * step)
tracks = [[random.randint(20, 60), random.randint(10, 40), 0]]
count = 30 + int(distance / 2)
_x, _y = 0, 0
for item in range(count):
x = round(__ease_out_expo(item / count) * distance)
t = random.randint(10, 20)
if x == _x:
continue
tracks.append([x - _x, _y, t])
_x = x
tracks.append([0, 0, random.randint(200, 300)])
passtime = sum([track[2] for track in tracks])
return tracks, passtime
userresponse
搜索userresponse就能找到,具体逻辑在这里
用python还原下就是,captcha_width是验证码图片的长度
def get_userresponse(setLeft, captcha_width=300):
"""获取e参数里面的userresponse"""
e = 340
i = .8876 * e / captcha_width
return setLeft / i
到这里,e参数里面的值,基本就完成了
然后一步步往下跟,到了这里,最后return的是hex编码后的o和a进行了字符串的拼接
先看a,a里面有个参数n,首先进入到c["guid"]这个函数里面看看,是这样的
n的参数有了,下面看看a是怎么生成的,在这个函数里,我们看到了一个关键词RSA,基本就可以断定,是RSA加密了,既然是RSA,那么肯定是有公钥的
接着往下跟,这里可以看到这个this里面有setPublic关键词
然后进入到这个函数中,打上断点,重新滑动滑块看看,这样就找到了公钥,这里有个坑,因为这个公钥是hex编码的,不同于常见的base64公钥。因为RSA加密是定长的,最后只要密文是256位的,基本就没问题了
然后是这个o的值,函数有两个参数,一个是e,一个是跟上面一样的n,e的值就是上面分析出来的e的值
单步进入后,发现了几个关键词
如果接着跟,还能发现一个关键词AES,就可以断定这个是AES加密,使用PKCS7填充,MODE为CBC
最后字符串拼接下这两个加密的值,就是最终的w值了
最后提交参数,没问题,撒花