🔍 执行摘要
基于对wx读书Android客户端的逆向工程分析,仅供学习交流
微信读书客户端无加固,使用jadx 反编译
开启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
1 2 3 4 5 6 7 8 9 10 public class Session extends Domain implements Parcelable { public static int generateId (String str) { return Domain.hashId(str); } private int generateId () { return Domain.hashId(this .sid); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 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))); }
1 2 public static final int ARG_GOTO_SWITCH_LOGIN = 131 ;
风险评估 :
风险类别
严重程度
描述
算法可预测性
高危
使用确定性BKDR哈希,相同输入产生相同输出
参数暴露
高危
哈希乘数(131)硬编码在客户端代码中
会话劫持
高危
攻击者可计算任意用户的Session ID
攻击向量 :
会话ID预测 :通过已知sid值计算对应的Session ID
用户冒充 :利用计算出的Session ID冒充其他用户
数据操作 :修改或删除他人的聊天记录和会话数据
财务数据安全风险
敏感财务信息明文存储
sources/com/tencent/weread/model/domain/Account.java
app/src/main/java/com/tencent/weread/model/domain/Account.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 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" ); } }
安全风险评估 :
数据类型
存储方式
风险等级
潜在影响
账户余额
明文存储
严重
财务数据泄露,可被任意修改
访问令牌
明文存储
严重
账户完全接管
刷新令牌
明文存储
严重
长期账户控制
微信令牌
明文存储
高危
跨平台账户风险
数据暴露风险 :
直接数据库访问 :Root设备可直接读取SQLite文件
应用间数据泄露 :恶意应用可能获取数据库访问权限
备份数据泄露 :系统备份可能包含明文敏感数据
认证凭证泄露风险
敏感信息记录到日志
1 2 3 4 5 6 ELog.INSTANCE.log(3 , TAG, "getLoginAccount from db:" + account.getVid() + ',' + account.getAccessToken() + ',' + account.getRefreshToken() + ',' + account.getFirstLogin());
安全隐患 :
访问令牌和刷新令牌被明文记录到日志文件
日志文件可本地随意读取 易导致令牌泄露
硬编码加密密钥风险
AES密钥硬编码在客户端
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 public class AESEncrypt { private static final String ALGORITHM = "AES" ; private static final String TRANSFORMATION = "AES/CTR/NoPadding" ; 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中提取密钥
数据库攻击:解密本地存储的加密数据
中间人攻击:解密网络传输中的加密数据(如果使用了该密钥)
批量数据泄露:处理大量用户的加密数据
1 2 3 // 攻击者可以直接使用硬编码密钥解密数据 String hardcodedKey = "isvrbeT7qUywVEZ1Ia0/aUVA/TcFaeV0wC8qFLc8rg4="; String decryptedData = AESEncrypt.decrypt(hardcodedKey, encryptedUserData);
支付系统绕过漏洞
1 2 3 4 5 6 7 8 9 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了也行)
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 ) ;
章节付费检查
1 2 3 4 5 6 boolean z12 = !BookHelper.isLimitedFree(bookInfoFromDB) && BookHelper.isChapterCostMoney(bookInfoFromDB, chapter.getChapterIdx(), chapter.getPrice(), chapter.getPaid());
会员状态验证
ComicFragment.java:2560-2562
1 2 3 4 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()))
本文仅用于技术研究和安全教育目的,请勿用于恶意攻击