前言
- 之前华为商城放出 Mate60 手机时, 想给自己和家人抢购一两台,手动刷了好几天无果后,决定尝试编写程序,直接发送 POST 请求来抢。
- 通过抓包和简单重放发送后,始终不成功。仔细研究,发现 Cookie 中有一个名为 device_data 的数据比较可疑,看起来是加密后的 base64, 很有可能服务器是使用这些值进行了验证。于是决定研究一下,看是否可以破解并用于秒杀。
- 最后虽然研究出加密算法,并尝试用于秒杀,但由于仍然有其他的限制,暂时放弃,并将相关信息开源。
- 华为的兄弟姐妹们需要辛苦更改算法了 😃
查找 device_data 的生成位置
通过搜索,定位到名为 cp_20230815/.../ars_event.js 的文件,里面有对 'device_data' 赋值的操作,那么加密算法就在这里了。
补充信息: 最新版本的地址已经是 cp_20231215/.../ars_event.js, 但内容没有改变。
算法分析
初看 ars_event.js , 是进行过混淆的, 简直和天书一样, 而且以前也没怎么用过 js, 不知道怎么下手。不过好在知道 js 的所有源码都在里面, 只要肯花一点时间, 必然是能解析出来的。
于是开始分析, 此处省略一万字。。。
功夫不负有心人,断断续续经过一两周的时间,总算把算法反推出来,并且编写了 java 代码进行验证和 POST 秒杀。虽然事后证明,服务器还有其他验证方式没有破解(比如 IP、UID 验证?),并发10个线程请求, 有4个给我返回非法请求。。。)
算法解释
- device_data 的数据分两部分:
- 最前面的
*2k
常量 + 从479752
中选出的两个数(间隔 3) - 要加密的字符串先 base64, 按 4 个字符进行编码
- 然后在前面加上 8 个随机字符
- 最后再按 8 个字符为一组的方式编码.
- 最前面的
源码
- 因为源码是从 js 中反推出来的, 主要是为了满足和原有的算法一致, 命名和写法上就比较乱.
java
/**
* device_data 的加密/解密 算法
* https://res.vmallres.com/cp_20230815/js/common/risk/ars_event.js
* https://res.vmallres.com/cp_20231215/js/common/risk/ars_event.js
*/
@Slf4j
public class ArsEventCrack {
//获取华为 function _0x272042 函数中,对字符串加密时采用的位置索引列表
public LinkedList<Integer> GetEncodeStringIndexArray(int strLength, int blockSize){
//最后4字节保持原样
int charArrayLength = strLength - 4;
//String[] charArray = strInput.substring(0, strInput.length() - 4).split("");
LinkedList<LinkedList<Integer>> tmpArray = new LinkedList<>();
for(int i = 0; i < blockSize; i++){
tmpArray.add(new LinkedList<>());
for(int j = 0; j < charArrayLength; j++){
int _tmpVal_1 = j * 2 * (blockSize - 1) + i;
int _tmpVal_2 = 0;
if (_tmpVal_1 < charArrayLength){
tmpArray.get(i).add(_tmpVal_1);
}
if (i != 0) {
_tmpVal_2 = j * 2 * (blockSize - 1) - i;
if (_tmpVal_2 < charArrayLength && _tmpVal_2 > 0) {
tmpArray.get(i).add(_tmpVal_2);
}
}
if(_tmpVal_1 > charArrayLength || _tmpVal_2 > charArrayLength){
break;
}
}
}
//log.info("tmpArray={}", tmpArray);
//排序和删除重复数据
List<List<Integer>> sortedDistinctLists = tmpArray.stream().map(
linked -> linked.stream().sorted().distinct().collect(Collectors.toList()))
.collect(Collectors.toList());
LinkedList<Integer> allPositions = new LinkedList<>();
sortedDistinctLists.forEach(integers -> allPositions.addAll(integers));
return allPositions;
}
public String EncodeString(String strInput, int blockSize){
LinkedList<Integer> allPositions = GetEncodeStringIndexArray(strInput.length(), blockSize);
StringBuilder sb = new StringBuilder();
for (Integer pos : allPositions) {
sb.append(strInput.charAt(pos));
}
sb.append(strInput.substring(strInput.length() - 4));
String result = sb.toString();
//log.info("result={}", result);
return result;
}
public String repeatString(String str, int times){
if (times <= 1){
return str;
}
StringBuilder sb = new StringBuilder();
for (int i = 0 ; i < times; i++){
sb.append(str);
}
return sb.toString();
}
public String DecodeString(String strInput, int blockSize){
//除了最后4个字节的,生成指定长度, 然后获取随机字符串位置索引, 并对应替换.
int encodeLength = strInput.length() - 4;
char[] chars = repeatString("0", encodeLength).toCharArray();
LinkedList<Integer> allPositions = GetEncodeStringIndexArray(strInput.length(), blockSize);
int index = 0;
for (Integer pos : allPositions) {
chars[pos] = strInput.charAt(index);
index++;
}
String strResult = String.copyValueOf(chars) + strInput.substring(encodeLength);
return strResult;
}
//再次调用就会恢复
public String BlockString(String strInput, int blockSize){
String strResult = "";
String strWithoutLast4 = strInput.substring(0, strInput.length()-4);
int blockCount = strWithoutLast4.length() / blockSize;
for (int i = blockSize; i > 0; i--){
String tmp = strInput.substring((i - 1)*blockCount, i*blockCount);
strResult += tmp;
}
strResult += strInput.substring(strInput.length()-4);
return strResult;
}
public String DecodeDeviceDataString(String strEncodedDeviceData){
//去除前面的 *2k47 一类的随机开头
String remove2KHeader = strEncodedDeviceData.substring(5);
//第一次解码
String outerDecode = DecodeString(remove2KHeader, 8);
//解码出来, 前面8个字节是随机值
String firstUnBlock = BlockString(outerDecode, 4);
String realFirstBlock = firstUnBlock.substring(8);
//再次解码,此时解出来的就是 base64
String innerDecode = DecodeString(realFirstBlock, 4);
String strOriginal = new String(Base64.getDecoder().decode(innerDecode));
return strOriginal;
}
public String Get2kHeader(){
//*2k92
String strInput = "479752";
int randomIndex = new Random(System.currentTimeMillis()).nextInt(3);
return "*2k" + strInput.charAt(randomIndex) + strInput.charAt(randomIndex + 3);
}
public String EncodeDeviceDataString(String strEncodedDeviceData){
String strBase64 = Base64.getEncoder().encodeToString(strEncodedDeviceData.getBytes());
String innerEncode = RandomStringUtils.randomAlphanumeric(8) + EncodeString(strBase64, 4);
String strBlocked = BlockString(innerEncode, 4);
String strEncodeDeviceData = Get2kHeader() + EncodeString(strBlocked, 8);
return strEncodeDeviceData;
}
@Test
public void testAstEventCrack(){
log.info("Get2kHeader={}", Get2kHeader());
String strOriginal = "ABCDEFGHIJKLMNOPQRSTWVXYZabcdefghijklmnopqrstuvwxyz123456789";
int blockSize = 8;
String strEncoded = EncodeString(strOriginal, blockSize);
String strDecoded = DecodeString(strEncoded, blockSize);
log.info("strEncoded={}, strDecoded={}", strEncoded, strDecoded);
Assert.assertEquals(strOriginal, strDecoded);
String strBlocked = BlockString(strOriginal, blockSize);
log.info("strBlocked={}", strBlocked);
String strUnblocked = BlockString(strBlocked, blockSize);
log.info("strUnblocked={}", strUnblocked);
Assert.assertEquals(strOriginal, strUnblocked);
}
@Test
public void TestDeviceData(){
if(true){
//只解密
String strEncodedDeviceData = "*2k75xxxxxx"; // 此处输入通过 F12 或抓包获取的 device_data 字符串,运行后即可解密
String strDeviceData = DecodeDeviceDataString(strEncodedDeviceData);
log.info("strDeviceData: {}", strDeviceData);
}
if(false){
//加密后再解密
String strOriginalData = ""; //输入想要加密的字符串
//String strOriginalData = GetDeviceFingerPrint() + "_" + "[object Object]";
String strEncode = EncodeDeviceDataString(strOriginalData);
String strDecode = DecodeDeviceDataString(strEncode);
log.info("strDecode:{}", strDecode);
Assert.assertEquals(strOriginalData, strDecode);
}
}
}