一夜梦惊人 发表于 2019-6-18 22:21

Android加固和脱壳原理浅论

一、前言由于种种原因,笔者写的很简略,但是不该没有的地方一点都没有省略,同时请大家注意,本文注重的讲解原理,而不是工具之类,所有如何修复啥的我都不会说,只会给出参考链接和资料。如果有错误请指出,私信或者回帖我将会修正。大概也就这样子了把,还真是应了标题,“浅论”。
二、正文
(1)介绍
环境与工具:Android-8.1.0_r1源码、Jadx、IDA 7.0。两个apk文件,其加固方式分别为娜迦、A加固。
(2)加固技术解析
关于加固技术而言,我这里引用于看雪JimmyJLUN的文章(一张表格看懂:市面上最为常见的 Android 安装包(APK)五代加固技术发展历程及优缺点比较!:https://bbs.pediy.com/thread-226864.htm)来进行划分。

我只会讲解前三代的加固技术。

第一代加固技术:提取DEX文件、so库和资源文件,然后在本地进行加密保存。在APK运行时会动态还原,并且还有具有一定的反调试代码来阻止对软件进行动态调试。(注:这种APK我是没有找到,也懒得寻找了并且这种脱壳软件太多)
第二代加固技术:在第一代的基础增加了运行时解密,运行完加密的特点。以娜迦为例:
public class lvmApplication extends Application {    private static int targetSdkVersion = -1;
    private Application userApplication;

    public void attachBaseContext(Context context) {
      super.attachBaseContext(context);
      targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
      try {
            manHelper.install(context);
            manHelper.ClearSharedPrefernce(context);
            ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(getPackageName(), 128);
            createApplication(appInfo.metaData != null ? (String) appInfo.metaData.get("_app_name") : null);
            if (this.userApplication != null) {
                Method attach = ContextWrapper.class.getDeclaredMethod("attachBaseContext", new Class[]{Context.class});
                attach.setAccessible(true);
                attach.invoke(this.userApplication, new Object[]{context});
            }
      } catch (Exception e) {
            e.printStackTrace();
      }
    }

    public void onCreate() {
      patcher.patchApplication(this, this, this.userApplication, null);
      super.onCreate();
      if (this.userApplication != null) {
            insideSdk.initInnerSdk(getBaseContext(), this.userApplication);
            this.userApplication.onCreate();
      }
    }

    private void createApplication(String name) {
      if (name != null) {
            try {
                this.userApplication = (Application) Class.forName(name).getConstructor(new Class).newInstance(new Object);
                return;
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }
      }
      this.userApplication = new Application();
    }
}
首先自定义一个lvmApplication,随后在attachBaseContext进行初始化各种东西,并且调用createApplication方法,该方法反射APK原有的Application并实例化。随后在onCreate中调用patch.patchApplication方法来把DEX还原。
public class patcher {
    public static void patchApplication(Context context, Application bootApplication, Application userApplication, String externalResourceFile) {
      Class<?> activityThread;
      Class<?> loadedApkClass;
      try {
            activityThread = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = RefManHelper.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class, new Object);
            Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            Application initialApplication = (Application) RefManHelper.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication");
            if (userApplication != null && initialApplication == bootApplication) {
                mInitialApplication.set(currentActivityThread, userApplication);
            }
            if (userApplication != null) {
                Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List<Application> allApplications = (List) mAllApplications.get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                  if (allApplications.get(i) == bootApplication) {
                        allApplications.set(i, userApplication);
                  }
                }
            }
            loadedApkClass = Class.forName("android.app.LoadedApk");
      } catch (ClassNotFoundException e) {
            loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
      } catch (Throwable e2) {
            IllegalStateException illegalStateException = new IllegalStateException(e2);
      }
      Field mApplication = loadedApkClass.getDeclaredField("mApplication");
      mApplication.setAccessible(true);
      Field mResDir = loadedApkClass.getDeclaredField("mResDir");
      mResDir.setAccessible(true);
      Field mLoadedApk = null;
      try {
            mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
      } catch (NoSuchFieldException e3) {
            e3.printStackTrace();
      }
      for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
            Field field = activityThread.getDeclaredField(fieldName);
            field.setAccessible(true);
            for (Entry<String, WeakReference<?>> entry : ((Map) field.get(currentActivityThread)).entrySet()) {
                Object loadedApk = ((WeakReference) entry.getValue()).get();
                if (loadedApk != null && mApplication.get(loadedApk) == bootApplication) {
                  if (userApplication != null) {
                        mApplication.set(loadedApk, userApplication);
                  }
                  if (externalResourceFile != null) {
                        mResDir.set(loadedApk, externalResourceFile);
                  }
                  if (!(userApplication == null || mLoadedApk == null)) {
                        mLoadedApk.set(userApplication, loadedApk);
                  }
                }
            }
      }
    }
}
反射ActivityThread,随后调用其currentActivityThread方法获取scurrentActivityThread(注:ActivityThread采用单例模式,而这个currentActivityThread则就是ActivityThrea的实例)。在第一个if处判断原APK有没有自己的Application,如果有且ActivityThread原来的mInitialApplication是lvmApplication的话,那么就把lvmApplication换成原APK的Application,在第二处if则是置换了ActivityThread的mAllApplication,这个是一个Application的集合,为的就是多进程。在jiangwei书中则还处理了mBoundApplication,这是一个AppBindData,其构造如下:
    static final class AppBindData {
      LoadedApk info;
      String processName;
      ApplicationInfo appInfo;
      List<ProviderInfo> providers;
      ComponentName instrumentationName;
      Bundle instrumentationArgs;
      IInstrumentationWatcher instrumentationWatcher;
      IUiAutomationConnection instrumentationUiAutomationConnection;
      int debugMode;
      boolean enableBinderTracking;
      boolean trackAllocation;
      boolean restrictedBackupMode;
      boolean persistent;
      Configuration config;
      CompatibilityInfo compatInfo;
      String buildSerial;

      /** Initial values for {@link Profiler}. */
      ProfilerInfo initProfilerInfo;

      public String toString() {
            return "AppBindData{appInfo=" + appInfo + "}";
      }
    }
其中需要注意的就是ApplicationInfo和LoadedApk这两项了,但是呢在这个APK中并没有修改,所以我也就不讲解,如有需要可以自行查找资料。随后代码中还反射LoadedApk,并设置mApplication等等。万变不离其宗,各家方法虽然不同,但是都跳不开ActivityThread和LoadedApk。
第三代加固技术:对DEX中的的类方法进行指令抽取,随后在运行时Hook系统函数进行还原,其指令抽取程度不一定。首先的了解DEX的格式,而ODEX虽然说是elf格式,但是却是Google自家创造出来为了满足Android的elf格式,里面却还有着DEX格式的存在。我把另一个APK的解密so拖到IDA中看一下,但是呢却发现没有so加密了。

而且还没有JNI_OnLoad函数,所以我初步判定时在init_array中。

果不其然我发现一个很特殊的函数。经过我的查看其加密原理大概是jiangwei的文章(Android逆向之旅---基于对so中的section加密技术加固:https://blog.csdn.net/jiangwei0910410003/article/details/49962173)。怎么搞定网上有很多且不是本文重点,我就不讲解了。但是呢我这里讲下另一种投机取巧的方式:那就是查看so中所有的string。

最终我找到了DefineClass这个方法(注:dvmResolveClass函数的文章,jiangwei的dexFindClass函数:http://www.wjdiankong.cn/archives/1115)。函数各有不同,但是都可以,接下来我讲讲原理。
我们加载class分别是findclass和loadclass,本贴讲的是findclass,其是实现在jni_internal.c中。
static jclass FindClass(JNIEnv* env, const char* name) {
    CHECK_NON_NULL_ARGUMENT(name);
    Runtime* runtime = Runtime::Current();
    ClassLinker* class_linker = runtime->GetClassLinker();
    std::string descriptor(NormalizeJniClassDescriptor(name));
    ScopedObjectAccess soa(env);
    mirror::Class* c = nullptr;
    if (runtime->IsStarted()) {
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader(soa)));
      c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
    } else {
      c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
    }
    return soa.AddLocalReference<jclass>(c);
}
首先判断className是否为空,然后根据class获取类的签名descriptor,判断runtime是否运行分别调用class_linker的findclass、findsystemclass方法,这里我们是用的findclass方法。而class_linkerfindclass类过长,这里我挑一种来讲。是class没有加载,不是array class和loader class是nullptr的。
// Class is not yet loaded.
if (descriptor != '[' && class_loader == nullptr) {
    // Non-array class and the boot class loader, search the boot class path.
    ClassPathEntry pair = FindInClassPath(descriptor, hash, boot_class_path_);
    if (pair.second != nullptr) {
      return DefineClass(self,
                         descriptor,
                         hash,
                         ScopedNullHandle<mirror::ClassLoader>(),
                         *pair.first,
                         *pair.second);
    } else {
      // The boot class loader is searched ahead of the application class loader, failures are
      // expected and will be wrapped in a ClassNotFoundException. Use the pre-allocated error to
      // trigger the chaining with a proper stack trace.
      ObjPtr<mirror::Throwable> pre_allocated =
          Runtime::Current()->GetPreAllocatedNoClassDefFoundError();
      self->SetException(pre_allocated);
      return nullptr;
    }
}
首先找到class的位置,然后调用DefindClass,寻找了许久终于找到这个函数了。看下函数的定义。
mirror::Class* ClassLinker::DefineClass(Thread* self,
                                        const char* descriptor,
                                        size_t hash,
                                        Handle<mirror::ClassLoader> class_loader,
                                        const DexFile& dex_file,
                                        const DexFile::ClassDef& dex_class_def);
我们可以通过descriptor来过滤,然后修改dex_file来达到类方法指令还原的目的。可以参考jiangwei的dexFindClass。

(3)脱壳技术解析
笔者已经猝死,正在复活中......{:301_1008:}


FUPK3(https://github.com/F8LEFT/FUPK3)
FUPK3的作者f8left在GitHub上说明原理了,我这里copy下。
有句话是说代码就是最好的文档,有兴趣的自行去查看代码吧。这里简单说一下.

遍历 gDvm 中的dvmUserDexFiles结构,获取所有cookie(已加载的dex)
对内存中的dex文件,遍历触发函数,并通过在解析器处插桩,截取解密后的code_item,获取后直接返回不执行该函数。
对截取出来的数据进行重组,生成dex文件。
利用修改过的smali/baksmali对dump下来的dex文件进行修复
Android修改过的源码额外记录为patch了,自行查看吧。
更详细的内容看Other.md文件其中最重要就是UserDexFile结构,我觉得一步步分析还是过于麻烦,直接上一张我找到的图。(https://www.jianshu.com/p/ab9c3984d995)

DexHunter这个很好的一个东西,只要进行修改还是能脱掉不少的,原本我是准备写的,但是最后还是删减了,因为如果真的写这个,足以写一篇了。


三、后序
我于6.18写完,但是真的感觉很虎头蛇尾。很多只是给出了那个系统函数,至于为什么是这个和怎么修复都没有讲到。但是呢,我也不准备搞了,这里就统一说下把。
这些脱壳系统函数的内容或者形参全部都是和DEX文件格式有关的结构啥的,有人说这不是废话,不是和有这些有关怎么拿到?那么,既然你知道有关系,那么你能不能费点心力去找出来呢,毕竟我觉得这是一个很简单的事,如果你连这个都觉得要有人给你弄好,那么你还是用Android脱壳工具的算了,当然,你如果是真的想学习,那么不妨看看我给的链接和实践,毕竟,我的chrome有着50多个书签。

https://blog.csdn.net/QQ1084283172/article/details/53557894(DexExtractor的原理分析和使用说明)
https://bbs.pediy.com/thread-225798.htm(某vmp壳原理分析笔记:注意是过反调试)
http://www.inforsec.org/wp/?p=581(DexHunter作者发表论文)
https://www.52pojie.cn/thread-405255-1-1.html(APK加固之类抽取分析与修复)

一夜梦惊人 发表于 2019-6-20 20:26

豌豆上的主公 发表于 2019-6-20 16:13
终于看到脱壳类的科普了,买了市面上的逆向书,都是在教逆向,关于脱壳都浅尝辄止或者讳莫如深,实战当中第 ...

脱壳的话,要说学习渠道还真就是这些论坛和博客,目前也就这些,需要自己多看Android源码和分析。

豌豆上的主公 发表于 2019-6-20 16:13

终于看到脱壳类的科普了,买了市面上的逆向书,都是在教逆向,关于脱壳都浅尝辄止或者讳莫如深,实战当中第一步却都是脱壳,小白想问问老哥脱壳有哪些系统的学习渠道?思维导图或者知识储备都行,有点迷茫,想试试每次遇到壳就不知所措,也不知道大家都是怎么跨过这道坎的。

oranges 发表于 2019-6-18 22:41

我是第一,大晚上的看到这个帖子真的很开心。

nj001 发表于 2019-6-18 22:44

讲的很好了,支持楼主

一夜梦惊人 发表于 2019-6-18 22:45

oranges 发表于 2019-6-18 22:41
我是第一,大晚上的看到这个帖子真的很开心。

多谢!!{:301_976:}

biutefo 发表于 2019-6-18 22:49

学习就是要有教材和实例啊

weimeigame 发表于 2019-6-18 23:14

++

讲的不错 支持楼主

wanmei195634 发表于 2019-6-18 23:39

虽然不懂,我居然看完了……

xiexie 发表于 2019-6-18 23:43

这个真是难得的好帖子

生如上善若水 发表于 2019-6-18 23:53

学习了,谢谢分享

jinfang 发表于 2019-6-19 00:25

好帖,收藏了,一下没消化
页: [1] 2 3 4
查看完整版本: Android加固和脱壳原理浅论