banner
NEWS LETTER

PWN题GDB调试指南

Scroll down

PWN题GDB调试指南

开始学习pwn的时候,总会因为自己找不到讲的详细的文章而烦恼,后来看了学长写了一篇文章,我受益良多。一篇好的文章能够给我们带来许多的收获,或许从那开始就对pwn顿悟了呢,也说不定。作者虽然笔力不太好,但是从学长那继承了这个系列,尽自己最大的努力去完善这个系列,所以开始更新这个《从0到有pwn学习系列》<_>

作者废话:

对于pwn题来说,调试是一个入门的关卡,会调试的pwn学者才算真正的入门pwn,但调试并不是一种技巧或一种理论并不是一蹴而就或者幡然醒悟,他是我们在学习pwn过程中将我们所学到的理论进行实践并积累经验的过程,你会在调试的过程中发现你学到的哪些知识(例如:栈溢出、shellcode、栈迁移等等)是如何实现的,并感叹前辈们设计出如此精妙的手法进行攻击,在漫漫的学习历程中你会一边觉得调试很无聊总是对着一堆代码进行调试;但同时你又会感觉很有趣,你利用自己一点点构造的程序输入,一点点将程序腐蚀达到了自己想要的效果,像攻城一样,看似坚不可摧的程序其实也不堪一击,pwn的枯燥在于此,pwn的乐趣也在于此,初学者莫要丧气,多尝试多思考。

文中将包含常用gdb调试指令,以及基础题型调试的关注重点和注意事项

该文章如果需要会持续更新

前言:

GDB是linux下非常好用且强大的调试工具,能够使让用户在程序运行的过程中观察内部结构与内存的使用情况,掌握gdb对于二进制pwn手的学习是入门的要求。

GDB在pwn中主要帮助你完四个方面:

1
2
3
4
1.启动程序,按照我们定义的输入进行运行程序(比如exp)
2.可以让被调试的程序停止在我们想要观察的地方(下断点)
3.动态的分析各个程序的走向、寄存器的变化、内存的变化
4.观察程序是否按照自己的预期(exp)所运行从而去调整

基本指令的掌握

gdb中的指令其实有很多很多,但我们只需要记住常用的,不要去死记硬背,在调试中慢慢就熟练了,对于不常用的可以做个笔记保存下来(这些是我平时比较常用的,其他可以以后在补充)

也可以看这个文章学习更多

GDB调试命令大全-CSDN博客

1730896346272

首先启动程序gdb

1
2
gdb 目标二进制程序
示例gdb text1

1730894068788

进入gdb模式

1730894082170

以下在gdb状态下(介绍常用)

1.直接运行程序 r

不会进入调试界面,跟直接运行程序差不多

1730894176421

2.单补执行运行程序,停在第一执行语句 start

会进入调试的界面

1730894252150

3.单步调试next(简写n)

每进行一次next的话,程序就会对应走一步(c语言级的断点定位)

1730894593005

1730894608909

每进行一次ni,程序就会对应走一步(汇编级别的断点定位)

1730894688235

1730894695211

你就明白,要想走的精细一点(慢一点)就用ni,走的快一点用n;比较建议用ni,因为更好观察程序结构与内存的变化

4.单步调试step(简写s)

s与si的区别同n与ni

s与n和si和ni的区别就在于s和s会进入函数的内部

1730894880528

我们用s,进入了函数的内部,更精确的调试

1730894888552

用n的话不会进入函数的内部

1730894918984

5.结束当前函数finish(简写fini)

与s相反,s是进入函数,finish是跳出这个函数

此时在puts函数内部

1730895076428

进行执行finish,跳出函数

1730895096393

6.设定程序在运行中停止的位置break(简写b)

1730896686246

1730896702976

下断点

1730896728774

如何c过去就会停在那个位置

1730896750990

7.继续运行程序continue/c

可以跳到断点处如上

在c让程序继续进行

1730896819345

8.查看栈中的内容stack

1730896921159

9.查看地址中的内容以及符号表

例如栈中的

1730896997512

用tele查看对应的内容

1730897022425

或者说看1730897034552的内容

1730897049044

10.打印值以及地址print(简写p)

1730897074549

前置知识的简略介绍我就到这里了,因为我这篇文章主要是针对题目的调试,网上有对gdb知识以及调试指令的文章很多也都很好,可以多去看看

gdb指令更加详细的分类与介绍https://www.cnblogs.com/ve1kcon/p/17812420.html

在gdb调试中一般要注意三个区域

区域1:当前状态下各个寄存器中存储的值(我称之为寄存器区)

区域2:当前执行步骤所执行的汇编指令以及该指令附近的汇编语句(我称为程序汇编区)

区域3:从rsp往下部分栈中的情况(我称为栈区)

1731033179021


介绍如何调试pwn的exp或payload

gdb.attach()

这是pwntools用于在exp脚本方便调试的部分

1
2
context.terminal = ["tmux","splitw","-h"]#产生左右分屏,不带的话是上下分屏比较难受
gdb.attach() #放到想要在exp中停止的位置,程序在运行时候会停止在该处进行调试

1730964743637

运行脚本得到

1730964775079

注意:要在tmux模式下才可以

安装 tmux

更新软件包列表:

打开终端并运行以下命令,以确保你的包管理器是最新的:

1
sudo apt update
安装 tmux:

使用以下命令安装 tmux

1
sudo apt install tmux
验证安装:

安装完成后,可以通过以下命令检查 tmux 的版本,以确认安装成功:

1
tmux -V

1730964965065

启动 tmux

直接终端输入tmux

1
tmux

但是我是不用gdb.attach的,因为毕竟只是pwntools的函数,很多大佬都有自己的函数库将其进行了修饰以及添加各种功能

我一直在用我学长的函数库

tools库

功能很多不在此介绍,本文主要讲述gdb调试,感兴趣的可以自行了解

https://zikh26.github.io/posts/ad411136.html#debug

稍微介绍一下这个库

1730983290919

调试的时候扔要在tmux模式下进行,只支撑python3运行

想要调试的时候

1
python3 exp

1730983426808

会进入分三屏的操作

当不想用调试模式的时候,无需进入exp中将debug(p)注释

只需要

1
python3 exp 2

就可以直接运行程序

1730983513568

你可以把想要下的断点直接下在exp中debug中

例如

1731058732386


接下来讲述本文正题

ret2text调试

前言:

ret2text为pwn中最基本的入门题,一般都会给出后门函数,主要注重的是对栈溢出的理解,当读入的内容大小超过读入位置(栈上)设定的内存大小就会造成栈溢出,导致覆盖到返回地址

(简略:只针对于ret2text的话,你读入能覆盖到rbp下方就行,哪里就是返回地址)

具备知识:

1.栈溢出

2.寄存器(重点是rsp栈顶寄存器,rbp栈底寄存器)

3.汇编指令(建议理解好leave,ret的含义以及函数中参数对应的意义)

4.栈对齐(执行system(“/bin/sh”)有时会因为没有栈对齐而导致无法通过,需要在返回地址处先进行一次ret在跳到后门函数)

调试注意点:

1.读入位置(距离栈底rbp的偏移)

2.覆盖栈的情况

3.是否覆盖到返回地址(rbp的下方)

4.调试后门函数是否需要栈对齐

例题:buuctf 中的rip

漏洞的重点

1730897643602

gets函数在遇到回车符\n之前是不限制读入的,所以存在栈溢出

1.然后第一步查看读入位置(距离栈底rbp的距离)

可以在ida中粗略的看一下,但建议在gdb中好好看看,增加理解,后期的对于栈溢出的话直接看ida差不多就行

在ida中看

1730984400584

在gdb中看

因为该题的话只有一次读入,用exp中的调试(gdb.attach)会直接进入到读入函数

1730984673390

而不太好看读入位置

我直接用gdb启动了,然后下断点

先找到我们要查看的函数

1730984708556

在ida中用tab键和空格找到这个位置,这个地址就是我们要停在的位置

1730984746727

然后启动程序(后续不会在写这一步了)

1
2
gdb 目标程序
start

下断点到gets函数利用 b *0x401162(记得加个0x转化为16进制,ida那个地址是16进制的)

1730984876418

其实也可以直接用b gets去定位,但是会进入函数中,该题不建议

下好断点直接c过去就行,下面是c过去的效果,正好停在了我们想要看的函数

1730985051126

每个函数的参数要求是不同的,对于gets函数的话rdi的位置存储的是读入位置

1730985288366

stack 30一下,我们算一下读入位置距离rbp的大小

1731032925306

当然可以直接看rbp寄存器(寄存器区),但我觉得直接看栈更形象

1731033052671

利用x/gx 地址1-地址2(一般地址1为大地址)计算偏移

x/gx该指令可以记住,有其他计算指令,我挺喜欢用的

1731033041338

1731033490033

计算得到偏移0xf,与ida中查看的一样,所以我们一般看读入位置与rbp的偏移可以直接在ida中看

1731033552832

1731033626333

现在完成第一步了,我写的比较详细,便于新生理解

简略:对于ret2text来说,直接看ida中距离rbp的偏移即可找到需要读入的大小
2.覆盖栈的情况

我们已经找到偏移大小了,那么我们就可以读入内容覆盖返回地址了(偏移为0xf)

我们先读入0xf的内容看覆盖栈的效果

读入前

1731034066389

读入0xf(15)个a后(0x61对应的acill码代表a)

马上覆盖到rbp了

1731034355154

用x/8gx 看字节更加详细,现在马上到rbp,如果我们在多读一个字节呢

1731034330679

1731034486225

读入了0x10(16)个a后,rbp被写入了一个字节

1731034609326

1731034635785

现在我们就可以填充rbp

记住那个偏移0xf是到rbp的位置,你的目标是rbp下方,所以填充rbp

而64位程序下rbp是8个字节,32位程序下ebp是4个字节(32位程序下栈底寄存器是ebp)

所以你想要覆盖到返回地址就要
$$
覆盖rbp大小=距离rbp(或ebp)的偏移+rbp(或ebp)的大小
$$

$$
覆盖返回地址大小=覆盖rbp大小+控制返回地址的大小
$$

读入覆盖rbp的大小,查看覆盖rbp效果

1731034890211

读入后,现在已经把rbp覆盖了,在往后读就是返回地址了

1731035070774

1731035123127

3.查看是否覆盖了返回地址

在读入覆盖返回地址

1731036246981

fun的地址希望你们已经会找了

1731036270357

读入后,就把rbp下方覆盖掉了(返回地址)

1731036384027

1731036402726

覆盖返回地址后,查看一下运行的效果

(你要记住,这个在return处查看只是对于最基本的ret2text,你要多思考为啥会在这里进行返回,这不是一种固定思维,你要多思考,建议把leave ret搞清楚,以后的题并不是都是填充在rbp下面就是返回地址的)

1731057442074

跳到断点处,其实你也可以直接ni过去查看程序走向就行

覆盖返回地址前的效果

1731059056822

覆盖后,你会发现后续的程序走向已经被改变,改变成我们刚刚覆盖的返回地址(后门函数的位置)

1731058966202

1731059178719

1731059228538

好了,不要以为这就结束了,你别忘了还有第四步,调试返回地址后程序能否正常执行完会不会卡住,是否需要栈对齐

4.查看执行system(‘/bin/sh’)是否需要栈对齐

现在的情况是吧返回地址覆盖为后门函数后

依旧跳到刚刚那个位置

1731059750132

然后用ni一步步执行汇编

查看程序执行流的情况,会不会卡在哪里

执行到这里按道理来说就要获得shell(得到目标权限)了,system(‘/bin/sh’)参数也正确,但是我们在ni一下你会发现

1731059848427

程序卡在了这个汇编指令(xmm 寄存器要求内存地址对齐,对齐由内存位数决定,本文中为16字节对齐。)

1731059922071

对于这种情况的话我们不能直接执行system(‘/bin/sh’)

我们要现执行一次ret进行栈对齐

ret可以直接去ida中

1731061114003

也可以用ROPgadget –binary 程序 | grep ‘ret’

1731061194779

稍微改动一下exp

1731060932986

这样你在尝试调试一下你就会发现可以绕过那个命令,具体想要搞清楚为什么,去学学栈对齐

1731061033448

然后你就会发现程序可以正常执行完了

这就是大致ret2text的调试过程,如有什么问题后续会在补充

1731061285569

最终的exp

1
2
3
4
5
6
7
8
9
10
from tools import *
context(arch='amd64', os='linux', log_level='debug')
p=process("./text1")
fun=0x401186
ret=0x401185
payload=b'a'*(0xf+8)+p64(ret)+p64(fun)
debug(p,0x040117F)

p.sendlineafter("please input",payload)
p.interactive()

转载自http://example.com/2024/11/06/GDB调试指南/

I'm so cute. Please give me money.

其他文章
目录导航 置顶
  1. 1. PWN题GDB调试指南
    1. 1.0.1. 作者废话:
  2. 1.1. 前言:
    1. 1.1.1. GDB在pwn中主要帮助你完四个方面:
  3. 1.2. 基本指令的掌握
    1. 1.2.1. 进入gdb模式
    2. 1.2.2. 以下在gdb状态下(介绍常用)
  4. 1.3. 介绍如何调试pwn的exp或payload
  • 2. ret2text调试
    1. 2.0.1. 前言: