-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
一般情况下,我们写的函数都经过编译和链接之后,都会存在.text section中,而代码中设计到的数据都被存在于.data section中。倘若我们要对特定函数或者特定数据加密,就需要在.text中或者.data精确找到函数起始位置和size。这篇文章,提供一种简便的方法实现上述目的。
SO加壳的原理
- ELF文件有执行视图和链接视图的区分。在linux下,利用readelf -S读出来的是section信息,这可以理解为在解析ELF链接视图,当程序加载时,加载器会将具有相同权限的section整合成一个segment,所以当程序真正执行时,就不存在section,而是一个一个的segment。同时,对于函数的索引页不再是用过section,而是用program header来寻找。利用执行视图和链接视图的差别,我们就可以修改链接视图的部分数据而不影响程序真正执行。
- 在我们编写代码的时候,我们可以使用 void func() attribute((constructor));将一个函数写到init段中。同样,我们可以使用attribute((section("hola"))) void hello(void);将一个函数或者数据写到我们自定义的section “hola”中。当我们需要对特定函数加密的时候就可以直接加密整个section即可。
- 根据Android linker的原理,当SO文件被加载到内存中时,首先执行的将会是init段中的内容(如果有的话),这之后会执行init_array的内容(如果有的话),这之后才会执行入口点的函数,在JNI中,一般是JNI_ONLOAD.所以,当我们对一个一个section加密之后,在init段中写上解密函数,那么在so文件被加载进内存时,就会把加密的段先解密出来。
加密SO文件
加密so文件的关键就是要找到我们需要加密的section的偏移和size。
- 从头表中找到节区头表
- 遍历节区表,会得到每一个表的 名字索引, 偏移, size
- 但是此时我们不知道哪一个section是字符串表。还好头表中有字符串表在节区头表中的索引,所以根据这个索引找到字符串表。
- 然后再次遍历节区表,然后根据节区头中的字符串表索引去匹配是否是我们需要的section。
- 找到section头之后,即可获得addr和size。
- 为了避免在解密之后再次寻找addr和size,我们可以先将这两个值保存起来。例如ELF头表中的某些项在执行视图中是没有用的,我们可以保存在这些地方。
init段解密section
tips: 我们在上一步找到section之后,一定要记录addr而不是offset。一个是内存偏移一个是文件偏移。如果这个section是代码段,那么这两个值是一样的,但如果这个段是数据段,那么这两个值很有可能是不同的。
通过读取ELF头表中我们保存的addr和size找到需要解密的地方,执行解密函数。当然,对于代码段,我们需要调用mprotect()函数改变页权限。同时,在解密之后,可以使用void __clear_chche(void* start, void *end);来刷新内存。
tips:为了使__clear_cache()生效(或者不报错),需要clear的内存页必须要有执行权限,这个好像是一个bug?总之,当我的页只有读写权限时,就执行该函数,程序是崩溃的。之后我修改了页的权限,就能够正常的执行了
Reactions are currently unavailable