3. 调试示例
首先要说明的是WinDbg中的某些指令是区分大小写的。最后我将以实际调试过程来说明Ollydbg与WinDbg的差别。先介绍一下目标软件,black light beta版。自动查ROOTKIT,使用修改文件名并重新启动系统的方法清除ROOTKIT。国庆节到期,爆破时间限制不难,设断改跳转就可以了,如果写这个,就没什么好写了。用PEID查了一下,没有壳,有TLS表。这个才是这次调试的主角,我希望通过调试来增强对TLSCALLBACK的了解。
先使用Ollydbg载入目标程序,如果你的设置是首次暂停在WINMAIN,并且没有使用任何关于TLS的增强插件,那么你会看到窗口出来了,但是Ollydbg却提示“进程已经终止,退出代码0”。
现在用WinDbg载入程序,选中“debug child processes also”。程序一开始停在系统断点。输入g $exentry.没断成功,落在子进程的系统断点。再次输入g $exentry,产生了一个非法访问的错误。gN $exentry,终于能断在EP了。好了,如果你喜欢用这个软件,你可以爆破它的时间限制了。
Ollydbg与WinDbg的第一次较量,在没有任何插件的情况,WinDbg由于支持子进程调试,使得它在这个特例里要简单一些。
现在我们可以知道,程序在入口点之前就已经被运行,很可能是TLSCALLBACK,并在建立新进程后退出。
-------------------------------------------------------关于多进程调试-------------------------------------------------------------
0:000> | ;$$查询当前进程数,注意0:000表示当前调试的进程的识别号是0
. 0 id: 46c create name: beta.exe
# 1 id: 410 child name: beta.exe
0:000> | 1 s ;$$切换到子进程1。S是切换参数,当前状态是默认显示的。
eax=00000000 ebx=7ffdf000 ecx=00010101 edx=ffffffff esi=00000000 edi=00000000
eip=0041e3ec esp=0012ffc4 ebp=0012fff0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
beta+0x1e3ec:
0041e3ec 6a60 push 60h
1:001> ;$$切换之后命令提示符变成1:001了。
--------------------------------------------------------------回到正题------------------------------------------------------------
现在我们需要调试TLS处的代码。回到Ollydbg,我用的插件是shoooo大大的WMOS,能显示TLSCALLBACK的首址。为了方便每次都停在TLSCALLBACK,我在入口处设了硬断,重新运行之后,我们可以开始调试了。
0040C7C0 8B4424 08 mov eax,dword ptr ss:[esp+8] 一开始便从初试栈中读取数据
0040C7C4 83EC 54 sub esp,54
0040C7C7 56 push esi 初始栈-58
0040C7C8 BE 01000000 mov esi,1
0040C7CD 3BC6 cmp eax,esi 比较初始栈+8是否为1
0040C7CF 0F85 E4010000 jnz blbeta.0040C9B9 没有跳转
0040C7D5 53 push ebx 初始栈-5c
0040C7D6 8D4424 64 lea eax,dword ptr ss:[esp+64] EAX=初始栈+8
0040C7DA 50 push eax 此处是0040c7e2的参数
0040C7DB FF15 ACA34300 call dword ptr ds:[<&KERNEL32.GetCommandLineW>;
0040C7E1 50 push eax
0040C7E2 FF15 60A44300 call dword ptr ds:[<&SHELL32.CommandLineToArg>;
0040C7E8 8BD8 mov ebx,eax EBX=参数首址
0040C7EA 397424 64 cmp dword ptr ss:[esp+64],esi 参数长度与1比较
0040C7EE 76 19 jbe short blbeta.0040C809 跳了
----------------------------------------------------------如果没有跳会怎么样?----------------------------------------------------
0040C7F0 8B4B 04 mov ecx,dword ptr ds:[ebx+4]
0040C7F3 68 74364400 push blbeta.00443674 ; UNICODE "/q"
0040C7F8 51 push ecx
0040C7F9 E8 B1290100 call blbeta.0041F1AF 从传入参数看,此CALL判断运行参数是否为/q
0040C7FE 83C4 08 add esp,8
0040C801 85C0 test eax,eax
0040C803 0F84 AF010000 je blbeta.0040C9B8 可能是参数/q对应的功能模块
Ollydbg的参考字符功能,基本可以猜到该程序能以参数启动,格式为“/ 参数”,长度不等于1
--------------------------------------------------------------回到正题------------------------------------------------------------
0040C809 33D2 xor edx,edx
0040C80B 55 push ebp 初始栈-60
0040C80C 57 push edi 初始栈-64
0040C80D 33C0 xor eax,eax
0040C80F 895424 10 mov dword ptr ss:[esp+10],edx
0040C813 B9 11000000 mov ecx,11
0040C818 8D7C24 20 lea edi,dword ptr ss:[esp+20]
0040C81C F3:AB rep stos dword ptr es:[edi]
0040C81E 397424 6C cmp dword ptr ss:[esp+6C],esi 仍然是跟初始栈+8比较
0040C822 895424 14 mov dword ptr ss:[esp+14],edx
0040C826 895424 18 mov dword ptr ss:[esp+18],edx
0040C82A C74424 20 44000>mov dword ptr ss:[esp+20],44
0040C832 895424 1C mov dword ptr ss:[esp+1C],edx
0040C836 76 70 jbe short blbeta.0040C8A8 ESI没有改变过,此处相当于JMP
-----------------------------------------------junk code如何引诱你改变EIP跟进去?------------------------------------------------
0040C838 8B43 04 mov eax,dword ptr ds:[ebx+4]
0040C83B 68 64364400 push blbeta.00443664 ; UNICODE "/expert"
0040C840 50 push eax
0040C841 E8 69290100 call blbeta.0041F1AF
0040C846 83C4 08 add esp,8
0040C849 85C0 test eax,eax
0040C84B 75 5B jnz short blbeta.0040C8A8
首先是一个非常引诱的字符参考“/expert”,跟进去仿佛能开启高级功能。然后是0040C8A8由于跟上面跳转一样,或者会让你觉得不跳转,继续跟下去便是开启高级功能的关键。攻城为下攻心为上,如果我在这里安排一些破坏性的代码。。。。。。
--------------------------------------------------------------回到正题------------------------------------------------------------
虽然跳过了一大堆junk code不过我猜肯定不止那么少。
0040C8A8 8B0B mov ecx,dword ptr ds:[ebx]
0040C8AA 51 push ecx
0040C8AB E8 21200100 call blbeta.0041E8D1
0040C8B0 68 74364400 push blbeta.00443674 ; UNICODE "/q"
0040C8B5 8BF0 mov esi,eax
0040C8B7 E8 15200100 call blbeta.0041E8D1 前面已经有处理/q的例程了,这里还处理?忽略
0040C8BC 83C4 08 add esp,8
0040C8BF 6A 04 push 4
0040C8C1 8D6C06 04 lea ebp,dword ptr ds:[esi+eax+4]
0040C8C5 68 00100000 push 1000
0040C8CA 8D7C2D 00 lea edi,dword ptr ss:[ebp+ebp]
0040C8CE 57 push edi
0040C8CF 6A 00 push 0
0040C8D1 FF15 B0A34300 call dword ptr ds:[<&KERNEL32.VirtualAlloc>] ; KERNEL32.VirtualAlloc
0040C8D7 8BCF mov ecx,edi 这条指令跟下一条指令相当于mov edx,edi
0040C8D9 8BD1 mov edx,ecx 到这里还有junk code
0040C8DB 8BF0 mov esi,eax 又是一个junk code
0040C8DD C1E9 02 shr ecx,2
0040C8E0 33C0 xor eax,eax
0040C8E2 8BFE mov edi,esi 相当于mov edi,eax
0040C8E4 F3:AB rep stos dword ptr es:[edi]
代码分析=体力活,本文重点介绍工具使用,而不是代码还原。现在忽略细节,直接看它用了什么API。
CALL KERNEL32.VirtualAlloc
Call KERNEL32.GetModuleFileNameW
call KERNEL32.CreateEventW
call KERNEL32.CreateProcessW 其中|CommandLine = ""D:\blbeta.exe" /q"
call KERNEL32.WaitForSingleObject
call KERNEL32.CloseHandle
call KERNEL32.VirtualFree
call KERNEL32.ExitProcess
ExitProcess之后又来到一开始的地方了。
0040C7C0 8B4424 08 mov eax,dword ptr ss:[esp+8]
0040C7C4 83EC 54 sub esp,54
0040C7C7 56 push esi
0040C7C8 BE 01000000 mov esi,1
0040C7CD 3BC6 cmp eax,esi
0040C7CF 0F85 E4010000 jnz blbeta.0040C9B9 这次跳转成功了
0040C9B9 5E pop esi
0040C9BA 83C4 54 add esp,54
0040C9BD C2 0C00 retn 0C retn后ESP-0c
关于TLS:现在我们知道TLSCALLBACK通过初始栈+8的数据来判断是什么情况下被调用。当它的值为1时,代表TLS被首次运行。假如只是简单判断是否第一次被运行,只要判断是否为1就够了。为什么结束时是retn 0c?现在让我们单步进入系统区域看看:
77F86209 FF75 14 push dword ptr ss:[ebp+14]
77F8620C FF75 10 push dword ptr ss:[ebp+10]
77F8620F FF75 0C push dword ptr ss:[ebp+C]
77F86212 FF55 08 call dword ptr ss:[ebp+8]
77F86215 8BE6 mov esp,esi
现在可以知道call dword ptr ss:[ebp+8]的最后是调用TLSCALLBACK,所以结束时必须代替系统释放堆栈,通过这次调试,我们可以用汇编写TLSCALLBACK,并把ANTI DEBUG代码放在这里欺负新人了: P
我们来看看,如果在WinDbg中,应该怎么做。为了让WinDbg看起来容易用一点,我又写了个SCRIPT,它在附件里的名字是gs。复制到程序安装目录,输入$$><gs,然后按ENTER。如果程序有TLSCALLBACK,将会断在那里(用了bp设断,然后g的方法,以后也会自动断在那里),没有就断在EP。
--------------------------------------------------------------gs的注释------------------------------------------------------------
r @$t0=@$peb+8
r @$t0=poi(@$t0) ;$$通过peb获得基址
r @$t1=@$t0+3c
r @$t1=poi(@$t1)+@$t0
r @$t1=@$t1+c0
r @$t1=poi(@$t1) ;$$取得TLS表基址
.if (@$t1!=0){ ;$$判断TLS表是否存在
r @$t1=@$t1+@$t0
r @$t1=@$t1+c
r @$t1=poi(@$t1)
r @$t1=poi(@$t1)
.if(@$t1!=0){ ;$$判断TLSCALLBACK是否存在
r @$t0=0 ;$$是否有TLSCALLBACK的标记
bp @$t1
g
}
}
.if (@$t0!=0){ ;$$没有TLSCALLBACK就GO EP
g $exentry
}
-------------------------------------------------------------注释结束------------------------------------------------------------
现在程序停在入口点了。我用OD的时候习惯静态分析,然后分析到我感兴趣的地方,设断,F9,看寄存器看堆栈。这在WinDbg中不可能,它的反汇编功能真的不怎么样。例如我单步到这里:
call dword ptr [beta+0x3a3ac (0043a3ac)] ds:0023:0043a3ac={KERNEL32!GetCommandLineW (77e7386f)}
才会显示这个是API调用,在反汇编窗口里面只是单纯显示call dword ptr [beta+0x3a3ac (0043a3ac)]。你必须跟进那个call,调用栈才有反应,还不如直接用db esp查看堆栈。而且进去之后也不会提示那些是什么参数。所以我一般在使用的时候,同时打开MSDN的搜索页面,当然还有IDA。
0040c7cf 0f85e4010000 jne beta+0xc9b9 (0040c9b9) [br=0]
后面br=0代表没有跳转成功,如果是1,那么就是成功跳转。当然WinDbg也有方便的地方。在Ollydbg的例子中,我是下拉反汇编窗口直接看后面用什么API。而在WinDbg中,你可以使用pc命令,现在直接自动单步到CALL,然后停下来,而不需要关心在这中间遇到多少个条件跳转。
第二次较量,在进行代码分析时WinDbg要逊色一点,它的SCRIPT功能倒是不错,可以弥补一些差距。
关于目标程序:现在我们知道程序通过参数/q启动一个新的进程。用Ollydbg和WinDbg带参数/q重新载入程序。开始的时候,我们曾经用WinDbg双进程调试过,当时新进程产生了一个错误,忽略错误运行之后,我们能停在EP。我希望知道系统是怎么处理这个错误的。
Ollydbg中按F9到达错误处,然后按SHIFT+F7进入SEH链中的第一个例程
WinDbg中按F5到达错误处,然后还是SCRIPT,对应附件中的lseh和disasm这两个文件。一定要复制到安装目录下。$$><lseh使用SCRIPT,效果如图:
这个SCRIPT的功能是列举所有SEH例程,可以下断,也可以反汇编该例程的前10条命令。我用DML做了一个简单的可视化界面。显然可以做一个界面,通过点鼠标来单步之类的,这样HOOK 快捷键ANTI DEBUG就不可行了。如果你点一下反汇编,你会发现系统DLL里面有很多符号标记,相反Ollydbg却认不出来。也许Micro$oft开发WinDbg就是鼓励别人调试它的产品。
-----------------------------------------------------------SCRIPT注释------------------------------------------------------------
r @$t0=poi(@$teb) ;$$取得SEH链表的第一个元素
.for (r $t3 = 0; @$t0 !=-1; r $t3 = @$t3 + 1){ ;$$到达终点则停止循环
r @$t1=poi(@$t0+4)
.printf /D "[%x] %X <link cmd=\"bp /1 %x\">下断</link> <link cmd=\"r @$t2=%x;$$><disasm \">反汇编</link>\n",@$t3,@$t1,@$t1,@$t1 ;$$dml的标记跟HTML很象。在””中使用”你需要在”前面加\
r @$t0=poi(@$t0) ;$$ 即\”\”=””。.printf的用法跟C语言中的一样,我希望你懂C语言
}
.if (@$t0=-1){
.echo "已达SEH链结尾"
}
反汇编的链接命令是使用disasm这个SCRIPT,并以@$t2来传递参数,下面来看看它的代码:
u @$t2 l10 ;$$反汇编@$t2指向的地址
.printf /D "<link cmd=\"$$><Lseh\">返回</link>\n" ;$$使用lseh
--------------------------------------------------------------结束----------------------------------------------------------------
这有点花俏,我写了另外一个SCRIPT,对应附件中的SEH。它的功能是在第一个SEH例程下断了。gN,断下来。
我们看到处理错误的函数在系统区域,由于忽略错误能回到EP,也就是说该系统SEH例程可以处理错误,然后返回到某处用户代码,于是我在WinDbg中使用guc这个SCRIPT,运行到用户代码为止,结果窗体出来了还没停下来。现在换Ollydbg取消了入口处的断点之后,使用执行到用户代码,效果跟WinDbg一样。重新开始,经过层层跟踪之后发现出问题的地方是INT 2E,单步过去就变成运行。看来R3调试器跟我一样,不认识那东西。也就是说,可以用INT 2E等调用来对抗模拟跟踪,当然这需要你掌握它的使用。至于TLSCALLBACK,或者我们可以假定它出错,系统就会把控制权交回EP,有待实践证明或高手调试证明了。
第三次较量,算是平手了。
最后简单的总结一下WinDbg在R3下的表现:
1. 强大的script功能,适用于自动化完成一些任务。
2. DML,提供一个简陋的可视化界面与用户进行简单的互动。
3. HTML文件输出支持,这是我在学习DML的过程中发现的。“.logopen”建立HTML文件,“.printf”输出HTML标记,“.logclose”关闭文件。
4. 适合调试系统内核,它对内核的符号标记比Ollydbg要多。为了获得更多WINDOWS内核的符号支持,你需要通过菜单"File=>Symbol File Path"
然后输入
“srv*%SystemRoot%\symbols*http://msdl.microsoft.com/download/symbols”
注意:此命令行使WinDbg到micro$oft的网站下载符号文件,由于网速以及符号文件库比较大的关系,会导致初次使用时较费时(我这里校园网,等了几分钟)。此外假如程序也加载了一些非micro$oft的模块,WinDbg也会尝试在该网站查找,因此假如你对内核不感兴趣,请把路径设为空格。