[android] 从加壳的so文件中抽出symbols

0x00 背景

大家可能碰到过加壳的so文件,一般很轻松能从内存中dump出解密后的so文件,但是问题是dump出来的so文件里的symbol虽然都在,但是symbol所对应的函数地址都没有了,这样你dump出的so即使放在IDA中分析,看上去也是没有symbol的,难以分析。所以我做了一个可以把内存里的符号表也dump出来的小工具。

0x01 原理

原理很简单,一般加壳工具会利用init_array来加密解密,当系统调用dlopen的load一个so文件时,最先执行的是init_array部分,如果将init_array的第一个函数替换成自己的解密函数的地址,然后在解密函数完成后调用原来的函数,so文件就能实现自解密,具体参见IDA调试android so的.init_array数组。所以其实dlopen被执行之后,so文件就已经是解密状态了。

接着,参考这篇文章:android下运行时动态链接dlopen()和dlsym()的实现,我们可以发现,dlopen返回的其实是一个soinfo结构体,这个结构体可以在安卓的
linker.h源码中找到。注意到这个结构体中有两个域分别是symtab和strtab,它们就存储了我们的主角符号表相关信息,symtab的定义参见elf.h的Elf32_Sym结构体。

再仔细读一下android下运行时动态链接dlopen()和dlsym()的实现这篇文章的最后一段关于soinfo_elf_lookup的源码,就可以分析出symtab和strtab的用法:symtab其实利用了hash来实现的快速查找,我们只要遍历所有hash bucket即可dump出所有symbol了。

static Elf32_Sym *soinfo_elf_lookup(soinfo *si, unsigned hash, const char *name)
{
    Elf32_Sym *s;
    Elf32_Sym *symtab = si->symtab;
    const char *strtab = si->strtab;
    unsigned n;

    TRACE_TYPE(LOOKUP, "%5d SEARCH %s in %s@0x%08x %08x %d\n", pid,
               name, si->name, si->base, hash, hash % si->nbucket);
    n = hash % si->nbucket;

    for(n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]){
        s = symtab + n;
        if(strcmp(strtab + s->st_name, name)) continue;

            /* only concern ourselves with global and weak symbol definitions */
        switch(ELF32_ST_BIND(s->st_info)){
        case STB_GLOBAL:
        case STB_WEAK:
            if(s->st_shndx == SHN_UNDEF)
                continue;

            TRACE_TYPE(LOOKUP, "%5d FOUND %s in %s (%08x) %d\n", pid,
                       name, si->name, s->st_value, s->st_size);
            return s;
        }
    }

    return NULL;
}

0x03 代码

待续。

0x04 代码混淆

这中间学到了一些代码混淆的方法,比如

SMLATTEQ        R0, R10, R0, R0

意思是R0 = R0 + (R0[31:16] * R10[31:16]),如果R0[31:16]或者R10[31:16]为零,或者条件EQ不满足(也就是Z flag寄存器为0),那么该语句将不会对任何寄存器产生影响。SMLATTEQ的具体解释可以参见:SMULxy and SMLAxy

再比如

BGT             0xFFFFFFFF

当GT条件满足时会跳转到0xFFFFFFFF这个无效地址,所以如果通过让GT条件不满足来避免跳转,程序就不会出现access violence,该指令也就是一条无用指令。如何让GT条件不满足,可以通过函数调用来实现。任何函数在执行结束后都会check stack canary,也就是说会进行CMP操作,如果函数正常结束,CMP的结果肯定为1,也就是Z flag寄存器一定为1,而只要Z = 1,那么GT条件就不会满足,具体可以参见这篇文章:Condition Codes 1: Condition Flags and Codes

0x05 关于section header和program header

观察一些加壳后的so文件,可以发现它们有些把section header给删除了,我们可以推测出,section header在装载和动态链接中并没有什么用处。让我们来分析一下库的装载代码:
首先find_library会调用load_library,load_library结束后还要再调用init_library,init_library会直接调用一个叫soinfo_link_image的函数,这个函数继续调用phdr_table_get_dynamic_section,而这个phdr_table_get_dynamic_section函数仅仅就是参照program header返回了dynamic这个域。之后soinfo_link_image拿到dynamic域,就能这样直接初始化诸如init_array,symtab,symstr等field。可以看到在这个过程中并没有用到section header。

0x06 其他的一些参考

从零打造简单的SODUMP工具--网页链接
从零打造简单的SODUMP工具.pdf
Android Linker 与 SO 加壳技术