换到 Hugo 作为新程序写文章,感觉省事了很多。
书客是近一两年来增长很快的小说站,主要是宅文比较多,有时候也会看点书在上面;自己也有一个聚合了所有小说站的追书爬虫,需要接口抓书客数据。
其实一看这前端像是线上的测试版直接发上来的,然而书客的加密是我见过的小说站做的最好难搞的
网页版
网页版有前辈分析出来了,在 http://blog.konge.pw/archives/10/
这里就不说了,本文主要进行的是 Android App 的逆向
而且网页版的更新一下让爬虫失效简直太容易了
Android App
App 使用 API 接口 http://app.hbooker.com/ 和服务器通信,但是直接抓包只能抓到这样的东西
很显然是加密的,简单尝试下 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,成功解包
dex2jar
有了 dex 就方便了,老生常谈使用 dex2jar 还原成 jar
jd-gui
使用 jd-gui 反编译 jar 为 java 代码
有很多无关代码,我们只关心 com.kuangxiang.novel
搜索 javax.crypto
发现在 utils/ParseKsy.class
中进行了 AES 加密
观察相关调用方法
得知先实例化 ParseKsy
然后调用 decrypt()
方法
ParseKsy
如下
decrypt
如下,使用 Base64 解码,然后 cipher 解密
阅读代码,使用 AES 加密,模式为 CBC,使用 PKCS7 作为填充算法,IV 为长度 16 的 0x00 Byte Array
密钥算法
阅读上面实例化 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))
}