ELF Tricks

前置知识:

ELF 段与节

ELF 文件格式提供了两种视图,分别是链接视图和执行视图:

ELF Tricks

因此,即使没有任何的 section, 一个 elf 仍然可以被正常执行。

DWARF 与 .eh_frame

调试信息标准 DWARF (Debugging With Attributed Record Formats) 定义了一个.debug_frame section。该调试信息格式支持处理无基址指针的方法,可以将 ebp 用作常规寄存器,但是当保存 esp 时,它必须在.debug_frame 节中产生一个注释,告诉调试器什么指令将其保存在何处。

现代 Linux 操作系统在 LSB (Linux Standard Base)标准中定义了一个.eh_frame section 来解决上述的难题。这个 section 和.debug_frame 非常类似 (其实我也不是很懂到底有什么区别,不过该有的信息是都有的),但是它编码紧凑,可以随程序一起加载。

奇怪的是,照理来说 DWARF debug_info 信息之中有定义 DW_AT_name 这样一个用于标识函数名称的变量,但是在我写的示例程序进行 unwind 的时候看起来仅仅是读取了 symbol table 中的名称。当无法在 symbol table 之中读取到时将不显示函数名称而仅显示地址。

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <execinfo.h>
#include <signal.h>

void dump(int signo) {void *buffer[30] = {0};
    size_t size;
    char **strings = NULL;
    size_t i = 0;

    size = backtrace(buffer, 30);
    fprintf(stdout, "Obtained %zd stack frames.nm\n", size);
    strings = backtrace_symbols(buffer, size);
    if (strings == NULL) {perror("backtrace_symbols.");
        exit(EXIT_FAILURE);
    }

    for (i = 0; i < size; i++) {fprintf(stdout, "%s\n", strings[i]);
    }
    free(strings);
    strings = NULL;
    exit(0);
}

void func_c() {*((volatile char *) 0x0) = 0x9999;
}

void func_b() {func_c();
}

void func_a() {func_b();
}

int main(int argc, const char *argv[]) {if (signal(SIGSEGV, dump) == SIG_ERR)
        perror("can't catch SIGSEGV");
    func_a();
    return 0;
}

以上的代码 gcc -g -rdynamic 与直接 gcc 中就能看出区别,然而背后的原因我也不是很懂,以后再来研究(逃

symtab 与 symbol table

正如上文所说,elf 在执行和链接的时候分别用到了 segment 和 section,而 symtab 正是一个 section,symbol table 是由地址偏移标记的一个 segment,两者中其实都包含了符号信息

ELF Tricks

 

然而,由于 symbol table 才是运行的时候系统会读取的,因此在链接完成之后 symtab 即无用,可以去掉或进行魔改,不会影响源程序运行

玩一点花样

样本思路来自 poor man's obfuscator

eh_frame 修改

前文已经提到,,eh_frame 中包含了一些栈回溯的信息。可以使用 readelf -wF 来读取。然而,正如我们所见,由于需要函数地址来进行 unwind(具体可以参照 这篇文章,讲得很详细),eh_frame 段中存了几乎所有的函数地址

ELF Tricks

那么,聪明的反编译工具们就要从这里入手了。。。有些工具会读取这些信息并从这些地方开始反汇编

因此,当我们不需要进行栈回溯的时候,对这些地址进行修改,就能达到欺骗工具的目的

不过,ida binjia 好像并没有受此影响,然而 ghidra 却报了错

ELF Tricks

看起来 ghidra 认为这个 elf 使用了非标准版本的 dwarf,因此依照默认参数无法继续加载

解决方法

要解决也很简单,在加载选项之中不勾选 dwarf 就可以了

EXPORT TABLE

导出表包含了所有需要外部访问的符号和函数,和他们的地址。因此,导出表常常被反编译器们相信。如果创建一些不需要的,但是是虚假的导出函数呢?

ELF Tricks ELF Tricks

很显然,ida 认为导出表是可信的,并且在一个函数的中间创建了函数,导致函数被破坏,无法正常的反编译

解决方法

既然导出表是假的,那就不要了(

所以 rip 掉导出表即可

SECTION NAME

还记得文章最开始说的,在执行的时候只需要 segment 而不需要 section 吗?那么如果互换 section 的名字会怎么样呢?

先说结论,会引发 ida 的 bug。。。当时第一次看到的时候还是挺震惊的。。。

ELF Tricks

进行这样的调换(由作者 Romain Thomas 提出,可能是他在经过尝试之后发现效果最好的)

然而虽然照理来说对 x86,x86_64 的 elf 是有效的,但是现在测试之后发现还是对 arm 的比较有效,而且 function 需要比较大才会出现中间有 byte 出来的情况

具体请看真题

ELF Tricks

前面的段名称出现了双重名称的盛况,一片爆红非常好看。。。中间出现很多不反编译的 bytes。。。还是挺离谱的

但是在 x64 上面我测试了会变成这样

ELF Tricks

可以看到,尽管载入时爆了一大堆错,但是最后函数可以正常反编译。所有段的名字也都没了 emmmmm,不过还需要再尝试吧

解决方法

我的思路是手动换回来,其实比较好分辨,经过测试换回来就正常了。。。

还有一个方法,就是在 ida 加载的时候取消勾选 sht 即可

dynsym

动态符号表 (.dynsym) 用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。'

所以好像直接修改这个东西的大小好像就能在一定程度上让反编译器出 bug。。。按照作者的话来说 ghidra 受的影响是最大的,据说 ida 也会受影响但我好像没有看出来

底层原理还是没看懂(

结论

其实都是些没啥用的花活。。。最粗暴的方法就是直接去掉这些乱七八糟的信息,但其实第一次看到这玩意还是挺头疼的,主要是不知道发生了啥,见过一次就好了

正文完
 0
评论(没有评论)