入门级加固——3种加固方式学习记录
看雪论坛作者ID:顺利毕业
本帖子涉及到3种加固:
1、在java层为.apk文件进行加固:跟着姜维前辈学~
2、在native层为.dex文件进行加固:逆向一款恶意软件,从中学习到它的加固方式。在后面的帖子的第二部分将着重贴出逆向的全过程。
3、在native层为.so文件进行加固:依旧跟着姜维前辈的步伐~
备注: 本帖子重点在于记录自己学习过程中遇到的问题及解决方法,以及部分原理知识。
本帖子作为学习记录,会部分赘述姜维前辈帖子里的内容,但不会全写,会附上具体的连接,有兴趣的同学最好提前先康康~
本文也将高亮自己学习过程中遇到的问题及对应的解决方法,方便大家查看。
还有一部分,是我逆向一款在native层进行加固的恶意样本,逆向的过程也会贴出来。
对于一些比较绕的地方,我根据自己的理解画了几张图,加深理解。
本帖子比较核心的内容为:遇到的问题及解决方法、逆向native层加固的样本。感兴趣的同学可以挑着看。
目录
一、在java层为.apk文件进行加固学习资料Android壳原理 Dex文件基础知识加固 原理 操作实践 基础操作 注意事项二、在native层为.dex文件进行加固壳原理 大致流程样本分析三、在native层为.so文件进行加固学习资料加密so的section 原理 实践加密so的函数 原理 实践两者比较四、3者比较>>>> 学习资料
学习资料
>>>> Android壳原理
Android壳原理
Dex文件基础知识
>>>> 加固
加固
原理
源程序项目(需要加密的Apk) 脱壳项目(解密源Apk和加载Apk) 对源Apk进行加密和脱壳项目的Dex的合并
操作
原理:通过反射置换android.app.ActivityThread 中的mClassLoader为加载解密出APK的DexClassLoader,该DexClassLoader一方面加载了源程序、另一方面以原mClassLoader为父节点,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码。随后找到源程序的Application,通过反射建立并运行。这里需要注意的是,我们现在是加载一个完整的Apk,让他运行起来,那么我们知道一个Apk运行的时候都是有一个Application对象的,这个也是一个程序运行之后的全局类。所以我们必须找到解密之后的源Apk的Application类,运行的他的onCreate方法,这样源Apk才开始他的运行生命周期。这里我们如何得到源Apk的Application的类呢?从源Apk的Androidmanifest.xml文件的meta标签获取源程序apk中的application对象。
操作:从脱壳程序apk中找到源程序apk,并进行解密操作;从源程序apk中获取dex文件、so文件;在脱壳程序的application中的oncreate方法中执行操作,找到源程序的application程序,让其运行;需在脱壳程序的AndroidManifest.xml中声明一下源程序中的Activity。
以二进制形式读取origin.apk文件形成数据流dataA,并获取该文件的大小sizeA; 以二进制形式读取脱壳dex文件形成数据流dataB,并获取该文件的大小sizeB; 采用自定义的加密方法,对数据流dataA进行加密,即加密origin.apk文件,形成数据流dataC; 设置壳dex的大小sizeC=sizeA+sizeB+4; 申请一个新的byte数组newdex,大小为sizeC; 将dataC拷贝到newdex的头部,紧随其后放置dataB的数据,在newdex的最后4个字节放置sizeA; 修改newdex中的file_size字段,即修改脱壳dex的file_size字段:对newdex计算其长度length,将该值替换掉newdex的file_size字段,即将newdex的第32-35共4个字节的地方修改成length。 修改newdex中的signature 字段,即修改脱壳dex的signature 字段:对newdex计算其sha1的值,将该值替换掉newdex的signature 字段,即将newdex的第12-31共20个字节的地方修改成sha1计算后的值。 修改newdex中的checksum 字段,即修改脱壳dex的checksum 字段:调用Adler32类,利用该类的实例对newdex计算其adler值,将该值替换掉newdex的checksum字段,即将newdex的第8-11共4个字节的地方修改成adler计算后的值。
>>>> 实践
实践
基础操作
准备待加壳apk
加壳程序
脱壳程序
解密源apk 初始化自定义类加载器 利用反射,设置LoadedApk中加载器对象为自定义加载器
获取源apk的Application名称 利用反射,生成正确的Application对象 利用反射,设置ActivityThread中的Application信息。(ActivityThread为当前主线程) 调用源apk的application对象的oncreate方法。
合体操作流程
注意事项
1、在教程《Android中的Apk的加固(加壳)原理解析和实现》:
https://blog.csdn.net/jiangwei0910410003/article/details/48415225 中,采用eclipse开发,其在将A.apk与B.dex合成为classes.dex时,利用new File()生成的classes.dex是直接生成在项目路径下的,但是用Android Studio不行,故直接在/sdcard/目录下生成该文件,并用adb将其拷贝到PC段。
2、欲使用7-zip打开dumpShell.apk并删除签名文件与classes.dex时,提示只读无法进行删除,对操作对象是/data/app/<myAppPkgName>/base.apk时也是同样只读,即便对其使用chmod 777操作也无法。
dumpShell.apk是自己用Android Studio生成的,若是用其他恶意软件的base.apk则可以正常用7-zip打开进行删除操作。具体原因暂未知道。故,用7-zip对其进行解压,解压后删除文件夹下的签名文件与classes.dex,放置目标classes.dex,随后将该文件夹下的所有文件一同压缩成一个zip文件,再对zip文件进行签名,称为apk文件。
3、问题:加固后将A.apk放置在dumpShell.apk中,运行dumpShell.apk时能调用A.apk的application类,但无法按照A.apk的运行周期调用到A.apk的mainActivity类。
原因:dumpShell.apk不应包含mainActivity类,即应只包含application类,在application类中完成脱壳且加载A.apk的任务。否则,将存在两个mainActivity(dumpShell.apk的,与A.apk的),则将运行dumpShell.apk的。
解决方法:去除dumpShell.apk的mainActivity类,且在dumpShell.apk的manifest文件中应该声明A.apk的mainActivity类。
4、问题:加固后将A.apk放置在dumpShell.apk中。单独运行A.apk时将输出A.apk的包名与mainActivity的组件名;运行dumpShell.apk时,尽管调用运行了A.apk,但其输出信息变成dumpShell.apk的包名,组件名称中的包名部分也变成了dumpShell.apk的包名。
(上面2张,是源apk单独运行的日志)
(上面2张,是将源apk加固进dumpShell.apk后,dumpShell.apk运行的日志)
原因:不详。不影响A.apk的运行,所以先不深究了。
5、问题:加固后将A.apk放置在dumpShell.apk中,运行dumpShell.apk时发现此时A.apk的mainActivity类能够被加载了,但是运行界面显示的依旧是dumpShell.apk的信息:
(图片分别为:单独运行A.apk,将A.apk加固进dumpShell.apk后运行dumpShell.apk)
原因:A.apk的mainActivity类在设置contentView时是使用R类去调用A.apk自身的activity_main.layout文件,而由于加固后的运行环境中R类是dumpShell.apk的R类(而非A.apk的),故即便调用了A.apk的mainActivity类,其在设置contentView时是将使用dumpShell.apk的R类去调用dumpShell.apk的的activity_main.layout文件,因而运行界面显示的是dumpShell.apk的信息。
>>>> 壳原理
壳原理
大致流程
提取原始apk的.dex文件,加密后重命名为dmeod.jar,将其放置在apk的Assets目录下 编写libdmeod.so文件,用于解密demod.jar文件并对其进行dexClassLoader操作 在原始apk中,新建application类作为壳的启动类,将其命名为com.vod.wbmp.yobl.chjiv,并重写attachBaseContext方法和onCreate方法。这两个方法,将调用libdeod.so文件内的方法 修改apk的manifest的application类名称为这个启动类的名称
>>>> 样本分析
样本分析
将原始apk的.dex文件加密成dmeod.jar文件,该dmeod.jar文件里包含apk的真实application类与入口activity类
在AndoridManifest.xml文件中,将apk的application入口修改成壳application类,即类chjiv
在chjiv类的attachBaseContext方法里调用native代码,实现功能:解密dmeod.jar文件并利用DexClassLoader对其进行加载,并将其设置成已加载apk的类加载器(即修改成为android.app.LoadedApk的mClassLoader),加载原始apk的入口activity类
在chjiv类的onCreate方法调用native代码,实现功能:调用与那时apk的真正application类里的attachBaseContext方法与DexClassLoader方法。
>>>> 学习资料
学习资料
>>>> 加密so的section
加密so的section
原理
加密时,利用so文件来获取段的偏移与大小的原理:
1、将so文件以二进制数据的形式进行读取,将头52个字节解析成Elf文件头hdr。
2、利用hdr获得e_shoff,即section header table offset节区头部表的偏移,该表中对于每个元素的数据大小是40个字节,该表中的所有元素个数总数存放在hdr的e_shsum中,每个元素代表一个节区的摘要信息,即:在e_shoff所指地址开始,存在着e_shsum个节区的摘要信息,每个节区的摘要信息占用40个字节。
3、针对每40个字节的元素进行解析(即解析每个节区的摘要信息),并将解析后的数据放置在so文件的shdrList变量中。
4、通过hdr获得e_shstrndx,即section header table’s “section header string table” entry offset(节区头部表的“节区头部字符串表”入口偏移),该值表示“节区头部字符串表”在“节区头部表”中的索引号,如该值为0x000a,则表示在section header table(节区头部表)中,第11个节区(索引从0开始)为字符串表。
5、以hdr的e_shstrndx作为index,从shdrList变量中获取第index个数据,即获得了节区头部字符串表的摘要shdr,简称为字符串表shdr。获取shdr的sh_offset,即获得字符串表的偏移地址offset,该offset所指的区域保存所有字符串。
6、遍历shdrList中的每个数据,即遍历每个节区的头部信息,将每个节区的头部信息记作变量tmp。针对每个节区的头部信息,获取其sh_name,该值为该节区名称在字符串表中的偏移,故用offset+sh_name即可表示该节区名称的总偏移sectionNameOffset。
7、在so文件的sectionNameOffset所指的位置开始,便代表了该节区名称的字符串实际存放位置。将该字符串与目标节区名“.mytext”进行比对,直到找到匹配的,即tmp此时所指的节区头部是.mytext的头部。
8、获取此时tmp的sh_offset与sh_size,即获取到了.mytext的偏移与大小。
解密时,利用so文件被装载后的内存数据来解密:
前期我们在加密时,将.mytext的偏移与大小存放在e_flags与e_entry中,故加密时应先获取这两个数据。
解密时,已经无法获得so文件,但能获得so文件被加载到内存后的数据。获取测试机上本进程的信息,即读取/proc/<pid>/maps的数据,并找到libsoShellDemo.so被加载到内存后的起始地址base。利用elf.h对base所指区域后面的数据进行解析,解析成so文件的头部ehdr。
Endr的e_flags存放.mytext的偏移,e_entry存放.mytext的大小(即为mySectionSize)。用e_flags的值加上base,即获得.mytext在内存中的位置text_addr。
由于.mytext属于代码段而非数据段,代码段不可写,故需改变内存中.mytext所处位置的读写权限,利用mprotect(start,len,prot)方法来进行修改。
mprotect作用的内存区间为n个内存页,故start必须是一个内存页的起始地址,且区间长度len必须是页大小的整数倍。但,.mytext在内存中的位置text_addr未必是一个内存页的起始地址,故使用text_addr/4096*4096来获得text_addr所处内存页的起始地址。
修改完权限后,即可开始对内存中的.mytext数据进行解密。
解密后,再修改回去权限。
ElfType32.java与elf.h关于大小端的处理:
ElfType32.java处理so文件时,仅把数据与“名称”进行比对,如把头16个字节的数据认定为e_ident,即以so文件的格式来解析二进制数据中的所有数据,其不管每个“名称”内的数据具体的存放顺序(大端、小端),而elf.h会处理数据的小端情况。
如so文件的0x18处开始的4个字节内容为0x24000000,在ElfType32.java中获取到的是0x24000000,利用elf.h获取到的是0x00000024,实际的值也应该是0x00000024。
故,若使用ElfType32.java来读取so文件时,若遇到数值的数据时应该进行小端处理(将0x24000000转换成0x024),即进行逆转,方能获取到真正的数值大小;若遇到字符串的数据,则无需逆转。
将.mytext的offset与size放到so文件头部的e_entry与e_flags字段时,也应该对offset与size进行小端处理(将0x024转换成0x24000000)再写入so文件头部。
当so文件被装载进内存后,将其在内存中的起始位置开始的数据均利用elf.h来读取并解析成so文件的头部,此时无论遇到数值型的数据还是字符串型的数据均无需进行小端处理。
Andorid7与so文件的e_shoff、e_entry:
so文件装载到Andorid7的测试机上时,dlopen会检查e_shoff与e_entry字段的值是否合理(是否符合小端),若不合理将报错:
且e_shoff被篡改后将影响so文件的装载,将报错:
故在加密时不可修改e_shoff字段,可选择使用e_flags字段。
实践
在mainActivity中load 待生成的so文件并声明且使用目标函数showMessage 对mainActivity使用javah工具,来生成.h文件,便于获取目标函数showMessage的签名 新建.cpp文件,编写showMessage函数与解密函数decrypt_soShell,并为showMessage指定属性(即在函数声明时附上__attribute__((section(".mytext")))),为decrypt_soShell函数指定属性(即在函数声明时附上__attribute(constructor))) 新建并填写CMakeLists.txt文件 在build.gradle文件中,设置so文件生成的abi为armV7,并设置cmake的路径 编译,则在Android Studio该项目的我们预设的路径下将生成libsoShellDemo.so文件
2、新建一个Andorid Studio项目作为加密的脚本,(可以直接在以前的shellTool项目里进行增加,反正目的都是加固),完成以下步骤:
将soShell项目生成的libsoShellDemo.so文件放置到shellTool项目的Assets目录下且重命名为A.so文件,并以二进制的形式进行读取; 新建ElfType32.java,该文件内容可百度,该文件用于将二进制数据解析成so文件格式; 新建encryptSoSeciton.java文件,该文件以二进制形式读取,并调用ElfType32来进行解析,根据目标段名称.mytext来获取到该段的偏移和大小,对该段数据进行翻转(即ABC变成CBA),并将偏移放置在so文件的e_flags字段,将大小放在so文件的e_entry字段。保存新的so文件数据。 编译,运行,获得新的so文件,B.so文件。
在libs目录下,新建armeabi-v7a文件夹,并将B.so文件放置在该目录下,重命名为libsoShellDemo.so文件 删除jniLibs目录 在build.gradle文件中,注释掉“设置cmake的路径,设置so文件生成的abi”部分,新增jniLibs.srcDirs 编译,运行,应用成功运行,且输出了目标日志
>>>> 加密so的函数
加密so的函数
原理
在so文件中寻找目标函数的地址与大小的原理:
寻找program header偏移时,加密用p_offset,解密用p_vaddr的原因:
加密时,是利用脚本对so文件“实体”进行解析从而得到program header偏移的,该值保存在p_offset字段。
解密时,是在app运行时对so文件“在内存中的数据”进行解析从而得到program header的,该值保存在p_vaddr中。
即:p_offset表示program header在文件中的偏移;p_vaddr表示program header在内存中的偏移。
d_val与d_ptr均为4字节类型的数据,但在Elf32_Dyn中两者共同占用4个字节的原因。
Elf32_Dyn的结构如下图:
在Elf32_Dyn中,存在d_tag、d_val与d_ptr,三者均为4字节的数据,其中后两者在一个联合体中,故可将Elf32_Dyn理解成包含两个元素:4字节的d_tag,4字节的联合体。
d_tag用来描述每个具体段的类型。
当d_tag描述的是“区域”,则联合体中的d_ptr有意义且d_val无意义,即可视为联合体此时等于d_ptr,如d_tag等于DT_HASH,则描述的是.hash的“区域”,此时d_ptr指向.hash,d_val无意义。
若d_tag描述的是区域的“大小”,则联合体中的d_val有意义且d_ptr无意义,即可视为联合体此时等于d_val,如d_tag等于DT_STRSZ,则描述的是字符串表的“大小”,此时d_val表示了字符串表的具体大小,二d_ptr此时无意义。
换言之:针对具体的d_tag时,d_val与d_prt不共存。
故,在用java解析Elf32_Dyn类型数据时,可用下图方法解析,待使用时再选择d_ptr或d_val:
动态符号表的st_value需要减一才能作为funcOffset的原因:
感谢论坛9楼和11楼的热心回答。
deff回答:
thumb指令模式函数真实的调用地址为真实地址减一。
实际上,在运行时原函数真实地址加1,实际是一个标志位,表明程序要从ARM状态跳转到Thumb状态,此时CPSR寄存器T位会从0变成1,代表arm变成thumb.故逆推时,st_value需要减一才是真实调用地址。
9楼的zylyy回答:
arm指令blx,bx之类得跳转得时候,会根据目标地址最低位是否是1切换到thumb模式。也就是如果一个函数是以thumb模式编译得,那么调用得时候要地址加1.基本上现在安卓系统系统,armv7指令,系统库的导出函数基本上都是thumb的.其实还有一点对齐原则。
arm或thumb指令总数2字节或者4字节对齐的。你要知道,这些指令在内存中总是2字节对齐的(内存分配起始点总是页对齐的,编译的so也保证指令相对模块起始位置对齐),也就是最低位不可能为1。所以最低位可以充当标志位。实际上pc取指令的,总是忽略掉最低位。所以那个加1.准确的说是或运算。
实践
在mainActivity中load 待生成的so文件并声明且使用目标函数showMessage 对mainActivity使用javah工具,来生成.h文件,便于获取目标函数showMessage的签名 新建.cpp文件,编写showMessage函数与解密函数decrypt_soShell,并为showMessage指定属性(即在函数声明时附上__attribute__((section(".mytext")))),为decrypt_soShell函数指定属性(即在函数声明时附上__attribute(constructor)) 新建并填写CMakeLists.txt文件 在build.gradle文件中,设置so文件生成的abi为armV7,并设置cmake的路径 编译,则在Android Studio该项目的我们预设的路径下将生成libsoShellDemo.so文件。
将soShell项目生成的libsoShellDemo.so文件放置到shellTool项目的Assets目录下且重命名为func_origin.so文件,并以二进制的形式进行读取 新建ElfType32.java,该文件内容可百度,该文件用于将二进制数据解析成so文件格式 新建encryptSoSeciton.java文件,该文件以二进制形式读取,并调用ElfType32来进行解析,根据目标段名称.mytext来获取到该段的偏移和大小,对该段数据进行翻转(即ABC变成CBA),并将偏移放置在so文件的e_flags字段,将大小放在so文件的e_entry字段。保存新的so文件数据 编译,运行,获得新的so文件,func_encrypt.so文件
在libs目录下,新建armeabi-v7a文件夹,并将func_encrypt.so文件放置在该目录下,重命名为libsoShellDemo.so文件 删除jniLibs目录 在build.gradle文件中,注释掉“设置cmake的路径,设置so文件生成的abi”部分,新增jniLibs.srcDirs 编译,运行,应用成功运行,且输出了目标日志
>>>> 两者比较
两者比较
看雪ID:顺利毕业
https://bbs.pediy.com/user-861444.htm
推荐文章++++