如何使用 GDB 处理剥离的二进制文件?没有来源,没有符号,GDB 只显示地址?

逆向工程 工具 动态分析 linux 调试器 数据库
2021-07-03 00:10:25

我有 GDB,但我想要动态逆向工程的二进制文件没有符号,也就是说,当我运行该file实用程序时,它显示我已剥离

ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped

如果运行的环境不允许远程 IDA Pro 实例连接到,我有什么选择gdbserver简而言之:您拥有的环境在其允许您执行的操作方面受到限制,但您确实拥有可信赖的旧版本gdb和二进制文件来进行逆向工程。

1个回答

使用的约定加上初步评论

为简洁起见,我正在修剪 GDB 的输出,因为它通常在每次会话开始时显示版权和其他信息。当我重现输出时,我将从第一个(gdb)提示行开始,或者从第一个真正的输出行开始自动执行的命令。

为了区分在 GDB 提示符下输入的命令,这些命令会(gdb)像在现实世界中一样有一个前导对于 shell 命令,这要么根本没有前缀,要么$就像大多数 unixoid 系统上的约定一样。

当我使用特定命令时,例如vim我的编辑器,您当然可以随意使用自己喜欢的编辑器。无论是emacsnano,我不会评判雅)

入门

本节是关于设置您的gdb环境和启动该过程。我还将为完整的新人提供一些花絮。

你应该知道的技巧

GDB 有一个很好的提示,您的光标将在程序中断后或在您步进等时停止。

  • 在运行 GDB 命令后RETURN(aka ENTER) 将再次运行相同的命令。当您使用stepnext步执行代码并且只想一一继续时,这很有用
  • 命令可以缩写,只要它们是明确的。对于一些经常使用的命令,存在一个特定的速记,尽管有歧义,但仍优先:
    • b对于break(尽管btbacktrace
    • ccontcontinue(尽管catchcall等等)
    • n对于next(尽管ninexti
  • 您可以使用命令从调试程序中调用实际的库函数甚至函数call这意味着您可以尝试行为或强制行为。
  • 您可以使用gdbtui启动 GDB,gdb -tui以获得一个 - 据说更方便 - 更直观的文本用户界面。它在顶部显示源代码,在(gdb)下面显示提示。您也可以通过layout src(gdb)提示符执行命令来切换到此布局
  • GDB 具有与许多 shell 非常相似的命令行完成功能,因此Tab请充分利用您的优势,并确保使用helphelp [keyword|command]在您需要帮助时使用。
  • shell允许您在 shell 中执行命令,以便您可以从 GDB 会话中运行命令。在开发过程中,一个例子是shell make.
  • print,examinedisplay了解/FMT可用于使输出更具可读性的各种格式 ( )。
  • 在源代码级调试时,您可以使用 C 类型转换来显示值。想象一个在 a 后面的 C 字符串void *(由于这种情况下的符号,GDB 知道了)。只需投射到(char*)并打印它:print (char*)variable.

使进程运行

由于我们要动态分析二进制文件,因此需要先启动它。

命令行

我们可以直接从命令行通过不仅传递二进制文件的路径,而且传递我们想要启动它的参数来做到这一点。整个过程看起来是这样的:

$ gdb --args ./exe argument1 argument2

很容易。(gdb)然后,您可以提示符发出run命令(简写r)以./exe使用命令行上给出的参数运行我更喜欢这种方法,但您的里程可能会有所不同。

使用所述方法启动进程

GDB 提示

启动 GDB 并在(gdb)提示符下使用该file命令加载二进制文件,然后使用该run命令以您要传递的参数启动它:

$ gdb
(gdb) file exe
(gdb) run argument1 argument2

上面的替代方法是使用set args这样的:

$ gdb
(gdb) file exe
(gdb) set args argument1 argument2
(gdb) run

使用所述方法启动进程

您还可以在任何情况下run通过发出以下命令来查看哪些参数将传递给启动的进程:

(gdb) show args

顺便说一句:如果您想知道环境变量,请使用 GDB 的内置help命令作为help sethelp show指针:set environment VARNAME=VALUEshow environment [VARNAME]unset environment VARNAME

呼,但为什么程序会因SIGSEGV(段错误)而停止

好吧,我们还不知道,但看起来这个小野兽想要得到适当的治疗既然我们练习防御性计算,我们不想运行任何我们不太了解的东西,对吧?所以让我们重新开始。如果这是恶意软件,我们必须刷新机器并重新安装或恢复快照(如果它是 VM 来宾)。

首先,我们要按info如下方式运行命令:

(gdb) info file

观察:

GDB中的信息文件命令

有两条重要的信息,对我们来说最相关的是以下内容:

Entry point: 0x400710

好的,所以我们可以在那个断点上设置一个断点,然后run使用我们喜欢的参数进行处理。

.gdbinit 为了胜利

但是等等,这已经变得乏味了。没有简单的方法可以以某种方式自动化这些步骤吗?事实上是有的。命名文件.gdbinit可用于在启动时向 GDB 发出命令。您还可以使用-x(shell)命令行上的参数通过 GDB 命令传递文件如果我有多个项目,通常它们位于子文件夹中,.gdbinit每个子文件夹都有一个文件。

旁注: -nx防止.gdbinit在启动时执行内容。

所以我们知道我们想要传递哪些参数,我们知道断点的地址,这将转换为以下.gdbinit文件:

file exe
break *0x400710
run argument1 argument2

gdb没有任何其他参数的情况下开始时得到的输出是:

Breakpoint 1 at 0x400710

Breakpoint 1, 0x0000000000400710 in ?? ()
(gdb) 

好的!但这看起来不一样......

组装和 GDB

因此,您习惯于看到要执行的下一行,然后是可靠的旧(gdb)提示。但没有这样的事情。我们没有这个二进制和符号的来源。呸!因此,我们考虑了(gdb)提示时闪烁的插入符号并想知道该怎么做。别担心,GDB 也可以处理汇编代码。唯一的问题是,它默认为 - 在我看来 - 不方便的 AT&T 汇编语法。我更喜欢 Intel 风格,以下命令告诉 GDB 这样做:

(gdb) set disassembly-flavor intel

显示汇编代码

它将如何向我们展示汇编代码?嗯,类似于 TUI 模式(检查标签维基) 使用以下命令:

(gdb) layout asm

如果你有这样的倾向,还有:

(gdb) layout regs

它还将以概览方式向您显示寄存器的内容。

让我们再次运行它

因此,.gdbinit出于我们的目的,我们最终得到以下内容

file exe
break *0x400710
set disassembly-flavor intel
layout asm
layout regs
run argument1 argument2

当我们开始gdb没有争论时,我们最终会得到这样的结果:

带有 asm 和 regs 布局的 GDB

甜的。因此,我们可以在逐步执行代码时看到反汇编。我们可以在这里总结这一点,但当然还有更多的技巧需要学习,所以为什么不更进一步。

注意:带有白色/灰色背景的寄存器表示值已更改。当我们刚刚启动程序时没有太大意义,但在以后单步执行代码时非常有用。

顺便说一句,如果您更喜欢节省屏幕空间

...并减少视觉效果,从 GDB 7.0 开始,您可以使用:

set disassemble-next-line on

在之前的 GDB 版本上,您可以通过设置自动display来模拟行为

display/i $pc

或更短的格式disp/i $pc在哪里/i,您可以通过考虑“指令”并$pc成为指令指针(也称为程序计数器)来最好地记住它pc

也很高兴知道

有时,当逐步执行装配时regsasm视图会变得无聊。只需layout再次执行相应的命令即可将它们恢复到昔日的辉煌:

(gdb) layout asm
(gdb) layout regs

装配级的“调试”

事实证明,当您处于汇编模式时,您习惯于从源代码级调试中使用的某些命令根本无法工作。这是有道理的,因为一个源代码行通常意味着十几个或更多的指令。nextstep命令,然而,有指令级的同行

  • nexti(速记ni......还有其他人想到灌木丛吗?)
  • stepi(简写si

从上面的拆解我们知道:

0x40072d        mov    rdi,0x40f961

出于所有实际目的,这就是main功能。当然,如果您要对恶意软件进行逆向工程,您应该更加小心,但在这种情况下确实如此。所以让我们在这个地址(0x40f961)而不是入口点添加一个断点

break *0x40f961

如果我们examine(简写x)我们当前所在的代码,我们可以看到:

(gdb) x/5i $pc
x/5i $pc
=> 0x40f961:    push   rbp
   0x40f962:    mov    rbp,rsp
   0x40f965:    mov    eax,0x0
   0x40f96a:    call   0x40911f
   0x40f96f:    pop    rbp

好的,这call就是我们想要遵循的,所以让我们使用si. call进入函数时,我们会立即在指令指针处看到另一个

(gdb) x/5i $pc
x/5i $pc
=> 0x40911f:    call   0x400b8c
   0x409124:    push   rbp
   0x409125:    mov    rbp,rsp
   0x409128:    push   r10
   0x40912a:    push   r11

call我们引向一个调用 的函数ptrace(PTRACE_TRACEME, ...),现在为什么要这样做?

0x400bab        call   0x4006b8 <ptrace@plt>

嗯,这是一个旧的反调试器技巧,Mellowcandle 在另一个问答中描述过:

但是我们如何解决它呢?我们必须覆盖call调用ptrace()withnop或类似内容的函数

这就是 GDB 变得有点笨拙的地方。但是我们可以使用set如此为我们做的魔法。让我们首先检查指令字节:

(gdb) x/10b $pc
x/10b $pc
0x40911f:       0xe8    0x68    0x7a    0xff    0xff    0x55    0x48    0x89
0x409127:       0xe5    0x41

0xe8是一个调用指令,我们现在知道它有 5 个字节长。所以让我们把nop它拿出来。x/10b $pc意味着在程序计数器检查 10 个字节 - 默认格式已经是十六进制)。

所以我们做的时候停在0x40911f

(gdb) set write
(gdb) set {unsigned int}$pc = 0x90909090
(gdb) set {unsigned char}($pc+4) = 0x90
(gdb) set write off

并验证修补位置:

(gdb) x/10i $pc
x/10i $pc
=> 0x40911f:    nop
   0x409120:    nop
   0x409121:    nop
   0x409122:    nop
   0x409123:    nop
   0x409124:    push   rbp
   0x409125:    mov    rbp,rsp
   0x409128:    push   r10
   0x40912a:    push   r11
   0x40912c:    push   rbx

优秀的。我们现在可以执行它。

给定方法的替代方法

  1. 修补的替代方法:set {unsigned int}0x40911f = 0x90909090其次是set {unsigned char}0x409123 = 0x90
  2. 改为操作程序计数器(指令指针):
    • set $pc+=5 或更明确的 set $pc=$pc+5
    • jump *$pc+5

操作/修补正在运行的程序的更好方法

Tavis Ormandy 提供了一些替代方法(和更高级的方法),例如Tavis Ormandy我正在复制assemble下面宏(以防它从其他地方脱机):

define assemble
 # dont enter routine again if user hits enter
 dont-repeat
 if ($argc)
  if (*$arg0 = *$arg0)
    # check if we have a valid address by dereferencing it,
    # if we havnt, this will cause the routine to exit.
  end
  printf "Instructions will be written to %#x.\n", $arg0
 else
  printf "Instructions will be written to stdout.\n"
 end
 printf "Type instructions, one per line.\n"
 printf "End with a line saying just \"end\".\n"
 if ($argc)
  # argument specified, assemble instructions into memory
  # at address specified.
  shell nasm -f bin -o /dev/stdout /dev/stdin \
    <<< "$( echo "BITS 32"; while read -ep '>' r && test "$r" != end; \
                do echo -E "$r"; done )" | hexdump -ve \
        '1/1 "set *((unsigned char *) $arg0 + %#2_ax) = %#02x\n"' \
            > ~/.gdbassemble
  # load the file containing set instructions
  source ~/.gdbassemble
  # all done.
  shell rm -f ~/.gdbassemble
 else
  # no argument, assemble instructions to stdout
  shell nasm -f bin -o /dev/stdout /dev/stdin \
    <<< "$( echo "BITS 32"; while read -ep '>' r && test "$r" != end; \
                do echo -E "$r"; done )" | ndisasm -i -b32 /dev/stdin
 end
end
document assemble
Assemble instructions using nasm.
Type a line containing "end" to indicate the end.
If an address is specified, insert instructions at that address.
If no address is specified, assembled instructions are printed to stdout.
Use the pseudo instruction "org ADDR" to set the base address.
end

同样,上面的脚本片段不是我写的,而是由 Tavis Ormandy 写的 - 请参阅上面的链接。

这个小问答到此结束。