课程链接:https://www.bilibili.com/video/BV1r7411A7hq?vd_source=4f5979757af4551dfc8d2f504918a338
相关特性
软件调试正在逐渐地变成一门学科,其目标为使用调试器或其他工具定位软件错误的过程
重现>定位错误根源>解决>验证 的循环过程
其为一种特别的搜索问题,具备以下特点
关键词不明确
目标空间很庞大,4GB/16TB,整个计算机系统,庞杂的代码,软件的复杂化和大型化
调试比写代码困难两倍
软件调试时一项系统工程
课程目录
int 3 指令,8086引入,是软件断点的基础
追踪标志(TF),8086引入,是单步追踪技术的基础
调试寄存器(DR0~DR7),80386引入
分支监视和记录, Pentium Pro 引入,是按分支单步的基础,记录软件的执行流程(TF一次只能单步一条分支指令,会很累)
特点
举例(winmine.exe)
step 1 查看ntdll著名的函数readfile
注意打开时要使用运行可执行文件的open executable,不要直接拖入代码框,运行指令x ntdll!*readfile
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hiVK1TgQ-1655386785124)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206122035532.png)]ntdll是每个windows程序都有的著名的特殊用户态模块,nt表示windows操作系统的别名,ntdll可以表示windows内核
随后使用指令u将地址上的二进制反汇编为代码输出,即指令u ntdll!NtReadFile
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JG1aE6dh-1655386785124)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206141134639.png)]
step 2 使用bp指令设置软件断点
使用指令bp对某地址下断点,bl指令看所有断点地址
此时可以使用bl指令来查看所有断点
step 3 使用g指令运行(go)
go指令之后可以看到程序加载了一些模块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bxQG0ZVU-1655386785125)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206122137680.png)]
可以使用k指令来查看当前线程函数堆栈,查看为什么触发断点
同样可以使用u指令查看反汇编
值得注意的是,触发断点时并没有int 0x03,这里是调试器故意做的事情,先使用int 0x03代替对应的指令,触发后将替换的内容恢复回来,如果想要看可以打开另外一个windbg,使用noninvasive模式进行调试,一般情况下两个调试程序不能调试同一个程序,但是强大的windbg使用非入侵模式可以使用只读模式读另一个程序的内存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JWDCx0v-1655386785126)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206122155229.png)]
我们对同一个地址进行反汇编,发现指令int 3出现
需要注意的是,由于int 3指令只是CC一个字节,后面出现的add指令是因为未被替换的指令未对齐解析生成的结果,因为0x00正好是add指令操作符的译码结果。
硬件断点的实现
硬件断点的基础就是调试寄存器,著名的X86架构的调试寄存器(Debug Registers,简称DR)如下
DR0~DR3可以放四个线性地址,即如果需要下硬件断点,CPU将会将断点地址放在其中一个线性地址。对应DRx寄存器中线性地址的断点的长度LEN信息和读写R/W信息对应DR7中的高16位标志位,即LENx和R/Wx。
DR6用于断点地址的检测,在执行每条指令前CPU都会检测当前地址是否与DR0~DR3中的地址匹配,如果匹配则设置DR6中的标志位,然后设置异常报告给操作系统。操作系统检查标志位时就知道哪一个断点匹配命中了。
这些标志位支持4个硬件断点,因此DR4~DR5目前没有用处,理论来讲每个CPU最多可以劫持4个硬件断点地址(软件层可以理解为每个线程最多4个地址)
硬件断点特点
陷阱标志
单步陷阱标志(TF),位于Flags寄存器中
任务状态段(TSS)陷阱标志,位于每个线程的任务状态段(TSS)内
分支到分支单步执行标志(BTF),位于MSR寄存器中(P6的DebugCtlMSR,P4的DebugCtlA,Pentium M的DebugCtlB)
使用r
指令观察寄存器的值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Koo9QlnP-1655386785126)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206131520862.png)]
其中efl的bit8(pf位),如果为单步调试执行时会被置1,CPU在执行任何一条指令时如果发现efl寄存器的第八位置1,则执行一步即触发异常。注意CPU检测pf位置1会自动清pf位,因此pf位很难调试时看到置1的。
x86的终端向量表(IDT)如下,前32个是留给CPU保留使用的,目前使用了19个,后面留作用户或者特定操作系统定义。如触发断点时的int 0x03即调用异常中的3号表项breakpoint进行处理,同理单步调试对应1号表项,页故障对应14号表项
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QINmv9Cw-1655386785126)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206131526884.png)]
JTAG调试标准
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mhsNlP4P-1655386785127)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206131531373.png)]
标准适用于所有的集成电路,其基于一个边界扫描条,对每个管脚的信号进行扫描,通过控制器移位输出出去。通过上位机发送特殊信号给JTAG调试接口以测试被调试系统。X86著名的硬件调试架构叫做ITT。
1.Windows XP用户态调试模型
step 1 为了访问内存,待调试程序调用调试API
step 2 调试事件通过内核沟通一般通过类似Int 0x03指令触发异常进内核态,调用IDT内核函数
step 3 通过raise和dispatch分发异常,其会通知调试子系统
step 4 dbgk判断是否有调试器
step 5 如果有调试器,向调试器发送信息
step 6 将调试事件放入队列
step 7 调试器进程会等待队列中下一个事件
step 8 当等待完成时调试器会取出和处理事件(如断点),能看到调试界面了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hhgCUyzp-1655386785127)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206132036281.png)]
WINDOWS XP的调试模型是由调试事件驱动的,主要分为以下几类
调试事件 | 功能 |
---|---|
EXCEPTION_DEBUG_EVENT | 异常调试事件(如断点就是特殊的异常) |
CREATE_THREAD_DEBUG_EVENT | 创建线程调试事件 |
CREATE_PROCESS_DEBUG_EVENT | 创建进程调试事件 |
EXIT_THREAD_DEBUG_EVENT | 线程退出调试事件 |
EXIT_PROCESS_DEBUG_EVENT | 进程退出调试事件 |
LOAD_DLL_DEBUG_EVENT | DLL加载调试事件 |
UNLOAD_DLL_DEBUG_EVENT | DLL卸载调试事件 |
OUTPUT_DEBUG_STRING_EVENT | 输出调试信息事件 |
2.XP之前的用户态调试模型
注意其不支持分离调试会话,一旦开始调试,则调试器和被调试器"生死与共"
3.工作线程机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7cUqmdNy-1655386785128)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206132055421.png)]
4.异常的来源
CPU产生
执行指令时检测到的错误,除O,GP,无效指令
Machine Check Exceptions, 总线错误,ECC错误, Cache错误
预先埋伏的,Int 3,调试异常
程序产生
RaiseException,Win32 API
C++,throw E, 编译器会翻译为对RaiseException 调用
C#,throw,最终仍是调用RaiseException
5.理解用户态调试
step 1 打开记事本notepad.exe,将该进程attach到相应程序上
用户态进程调试的特点——当将进程attach到调试器时,进程处于freeze状态
step 2 使用~*
命令列出当前进程中的所有线程的详细信息,使用~k查看第k个子线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-snMgwxuo-1655386785128)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206132138644.png)]
使用~0
看0号线程的栈回溯,0号线程即主线程(注意要切换线程要使用~0s
,提示符会变为0:000>
,即提示符冒号后的数表示线程号)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GREtMlmJ-1655386785128)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206141134251.png)]
结合k
指令查看当前线程的堆栈
可能需要联网下载符号表,可以看到堆栈02号函数调用的notepad!wWinmain即notepad.exe的wWinMain函数,堆栈01号函数的USER32!GetMessageW表示等待消息队列的函数GetMessage是从调用线程的消息队列里取得一个消息并将其放于指定的结构。
step 3 下断点
系统下断点的方法:
注意int 3 进入内核态时,通过分发异常,调试器会把所有线程都freeze掉继续执行g
指令就可以正常运行了。
通过k
指令能看到相关函数堆栈,通过u指令看函数反汇编结果,能看到syscall指令(陷入内核态),由于这里用户态调试看不见,所以只能
使用以下指令可以在断点时继续执行多条指令,这里为执行echo指令,打印堆栈并继续
bp ntdll!NtReadFile ".echo hello from shanghai, readfile is being invoked;k;gc"
有的时候windbg在下载符号表或者继续运行速度较慢,一直显示Debugee is running,建议进行等待
step 2 sxe加载模块事件
使用指令sxe ld
加载一个ld模块,每一个模块加载时都会报告。 实现原理即触发加载模块(LOAD_DLL_DEBUG_EVENT)的内核事件,放入调试事件队列并通知调试子系统,最终报告给调试器
1.linux进程跟踪
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yf4yEVAf-1655386785129)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206141539138.png)]
2.ptrace函数
linux下面没有专门的调试事件,都是接收Signal(如Page Fault、Segmentation Fault)
ptrace相当于万能接口,其中有attach/detach进程的步骤、访问和写入代码数据、KILL进程等全部通过这一个函数完成
3.waitpid函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7W9elTh5-1655386785129)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206141546368.png)]
总体上讲,linux下的调试机制依然过于简陋,比如子进程中有多进程,操作系统没有主动记录和freeze这些进程,而是需要调试器进行进程操作
1.windows中的异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mi21DaT8-1655386785129)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206141552884.png)]
2.windows 异常分发函数详解
异常分发函数(KiDispatchException)总共分为两轮,FirstChance为第一轮分发,注意没有用户态调试器但可能有内核态调试器(KD)。
Step 1 在第一轮分发中,如果没有用户态调试器或者需要把异常分发给内核调试器的情况,将其分发给内核调试器
Step 2 如果内核调试器没有处理,则分发给用户态调试器(Dbgk相关分发函数),并执行异常后返回用户态
step 3 第二轮分发中,分别对处理普通和异常端口的情况进行分发,如果还不能分发则在内核态kill掉进程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PgBfPo48-1655386785130)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206142015075.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ky9rJAlO-1655386785130)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206151556765.png)]
以上为KiDispatchException的流程,注意没有包含所有细节,并且因版本不同会略有不同。在使用WinDBG的内核调试中,不能对这个函数设置断点——死循环,可以使用ITP来跟踪。
举例,小程序抛出的C++异常
3.寻找异常处理器
通过结构化异常处理器的FS0链条,windows的每个线程都有一个特殊链条,即段寄存器指向的特殊信息块。链条的每个节点都是一个异常的注册结构,每个异常注册结构指向handle(句柄)函数,再指向自己的一个前向指针。
结构化异常处理SEH,通过运行保护代码块,看是否触发异常,分发异常过程中处理过滤表达式,过滤表达式的返回值决定是否执行异常代码块。返回值有3中情况,如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1yyt1cwL-1655386785130)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206152026544.png)]
4.调试实例
加入try…except语句,在VC++6.0中查看汇编代码,会看到如下的部分,即先建立异常处理结构,然后再移入FS链表节点。注册了异常处理结构之后,等下发生异常就会找到异常处理结构,后面在函数的末尾会有相关的代码来反注册。
这里故意使用除0样例,并在try…catch中将被除数改为1。当触发异常时,内核态会经过两轮分发异常,第一轮内核态调试不予分发,然后转到用户态分发函数,将异常信息复制到用户态栈,找当前线程的异常处理链条(FS0链条),并且找到了相应的异常处理器SEH,在SEH中执行过滤表达式(过滤表达式可以认为是一个特殊函数,编译器会将其认为成一个特殊函数)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UIS61xH2-1655386785131)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206152040826.png)]
断点已经下在了SEH执行过滤表达式的位置,使用r
指令查看寄存器的值,使用dd
指令查看内存信息(重点查找由内核态复制到用户态的异常信息和线程上下文 )
异常结构体结构如下,第一个字段是指示异常的异常代码(这里除0异常即c0000094),后面有导致异常的地址(这里是0040108b),随后是context(线程上下文结构体),其具备典型标识符。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UMTlMStn-1655386785131)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206162103506.png)]
使用.cxr
指令回到除0错误代码,注意其转入内核态时把出错指令的地址压入栈,这也是我们知道哪条指令出错的原因,由于内核态将寄存器上下文复制到用户态,因此我们可以在用户态中看到寄存器上下文,使用指令dt _CONTEXT (address)
即使用_CONTEXT结构解析address处的数据(dt指令用来显示数据类型以及按照类型来显示数据)。Windows也公开了一些API可以从栈上取到异常代码。
当在内核态执行完SEH过滤表达式后,程序会回到用户态处理异常,如下图栈回溯,最后通过ZwContinue()函数回到用户态继续执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zP24ugdz-1655386785132)(https://cdn.jsdelivr.net/gh/YOURLEGEND/PictureBedForCSDN@main//img/202206162134519.png)]