当前位置: 移动技术网 > 移动技术>移动开发>Android > 为Android的apk应用程序文件加壳以防止反编译的教程

为Android的apk应用程序文件加壳以防止反编译的教程

2019年07月24日  | 移动技术网移动技术  | 我要评论

一、什么是加壳?
加壳是在二进制的程序中植入一段代码,在运行的时候优先取得程序的控制权,做一些额外的工作。大多数病毒就是基于此原理。

二、加壳作用
加壳的程序可以有效阻止对程序的反汇编分析,以达到它不可告人的目的。这种技术也常用来保护软件版权,防止被软件破解。

三、android dex文件加壳原理
pc平台现在已存在大量的标准的加壳和解壳工具,但是android作为新兴平台还未出现apk加壳工具。android dex文件大量使用引用给加壳带来了一定的难度,但是从理论上讲,android apk加壳也是可行的。
在这个过程中,牵扯到三个角色:
1、加壳程序:加密源程序为解壳数据、组装解壳程序和解壳数据
2、解壳程序:解密解壳数据,并运行时通过dexclassloader动态加载
3、源程序:需要加壳处理的被保护代码
根据解壳数据在解壳程序dex文件中的不同分布,本文将提出两种android dex加壳的实现方案。

解壳数据位于解壳程序文件尾部:该种方式简单实用,合并后的dex文件结构如下。

2016428142304174.png (242×541)

四、加壳程序工作流程:
1、加密源程序apk文件为解壳数据
2、把解壳数据写入解壳程序dex文件末尾,并在文件尾部添加解壳数据的大小。
3、修改解壳程序dex头中checksum、signature 和file_size头信息。
4、修改源程序androidmainfest.xml文件并覆盖解壳程序androidmainfest.xml文件。

五、解壳dex程序工作流程:
1、读取dex文件末尾数据获取借壳数据长度。
2、从dex文件读取解壳数据,解密解壳数据。以文件形式保存解密数据到a.apk文件
3、通过dexclassloader动态加载a.apk。

解壳数据位于解壳程序文件头
该种方式相对比较复杂, 合并后dex文件结构如下:

2016428142331268.png (241×526)

六、加壳程序工作流程:
1、加密源程序apk文件为解壳数据
2、计算解壳数据长度,并添加该长度到解壳dex文件头末尾,并继续解壳数据到文件头末尾。
(插入数据的位置为0x70处)
3、修改解壳程序dex头中checksum、signature、file_size、header_size、string_ids_off、type_ids_off、proto_ids_off、field_ids_off、
method_ids_off、class_defs_off和data_off相关项。  分析map_off 数据,修改相关的数据偏移量。 
4、修改源程序androidmainfest.xml文件并覆盖解壳程序androidmainfest.xml文件。

七、加壳程序流程及代码实现
1、加密源程序apk为解壳数据
2、把解壳数据写入解壳程序dex文件末尾,并在文件尾部添加解壳数据的大小。
3、修改解壳程序dex头中checksum、signature 和file_size头信息。

代码实现如下:

package com.android.dexshell; 
import java.io.bytearrayoutputstream; 
import java.io.file; 
import java.io.fileinputstream; 
import java.io.fileoutputstream; 
import java.io.ioexception; 
import java.security.messagedigest; 
import java.security.nosuchalgorithmexception; 
import java.util.zip.adler32; 
 
public class dexshelltool { 
 /** 
  * @param args 
  */ 
 public static void main(string[] args) { 
  // todo auto-generated method stub 
  try { 
   file payloadsrcfile = new file("g:/payload.apk"); 
   file unshelldexfile = new file("g:/unshell.dex"); 
   byte[] payloadarray = encrpt(readfilebytes(payloadsrcfile)); 
   byte[] unshelldexarray = readfilebytes(unshelldexfile); 
   int payloadlen = payloadarray.length; 
   int unshelldexlen = unshelldexarray.length; 
   int totallen = payloadlen + unshelldexlen +4; 
   byte[] newdex = new byte[totallen]; 
   //添加解壳代码 
   system.arraycopy(unshelldexarray, 0, newdex, 0, unshelldexlen); 
   //添加加密后的解壳数据 
   system.arraycopy(payloadarray, 0, newdex, unshelldexlen, 
     payloadlen); 
   //添加解壳数据长度 
   system.arraycopy(inttobyte(payloadlen), 0, newdex, totallen-4, 4); 
      //修改dex file size文件头 
   fixfilesizeheader(newdex); 
   //修改dex sha1 文件头 
   fixsha1header(newdex); 
   //修改dex checksum文件头 
   fixchecksumheader(newdex); 
 
 
   string str = "g:/classes.dex"; 
   file file = new file(str); 
   if (!file.exists()) { 
    file.createnewfile(); 
   } 
    
   fileoutputstream localfileoutputstream = new fileoutputstream(str); 
   localfileoutputstream.write(newdex); 
   localfileoutputstream.flush(); 
   localfileoutputstream.close(); 
 
 
  } catch (exception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } 
 } 
  
 //直接返回数据,读者可以添加自己加密方法 
 private static byte[] encrpt(byte[] srcdata){ 
  return srcdata; 
 } 
 
 
 private static void fixchecksumheader(byte[] dexbytes) { 
  adler32 adler = new adler32(); 
  adler.update(dexbytes, 12, dexbytes.length - 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.out.println(integer.tohexstring(newcs[i])); 
  } 
  system.arraycopy(recs, 0, dexbytes, 8, 4); 
  system.out.println(long.tohexstring(value)); 
  system.out.println(); 
 } 
 
 
 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; 
 } 
 
 
 private static void fixsha1header(byte[] dexbytes) 
   throws nosuchalgorithmexception { 
  messagedigest md = messagedigest.getinstance("sha-1"); 
  md.update(dexbytes, 32, dexbytes.length - 32); 
  byte[] newdt = md.digest(); 
  system.arraycopy(newdt, 0, dexbytes, 12, 20); 
  string hexstr = ""; 
  for (int i = 0; i < newdt.length; i++) { 
   hexstr += integer.tostring((newdt[i] & 0xff) + 0x100, 16) 
     .substring(1); 
  } 
  system.out.println(hexstr); 
 } 
 
 
 private static void fixfilesizeheader(byte[] dexbytes) { 
 
 
  byte[] newfs = inttobyte(dexbytes.length); 
  system.out.println(integer.tohexstring(dexbytes.length)); 
  byte[] refs = new byte[4]; 
  for (int i = 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); 
 } 
 
 
 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(); 
   } 
  } 
 } 
 
 
} 


八、解壳程序流程及代码实现
在解壳程序的开发过程中需要解决如下几个关键的技术问题:
1.解壳代码如何能够第一时间执行?
android程序由不同的组件构成,系统在有需要的时候启动程序组件。因此解壳程序必须在android系统启动组件之前运行,完成对解壳数据的解壳及apk文件的动态加载,否则会使程序出现加载类失败的异常。
android开发者都知道applicaiton做为整个应用的上下文,会被系统第一时间调用,这也是应用开发者程序代码的第一执行点。因此通过对androidmainfest.xml的application的配置可以实现解壳代码第一时间运行。

<application 
 android:icon="@drawable/ic_launcher" 
 android:label="@string/app_name" 
 android:theme="@style/apptheme" android:name=" 

</application> 

2.如何替换回源程序原有的application?
当在androidmainfest.xml文件配置为解壳代码的application时。源程序原有的applicaiton将被替换,为了不影响源程序代码逻辑,我们需要              在解壳代码运行完成后,替换回源程序原有的application对象。我们通过在androidmainfest.xml文件中配置原有applicaiton类信息来达到我们              的目的。解壳程序要在运行完毕后通过创建配置的application对象,并通过反射修改回原application。

<application 
 android:icon="@drawable/ic_launcher" 
 android:label="@string/app_name" 
 android:theme="@style/apptheme" android:name=" 
</application> 

3.如何通过dexclassloader实现对apk代码的动态加载。
我们知道dexclassloader加载的类是没有组件生命周期的,也就是说即使dexclassloader通过对apk的动态加载完成了对组件类的加载,当系统启动该组件时,还会出现加载类失败的异常。为什么组件类被动态加载入虚拟机,但系统却出现加载类失败呢?
通过查看android源代码我们知道组件类的加载是由另一个classloader来完成的,dexclassloader和系统组件classloader并不存在关系,系统组件classloader当然找不到由dexclassloader加载的类,如果把系统组件classloader的parent修改成dexclassloader,我们就可以实现对apk代码的动态加载。

4.如何使解壳后的apk资源文件被代码动态引用。
代码默认引用的资源文件在最外层的解壳程序中,因此我们要增加系统的资源加载路径来实现对借壳后apk文件资源的加载。

解壳实现代码:
 

package com.android.dexunshell; 
 
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.util.arraylist; 
import java.util.hashmap; 
import java.util.iterator; 
import java.util.zip.zipentry; 
import java.util.zip.zipinputstream; 
 
import dalvik.system.dexclassloader; 
import android.app.application; 
import android.content.pm.applicationinfo; 
import android.content.pm.packagemanager; 
import android.content.pm.packagemanager.namenotfoundexception; 
import android.os.bundle; 
 
public class proxyapplication extends application { 
 
 private static final string appkey = "application_class_name"; 
 private string apkfilename; 
 private string odexpath; 
 private string libpath; 
 @override 
 public void oncreate() { 
  // todo auto-generated method stub 
  super.oncreate(); 
  try { 
   file odex = this.getdir("payload_odex", mode_private); 
   file libs = this.getdir("payload_lib", mode_private); 
   odexpath = odex.getabsolutepath(); 
   libpath = libs.getabsolutepath(); 
   apkfilename = odex.getabsolutepath()+"/payload.apk"; 
   file dexfile = new file(apkfilename); 
   if(!dexfile.exists()) 
    dexfile.createnewfile(); 
   //读取程序classes.dex文件 
   byte[] dexdata = this.readdexfilefromapk(); 
   //分离出解壳后的apk文件已用于动态加载 
   this.splitpayloadfromdex(dexdata); 
   //配置动态加载环境 
   this.configapplicationenv(); 
    
  } catch (exception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } 
 } 
  
 private void configapplicationenv() throws namenotfoundexception, illegalaccessexception, instantiationexception, classnotfoundexception, ioexception{ 
 
  object currentactivitythread = refinvoke.invokestaticmethod("android.app.activitythread", "currentactivitythread", new class[]{}, new object[]{}); 
  hashmap mpackages = (hashmap)refinvoke.getfieldojbect("android.app.activitythread", currentactivitythread, "mpackages"); 
  //替换组件类加载器为dexclassloader,已使动态加载代码具有组件生命周期 
  weakreference wr = (weakreference) mpackages.get(this.getpackagename()); 
  dexclassloader dloader = new dexclassloader(apkfilename, 
    odexpath, libpath, (classloader) refinvoke.getfieldojbect("android.app.loadedapk", wr.get(), "mclassloader")); 
  refinvoke.setfieldojbect("android.app.loadedapk", "mclassloader", wr.get(), dloader); 
 
  //如果源应用配置有appliction对象,则替换为源应用applicaiton,以便不影响源程序逻辑。 
  applicationinfo appinfo = this.getpackagemanager().getapplicationinfo(this.getpackagename(),packagemanager.get_meta_data); 
  bundle bundle = appinfo.metadata; 
  if(bundle != null && bundle.containskey(appkey)){ 
    
   string appclassname = bundle.getstring(appkey); 
   application app = (application)dloader.loadclass(appclassname).newinstance(); 
   refinvoke.setfieldojbect("android.app.contextimpl", "moutercontext", this.getbasecontext(), app); 
   refinvoke.setfieldojbect("android.content.contextwrapper", "mbase", app, this.getbasecontext()); 
   object mboundapplication = refinvoke.getfieldojbect("android.app.activitythread", currentactivitythread, "mboundapplication"); 
   object info = refinvoke.getfieldojbect("android.app.activitythread$appbinddata", mboundapplication, "info"); 
   refinvoke.setfieldojbect("android.app.loadedapk", "mapplication", info, app); 
   object oldapplication = refinvoke.getfieldojbect("android.app.activitythread", currentactivitythread, "minitialapplication"); 
   refinvoke.setfieldojbect("android.app.activitythread", "minitialapplication", currentactivitythread, app); 
   arraylist<application> mallapplications = (arraylist<application>)refinvoke.getfieldojbect("android.app.activitythread", currentactivitythread, "mallapplications"); 
   mallapplications.remove(oldapplication); 
   mallapplications.add(app); 
   hashmap mprovidermap = (hashmap) 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.providerclientrecord", providerclientrecord, "mlocalprovider"); 
    refinvoke.setfieldojbect("android.content.contentprovider", "mcontext", localprovider, app); 
   } 
   refinvoke.invokemethod(appclassname, "oncreate", app, new class[]{}, new object[]{}); 
  } 
 } 
  
 private void splitpayloadfromdex(byte[] data) throws ioexception{ 
  byte[] apkdata = decrypt(data); 
  int ablen = apkdata.length; 
  byte[] dexlen = new byte[4]; 
  system.arraycopy(apkdata, ablen - 4, dexlen, 0, 4); 
  bytearrayinputstream bais = new bytearrayinputstream(dexlen); 
  datainputstream in = new datainputstream(bais); 
  int readint = in.readint(); 
  system.out.println(integer.tohexstring(readint)); 
  byte[] newdex = new byte[readint]; 
  system.arraycopy(apkdata, ablen - 4 - readint, newdex, 0, readint); 
  file file = new file(apkfilename); 
  try { 
   fileoutputstream localfileoutputstream = new fileoutputstream(file); 
   localfileoutputstream.write(newdex); 
   localfileoutputstream.close(); 
 
  } catch (ioexception localioexception) { 
   throw new runtimeexception(localioexception); 
  } 
   
  zipinputstream localzipinputstream = new zipinputstream( 
    new bufferedinputstream(new fileinputstream(file))); 
  while (true) { 
   zipentry localzipentry = localzipinputstream.getnextentry(); 
   if (localzipentry == null) { 
    localzipinputstream.close(); 
    break; 
   } 
   string name = localzipentry.getname(); 
   if (name.startswith("lib/") && name.endswith(".so")) { 
    file storefile = new file(libpath+"/"+name.substring(name.lastindexof('/'))); 
    storefile.createnewfile(); 
    fileoutputstream fos = new fileoutputstream(storefile); 
    byte[] arrayofbyte = new byte[1024]; 
    while (true) { 
     int i = localzipinputstream.read(arrayofbyte); 
     if (i == -1) 
      break; 
     fos.write(arrayofbyte, 0, i); 
    } 
    fos.flush(); 
    fos.close();  
   } 
   localzipinputstream.closeentry(); 
  } 
  localzipinputstream.close();  
   
 } 
  
 private byte[] readdexfilefromapk() throws ioexception { 
  bytearrayoutputstream dexbytearrayoutputstream = new bytearrayoutputstream(); 
  zipinputstream localzipinputstream = new zipinputstream( 
    new bufferedinputstream(new fileinputstream(this.getapplicationinfo().sourcedir))); 
  while (true) { 
   zipentry localzipentry = localzipinputstream.getnextentry(); 
   if (localzipentry == null) { 
    localzipinputstream.close(); 
    break; 
   } 
   if (localzipentry.getname().equals("classes.dex")) { 
    byte[] arrayofbyte = new byte[1024]; 
    while (true) { 
     int i = localzipinputstream.read(arrayofbyte); 
     if (i == -1) 
      break; 
     dexbytearrayoutputstream.write(arrayofbyte, 0, i); 
    } 
   } 
   localzipinputstream.closeentry(); 
  } 
  localzipinputstream.close(); 
  return dexbytearrayoutputstream.tobytearray(); 
 } 
  
 ////直接返回数据,读者可以添加自己解密方法 
 private byte[] decrypt(byte[] data){ 
  return data; 
 } 
} 

refinvoke为反射调用工具类:

package com.android.dexunshell; 
 
import java.lang.reflect.field; 
import java.lang.reflect.invocationtargetexception; 
import java.lang.reflect.method; 
 
public class refinvoke { 
  
 public static object invokestaticmethod(string class_name, string method_name, class[] paretyple, object[] parevaules){ 
   
  try { 
   class obj_class = class.forname(class_name); 
   method method = obj_class.getmethod(method_name,paretyple); 
   return method.invoke(null, parevaules); 
  } catch (securityexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalargumentexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalaccessexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (nosuchmethodexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (invocationtargetexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (classnotfoundexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } 
  return null; 
   
 } 
  
 public static object invokemethod(string class_name, string method_name, object obj ,class[] paretyple, object[] parevaules){ 
   
  try { 
   class obj_class = class.forname(class_name); 
   method method = obj_class.getmethod(method_name,paretyple); 
   return method.invoke(obj, parevaules); 
  } catch (securityexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalargumentexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalaccessexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (nosuchmethodexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (invocationtargetexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (classnotfoundexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } 
  return null; 
   
 } 
  
 public static object getfieldojbect(string class_name,object obj, string filedname){ 
  try { 
   class obj_class = class.forname(class_name); 
   field field = obj_class.getdeclaredfield(filedname); 
   field.setaccessible(true); 
   return field.get(obj); 
  } catch (securityexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (nosuchfieldexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalargumentexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalaccessexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (classnotfoundexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } 
  return null; 
   
 } 
  
 public static object getstaticfieldojbect(string class_name, string filedname){ 
   
  try { 
   class obj_class = class.forname(class_name); 
   field field = obj_class.getdeclaredfield(filedname); 
   field.setaccessible(true); 
   return field.get(null); 
  } catch (securityexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (nosuchfieldexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalargumentexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalaccessexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (classnotfoundexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } 
  return null; 
   
 } 
  
 public static void setfieldojbect(string classname, string filedname, object obj, object filedvaule){ 
  try { 
   class obj_class = class.forname(classname); 
   field field = obj_class.getdeclaredfield(filedname); 
   field.setaccessible(true); 
   field.set(obj, filedvaule); 
  } catch (securityexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (nosuchfieldexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalargumentexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalaccessexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (classnotfoundexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  }  
 } 
  
 public static void setstaticojbect(string class_name, string filedname, object filedvaule){ 
  try { 
   class obj_class = class.forname(class_name); 
   field field = obj_class.getdeclaredfield(filedname); 
   field.setaccessible(true); 
   field.set(null, filedvaule); 
  } catch (securityexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (nosuchfieldexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalargumentexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (illegalaccessexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  } catch (classnotfoundexception e) { 
   // todo auto-generated catch block 
   e.printstacktrace(); 
  }   
 } 
 
} 


九、总结
本文代码基本实现了apk文件的加壳及脱壳原理,该代码作为实验代码还有诸多地方需要改进。比如:
1、加壳数据的加密算法的添加。
2、脱壳代码由java语言实现,可通过c代码的实现对脱壳逻辑进行保护,以达到更好的反逆向分析效果。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网