1.png

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!

前言

粉丝答疑又双叒来了!最近仍有不少粉丝咨询逆向相关问题,K哥会尽力回复,不过毕竟精力有限,不可能说是面面俱到(忙忘了@.@)。当然,还是会尽可能多筛选些典型的案例,产出相关文章,以供大伙学习交流。本期分析的验证码,如果没有相关业务,可能很多人不会接触到,也算是较为新颖,话不多说,开始正文:

2.png

逆向目标

目标:xx 邮政滑块逆向分析

网站:aHR0cHM6Ly9wYXNzcG9ydC4xMTE4NS5jbi9jYXMvbG9naW4=

<img src="https://v1.ax1x.com/2024/08/12/7G45fw.png" alt="3.png" style="zoom:50%;" />

抓包分析

抓包分析,先看图片接口 gen:

4.png

返回的内容比较多,后面估计会用到,我们留意一下就行,clientUid 后文分析:

5.png

再来看验证接口,即发验证码的接口:

6.png

  • id : 图片接口返回;
  • bgImageHeight: 写死即可;
  • bgImageWidth:写死即可;
  • clientUid:同图片接口参数 clientUid;
  • deviceId:图片接口返回 deviceId;
  • enmiid:需要分析;
  • entSlidingTime:结束验证时间,模拟一下,校验不严;
  • print:需要分析;
  • sliderImageHeight:写死即可;
  • sliderImageWidth:写死即可;
  • startSlidingTime:开始验证时间,模拟一下,校验不严;
  • trackList:轨迹列表。

验证结果:

  • 失败

7.png

  • 成功

8.png

逆向分析

gen 接口

该接口有个 clientUid 参数,目测是 uuid 类型, 我们全局搜索一下,发现只在 main.js 文件中存在,进去后发现被 ob 混淆:

9.png

又是经典的 ob 混淆, 这里直接借用 v佬 的插件快速还原,工具地址如下:

Github 仓库:https://github.com/cilame/v_jstools

在线分析工具:https://astexplorer.net

还原之前还需要稍微的预处理一下,比如下面这样一堆常量的花指令,以及十六进制形式的数字:

比如下面这样一堆常量的花指令,以及十六进制形式的数字

10.png

附上简单的预处理 AST 代码:

// 字符串美化
const simplifyLiteral = {
    NumericLiteral({ node }) {
        if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
        node.extra = undefined;
        }
    },
    StringLiteral({ node }) {
        if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
        node.extra = undefined;
        }
    },
}

traverse(ast, simplifyLiteral);



let my_obj = {};


const merge_odj = {
    VariableDeclarator(path){
        let {init} = path.node;

        if (!types.isObjectExpression(init)) return;

        let {properties} = init;

        properties.forEach(property => {
            // 确保属性值是字面量
            if (types.isLiteral(property.value)) {
              my_obj[property.key.name] = property.value;
            }
        });
       
    }
};

traverse(ast, merge_odj);


const clearObjectofValue = {
    MemberExpression(path){
        let {object, property} = path.node
        if (!property || !types.isIdentifier(property) || !types.isIdentifier(object)) return;
        let key = property.name
        let value = my_obj[key]
        if (!value) return;
        console.log(`${path.toString()} --> ${value.value}`)
        path.replaceWithSourceString(value.value)
    }
}

traverse(ast, clearObjectofValue);

ast = parser.parse(generator(ast,{compact:true}).code)


remove_no_citation_obj = {
    VariableDeclarator(path){
        let {init,id} = path.node
        if (!types.isObjectExpression(init) && !types.isIdentifier(id)) return;
        let {scope} = path
        let binding = scope.getBinding(id.name)
        if (binding.referencePaths.length !== 0) return;
        path.remove()
    }
}

// 删除未被引用的对象
traverse(ast,remove_no_citation_obj)

然后再利用 v_jstools 插件中的解密高级 ob 混淆即可还原了,good:

11.png

利用浏览器的替换功能,替换 main.js 文件,注意将代码压缩替换,我们再来搜索 clientUid 看看:

12.png

返回 _0x349b8e,我们再进去到 _0x236e93 函数中看看:

13.png

发现确实是 uuid 的构造,这边引库或者扣代码都行:

  • clientUid
var _0x5e9294;
var _0x31a6e3 = new Uint8Array(16);

function _0x2f762f() {
    if (!_0x5e9294 && !(_0x5e9294 = typeof crypto != "undefined" && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto != "undefined" && typeof msCrypto.getRandomValues == "function" && msCrypto.getRandomValues.bind(msCrypto))) {
        throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
    }
    return _0x5e9294(_0x31a6e3);
}

for (var _0x22667e = function (_0x37736f) {
    return typeof _0x37736f == "string" && /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i.test(_0x37736f);
}, _0x49863c = [], _0x18d3c6 = 0; _0x18d3c6 < 256; ++_0x18d3c6) {
    _0x49863c.push((_0x18d3c6 + 256).toString(16).substr(1));
}

function _0x19f649(_0x3d7e8f) {
    var _0x238244 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
    var _0x313ed4 = (_0x49863c[_0x3d7e8f[_0x238244 + 0]] + _0x49863c[_0x3d7e8f[_0x238244 + 1]] + _0x49863c[_0x3d7e8f[_0x238244 + 2]] + _0x49863c[_0x3d7e8f[_0x238244 + 3]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 4]] + _0x49863c[_0x3d7e8f[_0x238244 + 5]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 6]] + _0x49863c[_0x3d7e8f[_0x238244 + 7]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 8]] + _0x49863c[_0x3d7e8f[_0x238244 + 9]] + "-" + _0x49863c[_0x3d7e8f[_0x238244 + 10]] + _0x49863c[_0x3d7e8f[_0x238244 + 11]] + _0x49863c[_0x3d7e8f[_0x238244 + 12]] + _0x49863c[_0x3d7e8f[_0x238244 + 13]] + _0x49863c[_0x3d7e8f[_0x238244 + 14]] + _0x49863c[_0x3d7e8f[_0x238244 + 15]]).toLowerCase();
    if (!_0x22667e(_0x313ed4)) {
        throw TypeError("Stringified UUID is invalid");
    }
    return _0x313ed4;
}

function _0x236e93(_0x5bbf46, _0x3b7cda, _0x21cf16) {
    var _0x574141 = (_0x5bbf46 = _0x5bbf46 || {}).random || (_0x5bbf46.rng || _0x2f762f)();
    _0x574141[6] = _0x574141[6] & 15 | 64;
    _0x574141[8] = _0x574141[8] & 63 | 128;
    if (_0x3b7cda) {
        _0x21cf16 = _0x21cf16 || 0;
        for (var _0x3503fc = 0; _0x3503fc < 16; ++_0x3503fc) {
            _0x3b7cda[_0x21cf16 + _0x3503fc] = _0x574141[_0x3503fc];
        }
        return _0x3b7cda;
    }
    return _0x19f649(_0x574141);
};


console.log(_0x236e93());

验证接口

重点就是 enmiid、print 参数的生成,我们通过搜索 enmiid、print 即可定位到:

14.png

_0x5e712c 对象中取的值,我们可以搜索 _0x5e712c 的赋值操作 "_0x5e712c =", 即可定位到目标位置:

15.png

可以看到目标的参数以及都生成好了,我们可以从前往后开始分析:

16.png

_0x5b4cc9 就是图片返回的内容,取了 webAName、miid、rc, 然后拼接了一个 weba.js 请求这个链接,通过三次 then 得到 _0x383eb4 这个对象,进去到 _0x383eb4 对象的方法或者去看 weba.js 这个文件就可以发现又是 WebAssembly 与 JavaScript 之间进行交互,跟上篇某东 m 端的 wasm 大差不差,敢兴趣的可以去看看,我们接着向下分析,通过 _0x383eb4 对象下的函数对 _0x2d1fb0_0x354e64 进行一系列的操作最终得到了目标参数。

_0x2d1fb0 就是取的 rc_0x354e64 是一个对象,里面的 hitId 对应 miid、deviceId 对应图片接口返回的 deviceId_print 通过 _0x468795(_0x34c930) 返回,_0x34c930 对应 miid,然后进入到 _0x468795 函数:

17.png

通过 WebGL 在画布上绘制图形,并最终将绘制的内容转换为图像数据URL, 然后 _0x492d6a() 函数生成密文

最后再将密文与 _0x5ad970 + "|" + 密文得到我们最终返回的数据,我们断点进入到 _0x492d6a() 函数:

18.png

可能有大佬一眼就看出来是什么加密了,也可以根据长度判断,我们也可以找一下是谁调用的,在 249 中,

我们直接找 (249) ,就在下面,加密也写出来了,我们验证一下没有问题:

19.png

20.png

最后就是 _0x383eb4 对象了,我们可以仿照着写,先请求 weba.js 链接,注意 weba.js 链接是动态:

 "https://kks.11185.cn/static/front/" + webAName + "/bin/weba.js"

保存发现:

21.png

已经帮我们把对象给导出了,如下操作即可:

weba = require('./Weba.js');

window = global;

function ZxptRestKks(rc, _0x354e64){
    var result;
    var flag = false;
    weba().then(function(Module) {
        if (Module) {
            _0x2d1fb0 = rc;   // rc
            window[_0x354e64.deviceId] = function () {
              return _0xa2b2bc = Module.allocateUTF8(_0x2d1fb0);
            };
            // 访问 PromiseResult 下的属性
            _0x156e64 = Module.allocateUTF8(JSON.stringify(_0x354e64));
            _0x3e92b3 = Module._rotate(_0x156e64);
            _0x57276e = Module.UTF8ToString(_0x3e92b3);
            result = JSON.parse(_0x57276e);
            flag = true;
        }
    }).catch(function(error) {
        // 处理 Promise 被拒绝的情况
        console.error(error);
    });
    while (!flag) {
        require('deasync').sleep(100);
    }
    ;
    return result;
};

最后就是一些细节风控的处理,该验证码对距离的识别要求较高,轨迹要求不高,会封 ip 和 ua,速度快了,也会影响成功率,在 cv2 识别后,加上延迟即可,成功率 99%。附上处理代码:

  • cv2 识别
def identify_gap(bg, tp):
    """
    bg: 背景图片
    tp: 缺口图片
    out: 输出图片
    """
    # 读取背景图片和缺口图片
    bg_img = cv2.imdecode(np.frombuffer(bg, np.uint8), cv2.IMREAD_GRAYSCALE)
    tp_img = cv2.imdecode(np.frombuffer(tp, np.uint8), cv2.IMREAD_GRAYSCALE)  # 缺口图片
    yy = []
    xx = []
    for y in range(tp_img.shape[0]):
        for x in range(tp_img.shape[1]):
            r = tp_img[y, x]
            if r < 200:
                yy.append(y)
                xx.append(x)
    tp_img = tp_img[min(yy):max(yy), min(xx):max(xx)]
    # 识别图片边缘
    bg_edge = cv2.Canny(bg_img, 100, 200)
    tp_edge = cv2.Canny(tp_img, 100, 200)
    # 转换图片格式
    bg_pic = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)
    tp_pic = cv2.cvtColor(tp_edge, cv2.COLOR_GRAY2RGB)
    # 缺口匹配
    res = cv2.matchTemplate(bg_pic, tp_pic, cv2.TM_CCOEFF_NORMED)
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)  # 寻找最优匹配
    # # 绘制方框
    th, tw = tp_pic.shape[:2]
    tl = max_loc  # 左上角点的坐标
    br = (tl[0] + tw, tl[1] + th)  # 右下角点的坐标
    cv2.rectangle(bg_img, tl, br, (0, 0, 255), 2)  # 绘制矩形
    cv2.imwrite('distinguish.jpg', bg_img)  # 保存在本地
    # 返回缺口的X坐标
    print(f'value: {tl[0]}')
    return max_loc[0]


value = identify_gap(requests.get(backgroundImage).content, requests.get(sliderImage).content)
value = int(value * 0.55)
print(value)
time.sleep(1)
  • 轨迹构造
def get_sub_trajectory(trajectories, value):
    # 检查value是否在轨迹的x值中
    for trajectory in trajectories:
        if trajectory['x'] == value:
            # 如果找到,截取从轨迹开始到该点的子数组
            return [t for t in trajectories if t['x'] <= value]

    # 如果value不在x值中,找到最接近value的x值
    closest_x = None
    min_diff = float('inf')
    for trajectory in trajectories:
        if abs(trajectory['x'] - value) < min_diff:
            min_diff = abs(trajectory['x'] - value)
            closest_x = trajectory['x']

    # 截取从轨迹开始到最接近的x值的子数组
    return [t for t in trajectories if t['x'] <= closest_x]


# 调用函数
# trackList : 浏览器生成的最大距离轨迹
value = 128  # 假设我们要找的value是 128
sub_trajectory = get_sub_trajectory(trackList, value)
sub_trajectory[-1]['type'] = 'up'
print(sub_trajectory)

结果验证

22.png