光鸡的文章,不错,转一下:
http://photonwen.i.sohu.com/blog/view/201923753.htm
///////////////////////////////////////////
参考资料
Linux中ELF文件动态链接的加载、解析及实例分析
ELF动态解析符号过程
android linker 浅析
ORACLE链接程序和库指南
Modern Day ELF Runtime infection via GOT poisoning
看了几天elf,找到不少文章,不过大多写的很难看明白,上面的链接里最后一个还算比较清晰。
不过我关心的是arm下的,而找到的资料大多是x86下的,还是有些不一样。
写了个小程序,里面定义了myfunc函数,这个函数里调用了dlopen。在android ndk环境下编译通过。
1 #include <stdio.h>
2 #include <stdlib.h>
3 void myfunc(){
4 dlopen("sdfasdfa",2);
5 }
6 main(){
7 myfunc();
8 }
下面我用objdump和readelf来分析下dlopen这个外部函数是如何被调用的。
不废话,直接看看
00008570 <myfunc>:
8570: 4803 ldr r0, [pc, #12] ; (8580 <myfunc+0x10>)
8572: 2102 movs r1, #2
8574: b510 push {r4, lr}
8576: 4478 add r0, pc
//r0,r1保存调用参数
8578: f7ff efd0 blx 851c <_start-0x24> //原来dlopen的地址在0x851c
857c: bd10 pop {r4, pc}
857e: bf00 nop
8580: 0000150e andeq r1, r0, lr, lsl #10
...
接着在.plt里找到了0x851c
000084d8 <.plt>:
84d8: e52de004 push {lr} ; (str lr, [sp, #-4]!)
84dc: e59fe004 ldr lr, [pc, #4] ; 84e8 <_start-0x58> //lr = 0x9790
84e0: e08fe00e add lr, pc, lr //lr = 0x9790+0x84e8
84e4: e5bef008 ldr pc, [lr, #8]!
//pc = [ 0x9790+0x84e8+8] = [0x11C80]
84e8: 00009790 muleq r0, r0, r7
84ec: e28fc600 add ip, pc, #0
84f0: e28cca09 add ip, ip, #36864 ; 0x9000
84f4: e5bcf790 ldr pc, [ip, #1936]! ; 0x790
84f8: e28fc600 add ip, pc, #0
84fc: e28cca09 add ip, ip, #36864 ; 0x9000
8500: e5bcf788 ldr pc, [ip, #1928]! ; 0x788
8504: e28fc600 add ip, pc, #0
8508: e28cca09 add ip, ip, #36864 ; 0x9000
850c: e5bcf780 ldr pc, [ip, #1920]! ; 0x780
8510: e28fc600 add ip, pc, #0
8514: e28cca09 add ip, ip, #36864 ; 0x9000
8518: e5bcf778 ldr pc, [ip, #1912]! ; 0x778
851c: e28fc600 add ip, pc, #0 //注意:pc总是指向当前地址+8
8520: e28cca09 add ip, ip, #36864 ; 0x9000
8524: e5bcf770 ldr pc, [ip, #1904]! ; 0x770
//pc=[0x8524+0x9000+0x770]=[0x11c94]
8528: e28fc600 add ip, pc, #0
852c: e28cca09 add ip, ip, #36864 ; 0x9000
8530: e5bcf768 ldr pc, [ip, #1896]! ; 0x768
8534: e28fc600 add ip, pc, #0
8538: e28cca09 add ip, ip, #36864 ; 0x9000
853c: e5bcf760 ldr pc, [ip, #1888]! ; 0x760
0x11c94是.got节中的一项
Disassembly of section .got:
00011c78 <_GLOBAL_OFFSET_TABLE_>:
11c78: 00011b90 muleq r1, r0, fp
11c7c
11c80
...
11c84: 000084d8 ldrdeq r8, [r0], -r8
11c88: 000084d8 ldrdeq r8, [r0], -r8
11c8c: 000084d8 ldrdeq r8, [r0], -r8
11c90: 000084d8 ldrdeq r8, [r0], -r8
11c94: 000084d8 ldrdeq r8, [r0], -r8
11c98: 000084d8 ldrdeq r8, [r0], -r8
11c9c: 000084d8 ldrdeq r8, [r0], -r8
...
所以pc现在指向0x84d8,看最前面的.plt节,就知道这就是plt0
plt0会跳转到[0x11C80],注意这个0x11c80也在.got节里。
从上面的分析得到一个疑惑,无论调用哪个外部函数都会去调用plt0,可plt0怎么知道我们到底在调用哪个外部函数? x86平台下是有传参数(got中的地址,和函数对应的符号表项)。在arm下我是没看到什么参数,只有ip比较可疑(见
绿色行)。
不过Android下现在是不支持lazy binding的,所以就算上面的plt0只是个摆设也没关系。可执行文件加载时其调用的外部函数对应的.got项就被修改了。这只是猜测,为了验证现在去看看linker的实现。
从dlopen入手一点点看进去:
void *dlopen(const char *filename, int flag)
{
soinfo *ret;
...
ret =
find_library(filename);
...
return ret;
}
dlopen本身很简单,只需要看find_library
soinfo *find_library(const char *name)
{
soinfo *si;
...
si = load_library(name); //将PT_LOAD段map到进程空间
if (si == NULL)
return NULL;
return
init_library(si);
}
find_library里调用了load_library和init_library。load_library只是将需要文件需要加载的部分用mmap映射到进程空间,所以这里就不贴出代码,下面来看init_library
static soinfo *init_library(soinfo * si)
{
...
if (
link_image(si, wr_offset)) {
/* We failed to link. However, we can only restore libbase
** if no additional libraries have moved it since we updated it.
*/
munmap((void *) si->base, si->size);
return NULL;
}
return si;
}
init_library只是调用了link_image,舒服,基本都是单线联系
static int link_image(soinfo * si, unsigned wr_offset)
{
...
for (d = si->dynamic; *d; d += 2) {
if (d[0] == DT_NEEDED) {
soinfo *lsi = find_library(si->strtab + d[1]);
d[1] = (unsigned) lsi;
//从这儿我们可以找到可执行程序所用到的so文件的si信息,好像si是链表,那甚至能便利系统中所有so以及可执行文件。
}
}
//phdr的type和shdr的type太似是而非了,让我有点发晕。:)
if (si->plt_rel) {
//
DT_JMPREL ==
.rel.plt 此节存的是外部函数
if (
reloc_library(si, si->plt_rel, si->plt_rel_count))
goto fail;
}
if (si->rel) {
//DT_REL == .rel.dyn
此节可能只是外部变量
if (
reloc_library(si, si->rel, si->rel_count))
goto fail;
}
...
}
link_map稍微复杂点,先是找到程序所依赖的别的so,将它们一一加载。关键的是这段程序居然将lsi都保存在好找的地方了,与己方便与人方便啊。窃喜中。.rel.plt和.rel.dyn这两个节都是Elf32_Rel结构数组。
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
仔细看objdump/readelf的输出可以知道.rel.plt里的ELF32_Rel.r_offset是外部函数在.got数组里的对应项的地址。有点绕,可能没说清楚,.got是地址的数组,回去看
行,对应dlopen这个外部函数的r_offset就是0x11c94. 不信? 再贴,数据说话
Disassembly of section .rel.plt:
000084a0 <.rel.plt>:
84a0: 00011c84 andeq r1, r1, r4, lsl #25
84a4: 00000416 andeq r0, r0, r6, lsl r4
84a8: 00011c88 andeq r1, r1, r8, lsl #25
84ac: 00000616 andeq r0, r0, r6, lsl r6
84b0: 00011c8c andeq r1, r1, ip, lsl #25
84b4: 00000b16 andeq r0, r0, r6, lsl fp
84b8: 00011c90 muleq r1, r0, ip
84bc: 00000c16 andeq r0, r0, r6, lsl ip
84c0:
00011c94
muleq r1, r4, ip
84c4:
00000e16
andeq r0, r0, r6, lsl lr
84c8: 00011c98 muleq r1, r8, ip
84cc: 00000f16 andeq r0, r0, r6, lsl pc
84d0: 00011c9c muleq r1, ip, ip
84d4: 00001216 andeq r1, r0, r6, lsl r2
看到
绿色的地址了吧。不要晕了,这段不是可执行代码,忽略反汇编代码吧。
好了,你要看不明白我也没辙了。下面接着看代码,
这就到了比较关键的函数reloc_library:
static int reloc_library(soinfo * si, Elf32_Rel * rel, unsigned count) //rel为.rel.plt
{
Elf32_Sym *symtab = si->symtab;
//SYMTAB == .dynsym 动态符号表
const char *strtab = si->strtab;
//STRTAB == .dynstr 动态字符表
...
for (idx = 0; idx < count; ++idx) {
unsigned type = ELF32_R_TYPE(rel->r_info); //0xe16
unsigned sym = ELF32_R_SYM(rel->r_info); //0x0e = 14
unsigned reloc = (unsigned) (rel->r_offset + si->base);
//对于可执行文件si->base总是0
//所以强调下reloc就是外部函数对应的.got项的地址
...
if (sym != 0) {
sym_name = (char *) (strtab + symtab[sym].st_name);
/*
这里有必要说说动态符号表,它是Elf32_Sym的数组,
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;
前面我们知道sym=14,所以对应dlopen的符号表项是
Disassembly of section .dynsym:
000081cc <.dynsym>:
... 忽略14*4行
82ac: 000000ad andeq r0, r0, sp, lsr #1
82b0:
0000851c
andeq r8, r0, ip, lsl r5
82b4: 00000000 andeq r0, r0, r0
82b8: 00000012 andeq r0, r0, r2, lsl r0
*/
s = _do_lookup(si, sym_name, &base);
//这个_do_lookup是在可执行文件内部以及它所依赖的外部so里查找相应函数对应的符号表项。
//是不是觉得symtab[sym]已经就是Elf32_Sym类型了,还找啥。
//关键在于查找的条件是要在hash里能找到才算,本地没有实现的函数是没有hash项的(我猜的)
if (s == NULL) {
...
} else {
...
sym_addr = (unsigned) (s->st_value + base);
//找到了外部函数的真实地址了!注意这个s可不是前面的symtab[sym],尽管类型一样。
}
} else {
s = NULL;
}
switch (type) {
case R_ARM_JUMP_SLOT:
...
*((unsigned *) reloc) = sym_addr;
//这里将找到的函数地址存到对应的.got项,大功告成!
break;
case R_ARM_GLOB_DAT:
...
*((unsigned *) reloc) = sym_addr;
break;
...
}
}
return 0;
}
通过上面的分析,证实了我的猜测,可执行文件在加载时所使用的外部函数的.got项都会被替换为真实的地址,这样就不会走到plt0.
总结一下,
.rel.plt的
r_offset保存这对应的.got项地址,这个地址的内容就是我们要修改的函数地址
.rel.plt的r_info包含type和sym两个信息,结合本地
动态符号表和
动态字符串表可以找到函数的名字。然后用这个名字在so文件的符号表查找函数对应的地址,最后将这个地址填到.got里。
所以这里主要涉及到下面四个节
.dynsym DYNSYM 00008280 000280 0002f0 10 A 4 1 4
.dynstr STRTAB 00008570 000570 0001ed 00 A 0 0 1
.rel.plt REL 000087a8 0007a8 0000f0 08 A 3 7 4
.got PROGBITS 0001293c 00293c 0000a8 04 WA 0 0 4
再就是和hash相关的节,用于确定函数的实现是否存在so里。
下面来思考下如何实现对可执行文件的动态注入,常规的ptrace这里就不多说了。
1,attach到某个运行中的进程,遍历PT_DYNAMIC段,取回已经加载的so的
soinfo,根据soinfo找到动态符号表
symtab,然后找到dlopen函数的地址以及我们希望替换的任意函数的地址。 <==已经搞定
2,调用dlopen加载自己的so <==就这步搞不定!
3,然后是替换 <==这一步应该没问题
补充:
关于base
对于可执行文件,编译时已经将所有的Addr加了0x8000,所以程序总是Load到0x8000,访问内存映像时不用加base了。
而对于动态链接文件,base是动态分配的,所以访问动态链接文件的内存映像需要用base+Addr
在coding中。。。
遇到问题了
取到的函数的地址怎么不是4字节对齐的?对arm太不熟悉了,用gdb跟踪又发现pc寄存器里的值居然是16位的,好像arm支持在32位和16位指令间切换。
然后只好写了段汇编代码,然后将这段代码压栈,将pc指向sp,这回倒是没有SIGILL了,可是压栈的程序就和没执行一样。郁闷了。
对齐的问题解决了
,首先地址最低位如果是1,那么需要将此bit置0,并且将cpu设置到T模式
不过直接调用dlopen尽管返回值不为NULL,实际上so并没有load。原因不知,但是可以先调用mmap分配一块内存空间,然后dlopen就可以了。
继续中。。。。