乐趣区

关于加密解密:91短视频加密算法抓包分析及刷邀请

指标 APP:91 短视频

之前发过一篇蚂蚁加速器刷邀请的文章,这次的 APP 就是和蚂蚁加速器一家的,加密算法根本一样,不提供下载。

0x00 工具筹备

  • Fiddler(代理抓包)
  • Xposed(hook 框架)

    • Inspeckage(通用 hook 插件,备选算法助手)
  • HEX 编码转换(base64 转 HEX)
  • 雷电模拟器(雷电 3 绿化版)
  • ProxyDroid(代理转发工具,用 Gitee 一键编译的)

工具安装包 配置好的雷电模拟器零碎备份 下载:https://lanzoui.com/b0eknupng 明码:4vpf

各工具应用办法介绍等具体内容可参考各自文档或百度。

0x01 现实的剖析过程

新建模拟器,装置好指标 APP 及工具,先不要关上指标 APP。

1、用 ProxyDroid 解决抓不到包的问题

关上 ProxyDroid 按下图示例填好代理服务器 IP、端口,协定选 HTTP(就算抓 HTTPS,也选 HTTP)

下滑能够抉择用全局代理还是分利用代理,这里间接全局代理了。

代理服务器 IP、端口为 Fiddler 中的代理,默认 8888 端口,IP 为局域网 IP,在 Fiddler 右上角有如下所示的图标,能够在下面悬停鼠标,就会显示局域网地址,个别为 192.168..

2、用 Inspeckage 主动 hook 加密算法及 hash

关上 Inspeckage,抉择指标 APP,界面如下。

依据图中提醒转发 8008 端口到电脑,即在 模拟器运行目录下 关上 cmd,再输出:

”’
adb forward tcp:8008 tcp:8008
”’

此时在电脑浏览器中关上 http://127.0.0.1:8008/ 即可看到 Inspeckage 网页界面。

点开设置,将不须要的都关掉,只留 Crypto 和 Hash 两个,关上主动刷新。

3、开始剖析

工具都配置好后,关上指标 APP,直到胜利绑定邀请码。(或者在开屏广告停留一下,查看 Inspeckage 的 Crypto,出后果后敞开主动刷新,因为不晓得为什么这里会主动革除记录,偏偏刚启动会有个注册的包要用到。)

分析方法是依据 Fiddler 中数据包参数去 Inspeckage 中寻找对应的加解密输入输出。

  1. 点开绑定邀请码的包,绑定邀请码的时候看着 Fiddler,很好确定是哪个包,不出意外的话就是指标域名在 Fiddler 中最下方倒数第二个。

    能够看到发送参数有 3 个,一个工夫戳,一个 HEX 格局的 data,一个 MD5 的 sign。
  2. 在 Inspeckage 的 Crypto 中搜寻本人输出的邀请码,没搜到就把折叠起来的加密数据点开再搜,明文在前的是加密,base64 在前的是解密。

    这里能够确定:

    • 加密算法是 AES,
    • keyh3PV8o444kNybrx77icyiriQ2q0uTjqUSsFRfaynkT8=(base64 编码),
    • ivDrRMfzwgpjgI1sIjfW8aXw==(base64 编码),
    • 解密模式是AES/CFB/NoPadding
    • 加密数据格式为{"mod":"user","build_id":"a1000","token":"","oauth_id":"xxxxx","oauth_type":"android","aff":"xxxxx","app_status":"xxxx:2","version":"4.5.5","apiV2":"v2","app_type":"local","code":"invitation"}
  3. 把加密后果用 HEX 编码转换转为 HEX,发现和 data 中去除前 32 个字符之后对的上。依据上一篇文章的教训,这 32 个字符应该是 IV。把刚刚失去的 iv 即DrRMfzwgpjgI1sIjfW8aXw== 转为 HEX,发现正好和 data 前 32 位相等。

    这里能够确定:发送的 data 是 iv 与加密数据的 HEX 编码拼接而成。
  4. 再用同样的办法,去 Hash 界面中搜抓到的 sign,能够看到是一长串 hash 进行 MD5 加密。
  5. 持续搜这一长串 hash,发现正好是上面一条 SHA256 加密的后果。

    加密字符串格局为data={}xtamp={}132f1537f85sjdpcm59f7e318b9epa51
  6. 这时候拿去验证 SHA256,发现后果对不上。认真看一下,发现 timestamp 没齐全显示,预计出了点问题,手动补全为data={}&timestamp={}132f1537f85sjdpcm59f7e318b9epa51,再次验证发现没问题了。
  7. 加密的数据曾经差不多弄清楚了,那返回的 data 解密应该也是一样的。将后面失去的 key 和 iv 拿去解密,发现不对。这个时候依据后面的教训,猜想返 data 也是前 32 位为 iv,前面为待解密数据,一试发现果然如此。

    因为各种 AES 加解密工具都把输入输出格局固定死了,很少有反对 HEX 或 base64 编码的输出以及 key、iv,所以倡议写代码验证或者应用其余工具,或者手动转换编码再去用工具验证,这里是将待解密数据[HEX 转为 base64][],将 key、iv 转为 HEX,再用工具验证
  8. 到这里只看了绑定邀请码的包,当初再去看看注册的包,毕竟不注册没方法刷邀请。依据 Fiddler 中先后顺序,顺次将发包的 data 解密查看,后果没有发现注册字样,且多个 data 完全相同。所以能够猜想,要么是没抓到注册包,要么是这些数据包主动实现了注册。

剖析局部到这里就能够完结了,有 key、有 iv、有加解密数据模板、有 sign 模板,能够间接写代码了。

4、怎么全是猜想?

看到这里,我也纳闷,怎么都是猜的,间接剖析能猜到这些吗?

预计还真不好猜。

因为理论剖析过程并不是这么顺利,所以两头反编译看了源码,同时联合上一次剖析蚂蚁加速器的教训,所以很多货色都一眼看进去了,这两个 APP 加密基本相同。

如果有切实看不明确怎么猜到的,看一看上一篇蚂蚁加速器刷邀请的文章,外面有反编译剖析的局部。

0x02 理论剖析过程

这里应用雷电 3,即安卓 5,因为安卓 7 装置证书麻烦。绿化版来自派大星模拟器多开助手网站。

开局先抓包,关上 Fiddler、模拟器中设置好代理、装置证书、关上 APP。

一顿操作猛如虎,一看后果啥也没有,而且 APP 能失常关上失常应用。这时候就能够猜想,APP 禁用了代理。

个别防抓包措施自行理解。抓不到包的时候,如果 APP 闪退或者不能拜访网络,则有可能是检测了代理;如果 APP 失常应用,那应该是禁用了代理。

这里能够应用抓包精灵、小黄鸟等手机端的抓包工具,或者用代理工具转发流量到 Fiddler,因为手机端不不便操作,所以这里用 ProxyDroid 来转发流量。

用其余工具能够,比方 Drony、Postern、socksdroid,不过我举荐用 ProxyDroid,代理模式齐全,设置过程简略明确。

用了代理工具,后果还是没有抓到想要的包,有点不对劲,我开始狐疑它不是 HTTP 协定,可能用了 ws 或者 tcp。

这里就没脉络了,所以只能去看 Inspeckage 的后果,而后反编译(没加壳)找算法,找半天找到了。这个时候能够用 frida 取 hook 到后果了,然而我还是想试试能不能抓到包。

之后陆续尝试了用 Inspeckage 增加代理、用 socks 代理,都没胜利。

最初无心中发现,新建模拟器后第一次关上这个 APP 的时候用 ProxyDroid 能够抓到包,之后测试屡次,无论什么姿态,除了第一次关上之外都抓不到包,难道除了第一次之外都改用了 tcp?

不过好在还是抓到了包,这时候联合 Inspeckage 中 hook 到的加密算法和 hash,就能够拼接出申请的数据包了,详情见上方。

还是想不通为什么,心愿有晓得的大佬能解惑。

Python 代码

# -*- encoding: utf-8 -*-
'''
@File    :   91.py
@Time    :   2022 年 01 月 04 日 20:24:32 星期二
@Author  :   erma0
@Version :   1.0
@Link    :   https://erma0.cn
@Desc    :   91 短视频刷邀请
'''

import requests
import time
import json
from base64 import b64decode
from hashlib import sha256, md5
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
# from Crypto.Hash import SHA256, MD5  # 和 hashlib 库一样


class Aff(object):
    """91 短视频刷邀请"""
    def __init__(self, aff: str = "gcKyA"):
        self.aff = aff
        self.oauth_id = ''self.timestamp =''
        self.url = 'http://api.91apiapi.com/api.php'
        # self.url = 'http://v2.my10api.com:8080/api.php'
        self.headers = {  # 加不加 header 都能够
            'Accept-Language': 'zh-CN,zh;q=0.8',
            'User-Agent':
            'Mozilla/5.0 (Linux; U; Android 5.1.1; zh-cn; M973Q Build/LMY49I) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
            'Content-Type': 'application/x-www-form-urlencoded'
        }
        # self.b64key = 'h3PV8o444kNybrx77icyiriQ2q0uTjqUSsFRfaynkT8='
        # self.b64iv = 'DrRMfzwgpjgI1sIjfW8aXw=='
        self.key = b64decode('h3PV8o444kNybrx77icyiriQ2q0uTjqUSsFRfaynkT8=')
        self.iv = b64decode('DrRMfzwgpjgI1sIjfW8aXw==')

    @staticmethod
    def get_timestamp(long: int = 10):
        """取工夫戳,默认 10 位"""
        return str(time.time_ns())[:long]

    def decrypt(self, data: str):
        """aes 解密"""
        ct_iv = bytes.fromhex(data[:32])
        ct_bytes = bytes.fromhex(data[32:])
        ciper = AES.new(self.key, AES.MODE_CFB, iv=ct_iv, segment_size=128)
        # CFB 模式,iv 指定,块大小为 128(默认为 8,需填 8 的倍数,貌似 AES 规范区块大小就是 128,和密钥大小 128/192/256 无关)
        plaintext = ciper.decrypt(ct_bytes)
        return plaintext.decode()

    def encrypt(self, data: str):
        """aes 加密"""
        ciper = AES.new(self.key, AES.MODE_CFB, iv=self.iv, segment_size=128)
        ct_bytes = self.iv + ciper.encrypt(data.encode())  # iv+ 加密后果合并
        return ct_bytes.hex().upper()  # hex 编码

    def get_sign(self):
        """生成 sign"""
        template = 'data={}&timestamp={}132f1537f85sjdpcm59f7e318b9epa51'.format(self.encrypt_data, self.timestamp)
        # sha256
        sha = sha256()
        sha.update(template.encode())
        res = sha.hexdigest()
        # md5
        m = md5()
        m.update(res.encode())
        res = m.hexdigest()
        return res

    def request(self, d: dict):
        """申请封包"""
        plaintext = {
            "build_id": "a1000",
            "token": "","oauth_type":"android","app_status":"A72B8E7B0E661AAEEB5280AAC3993DC6F4A2D8C0:2","version":"4.5.5","apiV2":"v2","app_type":"local"
        }
        d.update(plaintext)
        self.timestamp = self.get_timestamp(10)
        self.encrypt_data = self.encrypt(json.dumps(d, separators=(',', ':')))
        sign = self.get_sign()
        data = {"timestamp": self.timestamp, "data": self.encrypt_data, "sign": sign}
        res = requests.post(url=self.url, data=data, headers=self.headers)
        resj = res.json()
        res = self.decrypt(resj.get('data'))
        print(res)
        return res

    def get_user(self):
        """生成新用户"""
        # 取随机 md5
        m = md5()
        m.update(get_random_bytes(16))
        oauth_id = m.hexdigest()

        data = {"mod": "system", "oauth_id": oauth_id, "code": "index"}
        self.request(data)
        self.oauth_id = oauth_id
        print(oauth_id)

    def invite(self):
        """刷邀请,邀请码:self.aff"""
        self.get_user()
        data = {"mod": "user", "oauth_id": self.oauth_id, "aff": self.aff, "code": "invitation"}
        self.request(data)


if __name__ == "__main__":
    aff = Aff('gcKyA')
    aff.invite()
    # data = 'x'
    # print(aff.decrypt(data))
退出移动版