可以确定哪些命令行参数或选项可以传递给 Linux 可执行文件。当然,如何做到这一点取决于程序的类型和设计以及混淆、加密、压缩等因素。
硬编码文档
Linux可执行设计是人类易于使用,其行为取决于他们所接收的命令行参数,如改变ls
,cat
,grep
等,通常是ELF二进制文件,并有其使用的硬编码的文件.rodata
部分。可以使用 来检查这些数据readelf -x .rodata ELF_BINARY_NAME
。
例子:
$ readelf -x .rodata /bin/ls | less
.
<lots of data>
.
0x00413e90 20202d61 2c202d2d 616c6c20 20202020 -a, --all
0x00413ea0 20202020 20202020 20202020 20646f20 do
0x00413eb0 6e6f7420 69676e6f 72652065 6e747269 not ignore entri
0x00413ec0 65732073 74617274 696e6720 77697468 es starting with
0x00413ed0 202e0a20 202d412c 202d2d61 6c6d6f73 .. -A, --almos
0x00413ee0 742d616c 6c202020 20202020 20202020 t-all
0x00413ef0 646f206e 6f74206c 69737420 696d706c do not list impl
0x00413f00 69656420 2e20616e 64202e2e 0a202020 ied . and ...
.
<even more data>
这意味着即使man
您的系统上没有程序的页面、README
文件和源代码,您仍然可以手动查看它是如何在内部记录的。
这特别与问题中提供的示例有关:
当我尝试使用参数/参数运行它时会发生这种情况:
me@there:~$ ./theFile theOption -a
./theFile : option requires an argument -- 'a'
usage: theOption [parameters]
在这个例子中发生的事情是一些命令行选项被传递给可执行文件,然后可执行文件将一些关于正确用法的字符串打印到 STDOUT。在推断有效命令行参数的上下文中,这意味着可执行文件中有硬编码的字符串,这些字符串记录了可以使用静态分析定位的正确用法。
没有硬编码文档
第 1 部分:静态分析
我们可以利用在设计接受命令行参数的 Linux 用户空间应用程序时所做的程序设计考虑来确定程序在分析二进制文件时需要哪些参数。例如,有什么机制可以解析在执行程序时传递给程序的n个命令行参数?程序是如何设计的,才能成功地确定哪些参数有效,哪些参数无效?如果传递了 1,000 个参数怎么办?处理命令行参数的程序设计必须反映这样一个事实,即此类输入可以是任意的。
在 Linux 环境中用来解决这个问题的一种常见设计模式是使用 GNU C 库函数getopt()
和switch
循环内部构造的组合。getopt()
解析命令行参数,然后由switch
构造评估解析的参数。在分析二进制文件时,这可以用作启发式方法。
为了说明这一点,我们可以分析一个处理命令行参数的“神秘”ELF 二进制文件。
$ file mystery_program
mystery_program: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=3286c11349d82c58257a14e4d174b9ec5f313714, stripped
这个程序是动态链接的,这意味着即使它被剥离,它仍然包含有关在运行时链接和加载哪些库以及程序使用这些库中的哪些函数的信息。由于getopt()
是glibc
(共享库或.so*
)函数,如果我们的动态链接二进制文件使用它,那么该.dynsym
部分中的条目将包含有关它的信息。readelf --dyn-syms mystery_program
确认输出中的条目 21getopt()
确实在程序中使用:
$ readelf --dyn-syms mystery_program
Symbol table '.dynsym' contains 79 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __uflow@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getenv@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND abort@GLIBC_2.2.5 (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __errno_location@GLIBC_2.2.5 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strncmp@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _exit@GLIBC_2.2.5 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strcpy@GLIBC_2.2.5 (2)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __fpending@GLIBC_2.2.5 (2)
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND iconv@GLIBC_2.2.5 (2)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND iswcntrl@GLIBC_2.2.5 (2)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.2.5 (2)
13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND textdomain@GLIBC_2.2.5 (2)
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fclose@GLIBC_2.2.5 (2)
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND bindtextdomain@GLIBC_2.2.5 (2)
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND stpcpy@GLIBC_2.2.5 (2)
17: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dcgettext@GLIBC_2.2.5 (2)
18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __ctype_get_mb_cur_max@GLIBC_2.2.5 (2)
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.2.5 (2)
20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)
==> 21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getopt_long@GLIBC_2.2.5 (2)
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mbrtowc@GLIBC_2.2.5 (2)
23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strchr@GLIBC_2.2.5 (2)
24: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strrchr@GLIBC_2.2.5 (2)
<output snipped>
getopt()
本程序中使用的版本是getopt_long()
. 要查看在什么上下文中getopt_long()
被调用,我们可以使用objdump
:
$ objdump -dj .text mystery_program | grep getopt -B10 -A25
401aa6: c6 84 24 80 00 00 00 movb $0x0,0x80(%rsp)
401aad: 00
401aae: c6 84 24 84 00 00 00 movb $0x0,0x84(%rsp)
401ab5: 00
401ab6: c6 44 24 57 00 movb $0x0,0x57(%rsp)
401abb: 8b 7c 24 58 mov 0x58(%rsp),%edi
401abf: 45 31 c0 xor %r8d,%r8d
401ac2: b9 00 90 40 00 mov $0x409000,%ecx
401ac7: ba 23 8f 40 00 mov $0x408f23,%edx
401acc: 48 89 de mov %rbx,%rsi
401acf: e8 3c fc ff ff callq 401710 <getopt_long@plt> <===
401ad4: 83 f8 ff cmp $0xffffffff,%eax
401ad7: 0f 84 10 01 00 00 je 401bed <__sprintf_chk@plt+0x1bd>
401add: 83 f8 62 cmp $0x62,%eax
401ae0: 0f 84 f3 00 00 00 je 401bd9 <__sprintf_chk@plt+0x1a9>
401ae6: 7e 2c jle 401b14 <__sprintf_chk@plt+0xe4>
401ae8: 83 f8 73 cmp $0x73,%eax
401aeb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
401af0: 0f 84 d6 00 00 00 je 401bcc <__sprintf_chk@plt+0x19c>
401af6: 7e 6a jle 401b62 <__sprintf_chk@plt+0x132>
401af8: 83 f8 75 cmp $0x75,%eax
401afb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
401b00: 74 b9 je 401abb <__sprintf_chk@plt+0x8b>
401b02: 7c 72 jl 401b76 <__sprintf_chk@plt+0x146>
401b04: 83 f8 76 cmp $0x76,%eax
401b07: 0f 85 d6 00 00 00 jne 401be3 <__sprintf_chk@plt+0x1b3>
401b0d: c6 44 24 53 01 movb $0x1,0x53(%rsp)
401b12: eb a7 jmp 401abb <__sprintf_chk@plt+0x8b>
401b14: 83 f8 41 cmp $0x41,%eax
401b17: 74 34 je 401b4d <__sprintf_chk@plt+0x11d>
401b19: 7f 19 jg 401b34 <__sprintf_chk@plt+0x104>
401b1b: 3d 7d ff ff ff cmp $0xffffff7d,%eax
401b20: 74 64 je 401b86 <__sprintf_chk@plt+0x156>
401b22: 3d 7e ff ff ff cmp $0xffffff7e,%eax
401b27: 0f 85 b6 00 00 00 jne 401be3 <__sprintf_chk@plt+0x1b3>
401b2d: 31 ff xor %edi,%ed
对getopt_long()
at 地址的调用401acf
之后是一个跳转表的反汇编,表明存在一个switch
序列。跳转表要长得多,但这里显示的内容足以理解。如果我们更仔细地查看跳转表,我们会发现看起来像 ASCII 代码的十六进制值与 %eax 进行了比较,按照惯例,%eax 保存了 的返回值getopt_long()
:
.
.
401add: 83 f8 62 cmp $0x62,%eax # 0x62 is '>'
.
.
401ae8: 83 f8 73 cmp $0x73,%eax # 0x73 is 'I'
.
.
401af8: 83 f8 75 cmp $0x75,%eax # 0x75 is 'K'
.
.
401b04: 83 f8 76 cmp $0x76,%eax # 0x76 is 'L'
.
.
401b14: 83 f8 41 cmp $0x41,%eax # 0x41 is ')'
.
.
401b34: 83 f8 45 cmp $0x45,%eax # 0x45 is '-'
.
.
401b39: 83 f8 54 cmp $0x54,%eax # 0x54 is '6'
.
.
<more comparisons>
该程序根据它具有特定条件的任何 ASCII 字符解析命令行参数及其执行路径分支。虽然这不会立即产生哪些参数有效,哪些参数无效,但这是一个开始。使用 Linux binutils 进行静态分析(以及我有限的知识和技能)只能让我们到此为止。
第 2 部分:动态分析
在动态分析这个“神秘”二进制文件以确定有效的命令行参数是什么时,有两个因素非常有用。
第一个是即使这个二进制文件被剥离,getopt_long()
它在一个共享库中,为了动态链接到二进制文件,一些符号信息getopt_long()
必须被保留。这就是为什么objdump
用于从这个剥离的二进制文件中静态分析一些反汇编代码的 whengetopt_long()
仍然按名称引用的原因。这意味着我们在getopt_long()
被调用的二进制文件中有确切的地址,这使我们可以在该地址设置断点,而不必在二进制文件中翻来覆去几个小时。换句话说,我们不需要知道在什么函数中getopt_long()
被调用,是inmain()
还是其他函数;我们可以让程序执行到那个地址的断点被命中。作为快速提醒,getopt_long()
在地址 401acf 被调用:
$ objdump -dj .text mystery_program | grep getopt
401acf: e8 3c fc ff ff callq 401710 <getopt_long@plt>
第二个是由所有有效命令行选项组成的字符串getopt()
在调用时传递给它,正如我们在其函数原型中看到的那样。从其man
页面中的概要:
#include <unistd.h>
int getopt(int argc, char * const argv[],
const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;
#include <getopt.h>
int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
int getopt_long_only(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
在这三个原型中,for 的原型getopt_long()
是这个特殊情况下感兴趣的一个。根据原型,getopt_long()
传递了一个指向选项字符串的指针以及一个指向包含长选项的结构的指针。这意味着可以通过在程序getopt_long()
调用点调查这些指针来发现单字符选项和选项字符串。gdb
可用于执行分析:
$ gdb -q mystery_program
Reading symbols from mystery_program...(no debugging symbols found)...done.
(gdb) break *0x401acf
Breakpoint 1 at 0x401acf
(gdb) run
Starting program: mystery_program
Breakpoint 1, 0x0000000000401acf in ?? ()
(gdb) x/i $rip
=> 0x401acf: callq 0x401710 <getopt_long@plt>
(gdb)
正如这里所展示的,没有必要知道main()
程序入口点在哪里,甚至是什么。
这是getopt_long()
被调用的点。是时候调查寄存器了。
(gdb) info registers
rax 0x0 0
rbx 0x7fffffffe138 140737488347448
rcx 0x409000 4231168
rdx 0x408f23 4230947
rsi 0x7fffffffe138 140737488347448
rdi 0x1 1
rbp 0x1000 0x1000
rsp 0x7fffffffdee0 0x7fffffffdee0
r8 0x0 0
r9 0x2 2
r10 0x7fffffffdca0 140737488346272
r11 0x7ffff7a51410 140737348178960
r12 0x402602 4204034
r13 0x7fffffffe130 140737488347440
r14 0x0 0
r15 0x0 0
rip 0x401acf 0x401acf
eflags 0x246 [ PF ZF IF ]
%rcx 和 %rdx 看起来很有趣。
(gdb) x/s $rdx
0x408f23: "benstuvAET"
那是选项字符串,由程序使用的所有字符选项组成。选项有 -b、-e、-n、-s 等。
长期期权呢?
(gdb) x/s $rcx
0x409000: "\225\217@" # oops, that is not a string
(gdb) x/xg $rcx
0x409000: 0x0000000000408f95
(gdb) x/s 0x0000000000408f95
0x408f95: "number-nonblank"
(gdb)
所以一个长选项是“--number-nonblank”。
(gdb) x/s 0x0000000000408f95+64
0x408fd5: "show-tabs"
(gdb)
另一个是“--show-tabs”。
等等。
最后的想法
如果您已经达到了这一点并且还没有因无聊而死,那么恭喜您。事实证明,这个“神秘”程序实际上是cat
一个著名的 Linux 实用程序。在这种特殊情况下,可以发现程序期望的命令行选项。
需要注意的是,在本示例分析的整个过程中,从未检查过传递给mystery_program
/的参数/选项cat
。当用户将选项 x、y 和 z 传递给某个程序然后对其进行分析时,然后转到main()
然后查看其中的字符串是没有用的,*argv[]
因为它们已经是已知的:argv[0]
指向程序名称,argv[1]
指向第一个参数在程序名称、x 等之后。
我同事说只是用objdump来反转main函数的第一部分
调查main()
是为了恢复在程序执行时传递给程序的参数,而不是确定程序实际使用的选项。如示例中所示,该信息位于程序的其他位置。