apk自动化加固1

加固技术

第一代加固技术——混淆技术;

第二代加固技术——加壳技术;

第三代加固技术——指令抽离;

第四代加固技术——指令转换,VMP;

接下来逐步实现一个自动化的加壳服务。
鉴于软件工程的重要性因此大致按着软工的步骤来,鉴于个人开发步骤有所删减

需求工程

首先需要一个修改apk的方案:

  1. 使得apk改动幅度尽量小,这样便于自动化实现以及通用性
  2. 保护覆盖面大,小部分的加密代码可以加密全部的重要资源/代码
  3. 扩展性强,易于后续维护开发

功能动态补充中:

  1. 壳dex替换保护
  2. 自动化dex替换

项目设计

第一点&第二点

因为apk在安装过程中会注册到PMS中,启动时大量的配置也是依据AndroidManifest.xml设置的,因此AndroidManifest.xml文件不能大量更改,lib、Assets、resource等需要时才加载因此可以提前加固。
classes.dex是加密核心,尽量全部隐藏。
因此采用替换app启动时最先加载的Application类来实现加固基础,以下称为壳application与源application

第三点

为了便于调试与增加功能,针对完全替换的新dex做开发,AS下可以方便进行调试与编码,因为先建立的classloader再建立的application,因此可以编写so进行加固。
开发在AS下进行,导出apk。从中提取出dex以及so就是需要植入的解密逻辑了。注意导出的dex中仅有固定名的壳application与系统库,编译时会自动加上无关的东西需要手动smali删除。
之后实现自动化加固工具,注意:

  1. 修改AndroidManifest.xml里application标签中的android:name为自己的壳application全名,其它地方不要改动。
  2. 将源application的名字存到某地方,协定到壳application中获取作为加载依据
  3. 加固工具应当加密源dex,将源dex提取到某处,协定到壳application中解密加载
  4. 替换全部dex为壳dex,只暴露壳代码
  5. 壳代码逐步native化,采取不落地加载。
  6. 源代码逐步native化,采取不落地加载。
  7. 前期壳代码变动较为频繁,因此先稳定下一个简单的自动加壳工具,不进行加密,只是替换dex与打包,方便测试开发。注意dex先放到应用自己的目录下,要不然总忘记加sd卡的权限。
  8. 基本的壳native完成之后,开发的重点为源dex的native化,此部分完成代码主要在自动加壳工具中,此时应保障壳代码的稳定

功能实现

功能实现设计在后续文章中

开发流程

暂无,学到啥加啥,不过因为涉及到俩部分比较独立又互有牵制的代码,应该保持一方基本功能稳定的情况下扩展开发。切记先做能提高开发效率的部分。

项目管理

SourceTree+github
备份频率一周一次

地址:https://github.com/imbaya2466/apksec

java壳dex的初步实现

本次先实现壳dex的通用demo
将目标dex置于sd下,修改AndroidManifest.xml中application name与sd权限,其它部分不变
删除全部java代码,新建自己的壳application代码,相当于替换dex,可以借用AS强大的java与so代码调试方便开发。
实际导出壳dex时切记反编译下清理其中无用类,若不清除其按表单自动添加的类会覆盖源dex的实现,之前启动成功但c++与按钮全失效的原因就是默认添加的activity实现覆盖了源的。

源码分析

首先务必熟悉这部分:http://xn--74q78i15hxv3arigm4e.cn/2018/10/21/android8-0-0%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-app%E5%90%AF%E5%8A%A83/

可知如下启动过程:
ActivityThread是应用的开始,向AMS注册之后进入循环接收返回的应用信息用于启动应用。启动的大部分过程都在handleBindApplication之中。

接下来分析需要的部分:
handleBindApplication之中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
//应该为false null
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
//会执行
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
// For process that contains content providers, we want to
// ensure that the JIT is enabled "at some point".
mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
}
}
// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
.....
try {
//里面直接调用app.onCreate()
mInstrumentation.callApplicationOnCreate(app);
}
.....
}

ActivityThread
data.info.makeApplication之中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
...//针对系统app的操作
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
}
...//catch
mActivityThread.mAllApplications.add(app);
mApplication = app;
...//instrumentation为null,这里不会执行
// Rewrite the R 'constants' for all library apks.
SparseArray<String> packageIdentifiers = getAssets().getAssignedPackageIdentifiers();
final int N = packageIdentifiers.size();
for (int i = 0; i < N; i++) {
final int id = packageIdentifiers.keyAt(i);
if (id == 0x01 || id == 0x7f) {
continue;
}
rewriteRValues(getClassLoader(), packageIdentifiers.valueAt(i), id);
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return app;
}

LoadedApk
newApplication:

1
2
3
4
5
6
7
8
9
10
11
12
13
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return newApplication(cl.loadClass(className), context);
}
static public Application newApplication(Class<?> clazz, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = (Application)clazz.newInstance();
app.attach(context);
return app;
}

Application
attach:

1
2
3
4
final void attach(Context context) {
attachBaseContext(context);
mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}

ContextWrapper
attachBaseContext:

1
2
3
4
5
6
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}

执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
makeApplication调用
检测mApplication存在
取mApplicationInfo.className作为目标名
获取ClassLoader
创建ContextImpl
newApplication
用cl创建Application对象
attach
attachBaseContext*
设置mLoadedApk
setOuterContext
设置mAllApplications
设置mApplication
资源操作
mInitialApplication赋值
初始化Providers
调用app.onCreate*

欺骗世界

接下来要做的就是欺骗了,欺骗系统使系统认为这是本来的应用,欺骗应用使应用以为这是它第一次进系统。有俩个插入点:attachBaseContext与onCreate其它的调用attach是final的。
针对这俩处最先运行的可控制代码,有俩个可行的方案:

方案一

attachBaseContext中

  1. 在壳attachBaseContext之中创建新的classloader,因为之前ClassLoader创建时便赋予了LoadedApk中的mClassLoader,因此此处可以获取到作为父。父会加载原lib,因此自己的dexpath与lib可以随意定
  2. 替换classloader,临时变量cl仅在加载application中使用了一下,因此替换了LoadedApk中的mClassLoader就保证了加载器的正常。
  3. 替换ApplicationName,将拥有ApplicationName的对象内application名字全部替换,保证之后加载正确

onCreate中:

  1. 删除mApplication,为了第二次调用make正常
  2. 删除mAllApplications,抹掉存在,这俩步其实为了恢复make为执行前状态
  3. 执行makeApplication此时用的cl会自动返回之前替换好的,加载目标的名字也是之前替换好的,ContextImpl是新的,之后正常过程
  4. 注意,此时make中的过程已经全部替换了,但是make到oncreate之间的步骤还是旧的,因此替换mInitialApplication为新的
  5. 恢复Providers的操作
  6. 调用原onCreate

方案二

attachBaseContext中

  1. 替换cl
  2. 替换ApplicationName
  3. 执行makeApplication

onCreate中
将attachBaseContext之后的操作全部替换为新的

编码

对比分析方案1的代码实现较少,意味着bug可能更少,因此实现方案一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package FFF.imbaya.protect;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import dalvik.system.DexClassLoader;
public class MainApplication extends Application {
private static String ApplicationName="com.example.imbaya.protectapk1.MyApplication";
private static String path="/sdcard/ls/classes.dex";
private Object currentActivityThread;
private Object boundApplication;//是AMS传入的AppBindData
private Object loadedapk;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//获取ActivityThread对象
currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});
boundApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mBoundApplication");
loadedapk= RefInvoke.getFieldOjbect(
"android.app.ActivityThread$AppBindData",boundApplication , "info");
Object applicationInfo_LoadedApk= RefInvoke.getFieldOjbect(
"android.app.LoadedApk",loadedapk, "mApplicationInfo");
Object applicationInfo_AppBindData= RefInvoke.getFieldOjbect(
"android.app.ActivityThread$AppBindData",boundApplication, "appInfo");
//获取当前包的名字,并更改为目标包的名字,有俩处:LoadedApk与AppBindData的ApplicationInfo
String nowname=(String) RefInvoke.getFieldOjbect(
"android.content.pm.ApplicationInfo", applicationInfo_LoadedApk, "className");
RefInvoke.setFieldOjbect(
"android.content.pm.ApplicationInfo", "className",applicationInfo_LoadedApk, ApplicationName);
RefInvoke.setFieldOjbect(
"android.content.pm.ApplicationInfo", "className",applicationInfo_AppBindData, ApplicationName);
//获取nativelib的路径
String nownativeLibraryDir=(String) RefInvoke.getFieldOjbect(
"android.content.pm.ApplicationInfo", applicationInfo_LoadedApk, "nativeLibraryDir");
//新的classloader
DexClassLoader dLoader = new DexClassLoader(path, null,
nownativeLibraryDir, (ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk",loadedapk, "mClassLoader"));
//设置成新的classloader
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
loadedapk, dLoader);
}
//因为前面的attachBaseContext执行时app等待返回,因此无法更改方法内动作,
@Override
public void onCreate() {
super.onCreate();
//LoadedApk.mApplication删除
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
loadedapk, null);
//mActivityThread.mAllApplications.add(app);删除
Object oldApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mInitialApplication");
ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
.getFieldOjbect("android.app.ActivityThread",
currentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);
//执行makeApplication,此时的ApplicationInfo与classloader是修改过的
Application app = (Application) RefInvoke.invokeMethod(
"android.app.LoadedApk", "makeApplication", loadedapk,
new Class[] { boolean.class, Instrumentation.class },
new Object[] { false, null });
//恢复makeApplication到oncreate之间的操作
RefInvoke.setFieldOjbect("android.app.ActivityThread",
"mInitialApplication", currentActivityThread, app);
//泛型的概念是在编译时替换内部,因此其class对象为不加泛型的本类
RefInvoke.invokeMethod(
"android.app.ActivityThread", "installContentProviders", currentActivityThread,
new Class[] { Context.class, List.class},
new Object[] { app, RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData",
boundApplication, "providers") });
app.onCreate();
}
}

欺骗完成,接下来实现自动化加固

自动化加固

因为易于与java服务器结合且跨平台开发便于个人部署,因此采用java开发。
因为assets资源不会自动解压出来,lib只解压需要的,因此决定开发阶段先暂时实现为:壳dex替换,源dex放于assets中,只改变application name,原名直接修改smali重新打包签名。
因此壳代码一开始就是smali态的,步骤为

  1. apktool壳,导出smali与so,置于自动化加固要求位置
  2. 自动化接受源apk,将源dex提出
  3. 解包源apk,修改application name,替换全部smali为壳smali,修改smali中的application名,导入so,导入assets源dex
  4. 打包apk,签名。
  5. 临时实现:壳中从assets中提出dex到data/data/自己/ls下,并作为目标dex路径

注意点:必须为smali替换,再打包,不能直接用压缩工具替换dex。原因未知

使用目录结构:

1
2
3
4
5
6
7
8
9
10
11
work/
main.jar
tool/
apktool/
housing/
smali/
lib/
work/
out/
mubiao.apk
unsign.apk

代码详见github
注意术业有专攻,文件格式解析用c、服务用java、目录等操作就用脚本