查看原文
其他

入门级加固——3种加固方式学习记录

顺利毕业 看雪学院 2021-03-07
本文为看雪论坛优秀文章

看雪论坛作者ID:顺利毕业



最近刚开始接触Andorid加固,是从姜维前辈在2015年的一些帖子开始学习的,同时也从个别恶意样本中学到了其他的加固手段。

虽然本帖子涉及到的知识大概都是第一代或者第二代加固,对于大佬们来说已经是“陈年旧识”了,但对于本菜鸡来说,依旧还是一个新世界~

在进入这个新世界的时候,虽然有姜维前辈的帖子为导,但总有一些不适用的地方,比如其加固so文件的section帖子对Android7不完全使用的问题等,so文件的大小端问题。

且由于本人学习过程中未完全照搬姜维前辈的代码,故过程中也遇到了些问题,如加固后可成功运行原apk的组件但依旧显示壳信息等问题。

同时,对于姜维前辈一笔带过或者没解释的知识点,我在学习过程中也进行了些补充。

具体的,都会在下面呈现出来。
(看姜维前辈的帖子,感悟最深的就是“万物皆可二进制”。。。)


本帖子涉及到3种加固:

1、在java层为.apk文件进行加固:跟着姜维前辈学~

2、在native层为.dex文件进行加固:逆向一款恶意软件,从中学习到它的加固方式。在后面的帖子的第二部分将着重贴出逆向的全过程。

3、在native层为.so文件进行加固:依旧跟着姜维前辈的步伐~


备注: 本帖子重点在于记录自己学习过程中遇到的问题及解决方法,以及部分原理知识。

      
本帖子作为学习记录,会部分赘述姜维前辈帖子里的内容,但不会全写,会附上具体的连接,有兴趣的同学最好提前先康康~


本文也将高亮自己学习过程中遇到的问题及对应的解决方法,方便大家查看。


还有一部分,是我逆向一款在native层进行加固的恶意样本,逆向的过程也会贴出来。


对于一些比较绕的地方,我根据自己的理解画了几张图,加深理解。


本帖子比较核心的内容为:遇到的问题及解决方法、逆向native层加固的样本。感兴趣的同学可以挑着看。


目录

一、在java层为.apk文件进行加固学习资料Android壳原理       Dex文件基础知识加固       原理       操作实践       基础操作       注意事项二、在native层为.dex文件进行加固壳原理        大致流程样本分析三、在native层为.so文件进行加固学习资料加密so的section         原理         实践加密so的函数         原理         实践两者比较四、3者比较


一、在java层为.apk文件进行加固



>>>>

学习资料


《Android加固原理研究》:
https://blog.csdn.net/jiangwei0910410003/article/details/48415225

需补充前期知识:

(1)《动态加载技术解读》:
https://blog.csdn.net/jiangwei0910410003/article/details/17679823

(2)《Java高新技术第一篇:类加载器详解》:
https://blog.csdn.net/jiangwei0910410003/article/details/17733153

(3)《类加载器分析》:
http://blog.csdn.net/jiangwei0910410003/article/details/41384667

(4)《资源加载问题(换肤原理解析)》:
http://blog.csdn.net/jiangwei0910410003/article/details/47679843

(5)《动态加载Activity(免安装运行程序)》:
http://blog.csdn.net/jiangwei0910410003/article/details/48104455

>>>>

Android壳原理


Dex文件基础知识


学习资料

《Android加固原理研究》:
https://juejin.im/entry/5a5c55426fb9a01c9f5b65ed

>>>>

加固


原理


1、学习资料

《Android中的Apk的加固(加壳)原理解析和实现》:
https://blog.csdn.net/jiangwei0910410003/article/details/48415225

2、加固原理图


3、加固的核心

如何将源Apk和壳Apk进行合并成新的Dex。

4、核心原理


只要关注上面红色标记的三个部分:

(1) checksum 

文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。

(2)signature 

使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。

(3)file_size

Dex 文件的大小 。

(4)原因:

要将一个文件(加密之后的源Apk)写入到脱壳Dex中,那么需要修改脱壳Dex的文件校验码(checksum)。

因为它是检查文件是否有错误。那么signature也是一样,也是唯一识别文件的算法。

还有就是需要修改脱壳dex文件的大小。此外,还需要一个操作,就是标注一下加密的Apk的大小,因为在脱壳的时候,需要知道加密后的源Apk的大小,才能正确的得到加密后的源Apk。

这个值直接放到文件的末尾就可以了。这样,就生成了一个壳dex文件。(脱壳dex追加源apk、源apk大小,并修改脱壳dex头部,从而生成了壳dex文件。)

即:修改Dex的三个文件头,将源Apk的大小追加到壳dex的末尾就可以了。

修改之后得到新的Dex文件样式如下:


对应涉及到三个工程:
  • 源程序项目(需要加密的Apk)
  • 脱壳项目(解密源Apk和加载Apk)
  • 对源Apk进行加密和脱壳项目的Dex的合并


操作


1、代码流程

(1)编写源程序项目,该项目需含有application类,生成origin.apk。

(2)编写脱壳程序项目,以生成“脱壳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。


(3)编写加壳程序项目:

  • 以二进制形式读取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


1、程序存放地址

\AndroidStudioProjects\testAppName

2、程序名称

testAppName

3、该apk最好具有application类。

4、在该apk的application类与mainActivity内,均打印出用于识别的信息。

5、生成apk(在Andorid Studio里build的话,应该是直接用test来签名了)。

6、Apk的包名为com.example.testappname,将该apk命名为A.apk。

7、A.apk运行成功时,将会输出以下信息,即在application类中输出当前的包名,在mainActivity类中输出当前组件的名称:


运行界面将提示此时为testAppName:


加壳程序


1、程序存放地址:\AndroidStudioProjects\shellTool(等脱壳dex生成后,再生成加壳apk的dex)

2、程序名称:shellTool

3、该程序仅用于将A.apk拼接在壳dex文件后部。

4、将“准备待加壳apk”步骤中生成的A.apk文件,“脱壳程序”步骤中生成的B.dex文件放置在Assets目录下,用二进制形式将两者拼接在一起,B.dex在前,A.apk在中间,尾部是A.apk的大小,生成tmp.dex文件。

5、修改tmp.class文件头部的fileSize字段、签名字段、checksum字段(这三个字段原先存放的都是B.dex的数据),生成classes.dex文件,保存在/sdcard/目录下。

脱壳程序


1、程序存放地址:

\AndroidStudioProjects\dumpShell\app,脱壳dex文件在\AndroidStudioProjects\dumpShell\app\build\outputs\apk\debug\ app-debug.apk内部。

2、程序名称:dumpShell

3、该apk应该含有application类,且不能含有其他组件。

4、在dumpShell的application类中,重写attachBaseContext方法,目的:
  • 解密源apk
  • 初始化自定义类加载器
  • 利用反射,设置LoadedApk中加载器对象为自定义加载器

5、在dumpShell的application类中,重写onCreate方法,目的:
  • 获取源apk的Application名称
  • 利用反射,生成正确的Application对象
  • 利用反射,设置ActivityThread中的Application信息。(ActivityThread为当前主线程)
  • 调用源apk的application对象的oncreate方法。

6、编译并build出dumpShell.apk,用7-zip对其进行解压,将classes.dex重命名为B.dex并将由“加壳程序”进行处理,并将“加壳程序”的输出结果classes.dex(与dumpShell.apk的原classes.dex不同)放置在该解压后的目录下,同时删除签名文件,将此时目录下的所有文件压缩到一个zip文件中,并对其进行签名得到最终的apk文件。此时的apk文件是本次“加固”的最终成果。

合体操作流程


1、编译运行testAppName程序,生成源apk,并将其命名为A.apk。

2、编译运行dumpShell程序,生成dumpShell.apk,从dumpShell.apk中提取classes.dex,并将其命名为B.dex,其作为脱壳dex。

3、将A.apk与B.dex放置在shellTool项目的Assets目录下,编译运行shellTool项目,该项目将在/sdcard/目录下生成classes.dex文件,利用adb将其拷贝到PC端。

4、用7-zip解压dumpShell.apk,删除里面的签名文件与classes.dex,并将步骤3得到的classes.dex文件放置到该文件夹下。将该文件夹下的所有文件一起压缩到dumpShell.zip文件,并对该文件进行签名成apk文件,安装运行该apk。

注意事项


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的信息。



二、在native层为.dex文件进行加固



>>>>

壳原理


将原始apk的.dex文件进行加密后,放在apk的资源路径下,随后通过native代码重新对该.dex文件进行解密、加载,从而执行原始.dex文件。

大致流程


以样本923872474d2b49df6b2715f1a10ac0e2为例。
  • 提取原始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类名称为这个启动类的名称



>>>>

样本分析


1、样本:
923872474d2b49df6b2715f1a10ac0e2

2、用IDA打开libdmeod.so文件,找到JNI_Onload方法,按F5查看其C代码。易知v6与v3的实际类型为JNIEnv*,故将光标停在这两个变量上,按y键修改其类型,可见JNI_Onload的C代码变成如下:


3、查看off_8004,可知wefiz被重命名为i,izogr被重命名为r,即:com.vod.wbmp.yobl.chjiv这个application类在其attachBaseContext方法中实际调用的是so文件里的i方法,onCreate方法中实际调用的是so文件中的r方法。


4、先看方法i,按F5可查看其c代码,但该c代码的可读性较差,如下图:


因此对其进行修改:可知红色框中的数据类型应该是char型,但此时显示的是int型,故按R键(或右键点击该数据,选择Char)将其显示成char型,通过从v197拼接到v222可知,从v197开始表示的字符串“android/app/ActivityThread”(为方便后续操作,此时可将v197重命名为android_app_ActivityThread)。

 


继续对其进行修改:在绿色框中,此时无法得知调用的是哪个函数,但易知v的数据类型为JNI *,v4的数据类型应该为JNIEnv,按y键修改v和v4的数据类型,便可看到此时调用的函数为:


故该部分代码的功能即:
FindClass(“android/app/ActivityThread”)


对方法i与方法r执行以上的相同操作,增强i与r的可读性。修改后的方法i部分截图为:


为方便查看,提取方法i里的主要功能代码,如下图:


5、方法i等同于以下java代码:

 


6、方法i的功能总结:调用edf方法来解密dmeod.jar(解密部分就不作说明啦),获取当前的主线程currentActivityThread,利用currentActivityThread获取mPackages,再把当前app的包名传递给mPachages来获得当前app的弱引用,从而找到当前app的类加载器mClassLoader。

利用DexClassLoader来加载dmeod.jar文件,并将其dexClassLoader设置成新的mClassLoader的值,随后加载dmeod.jar文件里的真正入口Acitivity。

7、对方法r进行同样的修改操作,增加其可读性。方法r的代码较少,下面的截图已经包含了其主要的功能了,对应的java代码就不贴上来了。方法r的主要功能是:调用dmeod.jar里真正的application类的attachBaseContext方法与onCreate()方法,即启动真正的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方法。


备注:

(1)若dmeod.jar文件里没有application类,即原始apk没有application类,则无需修改壳application类的onCreate方法也能使原始apk正常运行。

(2)根据该样本在native层的加固,写了个java层的同功能的加固,放在:\AndroidStudioProjects\helloShell2



三、在native层为.so文件进行加固



>>>>

学习资料


《Android中对Apk加固(加壳)续篇之---对Native层(so文件)进行加固》:
https://blog.csdn.net/jiangwei0910410003/article/details/49967375/

需补充前期知识:

(1)《Android逆向之旅---SO(ELF)文件格式详解》:
https://blog.csdn.net/jiangwei0910410003/article/details/49336613

(2)《Android逆向之旅---Android应用的汉化功能(修改SO中的字符串内容)》:
https://blog.csdn.net/jiangwei0910410003/article/details/49361281

(3)《Android逆向之旅---基于对so中的section加密技术实现so加固》
http://blog.csdn.net/jiangwei0910410003/article/details/49962173

(4)《Android逆向之旅---基于对so中的函数加密技术实现so加固》
http://blog.csdn.net/jiangwei0910410003/article/details/49966719


>>>>

加密so的section


原理


利用c代码生成so文件时:

声明重要函数showMessage时指定将其存放在自定义的.mytext段(通过__attribute__((section(".mytext")))。

编写解密函数并在其声明时指定其在main函数前运行(即通过__attribute(constructor)))。从而使得so文件在被加载到内存后可第一时间对showMessage函数进行解密。此时生成初始so文件section_origin.so,对so文件尚未加密。

写一个脚本对section_origin.so中的“.mytext”段进行加密并重写回原处。为了便于解密,将.mytext段的偏移地址与段大小保存在so文件的头部。此时生成section_encrypt.so即为被加密过的so文件。


加密时,利用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字段。


实践


1、在Andorid Studio里新建soShell项目,按照本文档上面的NDK开发步骤:
  • 在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文件。

3、返回soShell项目中,将该项目从ndk开发转换为普通android项目:
  • 在libs目录下,新建armeabi-v7a文件夹,并将B.so文件放置在该目录下,重命名为libsoShellDemo.so文件
  • 删除jniLibs目录
  • 在build.gradle文件中,注释掉“设置cmake的路径,设置so文件生成的abi”部分,新增jniLibs.srcDirs
  • 编译,运行,应用成功运行,且输出了目标日志


4、对比so文件加密前后

用010Editor查看两者对比:


用IDA查看两者对比:

(加密前)


(加密后,IDA提示ELF-flag数据有问题,该值0x232c正是我们保存的.mytext的偏移)



>>>>

加密so的函数


原理


利用c代码生成so文件时:

声明重要函数showMessage时指定将其存放在自定义的.mytext段(通过__attribute__((section(".mytext")))。

编写解密函数并在其声明时指定其在main函数前运行(即通过__attribute(constructor)))。从而使得so文件在被加载到内存后可第一时间对showMessage函数进行解密。此时生成初始so文件func_origin.so,对so文件尚未加密。

写一个脚本对func_origin.so中的showMessage函数进行加密并重写回原处。此时生成func_encrypt.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.准确的说是或运算。


实践


1、在Andorid Studio里新建soShell项目,按照本文档上面的NDK开发步骤:
  • 在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目录下且重命名为func_origin.so文件,并以二进制的形式进行读取
  • 新建ElfType32.java,该文件内容可百度,该文件用于将二进制数据解析成so文件格式
  • 新建encryptSoSeciton.java文件,该文件以二进制形式读取,并调用ElfType32来进行解析,根据目标段名称.mytext来获取到该段的偏移和大小,对该段数据进行翻转(即ABC变成CBA),并将偏移放置在so文件的e_flags字段,将大小放在so文件的e_entry字段。保存新的so文件数据
  • 编译,运行,获得新的so文件,func_encrypt.so文件

3、返回soShell项目中,将该项目从ndk开发转换为普通android项目:
  • 在libs目录下,新建armeabi-v7a文件夹,并将func_encrypt.so文件放置在该目录下,重命名为libsoShellDemo.so文件
  • 删除jniLibs目录
  • 在build.gradle文件中,注释掉“设置cmake的路径,设置so文件生成的abi”部分,新增jniLibs.srcDirs
  • 编译,运行,应用成功运行,且输出了目标日志



4、对比so文件加密前后

用010Editor查看两者对比:


用IDA查看两者对比:

(加密前)


IDA可正常打开加密后的文件:


>>>>

两者比较


加固so文件的section时:


加固so文件的函数时:
 


四、3者比较



在java层为.apk文件进行加固:

 

在native层为.dex文件进行加固:

 


加固so文件:


(自认为我用excel表格画的几张图已经挺清晰的了,所以就不多做解释啦~)




- End -






看雪ID:顺利毕业

https://bbs.pediy.com/user-861444.htm 


*本文由看雪论坛  顺利毕业  原创,转载请注明来自看雪社区




推荐文章++++

Kali下的Osmocom-BB GSM-SMS 嗅探

格式化字符串漏洞

安卓源码+内核修改编译(修改内核调试标志绕过反调试)

GlobeImposter3.0 勒索分析

恶意样本检测——Mathematics Malware Detected Tools





进阶安全圈,不得不读的一本书







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com




“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存