Android中的Apk的加固(加壳)原理解析和实现
转载:http://blog.csdn.net/jiangwei0910410003/article/details/48415225
一、前言
今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理。现阶段。我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk,结果被人反编译了,那心情真心不舒服。虽然我们混淆,做到native层,但是这都是治标不治本。反编译的技术在更新,那么保护Apk的技术就不能停止。现在网上有很多Apk加固的第三方平台,最有名的应当属于:爱加密和梆梆加固了。其实加固有些人认为很高深的技术,其实不然,说的简单点就是对源Apk进行加密,然后在套上一层壳即可,当然这里还有一些细节需要处理,这就是本文需要介绍的内容了。
二、原理解析
下面就来看一下Android中加壳的原理:
我们在加固的过程中需要三个对象:
1、需要加密的Apk(源Apk)
2、壳程序Apk(负责解密Apk工作)
3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)
主要步骤:
我们拿到需要加密的Apk和自己的壳程序Apk,然后用加密算法对源Apk进行加密在将壳Apk进行合并得到新的Dex文件,最后替换壳程序中的dex文件即可,得到新的Apk,那么这个新的Apk我们也叫作脱壳程序Apk.他已经不是一个完整意义上的Apk程序了,他的主要工作是:负责解密源Apk.然后加载Apk,让其正常运行起来。
在这个过程中我们可能需要了解的一个知识是:如何将源Apk和壳Apk进行合并成新的Dex
这里就需要了解Dex文件的格式了。下面就来简单介绍一下Dex文件的格式
具体Dex文件格式的详细介绍可以查看这个文件:http://download.csdn.net/detail/jiangwei0910410003/9102599
主要来看一下Dex文件的头部信息,其实Dex文件和Class文件的格式分析原理都是一样的,他们都是有固定的格式,我们知道现在反编译的一些工具:
1、jd-gui:可以查看jar中的类,其实他就是解析class文件,只要了解class文件的格式就可以
2、dex2jar:将dex文件转化成jar,原理也是一样的,只要知道Dex文件的格式,能够解析出dex文件中的类信息就可以了
当然我们在分析这个文件的时候,最重要的还是头部信息,应该他是一个文件的开始部分,也是索引部分,内部信息很重要。
我们今天只要关注上面红色标记的三个部分:
1) checksum
文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。
2) signature
使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
3) file_size
Dex 文件的大小 。
为什么说我们只需要关注这三个字段呢?
因为我们需要将一个文件(加密之后的源Apk)写入到Dex中,那么我们肯定需要修改文件校验码(checksum).因为他是检查文件是否有错误。那么signature也是一样,也是唯一识别文件的算法。还有就是需要修改dex文件的大小。
不过这里还需要一个操作,就是标注一下我们加密的Apk的大小,因为我们在脱壳的时候,需要知道Apk的大小,才能正确的得到Apk。那么这个值放到哪呢?这个值直接放到文件的末尾就可以了。
所以总结一下我们需要做:修改Dex的三个文件头,将源Apk的大小追加到壳dex的末尾就可以了。
我们修改之后得到新的Dex文件样式如下:
那么我们知道原理了,下面就是代码实现了。所以这里有三个工程:
1、源程序项目(需要加密的Apk)
2、脱壳项目(解密源Apk和加载Apk)
3、对源Apk进行加密和脱壳项目的Dex的合并
三、项目案例
下面先来看一下源程序
1、需要加密的源程序Apk项目:ForceApkObj
需要一个Application类,这个到后面说为什么需要:
MyApplication.Java
[java] view plain copy
- packagecom.example.forceapkobj;
- importandroid.app.Application;
- importandroid.util.Log;
- publicclassMyApplicationextendsApplication{
- @Override
- publicvoidonCreate(){
- super.onCreate();
- Log.i("demo","sourceapkonCreate:"+this);
- }
- }
就是打印一下onCreate方法。
MainActivity.java
[java] view plain copy
- packagecom.example.forceapkobj;
- importandroid.app.Activity;
- importandroid.content.Intent;
- importandroid.os.Bundle;
- importandroid.util.Log;
- importandroid.view.View;
- importandroid.view.View.OnClickListener;
- importandroid.widget.TextView;
- publicclassMainActivityextendsActivity{
- @Override
- protectedvoidonCreate(BundlesavedInstanceState){
- super.onCreate(savedInstanceState);
- TextViewcontent=newTextView(this);
- content.setText("IamSourceApk");
- content.setOnClickListener(newOnClickListener(){
- @Override
- publicvoidonClick(Viewarg0){
- Intentintent=newIntent(MainActivity.this,SubActivity.class);
- startActivity(intent);
- }});
- setContentView(content);
- Log.i("demo","app:"+getApplicationContext());
- }
- }
2、加壳程序项目:DexShellTools
加壳程序其实就是一个Java工程,因为我们从上面的分析可以看到,他的工作就是加密源Apk,然后将其写入到脱壳Dex文件中,修改文件头,得到一个新的Dex文件即可。
看一下代码:
[java] view plain copy
- packagecom.example.reforceapk;
- importjava.io.ByteArrayOutputStream;
- importjava.io.File;
- importjava.io.FileInputStream;
- importjava.io.FileOutputStream;
- importjava.io.IOException;
- importjava.security.MessageDigest;
- importjava.security.NoSuchAlgorithmException;
- importjava.util.zip.Adler32;
- publicclassmymain{
- /**
- *@paramargs
- */
- publicstaticvoidmain(String[]args){
- //TODOAuto-generatedmethodstub
- try{
- FilepayloadSrcFile=newFile("force/ForceApkObj.apk");//需要加壳的程序
- System.out.println("apksize:"+payloadSrcFile.length());
- FileunShellDexFile=newFile("force/ForceApkObj.dex");//解客dex
- byte[]payloadArray=encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
- byte[]unShellDexArray=readFileBytes(unShellDexFile);//以二进制形式读出dex
- intpayloadLen=payloadArray.length;
- intunShellDexLen=unShellDexArray.length;
- inttotalLen=payloadLen+unShellDexLen+4;//多出4字节是存放长度的。
- byte[]newdex=newbyte[totalLen];//申请了新的长度
- //添加解壳代码
- System.arraycopy(unShellDexArray,0,newdex,0,unShellDexLen);//先拷贝dex内容
- //添加加密后的解壳数据
- System.arraycopy(payloadArray,0,newdex,unShellDexLen,payloadLen);//再在dex内容后面拷贝apk的内容
- //添加解壳数据长度
- System.arraycopy(intToByte(payloadLen),0,newdex,totalLen-4,4);//最后4为长度
- //修改DEXfilesize文件头
- fixFileSizeHeader(newdex);
- //修改DEXSHA1文件头
- fixSHA1Header(newdex);
- //修改DEXCheckSum文件头
- fixCheckSumHeader(newdex);
- Stringstr="force/classes.dex";
- Filefile=newFile(str);
- if(!file.exists()){
- file.createNewFile();
- }
- FileOutputStreamlocalFileOutputStream=newFileOutputStream(str);
- localFileOutputStream.write(newdex);
- localFileOutputStream.flush();
- localFileOutputStream.close();
- }catch(Exceptione){
- e.printStackTrace();
- }
- }
- //直接返回数据,读者可以添加自己加密方法
- privatestaticbyte[]encrpt(byte[]srcdata){
- for(inti=0;i<srcdata.length;i++){
- srcdata[i]=(byte)(0xFF^srcdata[i]);
- }
- returnsrcdata;
- }
- /**
- *修改dex头,CheckSum校验码
- *@paramdexBytes
- */
- privatestaticvoidfixCheckSumHeader(byte[]dexBytes){
- Adler32adler=newAdler32();
- adler.update(dexBytes,12,dexBytes.length-12);//从12到文件末尾计算校验码
- longvalue=adler.getValue();
- intva=(int)value;
- byte[]newcs=intToByte(va);
- //高位在前,低位在前掉个个
- byte[]recs=newbyte[4];
- for(inti=0;i<4;i++){
- recs[i]=newcs[newcs.length-1-i];
- System.out.println(Integer.toHexString(newcs[i]));
- }
- System.arraycopy(recs,0,dexBytes,8,4);//效验码赋值(8-11)
- System.out.println(Long.toHexString(value));
- System.out.println();
- }
- /**
- *int转byte[]
- *@paramnumber
- *@return
- */
- publicstaticbyte[]intToByte(intnumber){
- byte[]b=newbyte[4];
- for(inti=3;i>=0;i--){
- b[i]=(byte)(number%256);
- number>>=8;
- }
- returnb;
- }
- /**
- *修改dex头sha1值
- *@paramdexBytes
- *@throwsNoSuchAlgorithmException
- */
- privatestaticvoidfixSHA1Header(byte[]dexBytes)
- throwsNoSuchAlgorithmException{
- MessageDigestmd=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值,可有可无
- Stringhexstr="";
- for(inti=0;i<newdt.length;i++){
- hexstr+=Integer.toString((newdt[i]&0xff)+0x100,16)
- .substring(1);
- }
- System.out.println(hexstr);
- }
- /**
- *修改dex头file_size值
- *@paramdexBytes
- */
- privatestaticvoidfixFileSizeHeader(byte[]dexBytes){
- //新文件长度
- byte[]newfs=intToByte(dexBytes.length);
- System.out.println(Integer.toHexString(dexBytes.length));
- byte[]refs=newbyte[4];
- //高位在前,低位在前掉个个
- for(inti=0;i<4;i++){
- refs[i]=newfs[newfs.length-1-i];
- System.out.println(Integer.toHexString(newfs[i]));
- }
- System.arraycopy(refs,0,dexBytes,32,4);//修改(32-35)
- }
- /**
- *以二进制读出文件内容
- *@paramfile
- *@return
- *@throwsIOException
- */
- privatestaticbyte[]readFileBytes(Filefile)throwsIOException{
- byte[]arrayOfByte=newbyte[1024];
- ByteArrayOutputStreamlocalByteArrayOutputStream=newByteArrayOutputStream();
- FileInputStreamfis=newFileInputStream(file);
- while(true){
- inti=fis.read(arrayOfByte);
- if(i!=-1){
- localByteArrayOutputStream.write(arrayOfByte,0,i);
- }else{
- returnlocalByteArrayOutputStream.toByteArray();
- }
- }
- }
- }
红色部分其实就是最核心的工作:
1>、加密源程序Apk文件
[java] view plain copy- byte[]payloadArray=encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
[java] view plain copy
- //直接返回数据,读者可以添加自己加密方法
- privatestaticbyte[]encrpt(byte[]srcdata){
- for(inti=0;i<srcdata.length;i++){
- srcdata[i]=(byte)(0xFF^srcdata[i]);
- }
- returnsrcdata;
- }
(说明:这里是为了简单,所以就用了很简单的加密算法了,其实为了增加破解难度,我们应该使用更高效的加密算法,同事最好将加密操作放到native层去做)
2>、合并文件:将加密之后的Apk和原脱壳Dex进行合并
[java] view plain copy- intpayloadLen=payloadArray.length;
- intunShellDexLen=unShellDexArray.length;
- inttotalLen=payloadLen+unShellDexLen+4;//多出4字节是存放长度的。
- byte[]newdex=newbyte[totalLen];//申请了新的长度
- //添加解壳代码
- System.arraycopy(unShellDexArray,0,newdex,0,unShellDexLen);//先拷贝dex内容
- //添加加密后的解壳数据
- System.arraycopy(payloadArray,0,newdex,unShellDexLen,payloadLen);//再在dex内容后面拷贝apk的内容
3>、在文件的末尾追加源程序Apk的长度
[java] view plain copy- //添加解壳数据长度
- System.arraycopy(intToByte(payloadLen),0,newdex,totalLen-4,4);//最后4为长度
4>、修改新Dex文件的文件头信息:file_size; sha1; check_sum [java] view plain copy
- //修改DEXfilesize文件头
- fixFileSizeHeader(newdex);
- //修改DEXSHA1文件头
- fixSHA1Header(newdex);
- //修改DEXCheckSum文件头
- fixCheckSumHeader(newdex);
这里我们还需要两个输入文件:
1>、源Apk文件:ForceApkObj.apk
2>、脱壳程序的Dex文件:ForceApkObj.dex
那么第一个文件我们都知道,就是上面的源程序编译之后的Apk文件,那么第二个文件我们怎么得到呢?这个就是我们要讲到的第三个项目:脱壳程序项目,他是一个Android项目,我们在编译之后,能够得到他的classes.dex文件,然后修改一下名称就可。
3、脱壳项目:ReforceApk
在讲解这个项目之前,我们先来了解一下这个脱壳项目的工作:
1>、通过反射置换android.app.ActivityThread 中的mClassLoader为加载解密出APK的DexClassLoader,该DexClassLoader一方面加载了源程序、另一方面以原mClassLoader为父节点,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码。
关于这部分内容,不了解的同学可以看一下ActivityThread.java的源码:
或者直接看一下这篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/48104455
如何得到系统加载Apk的类加载器,然后我们怎么将加载进来的Apk运行起来等问题都在这篇文章中说到了。
2>、找到源程序的Application,通过反射建立并运行。
这里需要注意的是,我们现在是加载一个完整的Apk,让他运行起来,那么我们知道一个Apk运行的时候都是有一个Application对象的,这个也是一个程序运行之后的全局类。所以我们必须找到解密之后的源Apk的Application类,运行的他的onCreate方法,这样源Apk才开始他的运行生命周期。这里我们如何得到源Apk的Application的类呢?这个我们后面会说道。使用meta标签进行设置。
下面来看一下整体的流程图:
所以我们看到这里还需要一个核心的技术就是动态加载。关于动态加载技术,不了解的同学可以看这篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/48104581
下面来看一下代码:
[java] view plain copy
- packagecom.example.reforceapk;
- importjava.io.BufferedInputStream;
- importjava.io.ByteArrayInputStream;
- importjava.io.ByteArrayOutputStream;
- importjava.io.DataInputStream;
- importjava.io.File;
- importjava.io.FileInputStream;
- importjava.io.FileOutputStream;
- importjava.io.IOException;
- importjava.lang.ref.WeakReference;
- importjava.lang.reflect.Method;
- importjava.util.ArrayList;
- importjava.util.HashMap;
- importjava.util.Iterator;
- importjava.util.zip.ZipEntry;
- importjava.util.zip.ZipInputStream;
- importandroid.app.Application;
- importandroid.app.Instrumentation;
- importandroid.content.Context;
- importandroid.content.pm.ApplicationInfo;
- importandroid.content.pm.PackageManager;
- importandroid.content.pm.PackageManager.NameNotFoundException;
- importandroid.content.res.AssetManager;
- importandroid.content.res.Resources;
- importandroid.content.res.Resources.Theme;
- importandroid.os.Bundle;
- importandroid.util.ArrayMap;
- importandroid.util.Log;
- importdalvik.system.DexClassLoader;
- publicclassProxyApplicationextendsApplication{
- privatestaticfinalStringappkey="APPLICATION_CLASS_NAME";
- privateStringapkFileName;
- privateStringodexPath;
- privateStringlibPath;
- //这是context赋值
- @Override
- protectedvoidattachBaseContext(Contextbase){
- super.attachBaseContext(base);
- try{
- //创建两个文件夹payload_odex,payload_lib私有的,可写的文件目录
- Fileodex=this.getDir("payload_odex",MODE_PRIVATE);
- Filelibs=this.getDir("payload_lib",MODE_PRIVATE);
- odexPath=odex.getAbsolutePath();
- libPath=libs.getAbsolutePath();
- apkFileName=odex.getAbsolutePath()+"/payload.apk";
- FiledexFile=newFile(apkFileName);
- Log.i("demo","apksize:"+dexFile.length());
- if(!dexFile.exists())
- {
- dexFile.createNewFile();//在payload_odex文件夹内,创建payload.apk
- //读取程序classes.dex文件
- byte[]dexdata=this.readDexFileFromApk();
- //分离出解壳后的apk文件已用于动态加载
- this.splitPayLoadFromDex(dexdata);
- }
- //配置动态加载环境
- ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
- "android.app.ActivityThread","currentActivityThread",
- newClass[]{},newObject[]{});//获取主线程对象http://blog.csdn.net/myarrow/article/details/14223493
- StringpackageName=this.getPackageName();//当前apk的包名
- //下面两句不是太理解
- ArrayMapmPackages=(ArrayMap)RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mPackages");
- WeakReferencewr=(WeakReference)mPackages.get(packageName);
- //创建被加壳apk的DexClassLoader对象加载apk内的类和本地代码(c/c++代码)
- DexClassLoaderdLoader=newDexClassLoader(apkFileName,odexPath,
- libPath,(ClassLoader)RefInvoke.getFieldOjbect(
- "android.app.LoadedApk",wr.get(),"mClassLoader"));
- //base.getClassLoader();是不是就等同于(ClassLoader)RefInvoke.getFieldOjbect()?有空验证下//?
- //把当前进程的DexClassLoader设置成了被加壳apk的DexClassLoader----有点c++中进程环境的意思~~
- RefInvoke.setFieldOjbect("android.app.LoadedApk","mClassLoader",
- wr.get(),dLoader);
- Log.i("demo","classloader:"+dLoader);
- try{
- ObjectactObj=dLoader.loadClass("com.example.forceapkobj.MainActivity");
- Log.i("demo","actObj:"+actObj);
- }catch(Exceptione){
- Log.i("demo","activity:"+Log.getStackTraceString(e));
- }
- }catch(Exceptione){
- Log.i("demo","error:"+Log.getStackTraceString(e));
- e.printStackTrace();
- }
- }
- @Override
- publicvoidonCreate(){
- {
- //loadResources(apkFileName);
- Log.i("demo","onCreate");
- //如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
- StringappClassName=null;
- try{
- ApplicationInfoai=this.getPackageManager()
- .getApplicationInfo(this.getPackageName(),
- PackageManager.GET_META_DATA);
- Bundlebundle=ai.metaData;
- if(bundle!=null&&bundle.containsKey("APPLICATION_CLASS_NAME")){
- appClassName=bundle.getString("APPLICATION_CLASS_NAME");//className是配置在xml文件中的。
- }else{
- Log.i("demo","havenoapplicationclassname");
- return;
- }
- }catch(NameNotFoundExceptione){
- Log.i("demo","error:"+Log.getStackTraceString(e));
- e.printStackTrace();
- }
- //有值的话调用该Applicaiton
- ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
- "android.app.ActivityThread","currentActivityThread",
- newClass[]{},newObject[]{});
- ObjectmBoundApplication=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mBoundApplication");
- ObjectloadedApkInfo=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread$AppBindData",
- mBoundApplication,"info");
- //把当前进程的mApplication设置成了null
- RefInvoke.setFieldOjbect("android.app.LoadedApk","mApplication",
- loadedApkInfo,null);
- ObjectoldApplication=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mInitialApplication");
- //http://www.codeceo.com/article/android-context.html
- ArrayList<Application>mAllApplications=(ArrayList<Application>)RefInvoke
- .getFieldOjbect("android.app.ActivityThread",
- currentActivityThread,"mAllApplications");
- mAllApplications.remove(oldApplication);//删除oldApplication
- ApplicationInfoappinfo_In_LoadedApk=(ApplicationInfo)RefInvoke
- .getFieldOjbect("android.app.LoadedApk",loadedApkInfo,
- "mApplicationInfo");
- ApplicationInfoappinfo_In_AppBindData=(ApplicationInfo)RefInvoke
- .getFieldOjbect("android.app.ActivityThread$AppBindData",
- mBoundApplication,"appInfo");
- appinfo_In_LoadedApk.className=appClassName;
- appinfo_In_AppBindData.className=appClassName;
- Applicationapp=(Application)RefInvoke.invokeMethod(
- "android.app.LoadedApk","makeApplication",loadedApkInfo,
- newClass[]{boolean.class,Instrumentation.class},
- newObject[]{false,null});//执行makeApplication(false,null)
- RefInvoke.setFieldOjbect("android.app.ActivityThread",
- "mInitialApplication",currentActivityThread,app);
- ArrayMapmProviderMap=(ArrayMap)RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mProviderMap");
- Iteratorit=mProviderMap.values().iterator();
- while(it.hasNext()){
- ObjectproviderClientRecord=it.next();
- ObjectlocalProvider=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread$ProviderClientRecord",
- providerClientRecord,"mLocalProvider");
- RefInvoke.setFieldOjbect("android.content.ContentProvider",
- "mContext",localProvider,app);
- }
- Log.i("demo","app:"+app);
- app.onCreate();
- }
- }
- /**
- *释放被加壳的apk文件,so文件
- *@paramdata
- *@throwsIOException
- */
- privatevoidsplitPayLoadFromDex(byte[]apkdata)throwsIOException{
- intablen=apkdata.length;
- //取被加壳apk的长度这里的长度取值,对应加壳时长度的赋值都可以做些简化
- byte[]dexlen=newbyte[4];
- System.arraycopy(apkdata,ablen-4,dexlen,0,4);
- ByteArrayInputStreambais=newByteArrayInputStream(dexlen);
- DataInputStreamin=newDataInputStream(bais);
- intreadInt=in.readInt();
- System.out.println(Integer.toHexString(readInt));
- byte[]newdex=newbyte[readInt];
- //把被加壳apk内容拷贝到newdex中
- System.arraycopy(apkdata,ablen-4-readInt,newdex,0,readInt);
- //这里应该加上对于apk的解密操作,若加壳是加密处理的话
- //?
- //对源程序Apk进行解密
- newdex=decrypt(newdex);
- //写入apk文件
- Filefile=newFile(apkFileName);
- try{
- FileOutputStreamlocalFileOutputStream=newFileOutputStream(file);
- localFileOutputStream.write(newdex);
- localFileOutputStream.close();
- }catch(IOExceptionlocalIOException){
- thrownewRuntimeException(localIOException);
- }
- //分析被加壳的apk文件
- ZipInputStreamlocalZipInputStream=newZipInputStream(
- newBufferedInputStream(newFileInputStream(file)));
- while(true){
- ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
- if(localZipEntry==null){
- localZipInputStream.close();
- break;
- }
- //取出被加壳apk用到的so文件,放到libPath中(data/data/包名/payload_lib)
- Stringname=localZipEntry.getName();
- if(name.startsWith("lib/")&&name.endsWith(".so")){
- FilestoreFile=newFile(libPath+"/"
- +name.substring(name.lastIndexOf('/')));
- storeFile.createNewFile();
- FileOutputStreamfos=newFileOutputStream(storeFile);
- byte[]arrayOfByte=newbyte[1024];
- while(true){
- inti=localZipInputStream.read(arrayOfByte);
- if(i==-1)
- break;
- fos.write(arrayOfByte,0,i);
- }
- fos.flush();
- fos.close();
- }
- localZipInputStream.closeEntry();
- }
- localZipInputStream.close();
- }
- /**
- *从apk包里面获取dex文件内容(byte)
- *@return
- *@throwsIOException
- */
- privatebyte[]readDexFileFromApk()throwsIOException{
- ByteArrayOutputStreamdexByteArrayOutputStream=newByteArrayOutputStream();
- ZipInputStreamlocalZipInputStream=newZipInputStream(
- newBufferedInputStream(newFileInputStream(
- this.getApplicationInfo().sourceDir)));
- while(true){
- ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();
- if(localZipEntry==null){
- localZipInputStream.close();
- break;
- }
- if(localZipEntry.getName().equals("classes.dex")){
- byte[]arrayOfByte=newbyte[1024];
- while(true){
- inti=localZipInputStream.read(arrayOfByte);
- if(i==-1)
- break;
- dexByteArrayOutputStream.write(arrayOfByte,0,i);
- }
- }
- localZipInputStream.closeEntry();
- }
- localZipInputStream.close();
- returndexByteArrayOutputStream.toByteArray();
- }
- ////直接返回数据,读者可以添加自己解密方法
- privatebyte[]decrypt(byte[]srcdata){
- for(inti=0;i<srcdata.length;i++){
- srcdata[i]=(byte)(0xFF^srcdata[i]);
- }
- returnsrcdata;
- }
- //以下是加载资源
- protectedAssetManagermAssetManager;//资源管理器
- protectedResourcesmResources;//资源
- protectedThememTheme;//主题
- protectedvoidloadResources(StringdexPath){
- try{
- AssetManagerassetManager=AssetManager.class.newInstance();
- MethodaddAssetPath=assetManager.getClass().getMethod("addAssetPath",String.class);
- addAssetPath.invoke(assetManager,dexPath);
- mAssetManager=assetManager;
- }catch(Exceptione){
- Log.i("inject","loadResourceerror:"+Log.getStackTraceString(e));
- e.printStackTrace();
- }
- ResourcessuperRes=super.getResources();
- superRes.getDisplayMetrics();
- superRes.getConfiguration();
- mResources=newResources(mAssetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
- mTheme=mResources.newTheme();
- mTheme.setTo(super.getTheme());
- }
- @Override
- publicAssetManagergetAssets(){
- returnmAssetManager==null?super.getAssets():mAssetManager;
- }
- @Override
- publicResourcesgetResources(){
- returnmResources==null?super.getResources():mResources;
- }
- @Override
- publicThemegetTheme(){
- returnmTheme==null?super.getTheme():mTheme;
- }
- }
首先我们来看一下具体步骤的代码实现:
1>、得到脱壳Apk中的dex文件,然后从这个文件中得到源程序Apk.进行解密,然后加载
[java] view plain copy- //这是context赋值
- @Override
- protectedvoidattachBaseContext(Contextbase){
- super.attachBaseContext(base);
- try{
- //创建两个文件夹payload_odex,payload_lib私有的,可写的文件目录
- Fileodex=this.getDir("payload_odex",MODE_PRIVATE);
- Filelibs=this.getDir("payload_lib",MODE_PRIVATE);
- odexPath=odex.getAbsolutePath();
- libPath=libs.getAbsolutePath();
- apkFileName=odex.getAbsolutePath()+"/payload.apk";
- FiledexFile=newFile(apkFileName);
- Log.i("demo","apksize:"+dexFile.length());
- if(!dexFile.exists())
- {
- dexFile.createNewFile();//在payload_odex文件夹内,创建payload.apk
- //读取程序classes.dex文件
- byte[]dexdata=this.readDexFileFromApk();
- //分离出解壳后的apk文件已用于动态加载
- this.splitPayLoadFromDex(dexdata);
- }
- //配置动态加载环境
- ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
- "android.app.ActivityThread","currentActivityThread",
- newClass[]{},newObject[]{});//获取主线程对象http://blog.csdn.net/myarrow/article/details/14223493
- StringpackageName=this.getPackageName();//当前apk的包名
- //下面两句不是太理解
- ArrayMapmPackages=(ArrayMap)RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mPackages");
- WeakReferencewr=(WeakReference)mPackages.get(packageName);
- //创建被加壳apk的DexClassLoader对象加载apk内的类和本地代码(c/c++代码)
- DexClassLoaderdLoader=newDexClassLoader(apkFileName,odexPath,
- libPath,(ClassLoader)RefInvoke.getFieldOjbect(
- "android.app.LoadedApk",wr.get(),"mClassLoader"));
- //base.getClassLoader();是不是就等同于(ClassLoader)RefInvoke.getFieldOjbect()?有空验证下//?
- //把当前进程的DexClassLoader设置成了被加壳apk的DexClassLoader----有点c++中进程环境的意思~~
- RefInvoke.setFieldOjbect("android.app.LoadedApk","mClassLoader",
- wr.get(),dLoader);
- Log.i("demo","classloader:"+dLoader);
- try{
- ObjectactObj=dLoader.loadClass("com.example.forceapkobj.MainActivity");
- Log.i("demo","actObj:"+actObj);
- }catch(Exceptione){
- Log.i("demo","activity:"+Log.getStackTraceString(e));
- }
- }catch(Exceptione){
- Log.i("demo","error:"+Log.getStackTraceString(e));
- e.printStackTrace();
- }
- }
1)、从脱壳程序Apk中找到源程序Apk,并且进行解密操作
[java] view plain copy- //创建两个文件夹payload_odex,payload_lib私有的,可写的文件目录
- Fileodex=this.getDir("payload_odex",MODE_PRIVATE);
- Filelibs=this.getDir("payload_lib",MODE_PRIVATE);
- odexPath=odex.getAbsolutePath();
- libPath=libs.getAbsolutePath();
- apkFileName=odex.getAbsolutePath()+"/payload.apk";
- FiledexFile=newFile(apkFileName);
- Log.i("demo","apksize:"+dexFile.length());
- if(!dexFile.exists())
- {
- dexFile.createNewFile();//在payload_odex文件夹内,创建payload.apk
- //读取程序classes.dex文件
- byte[]dexdata=this.readDexFileFromApk();
- //分离出解壳后的apk文件已用于动态加载
- this.splitPayLoadFromDex(dexdata);
- }
A) 从Apk中获取到Dex文件
[java] view plain copy
- /**
- *从apk包里面获取dex文件内容(byte)
- *@return
- *@throwsIOException
- */
- privatebyte[]readDexFileFromApk()throwsIOException{
- ByteArrayOutputStreamdexByteArrayOutputStream=newByteArrayOutputStream();
- ZipInputStreamlocalZipInputStream=newZipInputStream(
- newBufferedInputStream(newFileInputStream(
- this.getApplicationInfo().sourceDir)));
- while(true){
- ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();
- if(localZipEntry==null){
- localZipInputStream.close();
- break;
- }
- if(localZipEntry.getName().equals("classes.dex")){
- byte[]arrayOfByte=newbyte[1024];
- while(true){
- inti=localZipInputStream.read(arrayOfByte);
- if(i==-1)
- break;
- dexByteArrayOutputStream.write(arrayOfByte,0,i);
- }
- }
- localZipInputStream.closeEntry();
- }
- localZipInputStream.close();
- returndexByteArrayOutputStream.toByteArray();
- }
B) 从脱壳Dex中得到源Apk文件
[java] view plain copy- /**
- *释放被加壳的apk文件,so文件
- *@paramdata
- *@throwsIOException
- */
- privatevoidsplitPayLoadFromDex(byte[]apkdata)throwsIOException{
- intablen=apkdata.length;
- //取被加壳apk的长度这里的长度取值,对应加壳时长度的赋值都可以做些简化
- byte[]dexlen=newbyte[4];
- System.arraycopy(apkdata,ablen-4,dexlen,0,4);
- ByteArrayInputStreambais=newByteArrayInputStream(dexlen);
- DataInputStreamin=newDataInputStream(bais);
- intreadInt=in.readInt();
- System.out.println(Integer.toHexString(readInt));
- byte[]newdex=newbyte[readInt];
- //把被加壳apk内容拷贝到newdex中
- System.arraycopy(apkdata,ablen-4-readInt,newdex,0,readInt);
- //这里应该加上对于apk的解密操作,若加壳是加密处理的话
- //?
- //对源程序Apk进行解密
- newdex=decrypt(newdex);
- //写入apk文件
- Filefile=newFile(apkFileName);
- try{
- FileOutputStreamlocalFileOutputStream=newFileOutputStream(file);
- localFileOutputStream.write(newdex);
- localFileOutputStream.close();
- }catch(IOExceptionlocalIOException){
- thrownewRuntimeException(localIOException);
- }
- //分析被加壳的apk文件
- ZipInputStreamlocalZipInputStream=newZipInputStream(
- newBufferedInputStream(newFileInputStream(file)));
- while(true){
- ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
- if(localZipEntry==null){
- localZipInputStream.close();
- break;
- }
- //取出被加壳apk用到的so文件,放到libPath中(data/data/包名/payload_lib)
- Stringname=localZipEntry.getName();
- if(name.startsWith("lib/")&&name.endsWith(".so")){
- FilestoreFile=newFile(libPath+"/"
- +name.substring(name.lastIndexOf('/')));
- storeFile.createNewFile();
- FileOutputStreamfos=newFileOutputStream(storeFile);
- byte[]arrayOfByte=newbyte[1024];
- while(true){
- inti=localZipInputStream.read(arrayOfByte);
- if(i==-1)
- break;
- fos.write(arrayOfByte,0,i);
- }
- fos.flush();
- fos.close();
- }
- localZipInputStream.closeEntry();
- }
- localZipInputStream.close();
- }
- ////直接返回数据,读者可以添加自己解密方法
- privatebyte[]decrypt(byte[]srcdata){
- for(inti=0;i<srcdata.length;i++){
- srcdata[i]=(byte)(0xFF^srcdata[i]);
- }
- returnsrcdata;
- }
2>、加载解密之后的源程序Apk
[java] view plain copy- //配置动态加载环境
- ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
- "android.app.ActivityThread","currentActivityThread",
- newClass[]{},newObject[]{});//获取主线程对象http://blog.csdn.net/myarrow/article/details/14223493
- StringpackageName=this.getPackageName();//当前apk的包名
- //下面两句不是太理解
- ArrayMapmPackages=(ArrayMap)RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mPackages");
- WeakReferencewr=(WeakReference)mPackages.get(packageName);
- //创建被加壳apk的DexClassLoader对象加载apk内的类和本地代码(c/c++代码)
- DexClassLoaderdLoader=newDexClassLoader(apkFileName,odexPath,
- libPath,(ClassLoader)RefInvoke.getFieldOjbect(
- "android.app.LoadedApk",wr.get(),"mClassLoader"));
- //base.getClassLoader();是不是就等同于(ClassLoader)RefInvoke.getFieldOjbect()?有空验证下//?
- //把当前进程的DexClassLoader设置成了被加壳apk的DexClassLoader----有点c++中进程环境的意思~~
- RefInvoke.setFieldOjbect("android.app.LoadedApk","mClassLoader",
- wr.get(),dLoader);
- Log.i("demo","classloader:"+dLoader);
- try{
- ObjectactObj=dLoader.loadClass("com.example.forceapkobj.MainActivity");
- Log.i("demo","actObj:"+actObj);
- }catch(Exceptione){
- Log.i("demo","activity:"+Log.getStackTraceString(e));
- }
- @Override
- publicvoidonCreate(){
- {
- //loadResources(apkFileName);
- Log.i("demo","onCreate");
- //如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
- StringappClassName=null;
- try{
- ApplicationInfoai=this.getPackageManager()
- .getApplicationInfo(this.getPackageName(),
- PackageManager.GET_META_DATA);
- Bundlebundle=ai.metaData;
- if(bundle!=null&&bundle.containsKey("APPLICATION_CLASS_NAME")){
- appClassName=bundle.getString("APPLICATION_CLASS_NAME");//className是配置在xml文件中的。
- }else{
- Log.i("demo","havenoapplicationclassname");
- return;
- }
- }catch(NameNotFoundExceptione){
- Log.i("demo","error:"+Log.getStackTraceString(e));
- e.printStackTrace();
- }
- //有值的话调用该Applicaiton
- ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
- "android.app.ActivityThread","currentActivityThread",
- newClass[]{},newObject[]{});
- ObjectmBoundApplication=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mBoundApplication");
- ObjectloadedApkInfo=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread$AppBindData",
- mBoundApplication,"info");
- //把当前进程的mApplication设置成了null
- RefInvoke.setFieldOjbect("android.app.LoadedApk","mApplication",
- loadedApkInfo,null);
- ObjectoldApplication=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mInitialApplication");
- //http://www.codeceo.com/article/android-context.html
- ArrayList<Application>mAllApplications=(ArrayList<Application>)RefInvoke
- .getFieldOjbect("android.app.ActivityThread",
- currentActivityThread,"mAllApplications");
- mAllApplications.remove(oldApplication);//删除oldApplication
- ApplicationInfoappinfo_In_LoadedApk=(ApplicationInfo)RefInvoke
- .getFieldOjbect("android.app.LoadedApk",loadedApkInfo,
- "mApplicationInfo");
- ApplicationInfoappinfo_In_AppBindData=(ApplicationInfo)RefInvoke
- .getFieldOjbect("android.app.ActivityThread$AppBindData",
- mBoundApplication,"appInfo");
- appinfo_In_LoadedApk.className=appClassName;
- appinfo_In_AppBindData.className=appClassName;
- Applicationapp=(Application)RefInvoke.invokeMethod(
- "android.app.LoadedApk","makeApplication",loadedApkInfo,
- newClass[]{boolean.class,Instrumentation.class},
- newObject[]{false,null});//执行makeApplication(false,null)
- RefInvoke.setFieldOjbect("android.app.ActivityThread",
- "mInitialApplication",currentActivityThread,app);
- ArrayMapmProviderMap=(ArrayMap)RefInvoke.getFieldOjbect(
- "android.app.ActivityThread",currentActivityThread,
- "mProviderMap");
- Iteratorit=mProviderMap.values().iterator();
- while(it.hasNext()){
- ObjectproviderClientRecord=it.next();
- ObjectlocalProvider=RefInvoke.getFieldOjbect(
- "android.app.ActivityThread$ProviderClientRecord",
- providerClientRecord,"mLocalProvider");
- RefInvoke.setFieldOjbect("android.content.ContentProvider",
- "mContext",localProvider,app);
- }
- Log.i("demo","app:"+app);
- app.onCreate();
- }
- }
下面来看一下AndoridManifest.xml文件中的内容:
在这里我们定义了源程序Apk的Application类名。
项目下载:http://download.csdn.net/detail/jiangwei0910410003/9102741
四、运行程序
那么到这里我们就介绍完了,这三个项目的内容,下面就来看看如何运行吧:
运行步骤:
第一步:得到源程序Apk文件和脱壳程序的Dex文件
运行源程序和脱壳程序项目,之后得到这两个文件(记得将classes.dex文件改名ForceApkObj.dex),然后使用加壳程序进行加壳:
这里的ForceApkObj.apk文件和ForceApkObj.dex文件是输入文件,输出的是classes.dex文件。
第二步:替换脱壳程序中的classes.dex文件
我们在第一步中得到加壳之后的classes.dex文件之后,并且我们在第一步运行脱壳项目的时候得到一个ReforceApk.apk文件,这时候我们使用解压缩软件进行替换:
第三步:我们在第二步的时候得到替换之后的ReforceApk.apk文件,这个文件因为被修改了,所以我们需要从新对他签名,不然运行也是报错的。
工具下载:http://download.csdn.net/detail/jiangwei0910410003/9102767
下载之后的工具需要用ReforeceApk.apk文件替换ReforceApk_des.apk文件,然后运行run.bat就可以得到签名之后的文件了。
run.bat文件的命令如下:
cd C:\Users\i\Desktop\forceapks
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk.apk jiangwei
del ReforceApk.apk
这里最主要的命令就是中间的一条签名的命令,关于命令的参数说明如下:
jarsigner -verbose -keystore 签名文件 -storepass 密码 -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA 签名后的文件 签名前的apk alias名称
eg:
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei
签名文件的密码:123456
alais的密码:123456
所以这里我们在得到ReforceApk.apk文件的时候,需要签名,关于Eclipse中如何签名一个Apk的话,这里就不多说了,自己google一下吧:
那么通过上面的三个步骤之后我们得到一个签名之后的最终文件:ReforceApk_des.apk
我们安装这个Apk,然后运行,效果如下:
看到运行结果的那一瞬间,我们是多么的开心,多么的有成就感,但是这个过程中遇到的问题,是可想而知的。
我们这个时候再去反编译一下源程序Apk(这个文件是我们脱壳出来的payload.apk,看ReforeceApk中的代码,就知道他的位置了)
发现dex文件格式是不正确的。说明我们的加固是成功的。
五、遇到的问题
1、研究的过程中遇到签名不正确的地方,开始的时候,直接替换dex文件之后,就直接运行了Apk,但是总是提示签名不正确。
2、运行的过程中说找不到源程序中的Activity,这个问题其实我在动态加载的那篇文章中说道了,我们需要在脱壳程序中的AndroidManifest.xml中什么一下源程序中的Activiity:
六、技术要点
1、对Dex文件格式的了解
2、动态加载技术的深入掌握
3、Application的执行流程的了解
4、如何从Apk中得到Dex文件
5、如何从新签名一个Apk程序
七、综合概述
我们通过上面的过程可以看到,关于Apk加固的工作还是挺复杂的,涉及到的东西也挺多的,下面就在来总结一下吧:
1、加壳程序
任务:对源程序Apk进行加密,合并脱壳程序的Dex文件 ,然后输入一个加壳之后的Dex文件
语言:任何语言都可以,不限于Java语言
技术点:对Dex文件格式的解析
2、脱壳程序
任务:获取源程序Apk,进行解密,然后动态加载进来,运行程序
语言:Android项目(Java)
技术点:如何从Apk中获取Dex文件,动态加载Apk,使用反射运行Application
八、总结
Android中的Apk反编译可能是每个开发都会经历的事,但是在反编译的过程中,对于源程序的开发者来说那是不公平的,那么Apk加固也是应运而生,但是即使是这样,我们也还是做不到那么的安全,现在网上也是有很多文章在解析梆梆加固的原理了。而且有人破解成功了,那么加固还不是怎么安全。最后一句话:逆向和加固是一个永不停息的战争。
更多相关文章
- android学习中遇到的问题
- OpenCore的代码结构
- android - adb命令的使用
- 基于 Android(安卓)NDK 的学习之旅-----HelloWorld (附源码)
- Android(安卓)adb的使用略解
- 谈谈Android的so
- Android系统上部署usb打印机
- android全格式多媒体播放器(一:ffmpeg移植)
- NPM 和webpack 的基础使用