windbg主要厉害的地方是在他可以对内核调试
并且本身微软的产品 对windows调试适配度够高
注意 windbg给出的图形操作并不好用 主要是使用命令行来进行操作
我们省略安装
直接进入调试
file
可以打开软件 可以附加
也可以分析dump文件
还可以进行内核和 远程调试
内核调试分为5种
NET USB 1394 COM和本地调试
前面四种是双机调试模式
附加进程的非入侵模式调试 dump文件调试和本地内核调试都是属于非实时调试模式
不能直接控制被调试目标的中断和运行
一般是用来观察的
也可以用来修改内存数据
1.开始调试
Ctrl+E 打开程序
F6可以附加调试
在windbg中反汇编代码默认停止在 ntdll.dll 的系统断点处
并不会停在程序入口处
我们需要在命令窗口 输入
g@$exentery 转到程序入口
2.这里给出目标的执行命令
命令 | 快捷键 | 功能 |
t | F8/F11 | 跟踪执行,进入call |
p | F10 | 单步执行,不进入call |
g | F5 | 运行程序 |
pa 地址 | 单步到指定地址 并且不进入call | |
ta 地址 | 追踪到指定地址 并且进入call | |
pc [count] | 单步执行到下一个call调用 | |
tc [count] | 追踪到下一个call调用,遇到call进行跟进 | |
tb [count] | 追踪到下一条分支指令 遇到call进行跟进 【只适用于内核调试】 | |
pt | 单步执行到下一条call的返回 | |
tt | 追踪到下一条call的返回,并且遇到call进行跟进 | |
ph | 单步执行到下一条分支指令 | |
th | 追踪执行到下一条分支指令,遇到call进行跟踪 | |
wt | 自动追踪函数执行过程 |
$ra代表当前函数的的返回地址
所以使用
“pa @$ra”
来走出当前函数
pc和tc都是执行到下一个call指令
count用于指定 遇到call的个数
默认是1
如果count为1 pc和tc这两个指令是等价的
3.这里给出的是断点指令
1.软件断点
bp断点
bp[ID] [Options] [Address [Passes]] ["CommandString"]
ID : 指定断点
options:
/l :一次性断点
/c :指定最大调用深度 大于这个深度断点不工作
/C :指定最小调用深度 小于这个深度断点不工作
Address:
地址或符号 例如 MessageBoxW
Passes:忽略中断的次数
CommandString : 指定一组命令 当断点中断的时候 自动执行这些命令
用双引号来包裹指令 分号来区分多指令
bu断点
bu用于对某一个符号断点
bu kernel32!GetVersiono
和bp的区别:
bu是和符号关联 如果符号的地址改变了
断点会保持和源符号的关联
bp是和地址的关联 如果模块把地址的指令转移到其他地址
断点不会移动 依然是在原地址
并且 bu会保存在 windbg的工作空间 下次启动自动断点
bm断点
bm断点是支持一次性创建多个bp或bu断点的指令
例如 对 msvcr80d模块的 print开头的函数都进行断点
bm msvcr80d!print*
实例
TraceMe.exe
自动在命令行处进行加载
我们设置断点
bp kernel32!GetVersion
然后使用g来运行程序
2.硬件断点
硬件断点可以实现 监视I/O访问等功能 这些是软件断点无法实现的
ba
ba[ID] Access Size [Options] [Address [Passes]] ["CommandString"]
ID : 指定断点
Access : 指定断点触发断点的访问方式
e :在读取指令或执行指令的时候触发断点
r :在读取指令的时候触发断点
w :在写入数据的时候触发断点
i :在执行输入/输出访问(I/O)触发断点
Size :
访问的长度
在X86可以为 1,2,4 代表 字节 字 双字
在X64多了一个 8 8字节访问
Address : 断点的地址 地址值按Size的值进行内存对齐
Passes和CommandString
和软件断点用法一样
3.条件断点
软件断点和硬件断点都支持 条件断点
断点触发后 WinDbg会执行一些自定义的判断 并且执行命令
bp|bu|bm|ba _Adddress "j (Condition) 'OptionalCommands';'gc'"
bp|bu|bm|ba _Adddress ".if (Condition) {OptionalCommands} .else {gc}"
例子
bp kernel32!GetVersion ".if(@eax=0x12ffc4){}.else{gc}"
当 GetVersion调用的时候 检测 eax
如果值为 0x12ffc4就中断
否则 gc指令继续执行
但是当值为 0x12ffc4的时候 不一定能断下
因为在内核态 MASM会对 eax进行符号扩展
0x12ffc4会变为 0xFFFFFFFFc012ffc4
这个时候就可以使用 & 把eax的高位清零
bp kernel32!GetVersion ".if(@eax& 0x0`ffffffff)=0xc012ffc4{}.else{gc}"
例子2
在不中断的情况下 打印 CreateFileA的函数调用
bp kernel32!CreateFileA ".echo;.printf\"CreateFileA(%ma,%p,%p),ret=\",poi(esp+4),dwo(esp+8),dwo(esp+c);gu;.printf\"%N\",eaxl.echo;g"
4.管理断点
bl
bl 列出断点
bc bd be 来删除 禁止 启动断点
bd 1-3,4 禁止 1234断点
bc * 删除所有断点
4.栈窗口
栈是观测函数调用的重要调试手段
因为 call指令会把返回地址记录在栈中
我们就可以通过遍历栈帧来追溯函数调用过程
这里我遇到一个问题 就是怎么一直在 系统领空
这里是我忘记了windbg会自动在系统断点停止
g @$exentry
就可以调到程序领空
这里我们开始看栈中的函数
k命令
栈帧的基地址是通过EBP来访问的 所以是childebp
下面是返回地址 就是调用本函数的那条call指令的下一条地址 就是作为返回地址
kb命令
只用于显示放在栈上的前3个参数
其他的k命令
kp 可以吧参数和参数值以函数的原型返回出来 包括
参数类型 名字 取值
kv命令可以在kb的基础上增加 栈指针省略信息和调用约定
kd命令用于列出栈的数据
内存命令
1.查看内存
d命令
d [类型][地址范围]
dd 4001000 L4
L4可以指定显示前4个
字节
d命令有很多衍生
dw 双字word
dd 四个字节
dq 八个字节
df 四个字节单精度浮点数格式
dD 八个字节双精度浮点数格式
dp 指针大小格式 在32位中为4字节 64中为 8字节
ASCII
da 表示字符串
db 表示字节和字符串
dc dword和ASCII
du 表示unicode字符串
dW 表示双字节word 和 ASCII
ds 用于显示 ANSI_STRING
dS 用于显示 UNICODE_STRING
二进制
dyb 表示二进制和字节
dyd 表示二进制和dword
结构
dt [模块名!]类型名 用于显示数据类型和数据结构
例如
dt ntdll!* 可以列出NTDLL模块的所有结构
dt _PEB 可以显示 PEB的结构
地址
dds dps dqs 用于显示 地址和相关符号
拿 0040115Eh来举例子
2.搜索指令
s指令
s - [type] range pattern
type :
搜索的数据类型
b 比特
w word
d dword
a ascii
u unicode
默认为 b
range : 地址范围 可以用两个方式
1.起始地址+终止地址
2.起始地址+L(长度)
如果超过 256MB 使用 L?length
pattern:指定要搜索的内容 可以使用空格分隔要搜索的值
例如
s -u 400000 430000 "pediy"
在 400000 到 430000中 搜索 unicode字符串 pediy
s -a 0x000000000 L?0x7fffffff mytest
表示在 2GB的 user mode 内存空间中搜索 ASCII字符串mytest
3.修改内存
e命令
写入字符串
e{a|u|za|zu} address "String"
za zu 是表示以 0 结尾的 ASCII 和 UNICODE 字符串
a u 表示不以0 结尾
例如
eaz 298438 "pediy"
就是在 298438 这个地址 写入 pediy这个ASCII 并且以0结尾
写入数值
e{a|b|d|D|f|q|u|w} address [values]
a ascii
b bite
d dword
D DOUBLE
f float
q 八个字节
u unicode
w word
eb 298438 70 65 64 69 79
会在 298438 写入 pediy 的数值形式
4.观察内存属性
!address [Address]
5.脚本
windbg的脚本是一个语言
例如
我们想输出 helloword
.echo helloword
1.伪寄存器
windbg给了很多伪寄存器
在表达式中使用伪寄存器 必须要使用转义符 @
$exentry 当前进程的入口地址 运行 g @$exentry 可以到达程序入扣
$ip 当前的指针寄存器
$ra 当前函数的返回地址
$retreg 当前函数返回地址存在这个寄存器中
$csp 当前的栈指针 esp/rsp
$tpid 当前进程的标识
$tid 当前线程的标识
$ea 最后一天被执行指令的有效地址
$p 最后一条 d 命令打印的值
$bpNumber 对应断点的地址
$t0~$t19 自定义伪寄存器
2.别名
类似 define宏
一个是固定别名
一个是自定义别名
固定别名
windbg提供了10个固定别名
$u0~$u9
在定义固定别名的时候需要使用 r 命令
r $.u0="helloword"
.echo $u0
自定义别名
自定义别名的命令有3个
as ad al
as为内存中的字符串定义别名
ad 删除别名 ad Name ad *
al 列出别名
as /选项 别名名称 别名实体
选项的选择
/ma ASCII
/mu Unicode
/msa ANSI_STRING
/msu UNICODE_STRING
/e 指定的环境变量
/f 指定文件的内容
as /ma bookname 0040115e
3.表达式
windbg识别两个表达式 MASM / c++
默认使用MASM
.expr 可以显示表达式语法
使用
@@c++()可以指定c++表达式
@@masm()可以指定 masm表达式
MASM表达式
除了 +-*/这些算术运算符
还支持转型运算符
hi/low 可以得到32位数的高16 或 低16
by/wo 可以得到指定地址的地位1字节/1字的值
dwo/pwo 可以得到指定地址的DWORD/QWORD
poi 可以得到指定地址的指针长度
为了支持复杂的调试命令 Windbg还定义了特殊的运算符
$fnsucc(FnAddress,RetVal,Flag)
将RetVal作为 FnAddress处的函数的返回值 如果返回值是一个成功码
$fnsucc返回 true 否则false
$iment(Address)
返回加载模块列表中的映像入口地址
$scmp("string1","string2"):比较 -1 0 1
$sicmp("string1","string2"):比较 -1 0 1
它们的差别在于比较时是否忽略大小写。
$spat("string","pattern")
根据 string匹配 pattern 计算得到 true or false
$vvalid(Address,Length)判断Address起Length的内存是否有效
有效返回1 否则 0
C++表达式
支持C++的操作符
包括 . ->
C++会把数值作为十进制 所以 要加上 0x
注释
支持两个注释方式 * 和 $$
* 后所有都会被当做注释
$$ 到 ; 结束为注释
6.例子
我们的要求是 打开CreateFileA函数 并且判断是不是 c:1212.txt
如果是就断点 如果不是就继续
命令行是
bp kernel32!CreateFileA "$<D:\\test.txt"
在D:\\test.txt的内容是
as /ma ${/v:fname} poi(esp+4)
.if ($sicmp("${fname}","c:\1212.txt")=0) {.echo ${fname}} .else{gc}
我们打入断点指令后
g运行程序
发现在原本读取 1212.txt的地方中断了
到此 windbg的基础就结束了