x86 way of doing this is easy and straightforward - through GetExitCodeThread. Unfortunately it's limited to returning 32 bit values. As I understand it WinAPI provides no 64 bit alternative.
So the problem is - I have no trouble calling the injected function by finding its base address through CreateToolhelp32Snapshot module loop then running CreateRemoteThread using it, it does whatever I wrote in it as it should but how exactly do I retrieve its return value without GetExitCodeThread? As an example I want to retrieve a 64bit pointer or even a struct (or a 64bit pointer to one) as a result of this function. What would be a correct way of doing it? And if it's ReadProcessMemory - then which memory address/offset should I read for a return value?
Edit: additional info: I'm calling a function inside an injected DLL. The function executes some stuff (e.g. collection of data from the process it's injected into, which is successful) - the problem is I want to retrieve one of those variables back into the calling process (the one that calls CreateRemoteThread). GetExitCodeThread is a no go because variables are 64 bit.
code snippet for reference (hExportThread function returns uint64_t in DLL):
// LLAddr = LoadLibraryA address, lpBaseAddr = dll path related argument
HANDLE hInjectionThread = CreateRemoteThread(hProc, NULL, NULL, LLAddr, lpBaseAddr, NULL, &idThread);
WaitForSingleObject(hInjectionThread, INFINITE);
dllBaseAddr = getDLLBaseAddr(); // gets base address of the injected DLL
dllExportOffset = getDLLExportOffset(dllExportName.c_str()); // opens the DLL in the local buffer and gets the correct offset
LPTHREAD_START_ROUTINE lpNewThread = LPTHREAD_START_ROUTINE(dllBaseAddr + dllExportOffset); // gets the correct address of the function in the injected dll
HANDLE hExportThread = CreateRemoteThread(hProc, NULL, NULL, lpNewThread, NULL, NULL, 0); // executes injected function
WaitForSingleObject(hExportThread, INFINITE);
I suggest that you use a shared memory region, have it open in both the injecting process and the injected DLL. When the injected library finishes you know that the memory should be ready.
Doing this, you aren't limited to 4 or 8 bytes, you can make the region of whatever size is needed to return the collected data.