静态防护技术是面向App程序组成部分的防护,通过防护程序代码来提高App的安全性。本章主要从两方面介绍静态防护技术:一是针对App源代码的保护技术;二是针对apk文件的加固技术,包括针对dex文件、资源文件、so文件等进行代码层面的安全加固。本章仅介绍App静态防护技术的实现思路,不讨论具体的实现方案细节。
源代码保护技术主要是将App的程序代码进行混乱变形,一方面隐藏原始程序的控制流,另方面减少原始程序不同控制流在代码表达方面的差异性,增加App程序代码中各部分程序逻辑结构的相似性,从而增加攻击者使用逆向工具分析还原App业务逻辑或核心算法的难度。源代码保护一般通过两种方式来实现:对于App开发者来说,可以利用代码混淆工具直接对源代码进行混淆;对于App防护者来说,一般需要先获取原App,利用代码反编译工具直接对原App安装文件进行反编译,然后对获得的反编译代码进行混淆,最后对混淆后的App反编译代码进行重打包和签名,得到加固后的App,整个过程如下图所示。
图:App 防护的一般过程
目前关于程序加密与解密的图书众多,很多书详细介绍了源代码混淆的理论知识,本节仅对 Android App源代码混淆涉及的混淆技术进行简要说明,不会深入讨论每一项源代码混淆的详细知识。源代码保护的核心就是使用程序代码混淆器实现源代码的混淆,核心算法包括控制流平坦 化( control flow fatterning )和不透明谓词( opaque predicate)等。
控制流平坦化技术是指将程序代码的执行控制逻辑( if.. else…语句、for 语句)等效变换为平坦的控制逻辑( switch...case...语句),这样就隐藏了初始的程序层次结构。
控制流平坦化的一般实现过程是,首先对源代码进行基本块分隔,然后建立调用流程图,最后将基本块在switch...case..和while/for循环下建立等效的平坦化结构。在源代码级别完成控制流扁平化,下面以我们熟悉的二分法查找为例进行说明。原始代码如下:
为了方便进行控制流表示,我们对上面的代码里的基本块进行了标注,分别为B1, B2,...,B9。以上示例代码的程序控制流图如下图所示。
图:扁平化处理 前的原始代码控制流
示例代码经过控制流扁平化之后,形成了如下等价代码:
int bsearch(int *buf, int count, int key) { int left, right, mid; int next = 1; while(true) { switch(next) { case 1: left = 0; right = count -1; next = 2; break; case 2: if (left <= right) next = 3; else next = 9; break; case 3: mid = (left + right) / 2; if (key == buf[mid]) next = 4; else next = 5; break; case 4: return mid; break; case 5: if ( key < buf[mid] ) next = 6; else next = 7; break; case 6: right= mid - 1; next = 8; break; case 7: left = mid + 1; next = 8; break; case 8: next = 2; break; case 9: return -1; break; } } }
图:扁平化处理后的代码控制流
不透明谓词技术是在程序代码的条件跳转节点处对程序跳转条件进行混淆。一般来说,App开发者在设计程序跳转条件时,会将程序变量之间的逻辑运算结果作为跳转条件,以提高跳转条件的可读性。不透明谓词技术就是将跳转条件设计成运算复杂度较高、与程序相关性较小的数学运算,增加攻击者通过静态分析方法逆向分析程序执行逻辑的难度。
代码混淆器在源代码中插入不透明谓词或无关的冗余代码时可进一步使用随机数来提高代码混淆强度,使在不同时间进行的代码混淆结果具有差异性,提高攻击者逆向分析代码混淆规则的难度。下面两段代码为在平坦化过程中使用不透明谓词的对比。
使用不透明谓词前:
bool TeapotRenderer: :Bind( ndk_helper: :TapCamera* camera ) { camera_ = camera; return true; }
使用不透明谓词后:
bool TeapotRenderer: :Bind(ndk_ helper ::TapCamera* camera) { volatile unsigned int_ nv_ state_ helper_ _53; _nv_ _state_ helper_ _53 = 304; unsigned int _local _var_54; _local_var_54 = 4; while (_ local_var_54 != 5) switch (_local _var_54) { case 4: { this->camera_ = camera; return true; _local _var__54= 5U; nv_ state_ helper_ _53 = 301U; break; } } }
对于分析人员来讲,通过一个简单的谓词就可以轻松分析出它的跳转目标。不透明谓词的使用就大大增加了分析的难度。在构造谓词的时候,一些数论中的结论或经验数据公式是我们必须掌握的。例如为了得到uintVal_4= 4,我们可以使用如下数学变化:
unsigned int _nv_state_helper_51 = 304; unsigned int _nv_bloc _r53=_nv_ state_ helper_51- ((_nv_state. _helper_51 * 37744U) >> 18U) * 6U; unsigned int uintVal _4= (50U - _nv_block _r53);
字符串加密技术是对程序代码中的特定字符串进行加密,同时自动随机生成一个字符串解密函数,完成针对特定字符串加密、解密的完整过程,在不影响原有程序正常运行的前提下,增加逆向分析程序代码的难度。例如App程序中有如下一行代码:
_mutableCodersAccessQueue=dispatch_queue_create("com.hackemist.SDWebImageCodersManager", DISPATCH_QUEUE_CONCURRENT);
作为开发者,我们认为dispatch_queue_create()中的两个参数较为敏感,需要进行加密处理,处理后这一行代码变为:
_mutableCodersAccessQueue=dispatch_queue_create(code_ protector_c_get_str_0(),(( _bridgedispatch_queue_attr_t)&(_dispatch_queue_attr_concurrent)));
code_protector_c_get_str_0()是返回类型为字符串指针的字符串解密函数,这个函数运行的结果就是原来的包名参数com. hackemist.SDWwebImageCodersManager
。这行代码进行字符串加密前后的对比情况如下所示。
App在字符串加密前的部分代码:
glBindAttribLocation( program, ATTRIB_VERTEX, "myVertex" );
glBindAttribLocation( program, ATTRIB_NORMAL, "myNormal" );
glBindAttribLocation( program, ATTRIB_UV, "myUV" );
App在字符串加密后的部分代码:
glBindAttribLocation(_local_var_program_42, ATTRIB_VERTEX,code_protector_c_get_str_3();
glBindAttribLocation(_local_var_program_42, ATTRIB_NORMAL, code_protector_C_get_str_4());
glBindAttribLocation(_local_var_progran_42, ATTRIB_UV, code_protector_C_get_str_5());
字符串加密常见的技术实现是将原始的字符串用一个解密函数去实现, 在程序运行时,解密函数再使用对应算法还原原始的字符串。例如, 通过原始字符串Str与key进行异或操作Str^Key=EnStr,对于加密后的字符串EnStr进行解密,可再做一次异或操作EnStr^Key=Str,该方法执行效率很高,所以异或操作常常用于对字符串进行简单加密。在以下这段字符串加密处理函数里,它可以保护"bplist"字符串原文。
#include <stdio.h> char* code_protector_c_get_str_4(){ static Char code_porector_str_key_3= (char) 0x54; static char code_protector_encrypted_str[8]={(char) 0xF9,(char) 0x37,(char) 0x26, (char) 0x3B, (char) 0x31,(char) 0x2A, (char) 0x2E, (char) 0x5B}; int i; if((code_protector_encrypted_str[0] %2) != 0) { code_protector_ encrypted_ str[0] += 1; for(i=1; i<8; ++) { code_ protector_encrypted_str[i]= code_ protector_encrypted_str[i]^(char) ((code_ protector_str_key_3 + i) % 256); } } return (char *) code_protector_encrypted_str + 1; } int main(){ printf("string before processing is: %sn", "bplist" ); printf("string after processing is: %sn", code_protector_c_get_ str_4()); return 0; }
运行以上程序,输出结果如下:
string before processing is: bplist string after processing is: bplist
这说明当输入字符串”bplist"后,经过函数code_protector_c_get_ str_4()
进行变换,字符串的值依然不变。
本节的App防护措施能够解决安全测试过程中出现的防反编译、本地数据存储和网络传输数据加密问题。
dex文件是App得以在Android系统Dalvik虚拟机中运行的可执行程序文件.类似于Windows系统的exe文件,每个apk安装文件中都必须包含dex文件。dex文件里面包含了AndroidApp的核心代码,因此,针对dex文件的加固防护成为了静态防护技术的重中之重。针对dex文件的加固方法有4种,分别是dex文件整体加壳、程序方法抽取加固、VMP加固和字符串加密,接下来我们逐一进行介绍。
dex文件整体加壳的基本原理是对classes.dex这个文件进行整体加密,将加密后得到的文件 存放在apk文件的资源文件中,并在App运行时将加密后的classes.dex文件在内存中进行解密,再让Dalvik虚拟机动态加载解密后的原始clases.dex文件并执行。 对dex文件整体加壳的加固流程如图所示。
图:App整体加壳加固技术的执行流程
1.加密原始dex文件
App开发者通过综合考虑安全需求、程序复杂度和运行效率,设计针对dex文件的加解密算法,将原apk文件中的casses.dex文件进行加密,加密后的文件存储在原apk文件的assets资源目录中。下面的示例代码将classes.dex进行了简单的异或加密,并且将其以数组的方式保存在了payloadArray变量里。
File payloadSrcFile=new File(“classes.dex”); byte[] payloadArray =encrpt(readFileBytes(payloadSrcFile));
方法readFileBytes()的示例代码如下:
private static byte[] readFileBytes(File file) throws IOException{ return Files.readAllBytes(file. toPath());}
encrpt()方法的实现中使用了字符串TheXorKey作为异或的密钥,
示例代码如下:
private static byte[] encrpt(byte[] srcdata){ String key="TheXorKey" ; byte[] keyBytes=key.getBytes(); byte[] encryptBytes=new byte[srcdata.length]; for(int i=0; i<srcdata.length; i++){ encryptBytesl[i]=(byte) (srcdata[i] ^ keyBytes[i % keyBytes.length]); } return encryptBytes; }
2.编写壳程序
为了保证原有App程序的正常运行,开发者需要编写一个App的壳程序,用于定位存储在 assets 资源目录中的classes.dex 加密文件,并通过解密算法对该加密文件进行解密,得到原始的dex文件,加载至内存中运行。
为了增加App壳程序的安全性和保密性,可将解密算法及加载原dex文件等操作通过JNI 的方式打包到so文件中进一步隐藏并执行, 然后在App的壳程序中通过使用Java层的程序代码调用so文件中的解壳函数,完成原始dex文件的解密和加载,保证原有App的正常运行。将App壳程序中的部分功能转移到so文件中,会增加攻击者逆向破解壳的难度,不但使破解流程变长,而且要求攻击者具备对so文件的逆向分析能力。
3.修改程序运行入口
在加壳的dex文件中,要保证程序一开始就运行App的壳程序,否则App的运行流程就会紊乱。Android App的程序运行入口在AndroidManifest.xml文件中设置,通过定义Application类以保证App在启动时优先运行壳程序。如果AndroidManifest.xml文件中存在Application类,则需要修改成壳程序的Application类;如不存在,则需要添加壳程序的Application类。加载过程程结束后,则需要修 壳程序就将程序的控制权交给解密后的原始classes.dex文件,解密后的代码开始执行原有App的功能,接下来的步骤就跟加壳前的App运行流程一致了。
Dex文件整体加壳技术是针对classes.dex文件进行整体的加解密操作。App一旦正常运行,就会在内存中解密出原始的classes.dex文件,即在一片连续完整的内存中存储解密后的App程序代码。攻击者通过修改Dalvik虚拟机,有可能以内存转储的方式获得解密后的App程序代码。可以采取些打补丁的方法来增加破解难度, 例如类加载结束后, 抹掉或者混淆内存中dex文件的头部或者尾部信息,但这些方法治标不治本,无法从本质上解决问题。针对dex文件整体加壳能被内存转储的方式绕过这一弱点,在对给定apk文件实现整体反编译的基础上,采用基于程序方法抽取的方式进行加固保护可以进步增加apk文件被破解的难度。
本节的App防护措施能够解决安全测试过程中出现的防反编译、防篡改、本地数据存储和网络传输数据加密问题。
程序方法抽取是指将App中原始dex文件中的函数方法实现抽离到加密文件中,并确保函数抽离后的App还能正常运行的一种加固方法。其基本原理是利用Java虚拟机方法执行机制,在App执行某个方法时,才动态解密该方法的程序代码,并以不连续的方式存到内存中。当App没有执行该方法时,该方法的程序代码不会被解密到内存中。
如果将前一节对dex文件进行整体加壳比作“程序文件保护”的话,那么程序方法抽取加固就相当于“程序运行时在内存中的代码保护”,这种加固方法可对App代码中的每个方法做单独加密。App在运行且需要执行具体方法的情况下,才对需要执行的方法在内存中进行动态解密,进而正常运行解密后的方法。因此,App在运行但还没有执行具体方法的情况下,可以防止攻击者通过转储内存导出完整的原始dex文件。即使攻击者转储内存,得到的dex文件也是不完整的,缺少还未运行的App程序方法。
App在方法抽取加密前的部分代码如下所示:
package com. test. package.activities; import android.app.Activity; import android.OS.Bundle; import android.widget.TextView; import com.test.aspiredoctor.R; public class AboutAppActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.about_app); ((TextView) findViewById(R.id.aboutVersion)).setText("版本: 1.0"); ((TextView) findViewById(R.id.right)).setText(""); } }
加固前,AboutAppActivity类可以被反编译。加固后,AboutAppActivity类被抽取保护,不能被反编译了。程序方法抽取加固的具体实现过程如下。
(1)确定方法。开发者根据App中各个程序方法的安全防护等级确定需要加固的程序方法列表,包括类名和方法名,将需要加固的程序方法写入一个配置文件中。
(2)方法抽取。读取配置文件获取App需要加固的类名和方法名,解析App安装文件中的dex文件,定位需要加固的类和方法代码,将要加固的程序代码及对应Dalvik虚拟机执行所需的字节码提取出来,在App的壳程序中实现一套代码加解密程序,将提取出来的程序方法和字节码进行加密并写入dex文件以外的加密文件中。
(3)解密运行。当App需要运行某个已被加固的程序方法时,壳程序会从加密文件中将该程序方法的程序代码和字节码解密至内存中,以供Dalvik虚拟机执行,保证App的正常运行。
本节的App防护措施能够解决安全测试过程中出现的防反编译、防篡改、本地数据存储和网络传输数据加密问题。
传统的软件代码保护技术主要包括代码混淆和软件加壳这两种技术。前两节介绍的dex文件整体加壳和程序方法抽取技术类似于计算机软件的加壳保护技术。都是原始代码隐藏技术,最终通过Android的Dalvik/ART 虚拟机执行的还是壳程序解密后的原始程序代码。因此,攻击者可以通过修改虚拟机的运行过程来对加固后的App进行脱壳。VMP加固就是针对这种破解方式的防护技术,使得攻击者即使修改了虚拟机也无法完成脱壳。
VMP加固通常是将保护后的代码放到自定义的虚拟机中运行。VMP加固是基于虚拟化保护技术实现的。虚拟化保护技术是使用一种全新的“语言”来翻译原来的代码,这种“语言”只有自定义的虚拟机引擎才能够理解,在没有任何参考资料的前提下,攻击者想要学会这种“语言’是非常困难的。因此,基于虚拟机的软件保护被业界认为是当前破解难度最大的保护方式,它不但加大了逆向分析的难度,而且极大地增加了还原代码的难度。简单来说,VMP加固是将原来App的可执行代码转换为自定义的字节码,只能在自定义的虚拟机中执行。犹如新建了一套与Dalvik/ART虚拟机不同的虚拟机指令体系,使得App在新的指令体系中运行。当攻击者熟悉了Dalvik/ART虚拟机的指令体系后,新的指令体系无疑大大增加了破解成本。
经VMP加固的App,其中未做指令保护的方法还是以正常的字节码的形式由Dalvik/ART虚拟机执行,而经指令保护后的方法则由自定义的虚拟机执行。对于攻击者来说,即使通过内存转储等动态分析方法拿到了自定义的字节码,还需要分析和理解自定义的字节码格式,因此攻击者逆向这种自定义虚拟机的成本会大大增加。另一方面,VMP加固也是基于方法的虚拟化保护,可以针对一一个App构建多个不同虚拟化的解释引擎,对App中不同的方法采用不同的虚拟化执行引擎,通过增加虚拟机异构特性的实现方式进一步提高App的安全防护能力。VMP加固技术中的虚拟机如图所示。
图:VMP 加固技术中的虚拟机示意图
VMP加固的具体实现过程如下:
(1)将APP中需要保护的java函数转换为native函数:
(2)将APP中需要保护的Java函数对应的字节码进行指令转换,使其符合自定义虚拟机的指令格式,得到能够运行在自定义虚拟机中拟机中的字节码;
(3)实现针对这个函数的Native函数,该函数的作用是读入经过虚拟化指令转换的新的字节码,并解释执行。
下面我们看一一个VMP加固的示例。App在VMP加固前的部分代码如下所示:
package com.test.package.activities; import android.app.Activity; import android.os.Bundle; import android. widget .TextView; import com. test. aspiredoctor.R; public class AboutAppActivity extends Activity { protected void onCreate( Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.about_app); ((TextView) findViewById(R.id.aboutVersion)). setText("版本: 1.0"); ((TextView) findViewById(R.id.right)).setText(""); } }
加固前,AboutAppActivity 类可以被反编译。App在VMP加固后的部分代码如下所示:
加固后,函数被修改为JniLib.V的调用方式。
在VMP加固技术中,Dalvik指令集的映射是核心工作,将App中java代码对应的操作数映射到native代码中。在实践中,为了增加VMP加固的强度,Dalvik虚拟机的原始opcode与VMP虚拟机的opcode并不只是一对多的关系,这增加了攻击者逆向的难度。本节的App防护措施能够解决安全测试过程中出现的防反编译、防篡改、防调试、本地数据存储和网络传输数据加密问题。
开发者在App开发过程中,不可避免地会使用各种明文存储的字符串信息,这些信息可能是攻击者破解App的入口点,比如错误提示、加密密钥等敏感的字符串信息很容易被攻击者通过反编译获取,为破解App提供关键线索。对dex文件中的字符串进行加密可以提高逆向分析App的攻击成本,增强App的安全防护能力。字符串加密的具体实现过程如下:
(1)提取dex文件内的明文字符串信息,确定需要加密的字符串内容;
(2)通过“一次一密”的字符串随机加密方式对待加密的字符串进行加密保护;
(3)将随机加密后的密文字符串回填到原字符串的位置;
(4)在app运行过程中,壳程序动态解密被加密的字符串,即只对执行过程中用到的字符串进行解密,没有使用的字符串仍然处于加密你保护状态。 App经字符串加密前的部分代码如下所示
App经字符串加密后的部分代码如下所示:
public class BackupSetActivity extends Activity { private final String FIRST_ DELAY = Helper.d("G6F8AC709AB0FAF2CA0F89" ); private final String IF_BACKUP_AUTOMATIC = Helper.d("G6085EA18BE33A03CF631915DE6EACED67D8AD6"); private final String IF_BACKUP_STRANGE = Helper . d("G6085EA18BE33A03CF631835C0E4CDD06C"); private final String INTERVAL_DAY = Helper.d("G6D82CC"); private final String INTERVAL_ TIME = Helper.d("G7D8AD81F"); private final String SMS_ MSG = Helper.d("G7A8EC625B223AC"); }
在进行字符串加解密实现的过程中,可以使用一些相对成熟的第三方开源工具库。因此就以stringfog为例说明使用方式。stringfog开发了gradle插件并更新到了jcenter上,在集成的时候非常便捷。下面是简要的代码示例。
在根目录build.gradle中引入插件依赖:
在build.gradle中配置插件:
apply plugin: 'com.android.application' apply plugin: 'stringfog' stringfog{ //加密时所使用的密钥 key 'THIS_IS_THE_KEY' enable true implementation 'com.github.megatronking.·stringfog.xor .StringFogImpl' //对于指定包名内的字符串进行处理,如果不配置则为全部 fogPackage=['com.example.myapplication'] } android { compileSdkVersion 27 defaultConfig{ applicationId "com.example.myapplication" minSdkVersion 20 targetSdkVersion 27 versionCode 1 versionName "1. 0" } buildTypes { release{ minifyEnabled false proguardFiles getDefaultProguardFile(' proguard-android.txt'), ' proguard-rules.pro' } } } dependencies{ implementation fileTree(dir:'libs', include: ['*.jar']) implementation com.android.support.constraint:constraint-layout:1.1.3 testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test. espresso:espresso-core:3.0.2' compile 'com.github.megatronking.stringfog:xor:1.1.0' }
示例代码如下:
package com.example.myapplication;
import android.support.v7.app.AppCompatActivity;
import android.oS.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState);
String strMessage = "String to be encoded"; //明文字符串 Log.d("MainActivity", strMessage); //明文字符串
setContentView(R.layout.activity.main);
}}
最后,打包apk文件并验证结果。在顶层使用命令./gradlew clean assembleDebug进行打包生成apk文件。使用工具JD-GUI打开生成的apk文件进行逆向,并在看明文字符串是否存在,结果显示字符串已经进行了加密处理。
本节的APP防护措施能够解决安全测试过程中出现的防反编译、防篡改、防调试、本地数据存储、挂钩框架检测和网络传输数据加密问题。
随着对App运行效率的需求不断增加,出现了采用HTML、JavaScript 等方式开发App的程序框架,例如PhoneGap、IBM Worklight等。这类开发框架解决了移动应用开发的跨平台移植问题,开发的程序可以在多个平台运行使用,提高了App开发的效率。
但是,这种便捷的开发框架带来了非常严重的安全问题: HTML、JavaScript等脚本文件需要以明文的方式存放在安装包的资源文件中或者运行时的本地数据中,攻击者甚至不需要进行逆向就可以直接拿到源代码,因此需要对使用这类开发框架开发的HTML、JavaScript 等文件进行加密保护。保护技术主要有资源加密和数据透明加密两种。
1.资源加密
通过拦截本进程读取资源的两数,对需要解密的资源自动进行解密操作。资源加密技术能保证脚本文件在安装包中以加密的方式存在。在PhoneGap、 IBM Worklight这些程序框架中,通常需要将脚本文件释放到手机的本地存储上,然后在从本地存储进行加载。此时,本地存储的脚本文件则要求是明文存储的。因此,需要辅助数据透明加密技术才能保证手机存储上的脚本文件也是密文的。
2.数据透明加密
运行时拦截本进程所有的I/O操作,如果I/O操作的对象是需要透明加密的文件,则自动对读和写的操作进行相应的解密和加密操作。具体来说,如果I/O操作的对象是需要透明加密的文件,在本进程读取该文件时,自动进行解密操作,然后再将解密后的数据交给本进程处理。如果本进程进行的是写操作,则自动进行加密操作,然后将加密后的数据写据交给本进程处理。
下面我们介绍简要的技术实现。
首先是拦截底层I/O函数,为透明数据I/O做准备。为了实现透明数据加密,即无感的方式进行文件I/O访问,至少要完成open()、read()、 write()、 close()、fopen()、fclose()等系统函数的拦截。下面是以write()函数为例的示例代码:
static ssize_ t (*tbc_write) (int fd, const void *buf, size_t_count); static ssize_t libc_write_ stub(int fd, const void *buf, size _t_count) { ssize_t ret; char *tmpbuf= (char *) malloc(sizeof(char) * count); memcpy( tmpbuf, buf, count); off_t curpos = lseek(fd, 0, SEEK_CUR); do_data_crypt(curpos, tmpbuf, count); ret = libc_write(fd, tmpbuf, count); free( tmpbuf); return ret; }
其次是进行数据加密/解密。对于数据文件的加密/解密,通用的对称分组加密算法尽管可以做到较高的安全强度,但是由于数据文件并非是静态处理的,它们经常在随机位置被存取,因此在实践中很少使用。在此推荐的加密/解密算法为流加密算法,即在上述示例中,在实现 do_data_crypt()函数实现时可以选择R**、祖冲之等算法。
通过资源加密技术和数据透明加密技术,能保证HTML、JavaScript 等脚本文件无论是在安装包中还是本地存储上都以密文的形式存在,提高了这些资源文件被破解的难度。
本节的App防护措施能够解决安全测试过程中出现的防反编译、防篡改和防调试问题。
Android App中的so文件是采用C++语言开发的动态库。针对so文件的逆向要求攻击者有一定的汇编语言基础,相比Java语言的dex文件反编译,so文件的逆向分析难度更高。但是so文件依然是可以被逆向分析破解的,因此需要对so文件进行加密保护。so文件保护的要求有以下两点。
• 保护so文件的代码不被逆向分析。so文件加密保护技术可以保证so文件的汇编代码不被攻击者通过IDA等反汇编逆向工具逆向分析。
• 保护so文件内嵌的加密算法及密钥等不泄露。针对so文件中的对称加密算法,so文件的物装可以保护加密算法及内嵌密钥,保证被加密算法以及加密的密钥不被攻击者通过IDA等反汇编工具逆向分析得到。
so文件的加密保护护技术与PE文件的加壳技术类似。加壳技术是指利用特殊的算法,将可执行程序文件或动态链接库文件的编码进行改变, 以达到加密程序编码的目的, 阻止IDA等反汇编工具的逆向分析,如图所示。
图:so文件加密示意图
so文件加固的具体实现过程主要有以下5个步骤。(1)汇编代码压缩及加密保护 在代码层面,对so文件中的汇编代码进行压缩后再进行加密保护,从而阻止代码被逆向分析。
(2)so文件的elf数据信息保护 so文件本质上是一个elf格式的文件,在elf格式中定义了大量的辅助数据信息,如动态定位信息、函数地址信息、符号表等信息。对于这些信息,so文件加固技术采用了清除映射等手段进行隐藏保护,从而使破解者无法修复这些数据信息。
(3)导入、导出函数隐藏 对so文件中的导入、导出函数信息进行隐藏,达到无法通过IDA等工具查找到对应函数信 息的效果。
(4)解密代码动态清除 采用解密代码动态清除技术,可进一步加强 so文件的安全性,所谓解密代码动态清除是指某个函数执行完成后,该函数的代码会从内存中清除掉,在下次执行的时候,函数会被重新解密并执行。通过这种技术,使程序在运行期间,内存中不存在完整的解密代码,从而极大地增加了试图通过内存转储方式进行破解的难度。
(5)so文件与App绑定 so文件绑定技术的技术原理是将so文件的解密密钥放在Java代码加固的apk文件中。因此,如果脱离了加固的apk,,该so0文件由于无法取得密钥,从而无法运行。此外, 在加固后的Ap中,如果 s0文件被替换,由于Java代码无法解密替换后so文件中的代码,程序也将无法正常运行。
本节的App防护措施能够解决安全测试过程中出现的防反编译、防篡改、防调试问题。
本章围绕App的静态防护技术进行了介绍。首先,从源代码保护的角度介绍了控制流平坦化、不透明谓词和字符串加密这3种实现方法,接着介绍了针对dex文件、资源文件和so文件这3种文件级的加固保护技术。其中,针对dex文件加固保护进行了重点展开,介绍了dex 文件整体加固、程序方法抽取加固、VMP加固和字符串加密这4种实现方法。通过阅读本章的内容,相信你会对App源代码级、文件级、方法级的静态安全防护技术有一定的了解。