游戏安全实验室 首页 技术入门 查看内容

 阅读目录

【技术入门】Android平台下静态插入ShellCode实现ELF的注入

发布于:2017-10-19 14:23   |    169265次阅读 作者: 管理员    |   原作者: TP

 l  导语

Android平台上注入方式多种多样,一般可以将这些注入方法分为两大类,即静态注入和动态注入。动态注入是指在进程运行过程中获取执行权限加载外部so,在ROOT环境下一般主要依靠ptrace来注入,在非root环境下可使用Virtual App等注入。

静态注入即在ELF装载至内存前对APK包或ELF文件进行静态处理,常见方法包括:重打包APK添加smali代码加载外部so、伪造so 注入、ELF文件添加额外DT_NEEDED项等。本文将介绍另外一种通过插入ShellCode到原ELF文件中来静态注入so的方法。

l  ELF文件格式

Android平台上Native代码一般会被编译成so后缀文件,so后缀文件则为ELF文件格式,Android 静态注入实际是一个修改ELF可执行文件的过程,在了解ELF静态注入前需要对ELF文件格式了如指掌,介绍ELF文件格式的文章较多,此处仅简单介绍一下ELF文件中一些基本结构以及本次静态注入中需要使用到的节区。(需要注意:以下文件格式介绍和静态注入仅针对32位的ELF文件)

下面的例子以一个简单编写的实例来研究ELF文件格式和静态注入,如下所示源代码会使用NDK编译生成libElfInject.so文件,其中定义的_init函数地址会被放在.dynamicDT_INIT项中,使用__attribute__((constructor)) 定义的函数会被放在.init_array节中,两个函数都会在该so文件加载进内存时执行。

void _init(void)
{
	LOGD(".Init Section Call");
	return;
}

__attribute__((constructor)) void Entry()
{
	LOGD(".Init_Array Section Call");
	return;		
}

void SoEntry()
{
	LOGD("So Func Entry");
	return;
}

l  链接视图和执行视图

ELF文件既要参与程序链接又要参与程序执行,因此ELF文件提供了两种视图:链接视图和执行视图,如下图所示,分别为链接视图和执行视图的大致结构。链接视图是方便静态查看ELF文件结构,执行视图则是在ELFlinker加载到内存中使用。本文主要是实现静态注入,因此以链接视图为主,节区头部表(Section Header Table,下文简称SHT)内容是本文关注的重点,对程序头部表(Program Header Table,下文简称PHT)有兴趣的同学可参考其他资料。(Android  N以下版本linker加载ELF文件时无需用到SHT,因此部分加壳的soSHT可能被抹去,此时可以从PHTDynamic段中获取节区信息来修复SHT,此种情况暂不在本文探讨范围内)

l  ELF  Header

无论在链接视图还是执行视图,ELF Header都是不可缺少的一部分,如下所示为ELF Header的数据结构定义,ELF Header包含了文件的一些基本信息以及指示了SHTPHT的位置,其中e_phoff字段为PHT起始文件偏移,e_phentsize字段为PHT中每项Program Header Entry大小,e_phnumPHTProgram Header Entry个数,e_shoffSHT起始文件偏移,e_shentsize为每项Section Header Entry大小,e_shnumSection Header Entry项数,e_shstrndx为节区名字符串表索引。

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;
 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;

使用”readelf -h libElfInject.so”命令可查看ELF文件的ELF Header,如下图所示,可以看到PHT文件偏移为0x34SHT文件偏移为0x3124

l  Program Header

在链接视图中PHT是可选的,PHT实际上是一个结构数组,每个结构描述了一个段,一个段可以包含多个节。如下所示为PHT中每项Program Header Entry的数据结构描述,PHT中每项都描述了一个对应段的类型、文件偏移、段大小,内存地址、段大小等信息。

typedef struct elf32_phdr{
 Elf32_Word p_type;
 Elf32_Off p_offset;
 Elf32_Addr p_vaddr;
 Elf32_Addr p_paddr;
 Elf32_Word p_filesz;
 Elf32_Word p_memsz;
 Elf32_Word p_flags;
 Elf32_Word p_align;
} Elf32_Phdr;

使用”readelf -l libElfInject.so”可查看libElfInject.soPHT,如下图所示,可看到libElfInject.so的各段的文件偏移、大小、内存地址等属性,每个段对应0个或多个节区。

l  Section Header

SHT在执行视图中是可选的,如下所示,为SHT中每个节区头部的数据结构描述,其中sh_name用于指定节区名称,sh_type字段用于区分节区的内容和语义,sh_addr字段指向该节加载到内存中的地址,sh_offset指向节区的起始文件偏移,sh_size字段为节区的大小。由于本文描述静态注入,因此下文中将针对静态注入中几个需要使用到的节区进行介绍。

typedef struct elf32_Shdr{
 Elf32_Word sh_name;
 Elf32_Word sh_type;
 Elf32_Word sh_flags;
 Elf32_Addr sh_addr;
 Elf32_Off sh_offset;
 Elf32_Word sh_size;
 Elf32_Word sh_link;
 Elf32_Word sh_info;
 Elf32_Word sh_addralign;
 Elf32_Word sh_entsize;
} Elf32_Shdr;

使用”readelf -S libElfInject.so”可查看libElfInjectSHT,如下图所示,列出了libElfInject.so的所有节区的属性,根据SHT中各节区头部内容可以索引到对应节区。

l  .shstrtab节区

若需要解析SHT中各节区头部信息,则首先需要识别.shstrtab节,该节区类型为SHT_STRTAB,包含了节区名称的字符串表,每个Section Headersh_name字段实际存储着一个相对.shstrtab节起始地址的偏移,指向了节区中一个以NULL结尾的字符串,该字符串则为对应节区的名称。

l  .dynsym.dynstr节区

.dynsym节区中包含了动态链接过程中的符号表,.dynsym中引用的字符串都来自.dynstr节,.dynstr节区类型也为SHT_STRTAB.dynsym节区中实际存储的是一个数组,其中每项符号信息的数据结构描述如下所示,其中st_name字段为相对.dynstr节起始地址的偏移,指向了符号名称,st_value字段给出相关联符号的取值,取值与符号类型有关。在ELF文件中引用符号时一般存储的都是符号表索引,根据该索引在符号表中查找到对应符号信息。

typedef struct elf32_sym{
 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;

使用”readelf -s libElfInject.so”可以查看libElfInject.so.dynsym结构内容,如下图所示,.dynsym节区中共包含58项符号,每项符号的名称、索引、类型等信息都逐一显示出来。

l  .rel.dyn.rel.plt节区

ELF文件中的重定位节为.rel.dyn.rel.plt节,.rel.dyn节区包含了除外部过程调用的符号以外的所有重定位对象,而.rel.plt节的每个表项对应了所有外部过程调用符号的重定位信息。重定位节区的重定位项数据结构描述如下,其中r_offset字段对应重定位地址,r_info字段的低8位为重定位类型,高24位为重定位项的符号表索引。

typedef struct elf32_rel {
 Elf32_Addr r_offset;     // 重定位地址
 Elf32_Word r_info;       // 符号表索引 + 类型
} Elf32_Rel;

#define ELF32_R_SYM(x) ((x) >> 8)
#define ELF32_R_TYPE(x) ((x) & 0xff)

ARM架构的ELF的重定位类型较多,比较常见的有R_ARM_JMP_SLOT(外部函数直接调用重定位)、R_ARM_GLOB_DAT(外部函数全局指针调用)、R_ARM_RELATIVE(基址重定位)等。

使用”readelf -r libElfInject.so”可查看libElfInject.so的重定位节内容,如下图所示,在.rel.dyn节区中包含9项重定位信息,在.rel.plt中包含8项重定位信息。

l  .plt节区

.plt节区存放过程链接代码,如下图所示,为libElfInject.soplt节区内容,由于Android上不对延迟绑定的调用做特殊处理,plt节首部的0x14字节可以暂时忽略。后面的每0xC字节分别表示一个外部函数调用重定位项的过程链接代码。在代码中需要调用外部函数时,一般会先跳转到对应的plt代码中,然后在跳转到got表中存储的外部函数地址。由于Android平台上不会对延迟绑定的调用做特殊处理,因此在ELF文件完成装载和链接后,got表中已经由linker填充了外部函数在内存中的虚拟地址。

l  .init_array.fini_array

若声明函数为constructor属性,则对应函数地址会被存放到.init_array节中,.init_array节区中地址将在ELF文件加载时执行,若声明destructor属性,则对应函数地址会被存放在.fini_array节中,.fini_array节中函数地址将在ELF文件被dlclose时执行。.init_array节和.fini_array区实际是DWORD数组,如下图所示,为libElfInject.so.init_array节和.fini_array节,可以看到.init_array节中存放了Entry的函数地址(该函数地址),fini_array中存放的是函数地址0xC2C

在前面介绍重定位节区时可看到0x3EA00x3EA8地址处都存在R_ARM_RELATIVE重定位项,linker在链接ELF文件时会针对两个重定位项进行处理,即将文件中存储的偏移加上基址,链接完成后,才能够正确调用.init_array.fini_array数组中重定位后的内存虚拟地址。

l  静态插入shellcode思考

l  如何加载so

一般情况下加载外部so主要通过调用dlopen函数来实现,当然也可以自实现linker装载和链接ELF文件的代码,但这种情况不适用于使用shellcode来实现。因此调用dlopen来加载外部so仍然是第一选择。dlopen函数的原型如下所示,传入第一个参数为外部so名称,第二个为flag,返回值为打开sohandle,实际为模块对应soinfo结构的指针。

void *dlopen(const char *filename, int flag);

l  获取dlopen函数地址

要在插入的shellcode中调用dlopen函数加载外部so,首先需要获取内存中dlopen函数地址。若待注入的ELF文件中导入了dlopen函数,则直接跳转到dlopen函数的plt项即可,但一般情况来说,ELF文件比较少会去导入dlopen函数,此时一般可通过下面两种方法来获取dlopen函数地址。

1、静态查看手机的linkerdlopen函数文件偏移,在内存中获取linker加载基地址,加上固定偏移即可定位到dlopen函数地址。

2、获取ELF文件中导入的linker其他函数内存地址,静态查看linker中该函数相对dlopen的文件偏移,计算得到dlopen函数地址。

上面两种方法虽然能够获取到dlopen函数地址,但一方面比较依赖静态查看linker获取文件偏移,而Android碎片化严重,不同厂商编译的linker完全不同,每个手机均需要适配,另外一方面代码量也较大,不适用于在短小的shellcode中实现。

是否有其他方法能够在解决上面两个缺点的基础上获取dlopen的内存地址呢?此时联想到若待注入的ELF文件中有导入dlopen函数则一切迎刃而解,shellcode只需要传入参数跳转到dlopen函数对应的plt项即可。若给ELF文件添加一项导入项相对来说工程量较大,会导致ELF中很多文件结构偏移发生变化。

若对某外部函数的导入项进行篡改呢?被篡改函数需要满足不会被调用或调用时也不会影响核心代码的执行,且若需要篡改主要是篡改重定位项的符号表中符号字符串,则该函数名称长度需要大于等于dlopen函数名称长度。此时” __cxa_finalize”” __cxa_atexit”函数走入了我们视野,”__cxa_finalize”函数在模块dlclose时由linker调用,即使被修改也不会影响模块核心代码的逻辑,只是可能在模块dlclose时发生崩溃,但一般so较少会被dlclose,因此修改实际不会对模块原本的执行造成影响。” __cxa_atexit”函数在linux中用于注册一个函数在目标文件unloaded时调用,但动态调试后发现实际在Android ARM平台的So文件加载时不会去调用” __cxa_atexit”函数,而是由linker负责调用__init.init_array.fini_array数组中的函数,因此修改该函数的导入项也不会对原模块执行造成影响。下面我们将选择” __cxa_finalize”来进行篡改。

l  执行shellcode时机

静态注入shellcode后,如何保证注入的shellcode执行?如下图所示,为Android 5.0.0_r2do_dlopen函数,linker在调用find_library函数完成ELF文件的装载和链接后,返回soinfo结构的指针,然后会调用CallConstructors函数。

CallConstructors函数代码如下所示,可看到主要是调用__init函数和调用.init_array数组中的函数,因此可通过替换Dynamic段的DT_INIT项和.init_array节中地址为shellcode地址,在ELF文件加载时shellcode将会被linker执行。此次我们选择替换.init_array节区中的地址为shellcode地址。

l  shellcode构造

shellcode中需要调用dlopen函数,若替换了.init_array数组中的函数,则在调用dlopen函数后还需要调用原.init_array数组中的函数,构造shellcode如下所示,首先保存R0R1LR寄存器到堆栈中,然后调用dlopen函数,其中R0寄存器存储待注入So名称字符串地址,该字符串存储在shellcode后,然后调用原.init_array数组中被替换的函数,调用完成后恢复R0R1PC寄存器。

static unsigned char  ShellCode[] ={
0x03,0x40,0x2D,0xE9,          // STMFD   SP!, {R0,R1,LR}
0x10,0x00,0x9F,0xE5,          // LDR    R0, [PC, #0x10]
0x01,0x10,0xA0,0xE3,          // MOV             R1, #1
0x00,0x00,0x8F,0xE0,          // ADD             R0, PC, R0
0x00,0x00,0x00,0xEB,          // BL              dlopen 
0x00,0x00,0x00,0xEB,          // BL              Entry
0x03,0x80,0xBD,0xE8,          // LDMFD           SP!, {R0,R1,PC}
0x0C,0x00,0x00,0x00
“/data/libtest.so”                            // 待注入so名称
};

上面构造的shellcode0x100x14处代码还需要进行修复,0x10处调用dlopen函数,实际是跳转到篡改后的dlopen函数plt代码,0x14处若替换.init_array数组中地址为0,则直接填入NOP指令即可,若不为0,则需要跳转到原始的.init_array地址,不会影响ELF文件的原始逻辑。(需要注意的是上面跳转到原始.init_array数组中地址时使用的是BL指令,因此若原始.init_array数组中存储的地址是奇数,表示地址处指令会被解析成thumb指令,而此时使用BL跳转到该地址处会被默认处理为ARM指令,从而导致崩溃,需要对shellcode进行修改,使用BX指令进行跳转,此处我们仅考虑了被替换.init_array节中函数地址为ARM指令的情况)

l  插入shellcode空间

目前已经成功构造shellcode,只需要将shellcode插入到ELF文件中,然后修改.init_array第一项指向shellcode地址即可。Shellcode大小(未包含注入so名称)为0x20大小,因此需要在ELF文件中找到一块空间能够成功插入shellcode,该块插入的空间需要有可执行权限,下面主要有三种思路来插入shellcode

1、在文件尾部插入shellcode指令,在Program Header Table中新添加一个可读可执行的PT_LOAD段,将shellcodeload到内存中,由于插入的新PT_LOAD段会影响其他文件偏移,因此可将PHT移动到文件尾部,修改ELF HeaderPHT的文件偏移等。

2、TEXT段代码中寻找一个非__init.init_array函数插入ShellCode,备份被修改地址处指令,执行shellcode实现外部so的注入后,对被修改地址处代码进行指令修复,与壳实现类似。

3、TEXT段中寻找一处无用空间填充shellcode,则无需修复。在获取dlopen函数地址小节中我们替换了”__cxa_finalize”函数的导入项,如下图所示,.fini_array数组中第一项指向的函数地址为0xEC8,该函数调用了” __cxa_finalize”函数,该函数在ELF文件dlclose时调用,另外我们发现大部分模块中该函数下方代码是调用”cxa_atexit”函数,实际上该处代码在ELF文件装载、链接、执行期间都不会被执行,因此在下图0xEC8处填充shellcode不会影响到原ELF文件的执行逻辑。

本次静态注入我使用了实现较为简单的第三种方法,其他两种方法相对来说稳定性更高,有兴趣的也可以实现。

l  静态插入shellcode实现So注入完整实现流程

1、  读取ELF头获取SHT文件偏移

2、  解析SHT,获取.rel.plt.rel.dyn.init.array.fini.array.dynsym.dynstr.plt节区的文件偏移和大小

3、  读取.rel.plt节,定位到"__cxa_finalize"的重定位项,修改” __cxa_finalize”字符串为”dlopen”,并且记录原” __cxa_finalize”函数的plt代码地址

4、  读取.rel.dyn节,解析.init_array节首部的重定位信息,若不存在重定位信息,则无需记录,若存在重定位信息,记录原重定位信息(一般来说为R_ARM_ABS32R_ARM_RELATIVE,获取对应重定位地址保存),修改该重定位类型为R_ARM_RELATIVE

5、  读取.fini.array节内容,第一项一般指向调用"__cxa_finalize"的函数,若未指定其他注入shellcode地址,则使用该处地址来作为注入shellcode地址

6、  读取.init.array节内容,修改.init_array数组第一项为注入shellCode的地址,记录.init_array数组中的原存储地址,需要注意的是要与前面解析的重定位信息结合起来计算才能得到真实的.init_array数组第一项地址

7、  构造shellcode,修复shellcode0x10处代码,通过BL跳转到原” __cxa_finalize”函数的plt代码地址,即当前dlopenplt代码地址,0x14处通过BL跳转到原.init_array数组第一项的地址,若该地址为0,则使用NOP指令替代

8、  shellcode写入到待注入地址处

9、  修改.fini_array重定位信息,若原.init_array第一项存在重定位信息,则直接将.fini_array的重定位信息置为NULL,若原.init_array第一项不存在重定位信息,则需要将.fini_array的重定位信息篡改成.init_array的重定位

l  测试

此处测试选取外网一款名为地铁跑酷的unity游戏,静态注入shellcode到该游戏lib目录下的libmono.so中,如下图所示,注入的shellcode将加载外部的”/data/libtest.so”

“/data/libtest.so”源代码如下图所示,其中_init函数和Entry函数在ELF文件被加载时会调用。

在手机上安装地铁跑酷游戏后,替换”/data/data/com.kiloo.subwaysurf/lib”目录下的原始libmono.so为经过静态注入处理的libmono.so,打开游戏后,使用logcat查看日志如下图所示,可看到/data/libtest.so__init函数和Entry函数已经成功执行。

查看地铁跑酷进程当前内存中已加载模块可看到”/data/libtest.so”已经成功注入到游戏进程中。

l  思考与优化

上述注入方法仍然存在一些瑕疵,还存在优化空间,例如直接修改ELF文件替换很可能被软件校验,若是注入系统库则更难被发现,但注入系统库容易造成系统崩溃,注入时需要谨慎。另外shellcode和待注入so名称占据空间较大,目前shellcode是使用ARM指令,若采用thumb指令来实现,能够节约shellcode的空间。另外本文选择插入shellcode的空间是在"__cxa_finalize"函数被调用位置,但部分so该处地址无足够空间能够填入shellcode,此时就需要采用另外两种方法来注入shellcode

分享到:
踩0 赞0

收藏

上一篇:【技术入门】加密算法识别和变异加密算法处理

最新评论
B Color Image Link Quote Code Smilies

发表评论