废话多了,现在是正文。WINDBG的指令比较多,还是英文的,所以我只挑了一部分经常会用到的,并通过实例去告诉大家那些指令的作用和格式。正如大多数高级语言教程一样,我们先来看看如何写一个HELLO WORLD的程序。
如果使用.echo "HELLO WORLD"作为例程就太简单了,我希望介绍更多的指令。所以我在OD直接用汇编写了个程序:
PUSH 0
PUSH 12345678 ;TITLE跟显示内容都在这个跟下一个PUSH
PUSH 12345678 ;我比较懒。。。就用一样的字符了
PUSH 0
MOV EAX,OFFSET MESSAGEBOXW
CALL EAX
先用EAX保存MESSAGEBOXW的指针,然后再CALL。这是为了你在任何一个程序下都能用使用这个SCRIPT。如果直接CALL MESSAGBOXW的指针,翻译成机器码是相对于当前位置的偏移,这样写出来的SCRIPT文件在这个程序能用,别的程序就不能用了。
机器码 6A 00 68 78 56 34 12 68 78 56 34 12 6A 00 b8 68 3d e2 77 ff d0
我虚拟机使用的是WIN 2000 连SP1都不是。。所以我不保证你的机器仍然能正常运行这个程序。为了能正常使用,你可以随便找一个程序,然后BP MESSAGEBOXW,中断之后当前的EIP就是了,把ff d0前面的68 3d e2 77换掉了就可以了。我的目的并不是介绍如何写一个兼容性差的程序,重点是学会如何写SCRIPT。
准备工作做好了,在看代码之前,先解释一些指令:
$exentry伪寄存器,数值上等于EP
$t0-$t19,WINDBG为我们提供了20个自定义的伪寄存器
R指令能改变几乎所有寄存器的值,包括EAX等
.dvalloc [/b] size 申请内存空间,带/b 地址,可在指定地址申请空间,不带则自动分配,指定地址时不一定成功,暂时的经验指定地址越大越容易成功。
e* 地址 在指定内存中写入数据,EW 写入WORD,EB写入BYTE,ED写入DWORD
注意: EW 00400000 12345会产生溢出错误,同理EB 00400000 123也是错的,正确的例子可见后面的代码
f 地址 L长度 BYTE 在长度的地址写入数据,你可以在示例中看到效果。同样BYTE的位置只能是BYTE,多于8位的数据都会造成溢出错误。
m 源地址 L源地址长度 目的地址 复制内存区域
d* 地址 显示地址中的数据,其中db的效果可在示例中看到。
.dvfree /d 地址 size 释放指定地址的内存,这里指定地址用的是/d要与 .dvalloc的/b相区别。
附件中helloworld.txt的代码:
---------------------------------------helloworld.txt--------------------------------
g $exentry
r $t0=00ff0000; $$ $t0:源内存基址
r $t2=@$exentry; $$ $t2:目的内存基址
r $t1=@$t0; $$ $t1:当前指针
.dvalloc /b $t0 1000; $$在00b90000申请1000BYTE的内存空间
ew $t1 006A 0068
db $t0
.echo "ew指令的效果"
r $t3 = @$t1 + 3; $$PUSH DWORD的机器码=68 DWORD
$$这里应该输入字符串的首址
r $t1 = @$t1 + 7; $$懒得计算,所以用$t3存起DWORD的指针
$$输入字符串的时候一起搞定
eb $t1 68 12 34
db $t0
.echo "eb指令的效果"
r $t4 = @$t1 + 1; $$同上
r $t1 = @$t1 + 5
f $t1 l20 6A 00 b8 68 3d e2 77 ff d0
db $t0
.echo "f指令的效果"
r $t1 = @$t1 + 9
r $t5 =$t1 - $t0 + $t2
ed $t3 $t5; $$ 添加字符串的指针回去
ed $t4 $t5
f $t1 l20 'h' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' '!' 00 00
$$ 把字符串写进内存中
m $t0 l30 $t2
r $ip=$t2; $$ 修改EIP
.dvfree /d $t0 1000; $$ 释放内存
g
---------------------------------------helloworld.txt完结的分割线--------------------------------
为了你的速度,请保证symbol path为空,只有在你有源代码或者系统核心的时候它的存在才有意义,否则你会发现它会非常费时且毫无意义,尤其是你得连上网络下载symbol资源的时候。
使用SCRIPT文件的命令有4个"$<","$$<","$><","$$><",他们的区别就是有没有空格或者换行符的限制。使用$$><没有任何限制,这样可使代码更具可读性。要使用附件中的SCRIPT请使用$$><指令。
例如你可以用下面指令访问在D盘下的helloworld.txt。
$$><d:\helloworld.txt
如果你把helloworld.txt放在WINDBG的安装目录,那么你可以使用下面指令:
$$><helloworld.txt
运行完helloworld.txt后你会发现报错了,因为我的代码覆盖了EP。通过上面的例子,我们能用SCRIPT做什么呢?在合适的时机,把没加密的IAT或者其他什么的,暂存到内存中,在脱壳完毕的时候再自动用正确的部分把加密部分覆盖掉。
也许在未来,也会遇到这样的需要,程序运行到某部分的时候,中断,然后运行我们自己的代码,运行完毕之后,我们需要返回到程序原来的流程。为此我把上面的SCRIPT修改了一下,写成BACKTOCODE.TXT。
--------------------------------------------backtocode.txt--------------------------------------
r $t0=00ff0000
r $t1=@$t0
r $t18=$ip ;$$ 用$t18暂存当前EIP,显然$ip=EIP
.dvalloc /b $t0 1000
ew $t1 006A 0068
r $t3 = @$t1 + 3
r $t1 = @$t1 + 7
eb $t1 68 12 34
r $t4 = @$t1 + 1
r $t1 = @$t1 + 5
f $t1 l20 6A 00 b8 68 3d e2 77 ff d0
r $t1 = @$t1 + 9
ed $t3 $t1
ed $t4 $t1
ba e1 $t1 ;$$ 内存运行断点,E代表运行1是长度,在E后面通常是1,断在最后一个指令后的第一个地址
f $t1 l20 'h' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' '!' 00 00
r $ip=$t0 ;$$ 在分配的内存中直接运行我们自己的代码
g
.dvfree /d $t0 1000
r $ip=$t18 ;$$ 把EIP设置为原来的
.cls ;$$ 当前命令行窗口清屏
p ;$$ 单步步过
-------------------------------------------backtocode.txt结束-----------------------------------
为了anti anti debug我们需要隐藏标题,虽然WINDBG本身有.wtitle指令,可是那个指令会把它后面的所有内容当作字符串输入,而且windbg+版本号是无论怎么改都会被默认添加到最后的。这样上面那个SCRIPT就有用了,就差CODE了。
为了anti anti debug,每次DEBUG的时候,我们都要做一些准备工作,用SCRIPT文件,我们可以自动完成这些操作,下面是我DEBUG之前都会用到的SCRIPT文件
-----------------------------------------------start.txt----------------------------------------
r $t0 = 00
eb 7FFDF002 $t0 ;$$去除DEBUG标志
.pcmd -s ".if(eax<70000000 and eax>00120000){da eax;du eax}; .if(edx<70000000 and edx>00120000){da edx;du edx}"
g $exentry ;$$入口点
-------------------------------------------start.txt结束----------------------------------------
如你所见的,这有点少。没办法水平有限,而且写这篇文章的时候我才勉强说是学会用,还是那句重点是教会大家用WINDBG。WINDBG的初始断点并不是入口点所以得自己用指令让它自动停在入口点,有的程序是有TLS表的,对着PE格式的介绍文章,写一个SCRIPT在有TLSCALLBACK的情况下自动停在TLSCALLBACK入口是有可能的,你会在文章的最后部分得到相关指令的介绍。现在来说说START.TXT中没有注释的指令。
; 分号,多条命令的分隔符。从左到右运行。
下面例子中,对MESSAGEBOXW下断后运行,中断之后便会运行r $t0=esp+8指令
bp messageboxw;g;r $t0=esp+8
注意:如果你使用CRTL+BREAK快捷键在中断之前暂停调试也会导致r $t0=esp+8的运行。
.if(条件表达式){命令} 跟C语言中的用法一样。
.pcmd 不带参数则显示每条指令之后自动使用的指令。-s "命令" 设置命令。-c 清除命令。
da 以ASCII显示内存地址,du以UNICODE显示内存地址
在示例中,整条指令的效果表现为,每单步一个指令,便会当EAX,EDX指向的是一个合法地址的时候,便以ASCII和UNICODE的方式分别显示它的值,就象OD那样。如果熟悉ASCII和UNICODE字符集的范围还能设置仅当有效字符时才显示结果。
标 题: 答复
作 者: 笨笨雄
时 间: 2006-10-22 17:04
详细信息:
在调试的过程中,有时我们希望自动化解决一些问题。例如调试使用了UnhandledExceptionFilter的SEH,我们需要自动修改ZwQueryInformationProcess的返回值。或者对于某些API的ANTI DEBUG,如果我们修改了输入参数,同样不能返回应该返回的值。学破解不久,一下子要找用了UnhandledExceptionFilter的软件还真不容易,用别的API代替了。我用OD把NOTEPAD修改一下,改名为TEST放在附件中。
流程MESSAGEBOXW,GETCOMMANDLINEW,MESSAGEBOXW输出COMMANDLINE,最后EXITPROCESS。
现在我要做的是改变GETCOMMANDLINEW的输出,和第二个MESSAGEBOXW的输入。现在让我们看看test2.txt
-------------------------------------------test2.txt-----------------------------------------------
g $exentry
r $t0=0
bp messageboxw "r $t0=$t0+1;j($t0=2)'r $t1=poi(esp+8);f $t1 l4 45;g';g"
bp getcommandlinew "g poi(esp);r $t1=eax+5;f $t1 l4 55;g"
g
------------------------------------------test2.txt完结---------------------------------------------
首先对相关指令作一些介绍
BP 地址或者函数名 "命令" 命令参数是可选的,存在的情况下,中断的同时会先运行那些命令。
J(条件表达式)'命令1';命令2 相当于.if但是又有点不同命令2只能是1个,后面所有命令会被忽略。
POI() 返回指针的指向位置的内容。
!= 不等于
这里用了条件中断的方法实现,第一个条件中断指令用$t0作为计数器,第二次中断的时候变修改堆栈中指针指向位置的内存区域。注意到调用API的返回地址在ESP中,直接跳出去,然后修改EAX就可以达到修改函数输出参数的效果了。
这里提供第二种可行的方法,并且更有可扩展性,现在看看test.txt中的代码。
-------------------------------------------test.txt------------------------------------------------
g $exentry
r $t0=0
bp messageboxw
bp getcommandlinew
bp exitprocess
.while (eip!=77e7b0bb){
g
.if($ip=77e116cc){
r $t0=$t0+1
.if($t0=2){
r $t1=poi(esp+8)
f $t1 l4 45
}
}
.if($ip=77e7c693){
g poi(esp)
r $t1=eax+5
f $t1 l4 55
}
.elsif($ip=77e7b0bb){
.break
}
}
g
------------------------------------------test.txt完结----------------------------------------------
仍然先介绍一些指令:
.while(条件表达式){} 跟C语言中的一样,循环结构,直到条件表示式为真
.elsif(){} 跟前面的.if用法一样,它的作用如字面上意思,只是小心别拼错为ELSEIF
.break 跟C语言中的一样,跳出循环。
如果在条件为真的时候不用.break跳出循环就会出错,这点要注意。
这里构造了一个循环结构,并且通过对比EIP的方法来识别函数,同样地因为我的虚拟机是WIN 2000 连SP1都不是,所以我不肯定该地址在你的机器中仍然可用。不过这里提供了一个思路,你可以用这个方法构造一个SCRIPT来加强WINDBG的功能,例如象OD一样中断的时候自动显示所有参数,并且带上英文提示那是什么参数。同样地,我们可以做一个自动化分析SCRIPT,分析每个CALL中包含了什么API,并且列出输入和输出参数,CALL的深度还指令数,并且自动生成报告文件,假如有人开发出这样一个SCRIPT,调试分析将会变得容易。WINDBG里面有个相似功能的指令。
WT 自动跟踪并生成报告,几乎跟我上面说的一样。带/l参数的时候可以设置深度,不过很多时候,我们看到一个CALL并不知道里面究竟有多深,但是我们希望得到一些关于那个CALL的详细信息来判断是否值得跟进。这里有两个问题:
1 递归,那这个指令不知道运行多久。
2 大量NATIVE API调用,显然大多数情况下,我们并不关心。
比起1,2更加常见,/i参数是用来避开指定模块的,不会用,帮助文件里也没提。。。。希望有大大能答我这个问题
WINDBG提供了下面3个指令用于保存分析过程进文件,通过适当的开关可以过滤一些无意义的信息,使分析过程易于观看。
.logopen 文件路径 带/U参数则以UNICODE方式输入文本。重写整个文件,并记录当前命令窗口在使用该指令之后的所有内容。
.logclose 文件路径 停止记录并关闭文件。
.logappend 文件路径 带/U参数则以UNICODE方式写文件。记录当前命令窗口在使用该指令之后的所有内容,并添加进文件。
提到了功能强化,大家都知道OD里面有个命令是运行到RET处吧,在WINDBG中似乎没有这样的指令,类似的有PC,即运行到CALL。我写了一个SCRIPT来模拟OD中的那个指令。现在我们来看看goret.txt
-------------------------------------------goret.txt------------------------------------------------
r $t0=0
.while(@$t0!=c3){
p
r $t0=by(eip)
.if(@$t0=c3){
.break
}
}
-----------------------------------------goret.txt完结----------------------------------------------
这里是最后一个示例分析,所以除了解释上面的指令之外也给出一些有价值的指令
not 非 and或者& 与
hi() 取高16位 or或者^ 或
low() 取低16位 xor或者| 异或
by() 取低8位 gu 步出,不知道具体原理,有时会出错
wo() 取低16位 t 步入
mod或者% 模运算
这个SCRIPT使用了一个循环,通过EIP取得当前指令的机器码,低8位既为指令,然后把指令存进$t0作比较。C3是RET的机器码,等于则跳出循环,否则一直步过。
这个示例表明,我们可以在SCRIPT里分析每一条指令。我们可以在WINDBG中进行2次开发,动态将那些简单使用JMP+内存指针或者寄存器作为跳转的乱序的程序重新排序,使花指令失效,并且实现自动清除垃圾指令,最后生成优化后的汇编代码文件。本论坛翻译区里的变形多态中的关于收缩器的理论已经为我们奠定了理论基础。
你可能会需要用到反汇编指令
u 起始地址 l长度 L代表的不是地址长度而是指令的个数
---------------------------------后续讨论,用SCRIPT把WINDBG变成脱壳机------------------------------
在准备写这篇文章的时候,我又把DEBUGGER COMMANDS看了一次,发现了这个指令
.writemen filename range 将目标内存区域写进文件。RANGE的格式为 地址 l长度
我没试过L后面是否接受寄存器作为参数。也没实际测试过这个指令的具体操作是怎么样的,无论如何,有这个可能存在。当然我们也可以申请内存区域以程序的方式来完成这个工作,不过我希望它仅用SCRIPT完成。
假如这的确可行,可以通过下面指令组合来自动寻找文件头,当然也能确定文件大小。
$p 伪寄存器,它将返回前一次用d*指令所显示的内存的内容。
假设00100000 01 02 03 04 05 06 07 08
我使用dd 00100000,那么$p = 04030201
显然我们可以通过这个方法来访问内存。
dw 取一个WORD; dd取DWORD; dw取qword
能访问内存也代表说我们在调试程序中插入的代码也能跟SCRIPT通信,并且把一些SCRIPT无法完成的工作交给程序执行,然后把结果返回给SCRIPT。
假如l的参数无法通过寄存器来传递,只能依靠用户按照提示进行操作,那么我们有更简单的方法
.imgscan 它将返回所有模块MZ的地址和它的SIZE
----------------------------------这里给出一些可能的疑问和解答--------------------------------------
Q:在调试SCRIPT文件的时候,我该如何知道寄存器跟内存的变化?
A:我们可以用下面的指令来观察寄存器跟内存的变化
d* 用于显示内存,之前已经提到就不详细说明了
? 寄存器 显示寄存器的值,例如
? poi(esp); ? $t0
这将先显示ESP指向的值,然后显示$t0的值
除了可以使用.echo命令对显示参数作说明之前,也可以使用.printf作格式化输出,它的用法跟C语言中的printf是一样的
Q:我写的SCRIPT文件出错了,语法跟参数都没错,为什么我找不到出错原因?
A:有的指令要注意的,BA只能在进入程序区域之后才能用。.dvalloc申请过的内存,即使用.dvfree释放了,也无法在同样的位置再申请,可能是BUG。
*是一个注释命令,它后面所有的内容都会被当作字符
$$则是以分号为结束
.restart指令跟.wtitle指令,不知道为什么不能放在SCRIPT中使用。
还有就是@这个标记,这个标记是告诉WINDBG后面的是一个伪寄存器而不是程序里的某一个变量的符号。有的指令在没有@标记的时候会报错,例如.while括号里的条件表达式,如果你用了伪寄存器,一定要在前面加上@否则一定报错。此外用帮助文件里的话来说,使用@,可以让SCRIPT文件运行得更快,因为在解读这个代码的时候不需要先搜索一次SYMBOL记录。
Q:我能把功能模块化然后在其他SCRIPT文件中使用吗?
A:我已经测试过$$><指令也能在SCRIPT里面使用
Q:我写的SCRIPT FILE能在64位系统中用吗?
A:如果你仅使用SCRIPT来实现功能,那么很可能与64位兼容。尽量使用伪寄存器。
$ip,$retreg,$csp在32位系统中分别表示EIP,EAX,ESP,而在64位系统中则表示RIP,RAX,RSP也分别对应Itanuim处理器中的相关寄存器。
通过函数名获取不同版本下的地址,可以通过下面代码:
bp messageboxw ; $$第一个断点,断点ID为0
bp getcommandlinew; $$第二个断点,断点ID为1
r $t10 = $bp0; $$将第一个断点的地址转存$t10
r $t11 = $bp1; $$将第二个断点的地址转存$t11
bc *; $$清除所有断点。
这段代码运行之后MESSAGEBOXW的地址便存于$t10中,而$t11里面的则是getcommandlinew的地址。这里要说明,断点用完要释放,否则不好估计断点的ID,此外在内核模式中,最多只允许32个断点。
$peb和$teb返回当前进程的PEB和TEB地址,这里的翻译区有介绍如何仅通过PEB或者TEB判断当前操作系统类型
为不同系统准备不同代码
总算写完了。。。。再废话几句
先来了解简单的,得到当前访问的文件名
先写段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,让进程运行起来,
点击按钮,果其然,得到文件名了,见下图,
23985
访问的文件获取了,那如何在访问指定文件时中断下来呢?字符串比较的脚本如何写呀?
上网查资料吧,不大一会,发现了$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时断下来了。
23986
如果我要在往该文件写数据时断下来,如何设断?直接在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
}
}
"
运行结果截图:
23987
WINDBG看起来很难用,是因为用的人不多。
即使没有插件功能,WINDBG SCRIPT的功能也已经很强大了。
如果大家都来做SCRIPT,调试分析难度会降低很多,新手也可以通过阅读SCRIPT FILE来学习。搜索引擎使用得好的确可以学到很多,可惜这跟作者的表达和使用引擎者的表达有关,很可能相同的内容,因为表达方式不同就查不到了。就象我学校的图书馆,在电脑搜索逆向工程是什么都找不到的,但是搜索加密解密,却看到好几本书。
OD虽然好,始终是RING3的,SOFTICE似乎也已经停止开发了。希望大家都能加入WINDBG的行列
=====================================================================================
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 =
$$ 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++()
.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 是怎么来的? 它前面的 $, @, & 是什么意思?
我首先会纳闷 $peb 是怎么来的, peb 前面的 $ 使它(peb)看起来好像伪寄存器, 带着猜测我们查看帮助文件。哈哈! 在
Pseudo-Register Syntax 一节可以查到: $peb 是自动伪寄存器,大概意思是说 $peb 可以直接引用并且是调试器自动赋值的, 难怪我们可 以不经过定义而直接用呢.
$peb 前面的 at 符号作用是明确告诉调试器 @ 后面内容是寄存器或伪寄存器而不是符号。对于使用寄存器的方法, 我们最好在寄存器和 伪寄存器之前都加上 @ 符号,这是个好习惯.
回答(2) 的问题: r? 给伪寄存器 $t0 赋值. 现在$t0值是 InLoadOrderModuleList 的地址了.
我们验证一下:
1. 输入命令: r? $t0 = ; 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}。