调试与发布二进制文件 - 溢出检测

逆向工程 艾达 ollydbg 缓冲区溢出 软件安全
2021-06-24 07:20:29

我正在阅读 IDA Pro 书籍,在第 20 章中,作者展示了来自调试版本的以下代码:

push ebp
mov ebp, esp
sub esp, 0F0h
push ebx
push esi
push edi 
lea edi, [ebp+var_F0]
mov ecx, 3Ch
mov eax, 0CCCCCCCCh
rep stosd
mov[ebp+var_8], 0
mov [ebp+var_14], 1
mov [ebp+var_20], 2
mov [ebp+var_2C], 3

正如我们所看到的,局部变量彼此不相邻。Chris Eagle 概述了这使得更容易检测一个变量的溢出,该溢出可能会溢出并破坏另一个变量,然后他就将它留在那里。这对我来说没有意义,在可能导致溢出的特定操作之后设置断点然后检查变量的值不是更容易吗?这究竟有多有用?

3个回答

最新的 Visual Studio 编译器使用运行时检查来检测溢出,它使用各种运行时检查来执行它们

您只能在未优化的构建中使用它们 /Od(这些在不使用 /O1 或 /O2 或 /Ox 的优化构建中不起作用)

这些可以是#pragmas/RTC1 /RTCS | 你| C命令行开关

通过分配比所需更大的缓冲区并用已知模式填充它来检测堆栈损坏

由于编译器知道应该使用多少空间,它可以检查边界是否被未知模式践踏

(是的,一些聪明的模式匹配漏洞可能仍然会试图欺骗这一点,但它适用于您正在编写无意中溢出的代码的真正用法)

以这个代码为例

#define CRT_SECURE_NO_WARNING
#include <string.h>
#include <stdio.h>
void foo(void){
    char flowoverme[0x10];
    strcpy(flowoverme,"yaddaaayadddaaafoo");
}
int main(void){
    foo();
    printf("checking overflows by pattern pasting \n");
}

(如果你使用 /analyze 编译器开关,它会吐出这段代码会溢出

:\>cl /nologo /Zi /RTC1 /analyze /Od /EHsc rtcchk.cpp /link /nologo /debug
rtcchk.cpp
rtcchk.cpp(8) : warning C6386: Buffer overrun while writing to 'flowoverme':  the wr
itable size is '16' bytes, but '19' bytes might be written.: Lines: 7, 8

但假设你刚刚做了 cl foo.cpp

:\>cl /nologo /Zi /RTC1 /Od /EHsc rtcchk.cpp /link /nologo /debug
rtcchk.cpp

如果您执行此编译后的代码,如果启用了运行时检查,则不会到达 printf

:\>rtcchk.exe

:\>

我们可以反汇编并查看函数 foo 内部发生了什么以及为什么不执行 printf()

让我们打开 windbg 中的二进制文件,转到 foo() 的开头并要求 windbg 上升(gu 即返回 main() 返回),如下所示,您会注意到 windbg 没有返回到 main 而是停止并显示错误消息

:\>cdb -c "g rtcchk!foo;gu" rtcchk.exe

Microsoft (R) Windows Debugger Version 10.0.16299.15 X86

0:000> cdb: Reading initial command 'g rtcchk!foo;gu'

rtcchk!failwithmessage+0x255:
013d75da cc              int     3

并且调用堆栈会显示

0:000> kP
ChildEBP RetAddr
0028f544 013d72a9 rtcchk!failwithmessage(
                        void * retaddr = 0x013d698a,
                        int crttype = 0n1,
                        int errnum = 0n2,
                        char * msg = 0x0028f568 "Stack around the variable 'flowoverme' was corrupted.")+0x255
0028f96c 013d6c3d rtcchk!_RTC_StackFailure(
                        void * retaddr = 0x013d698a,
                        char * varname = 0x013d69b8 "flowoverme")+0x94
0028f98c 013d698a rtcchk!_RTC_CheckStackVars(
                        void * frame = 0x0028f9b8,
                        struct _RTC_framedesc * v = 0x013d69a4)+0x42
0028f9b8 013d69d8 rtcchk!foo(void)+0x4a
0028f9c0 013d6ecd rtcchk!main(void)+0x8
(Inline) -------- rtcchk!invoke_main+0x1c
0028fa08 76a9ed6c rtcchk!__scrt_common_main_seh(void)+0xf9
0028fa14 77cb37eb kernel32!BaseThreadInitThunk+0xe
0028fa54 77cb37be ntdll!__RtlUserThreadStart+0x70
0028fa6c 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000>

如果您仍然想知道这些函数是什么或如何运行,请在 vs 中打开 crt 源或反汇编这些函数

编译器知道所需的大小和边界在哪里

0:000> dx -r3 (_RTC_framedesc *) 0x013d69a4
(_RTC_framedesc *) 0x013d69a4  : 0x13d69a4 [Type: _RTC_framedesc *]
    [+0x000] varCount         : 1 [Type: int]
    [+0x004] variables        : 0x13d69ac [Type: _RTC_vardesc *]
        [+0x000] addr             : -24 [Type: int]
        [+0x004] size             : 16 [Type: int]
        [+0x008] name             : 0x13d69b8 : "flowoverme" [Type: char *]
0:000>

我已经检查了提到的章节,还有关于检测堆栈上指令执行的信息。这是我认为比较常见的场景。

至于溢出检测,我只能推测,但对我来说,在一个地方更容易检查所有应该是的值是否0xCC实际上仍然完好无损,而不是每次在可能溢出和检查的操作之后执行此操作如果值应该是操作的结果。还考虑一个数组,使用这种方法可以检查它们是否不超出范围。

OxCC可以覆盖使用这两种检查。

您粘贴的是 Visual C++ 的/GZ开关或更新版本/RTC它用 0xCC 填充局部变量的堆栈。我找不到的是是否有一个实际的函数在运行时检查是否有任何间隙被污染。

我认为这是因为编译器应该能够在知道堆栈布局的编译时生成那些,所以在某个时刻(函数结束,或异常?)他可以自动验证间隙是否干净。

如果没有间隙,您将无法判断一个变量写入是否溢出,因为它只会在下一个变量中结束。我想这就是本书作者所暗示的。

不幸的是,我找不到这些标志的明确文档,除了说明它确实填充堆栈并使用它来验证完整性。