安全中国首页 > 文章中心 > 基础知识
 
安全中国网友投稿专用上传FTP空间:
Ftp服务器:download.anqn.com
Ftp端口:21
用户名:anqn
密 码:anqn.com
 

2.1.4 基本操作(1)(图)

更新时间:2008-8-20 0:30:54
责任编辑:池天
热 点:
2.1.4 基本操作
对于习惯Borland开发环境的朋友来说,用OllyDbg比较容易上手,例如单步功能是用F7键和F8键等,这与Borland的产品习惯完全一样。
在这里,以一个用Visual C++ 6.0编译的程序TraceMe来讲解OllyDbg的操作,编译时优化选项按默认设置为“Maximize speed”。读者也可以按“Minimize Size”优化选项编译一下,并与本文比较。因为优化选项不同,生成的汇编代码也会有所不同。
1.准备工作
拆解一个Windows程序要比拆解一个DOS程序容易得多,因为在Windows中,只要API函数被使用,想对寻找蛛丝马迹的人隐藏一些东西是比较困难的。因此分析一个程序,用什么API函数作为切入点就显得比较关键了,如果有些编程经验,这方面就更得心应手了。
为了便于理解,先简单地看一下TraceMe的序列号验证流程,如图2.9所示。将姓名与序列号输入到文字框中,程序调用GetDlgItemTextA函数把字符读出来,然后进行计算,最后用函数lstrcmp进行比较。因此,这些调用的函数就是解密跟踪的目标,用这些函数作为断点,跟踪程序的序列号验证过程就能找出正确的序列号。
 
图2.9  TraceMe程序序列号验证过程
2.加载目标文件调试
为了能让OllyDbg中断在程序的入口点,加载程序前必须要设置一下。运行OllyDbg后,单击菜单“Options/Debugging options”,打开调试选项配置对话框,再单击“Event”标签,如图2.10所示。这里设置OllyDbg对中断入口点、模块加载/卸载、线程创建结束等事件的处理,一般调试只需要将暂停点设置在“Entry point of main module”或“WinMain”即可。
 
图2.10  设置OllyDbg第一次暂停
System breakpoint:系统断点,OllyDbg用CreateProcessA加载DEBUG_ONLY_THIS_PROCESS参数执行,程序运行之后会触发一个INT 3,在系统空间里。
 
Entry point of main module:主模块的入口点,即文件的入口点。
 
WinMain:程序的WinMain()函数入口点,即使设置这个选项,OllyDbg一般也只会中断在文件入口点处。
设置好后,单击菜单“File/Open”打开TraceMe.exe,此时OllyDbg会中断在TraceMe的入口点,如图2.11所示。光条停在4013A0这行,这个4013A0就是程序开始执行的入口地址(EntryPoint)。
 
图2.11  OllyDbg加载目标程序停在入口点
在图2.11中,代码中各部分含义如下:
 
① 虚拟地址:一般情况下,同一程序的同一条指令在不同系统环境下此值相同;
 
② 机器码:这就是CPU执行的机器代码;
 
③ 汇编指令:和机器码对应的程序代码。
3.单步跟踪
调试器的一个最基本功能就是动态跟踪,OllyDbg在菜单“Debug”里控制运行的命令,各个菜单项都有相应的快捷键。OllyDbg的单步跟踪功能键如表2-1所示。
表2-1  OllyDbg的单步跟踪功能键
OllyDbg功能键
   
F7
单步步进,遇到CALL跟进
F8
单步步过,遇到CALL跳过,不跟进
Ctrl+F9
直到出现RET指定时中断
Alt+F9
若进入系统领空,此命令可瞬间回到应用程序领空
F9
运行程序
F8键在调试中用得很频繁,可以一句句地单步执行汇编指令,遇到CALL指令不会跟进,而路过。例如:
004013F7  xor     esi, esi
004013F9  push    esi
004013FA  call    00401DA0            ;按F8键不会进去,而直接路过这个CALL
004013FF  pop     ecx
00401400  test    eax, eax
F7和F8功能键的主要差别就在于若遇到CALL、LOOP等指令,F8键是路过,而F7键是跟进去。
004013F7  xor     esi, esi
004013F9  push    esi
004013FA  call    00401DA0            ;按F7键会进入这个CALL

00401DA0  xor     eax, eax        ;上面那句4013FA,按F7键就会来到这里
00401DA2  push    0 
00401DA4  cmp     [esp+8], eax
00401DA8  push    1000
00401DAD  sete    al
……
}
当要重复按多次F7键或F8键时,OllyDbg提供了“Ctrl+F7”和“Ctrl+F8”快捷键,直到用户按Esc键、F12键或遇到其他断点时停止。
当位于某个CALL中,这时想返回到调用这个CALL的地方时,可以按“Ctrl+F9”快捷键执行“执行到返回(Execute till return)”功能。OllyDbg就会停在遇到的第一个返回命令(RET、RETF或者IRET),这样可以很方便地略过一些没用的代码。例如上面的代码,在401DA0这行,如果按“Ctrl+F9”快捷键就会回到4013FA这句。遇到RET指令是暂停还是步过可以在选项里设置,方法是:打开调试设置选项对话框,在“Trace”页面,设置“After Execting till RET,step over RET(执行到RET后,单步步过RET)”。
如果跟进系统DLL提供的API函数中,此时想返回到应用程序领空里,可以按快捷键“Alt+F9”执行“Execute till user code(执行到用户代码)”命令。例如:
004013C0  push    ebx
004013C1  push    esi
004013C2  push    edi
004013C3  mov     [ebp-18], esp
004013C6  call    [<&KERNEL32.GetVersion>]   ;按F7键跟进 KERNEL32.dll里
004013CC  xor     edx, edx

在上面的4013C6一行,按F7键就可跟进系统KERNEL32.DLL里的领空:
7C8114AB kernel32.GetVersion mov     eax, fs:[18]
7C8114B1                         mov     ecx, [eax+30]   ;假设当前光标在这行
7C8114B4                         mov     eax, [ecx+B0]
7C8114BA                         movzx   edx, word ptr [ecx+AC]
7C8114C1                         xor     eax, FFFFFFFE
像地址7C8114AB等都是系统DLL所在的地址空间,这时只要按一下快捷键“Alt+F9”就可回到应用程序领空里。代码如下:
 004013C0   push    ebx
004013C1   push    esi
004013C2   push    edi
004013C3   mov     [ebp-18], esp
004013C6   call    [<&KERNEL32.GetVersion>]    
004013CC   xor     edx, edx                      ;会返回到此行
注意:所谓领空,实际上是指在某一时刻,CPU的CS:EIP所指向的某段代码的所有者。
如果不想单步跟踪,让程序直接运行起来,可以按F9键或单击工具栏中的   按钮。如果想重新调试目标程序,可以按“Ctrl+F2”快捷键或单击工具栏中的  按钮,Ollydbg结束被调试进程并重新加载它。有时程序进入死循环,可以按F12键暂停程序。
4.设置断点
断点是调试器的一个重要功能,可以让程序中断在需要的地方,从而方便对其分析。最常用的断点是INT 3断点,其原理是Ollydbg将断点地址处的代码修改为INT 3指令。在图2.12中,将光标移动到4013A5一行,按F2键即可设置一个断点,再按一次F2键取消断点。也可以用鼠标双击“Hex dump”列中相应的行设置断点,再次双击取消断点。当关闭程序时,OllyDbg会自动将当前应用程序的断点位置保存在其安装目录*.udd文件中,以便下次运行时,这些断点继续有效。如果将断点设置到当前应用程序代码外,OllyDbg将会警告。可以在菜单“Options/Debugging options/Security”选项里将“Warn when breakpoint is outside the code section”取消选中,以关闭这个警告。
 
图2.12  设置断点
现在开始一个完整的调试分析过程,取消开始设置的所有断点,在OllyDbg里按F9键将实例TraceMe.exe运行起来,如图2.13所示。
 
图2.13  实例运行起来的界面
字符通常利用Windows文本框输入。为了检查输入的字符,程序常采用下面这些函数把文本框中的内容读出来,如表2-2所示。
表2-2  程序采用的将文本框中内容读出来的函数
16
32位(ANSI版)
32位(Unicode版)
GetDlgItemText
GetDlgItemTextA
GetDlgItemTextW
GetWindowText
GetWindowTextA
GetWindowTextW
一般事先不会知道程序具体是调用了什么函数来处理字符的,只好多试几遍,找出相关的函数。
首先,需要在OllyDbg中设定一个“陷阱”(或称断点)。因为这个TraceMe是32位ANSI版的程序,所以在GetDlgItemTextA处设一个断点。按“Ctrl+G”键打开跟随表达式的窗口,输入GetDlgItemTextA字符,如图2.14所示。
 
图2.14  打开跟随表达式窗口
注意:OllyDbg里对API的大小写敏感,输入的函数名大小写必须正确。
单击OK按钮后,会来到系统USER32.DLL中的GetDlgItemTextA函数入口处,如图2.15所示。
 
(点击查看大图)图2.15  跳到函数入口处
在77D6AC1E这一行,按F2键设个断点,即在GetDlgItemTextA函数入口处设了断点(操作系统版本不同,这个函数入口地址是不一样的),如果这个函数被调用,OllyDbg就会中断。
注意:在Windows 9x系统中,OllyDbg是无法对API函数入口点下断的,因此不能用此方法设断。

下一步可以列出所有断点来检查一下,按“Alt+B”快捷键或单击  按钮打开断点窗口,如图2.16所示。
 
(点击查看大图)图2.16  断点窗口
这里可以显示除硬件断点外的其他断点,其中“Always”表示断点处于激活状态,“Disable”表示断点停用,按空格键可切换其状态,也可以用鼠标右键菜单管理这些断点。
现已经设定了断点,可以捕捉任何对GetDlgItemTextA函数的调用。然后输入姓名和序列号,如姓名为“pediy”,序列号为“1212”。单击“Check”按钮,程序中断在OllyDbg中,就在函数GetDlgItemTextA开始的地方。
也可通过输入表设置断点。在OllyDbg里,按“Ctrl+N”键打开应用程序的输入表,会发现USER32.GetDlgItemTextA函数,在这个函数上按Enter键或右键菜单执行“Find references to import”命令打开调用此函数的参考代码窗口,找到相应的代码,按Enter键即可切换到相应的代码,接下来按F2键设置断点。
5.调试分析
按“Alt+F9”键,回到调用函数的地方,当然也可以按F8键单步走出GetDlgItemTextA这个函数。OllyDbg非常强大,已将各函数的调用参数及当前值都注释出来了。相关代码如下:
 004011AE  push    51                ; /Count = 51 (81.)
004011B0  push    eax               ; |Buffer
004011B1  push    6E                ; |ControlID = 6E (110.)
004011B3  push    esi               ; |hWnd
004011B4  call    edi               ; \GetDlgItemTextA
004011B6  lea     ecx, [esp+9C]   ; 从GetDlgItemTextA函数里出来会回到这行
004011BD  push    65                ; /Count = 65 (101.)
004011BF  push    ecx               ; |Buffer
004011C0  push    3E8               ; |ControlID = 3E8 (1000.)
004011C5  push    esi               ; |hWnd
004011C6  mov     ebx, eax         ; |
004011C8  call    edi               ; \GetDlgItemTextA
来到TraceMe领空后,可以按“Alt+B”键打开断点窗口,将GetDlgItemTextA处的断点禁止。
很多时候必须重复跟踪同一段代码,因此可以先设置一个断点。将光标移到4011AE一行,按F2键设置新的断点,以方便反复跟踪调试。
中断后的代码如下(可结合源码阅读):
;len=GetDlgItemText(hDlg,IDC_TXT0,cName,sizeof(cName)/sizeof(TCHAR)+1)
004011AA   lea eax, [esp+4C]
004011AE   push 00000051           ; 参数:最大字符数
004011B0    push eax                 ; 参数:文本缓冲区指针
004011B1   push 0000006E           ; 参数:控件标识(ID号),见resource.h
004011B3    push esi                 ; 参数:对话框句柄
004011B4   call edi                 ; 调用GetDlgItemTextA函数取姓名
004011B6   lea ecx, [esp+9C]      ; 上句执行后,姓名长度返回到eax中
;----------------------------------------
;GetDlgItemText(hDlg,IDC_TXT1,cCode,sizeof(cCode)/sizeof(TCHAR)+1)
004011BD    push 00000065           ; 最大字符数
004011BF    push ecx                 ; 文本缓冲区指针
004011C0    push 000003E8           ; 控件标识(ID号)
004011C5    push esi                 ; 对话框句柄
004011C6    mov ebx, eax            ; 将用户名的长度转到ebx中
004011C8    call edi                  ; 调用GetDlgItemTextA函数取序列号
;----------------------------------------
; if (cName[0] == 0 || len<5)
004011CA   mov al, byte ptr [esp+4C]; 将用户名第一个字节给al
004011CE   test al, al            ; 检查有没有输入用户名
004011D0   je 00401248                ; 如果没有输入用户名跳走,告知输入字符太少
004011D2    cmp ebx, 00000005     ; 用户名长度是<5
004011D5   jl 00401248
;----------------------------------------
;GenRegCode(cCode, cName ,len) (GenRegCode子程序采用C调用约定)
004011D7   lea edx, [esp+4C]      ; 用户名地址放到edx中
004011DB   push ebx                 ; 用户名长度入栈(len参数)
004011DC    lea eax, [esp+000000A0]  ; 序列号地址放到eax中
004011E3   push edx                   ; 用户名入栈(cName参数)
004011E4   push eax                   ; 序列号入栈(cCode参数)
004011E5   call 00401340             ; 这个CALL就是GenRegCode函数
004011EA   mov edi, [004040BC]
004011F0   add esp, 0000000C        ; 平衡堆栈(C调用约定)
004011F3   test eax, eax           ; eax=0注册失败,eax=1注册成功
004011F5    je 0040122E
在阅读这些代码时:
 
要搞清各API函数的定义(查看相关API手册)。
 
API函数基本采用的是__stdcall调用约定,即函数入口参数按从右到左的顺序入栈,并由被调用者清理栈中参数,返回值放在eax寄存器中。因此,对相关的API函数要分析其前的push指令,这些指令将参数放进堆栈以传送给API调用。整个跟踪过程中要关注堆栈数据变化。
 
C代码中的子程序采用的是C调用约定,函数入口参数按从右到左的顺序入栈,由调用者清理栈中的参数。
有关调用约定、参数传递等知识,可以从本书第4章获得。阅读上面代码时,需理解的GetDlgItemTextA函数原型如下:
UINT GetDlgItemText(      
HWND hDlg,        // 对话框句柄
int nIDDlgItem,       // 控件标识(ID号)
LPTSTR lpString,      // 文本缓冲区指针
int nMaxCount        // 最大字符数
);
GetDlgItemText采用标准调用约定,参数按从右到左的顺序入栈。例如本例中的汇编代码:
 004011AE  push    51             ; int nMaxCount
004011B0  push    eax           ; LPTSTR lpString,
004011B1  push    6E          ; int nIDDlgItem
004011B3  push    esi         ; HWND hDlg
004011B4  call    GetDlgItemTextA         
当GetWindowText函数执行后,将把取出的文本放到由lpString(LPTSTR是一个长的指针,指向由空字符终止的字符串)指定的位置。如想看到输入的字符串,跟踪的时候,在4011B0一行停住,在eax寄存器单击右键,执行菜单“Follow in Dump”命令查看数据窗口中的内容,当然此时数据窗口中没什么有价值的东西。继续按F8键单步执行完下面一句:

004011B4   call edi            ; GetDlgItemTextA函数取姓名

此时GetDlgItemTextA函数已将字符串取出,放到eax所指的地址里。数据窗口右边字符段显示出刚输入的字符“pediy”,如图2.17所示。
 
(点击查看大图)图2.17  数据窗口查看字符
6.保存修改后的文件
在上一节中,已找到序列号的判断核心,这里的一段代码是关键:
004011E5   call   00401340                   ; 序列号计算的CALL
004011EA   mov    edi, [<&USER32.GetDlgItem>]
004011F0   add    esp, 0C
004011F3   test  eax, eax                   ; eax=0注册失败,eax=1注册成功
004011F5   je     short 0040122E            ; 不跳转则成功

 
相关文章
一日一文章
 
一日一软件
一日一动画