某工宝协议逆向-视频快进
1. 介绍
最近一个在建筑公司上班的朋友给我说,他们现在每天都要用手机在某工宝上面看视频学习建筑知识,不能一起愉快的游戏了,让我帮忙看看能不能快进一丢丢。本着积极学习的态度,顺便看看那些并不那么出名的APP的在数据传输的时候用的协议算法,见见世面。
2.抓包
为了简便起见,这里直接使用HTTPCanary
开始抓包,定位到记录视频进度的关键部分。
-
登录。
登录部分比想象中简单,竟然是直接明文传输
这就很有意思了,说明重点不在这里。
-
播放视频
播放视频这里果然才是重点。目前市面上大概有以下两种记录学习进度方法:
-
每隔固定时间上传一次记录
-
自由上传记录,时间搭配在参数中
不过,第一种方式,服务端可能存在主动校验,将服务器经过的时间与获得的请求经过的时间做一个比较,这种方式只能通过快速提交的方法实现快进。第二种方式,可能会存在其他与时间有关的东西做签名函数做重复校验,这种方式可以通过伪造观看位置的方法实现快进。
根据抓包内容,可以看到,这里的记录进度的方式显示是通过将时间放在参数中记录。
在提交的参数中,总共有以下几种:
[
{
"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
关键字
包含了太多的sign
,其实并不好找,我们可以根据发包链接videoLogSign
去寻找
最后可以找到这是返回Observable的函数注解函数实现的发包,继续深入检索postCommonVideoLog
其中这一个吸引了注意,其中的List<VideoPlayLogEntity>
猜测可能是日志记录的实体列表。点进去查看得到VideoPlayLogEntity
,这就是我们对应的视频记录的实体类。
继续搜索VideoPlayLogEntity
看看他的sign
参数是何时被set进去的,真相已经很接近了。
根据反编译的代码变量名,我们基本可以确定刚才的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
类中的playStartPoint
、playEndPoint
、playStartTime
、playEndTime
、playType
这五个参数放入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
为:
5. 总结
- 现有许多app基本混淆都不加,很容易被反汇编获取源代码,甚至对服务器发起攻击。
- 就算不加混淆,也应该将加密函数放在so层,增加逆向难度。