首页 文章详情

Crack Slide | 某验 4 代分析笔记

咸鱼学Python | 159 2022-05-21 15:27 1 1 0
UniSMS (合一短信)

前言

以官网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 !== 1return;
    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(2060), random.randint(1040), 0]]
    count = 30 + int(distance / 2)
    _x, _y = 00
    for item in range(count):
        x = round(__ease_out_expo(item / count) * distance)
        t = random.randint(1020)
        if x == _x:
            continue
        tracks.append([x - _x, _y, t])
        _x = x
    tracks.append([00, random.randint(200300)])
    passtime = sum([track[2for 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值了

最后提交参数,没问题,撒花

End.


good-icon 1
favorite-icon 0
收藏
回复数量: 1
  • 你好,如何运行那段ast解混淆代码。在哪运行?

暂无评论~~
Ctrl+Enter