背景介绍
固件系统中的二进制文件依赖于特定的系统环境执行,针对固件的研究在没有足够的资金的支持下需要通过固件的模拟来执行二进制文件程序。依赖于特定硬件环境的固件无法完整模拟,需要hook掉其中依赖于硬件的函数。
LD_PRELOAD的劫持
对于特定函数的劫持技术分为动态注入劫持和静态注入劫持两种。静态注入指的是通过修改静态二进制文件中的内容来实现对特定函数的注入。动态注入则指的是在运行的过程中对特定的函数进行劫持,动态注入劫持一方面可以通过劫持PLT表或者GOT表来实现,另一方面可以通过环境变量LD_PRELOAD来实现。
在《揭秘家用路由器0day漏洞挖掘》中作者针对D-linkDIR-L(FW_)路由器中的Web应用程序boa,通过hook技术劫持apmib_init()和apmib_get()函数修复boa对硬件的依赖,使得qemu-static-mips可以模拟执行,在文中作者通过LD_PRELOAD环境变量实现对函数的劫持。网上针对LD_PRELOAD的劫持也有大量的描述。但是LD_PRELOAD仍旧不是普适的。
存在的问题
LD_PRELOAD环境变量的开关在编译生成ulibc的时候指定,当关闭该选项的时候,无法使用LD_PRELOAD来预加载指定的动态链接库文件。
固件的二进制文件中会将二进制文件中的Section信息去除掉,只保留下Segment的信息,使得无法通过patchelf来增加动态链接库实现劫持。patchelf支持对二进制文件的patch修改,或者添加执行过程中的链接lib。
本文方案思路
在ELF文件中,存在DYNAMICSegment,ld.so通过该段内容来加载程序运行过程中需要的lib文件,图1为在IDA中反编译后查看的DYNAMIC段的内容。
图1Elf32_Dyn结构体数组
DYNAMIC段介绍
dynamic段开头包含了由N个Elf32_Dyn组成的结构体,该结构体的D_tag代表了结构体的类型。d_un为通过union联合的指针d_ptr或者对应的结构体的值d_val。
typedefstruct{
Elf32_Swordd_tag;
union{
Elf32_Wordd_val;
Elf32_Addrd_ptr;
}d_un;
}Elf32_Dyn;
d_tag字段保存了类型的定义参数,详见ELF(5)手册。下面列出了动态链接器常用的比较重要的类型值
1-DT_NEEDED该类型的数据结构保存了程序所需要的共享库的名字相对字符串表的偏移量
2-DT_SYMTAB动态符号表的地址,对应的节名为.dynsym
3-DT_HASH符号散列表的名称,对应的节名为.hash又称为.gnu.hash
4-DT_STRTAB符号字符串表的地址,对应的节名为.dynstr
5-DT_PLTGOT全局偏移表的地址
如上各个字段对应到IDA中显示如下,d_tag字段代表了动态段的类型,当该值为1的时候。表示程序依赖的共享库的名字,其对应的d_val表示了是要加载的lib的名称在stringtable中的偏移。
共享库文件名对应的结构体的类型值为0x01,如图2所示,0xC4-0xE4保存有5个二进制文件所依赖的共享库名字,其D_VAL的值是指向stringtable的偏移量。
图2DT_NEEDED共享库依赖图
DYNAMIC段的定位
从二进制文件头起始定位DYNAMIC段的流程如下:
ELF_HEADER-ProgramHeader-DYNAMICProgram
ELF_HEADER中保存有ProgramHeader的偏移
图3ELFHeader
ProgramHeader中保存有DynamicSegment的相关结构体信息
图4ProgramHeader
展开该结构体,可以找到Dynamic段在文件中的偏移量0xh
ProgramHeader结构体
DT_NEEDED伪造
动态段由dynamic的结构体数组组成,dynamic的结构体定义如下:
在dynamic和elf_hash之间仍存在一段空余空余可填充空间。本文利用该段空间填充,实现特定lib的加载。
①网安学习成长路径思维导图
②60+网安经典常用工具包
③+SRC漏洞分析报告
④+网安攻防实战技术电子书
⑤最权威CISSP认证考试指南+题库
⑥超页CTF实战技巧手册
⑦最新网安大厂面试题合集(含答案)
⑧APP客户端安全检测指南(安卓+IOS)
本文方案实验与测试
利用dynamic和elf_hash之间的空余区域,在该区域伪造出新的dynamic的一个数组。如下图,不修改二进制文件大小,伪造增添ibcjson.so,使得二进制文件加载ibcjson.so。在ibcjson.so中编写对应的劫持函数。
思路:将原有的Elf32_Dyn数组元素依次后移,并在该数组的首部添加伪造的ibcjson.so,该lib的命名可以选用stringtable中的任一字符串即可。
核心移动代码,将Elf32_Dyn中的元素依次后移一个,dynamic段dynamic[0]元素作为要伪造填充的数据,在本文的实验中,将dynamic[0]中的value值加1。由于在MIPS下存在大小端两种架构,在小端机器上的代码解决大端架构的填充伪造时要注意大小端的转换问题。
voidmove_dynamic(char*buf){
intx=0;
Elf32_Dyn*dyn=(Elf32_Dyn*)buf;
while(1){
if(dyn[x].d_tag==0dyn[x].d_un.d_ptr==0){
break;
}
x++;
if(x){
printf("Errorbreak\n");
break;
}
}
while(x--){
//printf("theindexxis%x\n",x);
mem_cpy(dyn[x],dyn[x+1],8);
}
dyn[x+1].d_un.d_val=b2l(l2b(dyn[x+1].d_un.d_val)+1);
}
测试环境
TOTOLinkNRE中boa程序,劫持函数参考DIRA,劫持apmib_init以及apmib_get
该固件的ld,关闭了LD_PRELOAD程序选项,未提供/etc/ld.preload
劫持函数代码,采用了《揭秘家用路由器漏洞挖掘》提供的示例代码如下,在原有的基础上,增加printf来查看显示是否劫持成功。
#includestdio.h
#defineMIB_HW_VER0x
#defineMIB_IP_ADDR
#defineMIB_CAPTCHA0x2C1
intapmib_init(void){
printf("helllo");
return1;
}
intfork(void){
return0;
}
voidapmib_get(intcode,int*value){
switch(code){
caseMIB_HW_VER:
*value=1;
break;
caseMIB_IP_ADDR:
*value=1;
break;
caseMIB_CAPTCHA:
*value=1;
break;
}
return;
}
通过mips-linux-gcc进行编译,这里注意mips-linux-gcc在编译过程中的编译文件的位置要位于参数之后(踩坑了!!!!)
mips-linux-gcc-Wall-fPIC-sharedapmib.c-oibcjson.so
通过如下代码,给原有的boa二进制文件添加一个dynamic
#includestdio.h
#includestdio.h
#includestdlib.h
#include"elf.h"
#definePATCH"boa_patch"
intb2l(intbe)
{
return((be24)0xff)
((be8)0xFF00)
((be8)0xFF)
((be24));
}
char*buf=NULL;
intl2b(intle){
return(le0xff)24
(le0xff00)8
(le0xff)8
(le24)0xff;
}
staticchar*_get_interp(char*buf)
{
intx;
//Checkfortheexistenceofadynamicloader
Elf_Ehdr*hdr=(Elf_Ehdr*)buf;
Elf_Phdr*phdr=(Elf_Phdr*)(buf+l2b(hdr-e_phoff));
printf("thephdraddressis:0x%x0x%x0x%x0x%x\n",phdr,l2b(hdr-e_phoff),buf,sizeof(hdr-e_phoff));
for(x=0;xhdr-e_phnum;x++){
if(l2b(phdr[x].p_type)==PT_DYNAMIC){
//Thereisadynamicloaderpresent,soloadit
returnbuf+l2b(phdr[x].p_offset);
}
}
returnNULL;
}
intmem_cpy(char*src,char*dst,intlen){
printf("thesrcaddris%x,%x\n",src-buf,dst-buf);
for(intx=0;xlen;x++){
dst[x]=src[x];
}
return0;
}
/*
temp=2
strcpy(1,2)
12
strcpy()
*/
voidmove_dynamic(char*buf){
intx=0;
Elf32_Dyn*dyn=(Elf32_Dyn*)buf;
//Elf32_Dyntmp_dyn;
//mem_cpy()
while(1){
if(dyn[x].d_tag==0dyn[x].d_un.d_ptr==0){
printf("thexis%d\n",x);
break;
}
x++;
if(x){
printf("Errorbreak\n");
break;
}
}
while(x--){
//printf("theindexxis%x\n",x);
mem_cpy(dyn[x],dyn[x+1],8);
}
dyn[x+1].d_un.d_val=b2l(l2b(dyn[x+1].d_un.d_val)+1);
//FILE*fw=fopen("")
}
voidanalyse(char*buf){
char*phdr_address=NULL;
phdr_address=_get_interp(buf);
printf("phdraddress:0x%x\n",phdr_address-buf);
move_dynamic(phdr_address);
}
voidsave_binary(char*buf,intsize){
FILE*fw=fopen(PATCH,"wb");
fwrite(buf,size,1,fw);
fclose(fw);
}
intmain(intargc,char*argv[],char*envp[]){
if(argc2){
printf("notenoughargc\n");
}
FILE*fp=fopen(argv[1],"rb");
fseek(fp,0,SEEK_END);
intsize=ftell(fp);
fseek(fp,0L,SEEK_SET);
buf=malloc(size);
fread(buf,size,1,fp);
analyse(buf);
save_binary(buf,size);
free(buf);
return0;
}
Makefile如下
all:elf.hanalyse_ph.c
gccanalyse_ph.c-m32-g3-oanalyse
./analyseboa_real_n
针对NRE的测试截图如下,通过exportLD_LIBRARY_PATH使得程序加载ibcjson.so,成功劫持boa,输出helllo
Ubuntu下rand函数劫持测试
rand函数的头文件是stdlib.h
编写rand.c
#includestdio.h
#includestdlib.h
intmain(){
inta=0;
a=rand()%;
printf("theais%d\n",a);
return0;
}
//gcc-m32rand.c-orand
编写rand_hook.so
#includestdio.h
intrand(){
printf("hook!\n");
return;
}
//gcc-fPIC-Wall-shared-m32rand_hook.c-orand_hook.so
使用LD_PRELOAD测试,成功实现对该函数的劫持
使用patch,针对dynamic段元素添加伪造,测试rand_patch,依赖的库文件增加了ibc.so.6,ibc.so.6需要通过exportLD_LIBRARY_PATH导入ibc.so.6的文件路径
实现对rand的劫持
总结
本文通过研究二进制文件中的dynamic段,通过修改二进制文件增加依赖共享库,可以解决在模拟固件的过程时,固件缺少节信息且固件函数无法通过LD_PRELOAD劫持的问题。该方案仍有不足之处,对于ld加载共享库的依赖顺序、共享库劫持的底层原理尚未深入探究。
参考
《揭秘家用路由器0day挖掘技术》
《二进制分析实战》
更多靶场实验练习、网安学习资料,请访问合天网安实验室。