Robert Baruch
2001-Dec-11 18:36 UTC
VirtualProtect and app crash: what's your interpretation?
Here is my thought process on why the application crashes with a protection violation reading a section of memory. I used IDA to disassemble the app. Here's the section where it reads from memory and crashes because of a protection violation: 00760D4A sub_760D4A proc near ; CODE XREF: sub_75FCB0+159^Xp 00760D4A push ebp 00760D4B mov eax, ds:dword_75D66C 00760D50 mov ebp, esp 00760D52 sub esp, 14h 00760D55 push ebx ; ; localvar <- *(long *)75D66C = 0x56000 ; 00760D56 mov [ebp-4], eax 00760D59 push esi 00760D5A push edi 00760D5B push eax 00760D5C push 75D5F0h 00760D61 call sub_75FC7B 00760D66 add esp, 8 00760D69 mov ds:dword_75D720, eax 00760D6E movzx ecx, ds:word_75D5F2 00760D75 cmp ecx, eax 00760D77 jbe loc_760EFC 00760D7D mov ecx, ds:dword_75D704 00760D83 imul eax, 28h 00760D86 mov eax, [eax+ecx+34h] 00760D8A sub eax, [ebp-4] 00760D8D cmp ds:dword_75D734, 2 00760D94 mov [ebp-14h], eax ; ; If (*(long *)75D734) != 2 then skip the call to ; VirtualProtect ; 00760D97 jnz short loc_760DC4 ; ; Push arguments to VirtualProtect( ; LPVOID lpAddress, ; SIZE_T dwSize, ; DWORD flNewProtect, ; PDWORD lpflOldProtect) ; 00760D99 lea eax, [ebp-8] ; ; lpflOldProtect is a local variable ; 00760D9C push eax ; ; flNewProtect = 4 = PAGE_READWRITE ; 00760D9D push 4 ; ; dwSize = size of region to protect = a local variable ; 00760D9F push dword ptr [ebp-14h] ; ; lpAddress = base address of region to protect ; (*(long *)75D728) + localvar ; 00760DA2 mov eax, ds:dword_75D728 00760DA7 add eax, [ebp-4] 00760DAA push eax 00760DAB call ds:VirtualProtect 00760DB1 test eax, eax ; ; On success go to loc_760DC4 ; 00760DB3 jnz short loc_760DC4 ; ; This routine prints an error message and exits ; 00760DB5 push 3 00760DB7 push 75B470h 00760DBC call sub_7602F6 00760DC1 add esp, 8 00760DC4 00760DC4 loc_760DC4: ; CODE XREF: sub_760D4A+4D^Xj 00760DC4 ; sub_760D4A+69^Xj 00760DC4 mov esi, ds:dword_75D728 00760DCA add esi, [ebp-4] ; ; here is the instruction causing the fault: ; eax <- *(0x10 + (*(long *)75D728) + localvar) ; 00760DCD mov eax, [esi+10h] 00760DD0 test eax, eax 00760DD2 jz loc_760EB4 00760DD8 00760DD8 loc_760DD8: ; CODE XREF: sub_760D4A+164^Yj 00760DD8 mov ecx, ds:dword_75D728 00760DDE lea edi, [ecx+eax] 00760DE1 mov eax, [esi+0Ch] 00760DE4 add eax, ecx 00760DE6 push eax 00760DE7 mov [ebp-10h], eax 00760DEA call ds:LoadLibraryA 00760DF0 mov [ebp-0Ch], eax 00760DF3 test eax, eax 00760DF5 jnz short loc_760E04 00760DF7 push 6 00760DF9 push dword ptr [ebp-10h] 00760DFC call sub_7602F6 00760E01 add esp, 8 At this point, winedbg says First chance exception: page fault on read access to 0x00456010 in 32-bit code (0x00760dcd) According to IDA, that section at 0x456000 is named .shrink1, is an uninitialized section of memory, and should be read/write: 00456000 ; Section 3. (virtual address 00056000) 00456000 ; Virtual size : 00002000 ( 8192.) 00456000 ; Section size in file : 00000000 ( 0.) 00456000 ; Offset to raw data for section: 00000000 00456000 ; Flags C0000082: Noload Bss Readable Writable 00456000 ; Alignment : 16 bytes by default 00456000 00456000 .shrink1 segment para public '' use32 00456000 assume cs:.shrink1 00456000 ;org 456000h In addition, the trace of the last call to VirtualProtect shows this: 0806d398:Call kernel32.VirtualProtect(00459000,00000018,00000002,40616e74) ret=0075fd82 0806d398:trace:virtual:VirtualProtect 0x459000 00000018 00000002 0806d398:trace:virtual:VIRTUAL_SetProt 0x459000-0x459fff c-r-- View: 0x400000 - 0x765fff 28 0x400000 - 0x400fff c-r-- 0x401000 - 0x458fff c---- <-- here is where the read is 0x459000 - 0x459fff c-r-- 0x45a000 - 0x758fff c---- 0x759000 - 0x765fff c-rw- 0806d398:Ret kernel32.VirtualProtect() retval=00000001 ret=0075fd82 Note that this call has a protect of 2, not 4. That means the branch at 00760D97 must have been taken. That branch gets taken if *(long *)75D734 is not equal to 2. gdb shows that the contents is 3. And where does this value come from? It is loaded only in this subroutine: 0075F000 sub_75F000 proc near ; CODE XREF: start+7C^Yp 0075F000 call ds:GetVersion 0075F006 movzx ecx, ah 0075F009 movzx edx, al 0075F00C mov dword_75D738, ecx 0075F012 mov dword_75D730, edx 0075F018 test eax, 80000000h 0075F01D jz short loc_75F03C 0075F01F cmp edx, 4 0075F022 jge short loc_75F030 0075F024 mov dword_75D734, 1 0075F02E jmp short locret_75F046 0075F030 0075F030 loc_75F030: ; CODE XREF: sub_75F000+22^Xj 0075F030 mov dword_75D734, 2 0075F03A jmp short locret_75F046 0075F03C 0075F03C loc_75F03C: ; CODE XREF: sub_75F000+1D^Xj 0075F03C mov dword_75D734, 3 0075F046 0075F046 locret_75F046: ; CODE XREF: sub_75F000+2E^Xj 0075F046 ; sub_75F000+3A^Xj 0075F046 retn 0075F046 sub_75F000 endp *(long *)75D738 is the major version of Windows, 4. *(long *)75D730 is the minor version, 0. *(long *)75D734 is a "level", 3 for NT/2000/XP, 2 for 95/98/Me, or 1 for win32s. It contains 3. Thus, GetVersion is correctly returning NT 4.0. This was the trace of the last call to GetVersion: 0806d398:Call kernel32.GetVersion() ret=0075f006 0806d398:Ret kernel32.GetVersion() retval=05650004 ret=0075f006 So the funny thing is that the app skips the VirtualProtect (which would have made the memory have read/write access) only if the Windows version is not 95/98/Me. But I didn't get a crash when I ran this natively under 2000 (which was running under VMWare)! So next I need to find the place where the protection is placed on the memory range in question. The wine trace shows this: 0806d398:Call kernel32.VirtualProtect(00401000,00358000,00000001,40616e74) ret=0075fd22 0806d398:trace:virtual:VirtualProtect 0x401000 00358000 00000001 0806d398:trace:virtual:VIRTUAL_SetProt 0x401000-0x758fff c---- View: 0x400000 - 0x765fff 28 0x400000 - 0x400fff c-r-- 0x401000 - 0x758fff c---- 0x759000 - 0x765fff c-rw- 0806d398:Ret kernel32.VirtualProtect() retval=00000001 ret=0075fd22 And the listing around that address is: 0075FCB0 sub_75FCB0 proc near ; CODE XREF: sub_7605E2+47^Yp 0075FCB0 push ebp 0075FCB1 mov ebp, esp 0075FCB3 sub esp, 4 0075FCB6 cmp dword_75D734, 2 0075FCBD push esi ; ; If "level" is not 2, call bunches of VirtualProtects. ; 0075FCBE jnz short loc_75FCE4 0075FCC0 mov eax, dword_75D728 0075FCC5 push eax 0075FCC6 push 75D5F0h 0075FCCB push dword_75D72C 0075FCD1 push dword_75B1B4 0075FCD7 call sub_7617D3 0075FCDC add esp, 10h 0075FCDF jmp loc_75FDAD 0075FCE4 0075FCE4 loc_75FCE4: ; CODE XREF: sub_75FCB0+E^Xj 0075FCE4 mov eax, dword_75D63C 0075FCE9 add eax, dword_75D728 0075FCEF push eax 0075FCF0 push dword_75D728 0075FCF6 call sub_75F07E 0075FCFB add esp, 8 ; ; This is VirtualProtect call from above: ; ; base address = *(long *)75D624 + *(long *)75D728 ; size = *(long *)75D63C - *(long *)75D624 ; access = PAGE_NOACCESS ; oldaccessptr = localvar ; 0075FCFE lea ecx, [ebp-4] 0075FD01 mov eax, dword_75D63C 0075FD06 push ecx 0075FD07 push 1 0075FD09 sub eax, dword_75D624 0075FD0F push eax 0075FD10 mov eax, dword_75D624 0075FD15 add eax, dword_75D728 0075FD1B push eax 0075FD1C call ds:VirtualProtect 0075FD22 test eax, eax 0075FD24 jnz short loc_75FD35 ; 0075FD26 push 7 0075FD28 push 75B3FCh 0075FD2D call sub_7602F6 0075FD32 add esp, 8 0075FD35 0075FD35 loc_75FD35: ; CODE XREF: sub_75FCB0+74^Xj 0075FD35 mov esi, dword_75D6AC 0075FD3B push esi 0075FD3C push 75D5F0h 0075FD41 call sub_75FC7B 0075FD46 add esp, 8 ; 0075FD49 mov [ebp-4], eax 0075FD4C movzx eax, word_75D5F2 0075FD53 cmp eax, [ebp-4] 0075FD56 jbe short loc_75FD95 ; ; This is the last VirtualProtect call. ; base address = esi + *(long *)dword_75D728 ; = 0x459000 ; size = *(long *)75D6B0 ; = 0x18 ; new = the return value of sub_75FC23 ; = 2 = PAGE_READONLY ; oldptr = ? ; 0075FD58 lea eax, [ebp-4] 0075FD5B push eax 0075FD5C push dword ptr [ebp-4] 0075FD5F push dword_75D704 0075FD65 call sub_75FC23 0075FD6A add esp, 8 0075FD6D push eax 0075FD6E mov eax, dword_75D728 0075FD73 push dword_75D6B0 0075FD79 add eax, esi 0075FD7B push eax 0075FD7C call ds:VirtualProtect 0075FD82 test eax, eax 0075FD84 jnz short loc_75FD95 ; 0075FD86 push 7 0075FD88 push 75B400h 0075FD8D call sub_7602F6 0075FD92 add esp, 8 0075FD95 0075FD95 loc_75FD95: ; CODE XREF: sub_75FCB0+A6^Xj 0075FD95 ; sub_75FCB0+D4^Xj 0075FD95 mov eax, dword_75D664 0075FD9A push eax 0075FD9B push 75D5F0h 0075FDA0 call sub_75FC7B 0075FDA5 add esp, 8 0075FDA8 mov dword_75D5E4, eax Note that *(long *)75D728 was also mentioned in the skipped VirtualProtect call. That address contains 0x400000, which is the base of the memory-mapped executable: 0806d398:trace:module:map_image mapped PE file at 0x400000-0x766000 0806d398:trace:module:map_image mapping section .rdata at 0x459000 off 400 size 200 flags 50000040 0806d398:trace:module:map_image mapping section .shrink3 at 0x759000 off 7600 size 200 flags 40000042 0806d398:trace:module:map_image clearing 0x759200 - 0x75a000 0806d398:trace:module:map_image mapping section .rdata at 0x75a000 off 301000 size 200 flags 40000040 0806d398:trace:module:map_image mapping section .data at 0x75b000 off 800 size 1600 flags c0000040 0806d398:trace:module:map_image clearing 0x75c600 - 0x75d000 0806d398:trace:module:map_image mapping section .idata at 0x75e000 off 1e00 size a00 flags c0000040 0806d398:trace:module:map_image mapping section .load at 0x75f000 off 2800 size 3200 flags 68040020 0806d398:trace:module:map_image mapping section .reloc at 0x763000 off 5a00 size 800 flags 42000040 0806d398:trace:module:map_image mapping section .rsrc at 0x764000 off 6200 size 1400 flags 50000040 0806d398: dup_handle( src_process=-1, src_handle=24, dst_process=-1, access=00000000, inherit=0, options=2 ) 0806d398: dup_handle() = 0 { handle=28, fd=-1 } View: 0x400000 - 0x765fff 28 0x400000 - 0x765fff c-rw- That address is filled via a call to VirtualQuery, storing the allocation base address of the virtual memory space containing the address 0x75D5F0. Why is the app trying to read from 0x456010? Where did it get the 0x56000 offset from? That offset was stored in 0x75D66C. That address is not mentioned explicitly anywhere else, but looking at the memory around that value, it looks like there is an array of file segment info there. So this call: ; ; This is VirtualProtect call from above: ; ; base address = *(long *)75D624 + *(long *)75D728 ; size = *(long *)75D63C - *(long *)75D624 ; access = PAGE_NOACCESS ; oldaccessptr = localvar ; actually sets PAGE_NOACCESS from the beginning of a segment to the end of the segment. That segment is at 0x1000, and the next segment in the array starts at 0x359000. At this point I'm totally nonplussed. Does anyone know of an NT/2000 debugger that works? I tried Borland's free TurboDebugger 5.5 but that failed to run the program. --Rob