We had a small problem that we had to overcome last week during an assessment. This blog entry is going to show the mistakes we made along the way and how it was finally solved.
We had acquired a 64 bit DLL which exported a C++ class and associated methods that contained some secret sauce (reversing was infeasible in the allotted time). We've put together the following example DLL to demonstrate:
#define DLLCPP_API __declspec(dllexport)
class DLLCPP_API CDLLCPP {
private:
bool bInit;
public:
CDLLCPP();
~CDLLCPP();
int SuperSecretSauce(char *strString);
};
As we didn't have the code, library or headers we ran dumpbin.exe /exports against the compiled DLL to reveal the exported names (note: the code project solution to dynamic C++ DLL loading needs headers):
Microsoft (R) COFF/PE Dumper Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file DLLCPP.dll
File Type: DLL
Section contains the following exports for DLLCPP.dll
00000000 characteristics
4F2E3F6C time date stamp Sun Feb 05 08:35:56 2012
0.00 version
1 ordinal base
4 number of functions
4 number of names
ordinal hint RVA name
1 0 00001010 ??0CDLLCPP@@QEAA@XZ
2 1 00001020 ??1CDLLCPP@@QEAA@XZ
3 2 00001000 ??4CDLLCPP@@QEAAAEAV0@AEBV0@@Z
4 3 00001030 ?SuperSecretSauce@CDLLCPP@@QEAAHPEAD@Z
Summary
1000 .data
1000 .pdata
1000 .rdata
1000 .reloc
1000 .rsrc
1000 .text
We wanted to call the SuperSecretSauce method, so ran undname.exe on the export (note: the DLL is 64bit in this example as well hence the __ptr64):
C:\>undname ?SuperSecretSauce@CDLLCPP@@QEAAHPEAD@Z
Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.
Undecoration of :- "?SuperSecretSauce@CDLLCPP@@QEAAHPEAD@Z"
is :- "public: int __cdecl CDLLCPP::SuperSecretSauce(char * __ptr64) __ptr64"
Pro Tip: You can run undname.exe against the output of dumpbin.exe /exports:
- dumpbin /exports DLLCPP.dll > out.txt
- undname < out.txt
We end up with (our comments are in blue text):
// The constructor1 0 00001010 public: __cdecl CDLLCPP::CDLLCPP(void) __ptr64// The deconstructor2 1 00001020 public: __cdecl CDLLCPP::~CDLLCPP(void) __ptr64// The class3 2 00001000 public: class CDLLCPP & __ptr64 __cdecl CDLLCPP::operator=(class CDLLCPP const & __ptr64) __ptr64// The SuperSecretSauce Method4 3 00001030 public: int __cdecl CDLLCPP::SuperSecretSauce(char * __ptr64) __ptr64
Pro Tip 2: There is also the the UnDecorateSymbolName function in DbgHelp
We put together some code to try and call the method like so:
#include "stdafx.h"
#include <Windows.h>
#include <strsafe.h>
#include <Dbghelp.h>
typedef void (__cdecl* _ExampleSecretSauce)(char *);
int _tmain(int argc, _TCHAR* argv[])
{
fprintf(stdout,"[i] Recx - C++ class DLL example\n");
HMODULE modDLL = LoadLibrary(L".\\DLLCPP.dll");
if (modDLL==NULL)
{
fprintf(stderr,"[!] Could not load .\\DLLCPP.dll");
return 1;
}
_ExampleSecretSauce OurImportedSecretSauce = (_ExampleSecretSauce)GetProcAddress(modDLL,"?SuperSecretSauce@CDLLCPP@@QEAAHPEAD@Z");
if(OurImportedSecretSauce==NULL){
fprintf(stderr,"[!] Could not get address of the secret sauce method");
return 1;
}
OurImportedSecretSauce("Hello Secret Sauce");
return 0;
}
But when we run this code we get the following response from the SuperSecretSauce method if were lucky enough that it doesn't crash:
[i] Recx - C++ class DLL example
[!] Our constructor has not been called!
The error above is due a check we've put in the demo method, in the real world all manner of unpredictable outcomes could occur. At which point its worth pointing out when you normally instantiate a method from a C++ class the code would be something like this:
#include "stdafx.h"
#include "DLLCPP.h"
#pragma comment(lib,".\\Release\\dllcpp.lib")
int _tmain(int argc, _TCHAR* argv[])
{
CDLLCPP *cppDLL = new CDLLCPP();
cppDLL->SuperSecretSauce("Hello");
return 0;
}
So we modified the code to first import and call the constructor.#include "stdafx.h"
#include <Windows.h>
#include <strsafe.h>
#include <Dbghelp.h>
typedef void (__cdecl* _ExampleConstruct)();
typedef void (__cdecl* _ExampleSecretSauce)(char *);
int _tmain(int argc, _TCHAR* argv[])
{
fprintf(stdout,"[i] Recx - C++ class DLL example\n");
HMODULE modDLL = LoadLibrary(L".\\DLLCPP.dll");
if (modDLL==NULL)
{
fprintf(stderr,"[!] Could not load .\\DLLCPP.dll");
return 1;
}
_ExampleConstruct OurImportedConstructor = (_ExampleConstruct)GetProcAddress(modDLL,"??0CDLLCPP@@QEAA@XZ");
if(OurImportedConstructor==NULL){
fprintf(stderr,"[!] Could not get address of the constructor");
return 1;
}
_ExampleSecretSauce OurImportedSecretSauce = (_ExampleSecretSauce)GetProcAddress(modDLL,"?SuperSecretSauce@CDLLCPP@@QEAAHPEAD@Z");
if(OurImportedSecretSauce==NULL){
fprintf(stderr,"[!] Could not get address of the secret sauce method");
return 1;
}
OurImportedConstructor();
OurImportedSecretSauce("Hello Secret Sauce");
return 0;
}
When we ran this code it just crashed with an access violation in the constructor . This should have been obvious in hindsight. If we look at the disassembly for a legitimate call on 64 bit to new() and the constructor in the demo above we see the following (our comments in blue text):
Also there was a likely hint in the dumpbin out (in red text):000000013FF71000 sub rsp,38h 000000013FF71004 mov qword ptr [rsp+20h],0FFFFFFFFFFFFFFFEh CDLLCPP *cppDLL = new CDLLCPP(); 000000013FF7100D mov ecx,1 000000013FF71012 call qword ptr [__imp_operator new (13FF72148h)]// Microsoft x64 calling convention says the ret comes back in RAX (pointer)000000013FF71018 mov qword ptr [rsp+50h],rax 000000013FF7101D test rax,rax 000000013FF71020 je wmain+2Ch (13FF7102Ch)// The first argument - the pointer to the object - is passed in RCX - to the constructor000000013FF71022 mov rcx,rax 000000013FF71025 call qword ptr [__imp_CDLLCPP::CDLLCPP (13FF72008h)] 000000013FF7102B nop cppDLL->SuperSecretSauce("Hello"); 000000013FF7102C lea rdx,[string "Hello" (13FF721D0h)]// Again passing a pointer to the object in RCX from the original RAX result from new000000013FF71033 mov rcx,rax 000000013FF71036 call qword ptr [__imp_CDLLCPP::SuperSecretSauce (13FF72000h)]
// The constructor1 0 00001010 public: __cdecl CDLLCPP::CDLLCPP(void)__ptr64// The deconstructor2 1 00001020 public: __cdecl CDLLCPP::~CDLLCPP(void)__ptr64// The class3 2 00001000 public: class CDLLCPP & __ptr64 __cdecl CDLLCPP::operator=(class CDLLCPP const & __ptr64)__ptr64// The SuperSecretSauce Method4 3 00001030 public: int __cdecl CDLLCPP::SuperSecretSauce(char * __ptr64)__ptr64
All of the exports, after the function definition, listed an extra 64bit pointer. While we don't know for sure (as the output of undname isn't fully documented), but we think this is likely an indicator of the need to pass the object pointer. So the reason for the crash was that there was no pointer to an object being passed to the constructor or the SuperSecretSauce method. The way we solved this problem was to supply an extra parameter at the start to the function typedef for any method in that class. This would ensure that the pointer to our object is passed in the RCX register (note: there is only one calling convention on Windows 64bit):
typedef void (__cdecl* _ExampleConstruct)(char *); typedef void (__cdecl* _ExampleSecretSauce)(char *, char *);We then allocate a block of memory big enough to hold it (an exercise for the reader to work out how large it needs to be), memset and pass it to the constructor and other methods.
char vFakeObject[4096];
memset(vFakeObject,0x00,4096);
OurImportedConstructor(vFakeObject);
OurImportedSecretSauce(vFakeObject,"Hello Secret Sauce");
So we end up with code that looks like this:#include "stdafx.h"
#include <Windows.h>
#include <strsafe.h>
#include <Dbghelp.h>
typedef void (__cdecl* _ExampleConstruct)(char *);
typedef void (__cdecl* _ExampleSecretSauce)(char *, char *);
int _tmain(int argc, _TCHAR* argv[])
{
fprintf(stdout,"[i] Recx - C++ class DLL example\n");
HMODULE modDLL = LoadLibrary(L".\\DLLCPP.dll");
if (modDLL==NULL)
{
fprintf(stderr,"[!] Could not load .\\DLLCPP.dll");
return 1;
}
_ExampleConstruct OurImportedConstructor = (_ExampleConstruct)GetProcAddress(modDLL,"??0CDLLCPP@@QEAA@XZ");
if(OurImportedConstructor==NULL){
fprintf(stderr,"[!] Could not get address of the constructor");
return 1;
}
_ExampleSecretSauce OurImportedSecretSauce = (_ExampleSecretSauce)GetProcAddress(modDLL,"?SuperSecretSauce@CDLLCPP@@QEAAHPEAD@Z");
if(OurImportedSecretSauce==NULL){
fprintf(stderr,"[!] Could not get address of the secret sauce method");
return 1;
}
char vFakeObject[4096];
memset(vFakeObject,0x00,4096);
OurImportedConstructor(vFakeObject);
OurImportedSecretSauce(vFakeObject,"Hello Secret Sauce");
return 0;
}
And... drum role... voilĂ :[i] Recx - C++ class DLL example
[!] You sent the secret sauce Hello Secret Sauce
Hopefully this will save you an hour or two of reading plus trial and error. All of the code for the above examples can be downloaded from here. The projects included in the download archive are:
- DLLCPP - The C++ DLL.
- LoaderAttempt1 - the example where we don't call the constructor.
- LoaderAttempt2 - the example where we don't pass an object.
- Loader - the working example.
The project as-is will only work for the 64bit build as the undecorated function names will need changing for the 32bit version of the DLL.
I ran across a similar situation recently. Luckily I was on ARM where the "this" pointer is passed in exactly the same way as the first argument of a normal function; this simplified the work a little (otherwise I might have had to use some inline assembly to set things up, or alternately make a dummy class to mimic the one I was calling). My approach was simple -- 1) preallocate the space for "this", I just made a big buffer that I knew was oversized. 2) use dlopen/dlsym to get pointers to the methods that I wanted to call, 3) call the methods as if they were functions, passing the address of my "this" pointer as the first argument.
ReplyDeleteI actually skipped calling the constructor because I knew that the constructor didnt do anything important other than zeroing out the structure. Also I skipped building any method table because I knew that there were no virtuals called in the functions I cared about. All of these shortcuts made my work a lot easier, but none of them would be very difficult to overcome if I had to. The resulting program was only about 30 or 40 lines of code and didnt require any special linking (since I was dynamically loading the library).