Windbg和od脚本编写

一般情况调试器的基本功能已经够用,尤其是有源码的时候,查找问题足够方便。但是很多时候我们需要让调试器自动完成一些功能,比如某个函数调用之后的数据记录、数次调用某函数的结果、两个函数调用之间发生的事情等等,简单的可以用条件断点或者基础log解决,稍微复杂的就需要用到脚本了。

平常用windbg和od最多,前者功能强大,还能调试驱动;后者简单易用画面清晰。偶尔也会用到Immunity Debugger,《python灰帽子》中对这款python调试器大加赞赏,兼容性上比OD略好一些,对python的支持扩展起来也更方便。我觉得只有掌握调试器脚本的用法才能在解决一些有难度或者复杂问题时做到游刃有余,既能利用现有调试器的功能,又能更灵活的找到自己需要的信息。

详细讲解三种调试器的脚本系统,目标是学会使用调试器脚本来辅助工作。

Windbg

Windbg是微软亲儿子,R0和R3都可以调试,命令丰富,显示完整,用好了绝对是第一利器。它的命令非常多,涉及到方方面面,完全可以称得上一个工作台。这里不介绍具体的命令,只讲解它的脚本编程。有些命令还是需要读者自己去查阅帮助文档。

脚本执行有几种方式,可以直接在命令行输入命令,也可以编辑好脚本文件之后就可以在windbg的命令栏输入下面的指令来执行:

$<MyScript
$><MyScript 
$$< MyScript 
$$>< MyScript 
$$>< MyScript [arg1 arg2 arg3 ... ]

‘<’和’><’的区别是后者会把脚本压缩成一行,把换行替换为’;’;$和$$的区别是文件名之前可否有空格;之后的是参数。我们只要知道“$$>< MyScript”就行了。

学习脚本也从常量变量等开始,到最后介绍语句和API。下面逐一学习。

常量

windbg脚本默认是十六进制,0n前缀用于十进制数,0t表示八进制,0y表示二进制。字符串常量用双引号包含。

变量

windbg使用伪寄存器$t0~$t19来当变量,自己不能随意定义变量。赋值语句使用r,跟操作寄存器一样。r $t0 = 10,r $t1 = eax。

宏也叫别名,跟变量类似,也是用r操作,解释器会替换为它的值。

windbg提供$u0~$u9十个默认宏,赋值时需要加’.’,比如 r $.u0=10,.echo $u0,r $.u1=“dd esp l4; g”。语法确实蛋疼。 可以用 as自定义宏,ad删除某个宏、al列出所有宏。自定义宏功能非常强大。Windbg的帮助文档有as的所有参数和注意事项,这里不再详述,只是举个例子:

0:000> .dvalloc 5
Allocated 1000 bytes starting at 00380000
0:000> ea 00380000 "12345"
0:000> as /ma test 00380000
0:000> .echo test
12345

/ma参数把宏当作ascii字符串。其他参数可以查看文档去了解。

as GoUp r eax=eax+1; r ebx=ebx+1 定义了一个操作宏 as Cmd "dd esp 14; g" 定义了一个操作序列

表达式

Windbg默认表达式跟常在命令栏输入的指令一样,比如 poi(esp+4) 取出esp+4 地址出的内容。还可以使用C++表达式,加上@@前缀就可以了。 控制语句

Windbg支持常见控制语句:.if .else .elif .for .while .do .break .continue. 另外常用.printf输出信息,.block{}语句块也支持。

0:000> .dvalloc 10
Allocated 1000 bytes starting at 00390000
0:000> ea 00390000 "123456789"
0:000> .printf "%ma,%p",00390000,00390000
123456789,003900000:000> .printf "Str:%ma, Address:%p",00390000,poi(00390000)
Str:123456789, Address:34333231

.printf语句经常用做记录断点,在bp后面用引号包含,内层引号需要转义:

bp address “.printf \”%ma\\n\”,poi(esp);gc;”

内置函数

内置函数和各种实现神奇功能的语句要经常查看文档,调试的时候多写脚本才能熟练掌握。列举一些警察使用的脚本模板,用的时候做简单修改就可以。 常用脚本 输出某段地址内容:

.foreach (value {dd 610000 L4}) 
{
   as /x ${/v:myAlias} value + 1
   .block{.echo value myAlias}
}
ad myAlias

${/v:Name}用于检测宏,防止未定义等情况出现。as定义的别名要在新进入语句块才会起作用,所以要加.block。.foreach枚举要比for更好用。

断点控制:

bp Address "
r;
dd poi(esp+4) l 1;
bp /1 WS2_32!sendto
/"
as /x ${/v:buffer} poi(esp+8);
as /x ${/v:len} poi(esp+c);
.block
{
	$$ 
    .if ($(len) > 0x10)
	{
		db buffer l len;
        $$
    }
}
ad ${/v:buffer};
gc;
/"
gc;
"

在某个函数执行之后再下sendto断点,记录发送的内容。用于记录特定的数据包。

Windbg的脚本虽然晦涩,但是功能强大,提供了非常多的命令,能高效完成各种任务。如果像ImmunityDebugger一样支持python,界面再做得好看一些,当之无愧windows平台第一调试器。

Od

od画面经典,功能简洁,是大多数人的第一调试器。它的插件也非常多,所以好多人不关注OD的命令和脚本,实际上它的脚本功能也非常强大。脱壳爱好者肯定知道,od脚本和汇编语言非常像。先放一个完整的脱壳脚本,然后从常量变量等开始(因为和汇编很像,比windbg脚本好理解太多,可能看一个脚本就能照猫画虎写了)。

var diroep
var filename
var Dumped
cmp $VERSION, "1.47"
jb odbgver
find eip,#61#
cmp $RESULT,0
je Exit
mov diroep,$RESULT
bp diroep
run
bc diroep
find eip,#E9????????#
cmp $RESULT,0
je Exit
mov diroep,$RESULT
bp diroep
run
bc diroep
sto
an eip
GPI PROCESSNAME
mov filename,$RESULT
eval "de_{filename}.exe"
mov Dumped,$RESULT
dpe Dumped,eip
msg "This is the OEP!  Found By Ashraf Cracker"
msg "The File was dumped successfully don't close OllyDbg and try now to Fix IAT with ImportREC"
cmt eip, "<== Original Entry Point"
ret
odbgver:
msg "This script work with ODbgscript 1.47 or above"
ret
Exit:
msg "This Program isn't pack with UPX"
ret

常量变量和寄存器

数字默认是十六进制,十进制数字要加 . 后缀,浮点数保留两位小数。可直接使用寄存器名,eax、edx等;EFLAGS寄存器的各个标志可以单独使用,!CF、!OF、!ZF等;内存地址直接用中括号,[400000];od脚本的变量用var定义;$RESULT表示函数返回结果,$VERSION表示脚本环境版本;#数字#直接表示十六进制序列,可以用?通配符。

指令

类汇编指令:add、dec、neg、mul、div、cmp、and、or、not、xor、inc、itoa、atoi、跳转指令(ja、jb等)、rol、ror、shl、shr、scmp(字符串比较)

操作数可以是内存地址,也可以是{变量名},对变量名做扩展,比如 mov a, “eax”,{a}表示eax。

可执行文件相关指令:

Sti 单步步入
Sto 单步步进
an eip 解释指令,类似ctrl+A
asm 地址,指令 修改指令,修改后长度返回到$RESULT
opcode 地址 反汇编地址处代码
cmt 地址,注释 加注释
dm  地址,大小,文件名 提取内存数据
dma 地址,大小,文件名 文件中添加内存数据
dpe 文件名,地址  提取可执行文件
wrt 将数据写入文件
Wrta 将数据添加到文件
esti 异常忽略步入
esto 异常忽略继续
eval 计算表达式值,结果放入$RESULT,常用于字符串扩展,大括号内的变量被扩展
exec/ende 两个关键字之间的语句插入被调试进程中执行
go 地址 执行到地址处
run 执行
rtr 执行到返回
preop 地址 执行序列回溯(这个非常非常有用)
Key 仿真键盘输入
Reset 重新加载
Ret 退出脚本
内存操作指令
alloc NUMBER 申请内存,结果在$RESULT中
free BUFFER 释放内存
Fill 地址,大小,值 内存填充
Readstr 读取某地址字符串,可指定大小
Rev 按字节反转

断点指令:

bp、ba、bpcnd(条件断点)、bphws(硬件断点,第二个参数设置r、w、x)、bpl(记录断点,blp 地址,表达式)、bplcnd(bplcnd 地址,,表达式, 条件)、bprm内存读断点,第二个参数大小)、bpwm内存写断点、bpx对函数字符串下断很常用

交互指令:

ask STRING 用户输入数据,保存在$RESULT中
特殊指令
COB 忽略中断
COE 忽略异常
EOB label 中断控制
EOE label 异常控制
DBH 隐藏调试器
Msg 消息
Msgyn 是否对话框
Setoption 动态设置调试选项
内置函数
find 地址,内容,大小 查找内容,结果返回到$RESULT
findcmd 地址,指令字符串 查找字符串表示的指令,如findcmd eip,”ret 10”
Findop 查找指令
Findmem 查找内存
Gapi 获取地址处API信息
Gcmt 获取地址处注释
Gci 获取地址处各种信息,第二个参数可以用不同宏表示功能
Gmemi 获取该地址内存属性,第二个参数也是功能宏
Gmi 获取模块信息
Gn 地址 获取地址的函数名(利用IAT和INT)
Gpa GetProcAddress函数
Gpi 获取进程信息
控制指令
条件控制:ifa、ifae、ifb、ifbe、ifg、ifge、ifl、ifle、ifq、ifnq、else、endif

Od脚本最大的特点就是跟汇编非常相似,你想做的每一步都可以用指令来模拟。而且脚本也比较好看。优化重复性工作有很大帮助。众多的脱壳脚本就是典型例子,将枯燥乏味的脱壳按照自己的操作过程写出脚本,实现自动化。

Immdbg

immunity debugger和python配合写脚本非常舒服。windbg脚本功能强大但是语句晦涩,od能把自己的想法一步一步用脚本实现,immdbg脚本就更高层、更优雅了。熟悉python和调试原理的话可以很快上手。

from immlib import *
def main(args):
    imm = Debugger()
    search_code = " ".join(args)
    search_bytes = imm.assemble( search_code )
    search_results = imm.search( search_bytes )
    for hit in search_results:
        code_page = imm.getMemoryPageByAddress(hit)
        access = code_page.getAccess(human = True)
        if "execute" in access.lower():
            imm.log("[*] Found: %s (0x%08x)" % (search_code, hit),
                    address = hit)
    return "[*] Finished searching for instructions, check the Log window"

脚本必须有一个main函数,接受参数列表。在main里Debugger()返回一个新的调试器对象。脚本输出结果在状态栏输出。main函数首先初始化一个immlib.Debugger对象,这个对象有很多调试相关的方法,查看官方文档。

可以利用这些库写出非常好用的调试脚本。实际上已经有了一个专用于漏洞调试的脚本:mona.py。它就是利用immdbg的库来实现的,不过也支持windbg。

最后

作为一个逆向爱好者,深感好的工具能极大提高工作效率。最近也在尽量使用windbg脚本和od脚本来辅助调试,写的时候有很大困难,但是省去了调试过程中一次一次的手动测试,真的非常方便。