非 HTTP 验证码别乱捅!一不小心就反爬了。

特别声明:本公众号文章只作为学术研究,不作为其他不法用途;如有侵权请联系作者删除。

4568cf636280ab6eb0201c13a13e66a2.gif

这是「进击的Coder」的第 947 篇技术分享

作者:TheWeiJun

来源:逆向与爬虫的故事

阅读本文大概需要 10 分钟。

立即加星标

5a125aa9aed58001c80ac2d32eb85dfe.png

每月看好文

 目录


一、前言介绍

二、网站分析

三、逆向分析

四、算法还原

五、文章结语

2778d6859973588b3c3f47e70ddcdeb9.gif


一、前言介绍

大家有没有想过,每次在网上注册账号或者登录时,那些不使用滑块验证码,也不依赖 HTTP 协议的验证码是如何识别我们是人而不是机器的呢?这些验证码可能只需要你简单地进行识别操作,例如输入一些文字或数字。但你可曾想过,这些看似简单的验证背后,蕴含着怎样的科技奥秘?在本文中,我们将揭开非 HTTP 验证码的神秘面纱,一起探索它的工作原理,了解它是如何巧妙地识别真人身份的。在这个有趣的旅程中,我们将一窥技术背后的精妙,带你进入一个充满挑战和创新的数字世界。快来和我一起探索吧!


二、网站分析

1、从某 APP 中抓包分析用户登录接口,发现登录接口为 webview,将登录接口 url 使用浏览器打开分析,截图如下:

0f500009cd9a65954859da1d407f65ea.png

2、打开浏览器开发者工具,然后输入手机号后,点击发送验证码,请求截图如下所示:

ff37a4b6caf16e4139f6687408ea28c0.png

3、浏览器 Network 中查看请求包,我们并没有发现 http 请求接口的验证码,只发现了疑似验证码的请求体和响应体,截图如下:

d613ee1bdfe462faf5ca254aa230a73a.png

9925de9938c62823609cc083aa41f7f5.png

总结:此处我们发现 response 的返回内容进行了加密,加密参数为 encrypt_body。我们再分析下 request 请求体,结果发现加密参数也是 encrypt_body。这里我们初步可以确定,请求入参和 response 出参使用的是一套加密算法,接下来我们进入逆向分析环节吧。


三、逆向分析

1、通过 Initiator 进行调试分析,过滤无限 debugger 模式,进行断点分析截图如下所示:

23a4a05b10c636b7b633e742812a12f3.png

2、这里我们定位到了请求体加密前参数,先暂时忽略。我们紧接着往下继续分析,截图如下:

775dde76935f5343ef8420f469de0657.png

3、此处我们查看返回参数 s,可以确定是请求体加密后的参数,截图如下:

2602762fb0dbf77e4488056cfa6059c9.png

总结:虽然前面简单分析过,请求体和响应体的 encrypt_body 使用的是一套加密算法。那么我们先还原请求体加密,再拿请求体加密的对称加密算法进行解密,就可以判断我们前面的猜测是否正确。通过断点分析我们发现请求体加密使用了 aes-ecb 模式,AES 是一种对称加密算法,这意味着加密和解密使用相同的密钥。接下来我们进入算法还原环节,来验证下我们的猜测吧。


四、算法还原

1、使用 Python 还原 JS 加密算法,还原后的完整加密算法如下:

# -*- coding: utf-8 -*-
# -------------------------------
# @author : TheWeiJUn
# @time   : 2024.08.04 20:58:33
# -------------------------------


from Crypto.Cipher import AES
import json
import base64


from Crypto.Util.Padding import pad




def encrypt(e, n):
    if e and n:
        if not isinstance(e, str):
            e = json.dumps(e, separators=(",", ":"))


        r = 'utf-8'


        key = n.encode(r)  # Assuming n is a string that needs to be encoded to bytes
        data = e.encode(r)


        cipher = AES.new(key, AES.MODE_ECB)
        padded_data = pad(data, AES.block_size)  # Pad the data to be a multiple of block size
        cipher_text = cipher.encrypt(padded_data)  # Encrypt the padded data


        s = base64.b64encode(cipher_text).decode('utf-8')
        return s




if __name__ == '__main__':
    # Example usage:
    e = {"phone": "xxxx", "countryCode": "CN", "type": "login"}
    n = "1234567890123456"  # Example key (must be 16 bytes for AES-128)
    result = encrypt(e, n)
    print(result)

2、输出 Python 加密后的参数s同浏览器中生成的加密参数 s 进行比对,截图如下:

3faada4d93a3cbf41011315e4cb831ce.png02547d26f867eed56945ca8e84c4f74a.png

总结:经过对比我们发现,两个参数字符串完全一致。接下来我们使用前面分析的方法对 response 加密体参数 encrypt_body 试着进行对称解密还原。截图如下所示:

1a57e00aedc53786a2eaef52da96a8eb.png

3、解密还原失败,此处我怀疑 response 加密参数 encrypt_body 应该是做了特殊处理,经过断点分析后,果真如我所言,编辑修改后的 decrypt 解密代码如下:

# -*- coding: utf-8 -*-
# -------------------------------
# @author : TheWeiJUn
# @time   : 2024.08.04 20:58:33
# -------------------------------


from Crypto.Cipher import AES
import json
import base64


from Crypto.Util.Padding import pad, unpad




def encrypt(e, n):
    if e and n:
        if not isinstance(e, str):
            e = json.dumps(e, separators=(",", ":"))


        r = 'utf-8'


        key = n.encode(r)  # Assuming n is a string that needs to be encoded to bytes
        data = e.encode(r)


        cipher = AES.new(key, AES.MODE_ECB)
        padded_data = pad(data, AES.block_size)  # Pad the data to be a multiple of block size
        cipher_text = cipher.encrypt(padded_data)  # Encrypt the padded data


        s = base64.b64encode(cipher_text).decode('utf-8')
        return s




def decode_base64(t):
    # Add padding to make the length a multiple of 4
    t = t + '=' * (4 - len(t) % 4)


    # Replace URL-safe characters with standard Base64 characters
    t = t.replace('-', '+').replace('_', '/')


    # Decode the Base64 string
    return base64.b64decode(t)




def decrypt(e, n):
    if e and n:
        # Decode the Base64 encoded input
        cipher_text_bytes = decode_base64(e)
        # Define the encoding and decoding formats
        a = 'utf-8'
        key = n.encode(a)  # Convert key to bytes


        # Create the cipher object for decryption
        cipher = AES.new(key, AES.MODE_ECB)
        padded_data = cipher.decrypt(cipher_text_bytes)  # Decrypt the data
        # Remove padding
        data = unpad(padded_data, AES.block_size)
        # Parse the JSON data
        return json.loads(data.decode(a))




if __name__ == '__main__':
    n = "1234567890123456"  # Example key (must be 16 bytes for AES-128)
    e = "encrypt_body"
    result = decrypt(e, n)
    print(result)

4、对加密参数进行 padding 填充及对特殊符号进行替换处理后,即可进行对称解密操作,解码后的截图如下所示:

36c11ce2701f5063acc8dc75feb20fd8.png

5、对还原后的响应体 graph 参数进行特殊处理后,进行 base64decode,相关代码和解码后保存的字节流如下:5db3ed24ecfa24099fd57fc0f7da8efe.png

d958852b7cb919d850993da3e2d1d3ff.png

6、到这里基本就结束了,接下来进行验证码识别还原,这个相对来说比较简单,我们这里使用 ddddocr,编辑代码后还原验证码如下所示:3cf1c22ce2616e1ea26aad70b24aa02a.png


五、文章结语

本篇分享到这里就结束了,感谢大家的阅读和支持。如果你对爬虫逆向分析、验证码破解及其他技术话题感兴趣,记得关注我的公众号,不错过下一期的更新。我们将继续深入探讨各种技术细节和实用技巧,一起探索数字世界的奥秘。期待与你在下期文章中再见,一起学习,一起进步!☀️☀️✌️

好文和朋友一起看~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值