由于Android应用程序中的大部分代码使用Java语言编写,而Java语言又比较容易进
行逆向,所以Android应用程序的自我保护具有一定的意义。本文总结了Android中可以使
用的一些APK自我保护的技术,大部分都经过实际的代码测试。

Dex文件结构

classes.dex文件是Android系统运行于DalvikVirtualMachine上的可执行文件,也是
Android应用程序的核心所在,所以我们首先来看下DEX文件的结构,这样能够更好的理解
后续的分析,需要更加详细的信息,可以参考 Google关于Dex的技术文档
从Java源文件(当然Android也支持JNI的调用方式)到生成Dex文件的基本映射关系
如图1所示,Java源文件通过Java编译器生成class文件,再通过dx工具转换为classes.dex
文件。Dex文件从整体上来看是个索引的结构,类名、方法名、字段名等信息都存储在常量
池中,这样能够充分减少存储空间,一个Dex文件的基本结构如图2所示,相关结构声明定
义在DexFile.h中,在AOSP中的路径为/dalvik/libdex/DexFile.h。
APK 的自我保护_第1张图片
图1Java源文件生成Dex文件的映射关系

header:Dex文件头,包含magic字段、adler32校验值、SHA-1哈希值、string_ids的个数
APK 的自我保护_第2张图片
图2Dex文件基本结构

以及偏移地址等。Dex文件头结构固定,占用0x70个字节,定义如下所示。

代码:
1.structDexHeader{2.u1magic[8];/*includesversionnumber*/3.u4checksum;/*adler32checksum*/4.u1signature[kSHA1DigestLen];/*SHA-1hash*/5.u4fileSize;/*lengthofentirefile*/6.u4headerSize;/*offsettostartofnextsection*/7.u4endianTag;8.u4linkSize;9.u4linkOff;10.u4mapOff;11.u4stringIdsSize;12.u4stringIdsOff;13.u4typeIdsSize;14.u4typeIdsOff;15.u4protoIdsSize;16.u4protoIdsOff;17.u4fieldIdsSize;18.u4fieldIdsOff;19.u4methodIdsSize;20.u4methodIdsOff;21.u4classDefsSize;22.u4classDefsOff;23.u4dataSize;24.u4dataOff;25.};
DexStringId:定义了字符串数据的偏移,stringDataOff指向字符串数据;
代码:
1.structDexStringId{2.u4stringDataOff;/*fileoffsettostring_data_item*/3.};
DexTypeId:表示应用程序代码中使用到的具体类型,如整型、字符串等,在Dalvik字节
码中表示为I、Ljava/lang/String;,descriptorIdx指向DexStringId列表的索引;
代码:
1.structDexTypeId{2.u4descriptorIdx;/*indexintostringIdslistfortypedescriptor*/3.};
DexProtoId:表示方法声明的结构体,shortyIdx是方法声明字符串,格式为返回值类型
后紧跟参数列表类型,如方法声明为VI,表示返回值为V(空,无返回值),参数为I
(整型),所有的引用类型用L表示;returnTypeIdx指向DexTypeId列表的索引,表示返
回值类型;parametersOff指向DexTypeList的偏移,表示参数列表类型;
代码:
1.structDexProtoId{2.u2classIdx;/*indexintotypeIdslistfordefiningclass*/3.u2typeIdx;/*indexintotypeIdsforfieldtype*/4.u4nameIdx;/*indexintostringIdsforfieldname*/5.};
DexFieldId:表示代码中的字段,classIdx指向DexTypeId列表索引,表示字段所属的类;
typeIdx表示字段类型,nameIdx指向DexStringId列表索引,表示字段名;
代码:
1.structDexFieldId{2.u2classIdx;/*indexintotypeIdslistfordefiningclass*/3.u2typeIdx;/*indexintotypeIdsforfieldtype*/4.u4nameIdx;/*indexintostringIdsforfieldname*/5.};
DexMethodId:表示代码中使用的方法,classIdx表示方法所属的类,protoIdx指向
DexProtoId列表索引,表示方法原型,nameIdx表示方法名;
代码:
1.structDexMethodId{2.u2classIdx;/*indexintotypeIdslistfordefiningclass*/3.u2protoIdx;/*indexintoprotoIdsformethodprototype*/4.u4nameIdx;/*indexintostringIdsformethodname*/5.};
DexClassDef:该结构相对要复杂一些,定义了代码中的使用的类,以及相关的代码指令。
代码:
1.structDexClassDef{2.u4classIdx;/*indexintotypeIdsforthisclass*/3.u4accessFlags;4.u4superclassIdx;/*indexintotypeIdsforsuperclass*/5.u4interfacesOff;/*fileoffsettoDexTypeList*/6.u4sourceFileIdx;/*indexintostringIdsforsourcefilename*/7.u4annotationsOff;/*fileoffsettoannotations_directory_item*/8.u4classDataOff;/*fileoffsettoclass_data_item*/9.u4staticValuesOff;/*fileoffsettoDexEncodedArray*/10.};
classIdx指向DexTypeId列表索引,表示该类的类型;accessFlags是类的访问标志,如
public,private,static等;superclassIdx表示父类的类型;interfacesOff指向一个DexTypeList
的偏移值,因为Java中可以实现多个接口,这里使用列表也就不难理解了;sourceFileIdx
指向DexStringIdx列表的索引,表示类所在的源文件名称;annotationsOff指向注解目录
结构;classDataOff指向DexClassData结构,表示类的数据部分;staticValuesOff表示类
中的静态数据。

DexClassData结构体定义在DexClass.h文件中,路径为/dalvik/libdex/DexClass.h,声明如
下,header中包含静态字段个数,实例字段个数,直接方法(通过类直接访问的方法)
个数,虚方法(通过类实例访问的方法)个数;
代码:
1.structDexClassData{2.DexClassDataHeaderheader;3.DexField*staticFields;4.DexField*instanceFields;5.DexMethod*directMethods;6.DexMethod*virtualMethods;7.};
DexField表示字段的类型和访问标志,fieldIdx指向DexFieldId;
代码:
1.structDexField{2.u4fieldIdx;/*indextoafield_id_item*/3.u4accessFlags;4.};
DexMethod结构描述了方法的原型、名称、访问标志以及代码指令的偏移地址,methodIdx
指向DexMethodId索引,需要注意的是在 Google的Dex文件文档中对此的定义:
indexintothemethod_idslistfortheidentityofthismethod(includesthenameanddescriptor),
representedasadifferencefromtheindexofpreviouselementinthelist.Theindexofthefirst
elementinalistisrepresenteddirectly.


注意红色字体部分,表示的是在Dex文件中,methodIdx是相对于前一个DexMethod中
的methodIdx的增量,例如如果一个类中有两个directMethods,第一个directMethod的
methodIdx值为0x13,表示指向索引为0x13的methodIdx,那么第二个directMethod的
methodIdx的值是相对于前一个值的增量,例如0x01,表示指向索引为0x14的methodIdx;
accessFlags为方法的访问标志,codeOff表示指令代码的偏移地址;
代码:
1.structDexMethod{2.u4methodIdx;/*indextoamethod_id_item*/3.u4accessFlags;4.u4codeOff;/*fileoffsettoacode_item*/5.};
DexCode的结构体声明如下。
代码:
1.structDexCode{2.u2registersSize;3.u2insSize;4.u2outsSize;5.u2triesSize;6.u4debugInfoOff;/*fileoffsettodebuginfostream*/7.u4insnsSize;/*sizeoftheinsnsarray,inu2units*/8.u2insns[1];9./*followedbyoptionalu2padding*/10./*followedbytry_item[triesSize]*/11./*followedbyuleb128handlersSize*/12./*followedbycatch_handler_item[handlersSize]*/13.};
需要注意的是,在DexClass.h中,所有的u4类型,实际上是uleb128类型。每个uleb128
类型是leb128的无符号类型,每个leb128类型的数据包含1-5个字节,表示一个32bit
的数值。每个字节只有7位有效,最高一位用来表示是否需要使用到下一个字节,比如
如果第一个字节最高位为1,表示还需要使用到第2个字节,如果第二个字节的最高位
为1,表示会使用到第3个字节,以此类推,最多5个字节。对于一个2个字节的leb128
类型数据,其结构如图3所示。
名称:  3.png查看次数: 3文件大小:  19.2 KB
图3两字节的leb128类型数据格式

Dex中方法的隐藏


此部分内容可参考 PlayingHideandSeekwithDalvikExecutables

前文分析了Dex文件的结构,根据Dex的文件结构,可以实现对Dex中特定方法的隐藏,
这样在使用 baksamli或者 apktool工具对classes.dex文件进行反汇编时,无法发现隐藏的方
法,不过会有特定的现象发生,其实也是比较容易检测出来的。

在Dex文件格式分析中关于method的结构体是DexMethod,如果将methodIdx的值指向
另一个method,同时修改相应的代码偏移量codeOff(accessFlags一般不需要修改),修改
后续相应的methodIdx,则可以实现特定方法的隐藏。对Dex文件修改后需要重新计算Dex
文件的SHA1值以及校验值,用来更新Dex文件。

隐藏方法的步骤如下:
修改Dex文件中需要隐藏方法的DexMethod结构体,如图4所示,图中隐藏了方法
B。具体包括:
  • 将DexMethod的methodIdx值设为0x0,相当于将原先的方法指向了前一个方
    法;
  • 访问标志符accessFlags一般不需要修改,在Dex文件格式里,directMethods
    和virtualMethods是分开的;
  • 将codeOffset设置为前一个方法的代码偏移地址。
  • 更新需隐藏方法的下一个方法的methodIdx,可以使用公式:
    next_method_idx=original_next_method_idx+original_method_idx
  • 重新计算Dex的SHA1哈希值和Adler校验值,并用以更新DexHeader,可以使用
    DexFixer修复classes.dex文件;
  • 重新打包生成APK文件:
    • 将APK解压缩,提取其中出META-INF文件夹之外的所有文件;
    • 压缩成Zip格式文件;
    • 使用jarsigner或者其他工具对生成的Zip文件签名,后缀名修改成.apk。

    隐藏的方法仍然需要在程序中进行调用,调用隐藏方法的步骤如下:
  • 使用反射调用android.content.res.AssetManager.openNonAsset方法打开当前应用程
    序的classes.dex文件,将数据保存到内存中;还可以通过调用
    Context.getPackageCodePath()来获得当前应用程序对应的apk文件的路径,利用此
    路径构造ZipFile对象,进而获取classes.dex的ZipEntry,利用ZipFile的
    getInputStream(ZipEntry)方法获取classes.dex的数据流,核心代码如下所示;
代码:
1.StringapkPath=this.getPackageCodePath();2.ZipFileapkfile=newZipFile(apkPath);3.ZipEntrydexentry=zipfile.getEntry("classes.dex");4.InputStreamdexstream=zipfile.getInputStream(dexentry);
  • 修复Dex文件,将之前隐藏方法的DexMethod结构体恢复;
  • 将修复后的Dex数据使用类加载器重新加载;
  • 搜索被隐藏的方法;
  • 调用被隐藏的方法。

需要注意的是,方法在Dex文件中是按方法名的字典序排序的,所以需要隐藏的方法如
果是该类中所有方法排序第一个的话,那么methodIdx值是个绝对值,如果要隐藏的话就不
是很方便,所以建议可以写个无用的方法,其方法名排序为第一个,让需要隐藏的方法重新
指向该方法。

使用修改methodIdx的方法,让其指向另一个DexMethodId的结构体,如果使用baksmali
进行反汇编,则会发现在一个类中有两个完全相同的函数。

那有没有更加隐蔽的手段来隐藏一个方法了?考虑到在DexClassData结构体中的
DexClassDataHeader头部,其中directMethodsSize和virtualMethodsSize分别表示直接方法个
数和虚方法个数,因此如果希望隐藏某个方法,可以通过将相应的directMethodsSize或
virtualMethodsSize减1,同时将表示该需要隐藏方法的DexMethod结构体中的数据全部修改
为0,这样就可以将该方法隐藏起来,使用baksmali反汇编时,不会显示出该方法的反汇编
代码,具体可以参考 Hashdays2012AndroidChanllenge

当然,上述这两种隐藏方法,都没能隐藏掉DexMethodId结构体,这个结构体中包含了
方法所属的类名、原型声明以及方法名,所以可以通过对比DexMethodId的个数和DexMethod
结构体的个数来判断是否存在方法隐藏的问题。

Dex完整性校验

classes.dex在Android系统上基本负责完成所有的逻辑业务,因此很多针对Android应用
程序的篡改都是针对classes.dex文件的。在APK的自我保护上,也可以考虑对classes.dex
文件进行完整性校验,简单的可以通过CRC校验完成,也可以检查Hash值。由于只是检查
classes.dex,所以可以将CRC值存储在string资源文件中,当然也可以放在自己的服务器上,
通过运行时从服务器获取校验值。基本步骤如下:
  • 首先在代码中完成校验值比对的逻辑,此部分代码后续不能再改变,否则CRC值
    会发生变化;
  • 从生成的APK文件中提取出classes.dex文件,计算其CRC值,其他hash值类似;
  • 将计算出的值放入strings.xml文件中。
核心代码如下:
代码:
1.StringapkPath=this.getPackageCodePath();2.LongdexCrc=Long.parseLong(this.getString(R.string.dex_crc));3.try{4.ZipFilezipfile=newZipFile(apkPath);5.ZipEntrydexentry=zipfile.getEntry("classes.dex");6.if(dexentry.getCrc()!=dexCrc){7.System.out.println("Dexhasbeen*modified!");8.}else{9.System.out.println("Dexhasn'tbeenmodified!");10.}11.}catch(IOExceptione){12.//TODOAuto-generatedcatchblock13.e.printStackTrace();14.}
但是上述的保护方式容易被暴力破解,完整性检查最终还是通过返回true/false来控制
后续代码逻辑的走向,如果攻击者直接修改代码逻辑,完整性检查始终返回true,那这种方
法就无效了,所以类似文件完整性校验需要配合一些其他方法,或者有其他更为巧妙的方式
实现?

APK完整性校验

虽然Android程序的主要逻辑通过classes.dex文件执行,但是其他文件也会影响到整个
程序的逻辑走向,以上述Dex文件校验为例,如果程序依赖strings.xml文件中的某些值,则
修改这些值就会影响程序的运行,所以进一步可以整个APK文件进行完整性校验。但是如
果对整个APK文件进行完整性校验,由于在开发Android应用程序时,无法知道完整APK文
件的Hash值,所以这个Hash值的存储无法像Dex完整性校验那样放在strings.xml文件中,
所以可以考虑将值放在服务器端。核心代码如下:
代码:
1.MessageDigestmsgDigest=null;2.try{3.msgDigest=MessageDigest.getInstance("MD5")4.byte[]bytes=newbyte[8192];5.intbyteCount;6.FileInputStreamfis=null;7.fis=newFileInputStream(newFile(apkPath));8.while((byteCount=fis.read(bytes))>0)9.msgDigest.update(bytes,0,byteCount);10.BigIntegerbi=newBigInteger(1,msgDigest.digest());11.Stringmd5=bi.toString(16);12.fis.close();13./*14.从服务器获取存储的Hash值,并进行比较15.*/16.}catch(Exceptione){17.e.printStackTrace();18.}
Java反射

Android应用程序开发主要使用Java语言,Java中可以使用反射技术来更加灵活地控制
程序的运行,为Java运行时的行为提供了强大的支持。Java反射机制允许运行中的Java程
序对自身进行检查,并能直接操作程序的内部属性或方法,可动态生成类实例、变更属性内
容以及调用方法。关于Java反射更详细内容可以参考 Javaprogrammingdynamics,Part2:
Introducingreflection


在Android中使用反射技术来动态调用方法,可以增加对应用程序进行静态分析的难度。
以下代码是使用Java反射的一个简单例子,需要使用反射调用的方法存在于Reflection类中。
代码:
1.publicclassReflection{2.publicvoidmethodA(){3.System.out.println("InvokemethodA");4.}5.publicvoidmethodB(){6.System.out.println("InvokemethodB");7.}8.}
以下代码完成对Reflection类中方法的直接调用和反射调用。
代码:
1.protectedvoidonCreate(BundlesavedInstanceState){2.......3.Reflectionreflection=newReflection();4.reflection.methodA();5.reflection.methodB();6.7.Class[]consTypes=newClass[]{};8.ClassreflectionCls=null;9.StringclassName="com.example.reflection.Reflection";10.StringmethodName="methodA";11.try{12.reflectionCls=Class.forName(className);13.Constructorcons=reflectionCls.getConstructor(consTypes);14.ReflectionreflectionIns=(Reflection)cons.newInstance(newObject[]{});15.Methodmethod=reflectionCls.getDeclaredMethod(methodName,newClass[]{});16.method.invoke(reflectionIns,newObject[]{});17.}catch(Exceptione){18.//TODOAuto-generatedcatchblock19.e.printStackTrace();20.}21.}
当然以上Java反射的例子过于简单,使用dex2jar反编译后,用jd-gui打开,还是能够很容
易的识别出需要调用的方法,如图5所示。

图5使用dex2jar+jd-gui反编译结果

所以需要进一步采取措施增加静态分析的难度。反射调用需要获取调用的类名和方法名,而
上述代码将需要调用的类名或方法硬编码在代码中,一方面违背了Java反射使用的场景,
Java反射主要是为了提供程序的运行时动态行为的控制,另一方面并没有增加了静态分析
的难度。

可以根据程序运行过程中的实时状态来调用相应的方法,从而进一步提高静态分析的难
度。一个可能的应用场景是:根据当前应用程序的状态,从网络服务器获取需要进行反射调
用的方法以及参数信息。例如对于上述例子,类名和方法名都可以从网络获取。这样做的好
处是使得仅仅通过静态分析无法获知程序运行过程中实际调用的方法,也会增加自动化分析
的难度。也可以使用反射加密的方式,将类名、方法名做加密处理,在实际调用时再进行解
密。当然以上两种处理方式可能对性能有较大影响(本身Java反射对性能就有一定影响),
不应该频繁使用,而且必须申请网络连接的权限(不过现在凡是个Android应用程序,不申
请个网络连接权限都不好意思说自己是个Android应用)同时还得需要接入网络。


动态加载

Android系统提供了DexClassLoader来支持在程序运行过程中动态加载包含classes.dex
的.jar或者.apk文件,如果再结合Java反射技术,可以实现执行非应用程序部分的代码。利
用动态加载技术,可以提供逆向分析的难度,在一定程度上可以保护APK自身的业务逻辑
防止被破解。

DexClassLoader的构造函数原型如下:
代码:
1.publicDexClassLoader(StringdexPath,StringoptimizedDirectory,StringlibraryPath,ClassLoaderparent)
其中,dexPath为包含dex文件的.apk或者.jar路径,optimizedDirectory是优化后的dex文件
的路径,libraryPath表示Native库的路径,parent是父类加载器。通过DexClassLoader实例
化对象,调用loadClass加载需要调用的类,获得Class对象后,就可以进一步使用Java反
射技术来调用相应的方法。如下:
代码:
1.DexClassLoaderclassLoader=newDexClassLoader(apkPath,dexPath,null,getClassLoader());2.try{3.Class<?>mLoadClass=classLoader.loadClass("com.example.dexclassloaderslave.DexSlave");4.Constructor<?>constructor=mLoadClass.getConstructor(newClass[]{});5.ObjectdexSlave=constructor.newInstance(newObject[]{});6.MethodsayHello=mLoadClass.getDeclaredMethod("sayHello",newClass[]{});7.sayHello.setAccessible(true);8.sayHello.invoke(dexSlave,newObject[]{});9.}catch(Exceptione)10.{11.e.printStackTrace();12.}
上述代码实现调用com.example.dexclassloaderslave.DexSlave类中的sayHello方法。

对于需要通过DexClassLoader被调用的.apk或者.jar文件的分发,可以将其放入Android
项目的assets或者res目录下,也可以将其放在服务器端,在实际需要调用时通过网络获取
文件。为了提高逆向的难度,可以对被调用的.apk或者.jar文件采取以下措施进行进一步的
保护:
  • 进行完整性校验,防止文件被篡改;
  • 进行加密处理,在调用加载前进行解密;
  • 对需要调用的函数相关信息使用通过网络获取的方式,而不是硬编码在代码中,可
    以真正实现动态调用,提高静态分析的难度;
  • 对于使用网络服务器分发的方式,注意对网络服务器地址的保护,不要以字符串硬
    编码的方式写在代码中,对下载请求也需要使用cookie等辅助识别的技术。

除了使用DexClassLoader类实现动态加载外,还可以使用dalvik.system.DexFile类实现
Dex文件的加载,但是DexFile类提供的构造方法在实例化过程中需要在/data/davik-cache目
录下生成相应的Dex文件,而/data/davik-cache目录对于一般应用程序是没有写权限的,所
以在程序中无法实例化DexFile对象,也就无法调用DexFile.loadClass方法。所以需要通过反
射调用DexFile类的openDex方法,具体可以参考 该代码中invokeHidden函数。

字符串处理

Android应用程序开发中难免会使用到字符串,如服务器的地址等一些敏感信息,对于
这些字符串如果使用硬编码的方式,容易通过静态分析获取,甚至可以使用自动化分析工具
批量提取。例如若在Java源代码中定义一个字符串如下:
代码:
1.Stringstr="Iamastring!";
则在反编译的.smali代码中对应的代码如下(寄存器可能会有区别):
代码:
1.const-stringv0,"Iamastring!"
对于自动化分析工具,只需要扫描到const-string关键字就可以提取到字符串值。因此应该
尽量避免在源代码中定义字符串常量,比较简单的做法可以使用StringBuilder类通过append
方法来构造需要的字符串,或者使用数组的方式来存储字符串。使用StringBuilder构造字符
串反编译后的代码如下,使用这种方式可以增加自动化分析的难度,如果想要完整提取一个
字符串,如果仅仅采用静态分析方法就必须要进行相应的词法语法解析了。
代码:
1..line262..localv10,strBuilder:Ljava/lang/StringBuilder;3.const-stringv11,"I"4.invoke-virtual{v10,v11},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;5..line276.const-stringv11,"am"7.invoke-virtual{v10,v11},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;8..line289.const-stringv11,"a"10.invoke-virtual{v10,v11},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;11..line2912.const-stringv11,"String"13.invoke-virtual{v10,v11},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;14..line3015.invoke-virtual{v10},Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
另外也可以对字符串进行加密处理,很多恶意代码就采用了此种方法,例如一些具有
bot功能的恶意代码会将C&C服务器地址以及命令进行加密处理,运行时再进行解密。

代码乱序

为了增加逆向分析的难度,可以将原有代码在smali格式上进行乱序处理同时又不会影
响程序的正常运行。乱序的基本原理如图6所示,将指令重新布局,并给每块指令赋予一个
label,在函数开头处使用goto跳到原先的第一条指令处,然后第一条指令处理完,再跳到
第二条指令,以此类推。
APK 的自我保护_第3张图片
图6代码乱序的基本原理

以两个整数相加为例,Java代码如下所示:
代码:
1.publicvoidtest(){2.inta=1;3.intb=2;4.intc=a+b;5.System.out.println(c);6.}
反编译后的smali代码如下所示
代码:
1..methodpublictest()V2..locals43.4..prologue5..line246.const/4v0,0x17.8..line259..localv0,a:I10.const/4v1,0x211.12..line2613..localv1,b:I14.add-intv2,v0,v115.16..line2717..localv2,c:I18.sget-objectv3,Ljava/lang/System;->out:Ljava/io/PrintStream;19.20.invoke-virtual{v3,v2},Ljava/io/PrintStream;->println(I)V21.22..line2823.return-void
我们可以根据上述提到的代码乱序原理,将test这个函数乱序成如下代码所示(删除了.line):
代码:
1..methodpublictest()V2..locals43.4..localv2,c:I5.goto:lab16.:lab37.sget-objectv3,Ljava/lang/System;->out:Ljava/io/PrintStream;8.invoke-virtual{v3,v2},Ljava/io/PrintStream;->println(I)V9.goto:end10.11..localv1,b:I12.:lab213.add-intv2,v0,v114.goto:lab315.16..localv0,a:I17.:lab118.const/4v0,0x119.const/4v1,0x220.goto:lab221.22.:end23.return-void24..endmethod
最后使用apktool重新打包发布。进行代码乱序可以在一定程度上增加逆向分析的难度,例
如可以使用dex2jar+jd-GUI工具来分析上述乱序前后的代码。乱序前代码如图7所示:

图7使用jd-GUI分析乱序前代码

乱序后的代码如图8所示。从乱序前后的代码可以看出,使用代码乱序技术能够在一定程
度上增加逆向分析的难度,当然这是因为dex2jar工具在进行代码解析时的问题,如果能够
针对性的处理这种代码乱序的情况,那么这种反编译的情况应该会有所好转。
关于代码乱序的技术,可以参考 ADAM:Anautomaticandextensibleplatformtostresstestandroidanti-virussystems、DroidChameleon:EvaluatingAndroidAnti-malwareagainstTransformationAttacks

图8使用jd-GUI分析乱序后代码

模拟器检测


般在分析APK的过程中会借助于Android模拟器,比如分析网络行为,动态调试等。
因此从APK自我保护的角度出发,可以增加对APK当前运行环境的检测,判断是否运行在
模拟器中,如果运行在模拟器中可以选择退出整个应用程序的执行或者跳到其他分支。模拟
器检测的手段有很多,下面逐一分析。

1.属性检测

Android属性系统类似于Windows的注册表机制,所有的进程可以共享系统设置值。关
于Android属性系统的详细原理,可以参考我对于Android属性系统的分析文章
http://www.kanxue.com/bbs/showthread.php?t=182901。一些属性值在Android模拟器和真机上
是不同的,例如对于Nexus4和SDK为4.1.2的模拟器来说,Build.BRAND和Build.DEVICE属
性值分别如图9和图10所示。根据这些属性值在真实机器和模拟器上的差别可以比较容易


图9Nexus4的BRAND和DEVICE值

图10Android模拟器的BRAND和DEVICE值

检测Android应用程序是否运行在模拟器中。不过对于这种检测方式,绕过也是比较容易的,
我现在想到的有3种方式绕过:
  • 在源码中修改相应的属性值,重新编译生成内核等镜像文件,再使用这些重新生成
    的镜像文件加载模拟器(对于BRAND属性值可以修改/build/target/product/generic.mk
    文件中的PRODUCT_BRAND值,重新编译过程没测试,是不是只需要修改这个值就
    能搞定不一定正确,可以参考下build.prop生成过程分析);
  • 修改boot.img文件;
  • 使用Xposed框架,可以hookSystemProperties.get函数,在before函数中检查需要
    获取的属性,根据情况修改对应的值,然后返回;
    另外还可以通过检测IMEI,IMSI等值来判断是否是模拟器,在模拟器中,这两个值默认
    分别是000000000000000和310260000000000。通过以下代码可以获取IMSI值:
代码:
1.TelephonyManagermanager=(TelephonyManager)getSystemService(TELEPHONY_SERVICE);2.Stringimsi=manager.getSubscriberId();
TELEPHONY_SERVICE需要申请android.permission.READ_PHONE_STATE权限。同样我们可以有
相应的绕过方式,一个相对简单的方法是直接修改AndroidSDK下/tools/emulator-arm.exe文
件(Windows版本)。使用010Editor打开emulator-arm.exe文件,搜索CIMI,如图11,”CIMI.”
后面的15位数字值是IMSI,”CGSN.”后面的15位数字为IMEI,修改这两个值(确保没有运行
模拟器),然后保存。

图11修改IMSI和IMEI

修改后再运行模拟器,此时查看IMSI值如所示,IMEI值如所示,可见可以成功修改这两个
值。
名称:  12.png查看次数: 2文件大小:  6.5 KB
图12修改后的IMSI值

APK 的自我保护_第4张图片
图13修改后的IMEI值

还存在其他的修改方式,可以参考 HidetheEmulator以及 Androidemulatorpatchforconfigurable
IMEI,IMSIandSIMcardserialnumber

当然还可以检查一些其他的值,如电池的电池状态、电池电量,Secure.ANDROID_ID,
DeviceId,手机号码等。

2.虚拟机文件检测

相对于真实设备,Android模拟器中存在一些特殊的文件或者目录,如
/system/bin/qemu-props,该可执行文件可以用来在模拟器中设置系统属性。另外还有
/system/lib/libc_malloc_debug_qemu.so文件以及/sys/qemu_trace目录。我们可以通过检测这些
特殊文件或者目录是否存在来判断Android应用程序是否运行在模拟器中,关键代码如下:
代码:
1.privatestaticString[]known_files={2."/system/lib/libc_malloc_debug_qemu.so",3."/sys/qemu_trace",4."/system/bin/qemu-props"5.};6.7.publicstaticbooleanhasQEmuFiles(){8.for(Stringpipe:known_files){9.Fileqemu_file=newFile(pipe);10.if(qemu_file.exists())11.returntrue;12.}13.returnfalse;14.}
更完整的代码可以参考TimStrazzere的Github中 anti-emulator,该项目中还列举了其他一些
模拟器检测的方法,如检测socket文件/dec/socket/qemud。

3.基于Cache行为的模拟器检测方法

BlueBox关于Android模拟器检测的方法
http://bluebox.com/corporate-blog/android-emulator-detection/

4.基于代码指令执行的模拟器检测方法

DexLabs关于Android模拟器检测的方法
http://dexlabs.org/blog/btdetect

5.其他方法

其他一些检测方法,可以参考如下文献:
  • DISSECTINGTHEANDROIDBOUNCER
  • 逃离安卓动态检测
  • GunsandSmoketoDefeatMobileMalware
  • DEXEDUCATION201ANTI-EMULATION
  • INSECUREMAGZINE34–IntroductiontoAndroidmalwareanalysis

APK伪加密

APK实际上是Zip压缩文件,但是Android系统在解析APK文件时,和传统的解压缩软
件在解析Zip文件时还是有所差异的,利用这种差异可以实现给APK文件加密的功能。Zip
文件格式可以参考 MasterKey漏洞分析的一篇文章。在CentralDirectory部分的FileHeader头
文件中,有一个2字节长的名为Generalpurposebitflags的字段,这个字段中每一位的作用
可以参考 Zip文件格式规范的4.4.4部分,其中如果第0位置1,则表示Zip文件的该Central
Directory是加密的,如果使用传统的解压缩软件打开这个Zip文件,在解压该部分Central
Directory文件时,是需要输入密码的,如图14所示。但是Android系统在解析Zip文件时并
没有使用这一位,也就是说这一位是否置位对APK文件在Android系统的运行没有任何影响。
一般在逆向APK文件时,会首先使用 apktool来完成资源文件的解析,dex文件的反汇编工
作,但如果将Zip文件中CentralDirectory的Generalpurposebitflags第0位置1的话,
apktool(version:1.5.2)将无法完成正常的解析工作,如图15所示,但是又不会影响到APK在
Android系统上的正常运行,如图16所示。
APK 的自我保护_第5张图片
图14传统解压缩软件需要输入密码进行解压缩

APK 的自我保护_第6张图片
图15apktool解析伪加密的APK文件失败

对APK文件进行伪加密可以使用这个脚本,在Python的zipfile模块中,ZipInfo类中记
录了Zip文件中相应的CentralDirectory的相关信息,包括Generalpurposebitflags,在ZipInfo
类中属性为flag_bits,因此上述脚本中将需加密的APK文件的每个ZipInfo的flag_bits和1做
或操作,实现在Generalpurposebitflags的第0位置1.
而需要去除这些伪加密的标志的话,可以使用这个脚本。相关内容可以参考BlueBox之
前提出的一个 AndroidSecurityAnalysisChanllenge.

图16伪加密的APK可以正常运行

代码:
1.unsignedintgpbf=get2LE(lfhBuf+kLFHGPBFlags);2.if((gpbf&kGPFUnsupportedMask)!=0){3.ALOGW("InvalidGeneralPurposeBitFlag:%d",gpbf);4.returnfalse;5.}
ManifestCheating

AndroidManifest.xml是Android应用程序的配置文件,包含了包名、应用程序名称、申请
的权限信息以及组件信息等。在Android应用程序开发,生成APK时,aapt会负责完成资源
的打包,打包会将文本格式的XML资源文件编译成二进制格式的XML资源文件。将文本格
式的XML文件转换成二进制格式,一方面通过字符串资源池的统一管理,减少文件体积;
另一方面二进制格式的XML文件解析速度也会更快。在Android开发过程中,生成的R.java
文件中包含了相应的资源类型、名称以及对应的id值。资源id是32bit的整型值,格式
为:0xPPTTNNNN。其中PP表示使用该资源的包,TT代表该资源的类型,而NNNN是该类型
中资源的名称。对于应用程序资源,PP值固定为7f,而对于被引用的系统资源包,其PP
值为01。TT和NNNN一般是aapt按照资源出现的顺序生成的。更多分析可以参考罗升阳的
Android应用程序资源的编译和打包过程分析

ManifestCheating的基本原理是,在AndroidManifest的<application>节点中插入一个未知
id(如0x0),名称为name的属性,其值可以是一个从未定义实现的Java类文件名。而对
AndroidManifest的修改需要在二进制格式下进行,这样才能不会破坏之前aapt对资源文件的
处理。由于是未知的资源id,在应用程序运行过程中,Android会忽略此属性。但是在使用
apktool进行重打包时,首先会将AndroidManifest.xml转换为明文,进而会包含名称为name
的属性,而相应的id信息会丢失,apktool重打包会重新进行资源打包处理,由于该name
属性值是一个未实现的Java类,重打包后的应用程序在运行过程中,由于application节点
中定义的类是先于所有其他组件运行的,若系统找不到对应的类,会出现运行时错误,Dalvik
虚拟机会直接关闭。另外,也可以实现name属性值对应的Java类,若此类被调用,则表明
被重打包了,可以采取进一步的措施。这样就可以起到保护自身APK的作用,防止被重打
包。但是这种方法也很容易被绕过,只需要在经过apktool解码的AndroidManifest文件中,
去掉在application节点中添加的name属性即可。整个过程如下:
  • 将APK解压缩,提取其中的AndroidManifest.xml文件;
  • 使用axml工具,修改二进制的AndroidManifest.xml文件,在application节点下插入
    id未知(如0x0),名为name的属性(值可以任意,只要不对应到项目中的类文件名
    即可,如some.class);
  • 将除META-INF文件夹之外的文件压缩成zip文件,签名后生成.apk文件。
    若是攻击者使用apktool重打包,运行重打包后的文件会出现如下运行时错误:
APK 的自我保护_第7张图片
图17使用ManifestCheating重打包后APK文件运行时错误

调试器检测

在对APK逆向分析时,往往会采取动态调试技术,可以使用 netbeans+apktool对反汇编
生成的smali代码进行动态调试。为了防止APK被动态调试,可以检测是否有调试器连接。
Android系统在android.os.Debug类中提供了isDebuggerConnected()方法,用于检测是否有调
试器连接。可以在Application类中调用isDebuggerConnected()方法,判断是否有调试器连接,
如果有,直接退出程序。

除了isDebuggerConnected方法,还可以通过在AndroidManifest文件的application节点中
加入android:debuggable=”false”使得程序不可被调试,这样如果希望调试代码,则需要修改
该值为true,因此可以在代码中检查这个属性的值,判断程序是否被修改过,代码如下:
代码:
1.if(getApplicationInfo().flags&=ApplicationInfo.FLAG_DEBUGGABLE!=0){2.System.out.println("Debug");3.android.os.Process.killProcess(android.os.Process.myPid());4.}
代码混淆

使用Java编写的代码很容易被反编译,因此可以使用代码混淆的方法增加反编译代码
阅读的难度。 ProGuard是一款免费的Java代码混淆工具,提供了文件压缩、优化、混淆和
审核功能。在Eclipse+ADT开发环境下,每个Android应用程序项目目录下会默认生成
project.properties和proguard-project.txt文件。如果需要使用ProGuard进行压缩以及混淆,首
先需要在project.properties文件中去掉对如下语句的注释:
代码:
1.proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
ProGuard的相关配置信息需要在proguard-project.txt文件中声明,在其中可以设置需要混淆
和保留的类或方法。由于在某些情况下,ProGuard会错误地认为某些代码没有被使用,如在
只在AndroidManifest文件中引用的类,从JNI中调用的方法等。对于这些情况,需要在
proguard-project.txt文件中添加-keep命令,用来保留类或方法。关于ProGuard更加详细的配
置项可以参考 ProGuardManual
除了使用ProGuard对Android代码进行混淆外,还可以使用 DexGuard。DexGuard是特别
针对Android的一款代码优化混淆的收费软件,提供代码优化混淆、字符串加密、类加密、
Assets资源加密、隐藏对敏感API的调用、篡改检测以及移除Log代码。DexGuard的进一步
分析可以参考JEB上的相关blog,可以在这里总结一下:

1. 字符串加密
经过DexGuard加固过的APK,对字符串的访问会通过调用一个解密函数来完成加密字
符串的解密。如图18所示,红框中的字节数组是加密后的字符串,在onCreate函数中,调
用了解密函数进行解密。字符解密函数如图19所示,对其进行处理后如图20所示,加密算
法也很简单,基本思路是:当前字符由前一个字符加上加密字符数组中的字符,再减去常量
8形成,当字符长度达到给定的长度时,会最终构成字符串并返回。
APK 的自我保护_第8张图片
图18DexGuard字符串加密


图19字符串解密函数


图20处理后的解密函数

2. assets加密
APK文件的assets目录下包含了应用程序需要使用到的资源文件,DexGuard提供了对
assets资源文件的加密功能。对于一个经过保护的asset资源文件,例如1.png文件,使用
十六进制查看器查看该文件,如图21所示。从图中可见,加密后的png文件,缺失了相应
的文件头。解密则是首先通过反射调用AssetManager.open函数,同时对该函数的反射调用

图21经过加密的png文件

又使用了加密处理,最后通过Cipher类完成png文件的解密。相关解密处理如所示。

图22asset解密处理

关于ProGuard和DexGuard还可以参考 ProGuardandDexGuard,其中除了介绍了ProGuard
和DexGuard,还提供了一些APK加固处理的方法。
关于代码混淆,还可以参考 Android:GameofObfuscation

NDK

Android软件的开发主要使用Java语言,但是Android也提供了对本地语言C、C++的支
持。借助JNI,可以在Java类中使用C语言库中的特定函数,或在C语言程序中使用Java
类库。一般来说,如果代码中对处理速度有较高要求或者为了更好地控制硬件,抑或者为了
复用既有的C/C++代码,都可以考虑通过JNI来实现对Native代码的调用。
由于逆向Native程序的汇编代码要比逆向Java汇编代码困难,因此可以考虑在关键代
码部位使用Native代码,如注册验证,加解密操作等。一个可能的借助Native代码保护APK
的方法是:将核心业务逻辑代码放入加密的.jar或者.apk文件中,在需要调用时使用Native
代码进行解密,同时完成对解密后文件的完整性校验,不过不管是.jar还是.apk文件,解密
后都会留在物理存储上,为了避免这种情况,可以使用反射技术直接调用
dalvik.system.DexFile.openDex()方法,该方法接受classes.dex文件字节流返回DexFile对象。
关于Native代码的编写,可以参考Google官方文档的 AndroidNDK

逆向工具对抗

在逆向分析Android应用程序时,一般会使用apktool,baksmali/smali,dex2jar,androguard,
jdGUI以及IDAPro等。因此可以考虑使得这些工具在反编译APK时出错来保护APK,这些工
具大部分都是开源的,可以通过阅读其源代码,分析其在解析APK、dex等文件存在的缺陷,
在开发Android应用程序时加以利用。可以参考 TimStrazzere的DexEducation:PracticingSafe
Dex
,相应的 Demo,看雪上的 中文翻译,不过其中的很多技巧已经失效了。DexLabs的 Dalvik
BytecodeObfuscationonAndroid
介绍了垃圾字节码插入的技术。

使用apktool进行重打包时,对于后缀为png的文件,会按照png格式的文件进行打包
处理,因此如果在项目开发时,有意将一个非png格式文件的文件名改为后缀为png的文件,
则使用apktool进行重打包时会出错。可以利用这种方法来对抗重打包。可以试试对 这个文
使用apktool进行重打包,会报很多错误,但是这种appt导致的错误,很多都是由于第一
个错误一起的,如图23所示。从第一个错误描述中可知,res/drawable-hdpi/station.png不是
APK 的自我保护_第9张图片
图23apktool重打包错误

一个PNG格式的文件,使用file命令,可以发现实际上是一个Windowsicon文件,如图24
所示。将这个文件后缀修改成.icon就可以重新打包了。

图24station.png的真实文件类型

总结


以上APK自我保护的技术并不能做到完全的保护作用,只是提高了逆向分析的难度,
在实际运用中应该根据情况多种技术结合使用。这些技术其实很多来源于Android恶意代码,
所以可以关注Android恶意代码中使用的一些技术来应用到自己开发的Android应用程序中。

注:本帖由看雪论坛志愿者PEstone重新将pdf整理排版,若和原文有出入,以原作者附件为准



总结的一些关于APK自我保护的方法,当然还有很多其他的技巧,无法一一列举,现在可以使用的一些服务包括:
1.梆梆: http://www.bangcle.com/
2.爱加密: https://www.ijiami.cn/
3.APKProtect: http://www.apkprotect.com/
4.Shield4J: http://shield4j.com/
5.DexGuard: http://www.saikoa.com/dexguard

欢迎补充!
上传的附件
文件类型: pdf APK自我保护.pdf(1.37 MB, 1964 次下载)

更多相关文章

  1. Android Launcher2源码分析主布局文件
  2. Android 权限控制代码分析
  3. 安装APK文件到Android模拟器
  4. Android中监听Home键的4种方法总结
  5. Android Canvas绘图描述Android Canvas 方法总结

随机推荐

  1. EditText的字体和大小
  2. Android--自定义Dialog,仿IOS对话框样式
  3. Android零基础入门第77节:Activity任务栈
  4. 短视频带货源码,解决Dialog 不铺满屏幕问
  5. android短信管理器SmsManager实例详解
  6. Android Studio中buildscript和allprojec
  7. (二)android升级--完整包updater-script脚
  8. Android(安卓)拍照适配方案
  9. Android获取局域网所有设备的ip地址
  10. Android屏蔽EditText软键盘的方法