华为商城秒杀时加密验证 device_data 的算法研究

前言

  • 之前华为商城放出 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);
    }
  }
}
相关推荐
中关村科金9 分钟前
中关村科金推出得助音视频鸿蒙SDK,助力金融业务系统鸿蒙化提速
华为·音视频·harmonyos
繁依Fanyi29 分钟前
828 华为云征文|华为 Flexus 云服务器部署 RustDesk Server,打造自己的远程桌面服务器
运维·服务器·开发语言·人工智能·pytorch·华为·华为云
小强在此4 小时前
基于OpenHarmony(开源鸿蒙)的智慧医疗综合应用系统
华为·开源·团队开发·健康医疗·harmonyos·开源鸿蒙
奔跑的露西ly11 小时前
【鸿蒙 HarmonyOS NEXT】popup弹窗
华为·harmonyos
繁依Fanyi12 小时前
828华为云征文|华为Flexus云服务器搭建OnlyOffice私有化在线办公套件
服务器·开发语言·前端·python·算法·华为·华为云
Bob999820 小时前
电脑浏览器访问华为路由器报错,无法访问路由器web界面:ERR_SSL_VERSION_OR_CIPHER_MISMATCH 最简单的解决办法!
开发语言·javascript·网络·python·网络协议·华为·ssl
让开,我要吃人了1 天前
OpenHarmony鸿蒙( Beta5.0)摄像头实践开发详解
驱动开发·华为·移动开发·harmonyos·鸿蒙·鸿蒙系统·openharmony
SUGERBOOM1 天前
华为eNSP使用详解
网络·华为
像风一样自由20202 天前
深入了解HarmonyOS(鸿蒙操作系统)
华为·harmonyos
青瓷看世界2 天前
华为HarmonyOS地图服务 -- 如何实现地图呈现?-- HarmonyOS自学8
华为·harmonyos