当前位置: 首页 > article >正文

Android一代整体壳简易实现和踩坑记录

Android一代整体壳简易实现和踩坑记录

    • 参考资料
    • 整体思路
    • 源程序
    • 壳程序
    • 加壳代码
    • 尾声

参考资料

[1] dex-shell
[2] Activity 的启动流程(Android 13)
[3] 基于 Android 13 的 Activity 启动流程分析
[4] FART:ART环境下基于主动调用的自动化脱壳方案
[5] Android漏洞之战(11)——整体加壳原理和脱壳技巧详解
[6] Android脱壳之整体脱壳原理与实践
[7] Android DEX加壳
[8] dex壳简单分析与实现过程
[9] Android第一代加壳的验证和测试

整体思路

1、在壳程序dex末尾追加源程序所有dex
2、在壳程序Application的attachBaseContext方法释放源程序所有dex并替换mClassLoader
3、在壳程序Application的onCreate方法注册源程序Application并开始生命周期

源程序

AndroidManifest.xml

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

    <application
        android:name=".SrcApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Shell1src"
        tools:targetApi="31">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

SrcApplication

package com.p1umh0.shell1src;

import android.app.Application;

public class SrcApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
    }

}

MainActivity,注意这里一定是继承Activity而不是AppCompatActivity

package com.p1umh0.shell1src;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

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

}

壳程序

AndroidManifest.xml

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

    <application
        android:name=".ShellApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Shell1shell"
        tools:targetApi="31" >

        <meta-data
            android:name="SRCAPPLICATIONNAME"
            android:value="com.p1umh0.shell1src.SrcApplication"/>

        <activity
            android:name="com.p1umh0.shell1src.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

ShellApplication

package com.p1umh0.shell1shell;

import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import dalvik.system.DexClassLoader;

public class ShellApplication extends Application {

    private final String LOGTAG = "p1umh0";
    private final String CLASS_NAME_ACTIVITYTHREAD = "android.app.ActivityThread";
    private final String CLASS_NAME_LOADEDAPK = "android.app.LoadedApk";

    // 私有路径
    private String privateodexpath; //dex文件路径
    private String privatelibspath; //lib文件路径

    @Override
    protected void attachBaseContext(Context base){
        super.attachBaseContext(base);
        try {
            // 创建私有目录
            File odex = this.getDir("myodex",MODE_PRIVATE);
            File libs = this.getDir("mylibs",MODE_PRIVATE);
            privateodexpath = odex.getAbsolutePath();
            Log.e(LOGTAG,"privateodexpath => " + privateodexpath);
            privatelibspath = libs.getAbsolutePath();
            Log.e(LOGTAG,"privatelibspath => " + privatelibspath);
            // odex目录为空,说明是安装后第一次启动
            // 如果odex需要更新,必须卸载掉原程序,以删除/清空私有目录
            if(odex.list().length==0){
                // 提取壳Apk的classes.dex文件数据
                byte[] dexdata = this.extractdexdata();
                // 从dex数据末尾提取源Apk的所有.dex文件数据并另存到privateodexpath目录
                this.parsedexdata(dexdata,privateodexpath);
            }
            // 获取当前ActivityThread实例
            Object currentActivityThread = RefInvoke.invokeStaticMethod(CLASS_NAME_ACTIVITYTHREAD, "currentActivityThread", new Class[]{}, new Object[]{});
            // 获取已加载的所有包
            ArrayMap mPackages = (ArrayMap) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mPackages");
            // 获取当前包名
            String packageName = this.getPackageName();
            // 获取LoadedApk的弱引用
            WeakReference weakReference = (WeakReference) mPackages.get(packageName);
            // 获取LoadedApk属性mClassLoader
            Object mClassLoader = RefInvoke.getField(CLASS_NAME_LOADEDAPK, weakReference.get(), "mClassLoader");
            Log.e(LOGTAG,"mClassLoader => " + mClassLoader);
            Log.e(LOGTAG,"mClassLoader.getParent => " + ((ClassLoader) mClassLoader).getParent());
            // 拼接源Apk的所有.dex文件的绝对路径(privateodexpath+.dex文件名)
            StringBuffer srcdexfilepath = new StringBuffer();
            for(File srcdexfile : odex.listFiles()){
                srcdexfilepath.append(srcdexfile.getAbsolutePath());
                srcdexfilepath.append(File.separator);
            }
            srcdexfilepath.delete(srcdexfilepath.length()-1,srcdexfilepath.length());
            Log.e(LOGTAG,"srcdexfilepath => " + srcdexfilepath.toString());
            // 创建新的DexClassLoader
            DexClassLoader myDexClassLoader = new DexClassLoader(srcdexfilepath.toString(),privateodexpath,privatelibspath,(ClassLoader) mClassLoader);
            Log.e(LOGTAG,"myDexClassLoader => " + myDexClassLoader);
            Log.e(LOGTAG,"myDexClassLoader.getParent => " + myDexClassLoader.getParent());
            // 替换LoadedApk属性mClassLoader为新的DexClassLoader
            // 相当于将新的DexClassLoader加入双亲委派加载链
            RefInvoke.setField(CLASS_NAME_LOADEDAPK,"mClassLoader",weakReference.get(),myDexClassLoader);
            // 尝试加载
            Object srcMainActivity = myDexClassLoader.loadClass("com.p1umh0.shell1src.MainActivity");
            Log.e(LOGTAG,"srcMainActivity => " + srcMainActivity);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        try {
            // 从AndroidManifest的meta-data读取源apk的Application名称
            String srcApplicationName = null;
            ApplicationInfo applicationInfo = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);
            Bundle bundle = applicationInfo.metaData;
            if (bundle != null && bundle.containsKey("SRCAPPLICATIONNAME")) {
                srcApplicationName = bundle.getString("SRCAPPLICATIONNAME");
            } else {
                return;
            }
            // 获取当前ActivityThread实例
            Object currentActivityThread = RefInvoke.invokeStaticMethod(CLASS_NAME_ACTIVITYTHREAD, "currentActivityThread", new Class[]{}, new Object[]{});
            // 获取当前绑定的应用
            Object mBoundApplication = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mBoundApplication");
            Log.e(LOGTAG,"mBoundApplication => " + mBoundApplication);
            // 获取当前LoadedApk
            Object loadedApk = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD + "$AppBindData", mBoundApplication, "info");
            Log.e(LOGTAG,"loadedApk => " + loadedApk);
            // 将当前LoadedApk中的mApplication设置为null
            RefInvoke.setField(CLASS_NAME_LOADEDAPK, "mApplication", loadedApk, null);
            // 获取当前ActivityThread实例中注册的mInitialApplication
            // 也就是壳apk的Application
            Object shellApplication = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mInitialApplication");
            Log.e(LOGTAG,"shellApplication => " + shellApplication);
            // 获取当前ActivityThread实例中所有注册的Application,并将壳apk的Application从中移除
            ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mAllApplications");
            mAllApplications.remove(shellApplication);
            // 从LoadedApk中获取应用信息
            ApplicationInfo appInfoInLoadedApk = (ApplicationInfo) RefInvoke.getField(CLASS_NAME_LOADEDAPK, loadedApk, "mApplicationInfo");
            // 从AppBindData中获取应用信息
            ApplicationInfo appInfoInAppBindData = (ApplicationInfo) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD + "$AppBindData", mBoundApplication, "appInfo");
            // 替换原来的Application
            appInfoInLoadedApk.className = srcApplicationName;
            appInfoInAppBindData.className = srcApplicationName;
            // 注册源apk的Application
            Application srcApplication = (Application) RefInvoke.invokeMethod(CLASS_NAME_LOADEDAPK, "makeApplication", loadedApk, new Class[]{boolean.class, Instrumentation.class}, new Object[]{false, null});
            Log.e(LOGTAG,"srcApplication => " + srcApplication);
            // 替换当前ActivityThread实例中的mInitialApplication
            // 也就是从shellApplication转为srcApplication
            RefInvoke.setField(CLASS_NAME_ACTIVITYTHREAD, "mInitialApplication", currentActivityThread, srcApplication);
            // 获取mProviderMap
            ArrayMap mProviderMap = (ArrayMap) RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD, currentActivityThread, "mProviderMap");
            // 遍历
            for (Object providerClientRecord : mProviderMap.values()) {
                Object mLocalProvider = RefInvoke.getField(CLASS_NAME_ACTIVITYTHREAD + "$ProviderClientRecord", providerClientRecord, "mLocalProvider");
                // 更新上下文mContext
                RefInvoke.setField("android.content.ContentProvider", "mContext", mLocalProvider, srcApplication);
            }
            // 启动新的Application
            srcApplication.onCreate();
        } catch (PackageManager.NameNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    private byte[] extractdexdata(){
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(this.getApplicationInfo().sourceDir)));
            while(true){
                ZipEntry zipEntry = zipInputStream.getNextEntry();
                if(zipEntry==null){
                    zipInputStream.close();
                    break;
                }
                if(zipEntry.getName().equals("classes.dex")){
                    byte[] buffer = new byte[1024];
                    int byteRead;
                    while ( (byteRead = zipInputStream.read(buffer)) != -1) {
                        byteArrayOutputStream.write(buffer, 0, byteRead);
                    }
                }
                zipInputStream.closeEntry();
            }
            zipInputStream.close();
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void parsedexdata(byte[] dexdata, String privateodexpath){
        try {
            // 壳Apk的classes.dex文件数据的总长度
            int dexdatalen = dexdata.length;
            Log.e(LOGTAG,"dexdatalen => " + dexdatalen);
            // 读末尾4字节,获取源Apk的所有.dex文件数据的总长度
            ByteArrayInputStream byteArrayInputStream;
            DataInputStream dataInputStream;
            byte[] srcdexdatalenbytearr = new byte[4];
            System.arraycopy(dexdata,dexdatalen-4,srcdexdatalenbytearr,0,4);
            byteArrayInputStream = new ByteArrayInputStream(srcdexdatalenbytearr);
            dataInputStream = new DataInputStream(byteArrayInputStream);
            int srcdexdatalen = dataInputStream.readInt();
            Log.e(LOGTAG,"srcdexdatalen => " + srcdexdatalen);
            // 拷贝出源Apk的所有.dex文件数据
            byte[] srcdexdata = new byte[srcdexdatalen];
            System.arraycopy(dexdata,dexdatalen-4-srcdexdatalen,srcdexdata,0,srcdexdatalen);
            byteArrayInputStream = new ByteArrayInputStream(srcdexdata);
            dataInputStream = new DataInputStream(byteArrayInputStream);
            // 读开头2字节,获取源Apk包含的.dex文件的数目
            short srcdexfilenum = dataInputStream.readShort();
            Log.e(LOGTAG,"srcdexfilenum => " + srcdexfilenum);
            // 借助srcdexfilenum完成循环,避免异常
            short srcdexfilenamelen;
            int srcdexfiledatalen;
            while(srcdexfilenum!=0){
                // 源Apk的.dex文件名称:2字节长度 + 不定长数据
                srcdexfilenamelen = dataInputStream.readShort();
                Log.e(LOGTAG,"srcdexfilenamelen => " + srcdexfilenamelen);
                byte[] srcdexfilename = new byte[srcdexfilenamelen];
                dataInputStream.read(srcdexfilename);
                // 源Apk的.dex文件数据:4字节长度 + 不定长数据
                srcdexfiledatalen = dataInputStream.readInt();
                Log.e(LOGTAG,"srcdexfiledatalen => " + srcdexfiledatalen);
                byte[] srcdexfiledata = new byte[srcdexfiledatalen];
                dataInputStream.read(srcdexfiledata);
                // 将源Apk的.dex文件另存到privateodexpath目录
                File srcdexfile = new File(privateodexpath + File.separator + new String(srcdexfilename));
                Log.e(LOGTAG,"srcdexfile => " + srcdexfile.getAbsolutePath());
                if(!srcdexfile.exists()){
                    srcdexfile.createNewFile();
                }
                FileOutputStream fileOutputStream = new FileOutputStream(srcdexfile);
                fileOutputStream.write(srcdexfiledata);
                fileOutputStream.close();
                // 数量递减
                srcdexfilenum -= 1;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

MainActivity

package com.p1umh0.shell1shell;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

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

}

加壳代码

dex追加参考[1],但略有不同
资源处理参考[8],直接用源程序的资源覆盖
笔者这里是从壳程序和源程序编译好的Apk包里提取出关键文件,处理后再打包成新的Apk,MT管理器签名安装

import hashlib
import os
import pathlib
import struct
import zlib
from zipfile import ZipFile

# 路径
thisDir = os.path.dirname(__file__)
srcApkPath = os.path.join(thisDir, "shell1src.apk")
shellApkPath = os.path.join(thisDir, "shell1shell.apk")
newShellApkPath = os.path.join(thisDir, "shell1newshell.apk")

# 文件
srcApk = ZipFile(srcApkPath, "r")
shellApk = ZipFile(shellApkPath, "r")
newShellApk = ZipFile(newShellApkPath, "w")

# 从源Apk中提取res文件夹、resources.arsc文件和所有.dex文件
srcApkUnzipTempDir = os.path.join(thisDir, "srcApkUnzipTempDir")
for srcFullName in srcApk.namelist():
    if srcFullName.startswith("res") or srcFullName.endswith(".dex"):
        srcApk.extract(srcFullName, srcApkUnzipTempDir)

# 从壳Apk中提取AndroidManifest.xml文件和classes.dex文件
shellApkUnzipTempDir = os.path.join(thisDir, "shellApkUnzipTempDir")
for shellFullName in shellApk.namelist():
    if shellFullName == "AndroidManifest.xml" or shellFullName == "classes.dex":
        shellApk.extract(shellFullName, shellApkUnzipTempDir)

# 为新壳Apk插入条目:源Apk的res文件夹、源Apk的resources.arsc文件
srcApkUnzipTempDirIns = pathlib.Path(srcApkUnzipTempDir)
for srcResFilePath in srcApkUnzipTempDirIns.rglob(r"*"):
    if os.path.isfile(srcResFilePath) and not srcResFilePath.name.endswith(".dex"):
        newShellApk.write(
            srcResFilePath, srcResFilePath.relative_to(srcApkUnzipTempDirIns))

# 为新壳Apk插入条目:壳Apk的AndroidManifest.xml文件
newShellApk.write(os.path.join(shellApkUnzipTempDir,
                  "AndroidManifest.xml"), "AndroidManifest.xml")

# 拼接壳Apk的classes.dex文件以及源Apk的所有.dex文件
# 拼接结构:
# 壳dex数据
# 源Apk的.dex文件数量(2字节)
# 源dex1名称长度(2字节) + 源dex1名称(不定大小) + 源dex1数据长度(4字节) + 源dex1数据(不定大小)
# 源dexN名称长度(2字节) + 源dexN名称(不定大小) + 源dexN数据长度(4字节) + 源dexN数据(不定大小)
# 除壳dex数据外的数据长度(4字节)

# 新壳Apk的classes.dex文件数据
newShellDexData = b""
# 拼接壳Apk的classes.dex文件数据
with open(os.path.join(shellApkUnzipTempDir, "classes.dex"), "rb") as f:
    newShellDexData += f.read()
# 壳dex数据长度
shellDexDataLen = len(newShellDexData)
# 源Apk的.dex文件数量(2字节占坑)
newShellDexData += b"??"
srcDexFileNum = 0
# 拼接源Apk的所有.dex文件数据
for srcDexFilePath in srcApkUnzipTempDirIns.rglob(r"*"):
    if os.path.isfile(srcDexFilePath) and srcDexFilePath.name.endswith(".dex"):
        srcDexFileNum += 1
        srcDexFileRelaPath = srcDexFilePath.relative_to(
            srcApkUnzipTempDirIns).name.encode()
        # 大端序short,为了适应java中DataInputStream.readShort()
        newShellDexData += struct.pack(">H", len(srcDexFileRelaPath))
        newShellDexData += srcDexFileRelaPath
        with open(srcDexFilePath, "rb") as f:
            srcDexFileData = f.read()
        # 大端序int,为了适应java中DataInputStream.readInt()
        newShellDexData += struct.pack(">I", len(srcDexFileData))
        newShellDexData += srcDexFileData
# 除壳dex数据外的数据长度,大端序int,为了适应java中DataInputStream.readInt()
newShellDexData += struct.pack(">I", len(newShellDexData)-shellDexDataLen)

# bytes转为list,用来item assignment
newShellDexData = list(newShellDexData)

# 设置源Apk的.dex文件数量,大端序short,为了适应java中DataInputStream.readShort()
newShellDexData[shellDexDataLen:shellDexDataLen +
                2] = list(struct.pack(">H", srcDexFileNum))

# 修新壳Apk的classes.dex文件的file_size,小端序int,为了匹配dex文件结构
newFileSize = list(struct.pack("<I", len(newShellDexData)))
newShellDexData[32:32+len(newFileSize)] = newFileSize

# 修新壳Apk的classes.dex文件的signature,没有设置端序,直接添加
newSignature = hashlib.sha1(bytes(newShellDexData[32:])).hexdigest()
newSignature = list(bytes.fromhex(newSignature))
newShellDexData[12:12+len(newSignature)] = newSignature

# 修新壳Apk的classes.dex文件的checksum,小端序int,为了匹配dex文件结构
newChecksum = zlib.adler32(bytes(newShellDexData[12:]))
newChecksum = list(struct.pack("<I", newChecksum))
newShellDexData[8:8+len(newChecksum)] = newChecksum

# 另存到新壳Apk的classes.dex文件
with open(os.path.join(thisDir, "classes.dex"), "wb") as f:
    f.write(bytes(newShellDexData))

# 为新壳Apk插入条目:新壳Apk的classes.dex文件
newShellApk.write(os.path.join(thisDir, "classes.dex"), "classes.dex")

srcApk.close()
shellApk.close()
newShellApk.close()

尾声

关于资源处理,参考资料[8]直接替换资源文件的方法可行,但要求源程序的MainActivity继承Activity
参考资料[9]的loadResources方法通过AssetManager.addAssetPath添加资源路径,按理说可行
但他是在壳程序dex末尾追加了源程序整个Apk,占用更多空间
笔者这里并没有复现出参考资料[9]中的资源处理方法
加壳时保留了壳程序的资源文件(否则程序启动崩溃)
在壳程序dex末尾追加源程序整个Apk(同参考资料[9])
源程序的MainActivity也是继承Activity而非AppCompatActivity(这样一比这种方法没有任何优势)
加壳后的程序可以顺利启动,也正常走到了源程序的MainActivity,但在onCreate中加载的布局却是壳程序的资源
看起来像是AssetManager.addAssetPath没起作用,笔者不太理解,还求大佬讲解~


http://www.kler.cn/news/355941.html

相关文章:

  • Linux Ubuntu dbus CAPI ---- #include<dbus.h>出现“无法打开源文件dbus/xxx.h“的问题
  • 数据结构-5.8.由遍历序列构造二叉树
  • Python进阶--海龟绘图turtle库
  • Dungeon Crawler Grid Controller 地牢移动控制器
  • iOS模拟弱网步骤
  • 法律文书审查专项使用大模型实现
  • 在docker的容器内如何查看Ubuntu系统版本
  • Linux零基础教程学习(黑马)
  • Netty
  • MatrixOne助力江铜集团打造炉前智慧作业AIoT大数据系统
  • 分布式介绍
  • 框架一 Mybatis Spring SpringMVC(东西居多 后边的没怎么处理)
  • 05 django管理系统 - 部门管理 - 修改部门
  • Linux :at crontab简述
  • 【论文解读系列】EdgeNAT: 高效边缘检测的 Transformer
  • 猫鼠游戏: KaijiK病毒入侵溯源分析
  • (一)Java1.8核心包rt.jar——java.lang.annotation
  • Web APIs - 第2天笔记(黑马笔记)
  • MySQL的事务处理Savepoint,commit
  • 管家婆工贸ERP BB081.订单收付款功能