精华内容
下载资源
问答
  • 二进制软件分析binary
    2021-02-09 15:29:31

     

    linux二进制文件分析

    “这个世界上有十​​种人:懂二进制的人和不懂二进制的人。”

    Linux提供了丰富的工具集,可轻松进行二进制分析! 无论您的工作角色是什么,如果您在Linux上工作,了解有关这些工具的基础知识将有助于您更好地了解系统。

    在本文中,我们将介绍这些Linux工具和命令中最流行的一些,其中大多数将作为Linux发行版的一部分本地提供。 如果没有,您可以随时使用软件包管理器来安装和浏览它们。 请记住:在正确的场合学习使用正确的工具需要足够的耐心和练习。

    文件

    作用:帮助确定文件类型。

    这将是您进行二进制分析的起点。 我们每天处理文件。 并非所有的东西都是可执行的类型。 那里有各种各样的文件类型。 在开始之前,您需要了解要分析的文件类型。 它是二进制文件,库文件,ASCII文本文件,视频文件,图片文件,PDF,数据文件等吗?

    file命令将帮助您确定要处理的确切文件类型。

       
    $ file /bin/ls
         
    /bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=94943a89d17e9d373b2794dcb1f7e38c95b66c86, stripped
         
    $ 
         
    $ file /etc/passwd
         
    /etc/passwd: ASCII text
         
    $ 
    

     

    dd

    它的作用:打印共享对象依赖性。

    如果您已经在可执行二进制文件上使用了上面的file命令,则不能错过输出中的“动态链接”消息。 这是什么意思?

    开发软件时,我们尽量不要重新发明轮子。 大多数软件程序需要执行一组通用任务,例如打印输出或从标准文件中读取或打开文件等。所有这些通用任务都通过一组通用函数抽象出来,每个人都可以使用它们代替编写自己的变体。 这些常用功能放在名为libcglibc的库中。

    如何找到可执行文件所依赖的库? 这就是ldd命令出现的地方。 针对动态链接的二进制文件运行它会显示其所有依赖的库及其路径。

     $ ldd /bin/ls
         
            linux-vdso.so.1 =>  (0x00007ffef5ba1000)
         
            libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fea9f854000)
         
            libcap.so.2 => /lib64/libcap.so.2 (0x00007fea9f64f000)
         
            libacl.so.1 => /lib64/libacl.so.1 (0x00007fea9f446000)
         
            libc.so.6 => /lib64/libc.so.6 (0x00007fea9f079000)
         
            libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fea9ee17000)
         
            libdl.so.2 => /lib64/libdl.so.2 (0x00007fea9ec13000)
         
            /lib64/ld-linux-x86-64.so.2 (0x00007fea9fa7b000)
         
            libattr.so.1 => /lib64/libattr.so.1 (0x00007fea9ea0e000)
         
            libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fea9e7f2000)
         
    $ 
    

     

    跟踪

    它的作用:库调用跟踪器。

    现在,我们知道如何使用ldd命令查找可执行程序所依赖的库。 但是,一个库可以包含数百个函数。 在这数百种中,二进制文件实际使用的功能是什么?

    ltrace命令显示在运行时从库中调用的所有函数。 在下面的示例中,您可以看到调用的函数名称以及传递给该函数的参数。 您还可以在输出的最右侧看到这些函数返回的内容。

    $ ltrace ls
         
    __libc_start_main(0x4028c0, 1, 0x7ffd94023b88, 0x412950 <unfinished ...>
         
    strrchr("ls", '/')                                                                  = nil
         
    setlocale(LC_ALL, "")                                                               = "en_US.UTF-8"
         
    bindtextdomain("coreutils", "/usr/share/locale")                                    = "/usr/share/locale"
         
    textdomain("coreutils")                                                             = "coreutils"
         
    __cxa_atexit(0x40a930, 0, 0, 0x736c6974756572)                                      = 0
         
    isatty(1)                                                                           = 1
         
    getenv("QUOTING_STYLE")                                                             = nil
         
    getenv("COLUMNS")                                                                   = nil
         
    ioctl(1, 21523, 0x7ffd94023a50)                                                     = 0
         
    << snip >>
         
    fflush(0x7ff7baae61c0)                                                              = 0
         
    fclose(0x7ff7baae61c0)                                                              = 0
         
    +++ exited (status 0) +++
         
    $ 
    

     

    十六进制转储

    作用:以ASCII,十进制,十六进制或八进制显示文件内容。

    通常,您使用不知道如何处理该文件的应用程序打开文件。 尝试使用vim打开可执行文件或视频文件; 您将看到的只是乱码在屏幕上。

    在Hexdump中打开未知文件可帮助您查看文件中包含的内容。 您也可以使用某些命令行选项选择查看文件中数据的ASCII表示形式。 这可能有助于您了解文件的类型。

    $ hexdump -C /bin/ls | head
         
    00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
         
    00000010  02 00 3e 00 01 00 00 00  d4 42 40 00 00 00 00 00  |..>......B@.....|
         
    00000020  40 00 00 00 00 00 00 00  f0 c3 01 00 00 00 00 00  |@...............|
         
    00000030  00 00 00 00 40 00 38 00  09 00 40 00 1f 00 1e 00  |....@.8...@.....|
         
    00000040  06 00 00 00 05 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
         
    00000050  40 00 40 00 00 00 00 00  40 00 40 00 00 00 00 00  |@.@.....@.@.....|
         
    00000060  f8 01 00 00 00 00 00 00  f8 01 00 00 00 00 00 00  |................|
         
    00000070  08 00 00 00 00 00 00 00  03 00 00 00 04 00 00 00  |................|
         
    00000080  38 02 00 00 00 00 00 00  38 02 40 00 00 00 00 00  |8.......8.@.....|
         
    00000090  38 02 40 00 00 00 00 00  1c 00 00 00 00 00 00 00  |8.@.............|
         
    $ 
    

     

    它的作用:打印文件中可打印字符的字符串。

    如果Hexdump在您的用例中看起来有点过大,而您只是在二进制文件中寻找可打印的字符,则可以使用strings命令。

    开发软件时,会向其中添加各种文本/ ASCII消息,例如打印信息消息,调试信息,帮助消息,错误等。 只要所有这些信息都存在于二进制文件中,它将使用字符串转储到屏幕上。

    $ strings /bin/ls

     

    Readelf

    它的作用:显示有关ELF文件的信息。

    ELF(可执行文件和可链接文件格式)是可执行文件或二进制文件的主要文件格式,不仅在Linux上,在各种UNIX系统上也是如此。 如果您使用过诸如file命令之类的工具,该工具会告诉您该文件为ELF格式,那么下一个逻辑步骤将是使用readelf命令及其各种选项来进一步分析该文件。

    使用readelf时,方便参考实际的ELF规范非常有用。 您可以在此处找到规格。

    $ readelf -h /bin/ls
         
    ELF Header:
         
      Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
         
      Class:                             ELF64
         
      Data:                              2's complement, little endian
         
      Version:                           1 (current)
         
      OS/ABI:                            UNIX - System V
         
      ABI Version:                       0
         
      Type:                              EXEC (Executable file)
         
      Machine:                           Advanced Micro Devices X86-64
         
      Version:                           0x1
         
      Entry point address:               0x4042d4
         
      Start of program headers:          64 (bytes into file)
         
      Start of section headers:          115696 (bytes into file)
         
      Flags:                             0x0
         
      Size of this header:               64 (bytes)
         
      Size of program headers:           56 (bytes)
         
      Number of program headers:         9
         
      Size of section headers:           64 (bytes)
         
      Number of section headers:         31
         
      Section header string table index: 30
         
    $ 
    

     

    objdump

    它的作用:显示来自目标文件的信息。

    当您编写源代码时,会创建二进制文件,而使用称为编译器的工具对源代码进行编译也就不足为奇了。 该编译器生成与源代码等效的机器语言指令,然后可由CPU执行以执行给定任务。 可以通过称为汇编语言的助记符来解释该机器语言代码。 汇编语言是一组指令,可以帮助您了解程序正在执行的操作以及最终在CPU上执行的操作。

    objdump实用程序读取二进制或可执行文件,并将汇编语言指令转储到屏幕上。 汇编知识对于理解objdump命令的输出至关重要。

    请记住:汇编语言是特定于体系结构的。

    $ objdump -d /bin/ls | head
         
     
         
    /bin/ls:     file format elf64-x86-64
         
     
         
     
         
    Disassembly of section .init:
         
     
         
    0000000000402150 <_init@@Base>:
         
      402150:       48 83 ec 08             sub    $0x8,%rsp
         
      402154:       48 8b 05 6d 8e 21 00    mov    0x218e6d(%rip),%rax        # 61afc8 <__gmon_start__>
         
      40215b:       48 85 c0                test   %rax,%rax
         
    $ 
    

     

    痕迹

    它的作用:跟踪系统调用和信号。

    如果您使用了前面提到的ltrace ,请考虑strace是相似的。 唯一的区别是, strace实用程序可以跟踪系统调用,而不是调用库。 系统调用是您与内核交互以完成工作的方式。

    举个例子,如果您想在屏幕上打印一些东西,您将使用标准库libc中printfputs函数; 但是,在幕后,最终将进行名为write的系统调用,以将某些内容实际打印到屏幕上。

    $ strace -f /bin/ls
         
    execve("/bin/ls", ["/bin/ls"], [/* 17 vars */]) = 0
         
    brk(NULL)                               = 0x686000
         
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f967956a000
         
    access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
         
    open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
         
    fstat(3, {st_mode=S_IFREG|0644, st_size=40661, ...}) = 0
         
    mmap(NULL, 40661, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9679560000
         
    close(3)                                = 0
         
    << snip >>
         
    fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
         
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9679569000
         
    write(1, "R2  RH\n", 7R2  RH
         
    )                 = 7
         
    close(1)                                = 0
         
    munmap(0x7f9679569000, 4096)            = 0
         
    close(2)                                = 0
         
    exit_group(0)                           = ?
         
    +++ exited with 0 +++
         
    $ 
    

     

    纳米

    它的作用:列出目标文件中的符号。

    如果您使用的是未剥离的二进制文件,那么nm命令将为您提供在编译过程中嵌入在二进制文件中的有价值的信息。 nm可以帮助您从二进制文件中识别变量和函数。 您可以想象如果您无法访问所分析的二进制代码的源代码,这将有多大用处。

    为了展示nm ,我们将快速编写一个小程序并使用-g选项对其进行编译,并且还将看到使用file命令不会剥离二进制文件。

    $ cat hello.c 
         
    #include <stdio.h>
         
     
         
    int main() {
         
        printf("Hello world!");
         
        return 0;
         
    }
         
    $ 
         
    $ gcc -g hello.c -o hello
         
    $ 
         
    $ file hello
         
    hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=3de46c8efb98bce4ad525d3328121568ba3d8a5d, not stripped
         
    $ 
         
    $ ./hello 
         
    Hello world!$ 
         
    $ 
         
     
         
     
         
    $ nm hello | tail
         
    0000000000600e20 d __JCR_END__
         
    0000000000600e20 d __JCR_LIST__
         
    00000000004005b0 T __libc_csu_fini
         
    0000000000400540 T __libc_csu_init
         
                     U __libc_start_main@@GLIBC_2.2.5
         
    000000000040051d T main
         
                     U printf@@GLIBC_2.2.5
         
    0000000000400490 t register_tm_clones
         
    0000000000400430 T _start
         
    0000000000601030 D __TMC_END__
         
    $ 
    

     

    gdb

    它的作用:GNU调试器。

    好吧,并不是二进制文件中的所有内容都可以静态分析。 我们确实执行了一些运行二进制文件的命令,例如ltracestrace ; 但是,软件包含多种条件,可能导致执行各种替代路径。

    分析这些路径的唯一方法是在运行时通过能够在任何给定位置停止或暂停程序,并能够分析信息然后进一步向下移动来进行分析。
    这就是调试器出现的地方,在Linux上, gdb是事实上的调试器。 它可以帮助您加载程序,在特定位置设置断点,分析内存和CPU寄存器等等。 它补充了上面提到的其他工具,使您可以进行更多的运行时分析。

    需要注意的一件事是,一旦使用gdb加载了程序,就会看到它自己的(gdb)提示符。 所有其他命令将在此gdb命令提示符下运行,直到退出。

    我们将使用我们先前编译的“ hello”程序,并使用gdb查看其工作方式。

    $ gdb -q ./hello
         
    Reading symbols from /home/flash/hello...done.
         
    (gdb) break main
         
    Breakpoint 1 at 0x400521: file hello.c, line 4.
         
    (gdb) info break
         
    Num     Type           Disp Enb Address            What
         
    1       breakpoint     keep y   0x0000000000400521 in main at hello.c:4
         
    (gdb) run
         
    Starting program: /home/flash/./hello 
         
     
         
    Breakpoint 1, main () at hello.c:4
         
    4           printf("Hello world!");
         
    Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.6.x86_64
         
    (gdb) bt
         
    #0  main () at hello.c:4
         
    (gdb) c
         
    Continuing.
         
    Hello world![Inferior 1 (process 29620) exited normally]
         
    (gdb) q
         
    $ 
    

    结论

    一旦您对使用这些本机Linux二进制分析工具感到满意,并理解了它们提供的输出,就可以使用更高级和专业的开源二进制分析工具,例如radare2 。

    翻译自: https://opensource.com/article/20/4/linux-binary-analysis

    linux二进制文件分析

    更多相关内容
  • Binary Viewer二进制文件读取软件,可读取二进制的语音数据,图像数据,从而分析音频或者图像存储的二进制信息
  • Pharos静态二进制分析框架是卡内基梅隆大学软件工程学院的一个项目。 该框架旨在促进二进制程序的自动化分析。 它使用Lawrence Livermore国家实验室开发的ROSE编译器基础结构进行反汇编,控制流分析,指令语义等。 ...
  • 在计算机安全与黑客攻防领域,CTF挑战经常以竞赛形式进行,目标是分析并利用指定的二进制文件,或者正在运行的进程/服务器,直至拿到隐藏在二进制文件中的“flag”为止。flag一般是十六进制的字符串,你可以用它来...

    在本文中,你将学习在Linux操作系统上进行分析所需的基本工具。

    除了向你简单展示工具列表,并解释它们的功能以外,我还将使用“夺旗”(Capture The Flag,CTF)挑战来说明它们的工作原理。在计算机安全与黑客攻防领域,CTF挑战经常以竞赛形式进行,目标是分析并利用指定的二进制文件,或者正在运行的进程/服务器,直至拿到隐藏在二进制文件中的“flag”为止。flag一般是十六进制的字符串,你可以用它来证明你已经完成了挑战,并解锁新挑战。

    本次CTF里面,我们从一个名为payload的神秘文件开始分析,你可以在虚拟机的本章目录中找到该文件。我们的挑战目标是,找出隐藏在payload中的flag。在分析payload、查找flag的过程中,你需要学习使用各种二进制分析工具,这些工具几乎可以在任何Linux操作系统上使用,大多数工具通常作为GNU coreutils或binutils的一部分。

    这里看到的大多数工具都有许多有用的选项,但是由于本章需要介绍的工具实在太多,因此,最好的方法是在虚拟机上使用man命令查询每个工具的手册页。在本章的最后,你需要用flag来解锁新挑战,我相信你可以独立完成该挑战!

    5.1 使用file解决类型问题

    在分析二进制文件的时候,因为没有关于payload内容的提示,所以无从下手。例如,在进行逆向工程,或者取证的时候发生这种情况,第一步就是要弄清楚文件类型及其内容。file工具应运而生,它可以接收多个文件,然后返回文件类型。

    使用file的好处是,它不受文件扩展名的影响,相反,它是通过搜索文件中的其他指示模式来识别文件类型的,如ELF二进制文件开头的0x7f序列的幻数字节。这是完美的选择,因为payload文件没有扩展名。以下是file返回的有关payload的消息。

    $ file payload
    payload: ASCII text

    如你所见,payload包含ASCII文本。为了详细检查文本,你可以使用head工具,head会将文本内容的前几行(默认是前10行)显示到stdout中。

    $ head payload
    H4sIAKiT61gAA+xaD3RTVZq/Sf9TSKL8aflnn56ioNJJSiktDpqUlL5o0UpbYEVI0zRtI2naSV5K
    YV0HTig21jqojH9mnRV35syZPWd35ZzZ00XHxWBHYJydXf4ckRldZRUxBRzxz2CFQvb77ru3ee81
    AZdZZ92z+XrS733fu993v/v/vnt/bqmVfNNkBlq0cCFyy6KFZiUHKi1buMhMLAvMi0oXWSzlZYtA
    v2hRWRkRzN94ZEChoOQKCAJp8fdcNt2V3v8fpe9X1y7T63Rjsp7cTlCKGq1UtjL9yPUJGyupIHnw
    /zoym2SDnKVIZyVWFR9hrjnPZeky4JcJvwq9LFforSo+i6XjXKfgWaoSWFX8mclExQkRxuww1uOz
    Ze3x2U0qfpDFcUyvttMzuxFmN8LSc054er26fJns18D0DaxcnNtZOrsiPVLdh1ILPudey/xda1Xx
    MpauTGN3L9hlk69PJsZXsPxS1YvA4uect8N3fN7m8rLv+Frm+7z+UM/8nory+eVlJcHOklIak4ml
    rbm7kabn9SiwmKcQuQ/g+3n/OJj/byfuqjv09uKVj8889O6TvxXM+G4qSbRbX1TQCZnWPNQVwG86
    /F7+4IkHl1a/eebY91bPemngU8OpI58YNjrWD16u3P3wuzaJ3kh4i6vpuhT6g7rkfs6k0DtS6P8l
    hf6NFPocfXL9yRTpS0ny+NtJ8vR3p0hfl8J/bgr9Vyn0b6bQkxTl+ixF+p+m0N+qx743k+wWmlT6

    上述内容看起来让人难以理解,但仔细看,你会发现它只包含字母、数字以及+、/等字符,并且整齐地按行排列。当你看到一个像这样的文件的时候,通常可以确认这是一个Base64文件。

    Base64是一种广泛使用的、将二进制数据编码为ASCII文本的方法。除了正常编码,Base64还常用于电子邮件和网页编码,以确保网络传输的二进制数据不会因为只能处理文本服务而意外变形。Linux操作系统自带了base64的小工具(通常作为GNU coreutils的一部分),这个工具可以对Base64进行编码和解码。默认情况下,base64会对提供的标准输入或者文件进行编码,但你也可以使用-d标志进行解码操作。我们现在来解码payload,看看会有什么结果。

    $ base64 -d payload > decoded_payload

    使用上述命令对payload进行解码,然后将解码的内容保存在一个名为decoded_ payload的新文件中。现在,你已经有了payload的解码内容,我们再次用file来检查解码后的文件类型。

    $ file decoded_payload
    decoded_payload: gzip compressed data, last modified: Tue Oct 22 15:46:43 2019, from Unix

    原来Base64编码后面的神秘文件,实际上是一个压缩文件,并使用gzip作为外部的压缩。这里将介绍file的另一个功能:查看压缩文件。将-z选项传递给file,查看压缩文件的内容而无须进行提取文件的操作,如下所示:

    $ file -z decoded_payload
    decoded_payload: POSIX tar archive (GNU) (gzip compressed data, last modified:
                      Tue Oct 22 19:08:12 2019, from Unix) 

    可以看到压缩文件里面还有一个压缩文件,外面用gzip压缩,里面用tar压缩(通常在里面包含文件)。为了显示存储在里面的文件,你可以使用tar解压缩提取decoded_payload里面的内容,如下所示:

    $ tar xvzf decoded_payload
    ctf
    67b8601

    如tar日志所示,从压缩文件中提取了两个文件:ctf和67b8601。我们再用file来看看这两个文件的类型。

    $ file ctf
    ctf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
    interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
    BuildID[sha1]=29aeb60bcee44b50d1db3a56911bd1de93cd2030, stripped

    第一个文件ctf,是一个动态链接的、64位的、剥离的ELF二进制文件。第二个文件67b8601,是一个512像素×512像素的位图(BitMap,BMP)文件。同样,你可以使用file查看此消息。

    $ file 67b8601
    67b8601: PC bitmap, Windows 3.x format, 512 x 512 x 24

    如图5-1(a)所示,这个BMP文件中画的是一个黑色的正方形。如果仔细看,在图片底部会发现一些不规则的颜色像素,图5-1(b)显示了这些像素的放大片段。

    在研究之前,我们先看看ctf,这个刚刚提取的ELF二进制文件。

    (a)完整的图片

    (b)底部某些颜色像素的放大图片

    《二进制分析实战》图5-1 提取的BMP文件67b8601

    5.2 使用ldd探索依赖性

    直接运行未知的二进制文件不是明智之举,但因为是在虚拟机中操作,所以直接运行ctf应该不会有什么大问题。

    $ ./ctf
    ./ctf: error while loading shared libraries: lib5ae9b7f.so:
           cannot open shared object file: No such file or directory

    在执行程序代码之前,动态链接器会提示缺少一个名为lib5ae9b7f.so的库文件。这个库文件听起来不像是在系统上可以找到的库文件,那么在搜索这个库文件之前,很有必要检查一下ctf是否有更多未解析的依赖项。

    Linux操作系统自带一个名为ldd的程序,你可以使用该程序找出文件依赖哪些共享库和依赖关系。你可以将ldd与-v选项一起使用,找出二进制文件期望的库文件版本,这对调试来说很有用。正如ldd手册页中描述的那样,ldd可能会通过运行二进制文件来找出依赖关系,所以除非你是在虚拟机或者其他隔离环境下运行,否则在不信任的二进制文件上使用ldd是不安全的。以下是ctf二进制文件的ldd输出:

    $ ldd ctf
            linux-vdso.so.1 => (0x00007fff6edd4000)
            lib5ae9b7f.so => not found
            libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f67c2cbe000)
            libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f67c2aa7000)
            libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f67c26de000)
            libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f67c23d5000)
            /lib64/ld-linux-x86-64.so.2 (0x0000561e62fe5000)

    幸运的是,除了刚刚发现的lib5ae9b7f.so库文件外,并不存在其他未解析的依赖项。现在我们可以集中精力来弄清楚这个神秘的库文件到底是什么了,以及通过它来拿到flag。

    因为从库的名称可以明显知道,如果它不在任何标准的存储库中,它肯定就在附近某个位置。回想第2章的内容,所有的ELF二进制文件和库文件都以幻数序列0x7f开头,通过这个字符串查找缺少的库文件相当方便。只要库文件未被加密,你就可以通过这种方式找到ELF头部。我们可以用grep简单地搜索字符串“ELF”,如下所示:

    $ grep 'ELF' *
    Binary file 67b8601 matches
    Binary file ctf matches

    正如我们的预期,字符串“ELF”出现在ctf中。这并不奇怪,因为你已经知道它就是一个ELF二进制文件。奇怪的是,你也可以在67b8601文件中发现该字符串,虽然乍一看该文件是一个无害的BMP文件,但共享库是否可以隐藏在BMP文件的像素数据中呢?这个问题的答案肯定可以向你解释为什么会看到图5-1(b)中所示的那些颜色奇怪的像素。我们现在来检查67b8601文件的内容,找出答案。



     

    快速查找ASCII值

    在将原始字节解释为ASCII码的时候,通常需要ASCII表,将字节值映射为ASCII符号。你可以使用名为man ascii的手册页来快速浏览该表,以下是该表的摘要:

    Oct     Dec  Hex    Char                       Oct  Dec  Hex   Char
    000     0    00     NUL '\0' (null character)  100  64   40    @
    001     1    01     SOH (start of heading)     101  65   41    A
    002     2    02     STX (start of text)        102  66   42    B
    003     3    03     ETX (end of text)          103  67   43    C
    004     4    04     EOT (end of transmission)  104  68   44    D
    005     5    05     ENQ (enquiry)              105  69   45    E
    006     6    06     ACK (acknowledge)          106  70   46    F
    007     7    07     BEL '\a' (bell)            107  71   47    G
    ...

    从上面我们可以看到,这个表可以轻松地查找映射关系,把八进制、十进制和十六进制编码到ASCII字符,比在浏览器里搜索ASCII值要快得多。


    5.3 使用xxd查看文件内容

    为了在不依赖任何标准的前提下,发现文件的内容,这里我们必须进行字节级别的分析。为此,我们需要在系统屏幕上显示位和字节内容。你可以使用二进制,显示出所有的1和0用于分析,但是因为这样做需要大量无用的运算,所以最好使用十六进制。在十六进制中,数字的范围是0~9,然后是a~f,其中a代表10,f代表15。另外,因为一字节有256种可能(28),所以正好适合表示两个十六进制值(16×16),十六进制编码可以简洁、方便地显示字节内容。

    这里我们使用十六进制转储程序显示二进制文件的字节内容,该程序可以编辑文件中的字节内容。在第7章中,我会再次谈到十六进制编辑的内容,但现在,我们使用一款简单的十六进制转储工具xxd,这款工具默认安装在绝大多数Linux操作系统上。

    以下内容是用xxd分析BMP文件的前15行得到的输出:

    $ xxd 67b8601 | head -n 15
    00000000: 424d 3800 0c00 0000 0000 3600 0000 2800 BM8.......6...(.
    00000010: 0000 0002 0000 0002 0000 0100 1800 0000 ................
    00000020: 0000 0200 0c00 c01e 0000 c01e 0000 0000 ................
    00000030: 0000 0000 ❶7f45 4c46 0201 0100 0000 0000 .....ELF........
    00000040: 0000 0000 0300 3e00 0100 0000 7009 0000 ......>.....p...
    00000050: 0000 0000 4000 0000 0000 0000 7821 0000 ....@.......x!..
    00000060: 0000 0000 0000 0000 4000 3800 0700 4000 ........@.8...@.
    00000070: 1b00 1a00 0100 0000 0500 0000 0000 0000 ................
    00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
    00000090: 0000 0000 f40e 0000 0000 0000 f40e 0000 ................
    000000a0: 0000 0000 0000 2000 0000 0000 0100 0000 ...... .........
    000000b0: 0600 0000 f01d 0000 0000 0000 f01d 2000 .............. .
    000000c0: 0000 0000 f01d 2000 0000 0000 6802 0000 ...... .....h...
    000000d0: 0000 0000 7002 0000 0000 0000 0000 2000 ....p......... .
    000000e0: 0000 0000 0200 0000 0600 0000 081e 0000 ................

    第一列输出的是以十六进制格式显示的文件偏移,接下来的8列显示的是文件中字节以十六进制表示的形式,在输出的最右侧,你可以看到相同字节对应的ASCII表示形式。

    我们可以使用xxd工具的-c选项修改每行显示的字节数,如xxd-c 32会将每行显示为32字节。你还可以使用-b选项显示二进制文件而不是十六进制文件,并使用-i选项输出包含字节的C风格数组。你可以直接将其包含在C或者C++源代码中。为了只输出某些字节,我们可以使用-s(搜索)选项指定起始的文件偏移量,使用-l(长度)选项指定要转储的字节数。

    在BMP文件的xxd输出中,ELF幻数字节出现在偏移量0x34处❶,对应十进制的52。虽然输出告诉你了可疑的ELF库文件的起始位置,但要找出ELF库文件的结尾并不容易。因为没有幻数字节来界定ELF库文件的结尾,所以在提取整个ELF库文件之前,先把ELF头部提取出来,再通过检查ELF头部来找出整个ELF库文件的大小。

    为了提取ELF头部,需要使用dd将BMP文件从偏移52开始,复制64字节到新的输出文件elf_header。

    $ dd skip=52 count=64 if=67b8601 of=elf_header bs=1
    64+0 records in
    64+0 records out
    64 bytes copied, 0.000404841 s, 158 kB/s

    使用dd是一个意外,这里不进行过多解释。但dd确实是一款功能强大[1]的工具,所以如果你还不熟悉怎么使用它,可以阅读dd的官方手册页。

    再用xxd查看elf_header的内容。

    $ xxd elf_header
    00000000: ❶7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
    00000010: 0300 3e00 0100 0000 7009 0000 0000 0000 ..>.....p.......
    00000020: 4000 0000 0000 0000 7821 0000 0000 0000 @.......x!......
    00000030: 0000 0000 4000 3800 0700 4000 1b00 1a00 ....@.8...@.....

    这看起来很像是ELF头部:你可以在起始位置清楚地看到幻数字节,并且还可以看到e_ident数组,其他字段看起来也很合理(有关这些字段的说明,请参阅第2章)。

    5.4 使用readelf解析并提取ELF库文件

    为了查看ELF头部(elf_header)的详细信息,最好使用第2章介绍的readelf,因为readelf可以在损坏的ELF库文件中正常工作,如清单5-1所示。

    清单5-1 使用readelf读取elf_header详细信息

    ❶ $ readelf -h elf_header
      ELF Header: 
        Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
        Class:                             ELF64
        Data:                              2's complement, little endian
        Version:                           1 (current)
        OS/ABI:                            UNIX - System V
        ABI Version:                       0
        Type:                              DYN (Shared object file)
        Machine:                           Advanced Micro Devices X86-64
        Version:                           0x1
        Entry point address:               0x970
        Start of program headers:          64 (bytes into file)
    ❷   Start of section headers:          8568 (bytes into file)
        Flags:                             0x0
        Size of this header:               64 (bytes)
        Size of program headers:           56 (bytes)
        Number of program headers:         7
    ❸   Size of section headers:           64 (bytes)
    ❹   Number of section headers:         27
        Section header string table index: 26
      readelf: Error: Reading 0x6c0 bytes extends past end of file for section headers
      readelf: Error: Reading 0x188 bytes extends past end of file for program headers

    -h选项❶告诉readelf只输出ELF头部,它仍然会告诉你节头表和程序头表指向文件外部,但没关系,重要的是现在你可以方便地表示elf_header。

    现在要如何通过elf_header来确定整个ELF库文件的大小?在第2章的图2-1中,你知道ELF二进制文件的最后一部分是节头表,而节头表的偏移在elf_header中指定了❷,elf_header头还告诉我们表中每个节头的大小❸,以及节头的数量❹。这意味着你可以通过以下公式计算隐藏在BMP文件中的完整ELF库文件的大小。

    在这个公式中,size是整个库文件的大小,e_shoff是节头表的偏移,e_shnum是表中节头的数量,e_shentsize是每个节头的大小。

    现在知道了库文件的大小应该是10 296字节,就可以使用dd完整地提取库文件了,如下所示。

    $ dd skip=52 count=10296 if=67b8601 ❶of=lib5ae9b7f.so bs=1
    10296+0 records in
    10296+0 records out
    10296 bytes (10 kB, 10 KiB) copied, 0.0287996 s, 358 kB/s

    由于lib5ae9b7f.so文件是ctf二进制文件缺少的库文件,dd会调用提取该文件。运行上述命令后,你将拥有一个功能齐全的ELF共享库。我们使用readelf查看该文件是否正常,如清单5-2所示。为了简化输出结果,我们只输出ELF头部(-h)和符号表(-s),后者让你对库文件提供的功能有所了解。

    清单5-2 使用readelf读取lib5ae9b7f.so库文件的输出

       $ readelf -hs lib5ae9b7f.so
       ELF Header: 
         Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
         Class:                             ELF64
         Data:                              2's complement, little endian
         Version:                           1 (current)
         OS/ABI:                            UNIX - System V
         ABI Version:                       0
         Type:                              DYN (Shared object file)
         Machine:                           Advanced Micro Devices X86-64
         Version:                           0x1
         Entry point address:               0x970
         Start of program headers:          64 (bytes into file)
         Start of section headers:          8568 (bytes into file)
         Flags:                             0x0
         Size of this header:               64 (bytes)
         Size of program headers:           56 (bytes)
         Number of program headers:         7
         Size of section headers:           64 (bytes)
         Number of section headers:         27
         Section header string table index: 26
    
       Symbol table '.dynsym' contains 22 entries:
         Num:    Value           Size Type  Bind   Vis     Ndx Name
           0: 0000000000000000   0 NOTYPE   LOCAL  DEFAULT UND
           1: 00000000000008c0   0 SECTION  LOCAL  DEFAULT   9
           2: 0000000000000000   0 NOTYPE   WEAK   DEFAULT UND __gmon_start__
           3: 0000000000000000   0 NOTYPE   WEAK   DEFAULT UND _Jv_RegisterClasses
           4: 0000000000000000   0 FUNC     GLOBAL DEFAULT UND _ZNSt7__cxx1112basic_stri@GL(2)
           5: 0000000000000000   0 FUNC     GLOBAL DEFAULT UND malloc@GLIBC_2.2.5 (3)
           6: 0000000000000000   0 NOTYPE   WEAK   DEFAULT UND _ITM_deregisterTMCloneTab
           7: 0000000000000000   0 NOTYPE   WEAK   DEFAULT UND _ITM_registerTMCloneTable
           8: 0000000000000000   0 FUNC     WEAK   DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3)
           9: 0000000000000000   0 FUNC     GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (4)
          10: 0000000000000000   0 FUNC     GLOBAL DEFAULT UND _ZSt19__throw_logic_error@ (5)
          11: 0000000000000000   0 FUNC     GLOBAL DEFAULT UND memcpy@GLIBC_2.14 (6)
    ❶     12: 0000000000000bc0 149 FUNC     GLOBAL DEFAULT  12 _Z11rc4_encryptP11rc4_sta
    ❷     13: 0000000000000cb0 112 FUNC     GLOBAL DEFAULT  12 _Z8rc4_initP11rc4_state_t
          14: 0000000000202060   0 NOTYPE   GLOBAL DEFAULT  24 _end
          15: 0000000000202058   0 NOTYPE   GLOBAL DEFAULT  23 _edata
    ❸     16: 0000000000000b40 119 FUNC     GLOBAL DEFAULT  12 _Z11rc4_encryptP11rc4_sta
    ❹     17: 0000000000000c60   5 FUNC     GLOBAL DEFAULT  12 _Z11rc4_decryptP11rc4_sta
          18: 0000000000202058   0 NOTYPE   GLOBAL DEFAULT  24 __bss_start
          19: 00000000000008c0   0 FUNC     GLOBAL DEFAULT   9 _init
    ❺     20: 0000000000000c70  59 FUNC     GLOBAL DEFAULT  12 _Z11rc4_decryptP11rc4_sta
          21: 0000000000000d20   0 FUNC     GLOBAL DEFAULT  13 _fini

    正如我们所期望的那样,整个库文件被完整地提取出来了,尽管符号被剥离,但动态符号表确实显示了一些有趣的导出函数(从❶到❺)。然而,有些名称看起来“乱七八糟”,难以阅读,让我们看看能否对其进行修复。

    5.5 使用nm解析符号

    C++运行函数重载,意味着可以存在多个具有相同名称的函数,只要它们有不同的签名即可。不幸的是,对链接程序来说,它对C++一无所知。如果有多个名称为foo的函数,链接器将不知道如何解析对foo的引用,也不知道要使用哪个版本的foo。为了消除重复的名称,C++编译器提出了符号修饰(mangled name)。符号修饰实质上是原始函数名称与函数参数编码的组合。这样,函数的每个版本都会获得唯一的名称,并且链接器也不会对重载的函数产生歧义。

    对二进制分析员来说,符号修饰带来的是一种喜忧参半的感觉。一方面,正如在readelf对lib5ae9b7f.so的读取输出(见清单5-2)中看到的那样,这些符号修饰很难阅读;另一方面,符号修饰实质上是通过泄露函数的预期参数来提供自由的类型信息的,该信息在对二进制文件进行逆向工程时会很有用。

    幸运的是,符号修饰的优点多于自身的缺点,因为符号修饰相对容易还原,我们可以使用几款工具来解析修饰过的名称。nm是最出名的工具之一,它可以列出二进制文件、对象文件或者共享库中的符号,在指定二进制文件的时候,nm默认会尝试解析静态符号表。

    $ nm lib5ae9b7f.so
    nm: lib5ae9b7f.so: no symbols

    但遗憾的是,如本例所示,你没有办法在lib5ae9b7f.so上使用nm的默认配置,因为文件已经被剥离了。此时你需要使用-D选项要求nm解析动态符号表,如清单5-3 所示。在清单中“…”表示已经截断了一行,并在下一行继续(符号修饰可能很长)。

    清单5-3 nm对lib5ae9b7f.so的输出

    $ nm -D lib5ae9b7f.so
                     w _ITM_deregisterTMCloneTable 
                     w _ITM_registerTMCloneTable
                     w _Jv_RegisterClasses
    0000000000000c60 T _Z11rc4_decryptP11rc4_state_tPhi
    0000000000000c70 T _Z11rc4_decryptP11rc4_state_tRNSt7__cxx1112basic_...
                       ...stringIcSt11char_traitsIcESaIcEEE
    0000000000000b40 T _Z11rc4_encryptP11rc4_state_tPhi
    0000000000000bc0 T _Z11rc4_encryptP11rc4_state_tRNSt7__cxx1112basic_...
                       ...stringIcSt11char_traitsIcESaIcEEE
    0000000000000cb0 T _Z8rc4_initP11rc4_state_tPhi
                     U _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE9_...
                       ...M_createERmm
                     U _ZSt19__throw_logic_errorPKc
    0000000000202058 B __bss_start
                     w __cxa_finalize
                     w __gmon_start__
                     U __stack_chk_fail
    0000000000202058 D _edata
    0000000000202060 B _end
    0000000000000d20 T _fini
    00000000000008c0 T _init
                     U malloc
                     U memcpy

    这次看起来好一点,能看到一些符号,但是符号名称仍然被修饰了。为了对其进行解析,我们要将--demangle选项传递给nm,如清单5-4所示。

    清单5-4 使用nm对lib5ae9b7f.so进行符号解析

    $ nm -D --demangle lib5ae9b7f.so
                     w _ITM_deregisterTMCloneTable
                     w _ITM_registerTMCloneTable
                     w _Jv_RegisterClasses
    0000000000000c60 T ❶ rc4_decrypt(rc4_state_t * , unsigned char * , int)
    0000000000000c70 T ❷ rc4_decrypt(rc4_state_t * ,
                                    std::__cxx11::basic_string<char, std::char_traits<char>,
                                    std::allocator<char> >&)
    0000000000000b40 T ❸ rc4_encrypt(rc4_state_t * , unsigned char * , int)
    0000000000000bc0 T ❹ rc4_encrypt(rc4_state_t * ,
                                    std::__cxx11::basic_string<char, std::char_traits<char>,
                                    std::allocator<char> >&)
    0000000000000cb0 T ❺ rc4_init(rc4_state_t * , unsigned char * , int)
                     U std::__cxx11::basic_string<char, std::char_traits<char>,
                           std::allocator<char> >::_M_create(unsigned long&, unsigned long)
                     U std::__throw_logic_error(char const * )
    0000000000202058 B __bss_start
                     w __cxa_finalize
                     w __gmon_start__
                     U __stack_chk_fail
    0000000000202058 D _edata
    0000000000202060 B _end
    0000000000000d20 T _fini
    00000000000008c0 T _init
                     U malloc
                     U memcpy

    终于,函数名称变得易于阅读了。在清单5-4中你会看到5个有意思的函数,这些函数似乎是实现已知的RC4加密算法[2]的加密函数。这里有一个名为rc4_init的函数,该函数将rc4_state_t类型的数据结构、无符号字符串以及整数❺作为输入参数。第一个参数大概是保存密码状态的数据结构,后面两个参数可能分别表示密钥的字符串,以及指定密钥长度的整数。上面还有几个加密和解密函数,每个函数都有一个指向加密状态的指针,以及指定用于加密或解密(❶到❹)的字符串参数(C和C++字符串)。

    符号修饰名称还有一种方法,即使用c++filt工具。这个工具会将修饰的名称作为输入,然后输出解析后的名称。c++filt的优点是它支持多种修饰格式,可以自动检测并修正指定输入的修饰格式。以下是c++filt解析函数名称
    _Z8rc4_initP11rc4_state_tPhi的示例:

    $ c++filt _Z8rc4_initP11rc4_state_tPhi
    rc4_init(rc4_state_t * , unsigned char * , int)

    现在我们来回顾一下到目前为止的进展:我们提取了神秘的payload,找到了一个名为ctf的二进制文件,该文件依赖lib5ae9b7f.so的库文件,然后你发现lib5ae9b7f.so隐藏在BMP文件里面,并且成功将其提取出来。与此同时,你还对该文件的功能有了大概的了解:这是一个加密库文件。现在我们再次运行ctf,这次没有提示丢失依赖项。

    $ export LD_LIBRARY_PATH=`pwd`
    $ ./ctf
    $ echo $?
    1

    运行成功!虽然运行后没有报错,但似乎没有提示任何功能,$?变量中包含的ctf退出状态为1,表示有错误。现在有了依赖文件,你可以继续研究,看看能否跳过错误提取到flag。

    5.6 使用strings查看Hints

    为了弄清楚二进制文件的功能,以及程序期望的输入类型,我们可以检查二进制文件是否包含有用的字符串,进而通过字符串揭露其用途。例如,当你看到字符串包含HTTP请求或者URL的时候,你会猜测该二进制文件正在执行与网络相关的操作;当你分析“僵尸网络”等恶意软件的时候,如果没有代码混淆,你将有可能找出包含后门接收命令的字符串,甚至会发现程序员在调试时留下的、忘记删除的字符串——这的确在现实的恶意软件中出现过。

    我们可以使用strings来查看二进制文件中的字符串,包括Linux操作系统上的任何其他文件。strings将单个或者多个文件作为输入参数,然后输出这些文件中找到的所有可输出字符串。要注意的是,strings不会检查找到的字符串是否真的是人类可读的,因此应用到二进制文件上的时候,由于某些二进制序列恰好可输出,导致输出的时候包含了一些假的字符串。

    当然我们可以使用选项来调整输出字符串的行为,如strings与-d选项一起使用,只输出在二进制文件的数据节中发现的字符串。默认情况下,strings只输出4个或者4个字符以上的字符串,但是你可以使用-n选项指定最小字符串长度。目前来说,我们用默认选项就可以了。先来看看使用strings在ctf二进制文件中可以找到什么,如清单5-5所示。

    清单5-5 找到ctf二进制文件中的字符串

      $ strings ctf
    ❶ /lib64/ld-linux-x86-64.so.2
      lib5ae9b7f.so 
    ❷ __gmon_start__
      _Jv_RegisterClasses
      _ITM_deregisterTMCloneTable
      _ITM_registerTMCloneTable
      _Z8rc4_initP11rc4_state_tPhi
      ...
    ❸ DEBUG: argv[1] = %s
    ❹ checking '%s'
    ❺ show_me_the_flag
      >CMb
      -v@Pˆ:
      flag = %s
      guess again!
    ❻ It's kinda like Louisiana. Or Dagobah. Dagobah - Where Yoda lives! 
      ; * 3$"
      zPLR
      GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
    ❼ .shstrtab
      .interp
      .note.ABI-tag
      .note.gnu.build-id
      .gnu.hash
      .dynsym
      .dynstr
      .gnu.version
      .gnu.version_r
      .rela.dyn
      .rela.plt
      .init
      .plt.got
      .text
      .fini
      .rodata
      .eh_frame_hdr
      .eh_frame
      .gcc_except_table
      .init_array
      .fini_array
      .jcr
      .dynamic
      .got.plt
      .data
      .bss
      .comment

    在清单 5-5 中你可以看到在大多数 ELF 二进制文件中都会遇到的字符串,如在.interp节中找到了程序解释器的名称❶,在.dynstr节❷找到一些符号名称。在strings输出的末尾,你可以在.shstrtab节❼找到所有节的名称,但这些字符串对我们的分析来说似乎没多大用处。

    幸运的是,上面还有一些更有用的字符串。如有一条调试消息,表明该程序需要提供命令行选项❸。还有一些格式化检查,大概是在输入字符串❹上执行的。虽然你暂时还不知道命令行参数应该是什么,但可以试着使用一些看起来有用的字符串,如show_me_the_flag❺,以及“神秘”字符串❻,里面包含一条目的不详的消息。虽然到目前为止还不知道消息的意思,但是通过对lib5ae9b7f.so的调查,我们知道该二进制文件用到了RC4加密操作,也许该消息是加密的密钥。

    既然已经知道二进制文件需要命令行参数,那么我们试着添加任意参数,看看能否让你找到flag。简单起见,这里我们采用字符串foobar,如下所示。

    $ ./ctf foobar
    checking 'foobar'
    $ echo $?
    1

    二进制文件在做一些新的事情,它告诉你正在检查你输入的字符串,但检查失败,因为二进制文件在检查后仍然会退出并显示错误代码。我们来赌一把,输入发现的其他字符串,如show_me_the_flag。

    $ ./ctf show_me_the_flag
    checking 'show_me_the_flag'
    ok
    $ echo $?
    1

    提示检查成功,但是退出状态仍然为1,所以这里肯定缺少了某些东西。更糟糕的是,字符串结果不再提供任何提示了。现在我们只能从ctf的系统调用、库文件调用开始,更加详细地研究ctf的行为,进一步确定下一步应该要做什么。

    5.7 使用strace和ltrace跟踪系统调用和库文件调用

    为了取得进展,我们通过查看ctf退出前的行为来调查exit显示错误代码的原因。这里有多种方法,其中一种方法是使用名为strace和ltrace的两个工具。这两个工具分别显示二进制文件执行时的系统调用和库文件调用。知道了二进制文件有哪些系统调用和库文件调用以后,会让你对程序有更深入的了解。

    首先我们使用strace跟踪ctf的系统调用行为。在某些情况下,你可能希望将strace附加到正在运行的进程中,为此你需要使用-p pid选项,其中pid是你要附加的进程ID。但是在这个示例里面,用strace运行ctf就足够了。清单5-6显示了ctf二进制文件的strace输出,某些内容被“…”截断。

    清单5-6 ctf二进制文件的strace输出

    $ strace ./ctf show_me_the_flag
    ❶ execve("./ctf", ["./ctf", "show_me_the_flag"], [/* 73 vars */]) = 0
      brk(NULL)                                 = 0x1053000 
      access("/etc/ld.so.nohwcap", F_OK)        = -1 ENOENT (No such file or directory)
      mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f703477e000
      access("/etc/ld.so.preload", R_OK)        = -1 ENOENT (No such file or directory)
    ❷ open("/ch3/tls/x86_64/lib5ae9b7f.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or ...)
      stat("/ch3/tls/x86_64", 0x7ffcc6987ab0) = -1 ENOENT (No such file or directory)
      open("/ch3/tls/lib5ae9b7f.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
      stat("/ch3/tls", 0x7ffcc6987ab0) = -1 ENOENT (No such file or directory)
      open("/ch3/x86_64/lib5ae9b7f.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
      stat("/ch3/x86_64", 0x7ffcc6987ab0) = -1 ENOENT (No such file or directory)
      open("/ch3/lib5ae9b7f.so", O_RDONLY|O_CLOEXEC) = 3
    ❸ read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\t\0\0\0\0\0\0"..., 832) = 832
      fstat(3, st_mode=S_IFREG|0775, st_size=10296, ...) = 0
      mmap(NULL, 2105440, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7034358000
      mprotect(0x7f7034359000, 2097152, PROT_NONE) = 0
      mmap(0x7f7034559000, 8192, PROT_READ|PROT_WRITE, ..., 3, 0x1000) = 0x7f7034559000
      close(3)                                 = 0
      open("/ch3/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
      open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
      fstat(3, st_mode=S_IFREG|0644, st_size=150611, ...) = 0
      mmap(NULL, 150611, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7034759000
      close(3)                                 = 0
      access("/etc/ld.so.nohwcap", F_OK)       = -1 ENOENT (No such file or directory)
    ❹ open("/usr/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 3
      read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \235\10\0\0\0\0\0"..., 832) = 832
      fstat(3, st_mode=S_IFREG|0644, st_size=1566440, ...) = 0
      mmap(NULL, 3675136, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7033fd6000
      mprotect(0x7f7034148000, 2097152, PROT_NONE) = 0
      mmap(0x7f7034348000, 49152, PROT_READ|PROT_WRITE, ..., 3, 0x172000) = 0x7f7034348000
      mmap(0x7f7034354000, 13312, PROT_READ|PROT_WRITE, ..., -1, 0) = 0x7f7034354000
      close(3)                                 = 0
      open("/ch3/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
      access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
      open("/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
      read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p * \0\0\0\0\0\0"..., 832) = 832
      fstat(3, st_mode=S_IFREG|0644, st_size=89696, ...) = 0
      mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7034758000
      mmap(NULL, 2185488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7033dc0000
      mprotect(0x7f7033dd6000, 2093056, PROT_NONE) = 0
      mmap(0x7f7033fd5000, 4096, PROT_READ|PROT_WRITE, ..., 3, 0x15000) = 0x7f7033fd5000
      close(3)                                 = 0
      open("/ch3/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
      access("/etc/ld.so.nohwcap", F_OK)         = -1 ENOENT (No such file or directory)
      open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
      read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
      fstat(3, st_mode=S_IFREG|0755, st_size=1864888, ...) = 0
      mmap(NULL, 3967392, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f70339f7000
      mprotect(0x7f7033bb6000, 2097152, PROT_NONE) = 0
      mmap(0x7f7033db6000, 24576, PROT_READ|PROT_WRITE, ..., 3, 0x1bf000) = 0x7f7033db6000
      mmap(0x7f7033dbc000, 14752, PROT_READ|PROT_WRITE, ..., -1, 0) = 0x7f7033dbc000
      close(3)                                 = 0
      open("/ch3/libm.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
      access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
      open("/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
      read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0V\0\0\0\0\0\0"..., 832) = 832
      fstat(3, st_mode=S_IFREG|0644, st_size=1088952, ...) = 0
      mmap(NULL, 3178744, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f70336ee000
      mprotect(0x7f70337f6000, 2093056, PROT_NONE) = 0
      mmap(0x7f70339f5000, 8192, PROT_READ|PROT_WRITE, ..., 3, 0x107000) = 0x7f70339f5000
      close(3)                                 = 0
      mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7034757000
      mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7034756000
      mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7034754000
      arch_prctl(ARCH_SET_FS, 0x7f7034754740) = 0
      mprotect(0x7f7033db6000, 16384, PROT_READ) = 0
      mprotect(0x7f70339f5000, 4096, PROT_READ) = 0
      mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7034753000
      mprotect(0x7f7034348000, 40960, PROT_READ) = 0
      mprotect(0x7f7034559000, 4096, PROT_READ) = 0
      mprotect(0x601000, 4096, PROT_READ)       = 0
      mprotect(0x7f7034780000, 4096, PROT_READ) = 0
      munmap(0x7f7034759000, 150611)            = 0
      brk(NULL)                                 = 0x1053000
      brk(0x1085000)                            = 0x1085000
      fstat(1, st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...) = 0
    ❺ write(1, "checking 'show_me_the_flag'\n", 28checking 'show_me_the_flag'
      ) = 28
    ❻ write(1, "ok\n", 3ok
      ) = 3
    ❼ exit_group(1) = ?
      +++ exited with 1 +++

    strace会从头开始跟踪程序,包括程序解释器用来创建进程的所有系统调用,使得这里的输出相当长。输出的第一个系统调用是execve,它是由Shell为了启动程序而调用的❶。然后程序解释器开始接管并设置执行环境,这里涉及使用mprotect创建内存区域,并且设置正确的内存访问权限。另外你还可以看到用来查找和加载所需动态链接库的系统调用。

    回顾5.5节,我们通过设置LD_LIBRARY_PATH环境变量来告诉动态链接器将当前工作目录添加到搜索路径中,这就是你会看到动态链接器在当前目录的多个子文件夹中搜索lib5aw9b7f.so库文件,直到最终在工作目录的根目录找到该库文件的原因❷。找到库文件以后,动态链接器读取该库文件并将其映射到内存中❸。这里还会重复设置一些其他的库文件(如libstdc++.so.6)❹,该过程占strace输出的绝大部分。

    直到最后3个系统调用,你才能看到应用程序的特定行为。ctf的第一个系统调用是write,用于输出checking‘show_me_the_flag’到屏幕❺。然后是一个write调用,用于输出字符串ok❻。最后的一个调用是exit_group,该退出导致状态码1的错误❼。

    这些信息很有趣,但是它们能够帮助我们找到 flag 吗?不能。这个示例中,strace没有显示任何有用的信息,但还是有必要向你展示strace的工作原理,因为它对理解程序的行为很有帮助。观察程序执行的系统调用不仅对二进制分析有用,而且对调试也很有帮助。

    查看ctf的系统调用行为没有太多帮助,所以我们将目光转向库文件调用。为了查看ctf执行的库文件调用,我们要用到ltrace。因为ltrace是strace的近亲,所以需要用到许多相同的命令行参数,包括将-p附加到现有进程。这里我们使用-I选项在每次调用库文件的时候输出指令指针(后面会用到),使用-C自动取消C++函数名称的修饰。如清单5-7所示,用ltrace运行ctf。

    清单5-7 ctf二进制文件进行的库文件调用

      $ ltrace -i -C ./ctf show_me_the_flag
    ❶ [0x400fe9] __libc_start_main (0x400bc0, 2, 0x7ffc22f441e8, 0x4010c0 <unfinished ...>
    ❷ [0x400c44] __printf_chk (1, 0x401158, 0x7ffc22f4447f, 160checking 'show_me_the_flag') = 28 
    ❸ [0x400c51] strcmp ("show_me_the_flag", "show_me_the_flag") = 0
    ❹ [0x400cf0] puts ("ok"ok) = 3
    ❺ [0x400d07] rc4_init (rc4_state_t * , unsigned char * , int)
                  (0x7ffc22f43fb0, 0x4011c0, 66, 0x7fe979b0d6e0) = 0 
    ❻ [0x400d14] std::__cxx11::basic_string<char, std::char_traits<char>,
                  std::allocator<char> >:: assign (char const * )
                   (0x7ffc22f43ef0, 0x40117b, 58, 3) = 0x7ffc22f43ef0
    ❼ [0x400d29] rc4_decrypt (rc4_state_t * , std::__cxx11::basic_string<char,
                  std::char_traits<char>, std::allocator<char> >&)
                   (0x7ffc22f43f50, 0x7ffc22f43fb0, 0x7ffc22f43ef0, 0x7e889f91) = 0x7ffc22f43f50
    ❽ [0x400d36] std::__cxx11::basic_string<char, std::char_traits<char>,
                  std::allocator<char> >:: _M_assign (std::__cxx11::basic_string<char,
                  std::char_traits<char>, std::allocator<char> > const&)
                   (0x7ffc22f43ef0, 0x7ffc22f43f50, 0x7ffc22f43f60, 0) = 0
    ❾ [0x400d53] getenv ("GUESSME") = nil
      [0xffffffffffffffff] +++ exited (status 1) +++

    如清单5-7所示,ltrace的输出比strace的输出更具可读性,因为它不会被进程设置的代码所“污染”。第一个库文件调用是_libc_start_main❶,该函数从_start函数开始将控制权转移到程序的main函数,一旦main启动,第一个库文件调用会把字符串“checking…”❷输出到屏幕,实际的检查过程是使用strcmp进行字符串比较,验证ctf的参数是否为show_me_the_flag❸,如果是就把ok输出到屏幕❹。

    以上主要是你之前见过的行为,另外还有一些新的操作:通过调用rc4_init初始化RC4加密,该函数位于你之前提取的库文件中❺;接着为一个C++字符串赋值,大概是用加密消息对其进行初始化❻;然后调用rc4_decrypt❼解密此消息,并将解密后的消息分配到新的C++字符串❽。

    最后调用getenv❾,该函数是用于查找环境变量的标准库函数。你可以看到ctf需要一个名为GUESSME的环境变量,该名称很可能就是之前解密的字符串。这里我们试着将GUESSME环境变量设置为虚拟值,看看ctf的行为是否发生变化,如下所示。

    $ GUESSME='foobar' ./ctf show_me_the_flag
    checking 'show_me_the_flag'
    ok
    guess again!

    设置GUESSME环境变量会导致输出新的一行,提示你再猜一次。看来ctf希望将GUESSME设置为另一个特定值,如清单5-8所示,也许ltrace的再次运行会揭露该期望值是多少。

    清单5-8 设置GUESSME环境变量后,通过ctf二进制文件进行库文件调用

      $ GUESSME='foobar' ltrace -i -C ./ctf show_me_the_flag
      ...
      [0x400d53] getenv ("GUESSME") = "foobar"
    ❶ [0x400d6e] std::__cxx11::basic_string<char, std::char_traits<char>,
                   std::allocator<char> >:: assign (char const * )
                   (0x7fffc7af2b00, 0x401183, 5, 3) = 0x7fffc7af2b00
    ❷ [0x400d88] rc4_decrypt (rc4_state_t * , std::__cxx11::basic_string<char,
                   std::char_traits<char>, std::allocator<char> >&)
                   (0x7fffc7af2b60, 0x7fffc7af2ba0, 0x7fffc7af2b00, 0x401183) = 0x7fffc7af2b60
      [0x400d9a] std::__cxx11::basic_string<char, std::char_traits<char>,
                   std::allocator<char> >:: _M_assign (std::__cxx11::basic_string<char,
                   std::char_traits<char>, std::allocator<char> > const&)
                   (0x7fffc7af2b00, 0x7fffc7af2b60, 0x7700a0, 0) = 0
      [0x400db4] operator delete (void * )(0x7700a0, 0x7700a0, 21, 0) = 0
    ❸ [0x400dd7] puts ("guess again!"guess again!) = 13
      [0x400c8d] operator delete (void * )(0x770050, 0x76fc20, 0x7f70f99b3780, 0x7f70f96e46e0) = 0
      [0xffffffffffffffff] +++ exited (status 1) +++

    调用getenv后,ctf继续分配❶并解密❷另一个C++字符串。遗憾的是,从解密操作到guess again输出到屏幕❸的那一瞬间,看不到有关GUESSME的任何提示,这说明对GUESSME的输入值与期望值的比较操作不需要任何库函数,我们需要使用另一种方法。

    5.8 使用objdump检查指令集行为

    由于我们知道GUESSME环境变量是在不使用任何已知库函数的情况下进行比较,那么下一步最合理的就是使用objdump,从指令级别上检查ctf到底发生了什么。[3]

    从清单5-8的ltrace输出我们知道,guess again字符串是通过地址0x400dd7上的puts调用输出到屏幕上的,围绕该地址进行objdump调查,有助于了解字符串的地址以找出加载该字符串的第一条指令。为了找到该地址,我们可以使用objdump -s查看ctf文件的.rodata节,并输出完整的节内容,如清单5-9所示。

    清单5-9 使用objdump –s查看ctf文件的.rodata节

    $ objdump -s --section .rodata ctf
    
    ctf: file format elf64-x86-64
    
    Contents of section .rodata:
     401140 01000200 44454255 473a2061 7267765b  ....DEBUG: argv[
     401150 315d203d 20257300 63686563 6b696e67  1] = %s.checking
     401160 20272573 270a0073 686f775f 6d655f74  '%s'..show_me_t
     401170 68655f66 6c616700 6f6b004f 89df919f  he_flag.ok.O....
     401180 887e009a 5b38babe 27ac0e3e 434d6285  .~..[8..'..>CMb.
     401190 55868954 3848a34d 00192d76 40505e3a  U..T8H.M..-v@Pˆ:
     4011a0 00726200 666c6167 203d2025 730a00❶67   .rb.flag = %s.. g
     4011b0 75657373 20616761 696e2100 00000000  uess again!.....
     4011c0 49742773 206b696e 6461206c 696b6520  It's kinda like
     4011d0 4c6f7569 7369616e 612e204f 72204461  Louisiana. Or Da
     4011e0 676f6261 682e2044 61676f62 6168202d  gobah. Dagobah -
     4011f0 20576865 72652059 6f646120 6c697665  Where Yoda live
     401200 73210000 00000000                    s!......

    使用objdump检查ctf的.rodata节,可以在地址0x4011af处再次看到guess again字符串。我们来看一下清单5-10,该清单显示了调用puts的指令,以找出ctf对于GUESSME环境变量的期望输入。

    清单5-10 检查GUESSME的指令

      $ objdump -d ctf
      ... 
    ❶    400dc0: 0f b6 14 03       movzx edx,BYTE PTR [rbx+rax * 1]
         400dc4: 84 d2             test  dl,dl
    ❷    400dc6: 74 05             je    400dcd <_Unwind_Resume@plt+0x22d>
    ❸    400dc8: 3a 14 01          cmp   dl,BYTE PTR [rcx+rax * 1]
         400dcb: 74 13             je    400de0 <_Unwind_Resume@plt+0x240>
    ❹    400dcd: bf af 11 40 00    mov   edi,0x4011af
    ❺    400dd2: e8 d9 fc ff ff    call  400ab0 <puts@plt>
         400dd7: e9 84 fe ff ff    jmp   400c60 <_Unwind_Resume@plt+0xc0>
         400ddc: 0f 1f 40 00       nop   DWORD PTR [rax+0x0]
    ❻    400de0: 48 83 c0 01       add   rax,0x1
    ❼    400de4: 48 83 f8 15       cmp   rax,0x15
    ❽    400de8: 75 d6             jne   400dc0 <_Unwind_Resume@plt+0x220>
      ...

    指令在0x400dcd处加载guess again❹字符串,然后使用puts❺将其输出,这是一个失败分支,从这里往回看。

    失败分支是起始地址为0x400dc0的循环的一个分支,在每次循环中,它会将数组(可能是字符串)的字节加载到edx中❶,rbx寄存器指向数组的基址,rax对其进行索引。如果载入的字节结果为NULL,那么地址0x400dc6的指令就会跳转到失败分支❷。这里与NULL进行比较其实是对字符串结尾的检查,如果这里已经到达字符串的结尾,说明字符串太短,没办法进行匹配。如果载入的字节结果不为NULL,那么je跳转到下一条指令,地址0x400dc8的指令将edx中的低字节与另一个字符串中的字节进行比较,该字符串以rcx为基址、rax为索引❸。

    如果比较的两字节相匹配,那么程序将跳转到地址0x400de0。增加字符串索引rax❻,并检查字符串索引是否等于字符串的长度0x15❼,如果相等,那么字符串比较完成,否则程序跳转到另一个迭代中❽。

    通过上面的分析,现在我们知道字符串是以rcx寄存器作为基址的基本事实。ctf程序会将从GUESSME环境变量获得的字符串与rcx字符串进行比较,这意味着如果可以转储rcx字符串,那么就可以找到GUESSME的值。因为该字符串是在运行时解密的,而静态分析不可用,所以需要使用动态分析来恢复。

    5.9 使用GDB转储动态字符串缓冲区

    GNU/Linux操作系统上最常用的动态分析工具可能是GDB,或者GNU Debugger。顾名思义,GDB主要用于调试,不过它也可用于各种动态分析。实际上,GDB是一种极为通用的调试工具,本章无法涵盖其所有功能,但是我会介绍GDB的一些最常用的功能,你可以使用这些功能来还原GUESSME的值。寻找GDB信息最好的地方不是Linux手册页,而是GNU官方网站手册,你可以在该网站找到所有支持GDB命令的详细内容。

    与strace和ltrace一样,GDB可以附加到正在运行的进程,但由于ctf不是一个长时间运行的进程,因此可以一开始就使用GDB运行。因为GDB是一种交互式工具,所以在GDB启动二进制文件的时候,不会立即执行该二进制文件。在输出带有用法说明的启动消息后,GDB暂停并等待命令,通过命令提示符(gdb)声明GDB正在等待命令。

    清单5-11显示了GUESSME环境变量的期望值所需的GDB命令序列。在讨论清单前,先解释一下每条命令的意思。

    清单5-11 GUESSME环境变量的期望值所需的GDB命令序列

      $ gdb ./ctf
      GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1 
      Copyright (C) 2016 Free Software Foundation, Inc.
      License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
      This is free software: you are free to change and redistribute it.
      There is NO WARRANTY, to the extent permitted by law. Type "show copying"
      and "show warranty" for details.
      This GDB was configured as "x86_64-linux-gnu".
      Type "show configuration" for configuration details.
      For bug reporting instructions, please see:
      <http://www.▓▓▓▓▓▓▓/software/gdb/bugs/>.
      Find the GDB manual and other documentation resources online at:
      <http://www.▓▓▓▓▓▓▓/software/gdb/documentation/>.
      For help, type "help".
      Type "apropos word" to search for commands related to "word"...
      Reading symbols from ./ctf...(no debugging symbols found)...done.
    ❶ (gdb) b *0x400dc8
      Breakpoint 1 at 0x400dc8
    ❷ (gdb) set env GUESSME=foobar
    ❸ (gdb) run show_me_the_flag
      Starting program: /home/binary/code/chapter3/ctf show_me_the_flag
      checking 'show_me_the_flag'
      ok
    ❹ Breakpoint 1, 0x0000000000400dc8 in ?? ()
    ❺ (gdb) display/i $pc
      1: x/i $pc
      => 0x400dc8:      cmp      (%rcx,%rax,1),%dl
    ❻ (gdb) info registers rcx
      rcx               0x615050 6377552
    ❼ (gdb) info registers rax
      rax               0x0       0
    ❽ (gdb) x/s 0x615050
      0x615050:         "Crackers Don't Matter"
    ❾ (gdb) quit

    调试器最基本的功能之一就是设置断点。断点是指调试器将要“中断”执行的地址或者函数名。每当调试器到达断点,它就会暂停执行并将控制权返回给用户,等待命令输入。为了转储GUESSME环境变量的“幻数”字符串,我们在发生比较的地址0x400dc8❶设置一个断点。在GDB中,在地址处设置断点的命令是b*address(b是命令break的简短版本)。如果符号可用(本案例中不可用),可以使用函数名称在函数的入口点设置断点,例如在main的开始位置设置断点,可以使用命令b main。

    设置断点后,还需要为GUESSME环境变量设置一个值,才能开始执行ctf,以防ctf过早退出。在GDB中,可以使用命令set env GUESSME = foobar❷设置GUESSME环境变量。现在可以通过命令run show_me_the_flag❸执行ctf。正如你所看到的,可以将参数传递给run命令,然后其会自动传递给正在分析的二进制文件,如ctf。现在ctf开始正常执行,并继续执行直到命中断点。

    当ctf命中断点的时候,GDB会暂停ctf的执行,并将控制权返回给你,告知你断点已经被命中❹。此时,可以使用display/i $pc命令在当前程序计数器($pc)上显示指令,确保在预期的指令上中断❺。不出所料,GDB提示下一条要执行的指令是cmp (%rcx,%rax,1), %dl,该指令确实是我们感兴趣的比较指令(采用AT&T格式)。

    现在已经来到ctf中将从GUESSME环境变量获得的字符串与预期字符串进行比较的位置,我们需要找到字符串的基址,并将其导出。为了查看rcx寄存器中包含的基址,使用命令info registers rcx❻,通过该命令还可以查看rax的内容,以确保循环计数器为零。正如预期的那样❼,我们也可以在不指定任何寄存器名称的情况下使用命令info registers,这个时候,GDB就会显示所有的通用寄存器内容。

    现在我们得到了要转储的字符串的基址,地址从0x615050开始,剩下要做的就是将字符串转储到该地址。在GDB中转储内存的命令是x,它能够以各种编码和粒度转储内存,如x/d以十进制形式转储单字节,x/x以十六进制形式转储单字节,x/4xw以4个十六进制字(4字节整数)的形式进行转储。这个示例中,最有用的就是x/s命令,它会以C风格的形式转储字符串,直到遇见NULL字节为止。当你用x/s 0x615050命令来转储字符串的时候❽,你会发现GUESSME环境变量的期望值是Crackers Don’t Matter,最后用quit❾命令退出GDB。

    $ GUESSME="Crackers Don't Matter" ./ctf show_me_the_flag
    checking 'show_me_the_flag'
    ok
    flag = 84b34c124b2ba5ca224af8e33b077e9e

    如上所示,我们终于完成了所有的步骤,得到神秘的flag。在虚拟机的本章目录上,找到一个名为oracle的程序,并将flag提交给oracle(./oracle
    84b34c124b2ba5ca224af8e33b077e9e)。现在我们已经成功解锁下一个挑战,你可以使用新学会的技能自行完成该练习。

    5.10 总结

    在本章中,我向你介绍了成为一名二进制分析师需要用到的Linux二进制分析工具。虽然这些工具大多数都非常简单,但是你可以将它们组合起来实现功能强大的二进制分析。在第6章中,你将要探索一些主要的反汇编工具,以及其他更高级的分析技术。

    本文摘自《二进制分析实战》

    二进制分析是分析计算机二进制程序(称为二进制文件)及其包含的机器代码和数据属性的科学和艺术。二进制分析的目标是确定二进制程序的真正属性,以理解它们真正的功能。

    本书是为安全工程师编写的,涉及二进制分析和检测的相关内容。本书首先介绍了二进制分析的基本概念和二进制格式,然后讲解了如何使用GNU/Linux二进制分析工具链、反汇编和代码注入这样的技术来分析二进制文件,最后介绍了使用Pin构建二进制插桩的方法以及使用libdft构建动态污点分析工具的方法等。

    本书适合安全工程师、学术安全研究人员、逆向工程师、恶意软件分析师和对二进制分析感兴趣的计算机科学专业的学生阅读。

    展开全文
  • 用于高效二进制级别覆盖率分析的工具。 bcov在没有编译器支持的情况下静态地检测x86-64 ELF二进制文件。它具有探针修剪,精确的CFG分析和复杂的仪器技术的功能。我们总结了本研究在2分钟预告片。 资源 有关详细信息...
  • 《娜璋带你读论文》系列主要是督促自己阅读优秀论文及听取学术讲座,并分享给...这篇文章将带来USENIXSec21恶意代码分析的经典论文,DeepReflect,它通过二进制重构发现恶意功能,来自于佐治亚理工。希望对您有所帮助~

    《娜璋带你读论文》系列主要是督促自己阅读优秀论文及听取学术讲座,并分享给大家,希望您喜欢。由于作者的英文水平和学术能力不高,需要不断提升,所以还请大家批评指正,非常欢迎大家给我留言评论,学术路上期待与您前行,加油。

    前一篇从个人角度介绍英文论文实验评估(Evaluation)的数据集、评价指标和环境设置如何撰写。这篇文章将带来USENIXSec21恶意代码分析的经典论文,DeepReflect,它通过二进制重构发现恶意功能,来自于佐治亚理工。一方面自己英文太差,只能通过最土的办法慢慢提升,另一方面是自己的个人学习笔记,并分享出来希望大家批评和指正。希望这篇文章对您有所帮助,这些大佬是真的值得我们去学习,献上小弟的膝盖~fighting!

    在这里插入图片描述

    原文作者:Evan Downing, Yisroel Mirsky, Kyuhong Park, Wenke Lee
    原文标题:DeepReflect: Discovering Malicious Functionality through Binary Reconstruction
    原文链接https://www.usenix.org/conference/usenixsecurity21/presentation/downing
    发表会议:USENIXSec 2021
    代码下载https://github.com/evandowning/deepreflect

    除了原文和作者的理解,本文还参考了下面三位老师的博客,再次感谢,向老师和好友们学习。

    作者感受:
    这篇论文的框架风格和我的很像,非常值得我学习,尤其是文中的英文表述、创新点、模型设计、四类特征设计以及实验评估(从五个方面评估)。整个工作非常充实,也是恶意代码分析中的一篇经典文章。

    前文赏析:


    一.摘要

    深度学习已在恶意软件分类任务中表现出良好的结果。然而:

    • 人工分析效率低:对于未知恶意软件的binary,分析人员仍要花大量时间来利用静态分析工具逆向整个binary,从而识别关键的恶意行为
    • 监督学习开销大:尽管机器学习可用来帮助识别二进制的重要部分,但由于获取足够大的标记数据集开销很大,因此监督学习方法是不切实际的

    为了提高静态(或手动)逆向工程的生产力,我们提出了DeepReflect:一种用于定位(localize)和识别(identify)恶意二进制文件中恶意软件组件的工具。

    • 为了定位恶意软件组件,我们以一种新型(novel)方式,即首先使用一个无监督的深度神经网络l来定位恶意软件中恶意组件(函数)的位置
    • 其次,通过半监督聚类分析对恶意组件进行分类,根据恶意行为分类确定恶意函数的行为,其中分析人员在他们的日常工作流程中逐步提供标签
    • 该工具是实用的,因为它不需要数据标记(require no data labeling)来训练定位模型,也不需要最小/非侵入性标记来增量地训练分类器

    我们通过5个恶意软件分析人员对超过26k个恶意软件样本进行评估。实验发现,DeepReflect让每个分析人员需要逆向工程的函数数量平均减少了85%。本文方法还可以检测到80%的恶意软件组件,而当使用基于签名的工具CAPA时,该值仅为43%。

    • 企业界对比:CAPA

    此外,DeepReflect提出的自动编码器(autoencoder)比Shap(一种人工智能解释工具)表现得更好。这一点很重要,因为Shap是一种最先进(state-of-the-art)的方法,需要一个标记的数据集,而我们的自动编码器不需要。

    • 学术界对比:Shap

    在这里插入图片描述


    二.引言

    由于每篇论文的引言都非常重要,因此该部分作者会全文翻译,后续章节则介绍重点内容。

    1.背景引出挑战

    静态逆向工程恶意软件可能是一个手动且乏味的过程。公司每周可以收到多达 500 万个PE样本。 虽然大多数组织提前对这些样本进行分类(triage),以减少要分析的恶意软件数量(即,检查 VirusTotal来获取反病毒 (AV) 引擎结果、在受控沙箱中执行样本、提取静态和动态签名等) ,但最终仍然需要静态逆向工程的恶意软件样本。这是因为总会有新的恶意软件样本,没有被反病毒公司分析过,或者缺乏签名来识别这些新样本。最终,该样本有可能会拒绝在分析人员的动态沙箱(sandbox)中执行。

    • Reverse engineering malware statically can be a manual and tedious process.
    • checking VirusTotal [12] for antivirus (AV) engine results, executing the sample in a controlled sandbox, extracting static and dynamic signatures, etc.

    当前的解决方案以为恶意软件样本创建签名、分类和聚类的形式存在。然而,这些解决方案只能预测样本的类别(例如,良性与恶意,或特定的恶意软件家族)。他们无法定位或解释恶意软件样本本身内部的行为(定位恶意函数位置、解释恶意函数行为),而分析师需要执行(perform)这些行为来生成报告并改进他们公司的恶意软件检测产品。事实上,由于工作量过大,该领域已呈现了倦怠。

    • Current solutions exist in the form of creating signatures [33,45,72], classification [14,30,36,41], and clustering [18,25,52] for malware samples.

    为了确定他们的需求,我们咨询了四名逆向工程恶意软件分析师(一名来自AV公司,三名来自政府部门)。本文发现,如果恶意软件分析师有一个工具可以:

    • (1) 识别恶意软件中恶意函数的位置
      identify where malicious functionalities are in a malware
    • (2) 标记这些恶意函数的行为
      label those functionalities

    那么,他们的工作将更有效率。开发这样一种工具的挑战在于:

    • (1) 需要能够区分什么是良性的(benign),什么是恶意的(malicious)
    • (2) 理解识别出的恶意行为的语义

    对于第一个挑战,区分良性和恶意是困难的,因为恶意软件和良性软件的行为通常在高层次上重叠。对于第二个挑战,自动标记和验证这些行为是很困难的,因为没有单独标记的恶意软件函数的数据集(与使用反病毒标签的开放数据集的恶意软件检测和分类系统不同)。


    2.如何解决挑战

    为了解决这些挑战,我们开发了DEEPREFLECT,它使用:

    • (1) 一个无监督的深度学习模型来定位二进制中的恶意函数
    • (2) 一个半监督聚类模型,它使用从分析人员的日常工作流程中获得的少量标签对识别的函数进行分类

    为了定位(locate)二进制文件中的恶意软件组件,我们使用自动编码器(autoencoder,AE)。AE是一种基于神经网络的机器学习模型,其任务是将其输入重构为输出(编码还原)。由于网络内层存在压缩,AE被迫学习训练分布中的关键概念。我们的直觉是,如果在良性二进制文件上训练AE,它将很难重建恶意二进制文件(即我们没有训练它的样本)。自然地,AE将无法重建(reconstruct)包含恶意行为的二进制数据区域(在良性样本中是不可见或罕见的)。因此(Thus),重构错误可以用来识别恶意软件中的恶意组件。此外,由于AE是以无监督的方式训练的,我们不需要数百万标记的样本,公司可以利用自己的恶意软件二进制数据集。

    该约束读者需要理解,本文使用恶意样本进行学习和识别。

    为了对定位的恶意软件组件进行分类,我们:

    • (1) 对恶意软件样本中所有已识别的函数进行聚类
    • (2) 使用分析人员在日常工作流程中所做的注释(即少量人工分析的函数行为标签)来标记聚类结果

    这种方法是半监督的,因为每个类簇(cluster)只需要少数函数的行为标签(如三个)即可将大多数标签分配给整个集群。随着时间推移,我们可以将AE识别的函数映射到聚类模型来预测函数的类别(如,C&C、特权升级等),即认为函数和最接近的类簇有相同的行为标签。这反过来又节省了分析人员的时间,因为他们不必一次又一次地对相同的代码进行逆向工程。

    注意,无监督 AE 为恶意软件分析人员提供了即时实用程序,无需训练或使用半监督聚类模型。这是因为它:

    • (1) 通过对最相关的函数进行排序(重构误差)来吸引分析师的注意力
    • (2) 过滤掉可能需要花费分析师数小时或数天时间来解释的函数

    DEEPREFLECT根据我们是为恶意软件分析人员的反馈进行设计和修改的,并评估其有效性和实用性。

    我们评估了DEEPREFLECT的性能,包括五个工作:

    • (1) 识别恶意软件中的恶意活动
    • (2) 聚类相关的恶意软件组件
    • (3) 将分析人员的注意力集中在重要事情上
    • (4) 揭示不同恶意软件家族之间的共享行为
    • (5) 处理涉及混淆的对抗性攻击

    3.创新(Contribution)

    我们的贡献如下:

    • 提出了一个新颖的工具,它可以帮助恶意软件分析师:(1) 在静态恶意软件样本中自动定位和识别恶意行为,(2) 洞察分析不同恶意软件家族之间的功能关系。
    • 提出一种在静态分析中使用机器学习的新颖实用方法:(1) AE训练是在一种无监督方式下进行的,无需为系统标注任何样本,就可以产生突出显示恶意软件组件的实用程序,(2) 分类是以半监督方式完成,具有最小的干预:分析人员的常规工作流的注释用作标签,群集中的大多数标签用于对相关的恶意软件组件进行分类。
    • 本文提出了一种解释框架(如我们提出的 AE 或 SHAP)定位恶意软件重要部分的方法,该方法可以映射回原始二进制或控制流图的特征

    三.Scope & Overview

    1.Motivation

    图1展示了一个典型的恶意软件分析师Molly的工作流程。当给定一个恶意软件样本,Molly的任务是了解该样本在做什么,以便她写一份技术报告并改进公司的检测系统,从而在未来识别该类样本。

    • (1) 首先查询VT(virtotul)和其他组织,以确定他们以前是否见过这个特定的样本,然而并没有
    • (2) 在一个自定义的沙箱中执行样本以了解其动态行为,然而没有显示任何恶意行为或拒绝执行;运行一些内部工具,诱使恶意软件执行其隐藏的行为,但仍无效时
    • (3) 尝试脱壳(unpacking)和静态逆向分析恶意样本,以了解其潜在行为
    • (4) 在反汇编程序(IDA Pro 或 BinaryNinja)中打开脱壳后的样本,被数千个函数淹没,接着运行各种静态签名检测工具来识别恶意软件的某些特定恶意组件,但仍无效
    • (5) 逐个查看每个函数(可能通过 API 调用和字符串过滤)以尝试了解它们的行为
    • (6) 在分析样本的行为后,撰写分析报告(包含基本信息、IOC、静态签名等)

    然而,当新的样本出现时,Molly需要重复同样的任务。由于这种重复的体力劳动,这项工作对Molly来说变得单调乏味和耗时。

    DEEPREFLECT旨在减轻恶意分析师的分析工作,能逆向一个未知的恶意软件样本,从而减轻他们繁重的任务,并为相似的函数标注行为标签。


    2.Proposed Solution

    我们提出了DEEPREFLECT,该工具能:

    • (1) 定位恶意软件binary中的恶意函数
      locates malicious functions within a malware binary

    • (2) 描述这些函数的行为
      describes the behaviors of those functions

    虽然分析人员可能首先尝试通过搜索特定的字符串和API调用来静态地识别行为,但这些行为很容易被分析人员混淆或隐藏( obfuscated or hidden)。DEEPREFLECT没有做出这样的假设,并试图通过控制流图(control-flow graph,CFG)特性和API调用(API calls)的组合来识别这些相同的行为。

    DEEPREFLECT通过学习正常情况下良性的二进制函数来工作。因此,任何异常都表明这些函数不会出现在良性二进制文件中,而可能被用于恶意行为中。这些异常函数更可能是恶意函数,分析师可以只分析它们,从而缩小工作范围。如图5所示,DEEPREFLECT将分析师必须分析的函数数量平均减少了 85%。此外,实验表明我们的方法优于旨在实现相同目标的基于签名的技术。

    在这里插入图片描述


    3.Research Goals

    本文有四个主要目标:

    • G1:准确地识别恶意软件样本中的恶意活动
    • G2:帮助分析人员在静态分析恶意软件样本时集中注意力
    • G3:处理新的(不可见的)恶意软件家族
    • G4:深入了解恶意软件家族的关系和趋势

    四.模型设计

    1.总体框架

    DEEPREFLECT的目标是识别恶意软件二进制中的恶意函数。在实践中,它通过定位异常基本块(感兴趣区域 regions of interest,RoI)来识别可能是恶意的函数。然后,分析人员必须确定这些函数是恶意行为还是良性行为。DEEPREFLECT有两个主要步骤,如图2所示:

    • RoI检测(RoI detection):通过AE(AutoEncoder)来执行的
    • RoI注释(RoI annotation):通过对每个函数的所有RoI聚类,并将标记聚类结果来执行注释。注意,一个函数可能有多个ROI,用每个函数自己的ROI的均值表示该函数,然后对函数聚类

    在这里插入图片描述

    (1) 术语 Terminology
    首先定义恶意行为(malicious behaviors)的含义。我们根据识别恶意软件源代码的核心组件(例如,拒绝服务功能、垃圾邮件功能、键盘记录器功能、命令和控制C&C功能、利用远程服务等)来生成真实情况(ground-truth)。通过MITRE ATT&CK框架描述,如表3所示。

    在这里插入图片描述

    然而,当静态逆向工程评估恶意软件二进制文件时(即在野恶意软件二进制 in-the-wild malware binaries),我们有时无法肯定地将观察到的低级函数归因于更高级别的描述。例如,恶意软件可能会因为许多不同的原因修改注册表项,但有时确定哪个注册表项因什么原因而被修改是很困难的,因此只能粗略地标记为“防御逃避:修改注册表(Defense Evasion: Modify Registry)”。即使是像CAPA这样的现代工具,也能识别出这些类型的模糊标签。因此,在我们的评估中,我们将“恶意行为”表示为可由MITRE ATT&CK框架描述的函数。

    (2) RoI Detection
    检测的目标是自动识别恶意软件二进制文件中的恶意区域。例如,我们希望检测C&C逻辑的位置,而不是检测该逻辑的特定组件(例如,网络API调用connect()、send() 和 recv())。RoI检测的优点是分析人员可以快速定位启动和操作恶意行为的特定代码区域。先前的工作只关注于创建临时签名,简单地将二进制文件标识为恶意软件或仅基于API调用的某些函数。这对于分析人员扩大他们的工作特别有用(即不仅仅依赖手动逆向工程和领域专业知识)。

    (3) RoI Annotation
    注释的目标是自动标记包含RoI的函数的行为,即识别恶意函数在做什么。由于分析人员为标记集群所执行的初始工作是一个长尾分布。也就是说,只需要前期做比较重要的工作,随着时间推移,工作量会减少。这个过程的优点很简单:它为分析人员提供了一种自动生成未知样本的报告及见解的方法。例如,如果恶意软件示例的变体包含与之前的恶意软件示例相似的逻辑(但对于分析人员来说看起来不同以至于不熟悉),我们的工具为他们提供了一种更快实现这一点的方法。


    2.RoI Detection

    首先介绍了AutoEncode(AE)神经网络。此外,先前的工作已经证明,当自动编码器在良性分布上进行训练时,AE可以检测到恶意(异常)行为。我们的假设是,与良性二进制文件相比,恶意软件二进制文件将包含相似但独特的功能。

    在这里插入图片描述

    当使用大量良性样本训练AE后,给定一个随机的样本,可以利用公式(2)计算,超过MSE的即认为是恶意区域,突出显示ROI异常基本块。与先前识别整个样本为恶意区域的工作相比,我们识别了每个样本中的恶意区域。具体而言,我们计算的 localized MSE 定义如下:

    在这里插入图片描述

    We denote the mapped set of RoIs identified in sample x as the set

    在这里插入图片描述

    (1) Features

    我们特征(c)的灵感来自于先前工作中发现的特征,即属性控制流图(attributed control flow graph,ACFG)特征[23,75]。在这些工作中,ACFG特征被选择来执行二进制相似性,因为它们假设这些特征(由结构和数字CFG特征组成)将在多个平台和编译器上是一致的。

    • Genius
    • Gemini

    为了在二进制样本中定位恶意行为的位置,编码使用的特征必须一对一的映射回原样本。因此,作者将每个二进制文件表示为一个 m×c 的矩阵,该矩阵使用c个静态特征捕获前m个基本块以总结样本的behavior。m设置为20k个基本块,是因为95%的数据集样本具有20k或者更少的基本块, c设置为18个特征。

    Our features consist of counts of instruction types within each basic block (a more detailed form of those extracted for ACFG features), structural features of the CFG, and categories of API calls (which have been used to summarize malware program behaviors).

    • Structural Characteristics
      结构特征2个,每个基本块的后代(offspring)数量和betweenness score,可以描述不同功能的控制流结构,比如网络通信(connect, send, recv)或文件加密(findfile, open, read, encrypt, write, close)。如图6所示。

    在这里插入图片描述

    • Arithmetic Instructions
      算术指令3个,每个基本块基本数学、逻辑运算、位移指令的数量(“basic math”, “logic operation”, and “bit shifting”)。这些算术指令特征可以用来表示如何对更高层次的行为执行数学运算,以及数字如何与函数交互。例如,加密函数可能包含大量的xor指令,混淆函数可能包含逻辑和位移操作的组合等。

    在这里插入图片描述

    • Transfer Instructions
      转移指令3个,每个基本块内堆栈操作,寄存器操作和端口操作的数量(“stack operation”, “register operation”, and “port operation”)。这些底层特征可描述更高级别函数的传输操作,比如函数的参数和返回值是如何与函数内其余数据交互的,从而描述更复杂的逻辑和数据操作。例如去混淆、解密函数可能设计更多move-related指令,C&C逻辑设计更多堆栈相关指令。

    在这里插入图片描述

    • API Call Categories
      API类别10个, 包括"filesystem", “registry”, “network”, “DLL”,“object”, “process”, “service”, “synchronization”, “system information”, and "time"相关的API调用数量。调用不同类型API可执行不同类型功能,直接的表示了高层的函数行为,是很关键的特征。

    在这里插入图片描述

    本文工作API特征的选择受到先前恶意软件检测工作[18]的启发。本文使用的ACFG特征比Genius和Gemini更细致。本文没有用字符串特征,因为容易被混淆、隐藏。


    (2) Model

    Autoencoder使用U-Net模型,U-Net的优点是其在编码器和解码器之间有跳过连接(skip connections),对样本x可以跳过某些特征的压缩以在重构的x’中保持更高的保真度。

    首先收集大量的良性样本,对每个binary抽取上述18个静态特征用于表示该binary。设有用feature表示的样本x,AE重构后得到x’,训练的目标是最小化重构损失,即输入x和输出x’之间的损失。

    在这里插入图片描述

    RoI Detection会在m个基本块中检测出一些异常基本块。这些基本块分别属于不同的函数,使用例如BinaryNinja的工具就可以确定ROI属于哪些函数,即认为这些函数可能是恶意函数,也就完成了恶意函数定位的任务。后续RoI Annotation就是对这些函数聚类,完成恶意函数行为标记(分类)的任务。


    3.RoI Annotation

    给定一个新样本x,我们希望识别其每个函数的行为(类别),并将其报告给Molly。由于标记所有的函数都是不实用的,所以我们只注释了少量的函数,并使用聚类分析来传播结果。

    (1) Clustering Features
    假设一组脱壳恶意软件,按上述特征提取方式(18种特征)得到每个binary的特征表示,其中一个binary为x。

    在这里插入图片描述

    (2) Clustering Model
    使用PCA将特征数从18降维至5,然后使用HDBSCAN算法对5维特征聚类。

    在这里插入图片描述


    4.Deployment

    接下来,我们将描述如何部署和使用它。

    (1) Initialization

    • 首先对良性和恶意binaries脱壳
    • 提取binary静态特征,形成20×18的矩阵
    • 用良性样本训练AutoEncoder
    • 使用训练好的AE从恶意样本中提取ROIs,即恶意基本块位置
    • 计算恶意二进制中恶意函数的行为表示,加入聚类的训练集D
    • PCA降维并聚类生成C

    人工分析恶意软件手动打标,这些label注释到聚类训练集中,从而评估实验结果。换句话说,每个cluster只需要其中几个函数的label,就可确定整个cluster的label,即确定整个cluster中函数的恶意行为。

    (2) Execution
    当Molly收到一个新的样本x,DeepReflect会自动定位恶意函数并标注恶意行为。

    • 对样本x执行脱壳(unpack)
    • 通过AutoEncoder获取ROIs
    • 使用BinaryNinja以及ROIs确定恶意函数集合,然后计算恶意函数的行为表示
    • PCA模型降维
    • 计算每个恶意函数最相近的集群,通过计算和聚类中心的距离实现
    • 分配大数据集群注释给函数

    接下来,Molly分析highlighted functions,从而实现:

    • obtains a better perspective on what the malware is doing
    • annotates any function labeled “unknown” with the corresponding MITRE category (dynamically updating D)
    • observe shared relationships between other malware samples and families by their shared clusters(共享关系,分析恶意软件家族的相关性)

    五.实验评估

    1.Dataset

    根据CNET爬取PE文件,然后经过脱壳、过滤得到23307个良性样本。根据VirusTotal ,脱壳、过滤,在沙箱中执行获取家族标签。得到36396个恶意样本,4407个家族。

    在这里插入图片描述

    特征18个:

    在这里插入图片描述


    2.Evaluation 1 – Reliability(可靠性)

    为了评估DeepReflect自动编码器的定位能力,我们与一般方法和领域特定方法进行比较:

    • SHAP(a classification model explanation tool)
      Scott M. Lundberg and Su-In Lee. A unified approach to interpreting model predictions. In Advances in Neural Information Processing Systems, pages 4765–4774, 2017.

    • CAPA (a signature-based tool by FireEye for identifying malicious behaviors within binaries)
      https://github.com/fireeye/capa

    • FunctionSimSearch(a function similarity tool by Google)
      https://github.com/googleprojectzero/functionsimsearch.

    静态的分析了三个恶意软件的源代码(rbot, pegasus, carbanak),分析了其中恶意组件的位置。结果如Figure 3,横线为80% True Positive Rate。

    在这里插入图片描述


    3.Evaluation 2 – Cohesiveness(凝聚)

    测试DeepReflect聚类的凝聚性,对恶意函数行为分类的能力。生成了22469个类簇,最大的簇包含6321个函数,最小的簇包含5个,如图10所示。在图10中,我们展示了类簇大小上的分布。图中显示,存在一个长尾分布(这在基于密度的聚类中很常见),其中最多的前10个集群占函数的5%。

    在这里插入图片描述

    在聚类质量分析中,89.7%的分析人员手工聚类功能与DeepReflect创建的功能相匹配。

    在这里插入图片描述

    此外,聚类质量存在问题,相同功能却被聚集在不同类簇中,分析了3个案例,主要因为小地方存在差异,聚类算法过于敏感。

    在这里插入图片描述


    4.Evaluation 3 – Focus

    DeepReflect缩小需要人工分析的函数的范围的能力。如图5所示,很多样本需要分析的函数数量降低了90%以上。平均降低85%。

    在这里插入图片描述


    5.Evaluation 4 – Insight

    为了评估DeepReflect是否为恶意软件家族间的关系及其行为提供了有意义的见解,我们探索了集群多样性。图4的左侧绘制了C中每个类簇中不同家族的数量。由图可知,在家族之间有许多共享的恶意软件技术和变体,部分恶意软件家族间分享了相同的函数,新的恶意软件家族的样本也可以被成功的分类。

    在这里插入图片描述


    6.Evaluation 5 – Robustness

    使用LLVM混淆,继续测试模型的鲁棒性;同时使用对抗样本攻击,将包含本文使用的特征的良性样本的代码插入到恶意样本中,但均未对结果产生显著影响。


    六.限制和相关工作

    Every system has weaknesses and ours is no exception.

    • Adversarial Attacks.
    • Training Data Quality.
    • Human Error.

    Related Works

    • Deep Learning and Malware
    • Autoencoders and Security

    七.Conclusion

    在这里插入图片描述

    在这里插入图片描述


    八.个人感受

    写到这里,这篇文章就分享结束了,再次感谢论文作者及引文的老师们。接下来是作者的感受,由于是在线论文读书笔记,仅代表个人观点,写得不好的地方,还请各位老师和博友批评指正,感恩遇见,读博路漫漫,一起加油~

    个人总结:
    这篇文章确实是恶意代码分析领域的顶级论文,写得非常棒,真心值得我去学习,感谢论文作者及Georgia Institute of Technology团队的分享。同时,论文的写作方式及框架、实验都非常棒,工作量也很大,每个部分甚至都需要我们去理解,具体优点如下:

    • 整体方面
      DeepReflect是一种新颖的用于定位(localize)和识别(identify)恶意二进制文件中恶意软件组件的工具,能有效提高静态(或手动)逆向工程的生产力。DeepReflect可以帮助分析人员实现:(1) 在静态恶意软件样本中自动定位和识别恶意行为,(2) 洞察分析不同恶意软件家族之间的功能关系。
    • 评估方面
      本文对比实验非常详细和充分。一方面,本文同企业界和学术界经典的工具进行了对比实验,包括CAPA、SHAP和FunctionSimSearch,这也是系统安全论文经典的实验比较方式;另一方面,本文涵盖了五个方向(Reliability、Cohesiveness、Focus、Insight、Robustness)的详细实验分析,包括Appendix部分的各种特征案例、恶意家族行为共享分析都非常值得我们学习,而不仅仅是PRF的比较。
    • 实战方面
      本文与ATT&CK框架进行有效结合,包括恶意功能或行为的映射。目前安全顶会论文与ATT&CK结合的趋势越来越多,包括溯源图、APT检测、恶意代码分析、家族分类、二进制等等,这进一步体现了与企业界结合的工作的重要性。同时,论文中详细举例介绍了各种情景,并附有对应的图形解释,让审稿老师更为信服,实验部分的实例对比也很重要。
    • 模型方面
      本文模型方面主要是AutoEncoder实现半监督学习,能在少样本标注的情况下识别更多的恶意行为或类别,有效减少了分析人员的手工标注压力。同时,采用了HDBSCAN聚类,并利用PCA降维,这些都是很常见的模型。但整个模型的框架非常精彩(图2胜万语),并且融合了RoI detection和RoI annotation描述故事,故事讲得非常棒。ROI区域之前在做APP地图热点开发时经常使用,没想到在二进制领域也有这么好的表达,确实ROI一个词就能准确表示想做的工作。或许,这种跨方向或学科专业词汇值得注意。
    • 特征方面
      特征方面本文采用4大类(Structural Characteristics、Arithmetic Instructions、Transfer Instructions、API Call Categories)18个特征(之前论文已提出),并且提出了一种解释框架定位恶意软件重要部分的方法,该方法可以映射回原始二进制或控制流图的特征。就我而言,我们也应该思考,在进行恶意代码分析或系统安全研究时,如何尽可能全地覆盖研究问题来提出特征非常重要,并且结合我们的故事。
    • 写作方面
      英文写作一直是我的短板,不说了,这篇文章从摘要、引言到结论,值得我全文背诵,学海无涯。

    在这里插入图片描述

    这篇文章就写到这里,希望对您有所帮助。由于作者英语实在太差,论文的水平也很低,写得不好的地方还请海涵和批评。同时,也欢迎大家讨论,继续加油!感恩遇见,且看且珍惜。

    在这里插入图片描述

    (By:Eastmount 2022-04-28 周四夜于武汉 http://blog.csdn.net/eastmount/ )


    最后给出几段经典的句子:

    • Deep learning has continued to show promising results for malware classification. However, to identify key malicious behaviors, malware analysts are still tasked with reverse engineering unknown malware binaries using static analysis tools, which can take hours.

    • To increase the productivity of static (or manual) reverse engineering, we propose DEEPREFLECT: a tool for localizing and identifying malware components within a malicious binary.

    • Our approach also detects 80% of the malware components compared to 43% when using a signature-based tool (CAPA). Furthermore, DEEPREFLECT performs better with our proposed autoencoder than SHAP (an AI explanation tool). This is significant because SHAP, a state-of-the-art method, requires a labeled dataset and autoencoders do not.

    • While most organizations triage (鉴别、分类) these samples ahead of time to reduce the amount of malware to analyze (i.e., checking VirusTotal [12] for antivirus (AV) engine results, executing the sample in a controlled sandbox, extracting static and dynamic signatures, etc.), at the end of the day there will still be malware samples which require static reverse engineering.

    • Current solutions exist in the form of creating signatures [33,45,72], classification [14,30,36,41], and clustering [18,25,52] for malware samples. However, these solutions only predict the class of the samples (e.g., benign vs. malicious, or a particular malware family). They cannot localize or explain the behaviors within the malware sample itself, which an analyst needs to perform to develop a report and improve their company’s malware detection product.

    • The goal of DEEPREFLECT is to identify malicious functions within a malware binary. In practice, it identifies functions which are likely to be malicious by locating abnormal basic blocks (regions of interest – RoI). The analyst must then determine if these functions exhibit malicious or benign behaviors. There are two primary steps in our pipeline, illustrated in Figure 2: (1) RoI detection and (2) RoI annotation. RoI detection is performed using an autoencoder, while annotation is performed by clustering all of the RoIs per function and labeling those clusters.

    • The challenges in developing such a tool are that (1) one would need to be able to distinguish between what is benign and what is malicious and (2) understand the semantics of the identified malicious behaviors.

    • For the second challenge, automatically labeling and verifying these behaviors is difficult because there are no published datasets of individually labeled malware functions (unlike malware detection and classification systems which use open datasets like antivirus labels).

    • To solve these challenges we developed DEEPREFLECT, a novel tool which uses (1) an unsupervised deep learning model which can locate malicious functions in a binary and (2) a semi-supervised clustering model which classifies the identified functions using very few labels obtained from analyst’s regular daily workflow.


    展开全文
  • windows/linux/mac pin 提供许多分析binary 用的api使用者可以用C/C++ 呼叫api 来撰写自己的pintoolRun Sample Code source/tools是pintools的预设路径底下已经有一些现成pintools可以使用根据README 的步骤来操作:...
  • FCB逐字节比较两个文件。 文件可以是常规文件,也可以是任何类型的设备,管道等,其中一个可以是stdin。 如果一个文件具有标题,则可以指定偏移量。 用法:FCB [-h0,1] [-p0,1] [-v0,1,2,3] [-b0,1] [-q0,1] ...
  • IDAPythonScripts: 二进制漏洞静态分析检测脚本(IDA Pro) QQ交流群:813115551 加微信-进入交流群:wwy18795980897 准备 首先下载IDA Pro软件,版本 <= 6.8,7.0以上部分脚本不支持。 安装IDAPython插件。 IDA...
  • MySQL从3.23版本开始引入了二进制日志,用于的数据复制, 二进制日志根据MySQL的版本不同,目前有4个版本:...

    MySQL从3.23版本开始引入了二进制日志,用于的数据复制, 二进制日志根据MySQL的版本不同,目前有4个版本:

    https://dev.mysql.com/doc/internals/en/binlog-version.html

    690e2ac2293c17934df89fa62cb44f71.png

    Version 1: supported statement based replication events.

    Version 2: can be ignored as it was only used in early alpha versions of MySQL 4.1.x

    and won't be documented here.

    Version 3: added the relay logs and changed the meaning of the log position.

    Version 4: added the FORMAT_DESCRIPTION_EVENT and made the protocol extensible.

    二进制日志版本是向后兼容的, 后一个版本可以看成是对前一个版本的继承和扩展,需要注意的是,version 2是一个临时版本, 可以忽略。事实上可以只关心v4版本,因为现在跑的MySQL都应该是MySQL 5+了,就如现再讨论Oracle 8i, 9i没什么实际意义。截至当前最新的MySQL 8版本,使用的依然是v4版本。

    题外话:

    BinlogMiner的解析器为保持代码稳定,做了接口将功能和具体实现分隔, 但实际上目前也只有一个BinlogParser4实现。也许以后会有V5版本,还有一个问题就是开源软件的碎片化,目前主流的有3个分支,本人偏爱Percona版本,Percona版本完全兼容官方版本,而在性能和可维护性上有提高,非常讨人喜欢。国内一些大的互联网企业如腾讯,阿里也有做基于MySQL的数据库,目前还没有研究,不知道兼容性如何。

    MySQL的二进制日志文件以事件为单位进行封装,文件的结构如下:

    1f9d7cae1bff79322bc139e4850a07e6.png

    说明:二进制日志可以看成是二进制事件的集合,不同的事件,对应于不同的功能,MySQL包含的事件类型可以参考:

    https://dev.mysql.com/doc/internals/en/binlog-event-type.html

    A start event (START_EVENT_V3) is the first event of a binlog for binlog-version 1 to 3.

    A format description event (FORMAT_DESCRIPTION_EVENT) is the first event of a binlog for binlog-version 4.

    v1-v3版本, 二进制日志文件的第一个事件是START_EVENT_V3, 而v4版本开始第一个事件是FORMAT_DESCRIPTION_EVENT,替代掉START_EVENT_V3.

    二进制日志的结束事件为STOP_EVENT或者ROTATE_EVENT,出现其中之一就说应二进制文件已经结束, 其中STOP_EVENT说应MySQL服务器已经关闭, 而ROTATE_EVENT则说明二进制达到了max_binlog_size的阈值,或者在线修改了binlog-format,导致了二进制文件的切换。

    二进制日志QUERY_EVENT和ROWS_EVENT(包括WRITE_ROWS_EVENT/UPDATE_ROWS_EVENT/DELETE_ROWS_EVENT)来记录数据变化, 所有的DDL,如(create table ...)都是通过QUERY_EVENT记录的, 而DML(inert/update/delete)则根据复制模式的不同(binlog-format)而不同, 基于语句的复制(Statement-Based),DML语句以语句形式记录在QUERY_EVENT中,而基于行的复制(Row-Based Replication),则将受到DML语句影响的行的值,记录在ROWS_EVENT中。显而易见, 基于语句的复制一个明显的优势就是数据量小,delete table xxx,只记录一个语句就可以了,但是行模式则需要记录所有行的值。但如前文说的基于语句的复制不是绝对安全的,当遇到"Nondeterministic"的语句,会由问题,比如SYSDATE(),如果将函数复制到备库执行,得到的结果和主库肯定不一样,又如USER()调用的用户不同,得到的结果也不同。当然可以通过一些选项,在遇到有些函数时转换成函数的结果复制,但并不是说有的函数都能解决,特别是自定义的函数。基于行的赋值,ROWS_EVENT中还包含修改的“前值”,BinlogMiner就是通过这些“前值”达到闪回的效果。

    1. magic number

    用于表示二进制日志文件, 4个字节长度, 其值为固定的:0xfe 0x62 0x69 0x6e; 紧接着的是一个个的二进制日志事件。二进制日志的每个事件的结构如下:

    d6134c55a6633fd7c77f9fac0baf5ca0.png

    2. Common Header

    通用文件头, 其实定义了一个事件的基本信息, 包含事件的起止位置, 类型, 时间搓和服务器ID等信息, 我们依赖这些信息特别起止位置来遍历整个二进制日志文件,Common Header的结构如下:

    https://dev.mysql.com/doc/internals/en/binlog-event-header.html

    Binlog header Payload:

    4  timestamp

    1  event type

    4  server-id

    4  event-size

    if binlog-version > 1:

    4 log pos

    2 flags

    可以看到Common Header的长度是固定的13个字节或者19个字节。只有v1版本是13个字节, 后续的版本都是19个字节, 主要是多了log pos, 也就是当前事件的结束位置, v1版本虽然没有结束位置, 但是是可以通过事件的开始位置 + 事件长度(event-size)计算出来的,之所以称为Common Header,是因为这部分是与具体事件无关的。

    3. Post-Header

    Common Header后紧跟着的是Post-Header部分, Post-Header是跟具体事件相关的,而且并不是每个事件都有Post-Header(可以为0),Post-Header的长度对于一个MySQL版本是固定的,但不同版本可能不同,每种事件的Post-header的长度在FORMAT_DESCRIPTION_EVENT中有记录。

    4. PlayLoad

    Post-Header后紧跟着负载(playload), 也就是具体的内容,这部分是不固定长度的,直到事件的结束(也就是Checksum)。

    以一个QUERY_EVENT的案例来概览一下Post-header和playLoad:

    * QUERY_EVENT: The query event is used to send text querys right the binlog.

    *

    * References:

    * https://dev.mysql.com/doc/internals/en/query-event.html

    * https://dev.mysql.com/doc/internals/en/event-data-for-specific-event-types.html

    *

    * Post-header :

    *   4  slave_proxy_id

    *   4  execution time

    *   1  schema length

    *   2  error-code

    * if binlog-version ≥ 4:

    *     2  status-vars length

    *

    * Payload:

    *   string[$len] status-vars

    *    string[$len] schema

    *   1 [00]

    *   string[EOF] query

    5. Checksum

    也就是事件的校验值, 在MySQL 5.6.2版本开始引入,5.6.6版本开始默认开启(CRC32), 这部分在事件的结尾处, 目前只支持CRC算法,检验值为4个字节,校验算法在FORMAT_DESCRIPTION_EVENT事件中通过1个字节记录。

    https://dev.mysql.com/doc/refman/5.6/en/replication-options-binary-log.html#option_mysqld_binlog-checksum

    展开全文
  • 二进制代码的分析是计算机科学和软件工程学科的许多领域的关键活动,从软件安全和程序分析到逆向工程。 手动二进制分析是一项困难且耗时的任务,并且有一些软件工具可以实现自动化或协助人类分析师。 然而,这些工具...
  • 二进制及代码分析工具

    千次阅读 2019-09-20 15:39:37
    二进制及代码分析工具: 吾爱破解论坛【爱盘】3.0 在线破解工具包https://github.com/ganlvtech/down_52pojie_cn Angrhttp://angr.io/ BAPhttps://github.com/BinaryAnalysisPlatform/bap Binary Ninjahttps://...
  • 代码二进制代码集成Binary Code is a representation format for the different types of data like Text, Image, Video, etc. Binary code is expressed two basic symbols or numbers 1 and 0. Binary code is ...
  • 命令中心以静态,动态方式调查和分析二进制文件,以防止缓冲区溢出。 用法:python3 BinMaster.py 语 文件名 MD5哈希 描述 版本 python3 BinMaster.py 5c2b7386b14aa687a8a2af351526a3be 指挥中心 全栈 一个...
  • 来看看这个集成框架在二进制代码分析的CTF中解决过哪些问题吧,下面是git中列举的解决过的CTF赛题: 其中,HackCon 2016 – angry-reverser花费31 min,SecurityFest 2016 – fairlight花费20s,Defcamp CTF ...
  • 二进制、16进制、大端小端

    千次阅读 2021-11-19 14:22:16
    对应的文件就要用对应的软件来查看,但是做为开发,要时候是要查看文件的二进制的,比如我们写一个音频转换器的时候,发现转换后的音频无法播放,就需要查看此音频文件的二进制,以分析是哪里的数据出了问题,而看...
  • GHIDRA的CERT Kaiju二进制分析框架CERT Kaiju是的二进制分析工具的。 这是某些功能的Ghidra / Java实现,尤其是函数哈希和恶意软件分析工具,但随着时间的推移,有望增加新的工具和功能。 由于这是一项新工作,因此...
  • MySQL二进制日志

    2021-06-13 09:23:34
    文章目录mysql复制功能解决了什么问题mysql二进制日志二进制日志二进制日志的格式(段)二进制日志的格式(行)二进制日志的格式(混合)mysql二进制日志格式对复制的影响基于SQL语名的复制( SBR )优点缺点基于行的...
  • Triton是一个动态二进制分析(DBA)框架。 它提供了内部组件,例如动态符号执行(DSE)引擎,动态污点引擎, x86 , x86-64 , ARM32和AArch64指令集体系结构(ISA)的AST表示, SMT简化过程, SMT求解器接口以及...
  • 二进制工具分析的主要是程序的各种信息,比如格式、体系结构、编译器标识、指令、助记符等。反汇编概览IDA是HexRays公司的商业产品,这个也是大家最熟悉的了。不过我是真的用不起,羡慕那些有钱的实验室还有土豪们啊...
  • ROS二进制日志包 ROS binary logger package

    千次阅读 2017-02-09 13:43:46
    原文网址:1 http://www.ros.org/news/2017/02/ros-binary-logger-package.html2 ...该包装设计为rosbag的替代品,当下列情况时:需要多个和长消息采集(二进制文件具有较小的尺寸)仅需要离线数据分析,并且在RO
  • 本篇文章翻译自:(State of) The Art of War: Offensive Techniques in Binary Analysis论文下载链接:http://www.cs.ucsb.edu/~chris/research/doc/oakland16_sokbin.pdf文章主要讲解的是常见的二进制分析技术,...
  • 在精确定位控制系统中,为了提高控制精度,准确测量控制对象的位置是十分重要的。...其是采用光电轴角编码器进行精确位置控制。光电轴角编码器根据其刻度方法及信号输出形式,可分为增量式、绝对式以及混...
  • 摘要:二进制分析技术通常被用来对应用进行安全审计、漏洞检测等,通过分析学术界近20年发表的上百篇学术论文来分析二进制代码相似度比较都有采用了哪些具体技术,二进制代码相似度比较的技术挑战是什么,后续的研究...
  • 二进制数据图片 前言 最近公司那台终端电脑的粘贴功能坏了, 连接远程想从里面拿一个文件出来, 试了好多种方法不顶用, 远程终端开了一堆程序也不能随便重启远程终端, 最终是关了一大堆不必要的应用, 又重启了下...
  • Binary Bomb 二进制炸弹

    千次阅读 2019-01-14 17:36:54
     二进制炸弹   计算机科学与技术学院   目 录   第1章 实验基本信息... - 3 - 1.1 实验目的... - 3 - 1.2 实验环境与工具... - 3 - 1.2.1 硬件环境... - 3 - 1.2.2 软件环境... - 3 - 1.2.3...
  • 因此,二进制代码分析被用于许多原因,包括软件取证,恶意软件分析,性能分析和调试。文献中有很多二进制代码分析方法,其中最常用的有符号执行、concolic执行、静态污染分析和动态污染分析为了验证我根据之前一篇...
  • 技术小阿哥 2017-11-27 1042浏览量 菜鸟学Linux 第080篇笔记 mysql备份 xtrabackup 菜鸟学Linux 第080篇笔记 mysql备份 xtrabackup 内容总览 percona-xtrabackup 备份软件安装 使用软件备份数据库 备份二进制日志...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,431
精华内容 7,372
关键字:

二进制软件分析binary