n1cef1sh's Blog

写在前面

最近的操作系统实践部分涉及到了gdb调试的内容,再者逆向题目中也有很多elf文件需要用gdb动态调试才能更好的解题。所以做一些小小的练习和学习笔记。

关于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

简单C语言的调试

参考书籍《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

显然第一次并没有中断,第二次中断了。

实战逆向题目(Keylead ASIS CTF 2015)

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寄存器的操作同理不再赘述。