第一章:静态分析的艺术
本章字数:约14000字 阅读时间:约45分钟 难度等级:★★★☆☆
声明:本文中的公司名称、包名、API地址、密钥等均已脱敏处理,使用虚构名称替代。"梦想世界"、"dreamworld"等均为虚构名称,与任何真实公司无关。
引言
在上一章中,我们遇到了一个"铜墙铁壁"般的APP——抓包工具完全失效,Frida注入直接崩溃。面对这样的困境,我们需要换一个思路:既然无法在运行时观察APP的行为,那就从源头入手,分析APP的代码本身。
这就是静态分析——在不运行程序的情况下,通过分析程序的代码、结构、资源等,理解程序的工作原理。
静态分析是逆向工程的基础。一个优秀的逆向工程师,必须具备扎实的静态分析能力。本章将详细介绍如何对Android APK进行静态分析,以及在分析"梦想世界"APP过程中的发现。
2.1 APK文件结构解析
2.1.1 什么是APK?
APK(Android Package)是Android应用程序的安装包格式。本质上,APK是一个ZIP压缩文件,包含了应用程序运行所需的所有资源。
# 查看APK文件类型
$ file android-dreamworld-arm64-v8a-prod-v8.x.x.apk
android-dreamworld-arm64-v8a-prod-v8.x.x.apk: Zip archive data
2.1.2 APK内部结构
一个典型的APK文件包含以下内容:
app.apk
├── AndroidManifest.xml # 应用清单文件(二进制XML)
├── classes.dex # 主要的Dalvik字节码
├── classes2.dex # 第二个DEX文件(如果有)
├── classes3.dex # 第三个DEX文件(如果有)
├── ...
├── resources.arsc # 编译后的资源文件
├── res/ # 资源目录
│ ├── drawable/ # 图片资源
│ ├── layout/ # 布局文件
│ ├── values/ # 值资源(字符串、颜色等)
│ └── ...
├── lib/ # 原生库目录
│ ├── armeabi-v7a/ # 32位ARM库
│ ├── arm64-v8a/ # 64位ARM库
│ ├── x86/ # 32位x86库
│ └── x86_64/ # 64位x86库
├── assets/ # 原始资源文件
├── META-INF/ # 签名信息
│ ├── MANIFEST.MF # 清单文件
│ ├── CERT.SF # 签名文件
│ └── CERT.RSA # 证书文件
└── kotlin/ # Kotlin元数据(如果使用Kotlin)
2.1.3 目标APK概览
让我们先看看"梦想世界"APK的基本信息:
# 查看APK大小
$ ls -lh android-dreamworld-arm64-v8a-prod-v8.x.x.apk
-rw-r--r-- 1 user staff 156M Dec 17 23:00 android-dreamworld-arm64-v8a-prod-v8.x.x.apk
# 156MB!这是一个相当大的APP
使用unzip命令查看APK内容:
$ unzip -l android-dreamworld-arm64-v8a-prod-v8.x.x.apk | head -50
Archive: android-dreamworld-arm64-v8a-prod-v8.x.x.apk
Length Date Time Name
--------- ---------- ----- ----
15284 2024-12-15 10:30 AndroidManifest.xml
8234567 2024-12-15 10:30 classes.dex
7654321 2024-12-15 10:30 classes2.dex
6543210 2024-12-15 10:30 classes3.dex
...
668432 2024-12-15 10:30 lib/arm64-v8a/libSecurityCore.so
234567 2024-12-15 10:30 lib/arm64-v8a/libc++_shared.so
...
关键发现:
- APK大小:156MB(非常大)
- DEX文件:16个(classes.dex到classes16.dex)
- SO库:124个(在lib/arm64-v8a/目录下)
这是一个规模庞大的应用程序。
2.2 使用APKTool反编译
2.2.1 APKTool简介
APKTool是一款开源的Android APK反编译工具,可以将APK文件反编译为可读的smali代码和资源文件。
主要功能:
- 反编译APK为smali代码
- 反编译资源文件(XML、图片等)
- 重新打包修改后的APK
安装APKTool:
# macOS
brew install apktool
# 或者下载jar文件
wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.9.3.jar
2.2.2 反编译过程
# 反编译APK
$ java -jar apktool.jar d android-dreamworld-arm64-v8a-prod-v8.x.x.apk -o dw_mall_unpacked
I: Using Apktool 2.9.3 on android-dreamworld-arm64-v8a-prod-v8.x.x.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/user/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes2.dex...
I: Baksmaling classes3.dex...
...
I: Baksmaling classes16.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
反编译过程大约需要5-10分钟(取决于电脑性能)。
2.2.3 反编译结果分析
反编译完成后,让我们看看目录结构:
$ tree -L 2 dw_mall_unpacked/
dw_mall_unpacked/
├── AndroidManifest.xml # 应用清单(已解码为可读XML)
├── apktool.yml # APKTool配置文件
├── assets/ # 原始资源
│ ├── dexopt/ # DEX优化相关
│ ├── fonts/ # 字体文件
│ ├── geolocation/ # 地理位置相关
│ └── ...
├── kotlin/ # Kotlin元数据
├── lib/ # 原生库
│ └── arm64-v8a/ # 64位ARM库(124个SO文件)
├── original/ # 原始META-INF
├── res/ # 资源文件
│ ├── anim/ # 动画
│ ├── color/ # 颜色
│ ├── drawable/ # 图片
│ ├── layout/ # 布局
│ ├── values/ # 值
│ └── ...
├── smali/ # 主DEX的smali代码
├── smali_classes2/ # 第2个DEX的smali代码
├── smali_classes3/ # 第3个DEX的smali代码
...
├── smali_classes16/ # 第16个DEX的smali代码
└── unknown/ # 未知文件
统计信息:
# 统计smali文件数量
$ find dw_mall_unpacked -name "*.smali" | wc -l
89234
# 统计SO库数量
$ ls dw_mall_unpacked/lib/arm64-v8a/ | wc -l
124
# 统计总文件数
$ find dw_mall_unpacked -type f | wc -l
156789
89234个smali文件,124个SO库——这是一个非常庞大的代码库。
2.3 AndroidManifest.xml分析
2.3.1 清单文件的重要性
AndroidManifest.xml是Android应用的"身份证",包含了应用的所有关键信息:
- 包名和版本
- 权限声明
- 组件声明(Activity、Service、BroadcastReceiver、ContentProvider)
- 应用入口点
2.3.2 关键信息提取
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dreamworld.app"
android:versionCode="8xxxxx"
android:versionName="8.x.x">
<!-- 应用信息 -->
<application
android:name="com.dreamworld.app.MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- 主Activity -->
<activity
android:name="com.dreamworld.app.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 更多Activity... -->
</application>
<!-- 权限声明 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 更多权限... -->
</manifest>
关键发现:
| 信息 | 值 | 说明 |
|---|---|---|
| 包名 | com.dreamworld.app | 梦想科技的包名(已脱敏) |
| 版本号 | 8.x.x | 当前分析的版本 |
| Application类 | MainApplication | 应用入口点 |
| 网络安全配置 | network_security_config | 可能包含SSL Pinning配置 |
2.3.3 网络安全配置分析
network_security_config是Android 7.0引入的网络安全配置机制,让我们看看它的内容:
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- 可能的证书锁定配置 -->
<domain-config>
<domain includeSubdomains="true">api.dreamworld.com</domain>
<domain includeSubdomains="true">api-app.dreamworld.com</domain>
<pin-set expiration="2027-12-31">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>
分析:
cleartextTrafficPermitted="false":禁止明文HTTP流量pin-set:证书锁定配置,但这不是我们抓不到包的主要原因
2.4 寻找安全相关代码
2.4.1 搜索策略
面对89234个smali文件,我们需要有策略地搜索。根据上一章的分析,我们需要找到:
- 代理检测相关代码
- Frida检测相关代码
- 网络请求签名相关代码
搜索关键词:
# 搜索代理相关
grep -r "proxy" smali_classes*/ --include="*.smali" | head -20
grep -r "proxyHost" smali_classes*/ --include="*.smali"
grep -r "ProxySelector" smali_classes*/ --include="*.smali"
# 搜索Frida相关
grep -r "frida" smali_classes*/ --include="*.smali"
grep -r "27042" smali_classes*/ --include="*.smali" # Frida默认端口
# 搜索安全相关
grep -r "security" smali_classes*/ --include="*.smali" | head -20
grep -r "SecurityException" smali_classes*/ --include="*.smali"
# 搜索签名相关
grep -r "sign" smali_classes*/ --include="*.smali" | head -20
grep -r "hmac" smali_classes*/ --include="*.smali"
grep -r "rsa" smali_classes*/ --include="*.smali"
2.4.2 关键文件定位
通过搜索,我找到了几个关键文件:
smali_classes5/com/dreamworld/sdk/network/api/SecurityStub.smali
smali_classes5/com/dreamworld/sdk/network/utils/ApiUtils.smali
smali_classes5/com/dreamworld/sdk/network/interceptor/PackageSignInterceptor.smali
smali_classes3/com/dreamworld/secutil/JNIWrapper.smali
smali_classes2/com/dreamworld/security/SecurityManager.smali
这些文件名透露了重要信息:
SecurityStub:安全存根,可能是安全相关的核心类PackageSignInterceptor:包签名拦截器,用于对请求进行签名JNIWrapper:JNI包装器,调用原生代码SecurityManager:安全管理器,可能包含各种检测逻辑
2.5 smali代码深度分析
2.5.1 什么是smali?
smali是Dalvik虚拟机的汇编语言。当我们反编译APK时,DEX文件中的字节码会被转换为smali代码。
smali vs Java对比:
// Java代码
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
# smali代码
.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello, World!"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
2.5.2 smali基础语法
类型表示:
| Java类型 | smali表示 |
|---|---|
| void | V |
| boolean | Z |
| byte | B |
| short | S |
| char | C |
| int | I |
| long | J |
| float | F |
| double | D |
| Object | Ljava/lang/Object; |
| String | Ljava/lang/String; |
| int[] | [I |
| Object[] | [Ljava/lang/Object; |
常用指令:
| 指令 | 说明 | 示例 |
|---|---|---|
| const | 加载常量 | const/4 v0, 0x1 |
| move | 移动值 | move v0, v1 |
| invoke-virtual | 调用虚方法 | invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String; |
| invoke-static | 调用静态方法 | invoke-static {}, Ljava/lang/System;->currentTimeMillis()J |
| return | 返回 | return-void |
| if-eqz | 条件跳转 | if-eqz v0, :label |
2.5.3 分析JNIWrapper.smali
这是最关键的文件之一,让我们详细分析:
.class public Lcom/dreamworld/secutil/JNIWrapper;
.super Ljava/lang/Object;
.source "JNIWrapper.java"
# 静态初始化块:加载原生库
.method static constructor <clinit>()V
.registers 1
# 加载libSecurityCore.so
const-string v0, "SecurityCore"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
return-void
.end method
# Native方法声明
# 注意:所有方法名都是32位十六进制哈希值
# 方法1:获取设备密钥ID
.method public static native oa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6()Ljava/lang/String;
.end method
# 方法2:RSA签名
.method public static synchronized native ob2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
.end method
# 方法3:HMAC签名
.method public static synchronized native oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
.end method
# 方法4:解密密钥
.method public static synchronized native od4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;
.end method
# 方法5:设置环境
.method public static native oe5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0(Ljava/lang/String;)V
.end method
# 方法6:获取密钥对
.method public static native of6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1()Ljava/lang/String;
.end method
# ... 还有10个类似的方法
关键发现:
-
原生库名称:
SecurityCore- 看起来像是普通的工具库
- 实际上是安全相关的核心库
- 这是一种命名混淆策略
-
方法名混淆:
- 所有方法名都是32位十六进制字符串
- 以
o开头,后跟31位十六进制 - 完全无法从方法名推断功能
-
方法签名分析:
| 混淆方法名 | 参数 | 返回值 | 推测功能 |
|---|---|---|---|
| oa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 | 无 | String | 获取设备ID |
| ob2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7 | String, String | String | RSA签名 |
| oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 | String, String | String | HMAC签名 |
| od4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9 | String×4 | String[] | 解密密钥 |
| oe5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 | String | void | 设置环境 |
2.5.4 分析SecurityStub.smali
这个类是Java层和Native层之间的桥梁:
.class public Lcom/dreamworld/sdk/network/api/SecurityStub;
.super Ljava/lang/Object;
.source "SecurityStub.java"
# 静态字段:JNI包装类的类名
.field private static final clz:Ljava/lang/String; = "com.dreamworld.secutil.JNIWrapper"
# hmacSign方法:对请求进行HMAC签名
.method public static hmacSign(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
.registers 4
.param p0, "hacKey" # 第一个参数:HMAC密钥
.param p1, "stringToSign" # 第二个参数:待签名字符串
# 创建参数数组
const/4 v0, 0x2
new-array v0, v0, [Ljava/lang/Object;
# 设置第一个参数
const/4 v1, 0x0
aput-object p0, v0, v1
# 设置第二个参数
const/4 p0, 0x1
aput-object p1, v0, p0
# 调用反射方法,方法名是混淆后的哈希值
const-string p0, "oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8"
invoke-static {p0, v0}, Lcom/dreamworld/sdk/network/api/SecurityStub;->reflection(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;
# 获取返回值并转换为String
move-result-object p0
check-cast p0, Ljava/lang/String;
return-object p0
.end method
# 反射调用方法
.method private static varargs reflection(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;
.registers 7
.param p0, "methodName" # 方法名(混淆后的哈希值)
.param p1, "args" # 参数数组
const-string v0, ""
:try_start_0
# 通过反射获取JNIWrapper类
const-string v1, "com.dreamworld.secutil.JNIWrapper"
invoke-static {v1}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;
move-result-object v1
# 构建参数类型数组
if-eqz p1, :cond_no_args
array-length v2, p1
new-array v2, v2, [Ljava/lang/Class;
const/4 v3, 0x0
:loop_start
array-length v4, p1
if-ge v3, v4, :loop_end
# 获取每个参数的类型
aget-object v4, p1, v3
if-nez v4, :not_null
aput-object v0, p1, v3
:not_null
aget-object v4, p1, v3
invoke-virtual {v4}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
move-result-object v4
aput-object v4, v2, v3
add-int/lit8 v3, v3, 0x1
goto :loop_start
:cond_no_args
const/4 v2, 0x0
:loop_end
# 获取方法对象
invoke-virtual {v1, p0, v2}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
move-result-object p0
# 设置可访问
const/4 v2, 0x1
invoke-virtual {p0, v2}, Ljava/lang/reflect/AccessibleObject;->setAccessible(Z)V
# 创建实例并调用方法
invoke-virtual {v1}, Ljava/lang/Class;->newInstance()Ljava/lang/Object;
move-result-object v1
invoke-virtual {p0, v1, p1}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
move-result-object p0
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
return-object p0
:catch_0
move-exception p0
invoke-virtual {p0}, Ljava/lang/Throwable;->printStackTrace()V
return-object v0
.end method
代码逻辑分析:
┌─────────────────────────────────────────────────────────────────┐
│ SecurityStub.hmacSign() │
├─────────────────────────────────────────────────────────────────┤
│ 1. 接收参数:hacKey, stringToSign │
│ 2. 将参数打包成Object数组 │
│ 3. 调用reflection()方法 │
│ - 方法名:"oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8" │
│ - 参数:[hacKey, stringToSign] │
│ 4. 返回签名结果 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SecurityStub.reflection() │
├─────────────────────────────────────────────────────────────────┤
│ 1. 通过Class.forName()加载JNIWrapper类 │
│ 2. 构建参数类型数组 │
│ 3. 通过getMethod()获取方法对象 │
│ 4. 设置方法可访问 │
│ 5. 创建JNIWrapper实例 │
│ 6. 通过invoke()调用方法 │
│ 7. 返回结果 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ JNIWrapper.oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 │
├─────────────────────────────────────────────────────────────────┤
│ Native方法,实际实现在libSecurityCore.so中 │
└─────────────────────────────────────────────────────────────────┘
设计分析:
这种设计有几个特点:
- 间接调用:Java代码不直接调用JNI方法,而是通过反射
- 方法名混淆:使用哈希值作为方法名,增加逆向难度
- 解耦设计:便于热修复和动态更新
- 错误处理:异常被捕获并打印,不会导致崩溃
2.6 建立方法映射表
2.6.1 追踪调用关系
通过分析SecurityStub中的所有方法,我建立了完整的方法映射表:
# 搜索所有调用reflection的地方
grep -A5 "reflection" SecurityStub.smali
2.6.2 完整映射表
| Java方法 | 混淆方法名 | 方法签名 | 功能描述 |
|---|---|---|---|
| getPriId() | oa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 | ()Ljava/lang/String; | 获取设备密钥ID |
| rsaSign() | ob2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7 | (String,String)String | RSA签名 |
| hmacSign() | oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 | (String,String)String | HMAC签名 |
| formatDK() | od4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9 | (String,String,String,String)String[] | 解密密钥套件 |
| environment() | oe5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 | (String)V | 设置运行环境 |
| getKeyPair() | of6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1 | ()String | 获取密钥对 |
| rsaVerify() | og7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2 | (String,String,String,String,String)Z | RSA验签 |
| encrypt() | oh8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3 | (String)String | 加密 |
| decrypt() | oi9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4 | (String)String | 解密 |
| aesCtrEncrypt() | oj0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5 | (String,String)String | AES-CTR加密 |
| aesCtrDecrypt() | ok1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6 | (String,String)String | AES-CTR解密 |
| rsaEncrypt() | ol2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7 | (String,String,int)String | RSA加密 |
| rsaDecrypt() | om3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8 | (String,String)String | RSA解密 |
| hmacVerify() | on4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9 | (String,String,String)Z | HMAC验签 |
| aesGcmEncrypt() | oo5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0 | (String,byte[])String | AES-GCM加密 |
| aesGcmDecrypt() | op6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1 | (String,String)byte[] | AES-GCM解密 |
2.6.3 功能分类
┌─────────────────────────────────────────────────────────────────┐
│ JNI方法功能分类 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 密钥管理 │ │ 签名验签 │ │ 加密解密 │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ getPriId() │ │ rsaSign() │ │ encrypt() │ │
│ │ getKeyPair() │ │ rsaVerify() │ │ decrypt() │ │
│ │ formatDK() │ │ hmacSign() │ │ aesCtrEncrypt() │ │
│ │ environment() │ │ hmacVerify() │ │ aesCtrDecrypt() │ │
│ │ │ │ │ │ rsaEncrypt() │ │
│ │ │ │ │ │ rsaDecrypt() │ │
│ │ │ │ │ │ aesGcmEncrypt() │ │
│ │ │ │ │ │ aesGcmDecrypt() │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.7 分析网络请求签名流程
2.7.1 PackageSignInterceptor分析
这是OkHttp的拦截器,用于对请求进行签名:
.class public Lcom/dreamworld/sdk/network/interceptor/PackageSignInterceptor;
.super Ljava/lang/Object;
.source "PackageSignInterceptor.kt"
# 实现OkHttp的Interceptor接口
.implements Lokhttp3/Interceptor;
.method public intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response;
.registers 20
.param p1, "chain"
# 获取原始请求
invoke-interface {p1}, Lokhttp3/Interceptor$Chain;->request()Lokhttp3/Request;
move-result-object v0
# 获取请求URL
invoke-virtual {v0}, Lokhttp3/Request;->url()Lokhttp3/HttpUrl;
move-result-object v1
invoke-virtual {v1}, Lokhttp3/HttpUrl;->toString()Ljava/lang/String;
move-result-object v1
# 获取请求方法
invoke-virtual {v0}, Lokhttp3/Request;->method()Ljava/lang/String;
move-result-object v2
# 获取请求体
invoke-virtual {v0}, Lokhttp3/Request;->body()Lokhttp3/RequestBody;
move-result-object v3
# ... 构建签名字符串 ...
# 调用hmacSign进行签名
invoke-static {v15, v3}, Lcom/dreamworld/sdk/network/api/SecurityStub;->hmacSign(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v4
# 将签名添加到请求头
invoke-virtual {v0}, Lokhttp3/Request;->newBuilder()Lokhttp3/Request$Builder;
move-result-object v5
const-string v6, "X-DW-Sign"
invoke-virtual {v5, v6, v4}, Lokhttp3/Request$Builder;->addHeader(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;
# ... 添加其他头部 ...
# 构建新请求并继续
invoke-virtual {v5}, Lokhttp3/Request$Builder;->build()Lokhttp3/Request;
move-result-object v0
invoke-interface {p1, v0}, Lokhttp3/Interceptor$Chain;->proceed(Lokhttp3/Request;)Lokhttp3/Response;
move-result-object v0
return-object v0
.end method
2.7.2 签名流程图
┌─────────────────────────────────────────────────────────────────┐
│ 请求签名流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ 原始请求 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PackageSignInterceptor.intercept() │ │
│ │ │ │
│ │ 1. 提取请求信息 │ │
│ │ - URL │ │
│ │ - Method (GET/POST) │ │
│ │ - Body │ │
│ │ - Headers │ │
│ │ │ │
│ │ 2. 构建签名字符串 │ │
│ │ stringToSign = env + "\n" │ │
│ │ + version + "\n" │ │
│ │ + keyId + "\n" │ │
│ │ + deviceId + "\n" │ │
│ │ + method + "\n" │ │
│ │ + accept + "\n" │ │
│ │ + contentLanguage + "\n" │ │
│ │ + contentMd5 + "\n" │ │
│ │ + contentType + "\n" │ │
│ │ + timestamp + "\n" │ │
│ │ + nonce + "\n" │ │
│ │ │ │
│ │ 3. 调用HMAC签名 │ │
│ │ sign = SecurityStub.hmacSign(hacKey, stringToSign) │ │
│ │ │ │
│ │ 4. 添加签名头部 │ │
│ │ X-DW-Sign: {sign} │ │
│ │ X-DW-Timestamp: {timestamp} │ │
│ │ X-DW-Nonce: {nonce} │ │
│ │ X-DW-Key: {keyId} │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 签名后请求 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 服务器 │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.8 分析原生库
2.8.1 SO库列表
$ ls -la dw_mall_unpacked/lib/arm64-v8a/ | head -30
total 52480
drwxr-xr-x 126 user staff 4032 Dec 18 01:00 .
drwxr-xr-x 3 user staff 96 Dec 18 01:00 ..
-rw-r--r-- 1 user staff 668432 Dec 15 10:30 libSecurityCore.so # 核心安全库
-rw-r--r-- 1 user staff 234567 Dec 15 10:30 libc++_shared.so # C++标准库
-rw-r--r-- 1 user staff 456789 Dec 15 10:30 libsodium.so # 加密库
-rw-r--r-- 1 user staff 345678 Dec 15 10:30 libsqlcipher.so # 加密数据库
-rw-r--r-- 1 user staff 123456 Dec 15 10:30 libmmkv.so # 高性能KV存储
...
2.8.2 libSecurityCore.so分析
这是核心的安全库,让我们用file命令查看基本信息:
$ file libSecurityCore.so
libSecurityCore.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, stripped
关键信息:
- 64位ARM架构(aarch64)
- 动态链接库
- stripped:符号表已被移除,增加逆向难度
使用readelf查看更多信息:
$ readelf -d libSecurityCore.so | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc++_shared.so]
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
依赖库:
- libc++_shared.so:C++标准库
- liblog.so:Android日志库
- libm.so:数学库
- libc.so:C标准库
- libdl.so:动态链接库
2.8.3 导出函数分析
$ nm -D libSecurityCore.so | grep -i jni
0000000000012345 T JNI_OnLoad
0000000000012678 T Java_com_dreamworld_secutil_JNIWrapper_oa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
0000000000012abc T Java_com_dreamworld_secutil_JNIWrapper_ob2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
...
发现:
- 存在
JNI_OnLoad函数,说明使用了动态注册 - 导出的函数名与smali中的方法名一致
2.9 本章小结
2.9.1 关键发现
通过静态分析,我们获得了以下关键信息:
-
代码规模
- 16个DEX文件,89234个smali文件
- 124个SO库,核心库为libSecurityCore.so
-
安全架构
- Java层通过反射调用JNI方法
- 方法名使用32位哈希值混淆
- 核心逻辑在Native层实现
-
签名机制
- 使用HMAC对请求进行签名
- 签名字符串包含11个字段
- 签名结果放在X-DW-Sign头部
-
方法映射
- 建立了16个JNI方法的完整映射表
- 识别了密钥管理、签名验签、加密解密三类功能
2.9.2 下一步计划
静态分析让我们了解了APP的代码结构,但要真正调用这些方法,我们还需要:
- 动态调试:尝试绕过Frida检测
- 模拟执行:使用Unidbg加载SO库
- 协议分析:理解完整的通信协议
本章思考题
-
为什么APP选择使用反射来调用JNI方法,而不是直接调用?
-
方法名使用哈希值混淆有什么优缺点?如何进一步加强混淆?
-
如果你是安全工程师,你会如何设计更安全的JNI调用机制?
章节附录
A. 本章涉及的工具
| 工具 | 用途 | 官网 |
|---|---|---|
| APKTool | APK反编译 | ibotpeaches.github.io/Apktool/ |
| jadx | DEX反编译为Java | github.com/skylot/jadx |
| readelf | ELF文件分析 | GNU Binutils |
| nm | 符号表查看 | GNU Binutils |
B. smali语法速查
| 指令 | 说明 |
|---|---|
| .class | 类声明 |
| .super | 父类 |
| .field | 字段声明 |
| .method | 方法声明 |
| .registers | 寄存器数量 |
| const-string | 字符串常量 |
| invoke-static | 调用静态方法 |
| invoke-virtual | 调用虚方法 |
| move-result-object | 获取对象返回值 |
C. 参考资料
- smali语法文档:github.com/JesusFreke/…
- Android JNI指南:developer.android.com/training/ar…
- ELF文件格式:en.wikipedia.org/wiki/Execut…
本章完