一、前言
对于用户在Android移动设备商保存重要的隐私文件,通常采用一些加密保存的软件。但在手机上实现隐私空间的软件鳞次栉比,但是问题在于打开文件都需要使用该隐私空间,将加密文件解密到临时文件,然后再选择应用程序打开文件。这将导致用户重要文件在设备上明文的存在,存在泄漏的风险。
而且根据笔者的调研,对于360隐私空间,应用程序对临时文件修改后不能再逆向加密回密文,导致加密操作只能一次进行。LBE隐私空间,相对较好,但其临时文件存在生命周期过长。
因此笔者通过现有知识讨论一种采用hook技术实现的透明加解密方法,不需要在设备上生成临时文件,从而保护用户重要隐私。
二、技术要点
由于Android是基于linux内核的开源系统,根据语言环境不同可以分为Java层、Native C层、Linux Kernel层。Java层的安全是使用Java语言开发,基于SDK,能实现的功能相对简单。Linux Kernel层安全,需要从源码做起,编译自己的系统,通用性不强。因此在Native C层,通过JNI开发,可以使用linux提供的函数实现更多的功能。
在hook API方面与linux的hook类似使用ptrace 函数与plt表实现,还可以采用Inline hook的方式实现,但是不是很稳定,操作难度大。其本质都是劫持函数调用。
但是由于处于Linux用户态,每个进程都有自己独立的进程空间,所以必须先注入到所要hook的进程空间,修改其内存中的进程代码,替换其中过程表的符号地址,因此其生存空间是所注入的进程,只能对某一进程进行HOOK。
Ptrace函数是调试程序所用,功能强大,不仅可以附加某一进程(PTRACE_ATTACH),而且可以任意修改目标进程的内存空间(PTRACE_PEEKDATA,读内存。PTRACE_POKEDATA,写内存),甚至是寄存器(PTRACE_SETREGS,PTRACE_GETREGS)
基本流程是利用寄存器指令中断:
① PTRACE_ATTACH,绑定目标进程。
② PTRACE_GETREGS,获取目标进程寄存器状态,并保存。
③ PTRACE_PEEKDATA与PTRACE_POKEDATA配合,保存原代码,写入要注入的代码到当前运行位置。
④ PTRACE_SETREGS,恢复寄存器状态,并继续执行,这是注入的代码开始在目标进程内执行,注入代码完成HOOK,过程与Windows下相似。
⑤ 在HOOK完成后,注入的代码执行int3被ptrace捕获,目标进程再次暂停执行。
⑥ PTRACE_GETREGS,再次保存寄存器。
⑦ PTRACE_PEEKDATA与PTRACE_POKEDATA配合还原代码。
⑧ PTRACE_SETREGS,恢复寄存器,目标进程继续执行。
⑨ PTRACE_DETACH,撤销绑定目标进程。
参考LBE实现原理和看雪上关于Hook Ioctl的文章都基本上按照这种流程实现HOOK。
在明白hook工作机制后,对于实现Android上的透明加解密需要找到open和close函数的符号存在哪个动态链接库中,hook该应用程序的这个动态链接库,在open操作进行的时候,将密文文件分块解密到内存中,并将该内存中的文件标识符返回。在close操作进行的时候将内存中的明文加密到本地密文存储。
三、关键流程
1、阅读Android代码查找打开文件和关闭文件过程。这是我们实现透明加解密的关键。
参考http://blog.chinaunix.net/uid-26926660-id-3326678.html的方式
可以发现读取文件流的函数最终通过JNI方式的read函数实现,同样打开文件的操作最终都归结于open函数。
而实现Java代码的JNI支持的动态库是nativehelper.so因此我们需要hook的动态库即nativehelper.so。
注:在Android早期版本即android2.3、Android4.0上open和close符号在nativehelper.so中,该文件有140k大小。而在android4.1版本以上,谷歌重写了android原生库的实现,nativehelper.so被拆分,笔者在4.0平台进行的开发并没有阅读寻找4.1版本之上的。
2、进行进程注入和ELF节替换
进程注入就是将一段代码拷贝到目标进程,然后让目标进程执行这段代码的技术。由于这样的代码构造起来比较复杂,所以实际情况下,只将很少的代码注入到目标进程,而将真正做事的代码放到一个共享库中,即.so文件。被注入的那段代码只负责加载这个.so,并执行里面的函数。由于.so中的函数是在目标进程中执行的,所以在.so中的函数可以修改目标进程空间的任何内存,当然也可以加钩子,从而达到改变目标进程工作机制的目的。
当然不是任何进程都有权限执行注入操作的。Android平台上的进程注入是基于ptrace()的,要调用ptrace()需要有root权限。目前市面上的主流安全软件也都是基于进程注入来管理和控制其他应用进程的。这也就是为什么这些安全软件需要获得root权限的原因。
关于如何.so注入的实现,可以参考看雪论坛的上的一个注入库LibInject 和洗大师的一个开源项目Android Injector Library。
笔者对这种两种方式都有实验,对于Libinject,就是向目标进程中注入libhook.so,首先调用ptrace()函数,挂起该进程。然后遍历进程加载的libc.so,通过dlopen和dlsym函数修改arm寄存器的值,然后压入参数,so路径,并将之前找到的dlopen地址压入寄存器,直接操作blx,就可以让目标进程调用dlopen加载我们的so,同理dlsym调用我们的so里的函数。
注入完成后,会调用libhook.so库中的hook_entry()函数,该函数实现加载hook函数实现的动态库,并对libnativehelper.so的got表和plt表的遍历和修改。修改为自己编写的实现open函数和close函数的动态库中的符号地址。因此需要注入两个库,因为libhook.so在执行完后需要detach目标进程,从而释放,而具体操作的动态库需要常驻内存。实现常驻内存需要在hook_entry()函数中显式加载动态库。
以上注入以后的过程都由自己编程实现,能够加深对ELF格式理解。
而采用Android Injector Library则相对简单,在调用可执行程序的主函数中实现即可:
这个文件编译生成注入入口和符号表替换逻辑。
* 1、在该函数中加载libhook.so通过其中的do_hook函数返回原来的open和close地址以及要替换的新的open和close函数地址
* 2、然后静态打开libnativehelper动态库,读取其结构遍历节表,找到全局符号表(GOT表),该表存储了外部依赖符号的地址
* 3、遍历GOT表找到原先的open函数和close函数地址,分别替换为新的open函数和新的close函数即可
3、在学习这一过程中,需要了解linux的ELF格式,以下是学习ELF的笔记:参见《程序员的自我修养》如果熟悉则可跳过。
ehdr->e_shstrndx索引指向shstrtab的节,可以用来索引节头的字符串名称描述。shstrtab表(Section Header String Table)保存段表中用到的字符串,最常见的就是段名、
常用的段名 说明 .rodata1 Read Only Data,这种段里存放的是只读数据,比如字符串常量、全局const变量。跟”.rodata”一样 .comment 存放的是编译器版本信息 .debug 调试信息 .dynamic 动态链接信息 .hash 符号哈希表 .line 调试时的行号表 .note 额外的编译器信息。比如程序的公司名、发布版本号等 .strtab String Table.字符串表 .symtab Symbol Table.符号表 .shstrtab Section String Table.段名表 .plt .got 动态链接的跳转表和全局入口表 .init .fini 程序初始化与终结代码段
符号节,遍历节头时候。判断每一个节的类型是不是SHT_SYMTAB或SHT_DYNSYM,那么对应的节就是符号节。符号节存放的是一张符号表,符号表也是一个连续存储的结构数组.
编程过程中用到的变量和函数都可以称之为符号,一个ELF文件中并不只有一个符号节,通常是两个,一个为”.dynsym”的动态节类型为SHT_DYNSYM,所有引入的外部符号在这里有所体现,另一个为SHT_SYMTAB,名字为“.symtab”保存了所有有用符号信息。
Symbol Table 符号表保存了一个程序在定位和重定位时需要的定义和引用的信息。一个符号表索引是相应的下标。符号表的存在意义是体现在多个目标文件进行链接的时候,在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用,而函数和变量可以统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。我们可以将符号看作是是链接中的粘合剂,整个链接过程就是基于符号才能够正确完成。在符号表”.symtab“中,其也是像段表的结构一样,是一个数组,每个数组元素是一个固定的结构来保存符号的相关信息,比如符号名(不是字符串,而是该符号名在字符串表的下标)、符号对应的值(可能是段中的偏移,也可能是符号的虚拟地址)、符号大小(数据类型的大小)等等。符号表中记录的一般是全局符号,比如全局变量、全局函数等等。
目标文件的符号表包含定位或重定位程序符号定义和引用时所需要的信息。符号表入口结构定义如下:
typedef struct{ Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; Unsigned char st_info; Unsigned char st_other; Elf32_Half st_shndx; }Elf32_Sym;
其中st_name包含指向符号表字符串表(strtab)中的索引,从而可以获得符号名。St_value指出符号的值,可能是一个绝对值、地址等。St_size指出符号相关的内存大小,比如一个数据结构包含的字节数等。St_info规定了符号的类型和绑定属性,指出这个符号是一个数据名、函数名、section名还是源文件名;并且指出该符号的绑定属性是local、global还是weak。
GOT表和PLT表
GOT(Global Offset Table)表中每一项都是本运行模块要引用的一个全局变量或函数的地址。可以用GOT表来间接引用全局变量、函数,也可以把GOT表的首地址作为一个基 准,用相对于该基准的偏移量来引用静态变量、静态函数。由于加载器不会把运行模块加载到固定地址,在不同进程的地址空间中,各运行模块的绝对地址、相对位 置都不同。这种不同反映到GOT表上,就是每个进程的每个运行模块都有独立的GOT表,所以进程间不能共享GOT表。
动态链接机制
首先回顾一下Linux平台上,一个模块甲需要调用另外一个模块乙中的函数时的动态链接机制:
1、模块甲在编译期间,将要引用的模块乙的名字与函数名写入自身的符号表。
2、运行期模块甲调用时,调用流程是从调用代码到PLT表到GOT表再跳入模块乙。
而如何保证模块甲的代码能从其PLT/GOT跳到正确的模块乙入口,这就是链接器做的事情。
标准Linux链接器是ld.so,支持懒绑定,也就是说,模块甲在编译期间生成的调用模块乙的原始代码,流程是从调用代码到PLT表到链接器。运行期第一次调模块乙时,首先进入链接器,链接器根据调用信息加载模块乙搜寻其符号并将找到的函数地址填入GOT表,之后的后续调用流程就直接走PLT/GOT表了。这种机制能减少加载时的开销,为Linux发行版等采用。
Android虽然内核基于Linux,但其动态链接机制却不是ld.so而是自带的linker,不支持懒绑定。也就是说,上述模块甲乙如果在Android平台上,则是模块甲加载时,linker就会根据模块甲中的.rel.plt表和字符串表中的内容加载模块乙并搜索其所需函数地址并预先填入GOT表。之后调用流程每次都直接走PLT/GOT表,不再进linker,PLT表中也省去了跳至linker的代码,这种流程和“勤劳”绑定类似,倒是为拦截提供了一点方便。如果拦截懒绑定的入口时模块乙还没加载地址也没找到,拦截就没法进行了。
要拦截模块甲对乙的调用,一般思路是通过ptrace远程注入并加载一新拦截模块至模块甲,并搜索模块甲的GOT表,找到对模块乙的调用地址,改成新模块内的某函数地址,然后新模块内的这个函数在进行了自己的处理后,再跳到模块乙中。
Android和Linux的链接器不同导致了内存布局的差异,也导致了网上流行的Linux注入与HOOK的方法行不通。网上的方法是通过ptrace注入后,搜索dynamic的section中的PLTGOT区,去里头取link_map以遍历此进程所加载的模块来搜索需要hook的函数地址。但Android上,dynamic的section的PLTGOT区前几项都是空的,没有link_map这个数据结构,只能通过分析/proc/
4、阅读代码中的注意事项
在Android Injector Library阅读过程中有几个需要注意的地方。
1)利用捕捉SIGSEGV的无效内存引用或者段错误的异常信号来执行ptrace。
2)ptrace(PTRACE_PEEKTEXT, pid, addr, data)
描述:从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据。
在Linux(i386)中用户代码段与用户数据段重合所以读取代码段和数据段数据处理是一样的。
3)linker 主要用于实现共享库的加载与链接。它支持应用程序对库函数的隐式和显式调用。查找/system/bin/linker中加载的libdl.so,加载位置固定,定义了dlopen,dlcose,dlsym,dlerror。
4)有下列代码理解,即dynsym和symtab的关系
5)在代码中有关于dynsym符号读取顺序的错误。但是不影响使用。使用androidSDK下的工具readelf
5、需要编写自己的open函数和close函数实现加解密操作
该过程使用Android 平台下的openssl EVP编程,该过程的难度不大。
关键点一是在于使用密钥空间构造。推荐密钥空间使用数组。使用char*字符串即使在字符串最后存在’’也会由于内存中的其他内容影响密钥初始化,出现意想不到的问题。
关键点二在close时,参数只有文件描述符,可以通过下述代码获得文件名。
关键点三在于使用Openssl进行对称加解密时会填充到相应的块大小,需要手动剥离这些填充。可以采用国际通用填充方式构造填充,或者自主构造密文文件头记录填充大小。
http://en.wikipedia.org/wiki/Padding_(cryptography)
6、记得在Makefile文件中加入
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -llog
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -lcrypto
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -lssl
7、需要再进行密钥管理模块的开发,该过程不再描述。
四、总结
该种方案能够实现在android平台上的透明加解密。不足之处在于需要使用root权限,提前捕捉用户程序启动,对其进行hook。在移动设备上效率是瓶颈,而且文件不宜过大。对docxlspdfppttxt等文本、jpg等图片支持较好,其他格式的文件笔者没有进行测试。