The Compiler Did What?
I was recently investigating a crash in an application. As I researched the issue I found a very old defect in the code that was only recently being exposed by the compiler.
The crash occurred at the below instruction because the ebx register does not hold a valid pointer.
0:001> r
eax=d9050cf7 ebx=003078c0 ecx=6e2e0000 edx=00000000 esi=00000001 edi=0c334468
eip=65637fbe esp=010eb408 ebp=010eb878 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
riched20!CTxtSelection::CreateCaret+0x429:
65637fbe 8b4b1c mov ecx,dword ptr [ebx+1Ch] ds:002b:003078dc=????????
0:001> dd 003078c0
003078c0 ???????? ???????? ???????? ????????
003078d0 ???????? ???????? ???????? ????????
003078e0 ???????? ???????? ???????? ????????
003078f0 ???????? ???????? ???????? ????????
00307900 ???????? ???????? ???????? ????????
00307910 ???????? ???????? ???????? ????????
00307920 ???????? ???????? ???????? ????????
00307930 ???????? ???????? ???????? ????????
Examining the assembly leading up to the crash, ebx came from [ebp-40c].
0:001> ub .
riched20!CTxtSelection::CreateCaret+0x408:
65637f9d 6a08 push 8
65637f9f ff156cf06465 call dword ptr [riched20!_imp__CreateBitmap (6564f06c)]
65637fa5 898784000000 mov dword ptr [edi+84h],eax
65637fab eb06 jmp riched20!CTxtSelection::CreateCaret+0x41e (65637fb3)
65637fad 8bb5e4fbffff mov esi,dword ptr [ebp-41Ch]
65637fb3 8b9df4fbffff mov ebx,dword ptr [ebp-40Ch]
65637fb9 ff775c push dword ptr [edi+5Ch]
65637fbc 6a01 push 1
0:001> dd @ebp-40c l1
010eb46c 003078c0
Looking at the whole function, [ebp-40c] was populated at the beginning of the function as the contents of edi+1C. The contents of edi+1Ch were first moved into ecx and later the value of ecx was moved into [ebp-40Ch]. Further examination of the whole function showed the edi register is unchanged at the time of the crash, so I can use its current value to determine what [ebp-40c] should contain.
0:001> uf riched20!CTxtSelection::CreateCaret
riched20!CTxtSelection::CreateCaret:
65637b95 8bff mov edi,edi
65637b97 55 push ebp
65637b98 8bec mov ebp,esp
65637b9a 81ec5c040000 sub esp,45Ch
65637ba0 a100e06465 mov eax,dword ptr [riched20!__security_cookie (6564e000)]
65637ba5 33c5 xor eax,ebp
65637ba7 8945fc mov dword ptr [ebp-4],eax
65637baa 53 push ebx
65637bab 56 push esi
65637bac 57 push edi
65637bad 8bf9 mov edi,ecx
65637baf 8b4f1c mov ecx,dword ptr [edi+1Ch] <<< The value originates from [edi+1Ch]
65637bb2 0fbf4740 movsx eax,word ptr [edi+40h]
65637bb6 898df4fbffff mov dword ptr [ebp-40Ch],ecx <<< Store the value on the stack
<snip>
65637fb3 8b9df4fbffff mov ebx,dword ptr [ebp-40Ch] <<< Read the value from the stack
<snip>
65637fbe 8b4b1c mov ecx,dword ptr [ebx+1Ch] <<< Crash here because ebx is invalid
<snip>
The expected value of [ebp-40C], and thus the expected value of the ebx register, is 091978c0 based on the value in [edi+1Ch] at the time of the crash. This would be a valid pointer and is not what is currently in [ebp-40C] or ebx. It is noteworthy that at the time of the crash, ebx is similar to what should be there, it differs only by the high word of the dword.
0:001> r ebx
ebx=003078c0
0:001> dd @edi+1c l1
0c334484 091978c0
The expected value, 091978c0, is a valid pointer.
0:001> dd 091978c0
091978c0 091978c8 00000000 00000501 05000000
091978d0 00000015 076c1a27 2a372f35 0c2e3998
091978e0 000049aa 00000000 00000000 00000000
091978f0 00000000 00000000 00000000 00000000
09197900 00000000 00000000 00000000 00000000
09197910 00000000 00000000 00000000 00000000
09197920 1a3098a8 00000000 00000000 00000000
09197930 00000000 00000000 00000000 00000000
Somehow the value at ebp-40C was changed between instruction 65637bb6, where [ebp-40C] was set, and instruction 65637fb3 where [ebp-40C] was read. Fortunately I had a mechanism to reproduce this crash so I was able to set a breakpoint and trace through how this happened.
First I set a breakpoint on the instruction that populates [ebp-40C].
0:003> bp 65637bb6
0:003> g
Breakpoint 0 hit
eax=ffffffff ebx=0c334468 ecx=091978c0 edx=00000060 esi=091978c0 edi=0c334468
eip=65637bb6 esp=010eb410 ebp=010eb878 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
riched20!CTxtSelection::CreateCaret+0x21:
65637bb6 898df4fbffff mov dword ptr [ebp-40Ch],ecx ss:002b:010eb46c=00000000
Next I calculated ebp-40C and set a break on write access breakpoint.
0:001> ?@ebp-40c
Evaluate expression: 17740908 = 010eb46c
0:001> ba w4 010eb46c
0:001> g
Breakpoint 1 hit
eax=00000030 ebx=00000000 ecx=00000000 edx=00000020 esi=00000001 edi=0c334468
eip=65637f67 esp=010eb40c ebp=010eb878 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
riched20!CTxtSelection::CreateCaret+0x3d2:
65637f67 66898475f4fbffff mov word ptr [ebp+esi*2-40Ch],ax ss:002b:010eb46e=0919
The write breakpoint hit at a location I was not expecting. The instruction where the breakpoint hit is not modifying the variable that was stored at [ebp-40C].
Although I cannot share the Windows source code on this blog, the code in question roughly resembles the below example. Note that a proficient assembly language reader could figure out the code flow, this example is not sharing any magic.
Struct1* p1;
WORD array[512];
…
p1 = GetStruct1();
…
array[i-2] = 0x30;
…
p1->p = variable2; // Crash here because p1 is not a valid pointer
…
We are crashing because p1 is not a valid pointer. The high word of p1 is being overwritten as 0030 by the line “array[i-2] = 0x30;” because i is 1, leading to an underflow of the array. This underflow is corrupting the pointer in p1.
0:001> r ebx
ebx=003078c0
Clearly there is a defect in the above code. If it is legitimate for i to be 1 (and it is), then a check must be made to prevent an underflow of the array. However further research found that this code has been consistent for many years and many releases of the product. Why is this suddenly crashing now? As the bank robber in Dirty Harry said, “I gots to know."
In the above assembly we calculate that “array” starts at ebp-408 (assuming i is always 2 or greater, 2*2-40c is -408). In the earlier assembly we see that p1 is placed at ebp-40c. In this configuration an underflow of “array” will always corrupt p1.
Examining the assembly on a system that does not crash, I found that the local variables are stored differently in a different version of this binary. In the beginning of the function we see that p1 is stored in ebx. In this version of the binary ebx is never stored on the stack, so it cannot be corrupted by an underflow.
0:000> uf riched20!CTxtSelection::CreateCaret
riched20!CTxtSelection::CreateCaret:
74e75c53 8bff mov edi,edi
74e75c55 55 push ebp
74e75c56 8bec mov ebp,esp
74e75c58 81ec58040000 sub esp,458h
74e75c5e a19010e974 mov eax,dword ptr [riched20!__security_cookie (74e91090)]
74e75c63 53 push ebx
74e75c64 56 push esi
74e75c65 8bf1 mov esi,ecx
74e75c67 8b5e1c mov ebx,dword ptr [esi+1Ch]
The code that populates array[i-2] with 0x30 is later in the function. In this version, array is stored at ebp-404. If there is an underflow it will corrupt ebp-408.
riched20!CTxtSelection::CreateCaret+0x3e1:
74e76034 66c7847df8fbffff3000 mov word ptr [ebp+edi*2-408h],30h
The value stored at ebp-408 is used in several places in this function, however it is never used after instruction 74e76034 executes. This means any underflow in the array only corrupts memory that is not used after the corruption, and as a result the corruption never results in a crash. Although this defect has existed for a long time, the compiler has protected us until now.
74e75d3f 0b85f8fbffff or eax,dword ptr [ebp-408h]
…
74e75e51 ffb5f8fbffff push dword ptr [ebp-408h]
…
74e75e8a 8b8df8fbffff mov ecx,dword ptr [ebp-408h]
…
74e75f20 398df8fbffff cmp dword ptr [ebp-408h],ecx
…
74e75fec 8b85f8fbffff mov eax,dword ptr [ebp-408h]
The issue discussed in this article was addressed as part of KB2883200.