使用的约定加上初步评论
为简洁起见,我正在修剪 GDB 的输出,因为它通常在每次会话开始时显示版权和其他信息。当我重现输出时,我将从第一个(gdb)
提示行开始,或者从第一个真正的输出行开始自动执行的命令。
为了区分在 GDB 提示符下输入的命令,这些命令会(gdb)
像在现实世界中一样有一个前导。对于 shell 命令,这要么根本没有前缀,要么$
就像大多数 unixoid 系统上的约定一样。
当我使用特定命令时,例如vim
我的编辑器,您当然可以随意使用自己喜欢的编辑器。无论是emacs
或nano
,我不会评判雅)
入门
本节是关于设置您的gdb
环境和启动该过程。我还将为完整的新人提供一些花絮。
你应该知道的技巧
GDB 有一个很好的提示,您的光标将在程序中断后或在您步进等时停止。
- 在运行 GDB 命令后按RETURN(aka ENTER) 将再次运行相同的命令。当您使用
step
或next
单步执行代码并且只想一一继续时,这很有用。
- 命令可以缩写,只要它们是明确的。对于一些经常使用的命令,存在一个特定的速记,尽管有歧义,但仍优先:
b
对于break
(尽管bt
和backtrace
)
c
或cont
为continue
(尽管catch
,call
等等)
n
对于next
(尽管ni
和nexti
)
- 您可以使用命令从调试程序中调用实际的库函数甚至函数
call
。这意味着您可以尝试行为或强制行为。
- 您可以使用
gdbtui
或启动 GDB,gdb -tui
以获得一个 - 据说更方便 - 更直观的文本用户界面。它在顶部显示源代码,在(gdb)
下面显示提示。您也可以通过layout src
在(gdb)
提示符处执行命令来切换到此布局。
- GDB 具有与许多 shell 非常相似的命令行完成功能,因此Tab请充分利用您的优势,并确保使用
help
或help [keyword|command]
在您需要帮助时使用。
shell
允许您在 shell 中执行命令,以便您可以从 GDB 会话中运行命令。在开发过程中,一个例子是shell make
.
print
,examine
并display
了解/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 set
和help show
。指针:set environment VARNAME=VALUE
和show environment [VARNAME]
和unset environment VARNAME
。
呼,但为什么程序会因SIGSEGV
(段错误)而停止?
好吧,我们还不知道,但看起来这个小野兽想要得到适当的治疗。既然我们练习防御性计算,我们不想运行任何我们不太了解的东西,对吧?所以让我们重新开始。如果这是恶意软件,我们必须刷新机器并重新安装或恢复快照(如果它是 VM 来宾)。
首先,我们要按info
如下方式运行命令:
(gdb) info file
观察:
有两条重要的信息,对我们来说最相关的是以下内容:
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
没有争论时,我们最终会得到这样的结果:
甜的。因此,我们可以在逐步执行代码时看到反汇编。我们可以在这里总结这一点,但当然还有更多的技巧需要学习,所以为什么不更进一步。
注意:带有白色/灰色背景的寄存器表示值已更改。当我们刚刚启动程序时没有太大意义,但在以后单步执行代码时非常有用。
顺便说一句,如果您更喜欢节省屏幕空间
...并减少视觉效果,从 GDB 7.0 开始,您可以使用:
set disassemble-next-line on
在之前的 GDB 版本上,您可以通过设置自动display
来模拟行为:
display/i $pc
或更短的格式disp/i $pc
在哪里/i
,您可以通过考虑“指令”并$pc
成为指令指针(也称为程序计数器)来最好地记住它pc
。
也很高兴知道
有时,当逐步执行装配时regs
,asm
视图会变得无聊。只需layout
再次执行相应的命令即可将它们恢复到昔日的辉煌:
(gdb) layout asm
(gdb) layout regs
装配级的“调试”
事实证明,当您处于汇编模式时,您习惯于从源代码级调试中使用的某些命令根本无法工作。这是有道理的,因为一个源代码行通常意味着十几个或更多的指令。在next
和step
命令,然而,有指令级的同行:
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
优秀的。我们现在可以执行它。
给定方法的替代方法
- 修补的替代方法:
set {unsigned int}0x40911f = 0x90909090
其次是set {unsigned char}0x409123 = 0x90
- 改为操作程序计数器(指令指针):
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 写的 - 请参阅上面的链接。
这个小问答到此结束。