Android下实现injectso

来源:互联网 发布:遍历二叉树非递归c语言 编辑:程序博客网 时间:2024/05/16 05:38

光鸡的文章,不错,转一下:

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就可以了。

继续中。。。。