ELF段加密研究 -- ELF文件格式

这几天也要尝试出题搞事。。于是尝试研究一下elf加密的原理吧

ELF段加密研究 – ELF文件格式

背景

之前做ctf之类逆向的题目的时候,总是有一种题目是这样的:

这个题目怪得很,没有main,但是实际上再运行的时候,会进入.init段,然后突然把某个段进行了异或,然后就会得到答案。我对这个过程很疑惑。。。所以打算好好研究一下这点

节(Section)的定义

一般一个正常的程序节中,会有.text(代码节),data(数据节),.bss(未定义数据节)等等。但是为了提供更加自由的定制化,gcc其实提供了给我们定制节的功能;

__attribute__((section (".sectionname")))

通过使用这个指令,能够将我们指定的一段内容放到一个我们自己定义的节中,如:

static int __attribute__((section (".loading"))) func(char* input){

这段内容能够把我们的func放入到.loading节里面,我们编译之后用readelf查看:

[Nr] Name              Type             Address           Offset   Size              EntSize          Flags  Link  Info  Align
[13] .text             PROGBITS         00000000004005f0  000005f0 0000000000000622  0000000000000000  AX       0     0     16
  [14] .loading          PROGBITS         0000000000400c12  00000c12 0000000000000167  0000000000000000  AX       0     0     1

从这可以看到,我们定义的.loading节被放到.text的后方。利用这个特点,我们就能够分离处指定的函数,然后可以对特定的节进行加密(本人主要还是担心如果直接对.text下手会不会出问题)

与段(Segment)的区别

注意一个特点,这个节(Section)和我们平时说的段(Segment)是不一样的。节是在程序编译之后存在的,而段是在程序载入主程序之后发生的。

从上图可以看出,程序linking阶段(也就是编译后,链接时),elf文件是以节(section)划分,而程序开始执行后,是以段(Segment)来划分的。这里要进行区别。
之所以这么做,因为中包含的很多帮助信息,比如说重定向的信息,链接的信息,以及调试信息等。这些信息在我们调试程序的时候会有用,但是在我们真正发布运行一个程序额度时候,是不需要都用上的,所以载入的时候,会选择性的抛弃一些数据。

操作系统会从程序的ph_table(program header table)中拷贝文件的段到虚拟地址段中,甚至利用这点创建内存共享资源

ELF文件格式

扯到加密,我们这边就要考虑到使用Elf.h文件,也就是对ELF本身直接进行操作,所以这里记录一波学习过程。

ElfN_Addr       Unsigned program address, uintN_t
ElfN_Off        Unsigned file offset, uintN_t
ElfN_Section    Unsigned section index, uint16_t
ElfN_Versym     Unsigned version symbol information, uint16_t
Elf_Byte        unsigned char
ElfN_Half       uint16_t
ElfN_Sword      int32_t
ElfN_Word       uint32_t
ElfN_Sxword     int64_t
ElfN_Xword      uint64_t

上面是官方对之后结构体的定义文档(这里可以看出,大佬写的代码是用来看的),其中N的意思是说,32bit中N为32,64bit中为64。知道这个方便我们接下来的分析

ElfN_Ehdr:

#define EI_NIDENT 16

typedef struct {
    unsigned char e_ident[EI_NIDENT];
    uint16_t      e_type;
    uint16_t      e_machine;
    uint32_t      e_version;
    ElfN_Addr     e_entry;          // 函数虚拟地址入口
    ElfN_Off      e_phoff;            // program header 偏移量(可选,不一定存在)
    ElfN_Off      e_shoff;            // section header 偏移量
    uint32_t      e_flags;
    uint16_t      e_ehsize;         // elf头部大小
    uint16_t      e_phentsize;        // 每个 program entries的大小
    uint16_t      e_phnum;            // program header 的数量
    uint16_t      e_shentsize;        // 每个 sections entries的大小
    uint16_t      e_shnum;            // sections header的数量
    uint16_t      e_shstrndx;        // 记录了sections header table 的地址
} ElfN_Ehdr;

这个是对整个ELF本身的定义,我们这里关注section header,就是从elf entry结束开始经过e_shoff个字节能够到达的节。同时,这个e_shentsize大小就是section header entry 的大小也就是说,我们从读入的位置开始移动e_shdoff + e_shentsize*e_shstrndx就能够到达【记录有sections的strndx】。通过这个段我们能够快速的定位到我们需要的段上,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
elf = (Elf64_Ehdr*)buf;
printf("\ne_phoff is %ld\ne_shoff is %ld\n", elf->e_phoff, elf->e_shoff);
file_ptr = (void*)buf;
shdr = (Elf64_Shdr*)(buf + elf->e_shoff );
shdr += elf->e_shstrndx;
if((shstr = (char *) malloc(shdr->sh_size)) == NULL){//开辟内存区域,这个用于保存shstrtab的字符串池
puts("Malloc space for section string table failed");
goto _error;
}
file_ptr = (void*)buf + shdr->sh_offset;
memcpy(shstr, file_ptr, shdr->sh_size);
int sec_num = elf->e_shnum;
shdr = (Elf64_Shdr*)(buf + elf->e_shoff);
// then, to find the sections that we need to now
int dst_sec = -1;
for (i = 0; i < sec_num; ++i)
{
if(strcmp(&shstr[shdr[i].sh_name], target_section) == 0){
printf("find sec: %s\n", &shstr[shdr[i].sh_name]);
dst_sec = i;
break;
}
}

但是。。。得到了段之后要怎么处理呢。。。这里就暂时不知道了。。

(对于混淆的问题,后来暂时用花指令处理掉了)

__asm__ __volatile__(
"lea label, %rax\t\n"\
"push %rax\t\n"\
"ret\t\n"\
"label:"
);

为了防止IDA的检测,编译的时候选择了一下: -fvisibility=hidden 还strip了一下符号表:

strip file

嗯。。。暂时是有一个防护措施了。