Filling CONTEXT structure pre-XP

Started by
7 comments, last by SiCrane 16 years, 4 months ago
I'm trying to get one of my currently XP or later applications running on Windows 2000/x86. Currently I'm having trouble getting some stack walking functionality working without the use of RtlCaptureContext(), which doesn't appear on Windows 2000 boxes. I'm currently using C++ with MSVC 7.1. So far I've tried throwing a dummy exception and capturing the context in the __except handler:

    EXCEPTION_POINTERS * eps = 0;
    __try {
      RaiseException(0, 0, 0, 0);
    } __except((eps = GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) {
    }
    CONTEXT ctx = *(eps->ContextRecord);
This is both with RaiseException() and various variations of throw, different exception codes, assigning the CONTEXT in the exception handler and outside the exception handler, and so on. This generates a CONTEXT, but the stack walk is funny: it only shows two entries on the stack, no matter where it's called, when both the debugger stack view and the RtlCaptureContext() version show significantly deeper stack frames. (RaiseException() is shown as the top most frame when the RaiseException() version is used, so I assume that the stack walk is to some degree valid, but it remains unhelpful.) I've also tried use inline assembly to get at the EIP, ESP and EBP registers. ESP and EBP are trivial, but getting the EIP seems problematic. So far I've tried:

    __asm    call x
    __asm x: pop eax
    __asm    mov ctx.Eip, eax
But this generates a runtime error, and my assembly skills are sufficiently poor to be unable to see how to improve this (the EIP register being something I usually don't have any need to mess with). I'd also rather avoid multiple threads so GetThreadContext() is out. Also, the application uses a commercial library, which has a license that conflicts with the GPL, so many free library solutions are out. Any ideas? No rush on this, as getting this to run on 2000 is a low priority.
Advertisement
What runtime error do you get from using the asm code? And does this code have to run on x64? (Because inline asm is out in that case).
Quote:Original post by SiCrane
    __asm    call x    __asm x: pop eax    __asm    mov ctx.Eip, eax

But this generates a runtime error...

What runtime error, exactly? It works fine for me.
This is the simplest method of acquiring EIP, and so it's often used in shellcode and the likes. For this reason, some security mechanisms pick up on the obviously devious 'E8 00 00 00 00' (call next instruction) and raise a security warning. There are a few workarounds that can't really be heuristically filtered (Google "Get EIP"), but they all involve overlapping instructions (CALL half-way into another instruction, for example) and so can't be assembled inline. Nevertheless, you may get away with something simple like the FLDZ trick:

__asm {    FLDZ;    FNSTENV [ESP-0xC];    POP ctx.Eip;}


Quote:I'd also rather avoid multiple threads so GetThreadContext() is out.
I don't understand. Threads are quite capable of opening themselves. At least this is the case on Windows XP, and I never believed any different for 2000.

CONTEXT ctx;DWORD thread_id = GetCurrentThreadId();HANDLE thread_handle = OpenThread(THREAD_ALL_ACCESS, false, thread_id);ctx.ContextFlags = CONTEXT_ALL;GetThreadContext(thread_handle, &ctx);CloseHandle(thread_handle);


But most of all, I don't understand why you can't read the stack manually. Stack memory is all-access, so once you have ESP, you can just read it as an array of DWORDs. What am I missing?
Ring3 Circus - Diary of a programmer, journal of a hacker.
I knew I was forgetting something. The run time error was that one of the Vista test boxes was giving a DEP error when the stack walk code was called. It ran fine on the other machines I tried.

Quote:
And does this code have to run on x64? (Because inline asm is out in that case).

Vista/x86-64 is supposed to be a supported platform; but the application itself is 32-bit.

Quote:I don't understand. Threads are quite capable of opening themselves. At least this is the case on Windows XP, and I never believed any different for 2000.

GetThreadContext() can only be called on a suspended thread, which means I'd need to spawn a new thread, suspend the current thread, call the function, and return control to the old thread.

Quote:But most of all, I don't understand why you can't read the stack manually. Stack memory is all-access, so once you have ESP, you can just read it as an array of DWORDs. What am I missing?

I'm using StackWalk64() to walk the stack, which requires the CONTEXT structure to be filled. I'm not interested in writing or maintaining the code to walk the stack manually.
I suspect your problem is that the exception pointers are valid only during the filter expression. You must latch the context immediately instead of storing the EXCEPTION_POINTER and grabbing its context after the __except.

The heavily documented code below (GPL, but consider it dual-licensed as BSD for the OP's use) shows how to hopefully get this right.

// called for each stack frame found by walk_stack, passing information// about the frame and <user_arg>.// return INFO::CB_CONTINUE to continue, anything else to stop immediately// and return that value to walk_stack's caller.//// rationale: we can't just pass function's address to the callback -// dump_frame_cb needs the frame pointer for reg-relative variables.typedef LibError (*StackFrameCallback)(const STACKFRAME64*, void*);static void skip_this_frame(uint& skip, void* context){	if(!context)		skip++;}// iterate over a call stack, calling back for each frame encountered.// if <pcontext> != 0, we start there; otherwise, at the current context.// return an error if callback never succeeded (returned 0).//// lock must be held.static LibError walk_stack(StackFrameCallback cb, void* user_arg = 0, uint skip = 0, const CONTEXT* pcontext = 0){	// to function properly, StackWalk64 requires a CONTEXT on	// non-x86 systems (documented) or when in release mode (observed).	// exception handlers can call walk_stack with their context record;	// otherwise (e.g. dump_stack from debug_assert), we need to query it.	CONTEXT context;	// .. caller knows the context (most likely from an exception);	//    since StackWalk64 may modify it, copy to a local variable.	if(pcontext)		context = *pcontext;	// .. need to determine context ourselves.	else	{		skip_this_frame(skip, (void*)pcontext);		// there are 4 ways to do so, in order of preference:		// - asm (easy to use but currently only implemented on IA32)		// - RtlCaptureContext (only available on WinXP or above)		// - intentionally raise an SEH exception and capture its context		//   (spams us with "first chance exception")		// - GetThreadContext while suspended* (a bit tricky + slow).		//		// * it used to be common practice to query the current thread's context,		// but WinXP SP2 and above require it be suspended.		//		// this MUST be done inline and not in an external function because		// compiler-generated prolog code trashes some registers.#if CPU_IA32		ia32_asm_GetCurrentContext(&context);#else		// preferred implementation (was imported during module init)		if(pRtlCaptureContext)			pRtlCaptureContext(&context);		// not available: raise+handle an exception; grab the reported context.		else		{			__try			{				RaiseException(0xF001, 0, 0, 0);			}			__except(context = (GetExceptionInformation())->ContextRecord, EXCEPTION_CONTINUE_EXECUTION)			{			}		}#endif	}	pcontext = &context;	STACKFRAME64 sf;	memset(&sf, 0, sizeof(sf));	sf.AddrPC.Mode      = AddrModeFlat;	sf.AddrFrame.Mode   = AddrModeFlat;	sf.AddrStack.Mode   = AddrModeFlat;#if CPU_AMD64	sf.AddrPC.Offset    = pcontext->Rip;	sf.AddrFrame.Offset = pcontext->Rbp;	sf.AddrStack.Offset = pcontext->Rsp;#else	sf.AddrPC.Offset    = pcontext->Eip;	sf.AddrFrame.Offset = pcontext->Ebp;	sf.AddrStack.Offset = pcontext->Esp;#endif	// for each stack frame found:	LibError ret  = ERR::SYM_NO_STACK_FRAMES_FOUND;	for(;;)	{		// rationale:		// - provide a separate ia32 implementation so that simple		//   stack walks (e.g. to determine callers of malloc) do not		//   require firing up dbghelp. that takes tens of seconds when		//   OS symbols are installed (because symserv is wanting to access		//   inet), which is entirely unacceptable.		// - VC7.1 sometimes generates stack frames despite /Oy ;		//   ia32_walk_stack may appear to work, but it isn't reliable in		//   this case and therefore must not be used!		// - don't switch between ia32_stack_walk and StackWalk64 when one		//   of them fails: this needlessly complicates things. the ia32		//   code is authoritative provided its prerequisite (FP not omitted)		//   is met, otherwise totally unusable.		LibError err;#if CPU_IA32 && !CONFIG_OMIT_FP		err = ia32_walk_stack(&sf);#else		sym_init();		// note: unfortunately StackWalk64 doesn't always SetLastError,		// so we have to reset it and check for 0. *sigh*		SetLastError(0);		const HANDLE hThread = GetCurrentThread();		BOOL ok = StackWalk64(machine, hProcess, hThread, &sf, (PVOID)pcontext,			0, SymFunctionTableAccess64, SymGetModuleBase64, 0);		// note: don't use LibError_from_win32 because it raises a warning,		// and this "fails" commonly (when no stack frames are left).		err = ok? INFO::OK : ERR::FAIL;#endif		// no more frames found - abort. note: also test FP because		// StackWalk64 sometimes erroneously reports success.		void* fp = (void*)(uintptr_t)sf.AddrFrame.Offset;		if(err != INFO::OK || !fp)			return ret;		if(skip)		{			skip--;			continue;		}		ret = cb(&sf, user_arg);		// callback is allowing us to continue		if(ret == INFO::CB_CONTINUE)			ret = INFO::OK;	//  make sure this is never returned		// callback reports it's done; stop calling it and return that value.		// (can be either success or failure)		else		{			debug_assert(ret <= 0);	// shouldn't return > 0			return ret;		}	}}
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3
Uh, have you compiled your dummy exception code lately? You have the assignment:
context = (GetExceptionInformation())->ContextRecord

Where context is a CONTEXT and (GetExceptionInformation())->ContextRecord is a pointer to a CONTEXT.

In any case, I just tried:
__try{  RaiseException(0xF001, 0, 0, 0);}__except(ctx = *(GetExceptionInformation())->ContextRecord, EXCEPTION_CONTINUE_EXECUTION){}

which generates a stack trace containing two items:

FFFFFFFF|kernel32.dll|Unknown function
7C93D2A1|ntdll.dll|RtlInitializeSListHead()

While the details are different from my other dummy exception versions, it isn't much of an improvement. As I've mentioned, I've already tried variations on assigning the CONTEXT in the exception handler and outside the exception handler.
Doh! Indeed I haven't (all devs are running on IA32); thanks for pointing that out.

hm, that's a pretty poor looking stack trace :P
Two other ideas: are you setting all required STACKFRAME64 addresses and address types? Does the call to StackWalk64 pass all parameters?
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3
I'm reasonably certain that the stack walk code itself works fine; since if I use RtlCaptureContext() or the inline assembly to generate the CONTEXT the stack walk performs as expected (aside from the one Vista test machine as mentioned).
    STACKFRAME64 sf = {};    sf.AddrPC.Offset    = ctx.Eip;    sf.AddrPC.Mode      = AddrModeFlat;    sf.AddrStack.Offset = ctx.Esp;    sf.AddrStack.Mode   = AddrModeFlat;    sf.AddrFrame.Offset = ctx.Ebp;    sf.AddrFrame.Mode   = AddrModeFlat;      BOOL stack_walk_ok = StackWalk64(IMAGE_FILE_MACHINE_I386, process, thread, &sf,                                       &ctx, 0, &SymFunctionTableAccess64,                                        &SymGetModuleBase64, 0);

I don't pass a ReadMemoryRoutine or a TranslateAddress value to StackWalk64(), but those should be optional.
Ok, I figured out how to get the dummy exception version working. When using RaiseException(), the CONTEXT generated by the exception is only valid within the __except exception filter, so the stack walk has to be done within the parenthesis after the __except; it can't be done in the handler or after the __except block ends.
    __try {      RaiseException(0, 0, 0, 0);    } __except( generate_stack_walk(*(GetExceptionInformation())->ContextRecord),                 EXCEPTION_CONTINUE_EXECUTION) {    }

Apparently the exception filter is executed within the context of the RaiseException() function itself. After the exception filter executes, and control has passed to the exception handler, the stack frame for the RaiseException() function has been destroyed, so the stack walk fails.

By using a hardware exception such as an access violation, no stack frames are involved, so the CONTEXT structure can be assigned and the stack walk performed after the exception handler is executed.
    EXCEPTION_POINTERS * eps = 0;    __try {      char * ptr = 0; *ptr = 0;    } __except((eps = GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) {    }    CONTEXT ctx = *(eps->ContextRecord);

This topic is closed to new replies.

Advertisement