http://hi.baidu.com/6908270270/blog/item/0d7fe423fc8612409922ed04.html
最近需要在访问指定文件时中断下来,但不知道如何下断,在网上搜索了一番无果,只好自己摸索了。听大侠说windbg的条件断点功能异常强大,可以实现,不禁心痒,特尝试一番,顺便熟悉一下windbg的脚本语法。
先来了解简单的,得到当前访问的文件名
先写段C代码,创建C:\a.txt并往文件中写任意几个字符,代码如下:
HANDLE hFile=CreateFile("C:\\a.txt",
GENERIC_WRITE|GENERIC_READ,
0,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return ;
}
char Buffer[]={"abcdefghijklemn"};
DWORD dwReturn;
WriteFile(hFile,Buffer,strlen(Buffer),&dwReturn,NULL);
CloseHandle(hFile);
把以上代码放到对话框的按钮点击响应事件中去。编译链接得到test.Exe.
文件名为CreateFile API的第一个参数,因而在执行到该API的入口时,esp+4即表示第一个参数的地址。故可这样下断:
bp kernel32!CreateFileW "r $t1=poi(esp+4);.echo;.printf\"FileName:%mu\",$t1;.echo;g"
$t0~$t19为伪寄存器,可用来存储临时值,poi表示取地址的值,
也可将脚本保存为文件,然后在windbg中输入: $$><脚本文件路径 来运行。
我把以上脚本保存为 ”C:\script.txt”
用windbg打开以上代码编译得到的test.exe,
Ctrl+Break中断windbg,然后下断,即输入:$$><C:\script.txt,再输入g,让进程运行起来,
点击按钮,果其然,得到文件名了,见下图,
访问的文件获取了,那如何在访问指定文件时中断下来呢?字符串比较的脚本如何写呀?
上网查资料吧,不大一会,发现了$scmp/$sicmp/$spat是用来字符串操作的。$spat正合我意呀。
$spat("string1”, "pattern”):判断参数1指定的字符串是否符合参数2指定的模式。模式字符串中可以包含?、*、#等特殊符号,WinDBG帮助文件中String Wildcard Syntax一节包含了详细的说明;
这样摸索了一番,写了如下脚本:
bp kernel32!CreateFileW "
r $t1=poi(esp+4)
as /mu $FileName $t1
.echo
.printf\"File:%mu\",$t1
.echo
.block
{
.if($spat(\"${$FileName}\",\"*a.txt\"))
{
.echo 'find...';
ad ${/v:$FileName}
}
.else
{
.echo no find...
ad ${/v:$FileName}
gc
}
}"
以上脚本,不复杂,实现当访问文件名类似“a.txt”时,windbg中断执行。
对脚本解析几个要点:
1. 使用伪寄存器,更快速的方法是在$前加上一个@符号。这样,WinDBG就知道@后面是一个伪寄存器,不需要搜索其他符号;
2. r $t1=poi(esp+4),poi(esp+4)取地址的值,并赋给伪寄存器$t1 ;
3. as /mu $FileName $t1 ,定义$t1 所指地址一个别名$FileName,用来在下面的$spat中使用。别名会在脚本加载时被解析程序替换一次。为何要用别名?你不用试试就知道了,直接用$t1不行,windbg提示你语法错误;
4. .block是啥东西,看windbg帮助就知道了。代码块,如果需要每次运行进入替换,请用.block括起来;我这里有个别名,需要每次替换,所以用了个.block括起来;
5. $spat为模式匹配函数,其他类似函数$scmp/$sicmp。
6. ${$FileName}、${/v:$FileName}这呢?听我说来,${ aliase} 明确的指出了, 大括号 {} 内的变量名是可以被替换的,即使 aliase 和其它文本相连。如果要求 ${} 这个别名不被替换, 即不被解析程序替换成其他值, 只保留它当前的字面值.如下面的ad ${/v:$FileName},删除别名,此时用/v 选项来了阻止对该别名的替换, 保留它原来的字面值;
7. 别名用完记得要删除;删除方法用ad命令。
试试看看结果,在访问a.txt时断下来了。
如果我要在往该文件写数据时断下来,如何设断?直接在writeFile下断?但不知道当前访问的是哪个文件呀?在脚本里,通过文件句柄能得到相应的文件名吗?我反正还不知道,如果你知道请一定不要忘了告我呀?以下为我的脚本:
$$Written by shakesky
$$10:44 2009-2-12
$$访问文件之windbg下断脚本
bp kernel32!CreateFileW "
r $t0=poi(esp+4)
as /mu $FileName $t0
.echo
.printf \"Prepare to visit file:%mu\",$t0
.echo
.block
{
$$ 判断是否是访问我所需观察的文件
.if($spat(\"${$FileName}\",\"*a.txt\"))
{
.echo 'Match...'
~.gu
$$ 得到文件句柄
r @$t1=eax
.if(@$t1!=0xFFFFFFFF)
{
$$往该文件写数据时下断
.printf \"File Handle:%08x\",$t1
.echo
bp kernel32!WriteFile \"
r @$t2=poi(esp+4)
.if(@$t2!=@$t1) {gc}
\"
}
ad ${/v:$FileName}
gc
}
.else
{
.echo No Match...
ad ${/v:$FileName}
gc
}
}
"
WinDbg 6.7.5.0 版本运行脚本时多了一个新的命令 $$>a<,可以给脚本传递参数。下面是一个简单的例子,演示了参数的用法。
$$
$$ calc v0.0.1
$$ by 小喂 2007.06.08
$$
.if(@@c++(${/d:$arg1} && ${/d:$arg2}))
{
.printf "\n%d + %d = %d\n", ${$arg1}, ${$arg2}, ${$arg1} + ${$arg2}
.printf "%d - %d = %d\n", ${$arg1}, ${$arg2}, ${$arg1} - ${$arg2}
.printf "%d * %d = %d\n", ${$arg1}, ${$arg2}, ${$arg1} * ${$arg2}
.printf "%d / %d = %d\n", ${$arg1}, ${$arg2}, ${$arg1} / ${$arg2}
}
.else
{
.printf "\nusage: $$>a< <path>\calc.txt arg1 arg2\n\n"
}
运行一下:
0:000> $$>a< d:\windbg\scripts\calc.txt @eax 4
1580724 + 4 = 1580728
1580724 - 4 = 1580720
1580724 * 4 = 6322896
1580724 / 4 = 395181
由于 softice 的逝去 , 现在学习 windbg 调试器的人越来越多了。我很高兴看到这样的情景, 因为我是从 windbg 调试器学习起,那还是一年半以前的事了。那时网上有关 windbg 的资料比较少,只能看 windbg 自带的帮助文档,学起来很费劲。 softice 宣布停止开发后这一年多时间里,由于使用windbg 的人数增多,很多相关的技术文档也随之出现,这些技术文档多数是作者从实践中得来得。我也很高兴劲自己得一点努力给后来者提供更多可以参考得资料。
debugger 脚本程序实例
下面小节描述了debugger 脚本程序的例子,我们就从学习例子代码出发 !
使用 .foreach 符号
.foreach ( place { s-[1]w 77000000 L4000000 5a4d } ) { dc ${place} L8 }
这条简单的语句功能是: 从线性地址 0x77000000 处查起,查找长度是 0x4000000, 凡是值为 0x5a4d 的地址,就用 dc 命令打印出该地址的内容。
place 是自己定义的别名符号,代表符合查找条件的每个内存地址。
s-[l]w:参看 windbg 帮助文档,可知 -[l] 说明只显示找到的内存地址而不显示其内容,w 指定了查找目标的类型, 即 5a4d 是 WORD 类型的。
当执行了 place { s-[1]w 77000000 L4000000 5a4d } 后,可以认为 place 中有很多查找到的地址, .foreach 语句作用于其中的每一项内容(找到的地址), 然后对该地址执行 dc dump 内存的命令。
我们看第 2 个例子:
.foreach (place { lm 1m }) { .if ((${place} >= 0x77000000) & (${place} <= 0x7f000000)) { lmva ${place} } }
上面的命令显示处于0x0x77000000 到 0x7F000000范围内的详细的模块信息。
lm 命令: 列出已加载的模块, 命令的输出信息包括模块的状态和路径. 其中 1m 选项是使列出的模块信息只包含模块名。
注意: 用该lm 1m 命令显示的模块信息不包含模块的后缀名。
place 用户自定义的别名, 代表每个模块名字。假设我们用 lm 1m 命令得到了已加载的模块信息。.foreach 语句解析每个输出的模块信息,
然后把这些信息作为后面命令的输入。 .foreach 后面的命令中, 凡是出现我们自定义的变量名 place 时,就用真正值替换它们。
我们经常用 ${place} 来引用 place 代表的变量。
lmva ${place} 命令显示详细的指定模块信息.
lm 1m 命令显示信息:
从图中能够看出: 当前的 lm1m 列出了
lm1m 模块信息 ${place} 被替换 替换后的命令
virtual_fun -------------------------> lmva virtual_fun
MSVCR80D -------------------------> lmva MSVCR80D
msvcrt -------------------------> lmva msvcrt
kernel32 -------------------------> lmva kernel32
ntdll -------------------------> lmva ntdll
这时我们就容易理解了, 脚本命令就相当于是在每个模块上调用 lmva 显示它的基本信息.
做为对上面脚本命令的修改,我们可以这样:
.foreach (place { lm 1m }) { .if ((${place} >= 0x77000000) & (${place} <= 0x7f000000)) { .echo "find a module: ${place} , Memory Dump: " ; db ${place} } }
这次脚本修改后的功能是, 列出所有位于线程地址 0x77000000 和 0x7f000000 之间模块内存的 dump 信息。内存 dump 是从每个模块加载地址开始的, 所有我们可以看到熟悉的 DOS 头标志 'M''Z' 。
注意: 这些命令要用一行来完成。
我们也可以把上面命令保存到 DumpModule.txt 文本文件中, 文件内容是
.foreach (place { lm 1m })
{
.if ((${place} >= 0x77000000) & (${place} <= 0x7f000000))
{
.echo "find a module: ${place} , Memory Dump: " ;
db ${place}
}
}
这样可读性会增强, 同时更像一个脚本程序了。
如果保存在文件中, 我们需要把 DumpModule.txt 文件放在 windbg 的安装目录, 然后输入下面的命令来执行: $$>< DumpModule.txt
执行结果如下图:
在源帮助文件中有这样一段话指出了 ${} 符号的作用
The ${ } (Alias Interpreter) token is used here to make sure aliases are replaced even if they are adjacent to other text. If this were not included, the opening parentheses adjacent to place would prevent alias replacement.
我们就用上面的脚本代码来说明 ${} 符号的作用.
(1) ${ aliase} 明确的指出了, 大括号 {} 内的变量名是可以被替换的,即使 aliase 和其它文本相连也要做替换.
例如: 下面的脚本命令中, lmva 和 ${place} 紧相连在一起,解析程序能否把 place 解析出呢? 可以的,因为我们用了 ${} 来明确告诉解析程序要解析这个符号,所以命令能够成功执行。
.foreach (place { lm 1m }) { .if ((${place} >= 0x77000000) & (${place} <= 0x7f000000)) { lmva${place} } }
但是如果去掉 ${} 符号, 即是下面的命令
.foreach (place { lm 1m }) { .if ((${place} >= 0x77000000) & (${place} <= 0x7f000000)) { lmvaplace } }
哈哈, 这次解析程序解析出错了, 因为不知道 lmvaplace 是做什么用的.
我们再次更改一下命令脚本, 看看解析程序有什么反应:
.foreach (place { lm 1m }) { .if ((${place} >= 0x77000000) & (${place} <= 0x7f000000)) { lmva ${Change_place} } }
因为前面没有定义过 Change_place, 所以解析程序不能解析 Change_place 而出错。
让我们来看另外的一个例子:
遍历用户模式的 LDR_DATA_TABLE_ENTRY 链表, 显示了每个链表的基地址和完整路径。
首先我们看一下脚本命令:
$$ Get module list LIST_ENTRY in $t0.
r? $t0 = &@$peb->Ldr->InLoadOrderModuleList
$$ Iterate over all modules in list.
.for (r? $t1 = *(ntdll!_LDR_DATA_TABLE_ENTRY**)@$t0;
(@$t1 != 0) & (@$t1 != @$t0);
r? $t1 = (ntdll!_LDR_DATA_TABLE_ENTRY*)@$t1->InLoadOrderLinks.Flink)
{
$$ Get base address in $Base.
as /msu /x ${/v:$Base} @@c++(@$t1->DllBase)
$$ Get full name into $Mod.
as /msu ${/v:$Mod} @@c++(&@$t1->FullDllName)
.block
{
.echo ${$Mod} at ${$Base}
}
ad ${/v:$Base}
ad ${/v:$Mod}
}
把上面的脚本命令保存为文件 "ModuleList.txt", 放在 windbg 的安装目录, 然后执行 $$><ModuleList.txt
执行结果如下图:
这次我们遇到了很多陌生点,别着急,一个一个来解决:
(1) 首先是 $$ 符号, 你一定猜到了: 这是个注释符号?!
是的,就像 C++ 中的 //, '$$' 注释一行或者是在本行中遇到 ';' 即结束作用。
(2) r? 是做什么用的,先不管他,继续向下分析.
(3) 一共有 20 个自定义伪寄存器,$t0,$t1.$t2......$t19. 调试器可以对这些寄存器进行读写等操作,在脚本命令中较常用.
(4) 下面就是 &@$peb->Ldr->InLoadOrderModuleList 了。开始分析这条语句时你可能会很奇怪:
peb 是怎么来的? 它前面的 $, @, & 是什么意思?
我首先会纳闷 $peb 是怎么来的, peb 前面的 $ 使它(peb)看起来好像伪寄存器, 带着猜测我们查看帮助文件。哈哈! 在
Pseudo-Register Syntax 一节可以查到: $peb 是自动伪寄存器,大概意思是说 $peb 可以直接引用并且是调试器自动赋值的, 难怪我们可 以不经过定义而直接用呢.
$peb 前面的 at 符号作用是明确告诉调试器 @ 后面内容是寄存器或伪寄存器而不是符号。对于使用寄存器的方法, 我们最好在寄存器和 伪寄存器之前都加上 @ 符号,这是个好习惯.
回答(2) 的问题: r? 给伪寄存器 $t0 赋值. 现在$t0值是 InLoadOrderModuleList 的地址了.
我们验证一下:
1. 输入命令: r? $t0 = &@$peb->Ldr->InLoadOrderModuleList ; db $t0 我们可以看到 $t0 的值是 0x00251eac.
2. 输入命令: dt _PEB @$peb Ldr , 得到 _PEB 成员 Ldr 的地址是 0x00251ea0,
输入命令: dt _PEB_LDR_DATA 0x00251ea0 可以看到从地址0x00251ea0 处偏 移量为 0x0c 的位置就是 InLoadOrderModuleList
经过验证正确无误.
(3) 好了, 如果你弄懂了上面几点 .for 语句中的条件语句就自己能够看懂了.
.for 语句中是遍历链表的过程, 语法上也就没有什么可说的了。
(4) 下面遇到的问题是:
as /msu /x ${/v:$Base} @@c++(@$t1->DllBase)
as 命令的作用是 定义一个新的变量名. /msu 选项指出了 $Base 数值为 @$t1->DllBase UNICODE_STRING 结构体地址.
${/v: aliase} : 评估各种和 自定义的 aliase 相关的值。/v 选项指出了阻止对该别名的评估(替换), 紧保留它原来的字面值。
在 ${} 的使用方法中有这样一种应用情况: 要求 ${} 这个别名不被评估, 即不被解析程序替换成其他值, 只保留它当前的字面值.
下面说点相关的:
为了更好地理解上面的命令,我们写几条语句测试一下:
>as Msg hello world 回车
>al
Alias Value
------- -------
Msg hello world
>ad Msg
=========================================================
>as ${Msg} hello world 回车
>al
Alias Value
------- -------
${Msg} hello world
=========================================================
>as ${/v:Msg} hello world 回车
>al
Alias Value
------- -------
Msg hello world
从上面的例子可以看到 as 把第一个参数做为自定义的 Aliase, 不管他是否含有 ${} 符号. 但例外情况是 S{/v:aliase}
在这种情况下解析程序会定义 aliase, 而不是 S{/v:aliase}。