首页 » 通讯 » 成为一名二进制分析师需要用到的Linux二进制分析对象有哪些?_字符串_文件

成为一名二进制分析师需要用到的Linux二进制分析对象有哪些?_字符串_文件

神尊大人 2024-12-21 03:41:46 0

扫一扫用手机浏览

文章目录 [+]

除了向你大略展示工具列表,并阐明它们的功能以外,我还将利用“夺旗”(Capture The Flag,CTF)寻衅来解释它们的事情事理。
在打算机安全与黑客攻防领域,CTF寻衅常常以竞赛形式进行,目标是剖析并利用指定的二进制文件,或者正在运行的进程/做事器,直至拿到隐蔽在二进制文件中的“flag”为止。
flag一样平常是十六进制的字符串,你可以用它来证明你已经完成了寻衅,并解锁新寻衅。

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

成为一名二进制分析师需要用到的Linux二进制分析对象有哪些?_字符串_文件 通讯

这里看到的大多数工具都有许多有用的选项,但是由于本章须要先容的工具实在太多,因此,最好的方法是在虚拟机上利用man命令查询每个工具的手书页。
在本章的末了,你须要用flag来解锁新寻衅,我相信你可以独立完成该寻衅!

5.1 利用file办理类型问题

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

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

$ file payloadpayload: ASCII text

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

$ head payloadH4sIAKiT61gAA+xaD3RTVZq/Sf9TSKL8aflnn56ioNJJSiktDpqUlL5o0UpbYEVI0zRtI2naSV5KYV0HTig21jqojH9mnRV35syZPWd35ZzZ00XHxWBHYJydXf4ckRldZRUxBRzxz2CFQvb77ru3ee81AZdZZ92z+XrS733fu993v/v/vnt/bqmVfNNkBlq0cCFyy6KFZiUHKi1buMhMLAvMi0oXWSzlZYtAv2hRWRkRzN94ZEChoOQKCAJp8fdcNt2V3v8fpe9X1y7T63Rjsp7cTlCKGq1UtjL9yPUJGyupIHnw/zoym2SDnKVIZyVWFR9hrjnPZeky4JcJvwq9LFforSo+i6XjXKfgWaoSWFX8mclExQkRxuww1uOzZe3x2U0qfpDFcUyvttMzuxFmN8LSc054er26fJns18D0DaxcnNtZOrsiPVLdh1ILPudey/xda1XxMpauTGN3L9hlk69PJsZXsPxS1YvA4uect8N3fN7m8rLv+Frm+7z+UM/8nory+eVlJcHOklIak4mlrbm7kabn9SiwmKcQuQ/g+3n/OJj/byfuqjv09uKVj8889O6TvxXM+G4qSbRbX1TQCZnWPNQVwG86/F7+4IkHl1a/eebY91bPemngU8OpI58YNjrWD16u3P3wuzaJ3kh4i6vpuhT6g7rkfs6k0DtS6P8lhf6NFPocfXL9yRTpS0ny+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_payloaddecoded_payload: gzip compressed data, last modified: Tue Oct 22 15:46:43 2019, from Unix

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

$ file -z decoded_payloaddecoded_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_payloadctf67b8601

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

$ file ctfctf: 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 67b860167b8601: 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 matchesBinary 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 A002 2 02 STX (start of text) 102 66 42 B003 3 03 ETX (end of text) 103 67 43 C004 4 04 EOT (end of transmission) 104 68 44 D005 5 05 ENQ (enquiry) 105 69 45 E006 6 06 ACK (acknowledge) 106 70 46 F007 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 1500000000: 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=164+0 records in64+0 records out64 bytes copied, 0.000404841 s, 158 kB/s

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

再用xxd查看elf_header的内容。

$ xxd elf_header00000000: ❶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=110296+0 records in10296+0 records out10296 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.sonm: 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_RegisterClasses0000000000000c60 T _Z11rc4_decryptP11rc4_state_tPhi0000000000000c70 T _Z11rc4_decryptP11rc4_state_tRNSt7__cxx1112basic_... ...stringIcSt11char_traitsIcESaIcEEE0000000000000b40 T _Z11rc4_encryptP11rc4_state_tPhi0000000000000bc0 T _Z11rc4_encryptP11rc4_state_tRNSt7__cxx1112basic_... ...stringIcSt11char_traitsIcESaIcEEE0000000000000cb0 T _Z8rc4_initP11rc4_state_tPhi U _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE9_... ...M_createERmm U _ZSt19__throw_logic_errorPKc0000000000202058 B __bss_start w __cxa_finalize w __gmon_start__ U __stack_chk_fail0000000000202058 D _edata0000000000202060 B _end0000000000000d20 T _fini00000000000008c0 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_RegisterClasses0000000000000c60 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_fail0000000000202058 D _edata0000000000202060 B _end0000000000000d20 T _fini00000000000008c0 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_tPhirc4_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 foobarchecking 'foobar'$ echo $?1

二进制文件在做一些新的事情,它见告你正在检讨你输入的字符串,但检讨失落败,由于二进制文件在检讨后仍旧会退出并显示缺点代码。
我们来赌一把,输入创造的其他字符串,如show_me_the_flag。

$ ./ctf show_me_the_flagchecking '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_flagchecking 'show_me_the_flag'okguess 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 ctfctf: file format elf64-x86-64Contents 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中,在地址处设置断点的命令是baddress(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_flagchecking 'show_me_the_flag'okflag = 84b34c124b2ba5ca224af8e33b077e9e

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

5.10 总结

在本章中,我向你先容了成为一名二进制剖析师须要用到的Linux二进制剖析工具。
虽然这些工具大多数都非常大略,但是你可以将它们组合起来实现功能强大的二进制剖析。
在第6章中,你将要探索一些紧张的反汇编工具,以及其他更高等的剖析技能。

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

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

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

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

相关文章

浅析C语言浮点除法,算法原理与方法应用

浮点除法是计算机科学中一项基本运算,广泛应用于科学计算、图形渲染、金融计算等领域。在C语言编程中,浮点除法是必须掌握的运算之一。本...

通讯 2025-01-02 阅读0 评论0

海盗的语言,探寻历史风帆下的独特语境

在蔚蓝的大海中,海盗们驾驭着帆船,乘风破浪,留下了一段段传奇。他们的冒险故事,如同海浪般跌宕起伏,引人入胜。而在这段传奇背后,还有...

通讯 2025-01-02 阅读1 评论0

淘宝购物指南,如何打造完美的网购体验

随着互联网的飞速发展,网购已成为我们生活中不可或缺的一部分。作为国内最大的电商平台,淘宝吸引了无数消费者。面对海量的商品和繁杂的购...

通讯 2025-01-02 阅读1 评论0