🔍 执行摘要

基于对wx读书Android客户端的逆向工程分析,仅供学习交流
微信读书客户端无加固,使用jadx反编译

开启unicode字符转译
开启unicode字符转译
开启unicode字符转译
开启反混淆
开启反混淆
开启反混淆

反混淆后的包已经相对较好阅读,使用jadx导出为gradle项目,使用idea/android studio方便阅读

安全风险分析

会话管理安全缺陷

可预测的Session ID生成

  • sources/com/tencent/weread/model/domain/Session.java
  • sources/moai/storage/Domain.java
  • sources/com/tencent/weread/WeReadFragmentActivity.java
java
1
2
3
4
5
6
7
8
9
10
// Session.java - 会话ID生成逻辑
public class Session extends Domain implements Parcelable {
public static int generateId(String str) {
return Domain.hashId(str); // 调用Domain层哈希方法
}

private int generateId() {
return Domain.hashId(this.sid); // 基于sid生成会话ID
}
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
// Domain.java - 哈希算法实现
public static int BKDRHashPositiveInt(String str) {
int iCharAt = 0;
for (int i10 = 0; i10 < str.length(); i10++) {
iCharAt = (iCharAt * WeReadFragmentActivity.ARG_GOTO_SWITCH_LOGIN) + str.charAt(i10);
}
return Integer.MAX_VALUE & iCharAt; // 确保返回正整数
}

public static int hashId(Object... objArr) {
Joiner joiner2 = joiner;
return BKDRHashPositiveInt(joiner2.b(Arrays.asList(objArr)));
}
java
1
2
// WeReadFragmentActivity.java - 哈希乘数常量
public static final int ARG_GOTO_SWITCH_LOGIN = 131; // BKDR哈希乘数

风险评估

风险类别 严重程度 描述
算法可预测性 高危 使用确定性BKDR哈希,相同输入产生相同输出
参数暴露 高危 哈希乘数(131)硬编码在客户端代码中
会话劫持 高危 攻击者可计算任意用户的Session ID

攻击向量

  1. 会话ID预测:通过已知sid值计算对应的Session ID
  2. 用户冒充:利用计算出的Session ID冒充其他用户
  3. 数据操作:修改或删除他人的聊天记录和会话数据

财务数据安全风险

敏感财务信息明文存储

  • sources/com/tencent/weread/model/domain/Account.java
  • app/src/main/java/com/tencent/weread/model/domain/Account.java
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Account.java - 用户账户数据模型
public class Account extends Domain implements Parcelable {
// 财务相关敏感字段
private double balance; // 账户余额
private double expiryBalance; // 过期余额
private double giftBalance; // 礼品余额
private double peerBalance; // 对等余额
private double peerGiftBalance; // 对等礼品余额

// 认证相关敏感字段
private String accessToken; // 访问令牌
private String refreshToken; // 刷新令牌
private String wxAccessToken; // 微信访问令牌

// 数据库表结构定义
static {
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
COLUMNS = linkedHashMap;
linkedHashMap.put("accessToken", "varchar");
linkedHashMap.put("refreshToken", "varchar");
linkedHashMap.put("balance", "double");
linkedHashMap.put("peerBalance", "double");
linkedHashMap.put("giftBalance", "double");
linkedHashMap.put("expiryBalance", "double");
linkedHashMap.put("wxAccessToken", "varchar");
}
}

安全风险评估

数据类型 存储方式 风险等级 潜在影响
账户余额 明文存储 严重 财务数据泄露,可被任意修改
访问令牌 明文存储 严重 账户完全接管
刷新令牌 明文存储 严重 长期账户控制
微信令牌 明文存储 高危 跨平台账户风险

数据暴露风险

  1. 直接数据库访问:Root设备可直接读取SQLite文件
  2. 应用间数据泄露:恶意应用可能获取数据库访问权限
  3. 备份数据泄露:系统备份可能包含明文敏感数据

认证凭证泄露风险

敏感信息记录到日志

java
1
2
3
4
5
6
// AccountManager.java
ELog.INSTANCE.log(3, TAG, "getLoginAccount from db:" +
account.getVid() + ',' +
account.getAccessToken() + ',' +
account.getRefreshToken() + ',' +
account.getFirstLogin());

安全隐患

  • 访问令牌和刷新令牌被明文记录到日志文件
  • 日志文件可本地随意读取 易导致令牌泄露

硬编码加密密钥风险

AES密钥硬编码在客户端

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// AESEncrypt.java - 加密工具类
public class AESEncrypt {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/CTR/NoPadding";

// 硬编码的AES密钥 - 严重安全缺陷
public static final String SDK_APP_SECRET = "isvrbeT7qUywVEZ1Ia0/aUVA/TcFaeV0wC8qFLc8rg4=";

// 默认加密方法直接使用硬编码密钥
public static String encrypt(String str) {
try {
return encrypt(SDK_APP_SECRET, str); // 所有用户共享同一密钥
} catch (Exception e10) {
e10.getLocalizedMessage();
return "";
}
}

// 解密方法同样使用硬编码密钥
public static String decrypt(String str, String str2) throws Exception {
// 使用硬编码密钥进行解密操作
SecretKeySpec secretKeySpec = new SecretKeySpec(a.b(str), ALGORITHM);
// ... 解密逻辑
}
}

该硬编码密钥在以下位置被引用:

  • sources/ga/b.java - 数据解密操作
  • app/src/main/java/ga/C9415b.java - 加密标签处理

安全风险评估

风险维度 严重程度 具体描述
密钥暴露 严重 AES-256密钥完全暴露在客户端代码中
全局影响 严重 所有用户共享同一加密密钥
密钥轮换 严重 无法进行密钥更新和轮换
数据保护 严重 加密形同虚设,任何人都可解密数据

攻击场景

  • 静态分析攻击:直接从APK中提取密钥
  • 数据库攻击:解密本地存储的加密数据
  • 中间人攻击:解密网络传输中的加密数据(如果使用了该密钥)
  • 批量数据泄露:处理大量用户的加密数据
plaintext
1
2
3
// 攻击者可以直接使用硬编码密钥解密数据
String hardcodedKey = "isvrbeT7qUywVEZ1Ia0/aUVA/TcFaeV0wC8qFLc8rg4=";
String decryptedData = AESEncrypt.decrypt(hardcodedKey, encryptedUserData);

支付系统绕过漏洞

java
1
2
3
4
5
6
7
8
9
// PaySQLiteHelper.java - 支付相关SQL操作
private static final String sqlUpdateChapterPaid =
"UPDATE Chapter SET paid=1 WHERE bookId=? AND chapterUid IN $inClause$";

private static final String sqlUpdateAllChapterPaid =
"UPDATE Chapter SET paid=1 WHERE bookId=?";

private static final String sqlSetBookChapterPaid =
"UPDATE Book SET intergrateAttr = intergrateAttr | 128 WHERE bookId=?";

小问题 只是本地状态 仍有多个api进行付费状态同步 (当然全hook了也行)

java
1
2
3
4
5
6
7
8
9
@POST("/book/chapterInfos")
Observable<ChapterInfoList> GetChapterInfos(
@JSONField("bookIds") Iterable<String> bookIds,
@JSONField("synckeys") Iterable<Long> syncKeys,
@JSONField("paidCount") Iterable<Integer> paidCounts, // 付费数量验证
@JSONField("price") Iterable<Double> prices, // 价格验证
@JSONField("maxfreeIdx") Iterable<Integer> iterable,
@JSONField("teenmode") int i10
);

章节付费检查

  • TTSBookPlayer.java:376
java
1
2
3
4
5
6
// TTSBookPlayer.java - 阅读时检查付费状态
boolean z12 = !BookHelper.isLimitedFree(bookInfoFromDB) &&
BookHelper.isChapterCostMoney(bookInfoFromDB,
chapter.getChapterIdx(),
chapter.getPrice(),
chapter.getPaid()); // 综合检查付费状态

会员状态验证

  • ComicFragment.java:2560-2562
java
1
2
3
4
// ComicFragment.java - 会员和付费状态综合验证
boolean zIsNeedPayChapter = this.mReaderCursor.isNeedPayChapter(getMBook(), chapter.getChapterUid());
WRLog.log(3, getTAG(), "checkReaderTips membership:" + isMemberShipValid() + " paid:" + zIsNeedPayChapter);
if (!(isMemberShipValid() && zIsNeedPayChapter && !AccountManager.Companion.getInstance().getMemberCardSummary().isFreeUsing()))

本文仅用于技术研究和安全教育目的,请勿用于恶意攻击