如何对 Linux 可执行文件进行逆向工程以查找参数?

逆向工程 linux 转储
2021-06-23 05:19:22

所以我在一个带有可执行文件的 Linux 机器上。Vague - 我知道。当我尝试使用参数/参数运行它时会发生这种情况:

me@there:~$ ./theFile theOption -a
./theFile : option requires an argument -- 'a'
usage: theOption [parameters]

我同事说just use objdump to reverse theFile first part of the main function

我的问题是是否可以找到可以传递给程序的参数。我有一些函数的名称,比如do_thisand do_that,但确保如何将它们传递给可执行文件。

2个回答

可以确定哪些命令行参数或选项可以传递给 Linux 可执行文件。当然,如何做到这一点取决于程序的类型和设计以及混淆、加密、压缩等因素。

硬编码文档

Linux可执行设计是人类易于使用,其行为取决于他们所接收的命令行参数,如改变lscatgrep等,通常是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()是为了恢复在程序执行时传递给程序的参数,而不是确定程序实际使用的选项。如示例中所示,该信息位于程序的其他位置。

由于我对您的可执行文件知之甚少,因此我会首先在您的程序中运行字符串以查看是否出现任何问题:如果二进制文件描述,则这项工作有效
(我现在被困在 Windows 上,但字符串的工作方式似乎与在 Linux 上完全相同)

{ ~ }  » strings.exe /bin/ls.exe  
...
Usage: %s [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
Mandatory arguments to long options are mandatory for short options too.
  -a, --all                  do not ignore entries starting with .
  -A, --almost-all           do not list implied . and ..
      --author               with -l, print the author of each file
  -b, --escape               print C-style escapes for nongraphic characters
      --block-size=SIZE      scale sizes by SIZE before printing them; e.g.,
                               '--block-size=M' prints sizes in units of
                               1,048,576 bytes; see SIZE format below
  -B, --ignore-backups       do not list implied entries ending with ~
...

在这种情况下,这些字符串是二进制文件的一部分,在参数无效或丢失时使用。如果失败了,你不能只用一些随机值运行它并运行ltrace(或者如果你想仔细看看,那么strace)并猜测可能的参数是什么?