91短视频刷邀请及frida-rpc的简单使用


目标APP:91短视频

之前思否上发过一篇91短视频刷邀请的文章,在某次app升级算法之后文章还没更新的时候,文章就被举报404了,悲哀,今天厚码写一下新版过程,源码放github需要自取,我看老六还怎么举报。

工具准备

用到的工具配置好的雷电模拟器系统备份release页面下载

PS. 直接使用备份的系统,环境都配置好了,打开frida转发端口就能用。

自行配置步骤:模拟器设置system可写并开启root,用MagiskDelta刷面具,删除自带的su文件,刷入LSPosed,安装算法助手插件,用FridaHooker安装Frida。算法助手中打开总开关和算法分析的3个开关,开启Frida,通过adb转发端口,打开HTTP Debugger Pro抓包(需安装证书到系统分区)

分析过程

工具都配置好后,打开目标APP,直到成功绑定邀请码。
(我测试过程中装完证书直接就可以正常抓包,说抓不到的可以试试在算法助手里打开JustTrustMe)

返回算法助手查看,发现没有aes加密,竟然和以前加密方式不同了

不过回算法助手可以看到sign还是有的,依然是老方法,sha256+md5

apk拖到GDA中,定位到算法助手中sign的调用堆栈位置,很容易发现加密点

按x查看交叉引用,最终定位到了sojm.so

package com.qq.lib.EncryptUtil;
import java.lang.System;
import java.lang.String;
import java.lang.Object;

public class EncryptUtil	// class@000900
{
    static {
       System.loadLibrary("sojm");
    }
    public void EncryptUtil(){
       super();
    }
    public static native String decrypt(String p0,String p1);
    public static native String decryptHls(String p0,String p1);
    public static native byte[] decryptImg(byte[] p0,int p1,String p2);
    public static native byte[] decryptImg2(byte[] p0,int p1,String p2);
    public static native String encrypt(String p0,String p1);
}

由于不会分析so,尝试一番后无果,且模拟器frida无法hook到第三方APP的so,手上又没有真机,遂放弃

改用frida-rpc,供python程序直接调用加密方法,除了使用过程比较麻烦,貌似也没什么大问题

学习了一番frida,直接开始改代码,还是之前的源码,只是改一改加密解密方法

最终hook到com.qq.lib.EncryptUtil这里就行,不用到so里面,反正只是外部调用而已

Python源码

https://github.com/erma0/test/blob/main/91/frida91.py


被404的旧文章

点击展开

目标APP:91短视频

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

0x00 工具准备

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

各工具使用方法介绍等详细内容可参考各自文档或百度。

0x01 理想的分析过程

新建模拟器,安装好目标APP及工具,先不要打开目标APP。

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

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

ProxyDroid配置1

下滑可以选择用全局代理还是分应用代理,这里直接全局代理了。

ProxyDroid配置2

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

Fiddler右上角

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

打开Inspeckage,选择目标APP,界面如下。

Inspeckage界面

根据图中提示转发8008端口到电脑,即在模拟器运行目录下打开cmd,再输入:

‘’’
adb forward tcp:8008 tcp:8008
‘’’

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

点开设置,将不需要的都关掉,只留Crypto和Hash两个,打开自动刷新。

Inspeckage 设置

3、开始分析

工具都配置好后,打开目标APP,直到成功绑定邀请码。(或者在开屏广告停留一下,查看Inspeckage的Crypto,出结果后关闭自动刷新,因为不知道为什么这里会自动清除记录,偏偏刚启动会有个注册的包要用到。)

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

  1. 点开绑定邀请码的包,绑定邀请码的时候看着Fiddler,很好确定是哪个包,不出意外的话就是目标域名在Fiddler中最下方倒数第二个。
    Fiddler抓包结果
    可以看到发送参数有3个,一个时间戳,一个HEX格式的data,一个MD5的sign。

  2. 在Inspeckage的Crypto中搜索自己输入的邀请码,没搜到就把折叠起来的加密数据点开再搜,明文在前的是加密,base64在前的是解密。
    Inspeckage 邀请码加密结果
    这里可以确定:

    • 加密算法是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+加密数据
    这里可以确定:发送的data是iv与加密数据的HEX编码拼接而成。

  4. 再用同样的方法,去Hash界面中搜抓到的sign,可以看到是一长串hash进行MD5加密。

  5. 继续搜这一长串hash,发现正好是下面一条SHA256加密的结果。
    Inspeckage sign结果
    加密字符串格式为data={}xtamp={}132f1537f85sjdpcm59f7e318b9epa51

  6. 这时候拿去验证SHA256,发现结果对不上。仔细看一下,发现timestamp没完全显示,估计出了点问题,手动补全为data={}&timestamp={}132f1537f85sjdpcm59f7e318b9epa51,再次验证发现没问题了。
    SHA256验证结果

  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))

文章作者: 二毛erma0
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 二毛erma0 !
  目录