前言
随着移动互联网的爆发性增长,人们对移动应用的需求变得越来越复杂,企业在带给用户众多便利和享受的同时,却容易忽视应用自身的安全性问题,一旦遭受攻击,就会给企业和用户的经济或声誉带来影响。本文主要是站在企业的角度,阐述如何通过给android SO(动态链接库)加壳来提升移动APP的安全性,减少SO被逆向反汇编分析的风险。
注:本文只做单方面的总结,如果对整体提升移动应用安全性有需求的人员,可参考作者另外一份文档:《移动应用安全开发指南v1.0(Android)》。
撰写本文的目的:
1、 为移动应用开发人员提供安全加固技术指导,作为发布时加固的实际依据。
2、 对实施过程做详细的记录和总结,为需要单独创建加固环境的人员提供具体细节,避免或少走弯路。
建议使用方法:
对于需要创建加固环境的人员,请阅读“创建加固环境”章节,对于只需要在加固环境下对SO加固的人员(比如开发人员),只需了解“加固步骤”章节即可。
创建加固环境(X64 Linux)
1、下载UPX和依赖组件的源码
UPX -3.92-src:https://www.pysol.org:4443/hg/upx.hg/archive/tip.tar.gz
注:v3.92为写本文档时的最新官方非正式发布版本(正式发布版本为v3.91)
下载入口如下:
LZMA4.43:http://nchc.dl.sourceforge.net/project/sevenzip/LZMA%20SDK/4.43/lzma443.tar.bz2
UCL1.03:http://www.oberhumer.com/opensource/ucl/download/ucl-1.03.tar.gz
2、删除UPX壳描述信息
编辑$(UPX_SRC_ROOT)/src/packer.cpp,删除该文件中定义的关于UPX壳的相关描述信息(详情请参考附录à相关问题总结5.5)。
3、编译
3.1、编译zlib:
tar zxvf zlib-1.2.3.tar.gz
cd zlib-1.2.3
make //编译生成libz.a
cp libz.a /usr/lib64/libz.a //拷贝到系统默认的动态链接库路径下。
3.2、编译UPX并执行
cd $(UPX_SRC_ROOT) //进入UPX源码根目录
CXX=g++ UPX_UCLDIR=/home/soft/ucl-1.03 UPX_LZMADIR=/home/soft/lzma-4.43 UPX_LZMA_VERSION=0x443 make all //编译UPX
说明:UPX_UCLDIR和UPX_LZMADIR的值分别为UCL和LZMA解压后的根路径,UPX_LZMA_VERSION环境变量则指定了LZMA的版本。
编译成功后可在$(UPX_SRC_ROOT)/src下查看到可执行文件upx.out,如下图所示:
执行效果如下:
加固步骤
1、配置NDK集成开发环境
参考附录《配置和使用NDK集成开发环境》的《配置步骤》章节。
2、修改native代码
2.1、在native代码中定义全局变量用于增加生成的二进制的体积,例如:
C:int const dummy_to_make_this_compressible[100000] = {1,2,3};
C++:extern "C" int const dummy_to_make_this_compressible[100000] = {1,2,3};
注意:如果编译出来的库本身足够大,则此步骤可省略。
2.2、在native代码中声明_init()函数,用于在编译时生成_init段,例如:
C:void _init(void){}
C++:extern "C" {void _init(void){}}
注意:C和C++代码定义或声明的方式是有所区别的,在C++中必须使用extern “C”关键 字进行修饰,被extern "C"修饰的变量和函数是按照C语言方式编译和连接的
2.3、在native代码中使用宏定义混淆函数名,用于增加静态反汇编分析难度,例如:
#define startSimpleWifi sSW
#define sendData sD
……
3、对SO库文件加壳
3.1、打开cygwin,进入Android工程目录,在NDK环境中编译native代码(详情可参考 附录《配置和使用NDK集成开发环境》的《编译native代码》章节),也可以通过CDT 自动编译(参考《配置和使用CDT编译环境》),编译通过后将在libs目录下生成SO 动态链接库文件。
3.2、将编译生成的SO库文件上传到加固服务器(本文将其和UPX执行文件放同一目录), 如下图所示:
3.3、对SO库进行加壳,常用命令:upx.out –o libhello-jniupx.so libhello-jni.so
4、验证
4.1、在eclipse的Android工程中使用加壳SO替换原有的SO,如图所示:
4.2、将使用加壳SO的Android程序安装到设备或模拟器中执行功能验证,若各项功能均正 常则表示加固成功。
加固前后效果对比
1、加固前反汇编:
2、加固后反汇编:
附录
1、术语表
术语 | 定义 |
UPX | UPX是一个著名的压缩壳,主要功能是压缩可执行文件(比如exe,dll和elf等文件),有时候也可能被病毒用于免杀。 |
加壳 | 加壳的全称应该是可执行程序资源压缩,是保护文件的常用手段。加壳的程序经常想尽办法阻止外部程序或软件对加壳程序的反汇编分析或者动态分析,以达到它不可告人的目的,这种技术也常用来保护软件版权,防止被软件破解。 |
交叉编译 | 就是在一个平台上生成另一个平台上的可执行代码,比如在X86 Linux上编译出可在ARM Linux上执行的程序。 |
JNI | JNI是Java Native Interface的缩写,中文为JAVA本地调用,它允许Java代码和其他语言(比如C/C++)写的代码进行交互。 |
反汇编 | 把目标代码转为汇编代码的过程,也可以说是把机器语言转换为汇编语言代码、低级转高级的意思,常用于软件破解、外挂技术、病毒分析、逆向工程、软件汉化等领域。 |
2、配置和使用NDK集成开发环境
2.1、配置步骤:
2.1.1、http://www.cygwin.com/下载cygwin并双击安装。
2.1.2、选择从internet安装。
2.1.3、选择一种能连上网络的方式
2.1.4、建议选国内的镜像站点(速度快)
2.1.5、在search中输入”make”,选择Devel列表中的所有package进行安装,选择方法是点击package名,由keep变成install字样即可。注:本机已安装,故显示为reinstall。
2.1.6、继续下一步直至安装完毕。
2.1.7、运行cgywin bash,输入cygcheck -c cygwin命令,若显示Cygwin 的package信息则表 示运行正常。
2.1.8、分别运行gcc –version、g++ --version make –version和gdb –version来检查相关组件是 否运行正常。
2.2、编译native代码:
2.2.1、使用NDK编译一个程序,首先我们要找到我们cygwin的程序安装目录,找到一个 home\<你的用户名>\.bash_profile文件,如下图所示
2.2.2、在该文件末尾添加ndk=/cygdrive/<你的盘符>/<android ndk 目录> 例如: ndk=/cygdrive/f/android/adt-bundle-windows-x86-20131030/android-ndk-r9d
export ndk
注:"ndk"这个名字随便起,因为后面要经常使用,建议不要太长。如下图所示
2.2.3、之后重新打开cygwin,输入 cd $ndk,如果进入了android ndk 目录,证明环境变量 设置成功了
2.2.4、尝试使用NDK编译android NDK的样例程序hello-jni,路径为:
<android ndk 目 录>/samples/hello-jni,比如:
F:\android\adt-bundle-windows-x86-20131030\android-ndk-r9d\samples\hello-jni
2.2.5、进入工程根目录
2.2.6、执行$ndk/ndk-build命令,执行成功后它会自动生成一个libs目录,把编译生成的.so文件放里边,使用file命令可查看到文件为经过交叉编译的ARM Linux 动态链接库。
注:执行$ndk/ndk-build实际等于执行NDK目录下的ndk-build命令,如下图所示:
3、配置和使用CDT编译环境(非必须)
3.1、为eclipse安装CDT插件,也可以直接下载带CDT插件的eclipse(略)。
3.2、在eclipse中选择右键选中项目,选择Properties,在弹出的对话框中选择Builders。
3.3、点击对话框中的New新建一个编译器,在新弹出对话框中选择ProgramàOK。
3.4、对新建编译器进行配置,如下图:
参数解析如下:
Name:编译器的名字,可随便取。
Location: <你cygwin安装路径>\bin\bash.exe程序,即cygwin的bash程序的路径。
Working Directory: 你cygwin安装路径>\bin目录。
Arguments:给cygwin bash传递的参数,里面主要包含进入工程目录并使用NDK执行编译的命令。
3.5、接着切换到Refresh选项卡,给Refresh resources upon completion打上钩,如图:
3.6、最后切换到Build Options选项卡,勾选上最后三项,如下图所示:
3.7、点击Specify Resources按钮,选择资源目录,勾选你的项目目录即可,如下图所示:
3.8、保存配置后将回到Properties对话框,点右边的Up按钮,把它排到第一位,否则C代码的编译晚于Java代码的编译,会造成你的C代码要编译两次才能看到最新的修改,如图:
3.9、在eclipse中build Android工程,可以看到同时编译native代码并生成了SO库。
4、APK重打包流程
4.1、下载并安装APK改之理,下载地址:http://www.xiaomiren.net/
4.2、启动APK改之理,选择项目à打开APK 打开要重打包的APK程序。
4.3、打开成功后可看到原有的工程目录结构,打开libs目录可以看到APK需要载入的SO库。
4.4、删除要替换的SO,并把修改过的SO(比如经过加壳的SO)添加进来。
4.5、重新编译APK,即可安装到设备中运行并观察替换SO后的效果。
5、相关问题总结
5.1、编译UPX出现“cannot find -lz”错误。
分析:原因是链接器LD没有找到编译出来的zlib库libz.so或libz.a。
解决方法:将libz.so或libz.a拷贝到系统默认的动态链接库路径下,比如/usr/lib,/usr/lib64 等。
5.2、编译UPX出现“CantPackException: DT_TEXTREL found; re-compile with -fPIC”错误。
分析:这是早期NDK版本的BUG。
解决方案:使用NDK9或以上的版本
5.3、编译UPX出现“NotCompressibleException”错误。
分析:UPX对被加壳的二进制文件有最小限制,太小的文件将无法被加壳。
解决方案:在native代码中定义足够大的数据变量,使得编译出来的二进制文件容易达 到UPX的要求(参考《加固步骤》之《修改native代码》章节)。
5.4、编译UPX出现“UnknownExecutableFormatException”错误。
分析:被加壳的二进制文件必须存在init段,否则UPX将无法脱壳还原原始代码。
解决方案:在native代码中定义_init()方法,需要注意C和C++的区别(参考《加固步 骤》之《修改native代码》章节)。
备注:查看二进制文件是否存在init段的命令:readelf –dynamic xxx.so,如下图:
5.5、使用UPX加壳的SO,在eclipse中启动Android程序时出现”Fatal signal…”错误,如图:
分析:此错误是因为UPX解析某些特殊字符处理不当导致的,该BUG已经有人提交UPX 官方解决,但是当前官方正式发布的正式版本(V3.91)并没有fix该问题,而是在未正 式发布的V3.92才解决了该问题,因此本文档使用源代码版本为V3.92而非V3.91。
解决方案:下载使用V3.92源码,下载入口请参考《创建加固环境》章节。
5.6、为何删除UPX源码中的软件信息,以及如何定位查找这些信息。
分析:UPX对文件进行加壳时会把这些信息写入壳内,通过静态反汇编可查看到这些壳信息,进而寻找对应的脱壳机进行脱壳,使得攻击难度降低。
解决方案:在UPX源码中删除这些信息,并重新编译,步骤如下:
5.6.1、使用原始版本对文件进行加壳。
5.5.2、使用IDA反汇编加壳文件,在反汇编文件的上下文中查找UPX壳特征字符串, 如下图所示:
5.5.3、在UPX源码中查找这些特征字符串(建议使用Search and Replace),并一一删除, 如下图:
5.5.4、重新编译UPX(参考“创建加固环境”章节)。
5.7、在没有eclipse源码工程的情况下如何直接替换APK的SO并观察结果?
解决方案:参考《附录》之《APK重打包流程》,值得注意的是,如果Android程序中 内置了检查签名合法性的安全机制,使用该方法的前提是先破解该签名验证机制。