最近的操作系统实践部分涉及到了gdb调试的内容,再者逆向题目中也有很多elf文件需要用gdb动态调试才能更好的解题。所以做一些小小的练习和学习笔记。
参考链接 http://blog.csdn.net/liigo/archive/2006/01/17/582231.aspx
GDB是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。
一般来说,GDB主要帮忙你完成下面四个方面的功能:
1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
3、当程序被停住时,可以检查此时你的程序中所发生的事。
4、动态的改变你程序的执行环境。
命令 | 解释 | 示例
—|—|—
file <文件名> | 加载被调试的可执行程序文件。因为一般都在被调试程序所在目录下执行GDB,因而文本名不需要带路径。| (gdb) file gdb-sample
r | Run的简写,运行被调试的程序。如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处。|(gdb) r
c | Continue的简写,继续执行被调试程序,直至下一个断点或程序结束。| (gdb) c
b <行号>
b<函数名称>
b*<函数名称>
b *<代码地址>d [编号] |b: Breakpoint的简写,设置断点。两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置。其中在函数名称前面加“*”符号表示将断点设置在“由编译器生成的prolog代码处”。如果不了解汇编,可以不予理会此用法。
d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。| (gdb) b 8
(gdb) b main
(gdb) b *main
(gdb)b *0x804835c
(gdb) d
s, n | s: 执行一行源程序代码,如果此行代码中有函数调用,则进入该函数;
n:执行一行源程序代码,此行代码中的函数调用也一并执行。
s 相当于其它调试器中的“Step Into (单步跟踪进入)”;
n 相当于其它调试器中的“Step Over (单步跟踪)”。这两个命令必须在有源代码调试信息的情况下才可以使用(GCC编译时使用“-g”参数)。| (gdb) s
(gdb) n
si,
ni | si命令类似于s命令,ni命令类似于n命令。所不同的是,这两个命令(si/ni)所针对的是汇编指令,而s/n针对的是源代码。| (gdb) si
(gdb) ni
p <变量名称> |Print的简写,显示指定变量(临时变量或全局变量)的值。| (gdb) p i(gdb) p nGlobalVar
display
...undisplay <编号> |display,设置程序中断后欲显示的数据及其格式。例如,如果希望每次程序中断后可以看到即将被执行的下一条汇编指令,可以使用命令“display /i $pc”其中 $pc代表当前汇编指令,/i表示以十六进行显示。当需要关心汇编代码时,此命令相当有用。undispaly,取消先前的display设置,编号从1开始递增。| (gdb) display /i $pc
(gdb) undisplay 1
i | Info的简写,用于显示各类信息,详情请查阅“help i”。| (gdb) i r
q | Quit的简写,退出GDB调试环境。|(gdb) q
help [命令名称] |GDB帮助命令,提供对GDB名种命令的解释说明。如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。| (gdb) help display编号>变量名称>代码地址>函数名称>函数名称>行号>文件名>
参考书籍《linux c编程一站式学习》
#include <stdio.h>
int main(void)
{
int sum = 0, i = 0;
char input[5];
while (1) {
scanf("%s", input);
for (i = 0; input[i] != '\0'; i++)
sum = sum*10 + input[i] - '0';
printf("input=%d\n", sum);
}
return 0;
}
程序的功能显而易见:先从键盘读入一串数字存到字符数组input中,然后转换成整型存到sum中,然后打印出来,一直这样循环下去。
下面进行编译调试看看是否有bug。
fangmu@fangmu-7:~/practice$ gcc -g simple.c -o simple
首先使用gcc将代码编译,要加上-g选项,生成的可执行文件才能用gdb进行源码级调试。
fangmu@fangmu-7:~/practice$ ./simple
666
input=666
777
input=666777
执行程序,发现只有第一次的输出结果是正确的,之后的循环就出错了。为了寻找原因开始调试。(虽然这个错误很低级,但是我也犯过好几次了……主要是体会调试过程……)
fangmu@fangmu-7:~/practice$ gdb simple
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.3) 7.7.1
Copyright (C) 2014 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.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
首先 gdb+文件名进入调试状态,然后start(第一次调试,把整体的效果和内容全都展示出来,以后就不一一展示了)
gef➤ start
[+] Breaking at '{int (void)} 0x40059d <main>'
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────[ registers ]────
$rax : 0x000000000040059d → <main+0> push rbp
$rbx : 0x0000000000000000
$rcx : 0x0000000000000000
$rdx : 0x00007fffffffdfe8 → 0x00007fffffffe363 → "XDG_VTNR=7"
$rsp : 0x00007fffffffded0 → 0x0000000000400630 → <__libc_csu_init+0> push r15
$rbp : 0x00007fffffffdef0 → 0x0000000000000000
$rsi : 0x00007fffffffdfd8 → 0x00007fffffffe346 → "/home/fangmu/practice/simple"
$rdi : 0x0000000000000001
$rip : 0x00000000004005a5 → <main+8> mov rax, QWORD PTR fs:0x28
$r8 : 0x00007ffff7dd4e80 → 0x0000000000000000
$r9 : 0x00007ffff7dea700 → <_dl_fini+0> push rbp
$r10 : 0x00007fffffffdd80 → 0x0000000000000000
$r11 : 0x00007ffff7a32e50 → <__libc_start_main+0> push r14
$r12 : 0x00000000004004b0 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdfd0 → 0x0000000000000001
$r14 : 0x0000000000000000
$r15 : 0x0000000000000000
$eflags: [carry parity adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
$ss: 0x002b $gs: 0x0000 $cs: 0x0033 $ds: 0x0000 $es: 0x0000 $fs: 0x0000
─────────────────────────────────────────────────────────────────[ stack ]────
0x00007fffffffded0│+0x00: 0x0000000000400630 → <__libc_csu_init+0> push r15 ← $rsp
0x00007fffffffded8│+0x08: 0x00000000004004b0 → <_start+0> xor ebp, ebp
0x00007fffffffdee0│+0x10: 0x00007fffffffdfd0 → 0x0000000000000001
0x00007fffffffdee8│+0x18: 0x0000000000000000
0x00007fffffffdef0│+0x20: 0x0000000000000000 ← $rbp
0x00007fffffffdef8│+0x28: 0x00007ffff7a32f45 → <__libc_start_main+245> mov edi, eax
0x00007fffffffdf00│+0x30: 0x0000000000000000
0x00007fffffffdf08│+0x38: 0x00007fffffffdfd8 → 0x00007fffffffe346 → "/home/fangmu/practice/simple"
──────────────────────────────────────────────────────[ code:i386:x86-64 ]────
0x40059c <frame_dummy+44> call QWORD PTR [rbp+0x48]
0x40059f <main+2> mov ebp, esp
0x4005a1 <main+4> sub rsp, 0x20
→ 0x4005a5 <main+8> mov rax, QWORD PTR fs:0x28
0x4005ae <main+17> mov QWORD PTR [rbp-0x8], rax
0x4005b2 <main+21> xor eax, eax
0x4005b4 <main+23> mov DWORD PTR [rbp-0x18], 0x0
0x4005bb <main+30> mov DWORD PTR [rbp-0x14], 0x0
0x4005c2 <main+37> lea rax, [rbp-0x10]
─────────────────────────────────────────────────────[ source:simple.c+4 ]────
1 #include <stdio.h>
2
3 int main(void)
→ 4 {
5 int sum = 0, i = 0;
6 char input[5];
7
8 while (1) {
9 scanf("%s", input);
───────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "simple", stopped, reason: BREAKPOINT
─────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x4005a5 → Name: main()
──────────────────────────────────────────────────────────────────────────────
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────[ registers ]────
$rax : 0x000000000040059d → <main+0> push rbp
$rbx : 0x0000000000000000
$rcx : 0x0000000000000000
$rdx : 0x00007fffffffdfe8 → 0x00007fffffffe363 → "XDG_VTNR=7"
$rsp : 0x00007fffffffded0 → 0x0000000000400630 → <__libc_csu_init+0> push r15
$rbp : 0x00007fffffffdef0 → 0x0000000000000000
$rsi : 0x00007fffffffdfd8 → 0x00007fffffffe346 → "/home/fangmu/practice/simple"
$rdi : 0x0000000000000001
$rip : 0x00000000004005a5 → <main+8> mov rax, QWORD PTR fs:0x28
$r8 : 0x00007ffff7dd4e80 → 0x0000000000000000
$r9 : 0x00007ffff7dea700 → <_dl_fini+0> push rbp
$r10 : 0x00007fffffffdd80 → 0x0000000000000000
$r11 : 0x00007ffff7a32e50 → <__libc_start_main+0> push r14
$r12 : 0x00000000004004b0 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdfd0 → 0x0000000000000001
$r14 : 0x0000000000000000
$r15 : 0x0000000000000000
$eflags: [carry parity adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
$ss: 0x002b $gs: 0x0000 $cs: 0x0033 $ds: 0x0000 $es: 0x0000 $fs: 0x0000
─────────────────────────────────────────────────────────────────[ stack ]────
0x00007fffffffded0│+0x00: 0x0000000000400630 → <__libc_csu_init+0> push r15 ← $rsp
0x00007fffffffded8│+0x08: 0x00000000004004b0 → <_start+0> xor ebp, ebp
0x00007fffffffdee0│+0x10: 0x00007fffffffdfd0 → 0x0000000000000001
0x00007fffffffdee8│+0x18: 0x0000000000000000
0x00007fffffffdef0│+0x20: 0x0000000000000000 ← $rbp
0x00007fffffffdef8│+0x28: 0x00007ffff7a32f45 → <__libc_start_main+245> mov edi, eax
0x00007fffffffdf00│+0x30: 0x0000000000000000
0x00007fffffffdf08│+0x38: 0x00007fffffffdfd8 → 0x00007fffffffe346 → "/home/fangmu/practice/simple"
──────────────────────────────────────────────────────[ code:i386:x86-64 ]────
0x40059c <frame_dummy+44> call QWORD PTR [rbp+0x48]
0x40059f <main+2> mov ebp, esp
0x4005a1 <main+4> sub rsp, 0x20
→ 0x4005a5 <main+8> mov rax, QWORD PTR fs:0x28
0x4005ae <main+17> mov QWORD PTR [rbp-0x8], rax
0x4005b2 <main+21> xor eax, eax
0x4005b4 <main+23> mov DWORD PTR [rbp-0x18], 0x0
0x4005bb <main+30> mov DWORD PTR [rbp-0x14], 0x0
0x4005c2 <main+37> lea rax, [rbp-0x10]
─────────────────────────────────────────────────────[ source:simple.c+4 ]────
1 #include <stdio.h>
2
3 int main(void)
→ 4 {
5 int sum = 0, i = 0;
6 char input[5];
7
8 while (1) {
9 scanf("%s", input);
───────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "simple", stopped, reason: BREAKPOINT
─────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x4005a5 → Name: main()
关注可能出现问题的变量sum。使用display命令使得每次停下来的时候都显示当前sum的值,然后继续往下走
gef➤ display sum
1: sum = 0x4004b0
gef➤ n
5 int sum = 0, i = 0;
1: sum = 0x4004b0
gef➤ n
9 scanf("%s", input);
1: sum = 0x0
gef➤ n
666
10 for (i = 0; input[i] != '\0'; i++)
1: sum = 0x0
单步走循环比较繁琐,可以直接在第九行加一个断点。
gef➤ b 9
Breakpoint 1 at 0x4005c2: file simple.c, line 9.
break命令的参数也可以是函数名,表示在某个函数开头设断点。现在用continue命令(简写为c)连续运行而非单步运行,程序到达断点会自动停下来,这样就可以停在下一次循环的开头。
gef➤ c
Continuing.
666
input=666
Breakpoint 1, main () at simple.c:9
9 scanf("%s", input);
1: sum = 0x29a
然后输入新的字符串准备转换
gef➤ n
777
10 for (i = 0; input[i] != '\0'; i++)
1: sum = 0x29a
此时就发现了问题所在,此时sum并没有清零,而是保留着上一次的结果,这样就把上次的结果也加进来了。
关于断点的操作很灵活,简单学习几个。
gef➤ delete breakpoints 2
gef➤ i breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004005c2 in main at simple.c:9
breakpoint already hit 2 times
gef➤ disable breakpoints 1
gef➤ i breakpoints
Num Type Disp Enb Address What
1 breakpoint keep n 0x00000000004005c2 in main at simple.c:9
gef➤ enable breakpoints 1
gef➤ i breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004005c2 in main at simple.c:9
gef➤ break 9 if sum != 0
Note: breakpoint 1 also set at pc 0x4005c2.
Breakpoint 3 at 0x4005c2: file simple.c, line 9.
gef➤ i breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004005c2 in main at simple.c:9
breakpoint already hit 2 times
3 breakpoint keep y 0x00000000004005c2 in main at simple.c:9
stop only if sum != 0
gef➤ r
Starting program: /home/fangmu/practice/simple
Breakpoint 1, main () at simple.c:9
9 scanf("%s", input);
1: sum = 0x0
显然第一次并没有中断,第二次中断了。
fangmu@fangmu-7:~/practice$ file keylead
keylead: XZ compressed data
fangmu@fangmu-7:~/practice$ xz -d keylead.xz
xz: keylead.xz: 没有那个文件或目录
fangmu@fangmu-7:~/practice$ mv keylead keylead.xz
fangmu@fangmu-7:~/practice$ xz -d keylead.xz
查看文件类型,xz压缩包,所以加个后缀将其解压。
fangmu@fangmu-7:~/practice$ ./keylead
bash: ./keylead: 权限不够
fangmu@fangmu-7:~/practice$ sudo su
[sudo] password for fangmu:
root@fangmu-7:/home/fangmu/practice# ./keylead
bash: ./keylead: 权限不够
root@fangmu-7:/home/fangmu/practice# chmod +x keylead
运行总是提示权限不够,使用chmod +x命令获得执行的权限。
root@fangmu-7:/home/fangmu/practice# chmod +x keylead
root@fangmu-7:/home/fangmu/practice# ./keylead
hi all ----------------------
Welcome to dice game!
You have to roll 5 dices and get 3, 1, 3, 3, 7 in order.
Press enter to roll.
You rolled 5, 4, 5, 4, 4.
You DID NOT roll as I said!
Bye bye~
运行后得知这是一个所谓的骰子游戏,需要掷出3 1 3 3 7这样的序列才可以。这个题目很眼熟了,bin100(ebCTF 2013)那道题目也是这样的数字序列,只不过那个是exe文件,可以直接用OD动态调试,把跳转的地方NOP掉。那么elf文件也不用怕,因为GDB同样可以动态调试。首先还是用IDA看一下逻辑结构。
首先是start部分
.text:00000000004005C0 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004005C0 ; __unwind {
.text:00000000004005C0 xor ebp, ebp
.text:00000000004005C2 mov r9, rdx ; rtld_fini
.text:00000000004005C5 pop rsi ; argc
.text:00000000004005C6 mov rdx, rsp ; ubp_av
.text:00000000004005C9 and rsp, 0FFFFFFFFFFFFFFF0h
.text:00000000004005CD push rax
.text:00000000004005CE push rsp ; stack_end
.text:00000000004005CF mov r8, offset fini ; fini
.text:00000000004005D6 mov rcx, offset init ; init
.text:00000000004005DD mov rdi, offset main ; main
.text:00000000004005E4 call ___libc_start_main
主函数里的逻辑结构和想象中的差不多,就是每次一个随机数,判断和规定的数字是否相等,还加了个时间验证防止作弊。最终符合序列的话,会进入这个函数 sub_4006B6(),flag就从这个函数里生成并输出。
接下来思路很明显,就是修改判断相等跳转的地方。可以像之前那个题目一样,把每次判断的跳转都挨个nop掉,这样就可以输出flag。但是这里可以更简单一些,start部分里
.text:00000000004005CF mov r8, offset fini ; fini
.text:00000000004005D6 mov rcx, offset init ; init
.text:00000000004005DD mov rdi, offset main ; main
.text:00000000004005E4 call ___libc_start_main
这里是把fini、init函数和main函数的偏移分别存到r8、rcx和rdi这两个寄存器里了,而根据以前积累的知识我们可以知道linux下程序执行的过程
_start -> __libc_start_main -> main. 具体一点就是:
_start -> __libc_start_main -> __libc_csu_init -> main. 再具体一点就是:
_start -> __libc_start_main -> __libc_csu_init -> _init -> main -> _fini.
所以我们可以直接把rdi里存的main函数地址替换成输出flag的sub_4006B6()函数,甚至可以把rcx里储存的执行在main函数之前的init地址替换成 sub_4006B6()函数。下面演示下替换效果。
(gdb) b *0x4005d6 //把init存到rcx这条命令的地址
Breakpoint 1 at 0x4005d6
(gdb) r //运行
Starting program: /home/fangmu/practice/keylead
Breakpoint 1, 0x00000000004005d6 in ?? ()
(gdb) i r //查看寄存器状态
rax 0x1c 28
rbx 0x0 0
rcx 0x7ffff7ffe758 140737354131288
rdx 0x7fffffffe628 140737488348712
rsi 0x1 1
rdi 0x7ffff7ffe1c8 140737354129864
rbp 0x0 0x0
rsp 0x7fffffffe610 0x7fffffffe610
r8 0x401180 4198784
r9 0x7ffff7dea700 140737351952128
r10 0x16 22
r11 0x1 1
r12 0x4005c0 4195776
r13 0x7fffffffe620 140737488348704
r14 0x0 0
r15 0x0 0
rip 0x4005d6 0x4005d6
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
---Type <return> to continue, or q <return> to quit---
然后单步执行一下,再查看寄存器的状态
gef➤ i r
rax 0x1c 0x1c
rbx 0x0 0x0
rcx 0x401110 0x401110
此时rcx已经存上init函数的地址了,把它修改成4006b6函数。
gef➤ set $rcx=0x4006b6
gef➤ c
Continuing.
ASIS{1fc1089e328eaf737c882ca0b10fcfe6}hi all ----------------------
Welcome to dice game!
You have to roll 5 dices and get 3, 1, 3, 3, 7 in order.
Press enter to roll.
执行即可得到flag。修改rci寄存器的操作同理不再赘述。