Meditation, The Art of Exploitation

Thinking? At last I have discovered it--thought; this alone is inseparable from me. I am, I exist--that is certain. But for how long? For as long as I am thinking. For it could be, that were I totally to cease from thinking, I should totally cease to exist....I am, then, in the strict sense only a thing that thinks.

Tuesday, May 16, 2006

WIN32 SEH and Memory Management considered harmful (Part 3)

Originally composed on June 3rd, 2004, editted formatting.

Think about this problem again, is there a way to break this dillema between memory allocation constraint and fault handling mechanism. If we could somehow manipulate the register value at the fault address machine instruction, we could change its value to an updated memory address allocated inside of the fault handler

To acheive this, we will have to sacrifice the code portability by directly embedding assembly code into the C source code. Remember the code was like this:

code = (PPATCHCODE)VirtualAlloc(NULL, NR*sizeof(PATCHCODE),
while(_ftscanf(inp, "%X%X%X", &addr, &o_val, &n_val) != EOF){
code[record].addr = addr;
code[record].orig_val = o_val;
code[record].new_val = n_val;

We will use EAX to contain the address that we will write the data into. The design is to then update EAX in the CONTEXT record delivered in the EXCEPTION_RECORD to the fault handler. After the fault handler returns, the CONTEXT record will have a new EAX value. The operating system switches to this new CONTEXT (the context of the process where the EXCEPTION occured) and blindly accepts the new EAX value. Since the new EAX value now contains a valid memory address, the write operation will proceed. Without doing this, we have no way to control what might come out of the simple "code[record].addr = addr;" from the compiler. The new code snippet looks like this:

code = (PPATCHCODE)VirtualAlloc(NULL, NR*sizeof(PATCHCODE),
while(_ftscanf(inp, "%X%X%X", &addr, &_val, &n_val) != EOF){
paddr = code+record;
mov eax, paddr;
mov ebx, addr;
mov [eax], ebx; <------- Fault occurs here. mov ecx, dwsize;
mov ebx, o_val;
mov [eax+ecx], ebx;
// The compiler cannot genereate correct code for
// mov [eax+dwsize], ebx
// it's translated to mov [eax+ebp-20], ebx
// we wanted mov [eax+[ebp-20]], ebx
// so just use hardcoded # here, mov [eax+4], ebx
// or use ecx to contain the number
mov ebx, n_val;
mov [eax+ecx*2], ebx

We first calculate the memory address of the patchcode record structure (which is padded and aligned on a 16-byte cache line on ia32 platform) by adding the starting address of the patchcode array and the current record number. We then assign this address to EAX, this is the address where the next write operation will access. The patchcode address value is assigned to EBX and then moved to the memory address pointed to by EAX. If we ever access the memory beyond we initially allocated, we will have a memory access violation at this instruction "mov [eax], ebx". After the exception occured, the execution is transfered to the user fault handler through a series of complicated operations, trap gate, kernel fault handler, compiler stud fault handler, and finally user fault handler. We do exactly what is explained in the fault handler, we update the EAX value in the user process CONTEXT record:

tmp_code = (PPATCHCODE)VirtualAlloc(NULL, 2*NR*sizeof(PATCHCODE),
_tcsncpy((_TCHAR *)tmp_code, (_TCHAR *)code, NR*sizeof(PATCHCODE));
VirtualFree(code, 0, MEM_RELEASE);
lpEP->ContextRecord->Eax = (ULONG)(tmp_code+NR);
code = tmp_code;

First we allocate twice large a memory space of the previous size, then copy the content from the previous patchcode array to the new memory space. After we free the previous memory allocated, we update the EAX value in the user process CONTEXT. After some other updates, we return EXCEPTION_CONTINUE_EXECUTION to tell the kernel that this should be the last unwind fault handler and the EXCEPTION has been handled, the execution should now be continued at the exact address where the fault occured. Since now the EAX has the updated memory address, this code surely works.

Although this code works, it's not without sacrifice of code portability and wasted memory allocation and data transfer inside of the memory space. The use of embedded assembly code and CONTEXT structure member prohibits this code to work on other platform other than 32bit intel architecture. The fault handler has to allocates new memory, transfer data from previous memory to new memory, and then free previous memory. It's a waste of memory space and CPU time.