跳转至

binarybook-chapter1-调试

原来我只会用devc++,调试只会用cout打印变量观察,我就是个傻懒子

调试原理

以gdb调试器为例,参考原来gdb的底层调试原理这么简单 - 知乎 (zhihu.com)

大体意思是:

gbd进程会调用fork函数创建一个子进程,该子进程会调用ptrace函数,让父进程gdb进程托管其所有的信号,然后子进程execv需要调试的程序,

img

如此该程序将完全处在gdb父进程的掌控之下

img

断点的原理:

gbd进程维护一个断点链表,

gdb进程将我们要下断点的指定行保存在断点列表,然后用int 3中断指令替换断点行指令(字节不足则补nop)

当子进程运行到断点处时执行一个int 3指令,操作系统原本应该向该子进程发送一个SIGTRAP指令让其陷入内核,但是这一信号被父进程gdb截胡了

此时子进程中的int 3已经执行过了,eip指向了下一条指令

现在轮到父进程登场了

父进程gdb收到了SIGTRAP指令,发现是子进程的哪一行引起了中断指令,然后去断点链表找到对应行的记录,再给子进程该回去,然后将子进程的eip程序计数器退一步,让子进程重新执行

这么麻烦实现了一个什么功能呢?

子进程会在断点处int 3指令停下等待信号,这就给了父进程趁机读写子进程堆栈和寄存器的机会

IDA pro静态观察wsample01b.exe

例程来自有趣的二进制kenjiaiko/binarybook (github.com)

winmain函数的行为

.text:00401080 ; int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
.text:00401080 _wWinMain@16    proc near               ; CODE XREF: ___tmainCRTStartup+153↓p
.text:00401080
.text:00401080 hInstance       = dword ptr  4
.text:00401080 hPrevInstance   = dword ptr  8
.text:00401080 lpCmdLine       = dword ptr  0Ch
.text:00401080 nShowCmd        = dword ptr  10h
.text:00401080
.text:00401080                 call    sub_401000      ; 上来西安调用函数
.text:00401080                                         ;
.text:00401085                 push    0               ; uType   
.text:00401087                 push    offset Caption  ; "MESSAGE"      ;Caption在rdata区,offset伪指令取了它的地址
.text:0040108C                 push    offset Text     ; "Copied!"
.text:00401091                 call    ds:GetActiveWindow
.text:00401097                 push    eax             ; hWnd   ;eax承载的是GetActiveWindow的返回值,一个窗口句柄,压栈做参数
.text:00401098                 call    ds:MessageBoxW   ;调用MessageBoxW,向屏幕显示对话框
.text:0040109E                 xor     eax, eax     ;eax置零
.text:004010A0                 retn    10h          ;winmain返回值10h
.text:004010A0 _wWinMain@16    endp

暂且不管sub_401000函数干了啥,先看一下后面的win32API干了啥

GetActiveWindow

该函数可以获得与调用线程的消息队列相关的活动窗口的窗口句柄。

函数原型:HWND GetActiveWindow(VOID)

参数:无

返回值:返回值是与调用线程的消息队列相关的活动窗口的句柄。否则,返回值为NULL。

既然GetActiveWindow不需要参数,那么前面三个push压栈是为谁准备的参数呢?

GetActiveWindow调用前后,在主函数中看栈帧没有变化,从栈顶向栈底还是&Text,&Caption,0

然后又将eax压栈,而eax存放的是GetActiveWindow的返回值,一个窗口句柄(如果失败则为NULL)

现在栈上压了四个参数,下面要调用MessageBoxW了

函数原型

int MessageBoxW(
  [in, optional] HWND    hWnd,
  [in, optional] LPCWSTR lpText,
  [in, optional] LPCWSTR lpCaption,
  [in]           UINT    uType
);

hWnd:一个窗口句柄

lpText:要在窗口中打印展示的文本

lpCaption:窗口标题

uType:指定对话框的内容和行为

宏定义 意义
MB_OK 0 窗口只有一个OK按钮,默认模式
MB_OKCANCEL 1 窗口有两个按钮,分别是OK和Cancel
MB_ABORTRETRYIGNORE 2 窗口有三个按钮,分别是Abort,Retry,Ignore(放弃,重试,忽略)
MB_YESNOCANCEL 3 窗口有三个按钮,分别是Yes,No,Cancle
MB_YESNO 4 窗口有两个按钮,Yes,No
MB_RETRYCANCEL 5 ...
MB_CANCELTRYCONTINUE 6 ...
... .. ...

还有很多定义好的窗口样式,现在不用管

返回值:int,返回用户点击的按钮号

宏定义 按钮号 按钮
IDOK 1 OK
IDCANCEL 2 Cancel
IDABORT 3 Abort
...

为啥要返回用户点选的按钮号呢?方便程序后续提供用户希望的服务,

比如当用户点选了Ok则确认并提交了一些信息,点选了Cancel则关闭窗口或者取消了一些信息

例程运行之后的窗口是这样的

image-20220707095841614

可以说这个win32窗口啥正事也没干

sub_401000函数的行为

.text:00401000 sub_401000      proc near               ; CODE XREF: wWinMain(x,x,x,x)↓p
.text:00401000
.text:00401000 Filename        = word ptr -2004h
.text:00401000 pszPath         = word ptr -1004h
.text:00401000 var_4           = dword ptr -4
.text:00401000
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 mov     eax, 2004h
.text:00401008                 call    __alloca_probe
.text:0040100D                 mov     eax, ___security_cookie
.text:00401012                 xor     eax, ebp
.text:00401014                 mov     [ebp+var_4], eax
.text:00401017                 push    1000h           ; nSize
.text:0040101C                 lea     eax, [ebp+Filename]
.text:00401022                 push    eax             ; lpFilename
.text:00401023                 push    0               ; hModule
.text:00401025                 call    ds:GetModuleFileNameW
.text:0040102B                 lea     ecx, [ebp+pszPath]
.text:00401031                 push    ecx             ; pszPath
.text:00401032                 push    0               ; dwFlags
.text:00401034                 push    0               ; hToken
.text:00401036                 push    7               ; csidl
.text:00401038                 push    0               ; hwnd
.text:0040103A                 call    ds:SHGetFolderPathW
.text:00401040                 push    offset String2  ; "\\wsample01b.exe"
.text:00401045                 lea     edx, [ebp+pszPath]
.text:0040104B                 push    edx             ; lpString1
.text:0040104C                 call    ds:lstrcatW
.text:00401052                 push    0               ; bFailIfExists
.text:00401054                 lea     eax, [ebp+pszPath]
.text:0040105A                 push    eax             ; lpNewFileName
.text:0040105B                 lea     ecx, [ebp+Filename]
.text:00401061                 push    ecx             ; lpExistingFileName
.text:00401062                 call    ds:CopyFileW
.text:00401068                 mov     ecx, [ebp+var_4]
.text:0040106B                 xor     ecx, ebp        ; StackCookie
.text:0040106D                 xor     eax, eax
.text:0040106F                 call    @__security_check_cookie@4 ; __security_check_cookie(x)
.text:00401074                 mov     esp, ebp
.text:00401076                 pop     ebp
.text:00401077                 retn
.text:00401077 sub_401000      endp

这个函数都干了啥呢?

.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp

winmain函数开端,压栈保存调用者的ebp帧指针,ebp用于winmain函数的帧指针

.text:00401003                 mov     eax, 2004h
.text:00401008                 call    __alloca_probe

这里调用了一个__alloca_probe函数,上一行往eax寄存器中存放的2004h是函数参数

这个函数干了啥呢?

call __alloca_probe

理论上函数开端在保存帧指针ebp之后接着就应该esp-xxx,为当前函数开辟栈帧,而在sub_401000中并没有这样的指令,或者说本应该开辟栈空间的指令的地方有一个call __alloca_probe那么这个函数调用是否就起到了开辟栈空间的作用呢?

__alloca_probe

从名字上看,该函数有两部分,一个是allocate,分配,另一个是probe,探针,探针?这个函数上网搜吧,就是找不到一个详细解释

在binary book上,该函数是wsample0._chkstk这个函数就有解释了

MSDN:

_chkstk Routine

Called by the compiler when you have more than one page of local variables in your function.

_chkstk Routine is a helper routine for the C compiler. For x86 compilers, _chkstk Routine is called when the local variables exceed 4K bytes; for x64 compilers it is 8K.

_chkstk例程:

当函数栈帧大小大于一个内存页时,编译器会调用该函数

该例程是C编译器的补充.对于x86编译器,当局部变量超过4K时调用,对于x64编译器,局部变量超过8k时调用

然而MSDN只是介绍了啥时候调用这个函数,并没有介绍为啥调用,和调用该函数的影响

下面参考了stackoverflow

Windows pages in extra stack for your thread as it is used. At the end of the stack, there is one guard page mapped as inaccessible memory -- if the program accesses it (because it is trying to use more stack than is currently mapped), there's an access violation. The OS catches the fault, maps in another page of stack at the same address as the old guard page, creates a new guard page just beyond the old one, and resumes from the instruction that caused the violation.

为线程添加额外的windows页.在栈底有一个被操作系统监管的被映射为不可访问内存的页.如果程序视图访问该页(栈空间太小了)就会发生访问冲突.操作系统会捕获该错误,映射到与旧保护页位于同一地址的另一个堆栈页中,在旧保护页之后创建一个新的保护页,然后从导致冲突的指令恢复。

If a function has more than one page of local variables, then the first address it accesses might be more than one page beyond the current end of the stack. Hence it would miss the guard page and trigger an access violation that the OS doesn't realise is because more stack is needed. If the total stack required is particularly huge, it could perhaps even reach beyond the guard page, beyond the end of the virtual address space assigned to stack, and into memory that's actually in use for something else.

如果一个函数有多个本地变量页,那么它访问的第一个地址可能是堆栈当前端之外的多个页面。因此它会错过保护页面并触发一个操作系统没有意识到的访问冲突,因为需要更多的堆栈。如果所需的总堆栈特别巨大,它甚至可能超出保护页面,超出分配给堆栈的虚拟地址空间的末尾,进入实际用于其他用途的内存。

So, _chkstk ensures that there is enough space for the local variables. You can imagine that it does this by touching the memory for the local variables at page-sized intervals, in increasing order, to ensure that it doesn't miss the guard page (so-called "stack probes"). I don't know whether it actually does that, though, possibly it takes a more direct route and instructs the OS to map in a certain amount of stack. Either way, if the total required is greater than the virtual address space available for stack, then the OS can complain about it instead of doing something undefined.

因此,_ chkstk 确保局部变量有足够的空间。可以想象,它通过按页面大小的间隔访问本地变量的内存来实现这一点,以递增的顺序,确保它不会错过保护页(所谓的“堆栈探测”)。我不知道它是否真的这样做,但是,可能它采取了一个更直接的例程,并指示操作系统映射到一定数量的堆栈。无论哪种方式,如果所需的总空间大于可用于堆栈的虚拟地址空间,那么操作系统可以报告这件事,而不是执行未定义的操作。

逆向__alloca_probe函数观察其行为

.text:004018E0 __alloca_probe  proc near               ; CODE XREF: sub_401000+8↑p
.text:004018E0                 push    ecx        ;压栈保存ecx
.text:004018E1                 lea     ecx, [esp+4]   ;ecx指向当前栈顶+4位置
.text:004018E5                 sub     ecx, eax       ;ecx-eax->ecx,显然ecx是一个内存地址,比eax要大,这里不会置CF
.text:004018E7                 sbb     eax, eax       ;eax-eax-CF->eax,由于上一步不需要置CF,因此这里eax=0
.text:004018E9                 not     eax            ;eax=反eax,即eax这个32位寄存器全置高
.text:004018EB                 and     ecx, eax       ;ecx和全1按位与还是ecx
.text:004018ED                 mov     eax, esp       ;esp->eax,eax获得栈顶指针快照
.text:004018EF                 and     eax, 0FFFFF000h    ;eax只保留高20位,低12位置0
.text:004018F4
.text:004018F4 cs10:                                   ; CODE XREF: __alloca_probe+29↓j
.text:004018F4                 cmp     ecx, eax           ;ecx-eax根据结果置flag
.text:004018F6                 jb      short cs20     ;如果ecx<eax则跳转cs20
.text:004018F8                 mov     eax, ecx           ;如果ecx>=eax,则eax=ecx
.text:004018FA                 pop     ecx                ;尾声,栈顶还给ecx
.text:004018FB                 xchg    eax, esp           ;eax和esp交换
.text:004018FC                 mov     eax, [eax]     ;
.text:004018FE                 mov     [esp+0], eax
.text:00401901                 retn                       ;唯一的函数出口
.text:00401902 ; ---------------------------------------------------------------------------
.text:00401902
.text:00401902 cs20:                                   ; CODE XREF: __alloca_probe+16↑j
;执行到此说明.text:004018F6 处有ecx<eax,于是循环执行下面三行,直到ecx>=eax
.text:00401902                 sub     eax, 1000h     ;eax-1000h->eax ,1000h就是4KB,32位win上一个页框的大小                                          ;栈顶下移4K,eax待会要赋值给esp栈顶指针
.text:00401907                 test    [eax], eax     ;蜜汁操作,test运算了一下结果下一行是无条件跳转,运算个寂寞?
.text:00401909                 jmp     short cs10
.text:00401909 __alloca_probe  endp

该函数用到了很多寄存器,ecx,eax,esp,纯静态分析很容易分析中忘记寄存器中存放的是什么了,这时候可以使用动态调试按步就班地观察

eaxsub_401000中被赋值2004h=8196d=2K然后作为参数传递给__alloca_probe显然这个大小大于一个页框

如果分配大小eax小于一个页框大小4k,则程序相当于

;size in eax
.text:004018E0                 push    ecx        
.text:004018E1                 lea     ecx, [esp+4]   ;ecx=esp+4
.text:004018E5                 sub     ecx, eax       ;ecx=esp+4-size
.text:004018F8                 mov     eax, ecx       ;eax=ecx=esp+4-size
.text:004018FA                 pop     ecx            
.text:004018FB                 xchg    eax, esp       ;esp=eax=esp+4-size while eax=esp
.text:004018FC                 mov     eax, [eax] ;eax指向老栈顶的元素
.text:004018FE                 mov     [esp+0], eax   ;老栈顶元素搬运到新栈顶位置
.text:00401901                 retn   

实际上就是把当前栈扩大size,然后将原来栈顶上存放的内容搬到新的栈顶上

当分配大小eax大于一个页框4K,则程序会有额外的循环步骤

.text:00401902 cs20:                                   ; CODE XREF: __alloca_probe+16↑j
.text:00401902                 sub     eax, 1000h ;栈上开辟4k空间,eax待会要拷贝给esp
.text:00401907                 test    [eax], eax ;触摸内存,触发缺页异常,让os将虚拟页载入物理页
.text:00401909                 jmp     short cs10 ;循环

循环啥时候停止呢?

.text:004018F4 cs10:                                   ; CODE XREF: __alloca_probe+29↓j
.text:004018F4                 cmp     ecx, eax           
.text:004018F6                 jb      short cs20     

ecx在最初的时候直接减去size,指向了希望的栈顶,这里就比较eax是否已经越过了希望的栈顶,

当eax首次越过(eax=ecx或者eax-ecx<一个页框的大小4k)

此时栈空间足够大了,满足我们的希望了,可以停止循环了

在这里可以看出,x86windows的栈帧大小是以页框4K为单位进行分配的.

这个__security_cookie带着下划线前缀,一看就不是用户写的,这是个啥呢?

从意义上看,安全cookie值,应该是和安全相关

.data:00403000 ; Segment permissions: Read/Write
.data:00403000 _data           segment para public 'DATA' use32
.data:00403000                 assume cs:_data
.data:00403000                 ;org 403000h
.data:00403000 ; uintptr_t __security_cookie
.data:00403000 ___security_cookie dd 0BB40E64Eh        ; DATA XREF: sub_401000+D↑r

___security_cookie位于.data段,程序拥有读写该段的权限.该段的段寄存器是cs寄存器

dword ___security_cookie=0BB40E64Eh是一个双字类型,相当于一个int,32字节

这就是一个常数啊,为啥要把一个八竿子打不着的常数压栈呢?

sub_401000尾声伊始,还有有一条涉及security_cookie指令

.text:0040106F                 call    @__security_check_cookie@4 ; __security_check_cookie(x)

该条指令调用了一个函数@__security_check_cookie@4

@__security_check_cookie@4

从汇编符号上看,应该是fastcall调用约定

本函数只需要一个参数,使用ecx寄存器传递

ecx寄存器传递了啥参数呢?在sub_40100中是这样写的:

.text:00401068                 mov     ecx, [ebp+var_4]   
.text:0040106B                 xor     ecx, ebp        ; StackCookie

var_4又是啥?

.text:0040100D                 mov     eax, ___security_cookie
.text:00401012                 xor     eax, ebp
.text:00401014                 mov     [ebp+var_4], eax

cookie放到eax里面然后和ebp异或一下再放到var_4,即var_4=___security_cookie ^ ebp,相当于一层加密

因此在尾声的时候把var_4拿出来还要和ebp异或一下才能得到___security_cookie,相当于一层解密

那么此时传递给@__security_check_cookie@4函数的ecx里面,理论上就应该是纯纯的~闸总~___security_cookie

.text:004010A3 @__security_check_cookie@4 proc near    ; CODE XREF: sub_401000+6F↑p
.text:004010A3                                         ; DATA XREF: __except_handler4+11↓o
.text:004010A3                 cmp     ecx, ___security_cookie
.text:004010A9                 jnz     short $failure$26820
.text:004010AB                 rep retn

该函数也确实将ecx和位于.data段的___security_cookie进行了比较,如果不一样则跳转$failure$26820

上述过程干了个什么事呢?防止栈缓冲区溢出

下面是sub_401000函数的栈帧,var_4是在调用者ebp保存值s和本函数返回地址r之上的(var_4相对靠近栈顶,r在栈帧底部)

栈倒着长但是栈内数据正着长,如果有一个缓冲区一直增长,把位于ebp-0x4的var_4覆盖了,甚至把位于ebp+0的s等等也覆盖了

在函数尾声的时候,就会把var_4拿出来看看其中异或保存的___security_cookie是否发生了变化.

一旦检查出var_4中异或保存的___security_cookie发生了变化,则至少表明栈缓冲区溢出已经到了ebp-0x4,

至于后面的调用者ebp和本函数返回地址有没有被溢出呢?不知道,但是不能做出乐观的假设,

为了防止返回地址被修改引起的攻击,此时应当立刻终止进程并报告错误

...
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004 var_4           dd ?
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008
+00000008 ; end of stack variables

为啥要把___security_cookie和ebp异或一下呢?

为啥不直接把___security_cookie副本压栈最后再将该副本退栈和位于.data___security_cookie比较呢?

这样相当于数据库保存了用户密码的明文,一旦脱库后果不堪设想.如果___security_cookie在栈上也是明文保存的,则可以利用printf格式化字符串漏洞尝试打印该值,在溢出的时候对于栈中___security_cookie副本位置,只需要装模做样的写上,后面继续溢出

这样就可以绕过检查

为啥要和ebp异或一下呢?为啥不能是其他值?

考虑这个与___security_cookie异或的值应该有什么特性?函数开端和函数尾声的时候都要与他异或,这个值应该保持不变,

满足这个特征的值可以想到的就是ebp了,对于当前函数,它永远指向栈帧底部不变.

栈顶指针就不行,esp会随着局部变量的声明或者子函数的调用而改变

为啥使用异或运算加密呢?使用按位与,按位或不行吗?

异或运算有一个性质:如果\(A\oplus B=C\)\(C\oplus A=(A\oplus B)\oplus A=B\)

显然按位与,按位或等运算没有这个性质

而这个性质正是在函数开端时\(var_4=ebp\oplus security\_cookie\),

在函数尾声时能够\(security\_cookie=var_4\oplus ebp\)的原理

这样就绝对安全了吗?能够完全抵御栈缓冲区溢出修改函数返回地址了吗?

使用security_cookie只能一定程度上保护调用者ebp和返回地址不被修改,栈帧中,存放在var_4之后,缓冲区之前的局部变量不受保护

并且security_cookie在编译之后就是一个定值了,运行时永远不变,使用ida就可以直接看到它多粗多长

在运行时动态调试一下就可以看到ebp是多少,

如果没有开启基址随机化,则每次ebp都是一个常数,

那么var_4=security_cookie ^ ebp也是一个常数,这就异或加密了个寂寞

call ds:GetModuleFileNameW

现在回到sub_401000函数中

push    1000h           ; nSize
lea     eax, [ebp+Filename]
push    eax             ; lpFilename
push    0               ; hModule
call    ds:GetModuleFileNameW

又调用了一个API函数GetModuleFileNameW,这个函数干了啥呢?

GetModuleFileNameW函数原型

DWORD GetModuleFileNameW(
  [in, optional] HMODULE hModule,
  [out]          LPWSTR  lpFilename,
  [in]           DWORD   nSize
);

hModule:应用程序或者DLL实力句柄,如果为NULL则获取当前程序路径

lpFilename:获取路径之后存放之的字符串缓冲区

nSize:缓冲区大小,作用是防止缓冲区溢出

在这里第一个参数hModule=0,表明要获取当前应用程序的目录

第二个参数lpFilename=Filename是sub_401000函数栈中的一个缓冲区

第三个参数nSize=1000h,表明缓冲区大小为4KB

关于LPWSTR类型,实际是wchar_t*类型,即宽字符unicode编码的字符串

L长

P指针

W宽

STR字符串

宽字符的作用是支持包括英文,中文,日文等等各种花言鸟语的符号,ASCII码最多表示\(2^8=256\)个字符,unicode最多表示\(2^{16}=65536\)个字符,常用汉字就3000个,显然unicode有能力森罗万象

call ds:SHGetFolderPathW

lea     ecx, [ebp+pszPath]
push    ecx             ; pszPath
push    0               ; dwFlags
push    0               ; hToken
push    7               ; csidl
push    0               ; hwnd
call    ds:SHGetFolderPathW

又是一个API函数

它亲戚SHGetFolderPathA函数原型

SHFOLDERAPI SHGetFolderPathA(
  [in]  HWND   hwnd,
  [in]  int    csidl,
  [in]  HANDLE hToken,
  [in]  DWORD  dwFlags,
  [out] LPSTR  pszPath
);

其中参数csidl=7是啥意思呢?表示"启动"文件夹

在win10上这个文件夹在C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp

获取"启动"文件夹目录,字符串存放到pszPath指向的缓冲区

csidl其他值的意义

CSIDL_DESKTOP = &H0 '// The Desktop - virtual folder
CSIDL_PROGRAMS = 2 '// Program Files
CSIDL_CONTROLS = 3 '// Control Panel - virtual folder
CSIDL_PRINTERS = 4 '// Printers - virtual folder
CSIDL_DOCUMENTS = 5 '// My Documents
CSIDL_FAVORITES = 6 '// Favourites
CSIDL_STARTUP = 7 '// Startup Folder
CSIDL_RECENT = 8 '// Recent Documents
CSIDL_SENDTO = 9 '// Send To Folder
CSIDL_BITBUCKET = 10 '// Recycle Bin - virtual folder
CSIDL_STARTMENU = 11 '// Start Menu
CSIDL_DESKTOPFOLDER = 16 '// Desktop folder
CSIDL_DRIVES = 17 '// My Computer - virtual folder
CSIDL_NETWORK = 18 '// Network Neighbourhood - virtual folder
CSIDL_NETHOOD = 19 '// NetHood Folder
CSIDL_FONTS = 20 '// Fonts folder
CSIDL_SHELLNEW = 21 '// ShellNew folder

call ds:lstrcatW

push    offset String2  ; "\\wsample01b.exe"
lea     edx, [ebp+pszPath]
push    edx             ; lpString1
call    ds:lstrcatW

String2是.rdata段的常量字符串

pszPath存放了刚才调用函数SHGetFolderPathW获取的文件夹目录

这里相当于调用了lstrcatw(&pszPath,&pszPath),将后者拼接到前者上,得到wsample01b.exe的绝对地址

C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\wsample01b.exe

call ds:CopyFileW

push    0               ; bFailIfExists
lea     eax, [ebp+pszPath]
push    eax             ; lpNewFileName
lea     ecx, [ebp+Filename]
push    ecx             ; lpExistingFileName
call    ds:CopyFileW

即调用了CopyFileW(&Filename,&pszPath,0)

CopyFileW函数原型

BOOL CopyFileW(
  [in] LPCWSTR lpExistingFileName,
  [in] LPCWSTR lpNewFileName,
  [in] BOOL    bFailIfExists
);

将一个已经存在的文件lpExistingFileName拷贝到一个新的位置lpNewFileName

如果bFailIfExists=1并且新的位置已有同名文件,则函数执行失败,返回FALSE

如果bFailIfExists=0并且新的位置已有同名文件,则覆盖该文件

综上程序干了一个将自己复制到"启动"文件夹下的工作,意图让自己每次开机自启,有了病毒的勤快但是没有病毒的毒性

尾声

mov     ecx, [ebp+var_4]
xor     ecx, ebp        ; StackCookie
xor     eax, eax
call    @__security_check_cookie@4 ; __security_check_cookie(x)
mov     esp, ebp
pop     ebp
retn
sub_401000 endp

首先检查金丝雀值是否被修改,这个前面已经分析过了

然后退还调用者的ebp,函数返回

总结程序行为

首先调用sub_401000函数,该函数将wsample01b.exe拷贝到"启动"文件夹,然后弹窗打印"copied"

ollydbg动态调试wsample01b.exe

x32dbg和ollydbg就像那黑牛和白牛,就是那海尔兄弟

打开

例程来自有趣的二进制kenjiaiko/binarybook (github.com)

用ollydbg打开wsample01b.exe,可以在ollydbg中的菜单栏中文件->打开,也可以快捷键F3打开

image-20220707161255625

还可以使用命令行参数打开PS C:\Users\86135\Desktop\bin\binarybook\chap01\wsample01b\release> od wsample01b.exe

这里我把olly dbg.exe重命名为od方便使用

需要将od的根目录添加到环境变量path,才能使用终端调用od

image-20220707160733203

在反汇编窗口第一行即0x401000位置已经自动有一个断点,

这个位置刚才我们已经经过ida静态分析过了,是sub_401000函数的起始地址,显然这个函数是根据地址起的哑名

这四个区的视图结构也是可以更改的

image-20220707171515399

但是一般都使用默认的视图模式,这个看的习惯

查看快捷键

image-20220707165636208'

image-20220707171131310

ctrl+G跳转

用ida静态分析时,我们知道WinMain函数的起始地址在0x00401080

可以使用ctrl+G打开跟随窗口进行跳转

image-20220707161059609

回车之后就跳转到该位置

image-20220707161215951

还可以跟踪一个函数,比如API函数MessageBoxA

image-20220707161728342

双击右侧列表中的MessageBoxA之后,反汇编窗口自动跳转到该函数实现的入口

image-20220707161815928

一看地址好家伙都到0x75539096了,

而ida静态分析时的地址最大才到.data:0040338C,不用ctrl+G,只拖动od反汇编窗口的滑块,也是最大可以看到401FFF,后面就一片空白了,就好像od懒得干活了一样

image-20220707162547049

0x75539096这个地址是啥呢?为什么会这么大?

一开始我还认为这是内核的地址空间,实际上不是,这个值还是在0 - 0x7FFFFFFF范围内的,是用户地址空间

地址范围 0 - 0x7FFFFFFF(2G),运行

应用程序代码、数据等等。

2.2.1 空指针区(NULL区)

地址范围 0 - 0x0000FFFF

2.2.2 用户区

地址范围 0x00010000 - 0x7FFEFFFF

2.2.3 64K禁入区

地址范围 0x7FFEFFFF - 0x7FFFFFFF

2.2 内核空间

地址范围 0x80000000 - 0xFFFFFFFF,被

系统使用,运行驱动、内核的数据和代码。

猜测这是DLL库,但是具体是不是,需要学习了windows上的链接阶段再说

alt+e查看模块

image-20220707164553789

刚才的问题0x75539096这个地址就属于user32.dll模块

调试快捷键

image-20220707165555280

运行

按下F9之后,程序会在第一个断点处停下,如果没有任何断点,程序也没有错误则程序直接执行完毕

步入和步过的区别:

对于函数调用,步入则反汇编窗口会跳转跟随该函数,一行一行执行.

而步过则是直接让函数执行完毕,反汇编窗口不会跟随函数,但是保留函数产生的影响,比如寄存器和一些全局变量等的值变化

执行到返回:

本来步入了一个函数,后来看烦了想跳出这个函数,就用执行到结束

或者一个需要114514次的大循环,已经循环到第10次了,后面还要循环114504次,烦死了,直接执行到结束跳出循环

函数中的循环则只跳出一层,再按一次执行到返回才会跳出函数

单步和自动的区别:

单步是拨一拨转一转,按一下F7或者F8才会执行一行,

自动是按下ctrl+F7或者ctrl+F8之后,od就会像过电影一样自动呼呼地执行,反汇编窗口等四个窗口都会实时跟随更新,相当于一直按着F7或者F8

啥时候自动的能停下呢?

- 按 Esc 键或发出任何单步命令

- OllyDbg 遇到断点

- 被调试程序发生异常

执行到用户代码:

如果当前正在库函数中跑,按下Alt+F9之后,od会在第一条回归到用户自己写的函数中的位置停下

插件

image-20220707171738040

我这个ollydbg是从吾爱破解论坛上下载的懒人包,里面已经集成了一些插件

+BP-OLLY

image-20220707172112679

这是一个小工具栏

image-20220707172151132

我的懒人包ollydbg启动时这个插件会自启动

其中BP是BreakPoint断点的缩写,作用是在API函数上下断点

image-20220707172244773

P是编辑命令快捷键

image-20220707172412802

比如BP MessageBoxA就相当于保存了一条命令,下一次只需要点击一下就可以自动让od执行该命令

实际作用和在ollydbg的底行输入命令回车执行相同

image-20220707172511015

这里Command还能干啥呢?现在不想炎鸠

VB也是在一些库函数上下断点,但是这些库函数目前没有遭遇过

image-20220707172623275

NotePad,调用windows系统自带的记事本程序

Calc,调用计算器

Folder,打开exployer文件系统资源管理器

CMD,打开命令提示符

Exit,关闭该插件

API断点

image-20220707171843420

这两个插件的功能差不多,都是让od自动找到我们调用API函数的地方下断点

比如image-20220707171936097

在GetWindowTextA处下断点,这个API的作用是获取用户在窗口中的文本框输入.

一些序列号注册验证逻辑往往就发生在获取用户输入之后,让od自动停在这种地方,方便我们单步调试后面的逻辑

花里胡哨的插件

这些插件我都没用到过,它们描述的功能,什么"花指令",什么"反混淆",看上去好高深,现在不想炎鸠

image-20220707172857063

中文搜索引擎

image-20220707172950546

搜索UNICODE之后的结果

image-20220707173009685

其作用相当于二进制工具Strings

PS C:\Users\86135\Desktop\bin\binarybook\chap01\wsample01b\release> strings wsample01b.exe -d -eb
\wsample01b.exe
MESSAGE
Copied!

-d选项只扫描.data区,

-e选项指定字符宽度,b或者l表示16字节即一个宽字符unicode

自动注释

image-20220707173038785

差评,这个插件根本跑不起来,现有的注释不是插件带来的,是od自带的

image-20220707173157070

这些注释已经足够看懂程序了

动态调试

正儿八经开始调试这个wsample01b.exe

由于od自动在最顶上一行0x401000下了断点,此处正好是sub_401000函数入口,直接F9运行观察该函数的行为

image-20220707184836611

开始运行时,程序会停止在第一个断点0x401000处,当前停止位置会有灰色高亮

左上角"暂停"表明当前调试器的状态

寄存器区的表现为:

image-20220707185010444

其中红色的是有变化的寄存器,刚开始执行一个程序,各个通用目的寄存器还有栈顶指针,帧指针等等都认为有变化

其中

eip=0x401000表明将要执行的指令地址

esp=0x0019FEE0表明当前栈顶指针位置

由于还没有经历sub_401000的开端,ebp=0x0019FF74这个值是谁的栈帧指针呢?

啃腚不是winmain的!啃腚不是winmain的!啃腚不是winmain的!

说三遍是因为一开始瞎几把分析都认为是winmain的帧指针了

winmain函数满足stdcall调用约定,不会使用栈帧指针ebp,那么此ebp有可能是winmain的调用者的帧指针,也不一定,要是调用者也是stdcall,则ebp还得往前找

谁调用了winmain呢?这个问题可以在ida的function calls中观察

image-20220707192428834

也可以在目前的栈帧中观察winmain的返回地址

栈帧区的表现为

image-20220707185758156

紫色高亮是手动选中的,栈顶指针在0x19FEE0,会有类似反汇编区中将要执行指令的灰色高亮

由于控制已经转到sub_401000的第一条指令,这表明,winmain中的call sub_401000已经执行过了,

因此sub_401000的返回地址0x401085已经压入栈中0x19FEE0位置

ida观察这件事,确实call指令下面一条指令的地址就是0x401085

.text:00401080                 call    sub_401000
.text:00401085                 push    0               ; uType

注意到还有另一个返回到 wsample0.00401255 来自 wsample0.00401080,这是啥呢?

这个指令地址在__tmainCRTStartup函数中

.text:00401250                 call    _wWinMain@16    ; wWinMain(x,x,x,x)
.text:00401255                 mov     dword_403038, eax

原来是winmain的返回地址,同时也知道了是__tmainCRTStartup这个函数调用了_wWinMain@16

至于__tmainCRTStartup这个函数干了啥呢?我非常好奇,但是现在不是炎鸠它的时候,后面专门炎鸠win32程序调用的全过程

下面接着两条指令都是mov指令,不涉及函数调用,因此单步步入和单步步过没有区别

image-20220707190703773

image-20220707191307452

这大概就是调试过程

调试时修改

改指令

反汇编区,任意一行汇编指令都是可以修改的,双击即可修改

image-20220707193913865

一定要选择使用NOP填充,因为运行时各种寻址已经确定,如果我们修改的汇编指令比原指令短,则从该指令以后的所有指令地址都会移动,各种寻址方式就寄了

这里修改指令带来的影响是永久的,即直接修改了可执行文件中的二进制代码,下一次运行本程序还会带着本次的修改

破解序列号注册程序时往往把jnz改成jz就可以让序列号判断寄掉

改寄存器

比如修改状态寄存器ZF,双击其数值就可以从0改到1或者从1改到0,后续的计算都是基于修改后的值

image-20220707194243839

也可以修改其他寄存器,比如程序计数器esp

image-20220707194718167

修改之后堆栈区的当前栈顶指针也会跟着改

有一个寄存器没法改,那就是eip程序计数器

修改寄存器造成的影响是临时的,仅限于本次程序执行,当程序重新执行时没有影响

改堆栈

比如可以把sub_401000的返回地址改成sub_401000的入口地址,ret2text?

image-20220707195024065

改堆栈也是临时的

IDA动态调试wsample01b.exe

image-20220707201135306

首先要选择调试器

image-20220707201155743

说是选择,然而只有一个Local Windows debugger可以用,其他的都找不到,没安装

选好之后下断点

比如在winmain第一行下断点

image-20220707201428364

此后按下F9就开始动态调试了

image-20220707201704143

各种快捷键都与ollydbg相似,包括F7单步步入,F8单步步过等等