前言
上次说了dex的加密,即是对编译生成的dex文件进行加密。这次就最近看的so加密,我们知道so文件是android通过JNI进行调用,由于是使用c进行编写,所以对它的逆向并不像对于java的逆向那么容易。逆向的arm汇编代码也显得十分地难懂。但是并不是说这样就能保证so文件的安全性,这种方法只是增加了一点难度。所以我们同样也思考过对so文件进行加密。
so文件加密,首先我想写一点自己对so文件的学习成果,由于深入地研究可能会涉及到编译等原理,所以只是浅显的介绍一下文件格式。再给出一个ctf的题解,这个题就是涉及到对于so文件的加密。
so文件解析
首先讲一下so文件的格式,在研究so加密时,不仅仅是破解它,还需要关注一下它的具体实现。由于so文件加密涉及到具体的二进制文件的修改,也就是说我们在实现so中段或是函数的加密时,其实是没有工具的,只有通过文件格式的定义,找到我们特定的需要加密的函数段,再对这堆二进制数据进行加密。并且需要在脱壳时也能准确地定位到我们的加密代码段,还原出来。
so文件格式为ELF:可执行链接格式(Executable and Linking Format)
下面这张图很详细的描述了elf文件的格式,通过这个图可以大致的了解so文件的格式
通过对so文件段的添加可以大致熟悉如何解析so文件的流程。由于so中的数据都是一个一个段存储的,所以如果涉及到二进制文件的修改,就需要对这些段的作用属性都非常了解,这样我们才能准确定位到我们需要的代码段。
ELF文件主要由文件头(ELF header)、代码段(.text)、数据段(.data)、.bss段、只读数据段(.rodata)、段表(section table)、符号表(symtab)、字符串表()、重定位表(.rel.text)
添加步骤:
1、找到elf header最后两个字段,elf header大小为34h,30h为整个section的大小(总共的section数目加1),32h为e_shstrndx,指向索引表Section names string table,指向的方法是为这个section为所有section的第几个,下图中看到一共有0x15个section,所以这个section为第0x14个,Section names string table为所有节区的名称,并且节区头部的sh_name为该表到节区头部的偏移。
找到Section names string table
需要在这个section中添加我们想要添加的table name,只能在结尾处添加table name并且将这个section的长度增加到table name的结尾减去该section的起始
2、添加新的section header到section header尾部,通过sh_name来找这个新的section的name
sh_name为新的section header到section name table的偏移值
sh_type:段的类型,这个字段我们需要把和设置的和.rodata段的类型相同即可
sh_flags:段的属性
sh_addr:段被映像到内存中的首地址,也就是这个section的起始地址,也就是文件的末尾地址在加上section name的大小,看到代码,我们把section name大小设置成了0x10,也就说新加的section段的name长度不可超过16个字节,当然这个数值是可以改的,但是我感觉没必要了,因为一个section name没必要搞那么长。那么这里的值就是:文件的长度+0x10
sh_offset:这个字段是值该段到文件开始位置的偏移值,这个值我们想一下就知道他的值和sh_addr的值应该一样的。
3、修改String Section Header中的大小
因为我们新加的Section name在String Section中,所以我们还得修改一下String Section的大小
4、修改elf头部中section的count数
这步就简单了,修改一下这个字段:e_shnum
5、修改Program Header中第一个类型为LOAD的段的文件大小和内存映像的大小
这里需要修改的是第一个Type为LOAD的file_size和mem_size字段的大小为文件的总大小即可。下面会说道type为PT_LOAD的都会被加载到内存中。那么我们如果想把我们新加的段的内容加载内存中,这里需要修改一下file_size和mem_size为文件的总大小。
段的添加完成了,我们可以看到,每一步也同样启示我们如何定位到具体每个段的位置。如果是定位到具体函数那么更加复杂,这里就不再赘述了。这只是一个简单的通过对elf文件解析的例子,可以通过这个理解so文件格式中每个字段的含义。
京津冀网安联赛sysndhy
最先做这个题的时候想得比较简单,只是好像大概猜出来解密方式是一个简单的异或,所以就直接解密当然运气比较好就顺利地过了,最近在总结之前做的一些东西时,重新看一下这个题,对整个实现过程又重新研究了一遍,首先这个题用的加密手段就是对so中的代码段和数据段进行加密。
第一个解密函数放在init_array段中
可以看到一共三个函数第一个是对后两个地解密,后两个函数中,第一个解密代码段,第二个解密字符串地那个段。当然这些都要通过动态调试分析出来,由于在解密时,地址其实是比较难计算的,所以直接用动态调试就直接看到它在对那一段地址地数据进行解密。
在两个都解密完成后就是进入我们的check函数了,这个题比较贱的应该是他把antidebug放在了Jstring2CStr中,最先我以为这就是一个简单的转化函数所以直接跳过,或者在静态分析时压根就没点进去看,后边动态调试崩到这儿才找到这个题目中用到的反调试
反调试用到了两种手段一个是自己fork一个进程,然后ptrace附加上去。再一个就是最常见的读取TracerPid,判断是否为零来kill进程。
以上是程序运行地具体流程,但是这个题的具体解答我是通过静态还原出来的,这里要用到ida中的idc脚本,比较推荐的是idapython来对数据进行处理,但是由于这里解密过程实在太简单就直接用idc来解密了,脚本如下:
|
|
对于特定的代码修改begin和end的地址即可。解密完所有的代码后可以看到处理过程了:
函数将输入分成三组,分别与0x11,0x22,0x33进行异或,然后与table进行对比
处理方法:同样使用idc脚本取出table中的数据存在txt文件中,编写python读取文件并且进行解密,最后得到正确的字符串
答案:decryptpass=Txmg
。
总结
对于这个题不得不说,最开始没有想这么复杂。最后分析完了才觉得里边有很多东西,虽然说最终的验证算法非常简单,可以直接求逆。但是里边用到的保护手段还是算比较好的。