Android一代壳详解
一代壳指的是Dex整体替换,二代壳是函数抽取,下篇文章再研究二代壳
ClassLoader
类的生命周期如下
类的数据是存储在class/Dex文件中的,Java虚拟机要想使用这些类,就得先用类加载器把类从文件中加载出来,初始化成内存中的元数据,当然这里类的class来源不限于文件,只要是满足class/Dex文件定义的数据流即可(动态生成)
每个类加载器ClassLoader是有自己的空间的,两个ClassLoader如果不存在父子关系,那么其加载的class相互不可见
每个ClassLoader只能有一个父类,Java虚拟机,包括Dalvik和Art采用的都是双亲委托机制:概括起来就是自底向上检查类是否已经加载,然后自顶向下尝试加载类。
Android里面的类加载器叫DexClassLoader
资源问题
一代壳的整体加壳方案有两种
- 壳Apk动态加载源Apk,完成Dex的替换,这种方式动态加载源Dex之后,就会存在一个资源问题,因为此时源Dex用的是壳Apk的资源,需要进行资源替换,这种方案基本不用修改源apk什么东西,直接套上去就行
- 用壳Dex去替换源Dex,资源仍用源Apk,这样就可以避免资源替换的问题
不管用哪种方案,这里都介绍一下Android的资源问题
Android资源文件分为两类,第一类是res目录下的可编译资源文件,编译时,系统会自动在R.java中生成资源文件的十六进制值,
例如我在strings.xml中定义了一个字符串
1 | <resources> |
当我们想使用这种类型的资源时
1 | protected void onCreate(Bundle savedInstanceState) { |
可以用Resources去getResources,为什么setText可以直接用资源id当参数,是因为其有这个重载函数
1 | public final void setText(int resid) { |
一代壳加壳原理
App启动流程
参考这篇文章
[原创]FART:ART环境下基于主动调用的自动化脱壳方案-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com
ActivityThread.main是进入APP世界的大门,ActivityThread是单例模式,静态函数currentActivityThread用于获取当前单例
1 | 1941 public static ActivityThread currentActivityThread() { |
Activity.main
1 | 5379 public static void main(String[] args) { |
这个函数主要是启动主消息循环,并创建Activity实例,Looper.loop()开始进入消息循环,等待接收来自系统的消息,当收到系统发送来的bindapplication的进程间调用时,用handlebindapplication来处理请求
1 | private void handleBindApplication(AppBindData data) { |
在 handleBindApplication函数中第一次进入了app的代码世界,该函数功能是启动一个application,并把系统收集的apk组件等相关信息绑定到application里,在创建完application对象后,接着调用了application的attachBaseContext方法,之后调用了application的onCreate函数。由此可以发现,app的Application类中的attachBaseContext和onCreate这两个函数是最先获取执行权进行代码执行的。这也是为什么各家的加固工具的主要逻辑都是通过替换app入口Application,并自实现这两个函数,在这两个函数中进行代码的脱壳以及执行权交付的原因。
LoadedApk 中的这个mBaseClassLoader是负责加载四大组建的ClassLoader
1 | 90 public final class LoadedApk { |
从上面追踪LoadedApk的创建过程
1 | 2093 public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, |
1 | 2110 private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, |
mResourcePackage是ActivityThread的成员,这里是根据PackageName获取LoadedApk
1 | 279 final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages = new ArrayMap<>(); |
new LoadedApk,这里有个问题,传入的BaseLoader始终为null
1 | 141 public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo, |
继续跟踪创建mApplication的地方
1 | 941 public Application makeApplication(boolean forceDefaultAppClass, |
newApplication,这里可以看到用传进来的ClassLoader 进行了loadClass
1 | 1083 public Application newApplication(ClassLoader cl, String className, Context context) |
再追踪一下classLoader的获取,这里是获取classLoader的地方,其会进一步调用createOrUpdateClassLoaderLocked 去创建classloader,这里会处理classloader的路径,就不继续跟了,反正到时候我们直接用这个classLoader肯定没错
1 | 706 public ClassLoader getClassLoader() { |
总结来说,Application的attachBaseContext和OnCreate是最先获得执行时机的地方
方案一
仔细想想方案一其实有很多缺点,在Application解密出源Apk之后,还要手动承担各种组件的启动过程,所以从逻辑上来讲远不如方案二,方案二只需要加一个外壳Application即可,所以这里只实现方案二。
方案二
首先编写一个测试APP,其中加点资源,为了验证正确性
程序有两个Activity,没有指定Application
MainActivity用一个Button启动SecondeActivity
1 | btn_startSecond = (Button) findViewById(R.id.button); |
在SecondActivity中引用了一个Strings资源
1 | public class SecondActivity extends AppCompatActivity { |
编写外壳APK
getDex,获取源Dex,源dex放在assets中,这里是用ZipInputStream 创建this.getApplicationInfo().sourceDir的输入流(apk文件就是zip文件),
1 | private byte[] getDex() throws IOException { |
读入源dex的字节流之后,得写出来
然后创建Classloader加载释放的源dex,并替换DexClassloader,这里要替换的classloader是前面介绍过的,
1 | Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", |
这里要用到一系列的反射操作,所以这里用了一个封装的反射类,这里列出invokeStaticMethod
1 | public class RefInvoke { |
手动模拟加壳
用前文介绍的重打包工具,先unpack testAPP,发现新版AS编译出来的APP会自动分包,实际上分包也没区别,我们只需要把所有的dex都替换成壳的分dex就行,但是这里为了简单就把multiDex关了
在build.gradle里面加上 multiDexEnabled false
1 | plugins { |
Android重打包 | zzrR0 (x1aor0.github.io)
因为我们不需要反编译dex,所以要修改下脚本,加个-s参数
1 | def unpack_apk(self): |
解包之后,把dex拷贝到assets里面,重命名为source.dex,再把壳的dex拷贝进来,然后修改AndroidManifest.xml
添加一个name,标注Application的类,这里apktool重打包可能会报错,需要删除这个属性
android:dataExtractionRules=”@xml/data_extraction_rules”
发现执行都正常,但是还是报错,查看log发现新的Classloader找不到,原来是nativeLibrararyDirectoris里面没加当前App的lib目录
所以在new PathClassLoader要加上lib目录,要得到lib目录可以从原先的classLoader中获取,这里根据官方文档,路径需要用:隔开
1 | private String generateLibPath(){ |
classloder重写如下
1 | PathClassLoader dexClassLoader = new PathClassLoader(dexFileName,generateLibPath(),getClassLoader()); |
加上lib之后,又出现个新错误
这个问题是由于我们的activity继承自AppCompatActivity,继承这个就会出现这个找不到资源的问题,查了很多,发现修改成继承自Activity就能正常找到资源(暂时不了解其原因),因此修改testApp的代码
1 | public class MainActivity extends Activity { |
OK,至此就完成了对testApp的手动加壳了,最终在我的Pixel2的Android7上测试成功
这里在谈谈资源问题,Android是通过资源id和资源的对应关系来加载资源的,我们壳方案下,资源我们没动,用的就是源Apk的资源,所以只要能保证id是对的,就能获取正确的资源,资源id定义在R类中,我们外壳Application是没load这个class的,所以当用到这个类的时候就会走双亲委托机制,这里你可能会有个疑问,按双亲委托机制,我们当前的Classloader也就是外壳的classloader加载的外壳Dex中也有R类,这样加载的R类不就是外壳Dex的吗,然后就会导致id错乱。
仔细想想,其实不是的,R类虽然都叫R类,但是其包名是不同的,
外壳是com.zzrr.zzrshell_out.R
源App是com.zzrr.testshell.R,所以这里是能从源dex中正确加载com.zzrr.testshell.R的。
保护
这里对源dex只是简单放置在assets中,实际加保护的话方案就很多了,就不实现了。
总结
测试App和外壳程序github地址