`
阿尔萨斯
  • 浏览: 4107766 次
社区版块
存档分类
最新评论

C++反汇编揭秘2 – VC编译器的运行时错误检查(RTC)

 
阅读更多
<iframe align="center" marginwidth="0" marginheight="0" src="http://www.zealware.com/csdnblog336280.html" frameborder="0" width="336" scrolling="no" height="280"></iframe>

我在上篇文章举了一个简单的C++程序非常简略的解释C++代码和汇编代码的对应关系,在后面的文章中我将按照不同的Topic来仔细介绍更多相关的细节。虽然我很想一开始的时候就开始直接介绍C++和汇编代码的对应关系,不过由于VC编译器会在代码中插入各种检查,SEHC++异常等代码,因此我觉得有必要先写一下一些在阅读VC生成的汇编代码的时候常见的一些东西,然后再开始具体的分析C++代码的反汇编。这篇文章会首先涉及到运行时检查(Runtime Checking

Runtime Checking

运行时检查是VC编译器提供了运行时刻的对程序正确性/安全性的一种动态检查,可以在项目的C++选项中打开Small Type CheckBasic Runtime Checks来启用Runtime Check

<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="Picture_x0020_4" style="VISIBILITY: visible; WIDTH: 453pt; HEIGHT: 28.5pt; mso-wrap-style: square" type="#_x0000_t75" o:spid="_x0000_i1027"><imagedata o:title="" src="file:///D:/tmp/msohtmlclip1/01/clip_image001.png"></imagedata></shape>

同时,也可以使用/RTC开关来打开检查,/RTC后面跟c, u, s代表启用不同类型的检查。Smaller Type Check对应/RTCc, Basic Runtime Checks对应/RTCs/RTCu

/RTCc开关

RTCc开关可以用来检查在进行类型转换的保证没有不希望的截断(Truncation)发生。以下面的代码为例:

char ch = 0;

short s = 0x101;

ch = s;

VC执行到ch = s的时候会报告如下错误:

<shape id="Picture_x0020_1" style="VISIBILITY: visible; WIDTH: 327pt; HEIGHT: 144.75pt; mso-wrap-style: square" type="#_x0000_t75" o:spid="_x0000_i1026"><imagedata o:title="" src="file:///D:/tmp/msohtmlclip1/01/clip_image003.png"></imagedata></shape>

原因是0x101已经超过了char的表示范围。

之前会导致错误地的代码对应的汇编代码如下所示:

; 42 : char ch = 0;

mov BYTE PTR _ch$[ebp], 0

; 43 : short s = 0x101;

mov WORD PTR _s$[ebp], 257 ; 00000101H

; 44 : ch = s;

mov cx, WORD PTR _s$[ebp]

call @_RTC_Check_2_to_1@4

mov BYTE PTR _ch$[ebp], al

可以看到,赋值的时候,VC编译器先将s的值放到cx寄存器中,然后调用_RTC_Check_2_to_1@4函数来检查是否有数据截断的问题,结果放在al中,最后将al放到ch之中。_RTC_Check_2_to_1@4顾名思义是检查2byte的数据被转换成1byte的数据(short2bytechar是一个byte),代码如下:

_RTC_Check_2_to_1:

00411900 push ebp

00411901 mov ebp,esp

00411903 push ebx

00411904 mov ebx,ecx

00411906 mov eax,ebx

00411908 and eax,0FF00h

0041190D je _RTC_Check_2_to_1+24h (411924h)

0041190F cmp eax,0FF00h

00411914 je _RTC_Check_2_to_1+24h (411924h)

00411916 mov eax,dword ptr [ebp+4]

00411919 push 1

0041191B push eax

0041191C call _RTC_Failure (411195h)

00411921 add esp,8

00411924 mov al,bl

00411926 pop ebx

00411927 pop ebp

00411928 ret

1. 00411904~00411906ecx保存着s的值,然后又被转移到eax中。

2. 00411908~0041190D:检查eax0xff00相与,并检查是否结果为0,如果结果为0,说明这个short值是0或者的正数,没有超过范围,直接跳转到00411924获得结果并返回

3. 0041190F~00411914:检查eax是否等于0xff00,如果相等,说明这个short值是负数,并且>=-128,在char的表示范围之内,可以接受,跳转到00411924

4. 如果上面检查都没有通过,说明这个值已经超过了范围,调用_RTC_Failure函数报错

要解决这个问题,很简单,把代码改为下面这样就可以了:

char ch = 0;

short s = 0x101;

ch = s & 0xff;

/RTCu开关

这个开关的作用是打开对未初始化变量的检查,比静态的警告要有用一些。考虑下面的代码:

int a;

char ch;

scanf("%c", &ch);

if( ch = 'y' ) a = 10;

printf("%d", a);

编译器无从通过Flow Analysis知道aprintf之前是否被正确初始化,因为a = 10这个分支是由外部条件决定的,所以只有动态的监测方法才可以知道到底程序有没有Bug(当然从这里我们可以很明显的看出这个程序必然是有Bug的)。显然把变量的值和一个具体值来比较是无法知道变量是否被初始化的,所以编译器需要通过一个额外的BYTE来跟踪此变量是否被初始化:

函数的开始代码如下:

push ebp

mov ebp, esp

sub esp, 228 ; 000000e4H

push ebx

push esi

push edi

lea edi, DWORD PTR [ebp-228]

mov ecx, 57 ; 00000039H

mov eax, -858993460 ; ccccccccH

rep stosd

mov BYTE PTR $T5147[ebp], 0

最后一句很关键,把$T5147变量的值设置为0,表示并没有初始化a这个变量。

ch = ‘y’的时候,编译器除了执行a=10之外还会将$T5147设置为1

mov BYTE PTR $T5147[ebp], 1

mov DWORD PTR _a$[ebp], 10 ; 0000000aH

之后,在printf之前,编译器会检查$T5147这个变量的值,如果为0,说明没有初始化,执行__RTC_UninitUse报告错误,否则跳转到相应代码执行printf语句:

cmp BYTE PTR $T5147[ebp], 0

jne SHORT $LN4@wmain

push OFFSET $LN5@wmain

call __RTC_UninitUse

add esp, 4

$LN4@wmain:

mov esi, esp

mov eax, DWORD PTR _a$[ebp]

push eax

push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@

call DWORD PTR __imp__printf

add esp, 8

cmp esi, esp

call __RTC_CheckEsp

/RTCs开关

这个开关是用来检查和Stack相关的问题:

1. Debug模式下把Stack上的变量初始化为0xcc,检查未初始化的问题

2. 检查数组变量的Overrun

3. 检查ESP是否被毁坏

Debug模式下初始化变量为0xcc

假设我们有下面的代码:

void func()

{

int a;

int b;

int c;

}

对应的汇编代码如下:

?func@@YAXXZ PROC ; func, COMDAT

; 38 : {

push ebp

mov ebp, esp

sub esp, 228 ; 000000e4H

push ebx

push esi

push edi

lea edi, DWORD PTR [ebp-228]

mov ecx, 57 ; 00000039H

mov eax, -858993460 ; ccccccccH

rep stosd

; 39 : int a;

; 40 : int b;

; 41 : int c;

; 42 :

; 43 : }

pop edi

pop esi

pop ebx

mov esp, ebp

pop ebp

ret 0

?func@@YAXXZ ENDP

1. sub esp, 228s编译器为栈分配了228byte

2. 接着3push指令保存寄存器

3. Lea edi, DWORD PTR [ebp-228]一直到repstosd指令是初始化从ebp-228开始写570xcccccccc,也就是57*4=2280xcc,正好填满之前sub esp, 228所分配的空间。这段代码会把所有的变量初始化为0xcc

选择0xcc是有一定理由的:

1. 0xcc不同于一般的初始化值,人们一般倾向于把变量初始化为0, 1, -1等比较简单的值,而0xcc一般情况下足够大,而且是负数,容易引起注意,而且一般变量的值很有可能不允许是0xcc,比较容易造成错误

2. 0xcc = int 3,如果作为代码执行,则会引发断点异常,比较容易引起注意

检查数组变量的Overrun

假设我们有下面的代码:

void func

{

char buf[104];

scanf("%s", buf);

return 0;

}

scanf调用之后,会执行下面的代码:

mov ecx, ebp

push eax

lea edx, DWORD PTR $LN5@wmain

call @_RTC_CheckStackVars@8

这段代码会调用_RTC_CheckStackVars@8函数会在数组的开始和结束的地方检查0xcccccccc有否被破坏,如果是,则报告错误。_RTC_CheckStackVars由于代码过长这里就不给出了,这个函数主要是利用编译器保存的数组位置和长度信息,检查数组的开头和结尾:

$LN5@func:

DD 1

DD $LN4@func

$LN4@func:

DD -112 ; ffffff90H

DD 104 ; 00000068H

DD $LN3@func

$LN3@func:

DB 98 ; 00000062H

DB 117 ; 00000075H

DB 102 ; 00000066H

DB 0

$LN5@func纪录了数组的个数,而$LN4@func保存了数组的偏移量ebp - 112和数组的长度104,而$LN3@func则保存了变量的名称(0x62, 0x75, 0x66, 0 = “buf”)。

检查ESP

ESP的错误很有可能是由调用协定的mistach造成,或者Stack本身没有平衡。编译器会在调用其他函数和在函数PrologEpilog(开始和结束代码)的时候插入对ESP的检查:

1. 在调用其他外部函数的时候:

假设我们有下面的代码:

printf( "%d", 1 );

对应的汇编代码如下:

mov esi, esp

push 1

push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@

call DWORD PTR __imp__printf

add esp, 8

cmp esi, esp

call __RTC_CheckEsp

可以看到检查的代码非常简单直接,把ESP保存在ESI之中,当调用printf,平衡堆栈之后,检查espesi的是否一致,然后调用__RTC_CheckESP__RTC_CheckESP代码也很简单:

_RTC_CheckEsp:

00412730 jne esperror (412733h)

00412732 ret

esperror:

……

00412744 call _RTC_Failure (411195h)

……

00412754 ret

如果不一致,跳转到esperror标号报告错误。

2. 函数返回的时候:

以下面的代码为例:

void func()

{

__asm

{

push eax

}

}

Func函数故意push eax来破坏堆栈的平衡性,对应的汇编代码如下:

?func@@YAXXZ PROC ; func, COMDAT

; 38 : {

push ebp

mov ebp, esp

sub esp, 192 ; 000000c0H

push ebx

push esi

push edi

lea edi, DWORD PTR [ebp-192]

mov ecx, 48 ; 00000030H

mov eax, -858993460 ; ccccccccH

rep stosd

; 39 : __asm

; 40 : {

; 41 : push eax

push eax

; 42 : }

; 43 : }

pop edi

pop esi

pop ebx

add esp, 192 ; 000000c0H

cmp ebp, esp

call __RTC_CheckEsp

mov esp, ebp

pop ebp

ret 0

?func@@YAXXZ ENDP

在函数的初始化代码中,func会将ebp保存在Stack中,并且把当前esp保存在ebp中。

?func@@YAXXZ PROC ; func, COMDAT

push ebp

mov ebp, esp

关键的检查代码在后面,当func函数恢复了堆栈之后,堆栈会恢复到之前刚保存espebp的那个状态,这个时候ebp必然等于esp,否则出错

cmp ebp, esp

call __RTC_CheckEsp

mov esp, ebp

pop ebp

ret 0

?func@@YAXXZ ENDP

出错的时候显示的对话框如下:

<shape id="Picture_x0020_6" style="VISIBILITY: visible; WIDTH: 328.5pt; HEIGHT: 147pt; mso-wrap-style: square" type="#_x0000_t75" o:spid="_x0000_i1025"><imagedata o:title="" src="file:///D:/tmp/msohtmlclip1/01/clip_image005.png"></imagedata></shape>

OK,这次就写到这里。下面几篇文章预定会写到下面这些内容:

1. /GS & Security Cookie

2. Calling Conventions

3. Name Mangling

4. Structured Exception Handling

5. Passing by Reference

6. Member functions

7. Object layout

8. Virtual functions

9. Virtual Inheritance

10. C++ Exceptions

11. Templates

敬请关注。

作者: ATField
Blog:
http://blog.csdn.net/atfield
转载请注明出处




分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics