侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计创建 22 个标签
  • 累计收到 27 条评论

目 录CONTENT

文章目录

某工宝协议逆向-视频快进

vchopin
2022-11-19 / 1 评论 / 1 点赞 / 268 阅读 / 4,698 字

某工宝协议逆向-视频快进

1. 介绍

最近一个在建筑公司上班的朋友给我说,他们现在每天都要用手机在某工宝上面看视频学习建筑知识,不能一起愉快的游戏了,让我帮忙看看能不能快进一丢丢。本着积极学习的态度,顺便看看那些并不那么出名的APP的在数据传输的时候用的协议算法,见见世面。

2.抓包

为了简便起见,这里直接使用HTTPCanary开始抓包,定位到记录视频进度的关键部分。

  1. 登录。

    登录部分比想象中简单,竟然是直接明文传输

image

这就很有意思了,说明重点不在这里。

  1. 播放视频

    播放视频这里果然才是重点。目前市面上大概有以下两种记录学习进度方法:

    • 每隔固定时间上传一次记录

    • 自由上传记录,时间搭配在参数中

      不过,第一种方式,服务端可能存在主动校验,将服务器经过的时间与获得的请求经过的时间做一个比较,这种方式只能通过快速提交的方法实现快进。第二种方式,可能会存在其他与时间有关的东西做签名函数做重复校验,这种方式可以通过伪造观看位置的方法实现快进。

      根据抓包内容,可以看到,这里的记录进度的方式显示是通过将时间放在参数中记录。

      image-1668873133126

      在提交的参数中,总共有以下几种:

      [
          {
              "classStuId": "6aa4b9df-fc16-4dc5-96b2-a9c7e8c68cc6", 
              "id": 1, 
              "lastPlayPotion": 0, 
              "logType": "1", 
              "memberId": "66ad20b2-f8e5-4576-a267-fdbf22189bb2", 
              "packId": "fcf228cc-1ee4-11ed-a2ee-00ff07067ec4", 
              "playEndPoint": 11, 
              "playEndTime": "2022-11-19 18:02:21", 
              "playStartPoint": 1, 
              "playStartTime": "2022-11-19 18:02:11", 
              "playType": 1, 
              "sign": "803227E1CA186200B97DDEA748033BA1", 
              "stuHourDetailId": "1ed655dc-9688-6ed7-9fca-39c8584a299d"
          }
      ]
      

      大胆猜测,其中的playEndPoint就是记录视频观看到那个位置的一个关键变量,因此我们只需要伪造这个变量,改成这个视频的总时间,应该就能实现快进的原理。

      其中有个关键参数sign应该就是我们这里最难的一部分了,这类跟随其余参数一起上传的sign一般都是某个散列函数,用来在服务端校验数据有没有被修改过,粗略猜测是个md5算法。

3. 反编译

为了能够确定sign是如何得到的,这里对原有app进行反编译,这里使用jadx快速看一眼,没有加壳,感谢作者。根据关键发包链接检索代码,我们这里就直接搜索sign关键字

image-1668873148570

包含了太多的sign,其实并不好找,我们可以根据发包链接videoLogSign去寻找

image-1668873164900

最后可以找到这是返回Observable的函数注解函数实现的发包,继续深入检索postCommonVideoLog

image-1668873173718

其中这一个吸引了注意,其中的List<VideoPlayLogEntity>猜测可能是日志记录的实体列表。点进去查看得到VideoPlayLogEntity,这就是我们对应的视频记录的实体类。

image-1668873182848

继续搜索VideoPlayLogEntity看看他的sign参数是何时被set进去的,真相已经很接近了。

image-1668873189240

根据反编译的代码变量名,我们基本可以确定刚才的sign肯定是跟MD5有关系,至于怎么做的散列,需要在看看源码,点进去查看源码。其代码如下:

    public static void sgin(List<VideoPlayLogEntity> list) {
        for (VideoPlayLogEntity videoPlayLogEntity : list) {
            LinkedHashMap linkedHashMap = new LinkedHashMap();
            linkedHashMap.put("playStartPoint", Integer.valueOf(videoPlayLogEntity.getPlayStartPoint()));
            linkedHashMap.put("playEndPoint", Integer.valueOf(videoPlayLogEntity.getPlayEndPoint()));
            linkedHashMap.put("playStartTime", videoPlayLogEntity.getPlayStartTime());
            linkedHashMap.put("playEndTime", videoPlayLogEntity.getPlayEndTime());
            linkedHashMap.put("playType", Integer.valueOf(videoPlayLogEntity.getPlayType()));
            String sgin = sgin(linkedHashMap, Constants.LogKey);
            Log.d("=====result:", "sgin=" + sgin);
            videoPlayLogEntity.sign = sgin;
        }
    }

在这个方法中是将VideoPlayLogEntity类中的playStartPointplayEndPointplayStartTimeplayEndTimeplayType这五个参数放入LinkedHashMap中,然后传入另外一个重载的sgin方法中。这个重载的sgin还有一个参数是Constants.LogKey,点击得到定义的salt:

  public static final String LogKey = "d^*A%8^43YsrYZ$9";

继续进入到重载的sgin方法中,代码如下:

    public static String sgin(Map<String, Object> map, String str) {
        Set<String> keySet = map.keySet();
        String[] strArr = (String[]) keySet.toArray(new String[keySet.size()]);
        StringBuilder sb = new StringBuilder();
        for (String str2 : strArr) {
            if (String.valueOf(map.get(str2)).trim().length() > 0) {
                sb.append(str2);
                sb.append("=");
                sb.append(String.valueOf(map.get(str2)).trim());
                sb.append("&");
            }
        }
        sb.append("key=" + str);
        System.out.print("=====sgin:" + sb.toString());
        return md5(str, sb.toString()).toUpperCase();
    }

这部分代码就是将接收到的Map中各个参数取出来并拼接成如下所示字符串:

playStartPoint=3&playEndPoint=2297&playStartTime=2022-11-19 17:48:00&playEndTime=2022-11-19 17:48:33&playType=1&key=d^*A%8^43YsrYZ$9

随后将这部分字符串放入定义的md5函数

    public static String md5(String str, String str2) {
        try {
            MessageDigest instance = MessageDigest.getInstance("MD5");
            instance.update(str.getBytes());
            instance.update(str2.getBytes(InternalZipConstants.CHARSET_UTF8));
            byte[] digest = instance.digest();
            String str3 = "";
            for (int i = 0; i < digest.length; i++) {
                str3 = str3 + Integer.toHexString((digest[i] & 255) | InputDeviceCompat.SOURCE_ANY).substring(6);
            }
            return str3.toUpperCase();
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }


很明显,这是对经典md5算法的一种魔改,将经典md5算法得到的结果与255做与操作然后与InputDeviceCompat.SOURCE_ANY(-256)做或操作,最后将得到的整数转成十六进制,取十六进制最后两位。

4. 协议复现

有了上述原理就很好复现出快进协议了,整理python的代码如下:

def get_sign(sp, ep, st='2022-11-19 17:48:00', et='2022-11-19 17:48:33'):
    salt = 'd^*A%8^43YsrYZ$9'
    sign_input = f'playStartPoint={sp}&playEndPoint={ep}&playStartTime={st}&playEndTime={et}&playType=1&key=d^*A%8^43YsrYZ$9'
    m = hashlib.md5()
    m.update(salt.encode())
    m.update(bytes(sign_input, 'UTF-8'))

    digest = m.digest()
    result = ''
    for b in digest:
        h = hex(((b & 255) | -256) & 0xffffffff)
        result += h[-2:]
    return result.upper()

这里有个坑,python的hex()函数转十六进制是不包括负号的,也就是不会把二进制第一个1看成是负号,而java的Integer.toHexString()这个函数会将二进制第一个1看作是负号。因此,python需要在与上0xffffffff才能得到与java一致的结果。

最终复现结果得到sign为:

image-1668873199633

5. 总结

  1. 现有许多app基本混淆都不加,很容易被反汇编获取源代码,甚至对服务器发起攻击。
  2. 就算不加混淆,也应该将加密函数放在so层,增加逆向难度。
1
  • 1
  • 1

评论区