首页
社区
课程
招聘
[原创]Android第一代加壳的验证和测试
2023-5-16 18:12 10658

[原创]Android第一代加壳的验证和测试

2023-5-16 18:12
10658

Android第一代加壳方法,网上有很多文章,本文只是在前人基础上的测试和验证,重点在于动手实践。主要的成果验证了有效性以及将加壳程序工具化,现在只需要一个cmd命令就可以对指定的Apk文件加壳。

第一代加壳技术有三个项目,分别是:

  1. 加壳程序。主要是把需要加壳的原程序加密后,放在壳程序中,一般是追加到壳程序的classes.dex文件的末尾,然后对壳程序的classes.dex文件中的长度、crc校验和sha1校验字段重新计算。
  2. 壳程序。运行后,将加壳后的原程序从本程序的的classes.dex末尾释放出来,然后在Application类中,设置壳中原程序的运行环境,并加载执行原程序。
  3. 原程序。

(一)项目下载地址
点击下载项目
该项目中包含3个工程,其中,apkUnshell是壳程序,flashplayer程序是源程序,apkshell是加壳程序,因为笔者在android studio中创建和编译java程序屡次失败,apkshell是用eclipse开发的。

(二)简要解析
此程序的加壳和解壳主要代码来自于网上,作者是yuxin,本文只是简单修剪。

加壳程序:如上所述,功能是将源程序追加到壳程序classes.dex文件的末尾,然后对壳程序的classes.dex文件中的长度、crc校验和sha1校验字段重新计算。该模块有很多细节,比如文件长度的计算,crc校验的是那些字节,sha1的计算方式等,具体可以结合dex文件结构和源代码,详细分析之。

如下几篇详细的dex文件解析文章:
dex文件解析
dex文件解析
dex文件解析

源程序加解密是简单的异或操作,密钥是:
图片描述
其主要的计算字段有classes.dex长度,sha1校验,crc校验,主要部分如下:

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
/**
  * 修改dex头 sha1值
  * @param dexBytes
  * @throws NoSuchAlgorithmException
  */ 
 private static void fixSHA1Header(byte[] dexBytes) throws NoSuchAlgorithmException { 
     MessageDigest md = MessageDigest.getInstance("SHA-1"); 
     md.update(dexBytes, 32, dexBytes.length - 32);
     //32为到结束计算sha-1 
     byte[] newdt = md.digest(); 
     System.arraycopy(newdt, 0, dexBytes, 12, 20);
     //修改sha-1值(12-31)
      
     //输出sha-1值,可有可无 
     String hexstr = ""; 
     for (int i = 0; i < newdt.length; i++) { 
         //Integer.toString(int i, int radix)将整数i(十进制)转化为radix进制的整数
         hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16).substring(1);
     
     System.out.println("new dex sha-1:" + hexstr); 
 
 
 /**
  * 修改dex头 file_size值
  * @param dexBytes
  */ 
 private static void fixFileSizeHeader(byte[] dexBytes) {
     //新文件长度
     byte[] newfs = intToByte(dexBytes.length); 
      
     byte[] refs = new byte[4];
     //高位在前 低位在前掉个个
     for (int i = 0; i < 4; i++) {
         refs[i] = newfs[newfs.length - 1 - i];
     }
      
     //修改(32-35)
     System.arraycopy(refs, 0, dexBytes, 32, 4);
      
     System.out.println("new dex file size:" + Integer.toHexString(dexBytes.length));
 
  
 /**
  * 修改dex头,CheckSum 校验码
  * @param dexBytes
  */ 
 private static void fixCheckSumHeader(byte[] dexBytes) { 
     Adler32 adler = new Adler32(); 
     adler.update(dexBytes, 12, dexBytes.length - 12);
     //12到文件末尾计算校验码 
     int value = (int)adler.getValue(); 
     byte[] newcs = intToByte(value); 
     //高位在前,低位在前掉个个 
     byte[] recs = new byte[4]; 
     for (int i = 0; i < 4; i++) { 
         recs[i] = newcs[newcs.length - 1 - i]; 
     
      
     //效验码赋值(8-11)
     System.arraycopy(recs, 0, dexBytes, 8, 4);
      
     System.out.println("new dex checksum:" +Integer.toHexString(value));
 

解壳程序:此模块是加壳技术的核心,其技术细节较为冷僻高深,所谓曲高和寡,这部分才是加壳技术的精华所在,奈何本人不学无术,才疏学浅,只能勉强看懂,但是知其然不知其所以然。我觉得,作者肯定花费了很多的心血,鄙人对原作者的创新行为充满敬意,深表钦佩

解壳程序通过反射调用实现的,在RefInvoke类中封装了多个反射调用的方法。该功能主要有2个部分,一部分是在Application方法中,其功能简述如下:

1.释放出源程序apk。

2.获取当前android.app.ActivityThread类对象currentActivityThread,并在其中找到ArrayMap类型的对象成员mPackages,并在mPackages中找到当前包名的android.app.LoadedApk对象。

3.在上述android.app.ActivityThread实例中,找到ClassLoader类的实例成员mClassLoader。

4 通过DexClassLoader加载释放出来的源程序apk。

5 将上述mClassLoader替换为加载源程序的DexClassLoader。

第二部分功能在onCreate方法中,其主要工作如下:

1.加载资源。

2.获取壳程序AndroidManifest.xml中源程序的包名和Application。

3.执行android.app.ActivityThread类的currentActivityThread方法,获取当前的android.app.ActivityThread实例currentActivityThread。

4.获取currentActivityThread中的android.app.ActivityThread类实例成员mBoundApplication。

5.在上述mBoundApplication中获取android.app.ActivityThread$AppBindData类实例info。

6.将上述info实例中的android.app.LoadedApk类实例成员mApplication设置为null。

7.在currentActivityThread中获取android.app.ActivityThread类实例mInitialApplication。

8.在currentActivityThread中获取android.app.ActivityThread类对象mAllApplications,该对象是个ArrayList<Application>结构,并在次结构中删除mInitialApplication对象。

9.获取info对象中android.app.LoadedApk类成员对象mApplicationInfo。

10.在mBoundApplication对象中获取android.app.ActivityThread$AppBindData类成员对象appInfo。

11.appInfo和mApplicationInfo实例的className成员变量设置为源程序的包名。

12.执行android.app.LoadedApk类对象info中的成员方法makeApplicationinfo,创建一个Application类实例。

13.设置android.app.ActivityThread类实例currentActivityThread中的成员对象mInitialApplication的值为上一步创建的Application类实例。

14.获取android.app.ActivityThread类对象currentActivityThread中的成员对象mProviderMap,该对象是一个ArrayMap结构,其元素是android.app.ActivityThread$ProviderClientRecord类。

15.遍历该成员对象,并获取其成员对象mLocalProvider,该成员对象是android.content.ContentProvider类,设置该android.content.ContentProvider类的成员对象mContext值为上述创建的Application。

16.调用上述步骤所创建的Application类实例中的onCreate方法。

上述复杂功能,概括的讲就是,将壳程序执行环境和资源转换到源程序的执行环境。这部分我也有些细节搞不明白,只是照抄代码和一些简单枯燥的字面解释,所谓,知其一不知其二,一叶障目不见泰山,这部分懂就是懂,咱不懂也装不出来。

其主要代码如下:

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
package com.apkUnshell;
 
 
 
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
 
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import dalvik.system.DexClassLoader;
 
/**
 * =============================================================================
 * Copyright (c) 2017 yuxin All rights reserved.
 * Packname com.jju.yuxin.reforceapk
 * Created by yuxin.
 * Created time 2017/6/18 0018 下午 5:03.
 * Version   1.0;
 * Describe :
 * History:
 * ==============================================================================
 */
 
//com.google.android.apps.plus
//com.adobe.flashplayer
//com.loader
//com.setup.loader
//Applicaiton做为整个应用的上下文,会被系统第一时间调用,这也是应用开发者程序代码的第一执行点
public class MyApplication extends Application{
 
    private static String DEXFILENAME = "update.apk";
 
    private static final String appkey = "APPLICATION_CLASS_NAME";
 
    private static String cryptKey = "fuck all the android crackers";
 
    public static String PAYLOAD_ODEX = "my_payload_odex";
 
    public static String PAYLOAD_LIB = "my_payload_lib";
 
    private  static final String TAG = MyApplication.class.getSimpleName();
 
    private String srcDexFilePath = "";
    private String odexPath = "";
    private String libPath = "";
 
    private static String gIPstr = "";
    private static String gUserNameStr = "";
 
    private Context context = null;
 
    //以下是加载资源
    protected AssetManager mAssetManager = null;
    protected Resources mResources = null;
    protected Resources.Theme mTheme = null;
 
 
    //why run 2 times?
    @SuppressWarnings("rawtypes")
    @Override
    protected void attachBaseContext(Context base) {
 
        super.attachBaseContext(base);
 
        //getApplicationContext() 返回应用的上下文,生命周期是整个应用,应用摧毁它才摧毁
        //Activity.this的context 返回当前activity的上下文,属于activity ,activity 摧毁他就摧毁
        //getBaseContext()  返回由构造函数指定或setBaseContext()设置的上下文
        //this.getApplicationContext()取的是这个应 用程序的Context,Activity.this取的是这个Activity的Context,
        //这两者的生命周期是不同 的,前者的生命周期是整个应用,后者的生命周期只是它所在的Activity。
        context = base;
 
        Log.e(TAG,"attachBaseContext");
 
        try {
//          /data/user/0/com.apkunshell/app_payload_odex
            File odexPathFile = this.getDir(PAYLOAD_ODEX, MODE_PRIVATE);
//          /data/user/0/com.apkunshell/app_payload_libs
            File libsPathFile = this.getDir(PAYLOAD_LIB, MODE_PRIVATE);
 
            //用于存放源apk释放出来的dex
            odexPath = odexPathFile.getAbsolutePath();
            //用于存放源Apk用到的so文件
            libPath = libsPathFile.getAbsolutePath();
            //用于存放解密后的apk
            srcDexFilePath = odexPathFile.getAbsolutePath() + "/" + DEXFILENAME;
 
//            String apppath = this.getFilesDir().getParent() + "/";
//            InputStream is = this.getAssets().open(APKFILENAME);
//            int size = is.available();
//            byte []buffer = new byte[size];
//            is.read(buffer);
//            is.close();
//            OutputStream os = new FileOutputStream(apppath + APKFILENAME);
//            os.write(buffer);
//            os.close();
 
            File srcDexFile = new File(srcDexFilePath);
            //第一次加载
            if (srcDexFile.exists() == false)
            {
                Log.e(TAG, "beFirstLoading");
 
                srcDexFile.createNewFile();
                //拿到dex文件
                byte[] dexdata = this.readDexFileFromApk();
                //取出源APK解密后放置在/payload.apk,及其so文件放置在payload_lib/
                this.splitPayLoadFromDex(dexdata);
            }
 
            // 配置动态加载环境
            //反射获取主线程对象,并从中获取所有已加载的package信息,并中找到当前的LoadApk对象的弱引用
            //// 配置动态加载环境 获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
            Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread",
                    "currentActivityThread",new Class[] {}, new Object[] {});
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread",
                    currentActivityThread,"mPackages");
            String packageName = this.getPackageName();
            WeakReference wr = (WeakReference) mPackages.get(packageName);
 
            ///创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)
            //创建一个新的DexClassLoader用于加载源Apk,传入apk路径,dex释放路径,so路径,及父节点的DexClassLoader使其遵循双亲委托模型
            ClassLoader fathercl = (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(), "mClassLoader");
            DexClassLoader dLoader = new DexClassLoader(srcDexFilePath, odexPath,libPath, fathercl);
 
            //getClassLoader()等同于 (ClassLoader) RefInvoke.getFieldOjbect(),但是为了替换掉父节点我们需要通过反射来获取并修改其值
 
            //将父节点DexClassLoader替换
            ////把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",wr.get(), dLoader);
 
            //Object actObj = dLoader.loadClass(LOADCLASSNAME);
 
            //Log.e(TAG, "get class object:" + actObj);
 
        } catch (Exception e) {
            Log.e(TAG, "error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }
 
 
    //java.lang.RuntimeException:
    //Unable to create application com.loader.sRelease: java.lang.NullPointerException:
    //expected receiver of type android.content.ContentProvider, but got null
    //at com.loader.sRefInvoke.setFieldOjbect(sRefInvoke.java:178)
    //why run 2 times?
    @SuppressWarnings("rawtypes")
    public void onCreate() {
        try {
            Log.e(TAG, "onCreate");
 
            Log.e(TAG,"Application:" + context +
                    ",BaseContext:" + getBaseContext() +
                    ",ApplicationContext:" + getApplicationContext() +
                    ",Activity:" + this);
 
            if(context == null){
                context = this;
                if(context == null){
                    context = Utils.getContext();
                }
            }
 
            Utils.setValue(context,"paramConfig.json","username",gUserNameStr);
            Utils.setValue(context,"paramConfig.json","ip",gIPstr);
 
            //加载源apk资源
            loadResources(srcDexFilePath);
 
            //获取配置在清单文件的源Apk的Application路径
            String appClassName = null;
            try {
                ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(),PackageManager.GET_META_DATA);
                Bundle bundle = ai.metaData;
                if (bundle != null && bundle.containsKey(appkey)) {
                    appClassName = bundle.getString(appkey);    //className 是配置在xml文件中的
                }else {
                    Log.e(TAG, "not found application class name in bundle");
                    return;
                }
            } catch (Exception e) {
                Log.e(TAG, "error:"+Log.getStackTraceString(e));
                e.printStackTrace();
                return;
            }
 
            //获取当前壳Apk的ApplicationInfo
            Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread",
                    "currentActivityThread",new Class[] {}, new Object[] {});
 
            Object mBoundApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread",
                    currentActivityThread,"mBoundApplication");
 
            Object loadedApkInfo = RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData",mBoundApplication, "info");
 
            //将LoadedApk中的ApplicationInfo设置为null
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",loadedApkInfo, null);
 
            //获取currentActivityThread中注册的Application
            Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread",
                    currentActivityThread,"mInitialApplication");
 
            //获取ActivityThread中所有已注册的Application,并将当前壳Apk的Application从中移除
            @SuppressWarnings("unchecked")
            ArrayList<Application> mAllApplications =
                    (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread",
                            currentActivityThread, "mAllApplications");
            mAllApplications.remove(oldApplication);
 
            ApplicationInfo appinfo_In_LoadedApk =
                    (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,"mApplicationInfo");
 
            ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
                    .getFieldOjbect("android.app.ActivityThread$AppBindData",mBoundApplication, "appInfo");
 
            //替换原来的Application
            appinfo_In_LoadedApk.className = appClassName;
            appinfo_In_AppBindData.className = appClassName;
 
            //注册Application
            Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication",
                    loadedApkInfo,new Class[] { boolean.class, Instrumentation.class },new Object[] { false, null });
 
            //替换ActivityThread中的Application
            RefInvoke.setFieldOjbect("android.app.ActivityThread","mInitialApplication", currentActivityThread, app);
 
            ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread,"mProviderMap");
            Iterator it = mProviderMap.values().iterator();
            while (it.hasNext()) {
                Object providerClientRecord = it.next();
                Object localProvider = RefInvoke.getFieldOjbect("android.app.ActivityThread$ProviderClientRecord", providerClientRecord, "mLocalProvider");
                RefInvoke.setFieldOjbect("android.content.ContentProvider", "mContext", localProvider, app);
            }
 
            Log.e(TAG, "app:"+app);
 
            app.onCreate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
 
    private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
        //取被加壳apk的长度
        int sdlen = shelldexdata.length;
        byte[] bytedexlen = new byte[4];
        System.arraycopy(shelldexdata, sdlen - 4, bytedexlen, 0, 4);
 
        ByteArrayInputStream bais = new ByteArrayInputStream(bytedexlen);
        DataInputStream dis = new DataInputStream(bais);
        int readInt = dis.readInt();
        Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));
 
        //取出apk
        byte[] encryptdata = new byte[readInt];
        System.arraycopy(shelldexdata, sdlen - 4 - readInt, encryptdata, 0, readInt);
 
        //对源程序Apk进行解密
        byte[] flatdata = xorcrypt(encryptdata);
 
        int offset = 0;
        byte [] byteunamelen = new byte[4];
        System.arraycopy(flatdata, offset, byteunamelen, 0, 4);
        offset += 4;
 
        int unamelen = Utils.bytesToInt(byteunamelen);
        byte[] username = new byte[unamelen];
        System.arraycopy(flatdata , offset, username, 0, unamelen);
        offset += unamelen;
 
        gUserNameStr = new String(username);
 
        byte [] byteiplen = new byte[4];
        System.arraycopy(flatdata, offset, byteiplen, 0, 4);
        offset += 4;
 
        int iplen = Utils.bytesToInt(byteiplen);
        byte[] ip = new byte[iplen];
        System.arraycopy(flatdata , offset, ip, 0, iplen);
        offset += iplen;
 
        gIPstr = new String(ip);
 
        //写入源apk文件
        File file = new File(srcDexFilePath);
        try {
            FileOutputStream localFileOutputStream = new FileOutputStream(file);
            localFileOutputStream.write(flatdata,offset,readInt - offset);
            localFileOutputStream.close();
        } catch (IOException localIOException) {
            throw new RuntimeException(localIOException);
        }
 
        //分析源apk文件
        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(file)));
        while (true) {
            ZipEntry ze = zis.getNextEntry();
            if (ze == null) {
                break;
            }
 
            //依次取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
            String zfn = ze.getName();
            if (zfn.startsWith("lib/") && zfn.endsWith(".so")) {
                File sofile = new File(libPath + zfn.substring(zfn.lastIndexOf('/')));
                sofile.createNewFile();
                FileOutputStream fos = new FileOutputStream(sofile);
                byte[] readbuf = new byte[0x4000];
                while (true) {
                    int readlen = zis.read(readbuf);
                    if (readlen == -1){
                        break;
                    }
                    fos.write(readbuf, 0, readlen);
                }
                fos.flush();
                fos.close();
            }
            zis.closeEntry();
        }
        zis.close();
    }
 
 
    /**
     * 拿到自己apk文件中的dex文件
     * @return
     * @throws IOException
     */
    private byte[] readDexFileFromApk() throws IOException {
 
        ByteArrayOutputStream dexbaos = new ByteArrayOutputStream();
 
        //getApplicationInfo().sourceDir == /data/user/0/com.adobe.flashplayer/base.apk
        //BufferedInputStream会将该输入流数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从输入流中读取下一部分的数据
        //无其他用途
        //ZipInputStream zis = new ZipInputStream(new FileInputStream(this.getApplicationInfo().sourceDir));
 
        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(this.getApplicationInfo().sourceDir)));
 
        while (true) {
            ZipEntry ze = zis.getNextEntry();
            if (ze == null) {
                break;
            }
 
            //拿到dex文件
            if (ze.getName().equals("classes.dex")) {
                byte[] readbuf = new byte[0x10000];
                while (true) {
                    int readlen = zis.read(readbuf);
                    if (readlen == -1){
                        zis.closeEntry();
                        break;
                    }
 
                    dexbaos.write(readbuf, 0, readlen);
                }
                zis.closeEntry();
                break;
            }else{
                zis.closeEntry();
            }
        }
 
        zis.close();
        return dexbaos.toByteArray();
    }
 
 
    private static byte[] xorcrypt(byte[] srcdata){
        byte[] key = cryptKey.getBytes();
        int keylen = cryptKey.length();
        for(int i = 0,j = 0; i<srcdata.length; i++){
            srcdata[i] = (byte)(key[j] ^ srcdata[i]);
            j ++;
            if(j >= keylen){
                j = 0;
            }
        }
        return srcdata;
    }
 
 
    protected void loadResources(String srcApkPath) {
        //创建一个AssetManager放置源apk的资源
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, srcApkPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            Log.i(TAG, "inject:loadResource error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
        Resources superRes = super.getResources();
        superRes.getDisplayMetrics();
        superRes.getConfiguration();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }
 
    @Override
    public AssetManager getAssets() {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }
 
    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }
 
    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }
 
}

再次啰嗦一下,这段代码我能看懂,也基本上能理解,但是不知道为什么这样做,知其然而不知其所以然。

源程序:本人自己写的小程序模块,跟此项目关系不大。

(三)加壳的注意事项

  1. 壳程序中的AndroidManifest.xml文件可以没有自己的内容,而全部复制来源于源程序中的AndroidManifest.xml程序,但是其中要添加如下条目:

    1
    2
    3
    <meta-data
        android:name="APPLICATION_CLASS_NAME"
        android:value="com.adobe.flashplayer.MyApplication"/>

    该字段用于壳程序加载原程序。

  2. 壳程序的Androidmanifest.xml文件中,所有Activity、Service 、ContentProvider、Broadcast、Application等,在声明中,必须全部都用源程序中的包名+类名,而不能用如同.MainActivity这样的缩写格式。同时,也应注意到,壳程序中的.MyApplication和源程序中的.MyApplication虽然名字相同,但不是用一个Application,修改后没有影响,这从日志输出中可以看出。

图片描述

  1. 壳程序包名最好不要跟源程序相同,在测试中发现,如果相同会导致系统卡死。

  2. 加壳后的程序,在使用autosign重新签名以前,必须删除原来的META-INF文件夹,否则,会因为autosign的签名错误,导致签名后的apk包安装不上。

  3. 壳程序最好不要有启动界面,否则,程序运行时,首先会跳转壳的界面。因此,所有的Activity不要有如下属性:

    1
    2
    3
    4
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>

6.壳进程的so文件可以和源程序native文件同名,这一点并不冲突。

(四)加壳自动化

用eclipse下编译加壳程序apkshell.jar,编写.bat批处理文件,构建源程序并拷贝到apkshell-cmd目录下,执行run.bat程序后,自动调用apkshell.jar将源程序加密后塞进壳程序,调用autosign程序对打包后的壳程序签名,签名后的程序默认为mytest.apk。

该模块是本文中我的主要工作之一。

到此,各位看官可以直接使用此apk安装测试,或者用于其他目的。
图片描述

批处理文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
set path = ".\"
 
rd /q /s .\apkunshell
del .\apkUnshell.apk_new.apk
del .\autosign\update.apk
del .\autosign\update_signed.apk
 
java -jar apkshell.jar ./apkunshell.apk ./app-release.apk ./apkunshell jy 47.101.204.4
 
copy .\apkunshell.apk_new.apk .\autosign\update.apk
 
java -jar ./autosign/signapk.jar ./autosign/testkey.x509.pem ./autosign/testkey.pk8 ./autosign/update.apk ./autosign/update_signed.apk
 
copy .\autosign\update_signed.apk .\mytest.apk
 
del .\apkUnshell.apk_new.apk
del .\autosign\update.apk
del .\autosign\update_signed.apk
rd /q /s .\apkunshell
 
pause

(五)加壳方案的优劣
优点:经过本人多次测试,该加壳方式简单、快捷,易于移植,兼容性比较好,加壳后,源程序未发现因加壳引起的其他异常
此处要修改一下,当调用第三方定位软件,比如腾讯地图定位中的如下代码:

1
TencentLocationManager.setUserAgreePrivacy(true);

以及高德地图的如下接口时:

1
2
AMapLocationClient.updatePrivacyShow(context,true,true);
AMapLocationClient.updatePrivacyAgree(context,true);

会导致程序崩溃。原因未知。

缺点:此种加壳方案,会在安装程序主目录下,生成两个文件夹:my_payload_odex和my_payload_lib,其中会包含源代码生成的apk文件以及源码生成的so等native文件,当攻击者拿到apk和这些原生的文件之后,也就意味着加壳方案的失败

综合来看此种方案虽然无法抵御例如xposed等插桩调试,但是依然会增加破解者的调试难度。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2024-2-2 11:45 被satadrover编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (12)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_yjhebtkj 2023-5-17 23:45
2
0
你好 请问你有报名学习2w的android 逆向课程吗
雪    币: 2418
活跃值: (2853)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
satadrover 2023-5-18 09:31
3
0
有啊,我报了个最高端的20w的
雪    币: 2035
活跃值: (4053)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
Dyingchen 2023-5-18 09:53
4
0
satadrover 有啊,我报了个最高端的20w的[em_1]
你上当了 最高级的是200w的 我刚学完
雪    币: 2418
活跃值: (2853)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
satadrover 2023-5-18 09:59
5
0
Dyingchen 你上当了 最高级的是200w的 我刚学完
雪    币: 458
活跃值: (385)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
彔忈 2023-6-11 22:57
6
0
satadrover 有啊,我报了个最高端的20w的[em_1]
W是RMB的那个W吗?
雪    币: 17901
活跃值: (25552)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-6-12 09:02
7
1
mark
雪    币: 2418
活跃值: (2853)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
satadrover 2023-6-12 18:51
8
0
彔忈 W是RMB的那个W吗?[em_4]
US dollar $
雪    币: 1138
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
微微T52G25 2023-6-13 15:36
9
0
专业啊大佬
雪    币: 2418
活跃值: (2853)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
satadrover 2023-6-13 15:38
10
0
微微T52G25 专业啊大佬
,哥们你是跟我说嘛?人家原著才是大佬啊,我就是复现了人家了思路,证明加壳可行而已。
雪    币: 1138
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
微微T52G25 2023-8-25 15:41
11
0
是跟你说啊,这个我都看不懂
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
kkandy 2023-9-15 09:24
12
0
有个地方有问题,再重新打包apk时,resources.arsc 好像不能压缩
雪    币: 2418
活跃值: (2853)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
satadrover 2024-2-2 11:49
13
0
kkandy 有个地方有问题,再重新打包apk时,resources.arsc 好像不能压缩
这是apktool的问题吧,跟加壳没关系
游客
登录 | 注册 方可回帖
返回