【文章标题】: WINDBG Script简易教程
【文章作者】: 笨笨雄
【作者邮箱】: nemo314@gmail.com
【工具名称】: WINDBG
【下载地址】: http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx
【作者声明】: 希望更多人使用WINDBG,然后大概就能看到debugging tools for windows的帮助文件的中文翻译了吧。花了几天时间才在翻译软件的帮助下看完

本节附件下载

debugger commands部分,痛苦死了。也找不到WINDBG的插件,还是WINDBG没插件功能?为了简化调试过程,只有学习使用SCRIPT了,现在把这几天的经验跟大家分享。附件中所有代码经NOTEPAD,REGEDIT等调试,花了几小时,基本通过。

    废话多了,现在是正文。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判断当前操作系统类型

为不同系统准备不同代码

  • 标 题: 答复
  • 作 者:笨笨雄
  • 时 间:2006-10-22 21:00

总算写完了。。。。再废话几句

WINDBG看起来很难用,是因为用的人不多。

即使没有插件功能,WINDBG SCRIPT的功能也已经很强大了。

如果大家都来做SCRIPT,调试分析难度会降低很多,新手也可以通过阅读SCRIPT FILE来学习。搜索引擎使用得好的确可以学到很多,可惜这跟作者的表达和使用引擎者的表达有关,很可能相同的内容,因为表达方式不同就查不到了。就象我学校的图书馆,在电脑搜索逆向工程是什么都找不到的,但是搜索加密解密,却看到好几本书。

OD虽然好,始终是RING3的,SOFTICE似乎也已经停止开发了。希望大家都能加入WINDBG的行列