ELF Format DIY For Android

ELF Format DIY For Android

Author: ThomasKing

本文只讨论安卓平台ELF格式一些可以DIY的地方。当然,有些DIY有使用价值,有些DIY仅好玩而已。为了完整性,均在下文讨论。

一、Elf32_Ehdr

1. e_ident[16]

这个字段,现ELF标准只使用了前7个字节,后9个字节是未定义的。在linux平台,这9个字节是填0,且不能动。而安卓平台,参看linker源码,对so文件格式只判定前4个字节,即’7f’, ‘45’, ‘4c’, ‘46’。故可DIY如下图:

图 1

2. 平台相关性标识

在Elf32_Ehdr中,于平台性标识有:ELF文件类型e_type;CPU平台属性e_machine;ELF版本号e_version(ELF版本只有1.2,故该值始终为1);文件相关属性e_flag。这些字段都是来说明ELF文件信息,类似产品说明,对SO文件的使用无任何影响。故可DIY如图。

图 2

Readelf查看信息:

图 3

3. e_entry

对于SO文件来说,这个值是无意义的。所以随便怎么都行。

4. 与section相关

与section相关的e_shoff,e_shentsize,e_shnum, e_shstrndx随意DIY,http://bbs.pediy.com/showthread.php?t=192874写得清楚,就不赘述。

5. 其余字段

其余字段由于加载时会被使用,故不能DIY。详见linker源码。

 

上述DIY无处理时机的限制,即既可对SO作预处理时,也可在代码中。

 

二、Section

1. 移动section

         使用readelf –l查看so文件的Section toSegment mapping:

图 4

         总的来说,除了与代码相关受寻址影响的section外,其余section都是可以移动的。受代码访问影响的section有:.plt,.ARM.extab,.ARM.exidx,.rodata,.got,.data,BSS。其余section可以随意移动。为了处理方便,移动到的位置最好选在当前所处LOAD末尾。由于受到segment的属性(RWX)影响,跨LOAD处理稍微繁琐。以移动到LOAD末尾为例,具体移动某section的处理流程如下:

Step1: 选定移动位置。

Step2: 根据对齐属性,计算合适的起始位置。

Step3: 复制section数据到新位置。

Step4: 修订section在.dynamic中的位置信息,即p_offset、p_vaddr和p_paddr。

Step5: 若移动segment,修订对应在segmentheader中的信息。

这里就不给出例子,下文将看到。

 

2. 增删section

查看section信息可知,LOAD内section之间是紧凑排列的。删除某section的数据,可不移动section。但增加section内容,就需要移动,并且修订section时,需修订p_filesz和p_memsz。有些section可以单独修改,而有些section修改后,需要重新调整与之相关的section。比如往dynsym末尾添加一个符号,并为该符号在dynstr添加一name字符串。便于查找,计算出name的hash值,然后往hash表中添加。如果还涉及到rel,还需修改rel的r_offset和r_info字段。根据上述处理,再移动修改相应的section。这里就不给例子,下文将看到。

 

3. 修改init_array

对fini_array和init_array类似,以init_array为例,讨论init_array的一些DIY。

3.1 变更执行顺序

通过__attribute__((constructor(num)))声明某一函数(num值越小,越先执行),即指定了在.init_array中的位置,修改其顺序即可实现。例如:

void __attribute__((constructor(101))) kingcoming();

void __attribute__((constructor(200)))soldiercoming();

void kingcoming(){

         __android_log_print(ANDROID_LOG_INFO,"init_array", "King is coming!");}

void soldiercoming(){

         __android_log_print(ANDROID_LOG_INFO,"init_array", "A soldier is coming!");}

执行结果:


图 5

修改其顺序,即交换图中红色区域中的数据:

图 6

再执行:

图 7

3.2 普通函数——init函数

将普通的函数,升级为init函数。具体操作步骤:

Step1: 查找目标函数的起始位置。

Step2: 移动rel.dyn到LOAD1末尾

Step3: 移动init_array到LOAD2末尾,添加目标函数地址

Step4: 修订rel.dyn和init_array在.dynamic中的位置。

Step5: 修订LOAD1和LOAD2的长度信息

 

在3.1代码中,添加:

void justHello(){

    __android_log_print(ANDROID_LOG_INFO,"JNITag","Hello!");

}

现将其修改,将justHello提升为init函数,且最先执行。实现流程上述已讨论,具体代码就不贴了,参看附件中的updater.c。处理后的so文件,需要重建section才能查看下图(重建工具:http://bbs.pediy.com/showthread.php?t=192874):

图 8

处理后的SO文件执行结果:

图 9

3.3 init函数转普通函数

相对3.2来说,init函数转普通函数算较简单吧,这里就不赘述了

 

4. GOT表 —— From HOOK to SelfPatch

针对GOT表的HOOK技术屡见不鲜,这里还是啰嗦下一般的HOOK应用场景和流程,以便讨论SO自Patch,实现类似目前基于函数Patch的第二代dex加固技术。下图简单描述了SO函数HOOK的基本原理。

图 10

其中,个人认为有一个很重要的点就是:HOOK的是Import函数,即访问的是外部函数。那么如果HOOK自身的函数,达到替换的函数的目的,就实现了类似DEX的函数patch效果(纯属个人YY)。

在linux平台,PIC模式的SO文件是不区分本地符号还是外部符号,即不管是Import符号还是Export符号,都走GOT过程。采用HOOK原理,即可实现对non-static的变量和函数的替换,达到HOOK和自Patch的效果。

思路清晰,似乎就成功了。但是,随意打开一个ARM架构的SO文件查看GOT表,发现Export函数根本不在GOT中,不过non-static变量还是在的(具体LINUX平台和android在这方面的比较,在附件《Android ELF GOT sectionの不同之处》,此文中仅根据表象,分析了差异点。限于水平,未找到相关资料佐证,其中的原因分析纯属个人YY,难免有错误之处,请各位批评指正)。

一种简单的Patch思路是,通过生成一个空壳libFuncStub.so文件,把需要Patch的函数放在其中。目的是构造一个stub在GOT中。然后SO加载起来之后,通过HOOK GOT自身函数,达到Patch效果。由于linker会将依赖的so也加载起来,libFuncStub.so不能扔掉。

作为SO DIY,不能扔掉是不能忍的。想扔掉libFuncStub.so,即要绕过linker加载机制。一旦绕过linker,让libFuncStub.so不加载,同时函数在执行前被patch掉,就能无缝完成这个过程。Patch利用HOOK原理实现,重点就在如何绕过linker不加载libFuncStub.so。

了解linker加载so大体过程的都知道,linker会将so所依赖的加载起来。具体是:通过DT_NEEDED找到对应在dynstr中的name再加载。同一个so文件不会被加载二次。另外,libdl.so一定会被加载。为了让linker不加载libFuncStub.so,我这里采用修改DT_NEEDED所指向的libFuncStub.so为libdl.so,达到fake的目的。另外还有一个问题就是函数重定向。

还是查看linker源码,发现linker对所有的重定位都采用同样的方法,如图所示:

图 11

似乎可以不用作任何处理。测试发现,加载时报出重定位错误,经过仔细分析linker重定位过程,发现一点:由于_elf_lookup函数寻找符号时,有此if(s->st_shndx == 0) continue;判定,返回NULL,导致relocate错误。st_shndx =0即SHN_UNDEF,故将此设置为非0 即可。那么整个流程的具体步骤就是:

预处理:

Step1: 抹去DT_NEEDED中对应的so文件

Step2: 找到对应的函数符号,修改st_shndx信息

调用函数前的Patch流程:

Step1: 找到SO起始内存

Step2: 找到符号表、字符串表、重定位表

Step3: 找到stub函数并替换

 

下面,构造一个简单的例子来说明。为了简单期间,不涉及section移动,rel表组合,加密等等。Java调用naïve patchTest,调用getNameStub函数获得字符串并打印。getNameStub定义在libFuncStub.so中。实现目标:通过patch,调用getNameStub即调用SO自身的getName函数。流程上述已说明,代码就不贴了,见附件。patchTest函数:

void patchTest(){

    char name[20];

    int i = 0;

    if(flag) //第一次调用函数时,进行Patch

    {

        patchName("getNameStub","getName");//Patch函数

        flag= 0;

    }

    getNameStub(name);//获取到字符串”ThomasKing”,并非”Stub!”

    __android_log_print(ANDROID_LOG_INFO,"JNITag","Show my name: %s", name);

}

运行效果很简单,就打印一句话:

图 12

当然,PatchName函数不仅仅只Patch,可以把基于函数解密融合在一起(基于函数加解密实现见贴:http://bbs.pediy.com/showthread.php?t=191649)。

 

这里稍微再YY一下,针对无源码加解密实现。(就起原因是上次做了一个很挫的SO加解密投去ALICTF热身赛)。一种较好的大致思路是,将原SO抹去section,加密添加在壳子SO末尾。把原SO的rel表组合到壳子的rel表,壳子可以自身进行基于SECTION、函数等等加密。执行时,将原SO 匿名映射到内存,修复SO的rel表。当然,如果为了保证JNI入口地址的一致性,再使用NDK HOOK(http://bbs.pediy.com/showthread.php?t=192047)到原SO函数。

 

这个Patch还算是完美,毕竟不依靠stub.so。不过编译时能否不依靠呢? 即又回到如何在GOT表中构造stub的问题。上述方案是通过HOOK函数符号实现。从原理上,还可以HOOK non-static变量。可能会问,non-static变量本来就是可以访问的,HOOK无非可以改变函数地址而已。从函数指针的间值寻址出发,又可以构造一种Patch方案,可使stub函数存在于自身SO中,在编译时不依靠stub.so文件。具体实现流程和上述差不多,只是修改rel.dyn,限于篇幅,就不贴了吧。相信各位读者都能做到。

 

三、Segment

由于segment已经包含了一些section,对segment的DIY只是简单的整体移动和长度增长。前面例子已经看到,就不赘述了。

 

四、参考文献

Linker源码

ELF文件格式

http://bbs.pediy.com/showthread.php?t=191649

http://bbs.pediy.com/showthread.php?t=192047

http://bbs.pediy.com/showthread.php?t=192874

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值