声明:原创文章,转载请备注来源: http://shuwoom.com/?p=360&preview=true

10.1 原理分析

10.1.1 APK文件结构

首先让我们先了解一下一个完整的Android应用程序都由哪些文件组成。解压一个apk包,我们可以看到一下的这些文件及文件夹:

每个文件及文件夹的作用如下表所示

文件或目录

说明

assets文件夹

存放资源文件的目录

lib文件夹

存放ndk编译出来的so文件

META-INF文件夹

该目录下存放的是签名信息,用来保证apk包的完整性和系统的安全性

CERT.RSA

保存着该应用程序的证书和授权信息

CERT.SF

保存着SHA-1信息资源列表

MANIFEST.MF

清单信息

res文件夹

存放资源文件的目录

AndroidManifest.xml

一个清单文件,它描述了应用的名字、版本、权限、注册的服务等信息。

classes.dex

java源码编译经过编译后生成的dalvik字节码文件,主要在Dalvik虚拟机上运行的主要代码部分

resources.arsc

编译后的二进制资源文件。

这里说明下META-INF文件夹下3个文件的关系:

(1) 首先对apk包中每个文件做一次算法(数据摘要+Base64编码),然后保存到MANIFEST.MF文件中

(2) 然后对MANIFEST.MF整个文件同样做一次算法(数据摘要+Base64编码),存放到CERT.SF文件的头属性中再对MANIFEST.MF文件中各个属性块做一次算法(数据摘要+Base64编码),存放到CERT.SF文件中

(3) 最后对CERT.SF文件做签名内容保存到CERT.RSA

TODOMETA-INF目录下三个文件中SHA-1值的计算

参考http://blog.csdn.net/jiangwei0910410003/article/details/50443505

10.1.2 APK打包流程

我们看一下APK打包的完整流程

上图中涉及到的工具及其作用如下

名称

功能介绍

aapt

打包资源文件,包括resassets文件夹下的资源、AndroidManifest.xml文件Android基础类库

aidl

.aidl接口文件转换成.java文件

javac

编译java文件,生成.class字节码文件

dx

将所有的第三方libraries.class文件转换成Dalvik虚拟机支持的.dex文件

apkbuilder

打包生成apk文件,但未签名

jarsigner

对未签名的apk文件进行签名

zipalign

对签名后的apk文件进行对其处理

TODO:单独学习APK打包流程

10.1.3 Dex文件结构

Dex文件整体结构如下:

dex文件结构的详细分析可以回看第四章-Dex文件结构分析这里我们在dex整体加密暂时只用到Dex文件头所以我们主要介绍下Dex文件头的结构

这里面3个成员我们需要特别关注,这在后面加固里会用到,它们分别是checksumsignaturefileSize。

0x01 checksum字段

checksum是校验码字段4bytes主要用来检查从该字段(不包含checksum字段,也就是从12bytes开始算起)开始到文件末尾这段数据是否完整也就是完整性校验。它使用alder32算法校验

0x02 signature字段

signatureSHA-1签名字段20bytes,作用跟checksum一样,也是做完整性校验。之所以有两个完整性校验字段,是由于先使用checksum字段校验可以先快速检查出错的dex文件,然后才使用第二个计算量更大的校验码进行计算检查。

0x03 fileSize字段

4bytes,保存classes.dex文件总长度。

3个字段当我们修改dex文件的时候,这3个字段的值是需要更新的,否则在加载到Dalvik虚拟机的时候会报错。

10.1.4 Dex整体加固原理

替换classloader时机
Application:attachBaseContext()
ContentProvider:onCreate()
APplication:onCreate()

加载原始application
1.获取原始application的class name
2.加载原始application并生成对象
3.替换API层所有Application引用
4.设置baseContext并调用原始application的onCreate()

Dex文件整体加固原理如下

在该过程中涉及到三个对象,分别如下:

源程序

源程序也就是我们的要加固的对象这里面主要修改的是原apk文件中的classes.dex文件和AndroidManifest.xml文件。

壳程序

壳程序主要用于解密经过加密了的dex文件并加载解密后的原dex文件并正常启动原程序

加密程序

加密程序主要是对原dex文件进行加密加密算法可以是简单的异或操作反转、rc4、des、rsa等加密算法

该加固过程可以分为如下4个阶段:

(1) 加密阶段

(2)合成新的dex文件

(3)修改原apk文件并重打包签名

(4)运行壳程序加载原dex文件

0x01 加密阶段

加密阶段主要是讲把原apk文件中提取出来的classes.dex文件通过加密程序进行加密加密的时候如果使用des对称加密算法则需要注意处理好密钥的问题。同样的,如果采用非对称加密,也同样存在公钥保存的问题。

0x02 合成新的dex文件

这一阶段主要是讲上一步生成的加密的dex文件和我们的壳dex文件合并将加密的dex文件追加在壳dex文件后面并在文件末尾追加加密dex文件的大小数值。这样我们就合成了新的classes.dex文件。由于我们修改了dex文件,这时候

当然还可以这样处理。将加密的dex文件保存到其他目录下,如assets资源目录。那么这时候我们新的classes.dex文件就直接是壳程序不需要合成两种方案都可以

前面讲了讲加密dex文件保存到文件末尾以及保存到资源目录下2中方法。其实还有一种更复杂的方案,就是讲加密dex文件保存到dex header我们回顾10.1.3Dex文件结构。Dex Header中有一个字段:header_size,这个字段是记录dex文件头的长度一般情况下dex头文件大小为112bytes我们可以将header_size修改为header_size和加密dex文件长度两者的总长度,并将加密dex文件嵌入到dex header末尾来达到隐藏原dex文件的目的。但是这里有个问题,就是原来dex文件头部以下的一些类、方法、字段等位移可能发生改变需要修改,这个修复工程很复杂,或者我们可以尝试使用修改dx工具的源码来实现。

在壳程序里面有个重要的类ProxyApplication类,该类继承Application类,也是应用程序最先运行的类。所以,我们就是在这个类里面,在原程序运行之前,进行一些解密dex文件和加载原dex文件的操作。

0x03 修改原apk文件并重打包签名

在这一阶段我们首先将apk解压会看到如下图的6个文件和目录。其中,我们需要修改的只有2个文件,分别是classes.dexAndroidManifest.xml文件,其他文件和文件加都不需要改动。

首先我们把解压后apk目录下原来的classes.dex文件替换成我们在0x02上一步合成的新的classes.dex文件然后由于我们程序运行的时候首先加载的其实是壳程序里的ProxyApplication类。所以,我们需要修改AndroidManifest.xml文件,指定applicationProxyApplication,这样才能正常找到识别ProxyApplication类并运行壳程序。这里需要注意的是

完成上述两个文件的替换后我们就重新打包apk并签名到此我们就完成了dex文件的整体加固

0x04运行壳程序加载原dex文件

通过上面3个步骤,我们了解了dex文件整体加固的流程。那么,当这个加固后的dex文件加载到Dalvik虚拟机中,它又是如何工作的呢?下面我们就来讲解下壳程序在这里面发挥的重要作用。

首先,Dalvik虚拟机会加载我们经过修改的新的classes.dex文件,并最先运行ProxyApplication类。在这个类里面,有2个关键的方法:attachBaseContextonCreate方法。ProxyApplication显示运行attachBaseContext再运行onCreate方法。

attachBaseContext方法里主要做两个工作

1) 读取classes.dex文件末尾记录加密dex文件大小的数值则加密dex文件在新classes.dex文件中的位置为len(classes.dex文件) – len(加密dex文件大小)然后将加密的dex文件读取出来加密并保存到资源目录下

2) 然后使用自定义的DexClassLoader加载解密后的原dex文件

onCreate方法中主要做两个工作

1) 通过反射修改ActivityThread类,并将Application指向原dex文件中的Application

2) 创建原Application对象并调用原ApplicationonCreate方法启动原程序

具体的app启动过程可以回看第2章的内容。

10.2 项目案例

10.2.1 源程序

源程序我们使用NDK开发一个简单的demo并在assets目录存放一个txt文件读取这样我们就可以得到跟10.1.1一样的文件结构,也可以检测下加固后的文件是否影响lib文件的和资源目录下文件的操作。

工程结构如下

MyApplication.java关键代码

public class MyApplication extends Application{

 

@Override

public void onCreate() {

// TODO Auto-generated method stub

super.onCreate();

}

 

@Override

protected void attachBaseContext(Context base) {

// TODO Auto-generated method stub

super.attachBaseContext(base);

}

}

 

MainActivity.java关键代码

public class MainActivity extends Activity

{

    /** Called when the activity is first created. */

 

    public native void test();

 

    @Override

    public void onCreate(Bundle savedInstanceState)

    {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

 

        System.loadLibrary(“demo1”);

        test();

 

        Log.v(“demo”, getFromAssets(“test.txt”));

 

    }

 

     public String getFromAssets(String fileName){

         String Result=””;

         try {

             InputStreamReader inputReader = new InputStreamReader( getResources().getAssets().open(fileName) );

             BufferedReader bufReader = new BufferedReader(inputReader);

             String line=””;

             

             while((line = bufReader.readLine()) != null)

                 Result += line;

             

         } catch (Exception e) {

             e.printStackTrace();

         }

         return Result;

     }

}

demo1.cpp关键代码

#include “com_demo_MainActivity.h”

#include <android/log.h>

#include <stdio.h>

 

#define LOG_TAG “AndroidNDK”

#define LOGI(…)  __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

 

JNIEXPORT void JNICALL Java_com_demo_MainActivity_test (JNIEnv * env, jobject obj)

{

   LOGI(“Hello World…\n”);

}

10.2.2 加密程序

加密程序是个java工程。

Main.java源码

public static void main(String[] args) throws Exception {

if(args.length != 2){

System.out.println(“Error! Please input 2 parameters!”);

return ;

}

//参数1:脱壳classes.dex

//参数2TargetApk.zip

File tuokeDexFile = new File(args[0]);

File targetFile = new File(args[1]);

byte[] tuokeDexFileArray = readFileBytes(tuokeDexFile);

byte[] targetFileArray = encrypt(readFileBytes(targetFile));

int tuokeDexFileLen = tuokeDexFileArray.length;

int targetFileLen = targetFileArray.length;

int totalLen = targetFileLen + tuokeDexFileLen + 4;

byte[] newdex = new byte[totalLen];

//添加脱壳classes.dex文件

System.arraycopy(tuokeDexFileArray, 0, newdex, 0, tuokeDexFileLen);

//添加目标TargetApk.zip文件

System.arraycopy(targetFileArray, 0, newdex, tuokeDexFileLen, targetFileLen);

//添加目标zip文件大小

System.arraycopy(intToByte(targetFileLen), 0, newdex, tuokeDexFileLen + targetFileLen, 4);

//修改Dex file size文件头

fixFileSizeHeader(newdex);

//修改Dex SHA1文件头

fixSHA1Header(newdex);

//修改Dex CheckSum文件头

fixCheckSumHeader(newdex);

File file = new File(“classes.dex”);

if(!file.exists()){

file.createNewFile();

}

FileOutputStream out = new FileOutputStream(file);

out.write(newdex);

out.flush();

out.close();

System.out.println(“Enforce apk successfully!”);

}

修改dex头的fileSize字段

    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];  

        }  

        System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)  

    }

修改dex头的signature字段

    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++) {  

            hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)  

                    .substring(1);  

        }  

    }

修改dex头的checkSum字段

private static void fixCheckSumHeader(byte[] dexBytes) {  

        Adler32 adler = new Adler32();  

        adler.update(dexBytes, 12, dexBytes.length – 12);//12到文件末尾计算校验码  

        long value = adler.getValue();  

        int va = (int) value;  

        byte[] newcs = intToByte(va);  

        //高位在前,低位在前掉个个  

        byte[] recs = new byte[4];  

        for (int i = 0; i < 4; i++) {  

            recs[i] = newcs[newcs.length – 1 – i];  

        }  

        System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)  

    }

加密

private static byte[] encrypt(byte[] srcdata){  

        for(int i = 0;i<srcdata.length;i++){  

            srcdata[i] = (byte)(0xFF ^ srcdata[i]);  

        }  

        return srcdata;  

    }

 

读取二进制文件内容

private static byte[] readFileBytes(File file) throws IOException {  

        byte[] arrayOfByte = new byte[1024];  

        ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();  

        FileInputStream fis = new FileInputStream(file);  

        while (true) {  

            int i = fis.read(arrayOfByte);  

            if (i != -1) {  

                localByteArrayOutputStream.write(arrayOfByte, 0, i);  

            } else {  

                return localByteArrayOutputStream.toByteArray();  

            }  

        }  

    }

intbyte数组

public static byte[] intToByte(int number) {  

        byte[] b = new byte[4];  

        for (int i = 3; i >= 0; i–) {  

            b[i] = (byte) (number % 256);  

            number >>= 8;  

        }  

        return b;  

    }

10.2.3 壳程序

壳程序我们主要分析ProxyApplication类:

0x01 attachBaseContext方法

首先分析attachBaseContext方法:

protected void attachBaseContext(Context base) {

super.attachBaseContext(base);

Log.v(“demo”, “[JiaguApk]=>attachBaseContext() start…”);

try {

File odex = this.getDir(“assets”, MODE_PRIVATE);

String odexPath = odex.getAbsolutePath();

String targetFilename = odexPath + “/TargetApk.zip”;

File targetApkZipFile = new File(targetFilename);

if(!targetApkZipFile.exists()){

//classes.dex中提取TargetApk.zip

targetApkZipFile.createNewFile();

byte[] classesDexData = readClassesDexFromApk();

extractTargetZipFileFromDex(classesDexData, targetFilename);

}

Object currentActivityThread = RefInvoke.invokeStaticMethod(“android.app.ActivityThread”, “currentActivityThread”, new Class[] {}, new Object[] {});

String packageName = getPackageName();

Map mPackages = (Map) RefInvoke.getFieldOjbect(“android.app.ActivityThread”, currentActivityThread, “mPackages”);

WeakReference wr = (WeakReference) mPackages.get(packageName);

DexClassLoader dLoader = new DexClassLoader(odexPath + “/TargetApk.zip”, odexPath, “/data/data/” + packageName + “/lib”, base.getClassLoader().getParent());

//替換成TargetApk.dexClassLoader

RefInvoke.setFieldOjbect(“android.app.LoadedApk”, “mClassLoader”, wr.get(), dLoader);

} catch (Exception e) {

Log.v(“demo”, “[JiaguApk]=>attachBaseContext() ” + Log.getStackTraceString(e));

}

Log.v(“demo”, “[JiaguApk]=>attachBaseContext() end…”);

}

在该方法中我们首先获取加密dex文件并保存到assets资源目录下然后进行解密

由于运行加固后的apk文件时,应用程序使用的是加固后dex文件的类加载器,而不是原dex文件的类加载器。所以,我们需要事先将类加载器替换成员dex文件的类加载器。

这里我们通过反射获取ActivityThread,通过其mPackages成员获取LoadedApk对象。通过第二章的app启动过程分析,我们知道类加载器的创建是在LoadedApk中完成,这也是为什么我们需要获取LoadedApk对象。通过该对象的mClassLoader成员,我们可以修改该成员指向我们自定义的DexClassLoader,这个类加载器就是原dex中的类加载器。这样我们就可以在后面的步骤中用该类加载器加载原dex文件。

0x02 onCreate方法

public void onCreate() {

try {

// 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。  

String appClassName = “com.demo.MyApplication”;

/**

 * 调用静态方法android.app.ActivityThread.currentActivityThread

 * 获取当前activity所在的线程对象

 */

Object currentActivityThread = RefInvoke.invokeStaticMethod(  

        “android.app.ActivityThread”, “currentActivityThread”,  

        new Class[] {}, new Object[] {});

/**

 * 获取currentActivityThread中的mBoundApplication属性对象,该对象是一个

 *  AppBindData类对象,该类是ActivityThread的一个内部类

 */

Object mBoundApplication = RefInvoke.getFieldOjbect(  

        “android.app.ActivityThread”, currentActivityThread,  

        “mBoundApplication”);

/**

 * 获取mBoundApplication中的info属性,info LoadedApk类对象

 */

Object loadedApkInfo = RefInvoke.getFieldOjbect(  

        “android.app.ActivityThread$AppBindData”,  

        mBoundApplication, “info”);  

 if(null == loadedApkInfo){

     Log.v(“demo”, “[JiaguApk]=>onCreate()=>loadedApkInfo is null!!!”);

  }else{

  Log.v(“demo”, “[JiaguApk]=>onCreate()=>loadedApkInfo:” + loadedApkInfo);

  }

/**

 * loadedApkInfo对象的mApplication属性置为null

 */

RefInvoke.setFieldOjbect(“android.app.LoadedApk”, “mApplication”,  

        loadedApkInfo, null);  

/**

 * 获取currentActivityThread对象中的mInitialApplication属性

 * 这货是个正牌的 Application

 */

Object oldApplication = RefInvoke.getFieldOjbect(  

        “android.app.ActivityThread”, currentActivityThread,  

        “mInitialApplication”);  

/**

 * 获取currentActivityThread对象中的mAllApplications属性

 * 这货是 装Application的列表

 */

ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke  

        .getFieldOjbect(“android.app.ActivityThread”,  

                currentActivityThread, “mAllApplications”);

//列表对象终于可以直接调用了 remove调了之前获取的application 抹去记录的样子

mAllApplications.remove(oldApplication);

/**

 * 获取前面得到LoadedApk对象中的mApplicationInfo属性,是个ApplicationInfo对象

 */

ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke  

        .getFieldOjbect(“android.app.LoadedApk”, loadedApkInfo,  

                “mApplicationInfo”);

/**

 * 获取前面得到AppBindData对象中的appInfo属性,也是个ApplicationInfo对象

 */

ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke  

        .getFieldOjbect(“android.app.ActivityThread$AppBindData”,  

                mBoundApplication, “appInfo”);

//把这两个对象的className属性设置为从meta-data中获取的被加密apkapplication路径

appinfo_In_LoadedApk.className = appClassName;  

appinfo_In_AppBindData.className = appClassName;

/**

 * 调用LoadedApk中的makeApplication 方法 造一个application

 * 前面改过路径了

 */

Application app = (Application) RefInvoke.invokeMethod(  

        “android.app.LoadedApk”, “makeApplication”, loadedApkInfo,  

        new Class[] { boolean.class, Instrumentation.class },  

        new Object[] { false, null });  

RefInvoke.setFieldOjbect(“android.app.ActivityThread”,  

        “mInitialApplication”, currentActivityThread, app);  

if(null == app){

Log.v(“demo”, “[JiaguApk]=>onCreate()=>app is null!!!”);

  }else{

      app.onCreate();

      Log.v(“demo”, “[JiaguApk]=>onCreate() success!”);

  }

} catch (Exception e) {

Log.v(“demo”, “[JiaguApk]=>onCreate() ” + Log.getStackTraceString(e));

}

Toast.makeText(this, “Enforced by 01hackcode”, Toast.LENGTH_LONG).show();

}

onCreate方法中我们主要做的是还原Application对象。由于在加固dex文件的时候,我们讲原来的Application替换成了我们定义的ProxyApplication对象。所以,在这一步里,我们需要还原它。至于怎么还原呢?同样参考第二章的app启动流程,我们知道,Application对象的创建发生在ActivityThread类的handleBindApplication方法中,该方法其实是调用LoadedApk类的makeApplication方法进行创建,而在该方法中,最终是调用Instrumentation类的newApplication方法完成Application对象的创建。最后调用原Application对象的onCreate方法完成启动过程

在这个过程中我们要将所有引用到Application的地方都进行修改,这也是为什么上面那么多种反射修改操作。通过第二章的分析,我们发现引用了Application的类有3个地方。

(1) ActivityThread类的mInitialApplication成员,它是Application类,我们需要将替换成原dex文件的Application对象

(2) ActivityThread类的mAllApplication成员,每次创建完Application对象后,就会将该对象添加到mAllApplication列表中,所以我们要讲该列表中的ProxyApplication对象移除。在后面我们通过反射调用makeApplication方法的时候会将创建的原dex文件的Application添加到该列表中

(3) LoadedApk类中的mApplicationInfo对象的className成员记录了ProxyApplication的类名,这里我们也需要修改,替换成原dex文件的Application类名

(4) 最后是ActivityThread的内部类AppBindData类对象mBoundApplication成员appInfo对象的className成员也记录了ProxyApplication类名我们也将它修改成员dexdex文件的Application类名

至此我们就差不多修改完了所有引用ProxyApplication对象的地方。最后还差一步,我们需要反射获取ActivityThread类的makeApplication方法完成原Dex文件中Application的创建工作,并调用该Application对象的onCreate方法到这里我们才可以正常运行原程序

ActivityThread.java文件

public final class ActivityThread{

Application mInitialApplication;

final ArrayList<Application> mAllApplications = new ArrayList<Application>();

final ArrayMap<String, WeakReference<LoadedApk>> mPackages

            = new ArrayMap<String, WeakReference<LoadedApk>>();

static final class AppBindData {

    

ApplicationInfo appInfo;

}

}

LoadedApk.java文件:

public final class LoadedApk {

private final ApplicationInfo mApplicationInfo;

}

ApplicationInfo.java文件

public class ApplicationInfo extends PackageItemInfo implements Parcelable {

/**

     * Class implementing the Application object.  From the “class”

     * attribute.

     */

public String className;

}

 

10.3 总结

参考

[1] 常见app加固厂商脱壳方法研究 http://www.mottoin.com/89035.html

[2] ANDROID原理解密之APK生成过程 http://blog.csdn.net/walid1992/article/details/51604673

[3] 从源码到apk http://sparkyuan.me/2016/04/01/%E4%BB%8E%E6%BA%90%E7%A0%81%E5%88%B0APK/

[4] Android APK打包流程 http://shinelw.com/2016/04/27/android-make-apk/

[5] Android官网-APK打包 https://developer.android.com/intl/zh-cn/sdk/installing/studio-build.html

[6] Dex文件格式详解 http://www.jianshu.com/p/f7f0a712ddfe

[7] Android安全-Dex文件格式详解 http://www.blogfshare.com/dex-format.html

[8] Dex文件结构 http://blog.csdn.net/androidsecurity/article/details/8664778

[9] 基于代理Application机制的Android应用加壳方法 http://bbs.pediy.com/thread-215691.htm

[11] android中的apk的加固加壳原理解析和实现 http://www.wjdiankong.cn/android%E4%B8%AD%E7%9A%84apk%E7%9A%84%E5%8A%A0%E5%9B%BA%E5%8A%A0%E5%A3%B3%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90%E5%92%8C%E5%AE%9E%E7%8E%B0/

[12] Android APK加固技术探索 https://chaman.gitbooks.io/techblog/Android/apk-enchance/apk-enchance.html

[13] Android APK加壳技术方案1http://blog.csdn.net/androidsecurity/article/details/8678399

[14] Android APK加壳技术方案2http://blog.csdn.net/androidsecurity/article/details/8809542

[15] Android签名机制之签名验证过程详解 http://blog.csdn.net/jiangwei0910410003/article/details/50443505

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注