为Android的apk应用程序文件加壳以防止反编译的教程
一、什么是加壳?
加壳是在二进制的程序中植入一段代码,在运行的时候优先取得程序的控制权,做一些额外的工作。大多数病毒就是基于此原理。
二、加壳作用
加壳的程序可以有效阻止对程序的反汇编分析,以达到它不可告人的目的。这种技术也常用来保护软件版权,防止被软件破解。
三、android dex文件加壳原理
pc平台现在已存在大量的标准的加壳和解壳工具,但是android作为新兴平台还未出现apk加壳工具。android dex文件大量使用引用给加壳带来了一定的难度,但是从理论上讲,android apk加壳也是可行的。
在这个过程中,牵扯到三个角色:
1、加壳程序:加密源程序为解壳数据、组装解壳程序和解壳数据
2、解壳程序:解密解壳数据,并运行时通过dexclassloader动态加载
3、源程序:需要加壳处理的被保护代码
根据解壳数据在解壳程序dex文件中的不同分布,本文将提出两种android dex加壳的实现方案。
解壳数据位于解壳程序文件尾部:该种方式简单实用,合并后的dex文件结构如下。
四、加壳程序工作流程:
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文件结构如下:
六、加壳程序工作流程:
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代码的实现对脱壳逻辑进行保护,以达到更好的反逆向分析效果。