导语:在本文中,我们将为读者介绍如何使用Frida和Javascript编写一个简单而通用的ELF解析器。
概述
在这个文章系列中,我们将为读者展示Frida的无穷的可能性和强大的功能。简单来说,Frida就是一个支持多种操作系统的动态检测工具包,可以将Javascript或自己的程序库注入到本地应用程序中。Frida通常用于hook和操纵各种函数。虽然当前可以从网上找到一些Frida教程,但是大部分都是介绍如何使用Frida的拦截器API的,即如何“拦截”目标函数调用。在这个系列中,我们要为读者介绍的,不仅仅涉及如何hook函数,更包括如何通过控制程序的执行流程来完成各种炫酷的事情。
利用Frida打造ELF解析器
在本系列的第一部分中,将为读者介绍如何使用Frida和Javascript编写一个简单而通用的EL解析器。对于这种技术,有许多适用和实用的场景,例如将代码注入另一个进程时;但对于本文来说,我们这样做只是为了好玩。
为了搭建基本的实验环境,需要:
· 运行Lollipop的Nexus 5
· 最新版本的Frida
· PoC应用程序
我为这个项目构建的应用程序包括一个具有单个JNI函数的共享库。这里将用Frida来解析这个共享库。之所以选用这个共享库,是因为本系列的第二部分将要用它做一些事情。实际上,如果单纯想要了解如何利用Frida解析程序的话,任何共享库或可执行文件都是不错的选择。
打开并读取ELF文件
为了解析我们的目标ELF文件,需要先打开该文件,并读取其内容。为此,我们可以借助于open和read系统调用来完成这两个操作。
int open(const char *path, int oflag, … ); ssize_t read(int fd, void *buf, size_t count);
当使用Frida实现上述功能时,可以使用下列Frida API:
· Memory
· NativeFunction
· Module
在编写相应的代码时,我们不妨问自己:“如果用C语言的话,该如何编写?”。 为了回答这个问题,我们执行下面的步骤。
· 系统调用open的第一个参数是const char *,即文件路径。对应的,这里可以使用Memory.allocUtf8String(path)。
· 为了调用open,我们需要获取其原始地址,以便可以使用NativeFunction构造函数,从而获取调用open的权限。
· 可以通过Frida的Module.findExportByName()来获得open的地址。
· 在调用open之后,系统会返回已打开的共享库的文件描述符返。
现在,已经成功地打开了共享库,下面,我们需要计算文件的大小,并根据该值读取共享库的内容。在这里,我们通过调用fstat来获取共享库的大小。
int fstat(int fd, struct stat *buf);
fstat函数需要两个参数
· 第一个参数是一个文件描述符,前面的open调用将返回相应的文件描述符。
· 第二个参数是一个指向已分配的stat struct的指针。
我们可以通过Frida的Memory.alloc()为stat struct分配内存。
调用fstat函数的具体步骤,请参考调用open的过程。
现在,我们需要从stat struct的st_size成员中读取数据,以获得共享库的大小。
off_t st_size 对于常规文件,它表示文件大小,以字节为单位。对于符号链接,它表示包含在符号链接中的路径名的长度,以字节为单位。
当我们在网上搜索off_t的大小相关知识时,只找到了以下内容:
blkcnt_t和off_t应该是有符号的整数类型。
鉴于此,同时,还知道到st_size成员在结构体0x30偏移处,我们可以使用Frida的Memory.readS32()来获取其大小。
function getFileSize(fd) { // TODO Get the actual size of this structure var statBuff = Memory.alloc(500); console.log('[+] struct stat --> ' + statBuff.toString()); var fstatSymbol = getSymbolAddress('libc.so', 'fstat'); console.log('[+] fstat --> ' + fstatSymbol); var fstat = new NativeFunction(fstatSymbol, 'int', ['int', 'pointer']); console.log('[+] Calling fstat() [!]'); if(fd > 0) { var ret = fstat(fd, statBuff); if(ret < 0) { console.log('[+] fstat --> failed [!]'); } } console.log(hexdump(statBuff, { offset: 0, length: 20, header: true, ansi: true })); var size = Memory.readS32(statBuff.add(0x30)) if(size > 0) { console.log('[+] size of fd --> ' + size.toString()); return size; } else { return 0; } }
最后,我们可以使用为共享库分配的内存(这段内存的大小是通过fstat获得的)以及通过open调用返回的文件描述符来实现系统调用read的功能。
function openAndReadLibrary(library_path) { library_path_ptr = Memory.allocUtf8String(library_path); console.log('[+] path --> ' + library_path_ptr.toString()); open = getSymbolAddress('libc.so', 'open'); console.log('[+] open --> ' + open.toString()); mOpen = new NativeFunction(open, 'int', ['pointer', 'int']); console.log('[+] Opening --> ' + library_path); var fd = mOpen(library_path_ptr, 0); if(fd < 0) { console.log('[+] Failed to open --> ' + library_path); } console.log('[+] fd --> ' + fd.toString()); var size = getFileSize(fd); var read_sym = getSymbolAddress('libc.so', 'read'); var read = new NativeFunction(read_sym, 'int', ['int', 'pointer', 'long']); var rawElf = Memory.alloc(size); if(read(fd, rawElf, size) < 0) { console.log('[+] Unable to read ELF [!]'); return -1; } console.log('[+] read --> ' + size + ' bytes [!]'); console.log(hexdump(rawElf, { offset: 0, length: 20, header: true, ansi: true })); return rawElf }
下面是运行解析ELF的Frida脚本的输出结果:
[+] Running elf parser [!] [+] path --> 0xa05af7c8 [+] open --> 0xb6e491dd [+] Opening --> /data/data/com.versprite.poc/lib/libnative-lib.so [+] fd --> 55 [+] struct stat --> 0xa05b0520 [+] fstat --> 0xb6e6d154 [+] Calling fstat() [!] 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 1c b3 00 00 00 00 00 00 00 00 00 00 57 7c 02 00 ............W|.. 00000010 ed 81 00 00 01 00 00 00 e8 03 00 00 e8 03 00 00 ................ 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030 a0 25 00 00 00 00 00 00 00 10 00 00 00 00 00 00 .%.............. [+] size of fd --> 9632 [+] read --> 9632 bytes [!] 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............ 00000010 03 00 28 00
解析ELF文件
由于这里只是为了演示,所以,我实现的ELF解析器的功能非常简单:
· 解析ELF头
· 解析程序头表
· 解析节头表
作为Javascript的新手,当数据从共享库中读取到缓冲区后,我还不清楚该如何访问和可视化这些数据。说得更精确些,是我发现DataView的时候。
“DataView视图提供了一个低级接口,用于以ArrayBuffer形式对多种数值类型进行读写操作,而不用关心平台的字节顺序如何。”
首先,我需要处理ELF头。截止写作本文为止,只支持32位。对于那些不熟悉ELF头结构的人,下面给出32位的具体定义。
#define EI_NIDENT 16 typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr;
解析二进制格式文件时,我也喜欢使用010 Editor,因为它可以将以代码方式进行的所有操作以可视化的形式完成。解析ELF头实际上非常简单,可以通过以下步骤完成。
由于我们的指针已经指向内存中的ELF文件,所以,可以使用Frida的Memory.readByteArray(),根据Elf32_Ehdr的大小,将ELF头读入缓冲区。然后,从这个缓冲区构造一个新的DataView实例。
DataView类提供了从指定索引读取数据类型的方法。例如,如果我们想读取Elf32_Ehdrstructure的e_type成员,我们可以这样做:
var e_type = elfHeaderDataView.getInt32(0x10, true);
通过组合使用Memory.readByteArray()和DataViews,同样也能够处理ELF程序头表和节头表。你可以从下列地址找到这些结构的定义:
https://raw.githubusercontent.com/torvalds/linux/master/include/uapi/linux/elf.h
我将把这个练习留给读者,同时,最终的ELF解析脚本可以从下面的地址找到。
https://github.com/VerSprite/engage/blob/master/js/elf_parser.js
下面是该脚本部分输出结果。
[+] HEADERS ----------------------------- [+] e_type --> 2621443 [+] e_machine --> 40 [+] e_version --> 1 [+] e_entry --> 0 [+] e_phoff --> 52 [+] e_shoff --> 0x21b8 [+] e_flags --> 0x5000200 [+] e_ehsize --> 52 [+] e_phentsize --> 32 [+] e_phnum --> 8 [+] e_shentsize --> 40 [+] e_shnum --> 25 [+] e_shtrndx --> 24 [+] SEGMENTS ----------------------------- [+] segment --> 0x34 : PT_PHDR [+] segment --> 0x54 : PT_LOAD [+] segment --> 0x74 : PT_LOAD [+] segment --> 0x94 : PT_DYNAMIC [+] segment --> 0xb4 : PT_NOTE [+] SECTIONS ----------------------------- [+] .note.gnu.build-id : 0x21e0 [+] s_addr --> 0x134 [+] s_offset --> 0x134 [+] s_size --> 0x24 [+] .dynsym : 0x2208 [+] s_addr --> 0x158 [+] s_offset --> 0x158 [+] s_size --> 0xf0
小结
在本文中,我们为读者展示了在解析二进制格式时,如何借助Frida帮我们处理一些非常重要的事情。在本系列的下一篇文章中,我们将进一步为读者介绍Frida的其他神奇功能。
参考文献:
https://www.frida.re/docs/javascript-api/#interceptor
https://www.frida.re/docs/javascript-api/#NativeFunction
https://www.frida.re/docs/javascript-api/#module
https://linux.die.net/man/3/open
https://linux.die.net/man/2/read
https://linux.die.net/man/2/fstat
https://raw.githubusercontent.com/torvalds/linux/master/include/uapi/linux/elf.h
https://www.sweetscape.com/010editor/