看雪上已经有好几个帖子讲述了这方面的原理了,也已经现成的dump工具,不过都没有开源出来,秉着学习的态度自己研究了下,最终实现了基于init_array加密的SO的脱壳。

具体的原理已经有人分享了出来:

我这里给出每个节区addr,offset,size的详细计算方法:

SHN_UNDEF:全部是0,没什么好说的

.dynsym,.hash,.rel.dyn,.rel.plt,.ARM.exidx,.fini_array,.init_array这几个节区直接通过soinfo结构体就能直接恢复:

  • .dynsym:addr = offset = si->symtab - si->base; size = si->nchain * 16;
  • .hash:addr = offset = hash_shdr(注1); size = (2 + si->nbucket + si->nchain) * 4;
  • .rel.dyn:addr = offset = si->rel - si->base; size = si->rel_count * sizeof(Elf32_Rel);
  • .rel.plt:addr = offset = si->plt_rel - si->base; size = si->plt_rel_count * sizeof(Elf32_Rel);
  • .ARM.exidx:addr = offset = si->ARM_exidx - si->base; size = si->ARM_exidx_count * 8;
  • .fini_array:addr = si->fini_array - si->base; offset = si->fini_array - si->base - 0x1000; size = si->fini_array_count * sizeof(Elf32_Addr);
  • .init_array:addr = si->init_array - si->base; offset = si->init_array - si->base - 0x1000; size = si->init_array_count * sizeof(Elf32_Addr);
  • .dynamic:addr = si->dynamic - si->base; offset = si->dynamic - si->base - 0x1000; size = dynamic_count * sizeof(Elf32_Dyn);

.dynstr,.hash,.dynamic:

Elf32_Word strsz = 0;
Elf32_Addr hash_shdr = 0;
size_t dynamic_count = 0;
for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
    switch (d->d_tag) {
    case DT_STRSZ:
        strsz = d->d_un.d_val;
        break;
    case DT_HASH:
        hash_shdr = d->d_un.d_ptr;
        break;
    }
    dynamic_count++;
}
dynamic_count++;
  • .dynstr:addr = offset = si->strtab - si->base; size = strsz;
  • .hash:addr = offset = hash_shdr; size = (2 + si->nbucket + si->nchain) * 4;
  • .dynamic:addr = si->dynamic - si->base; offset = si->dynamic - si->base - 0x1000; size = dynamic_count * sizeof(Elf32_Dyn);

.plt,通过遍历plt头部的固定十六个字节确定起始位置:

unsigned int plt_start[] = {0xe52de004, 0xe59fe004, 0xe08fe00e, 0xe5bef008 };
Elf32_Addr plt_shdr = 0;
for (int i = 0; i < dump_size - 16; i++) {
    if (memcmp(dump_correct_so + i, plt_start, 16) == 0) {
        plt_shdr = i;
        break;
    }
}

其中dump_size为dump且内存修正过后的SO的大小(实际上就是第二个load段的vaddr加上filesz的值),dump_correct_so为dump且内存修正后的SO的指针。

  • .plt:addr = offset = plt_shdr; size = 20 + 12 * si->plt_rel_count;

.got

int type = 0;
int flag = 0;
Elf32_Addr got_addr = 0;
Elf32_Word gotsz = 0;
Elf32_Word global_offset_table = (Elf32_Word)si->plt_got - (Elf32_Word)si->base;
for (Elf32_Rel* rel = si->rel; (Elf32_Addr)rel < (Elf32_Addr)si->rel + si->rel_count * sizeof(Elf32_Rel); rel++) {
    if (rel->r_offset == global_offset_table - 4) {
        type = 1;
        break;
    }
}
if (type == 1) {
    for(Elf32_Word global_offset = global_offset_table - 4; global_offset > 0; global_offset -= 4) {
        flag = 0;
        for (Elf32_Rel* rel = si->rel; (Elf32_Addr)rel < (Elf32_Addr)si->rel + si->rel_count * sizeof(Elf32_Rel); rel++) {
            if (rel->r_offset == global_offset) {
                got_addr = global_offset;
                flag = 1;
                break;
            }
        }
        if(flag == 0) {
            break;
        }
    }
    gotsz = global_offset_table + 8 + si->plt_rel_count * 4 + 4 - got_addr;
    rebuildSectionHeader(".got", shdr_addr, 119, SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, got_addr,
            (Elf32_Off)got_addr - 0x1000, gotsz, 0, 0, 4, 0);
    shdr_addr += sizeof(Elf32_Shdr);
}
else {

}

根据开始贴的两篇帖子,可以知道__global_offset_table__之前和之后为不同的got结构,可能为:{.got, .got.plt}、{.got.plt, .got}中的一种,并且.got.plt与.rel.plt一一对应,.got中的项一定会出现在.rel.dyn中。
Step1: 读取 DT_PLTGOT,获取__global_offset_table__地址,记为:plt_got;
Step2: 读取 plt_got – 4 地址的数值,在.rel.dyn表中进行搜索。如果匹配,说明 GOT 结构式:{.got, .got.plt},转 3.1;否则为{.got.plt, .got}转 3.2;
Step3.1: 继续向前搜索(plt_got -= 4),直到没有在.rel.dyn匹配,从而确定.got起始地址;由于.got.plt与.rel.plt一一对应,所以plt_got之后的大小也可以确定,从而得到gotsz;
Step3.2: 同理由于.got.plt与.rel.plt一一对应,可以确定.got的起始位置,再对(plt_got += 4)地址的数值在.rel.dyn表中进行搜索,直到找到末尾位置。

  • .got:addr = offset = got_addr; size = gotsz;

.text,.ARM.extab,.rodata,.data,.bss,这几个节表均是通过节表之间的相对位置进行重建的,如果节表位置被diy过,则重建无效,两篇帖子已有介绍,不再详述

.shstrtab,这个节表也没什么好说的,按照上面重建的节表,直接重建即可

完整代码就不上了,写的太丑了,真是毫无美感。