Compartilhar via


Native code caller verification - and how not to do it.

[Since people asked - I re-posted this from my other blog.  I didn't “steal“ it.]

Recently on Raymond Chen’s blog he had a post about not trusting return addresses. Specifically to not use the _ReturnAddress() intrinsic and GetModuleHandleEx to figure out if the caller is “trusted”.

I had to try and come up with a simple example that demonstrated what he was talking about – for my own education if nothing else.

So given this code – there is a function “doSecureAction” which attempts to verify that it’s caller is located in it’s own module, and therefore trusted, by doing exactly what Raymond outlines.

Additionally there is a function “trustedCaller” which, because it resides in the same module, is trusted. It is allowed to call into doSecureAction.

Those functions are in verifyCaller.dll.

The test executable consists simply of “main” and builds to testCaller.exe.

Notice that it first calls doSecureAction to validate to it fails (writes failure message to the console) – then we build a simple stack frame, jump into doSecureAction and end up right at the next statement.

Ok, so why not jump right to the code we care about? Why not just jump past the validation? This example is trivial but it’s not hard to contrive one that does the validation after setting up some variables or other state – so you need to be able to hit both code paths. Also you may destructors running, balancing COM AddRef/Releases, freeing handles, deallocations, etc.

Why a jump and not a call?    Because I rolled the stack my own.
What is 0x10B9?    It is the “ret” in trustCaller.
What is 0x53?    It is the offset in main to the statement after the jmp.

Either of these may need to change in your environment.

The code ...


// verifyCallerDLL.cpp
#include <windows.h>
#include <iostream>
using namespace std;

#include "vcall.h"

extern "C" void * _ReturnAddress(void);

/*
* Super secure function. We are so tricky We ensure that we
* have a valid caller by checking the stack frames to make sure the
* return address is in our module. Because if we made the call then
* we know that it must be safe to do anything.
*/
void doSecureAction()
{
    void *caller = _ReturnAddress();

    HMODULE hCallerModule = NULL;
HMODULE hMyModule = NULL;

    if(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(LPCSTR)caller,
&hCallerModule) && hCallerModule != NULL)
{
        if(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(LPCSTR)doSecureAction,
&hMyModule) && hMyModule != NULL)
{
            if(hMyModule == hCallerModule)
{
cout << "caller is trusted\n";
}
            else
{
cout << "caller is not trusted\n";
}
}
}

    if(hCallerModule)
{
FreeLibrary(hCallerModule);
}

    if(hMyModule)
{
FreeLibrary(hMyModule);
}

    return;
}

/*
* This is a function we trust. We know it does proper validation
* of every thing before calling off to our ultra-secure function.
*/
void trustedCaller()
{
doSecureAction();
}


// testVerify.cpp
#include <windows.h>
#include <iostream>
using namespace std;

#include <vcall.h>

int main()
{
    // yup ... this fails as we expect.
doSecureAction();

    HMODULE hTrustedModule = NULL;

    // if using edit/continue or incremental linking then this address might
    // actually be a thunk in our own process. Disable those or use 
    // LoadLibrary/GetProcAddress
    if(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(LPCSTR)trustedCaller,
&hTrustedModule))
{
        if(hTrustedModule != NULL)
{
            // a ret opcode in the trusted module.
            // doSecureAction will return to here
            int retTrusted = (int)hTrustedModule+0x10B9;

            // the instruction following the __asm block (cout)
            // The ret at retTrusted will return us to here
            int retMain = (int)(main)+0x53;

            __asm
{
push dword ptr retMain
push dword ptr retTrusted
jmp dword ptr doSecureAction
}

            cout << "done\n";
}
}

    return 0;
}