雅白

Programmer, Data Analyst and Gamer

github twitter rss
欢乐书客加密算法及 360 加固脱壳方式研究
Jul 27, 2017
已阅读了 3 分钟

换到 Hugo 作为新程序写文章,感觉省事了很多。

书客是近一两年来增长很快的小说站,主要是宅文比较多,有时候也会看点书在上面;自己也有一个聚合了所有小说站的追书爬虫,需要接口抓书客数据。

其实一看这前端像是线上的测试版直接发上来的,然而书客的加密是我见过的小说站做的最好难搞

网页版

网页版有前辈分析出来了,在 http://blog.konge.pw/archives/10/

这里就不说了,本文主要进行的是 Android App 的逆向

而且网页版的更新一下让爬虫失效简直太容易了

Android App

App 使用 API 接口 http://app.hbooker.com/ 和服务器通信,但是直接抓包只能抓到这样的东西

Encrypted Traffic

很显然是加密的,简单尝试下 Param 的参数也没法解密。

于是尝试解包 Apk 分析加密算法

解包后看是 360 加固过的,真 classes.dex 被封装在 .so 里面,自己又不会 IDA,陷入江局

直到我注意到了

ART

dex 文件在 ART 模式上运行需要转换为 oat 格式,因此不管是什么壳在还原代码时都少不了要将解密后的 dex 文件利用 dex2oat 进行还原

原代码 https://android.googlesource.com/platform/art/+/kitkat-release/dex2oat/dex2oat.cc#924

// art/dex2oat/dex2oat.cc L924
// Ensure opened dex files are writable for dex-to-dex transformations.
    for (const auto& dex_file : dex_files) {
      if (!dex_file->EnableWrite()) {
        PLOG(ERROR) << "Failed to make .dex file writeable '" << dex_file->GetLocation() << "'\n";
      }
    }

改写成这样

// art/dex2oat/dex2oat.cc L924
// Ensure opened dex files are writable for dex-to-dex transformations.
    for (const auto& dex_file : dex_files) {
      if (!dex_file->EnableWrite()) {
        PLOG(ERROR) << "Failed to make .dex file writeable '" << dex_file->GetLocation() << "'\n";
      }
      // begin 360 jiagu dump
      std::string dex_name = dex_file->GetLocation();
      LOG(INFO) << "harvey:dex file name-->" << dex_name;
      if(dex_name.find(".jiagu")!=std::string::npos){
        int len = dex_file->Size();
        char filename[150] = {0};
        sprintf(filename,"%s_%d.dex",dex_name.c_str(),len);
        int fd = open(filename,O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
        if(fd>0){
          if(write(fd,(char*)dex_file->Begin(),len)<=0){
            LOG(INFO) << "harvey:write target dex file failed-->" << filename;
          }
          LOG(INFO) << "harvey:write target dex file successfully-->" << filename;
          close(fd);
        }else{
          LOG(INFO) << "harvey:open target dex file failed-->" << filename;
        }
      }
      // end
    }

因为 360 存在 .jiagu 目录下,我们可以使用 .jiagu 进行过滤,如果是 360 加固,则将未还原成 oat 的 classes.dex 写到 app 目录的 .jiagu 文件夹内

编译创建虚拟机,安装运行书客 app,成功解包

Dex File

dex2jar

有了 dex 就方便了,老生常谈使用 dex2jar 还原成 jar

jd-gui

使用 jd-gui 反编译 jar 为 java 代码

Decompiled

有很多无关代码,我们只关心 com.kuangxiang.novel

搜索 javax.crypto 发现在 utils/ParseKsy.class 中进行了 AES 加密

ParseKsy.class

观察相关调用方法

Ref

得知先实例化 ParseKsy 然后调用 decrypt() 方法

ParseKsy 如下

ParseKsy

decrypt 如下,使用 Base64 解码,然后 cipher 解密

decrypt

阅读代码,使用 AES 加密,模式为 CBC,使用 PKCS7 作为填充算法,IV 为长度 16 的 0x00 Byte Array

IV

密钥算法

阅读上面实例化 ParseKsy 的代码,密钥为 sha256(zG2nSeEfSHfvTCHy5LCcqtBbQehKNLXn) 的结果取前 32 位

其它

经过测试,书客本地存储的 txt 小说文件也可通过此方法解密

测试代码

以下代码在 Windows 10, Go v1.8.1 x86_64 下通过

package main

import (
	"fmt"
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"crypto/sha256"
)
const (
	Encrypt_Key = "zG2nSeEfSHfvTCHy5LCcqtBbQehKNLXn"
)

var (
	IV = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
	test = "c1SR02T7X+xmq37zfs0U8NAj73eedAs3tnXMQKDNUPlI2vcaNRXpKA3JktMoffp3EYPCsvCjzeCJUynjDISbNP4D5HjaCp6tMrOsBBfQzVI="
)

func SHA256(data []byte) []byte {
	ret := sha256.Sum256(data)
	return ret[:]
}

func Base64Decode(encoded string) ([]byte, error) {
	decoded, err := base64.StdEncoding.DecodeString(encoded)
	if err != nil {
		return decoded, err
	}
	return decoded, nil
}

func LoadKey() []byte {
	Key := SHA256([]byte(Encrypt_Key))
	return Key[:32]
}

func AESDecrypt(ciphertext []byte) ([]byte, error) {
	key := LoadKey()
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	// Generally use first 16 bytes cipher text as IV
	// in this case they use 16 bytes 0x00
	blockModel := cipher.NewCBCDecrypter(block, IV)
	plainText := make([]byte, len(ciphertext))
	blockModel.CryptBlocks(plainText, ciphertext)
	plainText = PKCS7UnPadding(plainText)
	return plainText, nil
}

func PKCS7UnPadding(plainText []byte) []byte {
	length := len(plainText)
	unpadding := int(plainText[length-1])
	return plainText[:(length - unpadding)]
}

func main(){
	decoded, err := Base64Decode(test)
	if err != nil {
		panic(err)
	}
	raw, err := AESDecrypt(decoded)
	if err != nil {
		panic(err)
	}
	fmt.Println("raw byte", raw)
	fmt.Println("string()", string(raw))
}

result


回到文章列表


comments powered by Disqus