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 16:13
终于看到脱壳类的科普了,买了市面上的逆向书,都是在教逆向,关于脱壳都浅尝辄止或者讳莫如深,实战当中第 ...
脱壳的话,要说学习渠道还真就是这些论坛和博客,目前也就这些,需要自己多看Android源码和分析。 终于看到脱壳类的科普了,买了市面上的逆向书,都是在教逆向,关于脱壳都浅尝辄止或者讳莫如深,实战当中第一步却都是脱壳,小白想问问老哥脱壳有哪些系统的学习渠道?思维导图或者知识储备都行,有点迷茫,想试试每次遇到壳就不知所措,也不知道大家都是怎么跨过这道坎的。 我是第一,大晚上的看到这个帖子真的很开心。 讲的很好了,支持楼主 oranges 发表于 2019-6-18 22:41
我是第一,大晚上的看到这个帖子真的很开心。
多谢!!{:301_976:} 学习就是要有教材和实例啊