记半次元App数据解密记录

Posted 痕迹天涯119

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记半次元App数据解密记录相关的知识,希望对你有一定的参考价值。

最近发现一个有意思的应用,半次元,这个应用中有很多Cosplay美图,很感兴趣便想试试能否通过抓包分析获取相应的接口,没想到自己实际上已经跳到了一个大大的深坑之中,一起来看下吧。


万里长征第一步:抓包分析

本次分析采用Fiddler和Charles皆可,若不会配置,请自行百度相关软件的使用,另外因为半次元采用的https的接口,所以这里必须要先配置CA证书,不太了解的同学可以参考:
Charles抓取https
fiddler抓包https
本文以Fiddler为例,配置完成后在Fiddler界面可以看到页面请求的https连接了,以Cos周榜为例:

如图,我们从这次post请求可以看到url,query参数,请求体body以及返回的json数据,完整url如下:
https://api.bcy.net/api/coser/topList?iid=36046028020&device_id=46019894537&ac=wifi&channel=huawei&aid=1250&app_name=banciyuan&version_code=412&version_name=4.1.2&device_platform=android&ssmix=a&device_type=BKL-AL20&device_brand=HONOR&language=zh&os_api=26&os_version=8.0.0&uuid=866953034499460&openudid=56028d53b0cb3095&manifest_version_code=20180605&resolution=1080*2160&dpi=480&update_version_code=412&_rticket=1531992443394
这里请求体body的key是data,加密内容是:
Lc59D72k2R4YFB0XMOPQRgpNKWGco6f1e86WkOur0ZArCiT+R6VlSvHQYEUtFtTVXrYpx4tE3WZV5vf043AL8XwSxskW592ULRAzrh6oEcMW9FBDzBrB+l9QIGFengtJ

因为是Post请求,这里如果直接点击打开会出现默认的提示,不会返回任何数据,所以我们可以通过Postman等工具进行请求测试,如下:

那么问题来了,data是加密的的,我们需要的是通过动态的生成data来自由根据参数获取数据,那么该怎么做呢?


万里长征第二步:反编译

为了能看到请求体中的data参数到底如何产生的,我们就需要通过反编译去分析下源码看看。

首先从官网下载半次元apk的安装包,这里地址是https://bcy.net/static/app,这里直接使用快捷方便的android killer来完成反编译逻辑,成功反编译后如下:

很容易的我们找到了班次元的源码路径,完整内容是com.banciyuan.bcywebview,由于我们能直接看到的是smali汇编源码,所以这里没太大意义,这里通过java查看器查看java源码,考虑到是网络请求,这里我们重点寻找http相关的内容,经过搜索查看,我注意到其中一个HttpUtils的文件,如下:

如图红框部分,可以看出data是在这里通过Encrypt加密后传递给服务器的,所以我们调到Encrypt来看,

  public static String a(String paramString)
  
    return a(paramString, 0);
  

在Encrypt有这样一个方法,我们继续查看调用

  public static String a(String paramString, int paramInt)
  
    return a(b(paramString, paramInt));
  

嗯,继续看b方法

  private static byte[] b(String paramString, int paramInt)
  
    Object localObject = getRandomString(paramInt);
    if (localObject != null)
    
      if (paramInt == 0) 
      try
      
        if (((String)localObject).length() != 16) 
          return null;
        
        localObject = new SecretKeySpec(((String)localObject).getBytes(Charset.defaultCharset()), "AES");
        Cipher localCipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC");
        localCipher.init(1, (Key)localObject);
        paramString = localCipher.doFinal(paramString.getBytes("utf-8"));
        return paramString;
      
      catch (Exception paramString) 
    
    return null;
  

哦,yes,看到这里应该很明白了,通过Cipher针对data数据进行了AES加密,那么密钥是什么呢?


万里长征第三步:模拟native调用

我们虽然看到了加密的逻辑,但是通过代码我们看到密钥key是通过getRandomString(paramInt)方法获取的,而这个方法又是一个native方法,代码里是这样定义的

private static native String getRandomString(int paramInt);

ok,既然是native方法,那我们也直接去在项目里调用打印出这个密钥的真实数值就行了,于是乎,我查看到这个native方法来自于librandom.so,

  static
  
    System.loadLibrary("random");
  

在我自己的项目中引入该so文件,写一个类似的调用代码,如下:

public class Encrypt 

    static
    
        System.loadLibrary("random");
    

    public void Test()
        Log.e("Encrypt",getRandomString(0));
    

    private static native String getRandomString(int paramInt);


外部调用:

        new Encrypt().Test();

嗯,执行代码结果并没有和想象的一样打印出来,反而提示找不到getRandomString这个native方法,百思不得其解,经过研究发现,native方法的命名是按照下面格式来的:
Java_com_banciyuan_bcywebview_utils_encrypt_Encrypt_getRandomString
如果你在任意项目去使用getRandomString,就会出现找不到的情况
ok,解决方案就是重新创建一个包名和半次元一致的工程,完整包名是com.banciyuan.bcywebview,再次调用就能成功打印了,结果如下:

注:这里为了防止被人商业使用,决定隐藏部分密钥内容

哈哈,到这里算是拿到了半次元的加密密钥,很多时候我们都是把密钥放到常量直接写在代码里,半次元为了这里可谓也是用心良苦了。


万里长征第四步:data数据解密

写到这里也算是清楚了半次元的加密过程以及密钥结果,理论上来讲通过这个密钥结果我们就可以伪造data进行任意的Post请求了,那么真的ok么?

如下,我在Android下写了一块解密的逻辑:

    private void decryptData() 
        Object localObject = "com_banciyuan_AI";
        String paramString = "Lc59D72k2R4YFB0XMOPQRgpNKWGco6f1e86WkOur0ZArCiT+R6VlSvHQYEUtFtTVXrYpx4tE3WZV5vf043AL8XwSxskW592ULRAzrh6oEcMW9FBDzBrB+l9QIGFengtJ";
        if (((String) localObject).length() != 16) 
            return;
        
        localObject = new SecretKeySpec(((String) localObject).getBytes(Charset.defaultCharset()), "AES");
        Cipher localCipher = null;
        try 
            localCipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC");
            localCipher.init(DECRYPT_MODE, (Key) localObject);
            paramString = new String(localCipher.doFinal(Base64.decode(paramString.getBytes("utf-8"), Base64.DEFAULT)), "UTF-8");
            Log.e("Main", paramString);
         catch (NoSuchAlgorithmException e) 
            e.printStackTrace();
         catch (NoSuchProviderException e) 
            e.printStackTrace();
         catch (NoSuchPaddingException e) 
            e.printStackTrace();
         catch (BadPaddingException e) 
            e.printStackTrace();
         catch (IllegalBlockSizeException e) 
            e.printStackTrace();
         catch (InvalidKeyException e) 
            e.printStackTrace();
         catch (UnsupportedEncodingException e) 
            e.printStackTrace();
        
    

那么,paramString打印出来到底是什么呢

“date”:“20180719”,“grid_type”:“timeline”,“token”:“4b98b48cb5b20e72”,“p”:“1”,“type”:“week”

吼吼吼,这些正是Post的核心数据了,同样的,借助于在线的加解密网站也可以看到,比如http://tool.chacuo.net/cryptaes,解密后是这样的:

结果是一样的,美滋滋,最后我们可能需要伪造data数据去请求api以达到某些不可告人的秘密==


万里长征第五步:尝试伪造body中的data

这里使用java的话应该比较简单,按照解密逆向处理就可以了,具体方法就是将DECRYPT_MODE修改为ENCRYPT_MODE,然后Base64在外层,内层仍然使用AES进行加密即可,具体可自行测试,这里我重点讲下通过Python来实现。
我们借助于Crytodome实现AES加解密,首先导入以下模块:

import base64
from Cryptodome.Cipher import AES 

# str不是16的倍数那就补足为16的倍数
def add_to_16(text):
    while len(text) % 16 != 0:
        text += '5'
    return str.encode(text)  # 返回bytes

def generate_encrypt_data():
    key = 'com_banxxxx' #隐藏部分密钥内容
    dict_data = "date": "20180719", "grid_type": "timeline", "token": "4b98b48cb5b20e72", "p": "1", "type": "week"
    data_json = json.dumps(dict_data).replace(' ', '')  # 移除多余的空格
    print(data_json)
    cipher = AES.new(add_to_16(key), AES.MODE_ECB)
    encrypted_text = str(base64.encodebytes(cipher.encrypt(add_to_16(data_json))), encoding='utf8')  # 加密
    print(encrypted_text)

generate_encrypt_data()

对比我们通过fiddler抓取到的data数据,如下:

Lc59D72k2R4YFB0XMOPQRgpNKWGco6f1e86WkOur0ZArCiT+R6VlSvHQYEUtFtTVXrYpx4tE3WZV5vf043AL8XwSxskW592ULRAzrh6oEcMW9FBDzBrB+l9QIGFengtJ
啊,什么鬼,后面几位不一样是什么鬼,不应该啊,去网页上把自己生成的解密看一下看看,

很ok啊,完全没问题,怎么编码后会不一样呢?

可能的远因:

  1. dict产生的data原数据和实际原数据不一样
  2. 填充问题,为了保证为16位的倍数,这里使用**‘\\0’**作为填充

首先来看看第一个我分别放上两次的dict转换出的数据和Android编译产生的数据:

“date”:“20180719”,“grid_type”:“timeline”,“token”:“4b98b48cb5b20e72”,“p”:“1”,“type”:“week”
“date”:“20180719”,“grid_type”:“timeline”,“token”:“4b98b48cb5b20e72”,“p”:“1”,“type”:“week”

这,一致的让我无言一队啊,完全对的上啊,再看看填充的问题,我首先查看了反编译的源码,并没有找到明确的填充逻辑,于是考虑通过全部的asiil码生成对应的加密数据在和正确的进行对比看看
经过测试,全部asciil码无一生成一致的加密数据(放弃)

经过审查代码发现,生成Cilper的逻辑是这样的

Cipher.getInstance("AES/ECB/PKCS7Padding", "BC")

这里采用的是PKCS7Padding的方式,但是在py中并没有哪里可以指定设置这个,那是如何保持一致呢?关于PKCS7Padding可以参考关于PKCS5Padding与PKCS7Padding的区别

这里我突然产生了新的灵感,如果填充不一致,那么两个原数据应该也是不一致的,但是肉眼看起来完全是一样的啊,即使通过文本对比也是一样,那么怎么去检查呢?
将两处分别产生的原数据转化为list,然后进行打印,进行字符级别的对比,新代码如下

def generate_encrypt_data():
    key = 'com_banciyuan_AI'
    dict_data = "date": "20180719", "grid_type": "timeline", "token": "4b98b48cb5b20e72", "p": "1", "type": "week"
    data_json = json.dumps(dict_data).replace(' ', '')  # 移除多余的空格
    print(len(data_json))
    print(list(data_json))
    cipher = AES.new(add_to_16(key), AES.MODE_ECB)
    encrypted_text = str(base64.encodebytes(cipher.encrypt(add_to_16(data_json))), encoding='utf8')  # 加密
    # print(encrypted_text)
    encrypted_text = "Lc59D72k2R4YFB0XMOPQRgpNKWGco6f1e86WkOur0ZArCiT+R6VlSvHQYEUtFtTVXrYpx4tE3WZV5vf043AL8XwSxskW592ULRAzrh6oEcMW9FBDzBrB+l9QIGFengtJ" #正确的加密数据
    text_decrypted = str(cipher.decrypt(base64.decodebytes(bytes(encrypted_text, encoding='utf8'))).rstrip(b'\\0').decode("utf8"))  # 解密
    print(len(text_decrypted))
    print(list(text_decrypted))

结果:

91(dict生成的)
[’’, ‘"’, ‘d’, ‘a’, ‘t’, ‘e’, ‘"’, ‘:’, ‘"’, ‘2’, ‘0’, ‘1’, ‘8’, ‘0’, ‘7’, ‘1’, ‘9’, ‘"’, ‘,’, ‘"’, ‘g’, ‘r’, ‘i’, ‘d’, ‘_’, ‘t’, ‘y’, ‘p’, ‘e’, ‘"’, ‘:’, ‘"’, ‘t’, ‘i’, ‘m’, ‘e’, ‘l’, ‘i’, ‘n’, ‘e’, ‘"’, ‘,’, ‘"’, ‘t’, ‘o’, ‘k’, ‘e’, ‘n’, ‘"’, ‘:’, ‘"’, ‘4’, ‘b’, ‘9’, ‘8’, ‘b’, ‘4’, ‘8’, ‘c’, ‘b’, ‘5’, ‘b’, ‘2’, ‘0’, ‘e’, ‘7’, ‘2’, ‘"’, ‘,’, ‘"’, ‘p’, ‘"’, ‘:’, ‘"’, ‘1’, ‘"’, ‘,’, ‘"’, ‘t’, ‘y’, ‘p’, ‘e’, ‘"’, ‘:’, ‘"’, ‘w’, ‘e’, ‘e’, ‘k’, ‘"’, ‘’]

96(java解密出来的)
[’’, ‘"’, ‘d’, ‘a’, ‘t’, ‘e’, ‘"’, ‘:’, ‘"’, ‘2’, ‘0’, ‘1’, ‘8’, ‘0’, ‘7’, ‘1’, ‘9’, ‘"’, ‘,’, ‘"’, ‘g’, ‘r’, ‘i’, ‘d’, ‘_’, ‘t’, ‘y’, ‘p’, ‘e’, ‘"’, ‘:’, ‘"’, ‘t’, ‘i’, ‘m’, ‘e’, ‘l’, ‘i’, ‘n’, ‘e’, ‘"’, ‘,’, ‘"’, ‘t’, ‘o’, ‘k’, ‘e’, ‘n’, ‘"’, ‘:’, ‘"’, ‘4’, ‘b’, ‘9’, ‘8’, ‘b’, ‘4’, ‘8’, ‘c’, ‘b’, ‘5’, ‘b’, ‘2’, ‘0’, ‘e’, ‘7’, ‘2’, ‘"’, ‘,’, ‘"’, ‘p’, ‘"’, ‘:’, ‘"’, ‘1’, ‘"’, ‘,’, ‘"’, ‘t’, ‘y’, ‘p’, ‘e’, ‘"’, ‘:’, ‘"’, ‘w’, ‘e’, ‘e’, ‘k’, ‘"’, ‘’, ‘\\x05’, ‘\\x05’, ‘\\x05’, ‘\\x05’, ‘\\x05’]

对比发现,首先长度是不一致的,后面的多了五个**’\\x05’,吼吼吼,问题就在这里了,填充在Android端半次元使用这个二进制字符来完成,我们再修改add_to_16将填充改成’\\x05’**,修改后如下:

def generate_encrypt_data():
    key = 'com_banciyuan_AI'
    dict_data = "date": "20180719", "grid_type": "timeline", "token": "4b98b48cb5b20e72", "p": "1", "type": "week"
    data_json = json.dumps(dict_data).replace(' ', '')  # 移除多余的空格
    print(data_json+"\\n")
    cipher = AES.new(add_to_16(key), AES.MODE_ECB)
    encrypted_text = str(base64.encodebytes(cipher.encrypt(add_to_16(data_json))), encoding='utf8')  # 加密
    print(encrypted_text)
    text_decrypted = str(cipher.decrypt(base64.decodebytes(bytes(encrypted_text, encoding='utf8'))).rstrip(b'\\x05').decode("utf8"))  # 解密
    print(text_decrypted)

执行代码:

好,到这里本文算是彻底完结了,其实这是很主流的移动端加密传输方式了,Https+AES+Native密钥存储,可以看到半次元在这块做的还是很充分的,如有疑问,请在评论指出。


参考博客链接:
Android Native方法找不到的问题
在线加解密
python3.6执行AES加密及解密方法

郑重声明:本文仅用于学习交流,禁止用于任何商业用途

以上是关于记半次元App数据解密记录的主要内容,如果未能解决你的问题,请参考以下文章

Python爬取半次元图片[一]

一篇文章,采集四个网站,它们是阳光理政,图虫网,书伴网,半次元网

一篇文章,采集四个网站,它们是阳光理政,图虫网,书伴网,半次元网

头条系定向流量包含哪些app

求一些楪祈的COS图片

gentoo annie youku video