APK的自我保护

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

Dex

Dex文件结构

        classes.dex文件是Android系统运行于Dalvik Virtual Machine上的可执行文件,也是Android应用程序的核心所在,因此咱们首先来看下DEX文件的结构,这样可以更好的理解后续的分析,须要更加详细的信息,能够参考Google关于Dex的技术文档php

        从Java源文件(固然Android也支持JNI的调用方式)到生成Dex文件的基本映射关系如图 1所示,Java源文件经过Java编译器生成class文件,再经过dx工具转换为classes.dex文件。Dex文件从总体上来看是个索引的结构,类名、方法名、字段名等信息都存储在常量池中,这样可以充分减小存储空间,一个Dex文件的基本结构如图 2所示,相关结构声明定义在DexFile.h中,在AOSP中的路径为/dalvik/libdex/DexFile.h。html

apk_protect_1 

图 1 Java源文件生成Dex文件的映射关系java

Ÿ   header: Dex文件头,包含magic字段、adler32校验值、SHA-1哈希值、string_ids的个数android

 apk_protect_2

图 2 Dex文件基本结构ios

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

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
     struct DexHeader {
         u1 magic [ 8 ] ; /* includes version number */
         u4 checksum ; /* adler32 checksum */
         u1 signature [ kSHA1DigestLen ] ; /* SHA-1 hash */
         u4 fileSize ; /* length of entire file */
         u4 headerSize ; /* offset to start of next section */
         u4 endianTag ;
         u4 linkSize ;
         u4 linkOff ;
         u4 mapOff ;
         u4 stringIdsSize ;
         u4 stringIdsOff ;
         u4 typeIdsSize ;
         u4 typeIdsOff ;
         u4 protoIdsSize ;
         u4 protoIdsOff ;
         u4 fieldIdsSize ;
         u4 fieldIdsOff ;
         u4 methodIdsSize ;
         u4 methodIdsOff ;
         u4 classDefsSize ;
         u4 classDefsOff ;
         u4 dataSize ;
         u4 dataOff ;
} ;

Ÿ   DexStringId: 定义了字符串数据的偏移, stringDataOff指向字符串数据;github

 
1
2
3
     struct DexStringId {
         u4 stringDataOff ; /* file offset to string_data_item */
     } ;

Ÿ   DexTypeId: 表示应用程序代码中使用到的具体类型,如整型、字符串等,在Dalvik字节码中表示为I、Ljava/lang/String;,descriptorIdx指向DexStringId列表的索引;服务器

 
1
2
3
     struct DexTypeId {
         u4 descriptorIdx ; /* index into stringIds list for type descriptor */
     } ;

Ÿ   DexProtoId:表示方法声明的结构体,shortyIdx是方法声明字符串,格式为返回值类型后紧跟参数列表类型,如方法声明为VI,表示返回值为V(空,无返回值),参数为I(整型),全部的引用类型用L表示;returnTypeIdx指向DexTypeId列表的索引,表示返回值类型;parametersOff指向DexTypeList的偏移,表示参数列表类型;cookie

 
1
2
3
4
5
     struct DexProtoId {
         u4 shortyIdx ; /* index into stringIds for shorty descriptor */
         u4 returnTypeIdx ; /* index into typeIds list for return type */
         u4 parametersOff ; /* file offset to type_list for parameter types */
     } ;

Ÿ   DexFieldId: 表示代码中的字段,classIdx指向DexTypeId列表索引,表示字段所属的类;typeIdx表示字段类型,nameIdx指向DexStringId列表索引,表示字段名;网络

 
1
2
3
4
5
     struct DexFieldId {
         u2 classIdx ; /* index into typeIds list for defining class */
         u2 typeIdx ; /* index into typeIds for field type */
         u4 nameIdx ; /* index into stringIds for field name */
     } ;

Ÿ   DexMethodId: 表示代码中使用的方法,classIdx表示方法所属的类,protoIdx指向DexProtoId列表索引,表示方法原型,nameIdx表示方法名;

 
1
2
3
4
5
     struct DexMethodId {
         u2 classIdx ; /* index into typeIds list for defining class */
         u2 protoIdx ; /* index into protoIds for method prototype */
         u4 nameIdx ; /* index into stringIds for method name */
     } ;

Ÿ   DexClassDef: 该结构相对要复杂一些,定义了代码中的使用的类,以及相关的代码指令。

 

 
1
2
3
4
5
6
7
8
9
10
     struct DexClassDef {
         u4 classIdx ; /* index into typeIds for this class */
         u4 accessFlags ;
         u4 superclassIdx ; /* index into typeIds for superclass */
         u4 interfacesOff ; /* file offset to DexTypeList */
         u4 sourceFileIdx ; /* index into stringIds for source file name */
         u4 annotationsOff ; /* file offset to annotations_directory_item */
         u4 classDataOff ; /* file offset to class_data_item */
         u4 staticValuesOff ; /* file offset to DexEncodedArray */
} ;

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
2
3
4
5
6
7
     struct DexClassData {
         DexClassDataHeader header ;
         DexField * staticFields ;
         DexField * instanceFields ;
         DexMethod * directMethods ;
         DexMethod * virtualMethods ;
     } ;

DexField表示字段的类型和访问标志, fieldIdx指向DexFieldId;

 
1
2
3
4
     struct DexField {
         u4 fieldIdx ; /* index to a field_id_item */
         u4 accessFlags ;
     } ;

DexMethod结构描述了方法的原型、名称、访问标志以及代码指令的偏移地址,methodIdx指向DexMethodId索引,须要注意的是在Google的Dex文件文档中对此的定义:

index into the method_ids list for the identity of this method (includes the name and descriptor), represented as a difference from the index of previous element in the list. The index of the first element in a list is represented directly.

注意红色字体部分,表示的是在Dex文件中,methodIdx是相对于前一个DexMethod中的methodIdx的增量,例如若是一个类中有两个directMethods,第一个directMethod的methodIdx值为0×13,表示指向索引为0×13的methodIdx,那么第二个directMethod的methodIdx的值是相对于前一个值的增量,例如0×01,表示指向索引为0×14的methodIdx;accessFlags为方法的访问标志,codeOff表示指令代码的偏移地址;

 
1
2
3
4
5
     struct DexMethod {
         u4 methodIdx ; /* index to a method_id_item */
         u4 accessFlags ;
         u4 codeOff ; /* file offset to a code_item */
     } ;

DexCode的结构体声明以下。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
     struct DexCode {
         u2 registersSize ;
         u2 insSize ;
         u2 outsSize ;
         u2 triesSize ;
         u4 debugInfoOff ; /* file offset to debug info stream */
         u4 insnsSize ; /* size of the insns array, in u2 units */
         u2 insns [ 1 ] ;
         /* followed by optional u2 padding */
         /* followed by try_item[triesSize] */
         /* followed by uleb128 handlersSize */
         /* followed by catch_handler_item[handlersSize] */
} ;

须要注意的是,在DexClass.h中,全部的u4类型,其实是uleb128类型。每一个uleb128类型是leb128的无符号类型,每一个leb128类型的数据包含1-5个字节,表示一个32bit的数值。每一个字节只有7位有效,最高一位用来表示是否须要使用到下一个字节,好比若是第一个字节最高位为1,表示还须要使用到第2个字节,若是第二个字节的最高位为1,表示会使用到第3个字节,以此类推,最多5个字节。对于一个2个字节的leb128类型数据,其结构如图 3所示。

 

apk_protect_3

图 3 两字节的leb128类型数据格式

Dex中方法的隐藏

         此部份内容可参考Playing Hide and Seek with Dalvik Executables

        前文分析了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值设为0×0,至关于将原先的方法指向了前一个方法;
  • Ÿ   访问标志符accessFlags通常不须要修改,在Dex文件格式里,directMethods和virtualMethods是分开的;
  • Ÿ   将codeOffset设置为前一个方法的代码偏移地址。
  • Ÿ   更新需隐藏方法的下一个方法的methodIdx,可使用公式:next_method_idx=original_next_method_idx + original_method_idx

 apk_protect_4

图 4 Dex方法隐藏

Ÿ   二、从新计算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
2
3
4
String apkPath = this . getPackageCodePath ( ) ;
ZipFile apkfile = new ZipFile ( apkPath ) ;
ZipEntry dexentry = zipfile . getEntry ( “classes.dex” ) ;
InputStream dexstream = zipfile . getInputStream ( dexentry ) ;

 

  •  Ÿ   修复Dex文件,将以前隐藏方法的DexMethod结构体恢复;
  • Ÿ   将修复后的Dex数据使用类加载器从新加载;
  • Ÿ   搜索被隐藏的方法;
  • Ÿ   调用被隐藏的方法。

        须要注意的是,方法在Dex文件中是按方法名的字典序排序的,因此须要隐藏的方法若是是该类中全部方法排序第一个的话,那么methodIdx值是个绝对值,若是要隐藏的话就不是很方便,因此建议能够写个无用的方法,其方法名排序为第一个,让须要隐藏的方法从新指向该方法。

        使用修改methodIdx的方法,让其指向另外一个DexMethodId的结构体,若是使用baksmali进行反汇编,则会发如今一个类中有两个彻底相同的函数。

        那有没有更加隐蔽的手段来隐藏一个方法了?考虑到在DexClassData结构体中的DexClassDataHeader头部,其中directMethodsSize和virtualMethodsSize分别表示直接方法个数和虚方法个数,所以若是但愿隐藏某个方法,能够经过将相应的directMethodsSize或virtualMethodsSize减1,同时将表示该须要隐藏方法的DexMethod结构体中的数据所有修改成0,这样就能够将该方法隐藏起来,使用baksmali反汇编时,不会显示出该方法的反汇编代码,具体能够参考Hashdays 2012 Android Chanllenge

        固然,上述这两种隐藏方法,都没能隐藏掉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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String apkPath = this . getPackageCodePath ( ) ;
Long dexCrc = Long . parseLong ( this . getString ( R . string . dex_crc ) ) ;
try
{
     ZipFile zipfile = new ZipFile ( apkPath ) ;
     ZipEntry dexentry = zipfile . getEntry ( “classes.dex” ) ;
     if ( dexentry . getCrc ( ) != dexCrc ) {
         System . out . println ( “Dex has been modified!” ) ;
     } else {
         System . out . println ( “Dex hasn’t been modified!” ) ;
     }
} catch ( IOException e ) {
             // TODO Auto-generated catch block
     e . printStackTrace ( ) ;
}

       可是上述的保护方式容易被暴力破解, 完整性检查最终仍是经过返回true/false来控制后续代码逻辑的走向,若是攻击者直接修改代码逻辑,完整性检查始终返回true,那这种方法就无效了,因此相似文件完整性校验须要配合一些其余方法,或者有其余更为巧妙的方式实现?

APK完整性校验

         虽然Android程序的主要逻辑经过classes.dex文件执行,可是其余文件也会影响到整个程序的逻辑走向,以上述Dex文件校验为例,若是程序依赖strings.xml文件中的某些值,则修改这些值就会影响程序的运行,因此进一步能够整个APK文件进行完整性校验。可是若是对整个APK文件进行完整性校验,因为在开发Android应用程序时,没法知道完整APK文件的Hash值,因此这个Hash值的存储没法像Dex完整性校验那样放在strings.xml文件中,因此能够考虑将值放在服务器端。核心代码以下:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
     MessageDigest msgDigest = null ;
     try {
       msgDigest = MessageDigest . getInstance ( “MD5″ )
       byte [ ] bytes = new byte [ 8192 ] ;
       int byteCount ;
       FileInputStream fis = null ;
       fis = new FileInputStream ( new File ( apkPath ) ) ;
       while ( ( byteCount = fis . read ( bytes ) ) > 0 )
         msgDigest . update ( bytes , 0 , byteCount ) ;
       BigInteger bi = new BigInteger ( 1 , msgDigest . digest ( ) ) ;
       String md5 = bi . toString ( 16 ) ;
       fis . close ( ) ;
       /*
       从服务器获取存储的Hash值,并进行比较
      */
 
       } catch ( Exception e ) {
         e . printStackTrace ( ) ;
     }

 

 

动态加载

         Java中可使用反射技术来更加灵活地控制程序的运行,为Java运行时的行为提供了强大的支持。Android系统提供了DexClassLoader来支持在程序运行过程当中动态加载包含classes.dex的.jar或者.apk文件,若是再结合Java反射技术,能够实现执行非应用程序部分的代码。利用动态加载技术,能够提供逆向分析的难度,在必定程度上能够保护APK自身的业务逻辑防止被破解。

       DexClassLoader的构造函数原型以下:

 
1
public DexClassLoader ( String dexPath , String optimizedDirectory , String libraryPath , ClassLoader parent )

其中,dexPath为包含dex文件的.apk或者.jar路径,optimizedDirectory是优化后的dex文件的路径,libraryPath表示Native库的路径,parent是父类加载器。经过DexClassLoader实例化对象,调用loadClass加载须要调用的类,得到Class对象后,就能够进一步使用Java反射技术来调用相应的方法。以下:

 
1
2
3
4
5
6
7
8
9
10
11
12
     DexClassLoader classLoader = new DexClassLoader ( apkPath , dexPath , null , getClassLoader ( ) ) ;
     try {
     Class <? > mLoadClass = classLoader . loadClass ( “com.example.dexclassloaderslave.DexSlave” ) ;
     Constructor <? > constructor = mLoadClass . getConstructor ( new Class [ ] { } ) ;
     Object dexSlave = constructor . newInstance ( new Object [ ] { } ) ;
     Method sayHello = mLoadClass . getDeclaredMethod ( “sayHello” , new Class [ ] { } ) ;
     sayHello . setAccessible ( true ) ;
     sayHello . invoke ( dexSlave , new Object [ ] { } ) ;
     } catch ( Exception e )
   {
       e . printStackTrace ( ) ;
     }

上述代码实现调用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函数。

APK伪加密

        APK其实是Zip压缩文件,可是Android系统在解析APK文件时,和传统的解压缩软件在解析Zip文件时仍是有所差别的,利用这种差别能够实现给APK文件加密的功能。Zip文件格式能够参考MasterKey漏洞分析的一篇文章。在Central Directory部分的File Header头文件中,有一个2字节长的名为General purpose bit flags的字段,这个字段中每一位的做用能够参考Zip文件格式规范的4.4.4部分,其中若是第0位置1,则表示Zip文件的该Central Directory是加密的,若是使用传统的解压缩软件打开这个Zip文件,在解压该部分Central Directory文件时,是须要输入密码的,如图 5所示。可是Android系统在解析Zip文件时并无使用这一位,也就是说这一位是否置位对APK文件在Android系统的运行没有任何影响。通常在逆向APK文件时,会首先使用apktool来完成资源文件的解析,dex文件的反汇编工做,但若是将Zip文件中Central Directory的General purpose bit flags第0位置1的话,apktool(version:1.5.2)将没法完成正常的解析工做,如图 6所示,可是又不会影响到APK在Android系统上的正常运行,如图 7所示。

 apk_protect_5

图 5 传统解压缩软件须要输入密码进行解压缩

 apk_protect_6

图 6 apktool解析伪加密的APK文件失败

       对APK文件进行伪加密可使用这个脚本,在Python的zipfile模块中,ZipInfo类中记录了Zip文件中相应的Central Directory的相关信息,包括General purpose bit flags,在ZipInfo类中属性为flag_bits,所以上述脚本中将需加密的APK文件的每一个ZipInfo的flag_bits和1作或操做,实如今General purpose bit flags的第0位置1.

       而须要去除这些伪加密的标志的话,可使用这个脚本。相关内容能够参考BlueBox以前提出的一个Android Security Analysis Chanllenge.

apk_protect_7 

图 7 伪加密的APK能够正常运行

 Manifest Cheating

         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应用程序资源的编译和打包过程分析

       Manifest Cheating的基本原理是,在AndroidManifest的<application>节点中插入一个未知id(如0×0),名称为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未知(如0×0),名为name的属性(值能够任意,只要不对应到项目中的类文件名便可,如some.class);
  • Ÿ   将除META-INF文件夹以外的文件压缩成zip文件,签名后生成.apk文件。

如果攻击者使用apktool重打包,运行重打包后的文件会出现以下运行时错误: apk_protect_8

图 8 使用Manifest Cheating重打包后APK文件运行时错误

调试器检测

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

       除了isDebuggerConnected方法,还能够经过在AndroidManifest文件的application节点中加入android:debuggable=”false”使得程序不可被调试,这样若是但愿调试代码,则须要修改该值为true,所以能够在代码中检查这个属性的值,判断程序是否被修改过,代码以下:

 
1
2
3
4
     if ( getApplicationInfo ( ) . flags &= ApplicationInfo . FLAG_DEBUGGABLE != 0 ) {
       System . out . println ( “Debug” ) ;
       android . os . Process . killProcess ( android . os . Process . myPid ( ) ) ;
     }

代码混淆

         使用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更加详细的配置项能够参考ProGuard Manual

       除了使用ProGuard对Android代码进行混淆外,还可使用DexGuard。DexGuard是特别针对Android的一款代码优化混淆的收费软件,提供代码优化混淆、字符串加密、类加密、Assets资源加密、隐藏对敏感API的调用、篡改检测以及移除Log代码。

       关于代码混淆,还能够参考Android:Game of Obfuscation

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官方文档的Android NDK

逆向工具对抗

         在逆向分析Android应用程序时,通常会使用apktool,baksmali/smali,dex2jar,androguard,jdGUI以及IDA Pro等。所以能够考虑使得这些工具在反编译APK时出错来保护APK,这些工具大部分都是开源的,能够经过阅读其源代码,分析其在解析APK、dex等文件存在的缺陷,在开发Android应用程序时加以利用。能够参考Tim Strazzere的Dex Education:Practicing Safe Dex,相应的Demo,看雪上的中文翻译,不过其中的不少技巧已经失效了。DexLabs的Dalvik Bytecode Obfuscation on Android介绍了垃圾字节码插入的技术。

总结

         以上APK自我保护的技术并不能作到彻底的保护做用,只是提升了逆向分析的难度,在实际运用中应该根据状况多种技术结合使用。这些技术其实不少来源于Android恶意代码,因此能够关注Android恶意代码中使用的一些技术来应用到本身开发的Android应用程序中。

来源:http://www.sanwho.com/445.html

相关文章
相关标签/搜索