首页 > 其他分享 >安卓整体加壳(一代壳)原理及实践

安卓整体加壳(一代壳)原理及实践

时间:2024-09-15 15:13:46浏览次数:1  
标签:dex 一代 apk 安卓 ActivityThread 加壳 android app 加载

安卓整体加壳(一代壳)原理及实践

目录

写在前面:写这篇文章真是呕心沥血,网上对一代壳的技术分析很多,但是有实践操作的文章少。一代壳虽然原理简单,但是实现细节很多,并且学习一代壳能够学习到很多二三代壳也用得到的原理和技术,写这篇文章反反复复看了很多其他大佬的文章,但难免还是会漏掉一些要点,比如双亲委派模型就一句话带过了,还需要读者们自己去看文章了解。由于整体加壳的方式是两个项目嵌套,想要写一步调试一步是有点麻烦的,写的过程中只能摸着石头过河,写完了再一起去debug,还是挺磨性子的。希望这篇文章能给刚入门脱壳的读者们带来一些启发,文中写的不严谨的地方欢迎指正。

1 一代壳简介

1.1 DEX加密(也称落地加载)

第一代壳将整个 apk 文件压缩加密到壳 dex 文件的后面,在壳 dex 文件上写上解压代码,动态加载执行,由于是加密整个 apk,在大型应用中很耗资源,因此这代壳很早就被放弃了但思路还是不变。其中这种加密还可以具体划分为几个方向,如下:

  • Dex 字符串加密
  • 静态 DEX 文件整体加密解密
  • 资源加密( xml 与 arsc 文件加密及十六进制加密)
  • 对抗反编译(针对反编译工具,如 apktool。利用反编译工具本身存在的缺陷,使得反编译失败,以此实现对反编译工具的抵抗)
  • Ptrace 反调试、TracePid 值校验反调试
  • 自定义 DexClassLoader(主要是针对 dex 文件加固、加壳等情况)
  • 落地加载( dex 可以在 apk 目录下看到)

1.2 相关脱壳方法

  1. 内存 Dump 法
  2. 缓存脱壳法
  3. 文件监视法
  4. Hook 法
  5. 定制系统法
  6. 动态调试法

2 app启动流程

ActivityThread.main()是进入App世界的大门,所以从ActivityThread.main()开始,了解一下app启动过程中的一些细节。如果十分了解app的启动流程,这部分就可以看的快一点。

了解启动流程主要关注调用了Application什么方法,因为一代壳的实现是依赖于app启动时的一些初始化调用来加载或解密dex文件的。

2.1 ActivityThread.java

源码地址:/frameworks/base/core/java/android/app/ActivityThread.java

attach方法如下

这里的调用栈就不深入了,接下来会调用到bindApplication方法↓

这里的HActivityThread的一个内置类

在这个H类中有处理消息的逻辑

最终调用handleBindApplication,进行app实例化。

从实例化开始,就要进行深入了。data.info是一个LoadedApk类。

2.2 LoadedApk.java

源码地址:/frameworks/base/core/java/android/app/LoadedApk.java

app的创建进入到了InstrumentationnewApplication

2.3 Instrumentation.java

源码地址:/frameworks/base/core/java/android/app/Instrumentation.java

newApplication有两种实现模式,这里看参数采用的应当是第一种。

两种方式都是先实例化app,然后调用app.attach。于是接下来看attach做了什么。

2.4 Application.java

源码地址:/frameworks/base/core/java/android/app/Application.java

attach中调用了attachBaseContext

2.5 ActivityThread.java

源码地址:/frameworks/base/core/java/android/app/ActivityThread.java

再回到ActivityThread.javahandleBindApplication中,还会有调用ApplicationOnCreate函数。

至此可知App的启动流程是

3 基本原理

Application启动流程结束之后才会进入MainActivity中的attachBaseContext函数、onCreate函数。

所以壳要在程序正式执行前,也就是上面的流程中进行动态加载和类加载器的修正,这样才能对加密的dex进行释放,而一般的壳往往选择在Application中的attachBaseContextonCreate函数进行。

简单点说,就是把源程序给藏起来,然后在外面包一层用于脱壳的程序,这个脱壳的程序会把源程序给释放出来,并通过反射机制,加载源程序。

4 加壳实践

加壳之前,需要明确分为哪几步。

  1. 生成一个源程序(安卓项目),一般来说是将源程序打包为apk之后藏起来,这样的好处在于源程序的各类资源也都被藏了起来。举一反三既然可以藏整个apk,那么也可以分开藏一些东西。
  2. 写一个加壳工具,这个程序不是一个安卓项目,可以用任意语言(本文使用python)实现功能,就是一个工具。
  3. 脱壳程序,确定了我们如何藏我们的apk文件之后,使用脱壳程序来释放源程序,并加载。

构建完成之后我们app的入口应当在脱壳程序里。

4.1 源程序

简单新建项目,创建一个空Activity

ActivityOnCreate方法中打印一下。

Log.i("demo", "app:"+getApplicationContext());

然后添加一个MyAppliaction类,并重写一下OnCreate,输出一下Log

4.2 加壳工具

将按照下图的结构构建新dex

这里为了理解画了一个流程图

这里没有对apk数据进行处理,如有需要修改process_apk_data即可。

import hashlib
import os.path
import shutil
import sys
import zlib
import zipfile


def process_apk_data(apk_data:bytes):
    """
    用于处理apk数据,比如加密,压缩等,都可以放在这里。
    :param apk_data:
    :return:
    """
    return apk_data

# 使用前需要修改的部分
keystore_path='demo1.keystore'
keystore_password='123456'
src_apk_file_path= '/Users/zhou39512/AndroidStudioProjects/MyApplication/app/build/outputs/apk/debug/app-debug.apk'
shell_apk_file_path= '/Users/zhou39512/AndroidStudioProjects/Unshell/app/build/outputs/apk/debug/app-debug.apk'
buildtools_path='~/Library/Android/sdk/build-tools/34.0.0/'

# 承载apk的文件名
carrier_file_name= 'classes.dex'
# 中间文件夹
intermediate_dir= 'intermediates'
intermediate_apk_name='app-debug.apk'
intermediate_aligned_apk_name='app-debug-aligned.apk'
intermediate_apk_path=os.path.join(intermediate_dir,intermediate_apk_name)
intermediate_carrier_path=os.path.join(intermediate_dir, carrier_file_name)
intermediate_aligned_apk_path=os.path.join(intermediate_dir,intermediate_aligned_apk_name)
if os.path.exists(intermediate_dir):
    shutil.rmtree(intermediate_dir)
os.mkdir(intermediate_dir)

# 解压apk
shell_apk_file=zipfile.ZipFile(shell_apk_file_path)
shell_apk_file.extract(carrier_file_name,intermediate_dir)

# 查找dex
if not os.path.exists(os.path.join(intermediate_dir, carrier_file_name)):
    raise FileNotFoundError(f'{carrier_file_name} not found')

src_dex_file_path= os.path.join(intermediate_dir, carrier_file_name)

#读取
src_apk_file=open(src_apk_file_path, 'rb')
src_dex_file=open(src_dex_file_path, 'rb')

src_apk_data=src_apk_file.read()
src_dex_data=src_dex_file.read()

# 处理apk数据
processed_apk_data=process_apk_data(src_apk_data)
processed_apk_size=len(processed_apk_data)

# 构建新dex数据
new_dex_data=src_dex_data+processed_apk_data+int.to_bytes(processed_apk_size,8,'little')

# 更新文件大小
file_size=len(processed_apk_data)+len(src_dex_data)+8
new_dex_data=new_dex_data[:32]+int.to_bytes(file_size,4,'little')+new_dex_data[36:]

# 更新sha1摘要
signature=hashlib.sha1().digest()
new_dex_data=new_dex_data[:12]+signature+new_dex_data[32:]

# 更新checksum
checksum=zlib.adler32(new_dex_data[12:])
new_dex_data=new_dex_data[:8]+int.to_bytes(checksum,4,'little')+new_dex_data[12:]

# 写入新dex
intermediate_carrier_file= open(intermediate_carrier_path, 'wb')
intermediate_carrier_file.write(new_dex_data)
intermediate_carrier_file.close()
src_apk_file.close()
src_dex_file.close()

# 添加环境变量,为重打包做准备
os.environ.update({'PATH':os.environ.get('PATH')+f':{buildtools_path}'})
# 重打包
r=os.popen(f"cp {shell_apk_file_path} {intermediate_apk_path}").read()
print(r)
os.chdir(intermediate_dir)
r=os.popen(f'zip {intermediate_apk_name} {carrier_file_name}').read()
os.chdir('../')
print(r)
# 对齐
r=os.popen(f'zipalign 4 {intermediate_apk_path} {intermediate_aligned_apk_path}').read()
print(r)
# 签名
r=os.popen(f'apksigner sign -ks {keystore_path} --ks-pass pass:{keystore_password} {intermediate_aligned_apk_path}').read()
print(r)
r=os.popen(f'cp {intermediate_aligned_apk_path} ./app-out.apk').read()
print(r)
print('Success')

这里涉及到重打包的过程,具体可以看安卓打包流程。这里需要用安卓SDK中的zipalignapksigner进行对齐和重打包。

4.3 脱壳程序

接下来编写套在源程序外面的脱壳程序。由于我们最终需要运行的是我们的源程序,所以我们必须在启动流程调用ApplicationOnCreate之前释放出源程序,并替换Application为我们的源程序Application实例(原来是脱壳程序的Application实例)。

我们在基本原理这一节中研究了启动流程,所以在ApplicationOnCreate之前,有一个attachBaseContext方法,我们可以通过重写该方法来实现上面的效果。

4.3.1 代理Application

这里我们要写一个代理Application,作为appApplication实例。并重写一下attachBaseContext

public class ProxyApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        Log.i("demo","attachBaseContext");
        super.attachBaseContext(base);
    }
}

然后要修改AndroidManifest.xml,将Application的实例改为我们自定义的ProxyApplication

之后运行。在Logcat中看到输出了log则说明成功。

4.3.2 读取自身apk

Application中,需要获取到自身的apk文件。

private ZipFile getApkZip() throws IOException {
    Log.i("demo", this.getApplicationInfo().sourceDir);
    ZipFile apkZipFile = new ZipFile(this.getApplicationInfo().sourceDir);
    return apkZipFile;
}

我们先测试一下,打印看看this.getApplicationInfo().sourceDir是什么

发现是一个缓存存储apk的地址,并且就是apk的路径(而非文件夹路径)。

4.3.3 读取dex

private byte[] readDexFileFromApk() throws IOException {
    /* 从本体apk中获取dex文件 */
    ZipFile apkZip = this.getApkZip();
    ZipEntry zipEntry = apkZip.getEntry("classes.dex");
    InputStream inputStream = apkZip.getInputStream(zipEntry);
    byte[] buffer = new byte[1024];
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int length;
    while ((length = inputStream.read(buffer)) > 0) {
        baos.write(buffer, 0, length);
    }
    return baos.toByteArray();
}

4.3.4 提取源apk

private byte[] splitSrcApkFromDex(byte[] dexFileData) {
    /* 从dex文件中分离源apk文件 */
    int length = dexFileData.length;
    ByteBuffer bb = ByteBuffer.wrap(Arrays.copyOfRange(dexFileData, length - 8, length));
    bb.order(java.nio.ByteOrder.LITTLE_ENDIAN); // 设置为小端模式
    long processedSrcApkDataSize = bb.getLong(); // 读取这8个字节作为long类型的值
    byte[] processedSrcApkData=Arrays.copyOfRange(dexFileData, (int) (length - 8 - processedSrcApkDataSize), length - 8);
    byte[] srcApkData=reverseProcessApkData(processedSrcApkData);
    return srcApkData;
}

4.3.5 修正加载器(重点)

这里开始需要了解双亲委派模型,简单而言就是java中的类加载器有父子关系,当某个加载器需要加载某个类的时候,先会交给其父类,如果加载过了就直接返回,如此往上,如果父加载器都加载不了,再抛回来自己加载。

关于加载源apk,这里有两个细节且重要的问题需要思考清楚。从这里开始希望大家放慢阅读速度。

  • 如何加载**dex**文件?
  • 如何让加载之后的**Application**进入后续的加载流程?

这里拿一张非常重要的图

首先解决第一个问题,如何加载**dex**文件?

引用佬的文章介绍一下BaseDexClassLoader类加载器

Android里边的BaseDexClassLoader可以实现在运行的时候加载在编译时未知的dex文件,经过此加载器的加载,ART虚拟机内存中会形成相应的数据结构,对应的dex文件也会由mmap映射到虚拟内存当中,通过此加载器的loadClass(String className, boolean resolve)方法就可以得到类的Class对象,从而可以使用该类。

查看源码可以看到PathClassLoader是继承自BaseDexClassLoader的,而PathClassLoader还有另外两个兄弟: InMemoryDexClassLoader以及DexClassLoader,而壳程序很多都使用了这两个类加载器来加载解密后的dex文件。其中InMemoryDexClassLoader是Android8.0以后新增的类,可以实现所谓的"不落地加载"。

作者:Jerry_Deng
链接:https://juejin.cn/post/6962096676576165918
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结一下,InMemoryDexClassLoaderDexClassLoaderPathClassLoader都继承自BaseDexClassLoader,我们可以用他们来加载dex

第二个问题,如何让加载之后的**Application**进入后续的加载流程?

后续的加载流程指的就是app组件(比如Activity)的加载,而加载组件时,使用的是加载应用程序的ClassLoader

如若不做任何处理,仅仅在**attachBaseContext方法中使用上面讲的某个类加载器对dex加载,后续加载源程序的组件时会出现ClassNotFoundException**的错误,为什么会这样?

这是因为如果仅仅在attachBaseContext方法中使用类加载器加载dex,之后加载组件时使用的ClassLoader和我们使用的加载器不同,并且,加载组件的ClassLoader通过双亲委派模型发现没有人能加载组件类(因为组件类在我们的dex中),导致ClassNotFoundException

还记得BaseDexClassLoader吗,其有一个DexPathList,记录了已加载的dex文件路径。

加载组件时对应的BaseDexClassLoaderDexPathList是没有源程序的dex路径的,如果尝试让BaseDexClassLoader加载不在这个列表中的类,就会报ClassNotFoundException

因此有两种方法可以解决这个问题。

  1. 既然使用的加载器不同,那么改成相同的不就行了。

    通过反射获取到LoadedApk,修改其mClassLoader为我们加载dex文件的ClassLoader实例,这样后续试图加载组件类的时候,就能找到相应的类。

  2. 通过打破原有双亲委派关系,添加我们的ClassLoader进入关系网。

    原先的mClassLoaderPathClassLoader,其在双亲委派关系中的父亲是BootClassLoader,所以只要将我们的ClassLoader添加进他们两个之间即可。也就是将PathClassLoader的父亲设置为我们自己的ClassLoader,再将我们自己的ClassLoader的父亲设置为BootClassLoader。如下图

理解完以上这些,可以开始实践了。


第一种方法,需要思考如何拿到LoadedApk。在启动流程的handleBindApplication中,data.info就是我们要拿到的LoadedApk

向上找到data.info初始化的地方。

跟进方法

关键代码

所以我们要从mPackages里面找LoadedApk

public static void replaceClassLoader1(Context context,DexClassLoader dexClassLoader){
    ClassLoader pathClassLoader = ProxyApplication.class.getClassLoader();
    try {
        // 1.通过currentActivityThread方法获取ActivityThread实例
        Class ActivityThread = pathClassLoader.loadClass("android.app.ActivityThread");
        Method currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread");
        Object activityThreadObj = currentActivityThread.invoke(null);
        // 2.拿到mPackagesObj
        Field mPackagesField = ActivityThread.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj);
        // 3.拿到LoadedApk
        String packageName = context.getPackageName();
        WeakReference wr = (WeakReference) mPackagesObj.get(packageName);
        Object LoadApkObj = wr.get();
        // 4.拿到mClassLoader
        Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");
        Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);
        Object mClassLoader =mClassLoaderField.get(LoadApkObj);
        Log.i("mClassLoader",mClassLoader.toString());
        // 5.将系统组件ClassLoader给替换
        mClassLoaderField.set(LoadApkObj,dexClassLoader);
    }
    catch (Exception e) {
        Log.i("demo", "error:" + Log.getStackTraceString(e));
        e.printStackTrace();
    }
}

4.3.6 加载源apk

我们使用DexClassLoader加载dex,还需要解决几个参数。

  • dexPathdex文件路径
  • optimizedDirectorydex 优化后存放的位置,在 ART 上,会执行 oatdex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置。
  • librarySearchPathnative 依赖的位置
  • parent:双亲委派中的父亲,这里是PathClassLoader

Context.getDir方法是在app的目录下新建app_的文件夹。比如打印base.getDir("opt_dex",0),结果是 /data/user/0/com.xxx.unshell/app_opt_dex

代码如下

@Override
protected void attachBaseContext(Context base) {
    Log.i("demo", "attachBaseContext");
    super.attachBaseContext(base);
    try {
        byte[] dexFileData=this.readDexFileFromApk();
        byte[] srcApkData=this.splitSrcApkFromDex(dexFileData);
        // 创建储存apk的文件夹,写入src.apk
        File apkDir=base.getDir("apk_out",MODE_PRIVATE);
        srcApkPath=apkDir.getAbsolutePath()+"/src.apk";
        File srcApkFile = new File(srcApkPath);
        srcApkFile.setWritable(true);
        FileOutputStream fos=new FileOutputStream(srcApkFile);
        Log.i("demo", String.format("%d",srcApkData.length));
        fos.write(srcApkData);
        fos.close();
        srcApkFile.setReadOnly(); // 受安卓安全策略影响,dex必须为只读
        Log.i("demo","Write src.apk into "+srcApkPath);
        // 新建加载器
        File optDir =base.getDir("opt_dex",MODE_PRIVATE);
        File libDir =base.getDir("lib_dex",MODE_PRIVATE);
        optDirPath =optDir.getAbsolutePath();
        libDirPath =libDir.getAbsolutePath();
        ClassLoader pathClassLoader = ProxyApplication.class.getClassLoader();
        DexClassLoader dexClassLoader=new DexClassLoader(srcApkPath, optDirPath, libDirPath,pathClassLoader);
        Log.i("demo","Successfully initiate DexClassLoader.");
        // 修正加载器
        replaceClassLoader1(base,dexClassLoader);
        Log.i("demo","ClassLoader replaced.");

    } catch (Exception e) {
        Log.i("demo", "error:" + Log.getStackTraceString(e));
        e.printStackTrace();
    }
}

当我们替换掉加载器之后,app加载流程走完,会加载Activity,此时我们为了让系统加载我们源程序的Activity,我们需要修改xml文件,将脱壳程序的Activity入口替换为源程序的入口。

之后我们build apk,然后用加壳程序处理,并安装。

启动程序,查看log,发现了我们在源程序中写的Log,说明启动源程序的Activity成功。

加载成功!

4.3.7 加载源Application

到了这里加壳的核心部分已经结束了,接下来都是补充的部分。

如果源程序也有自定义的Application,我们就需要重新makeApplication,进入到源程序的Application,保证程序的完整生命周期。

  • 注册application(用LoadedApk中的makeApplication方法注册)。

    为了使用makeApplication重新注册application,需要先把mApplication置空

    并且还需要在在ActivityThread下的链表mAllApplications中移除mInitialApplicationmAllApplications存放的是所有的应用,mInitialApplication存放的是初始化的应用(即当前壳应用)。把当前的壳应用,从现有的应用中移除掉,然后在makeApplication方法中会把新构建的加入到里面去。

    之后,替换ActivityThreadmInitialApplication为刚刚makeApplication创建的app

    总结操作流程就是

    • LoadedApkmApplication置空
    • ActivityThreadmAllApplications中移除mInitialApplication
    • makeApplication()
    • 替换ActivityThreadmInitialApplication
    super.onCreate();
    Log.i(TAG,"进入onCreate方法");
    
    // 提取提前配置的ApplicationName,来引导到源程序的Application入口
    String applicationName="";
    ApplicationInfo ai=null;
    try {
        ai=getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        if (ai.metaData!=null){
            applicationName=ai.metaData.getString("ApplicationName");
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    
    // 将当前进程的mApplication设置为null
    Object activityThreadObj=RefinvokeMethod.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Object[]{});
    Object mBoundApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mBoundApplication");
    Object info=RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"info");
    RefinvokeMethod.setField("android.app.LoadedApk","mApplication",info,null);
    
    // 从ActivityThread的mAllApplications中移除mInitialApplication
    Object mInitApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mInitialApplication");
    ArrayList<Application> mAllApplications= (ArrayList<Application>) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mAllApplications");
    mAllApplications.remove(mInitApplication);
    
    // 更新两处className
    ApplicationInfo mApplicationInfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.LoadedApk",info,"mApplicationInfo");
    ApplicationInfo appinfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"appInfo");
    mApplicationInfo.className=applicationName;
    appinfo.className=applicationName;
    
    // 执行makeApplication(false,null)
    Application app= (Application) RefinvokeMethod.invokeMethod("android.app.LoadedApk","makeApplication",info,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null});
    
    // 替换ActivityThread中mInitialApplication
    RefinvokeMethod.setField("android.app.ActivityThread","mInitialApplication",activityThreadObj,app);
    
    // 更新ContentProvider
    ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
    Iterator iterator=mProviderMap.values().iterator();
    while (iterator.hasNext()){
        Object mProviderClientRecord=iterator.next();
        Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
        RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
    }
    
    // 执行新app的onCreate方法
    app.onCreate();
    

    解释一下这里修改className的操作。源程序可能也有自定义的一个Application类,如果有的话我们需要提前配置在xmlmeta-data中提前设置,之后提取出来。

    当然也可以通过解析源程序的xml来实现,感兴趣可以研究一下。

  • 更新ContentProvider

    ContentProviderAndroid系统中的一个组件,用于在不同的应用程序之间共享数据。需要修改mProviderMap中所有ContentProvidermContext为新app

    ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
    Iterator iterator=mProviderMap.values().iterator();
    while (iterator.hasNext()){
        Object mProviderClientRecord=iterator.next();
        Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
        RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
    }
    
  • 执行新app的onCreate方法

    app.onCreate();
    

总体代码如下,这里用到的RefinvokeMethod贴到了文章最下面问题一节中。

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

    loadResources(apkFileName);
    Log.i(TAG,"进入onCreate方法");

    // 提取提前配置的ApplicationName,来引导到源程序的Application入口
    String applicationName="";
    ApplicationInfo ai=null;
    try {
        ai=getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        if (ai.metaData!=null){
            applicationName=ai.metaData.getString("ApplicationName");
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }

    // 将当前进程的mApplication设置为null
    Object activityThreadObj=RefinvokeMethod.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Object[]{});
    Object mBoundApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mBoundApplication");
    Object info=RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"info");
    RefinvokeMethod.setField("android.app.LoadedApk","mApplication",info,null);

    // 从ActivityThread的mAllApplications中移除mInitialApplication
    Object mInitApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mInitialApplication");
    ArrayList<Application> mAllApplications= (ArrayList<Application>) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mAllApplications");
    mAllApplications.remove(mInitApplication);

    // 更新两处className
    ApplicationInfo mApplicationInfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.LoadedApk",info,"mApplicationInfo");
    ApplicationInfo appinfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"appInfo");
    mApplicationInfo.className=applicationName;
    appinfo.className=applicationName;

    // 执行makeApplication(false,null)
    Application app= (Application) RefinvokeMethod.invokeMethod("android.app.LoadedApk","makeApplication",info,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null});

    // 替换ActivityThread中mInitialApplication
    RefinvokeMethod.setField("android.app.ActivityThread","mInitialApplication",activityThreadObj,app);

    // 更新ContentProvider
    ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
    Iterator iterator=mProviderMap.values().iterator();
    while (iterator.hasNext()){
        Object mProviderClientRecord=iterator.next();
        Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
        RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
    }

    // 执行新app的onCreate方法
    app.onCreate();
}

这里通过Log我们可能会看到报错如下

这是不影响的,因为我们等于是又重新启动了一次App,只不过这次的Application设置的是源程序的Application

这个报错的源码位于LoadedApk.java


可以看到我们的源程序自定义的Application还是成功加载了

4.3.8 加载资源

我们通过这样的方式加载源程序,源程序的资源似乎并没有被加载进来,所以这里继续讲如何把源程序的资源加载进来。

当然我们也可以直接把源程序的资源复制到壳程序下面,但加壳的目就是为了保护代码和资源,所以最好还是动态加载。

这里资源加载可能还涉及到Resources类的更换,实现起来还有点麻烦,这里就暂且阁下不表。

5 问题

  • 在过程中出现 Writable dex file '/data/user/0/com.jok.unshell/app_opt_dex/src.apk' is not allowed这样的报错,原因是Android14有一个改动: 更安全的动态代码加载,简单来说就是打开DEXJARAPK等文件时必须将DEX文件设置为只读。
  • RefinvokeMethod.java
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    
    public class RefinvokeMethod {
        public static Object invokeStaticMethod(String class_name,String method_name,Class[] classes,Object[] objects){
            try {
                Class aClass = Class.forName(class_name);
                Method method = aClass.getMethod(method_name, classes);
                return method.invoke(null,objects);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static Object invokeMethod(String class_name,String method_name,Object obj,Class[] classes,Object[] objects){
            try {
                Class aClass = Class.forName(class_name);
                Method method = aClass.getMethod(method_name, classes);
                return method.invoke(obj,objects);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static Object getField(String class_name,Object obj,String field_name){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                return field.get(obj);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static Object getStaticField(String class_name,String field_name){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                return field.get(null);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static void setField(String class_name,String field_name,Object obj,Object value){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                field.set(obj,value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void setStaticField(String class_name,String field_name,Object value){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                field.set(null,value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
  • 如果没在xml里面指定源程序的Activity,那就需要在壳程序的attachBaseContext中添加如下代码运行MainActivity
    try {
        Object objectMain = dexClassLoader.loadClass("com.example.sourceapk.MainActivity");
        Log.i(TAG,"MainActivity类加载完毕");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    

6 引用

安卓逆向-脱壳学习记录 - Is Yang's Blog

Android漏洞之战——整体加壳原理和脱壳技巧详解

Android中的Apk的加固(加壳)原理解析和实现 - roccheung - 博客园

android一代壳脱壳方法总结 - 怎么可以吃突突 - 博客园

Android脱壳之整体脱壳原理与实践这里所说的壳都是指dex加壳,不涉及到so的加壳。 涉及到的代码分析基于AOSP - 掘金

【Android 脱壳】DEX壳简单实现过程分析_mboundapplication-CSDN博客

标签:dex,一代,apk,安卓,ActivityThread,加壳,android,app,加载
From: https://www.cnblogs.com/Joooook/p/18415261

相关文章

  • 安卓应用启动流程
    安卓应用启动流程目录1冷启动热启动2zygote和SystemServer3应用启动流程简述(记得补充)4从点击图标到通知Zygote4.1Launcher4.2Activity.java4.3Instrumentation.java4.4ActivityTaskManager.java4.5ActivityTaskManagerService.java4.6ActivityStarter.java......
  • 安卓签名校验机制
    安卓签名校验机制目录1V1方案1.1V1方案的安全性2V2方案2.1摘要计算过程2.2防回滚绕过3V3方案4V4方案5签名实践5.1keytool生成密钥库5.2jarsigner5.3apksigner6引用安卓的签名校验机制共有三代。9.0以上的系统会判断apk是否使用到V3版本的签......
  • 基于微信小程序/安卓APP的饮食健康服务系统设计与实现
    ......
  • 基于微信小程序/安卓APP的计算机课程学习系统设计与实现
    ......
  • 安卓系统启动流程解析
    安卓系统启动流程目录1init阶段1.1FirstStage1.2SELinuxSetup1.3SecondStage2init.rc的配置3Zygote的启动3.1app_process3.2Zygoteinit.java4SystemServer5总结6引用光看分析文章还是不够的,还是要和实践结合。1init阶段init命令的入口是init......
  • 安卓架构
    安卓架构目录1Linux内核层2硬件抽象层HAL3NativeC/C++库&&AndroidRuntime4JavaFramework层5SystemApps层1Linux内核层Android平台的基础是Linux内核。例如,ART依靠Linux内核来执行底层功能。Linux内核的安全机制为Android提供了相应的保障,也......
  • 【Ehviewer绿色版】1.9.8.4最新版本下载2024安卓苹果
     Ehviewer是一款主要用于浏览和下载漫画、插画等二次元图像内容的软件。适用安卓和苹果系统,Ehviewer拥有海量的漫画作品,涵盖各种题材和风格,包括日本漫画、韩国漫画、欧美漫画以及国内的一些同人创作等。无论是热门的商业漫画还是小众的独立作品,都能在Ehviewer上找到,现在已经更......
  • uniapp - 最新详细实现web-view网页与安卓苹果App端之间互相通信功能,苹果app/安卓app
    前言在uni-app项目开发中,详解实现web-view和App之间的互相通信完整流程及代码教程,Uniappapp端向webview网站传递数据,同时webview又可以向app端传递数据参数,完成二者的数据通信方案,支持嵌入本地移动端H5页面、第三方网站、自定义网页,附带各种常见问题,解决发送数据通信没......
  • 安卓玩机工具-----多设备同时投屏操控的安卓手机设备投屏工具 工作室推荐
    多设备QtScrcpy投屏工具        对于安卓设备较多的机型。在电脑端实时操作必备工具。他可以同时投屏连接到当前电脑端的安卓设备,而且可以同时操作。对于工作室或者多安卓设备玩家推荐使用。工具特点           QtScrcpy是一款在Scrc......
  • 新一代IO模块:智驭未来,故障无忧的高效之选
    在工业自动化与智能控制的浪潮中,我们的IO模块以其卓越的性能与稳定性,成为连接现实与数字世界的桥梁。但面对复杂多变的运行环境,如何快速、准确地诊断并解决IO模块的故障,是每位工程师关注的重点。产品优势智能控制与精确性:明达技术的IO模块作为设备的神经中枢,能够实时采集传感器数据......