第一章:静态分析的艺术

46 阅读16分钟

第一章:静态分析的艺术

本章字数:约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文件,我们需要有策略地搜索。根据上一章的分析,我们需要找到:

  1. 代理检测相关代码
  2. Frida检测相关代码
  3. 网络请求签名相关代码

搜索关键词

# 搜索代理相关
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表示
voidV
booleanZ
byteB
shortS
charC
intI
longJ
floatF
doubleD
ObjectLjava/lang/Object;
StringLjava/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个类似的方法

关键发现

  1. 原生库名称SecurityCore

    • 看起来像是普通的工具库
    • 实际上是安全相关的核心库
    • 这是一种命名混淆策略
  2. 方法名混淆

    • 所有方法名都是32位十六进制字符串
    • o开头,后跟31位十六进制
    • 完全无法从方法名推断功能
  3. 方法签名分析

混淆方法名参数返回值推测功能
oa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6String获取设备ID
ob2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7String, StringStringRSA签名
oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8String, StringStringHMAC签名
od4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9String×4String[]解密密钥
oe5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0Stringvoid设置环境

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中                      │
└─────────────────────────────────────────────────────────────────┘

设计分析

这种设计有几个特点:

  1. 间接调用:Java代码不直接调用JNI方法,而是通过反射
  2. 方法名混淆:使用哈希值作为方法名,增加逆向难度
  3. 解耦设计:便于热修复和动态更新
  4. 错误处理:异常被捕获并打印,不会导致崩溃

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)StringRSA签名
hmacSign()oc3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8(String,String)StringHMAC签名
formatDK()od4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9(String,String,String,String)String[]解密密钥套件
environment()oe5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0(String)V设置运行环境
getKeyPair()of6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1()String获取密钥对
rsaVerify()og7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2(String,String,String,String,String)ZRSA验签
encrypt()oh8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3(String)String加密
decrypt()oi9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4(String)String解密
aesCtrEncrypt()oj0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5(String,String)StringAES-CTR加密
aesCtrDecrypt()ok1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6(String,String)StringAES-CTR解密
rsaEncrypt()ol2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7(String,String,int)StringRSA加密
rsaDecrypt()om3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8(String,String)StringRSA解密
hmacVerify()on4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9(String,String,String)ZHMAC验签
aesGcmEncrypt()oo5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0(String,byte[])StringAES-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 关键发现

通过静态分析,我们获得了以下关键信息:

  1. 代码规模

    • 16个DEX文件,89234个smali文件
    • 124个SO库,核心库为libSecurityCore.so
  2. 安全架构

    • Java层通过反射调用JNI方法
    • 方法名使用32位哈希值混淆
    • 核心逻辑在Native层实现
  3. 签名机制

    • 使用HMAC对请求进行签名
    • 签名字符串包含11个字段
    • 签名结果放在X-DW-Sign头部
  4. 方法映射

    • 建立了16个JNI方法的完整映射表
    • 识别了密钥管理、签名验签、加密解密三类功能

2.9.2 下一步计划

静态分析让我们了解了APP的代码结构,但要真正调用这些方法,我们还需要:

  1. 动态调试:尝试绕过Frida检测
  2. 模拟执行:使用Unidbg加载SO库
  3. 协议分析:理解完整的通信协议

本章思考题

  1. 为什么APP选择使用反射来调用JNI方法,而不是直接调用?

  2. 方法名使用哈希值混淆有什么优缺点?如何进一步加强混淆?

  3. 如果你是安全工程师,你会如何设计更安全的JNI调用机制?

章节附录

A. 本章涉及的工具

工具用途官网
APKToolAPK反编译ibotpeaches.github.io/Apktool/
jadxDEX反编译为Javagithub.com/skylot/jadx
readelfELF文件分析GNU Binutils
nm符号表查看GNU Binutils

B. smali语法速查

指令说明
.class类声明
.super父类
.field字段声明
.method方法声明
.registers寄存器数量
const-string字符串常量
invoke-static调用静态方法
invoke-virtual调用虚方法
move-result-object获取对象返回值

C. 参考资料

  1. smali语法文档:github.com/JesusFreke/…
  2. Android JNI指南:developer.android.com/training/ar…
  3. ELF文件格式:en.wikipedia.org/wiki/Execut…

本章完