Trigger Screenshot in nwmain
#1
Posté 03 mai 2014 - 09:27
In my PW, I use a piece of Software that the players can optionally have running to sync the online resources with their local machines.
Eg: Optional content like music etc
I have created new functionality that allows it to detect when screenshots are taken in-game (on the server), and then post them to our facebook community page.
I am now working on some more functionality, that will allow the PW Server to request the Client to take a screenshot.
Eg: it sends a message over tcp, and the client when they receive it, will trigger a screenshot, then the screenshot is sent to the facebook page.
Uses for this would be along the lines of immortalizing boss defeats, or epic battles etc, or cutscene moments.
The problem I am having, is that the Windows API does provide methods for triggering/simulating the Print Screen key is being pressed, but they dont seem to be interacting well with nwmain.
Eg: Nothing happens - when the code to trigger a print screen fires, no screenshot is being taken by nwn.
I've also tried using the clipboard, but any image stored there is just a black screen.
OpenGL and DirectX Games do not make it easy to capture screenshots, because the graphics exist in a different memory portion than the OS.
I was wondering if anyone has any ideas or clues about how one might trigger NWN to actually take a screenshot?
It seems that the windows API refuses to let me simulate a user pressing the print screen key, so I need to take another avenue : maybe unsafe code, and actually fire a method inside the nwmain process?
Does anyone have a symbols file for nwmain?
I'd be interested to see if they have a method inside that handles the screenshots, and if it can be hooked externally?
#2
Posté 04 mai 2014 - 09:13
Call __thiscall 0x004269A0 (void * this, int, int, int)
with params: *(**(0x0092DC50)+0x4), 0x5E, 0x80, 0
- Rolo Kipp aime ceci
#3
Posté 04 mai 2014 - 09:37
Or just use nwnapi from NWNCX:
https://github.com/v.../include/nwnapi
Define a new function: void (*HandleInputEvent)(CClientExoAppInternal *, int, int, int) = 0x004269A0
And call it: HandleInputEvent(g_pAppManager->ClientExoApp->Internal, 0x5E, 0x80, 0);
See nwncx_connect and nwncx_serverlist sources to figure out how to link with nwnapi.
Alternatively, you can look at the disassembly at 0x0042765B and try to replicate that: set CClientExoAppInternal+0x170 to 1, write &char * with destination filename to CClientExoAppInternal+0x178. This will make a screenshot on the next cycle.
- Rolo Kipp aime ceci
#4
Posté 06 mai 2014 - 12:15
Thanks Virusman for posting the offset information, and method signature.
I am currently researching dll injection techniques that are viable for .net (c#)
I believe that I might be able to inject a dll into the nwmain process, and then have that dll (a c++ bootstrapper) execute a method contained in a c# dll, for triggering the method discussed above.
I've already got it working to the point where a dll can be injected into the process, however, I soon learned that I need to inject a c++ dll, which initiates the .net framework (CLR) within the NWN Process, and then have the bootstrapper make a further call to my 'actual' dll, which will contain the instructions to execute the delegate for the method pointer/address above.
Don't suppose you have any experience with c++ bootstrapping and .net?
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Bootstrap();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
which then calls
#include "stdafx.h"
#include <stdio.h>
#include "objbase.h"
#include "MSCorEE.h"
#import "C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.tlb" raw_interfaces_only
using namespace mscorlib;
void Bootstrap() {
OutputDebugString(L"MceFM Bootstrap Started");
CoInitializeEx(0, COINIT_MULTITHREADED );
ICorRuntimeHost* pICorRuntimeHost = 0;
HRESULT st = CoCreateInstance(CLSID_CorRuntimeHost, 0, CLSCTX_ALL,
IID_ICorRuntimeHost, (void**)&pICorRuntimeHost);
if(!pICorRuntimeHost) return; // Clean up and log errror ...
HDOMAINENUM hEnum = NULL;
pICorRuntimeHost->EnumDomains(&hEnum);
if(!hEnum) return; // Clean up and log errror ...
IUnknown* pUunk = 0;
st = pICorRuntimeHost->NextDomain(hEnum, &pUunk);
if(!pUunk) return; // Clean up and log errror ...
_AppDomain * pCurDomain = NULL;
st = pUunk->QueryInterface(__uuidof(_AppDomain), (VOID**)&pCurDomain);
if(!pCurDomain) return; // Clean up and log errror ...
_bstr_t assemblyName =
"Last, Version=1.1.0.0, Culture=neutral, PublicKeyToken=792d614cdf38e9ce";
_bstr_t typeName = "MceFM.Last.Inject.Injectee";
_ObjectHandle* pObjectHandle = 0;
pCurDomain->CreateInstance(assemblyName, typeName, &pObjectHandle);
}
I've never worked on making a c++ dll from scratch before, so the bootstrapper is my current challenge.
From what I understand, I should be able to get the .net DLL to execute a delegate for the memory location you posted above... if I can get the code to execute under the context of the nwmain process.
To do that, I need to inject into the process, so its the Process firing the method, and not my external application. (although it will be my external application that triggers it)
Am I right that when the dll is injected, DLLMain is executed automatically for the c++ dll, and then I need to use that to spawn off my .net dll, and then execute methods within it?
Even if I don't get it finished, its still interesting, cause I am learning more about Injection etc
#5
Posté 06 mai 2014 - 12:19
Alternatively, you can look at the disassembly at 0x0042765B and try to replicate that: set CClientExoAppInternal+0x170 to 1, write &char * with destination filename to CClientExoAppInternal+0x178. This will make a screenshot on the next cycle.
Does this mean a screenshout could be achieved by simple memory editing?
Eg: not needing to inject any dll?
Eg:
If I find the memory offset of CClientExoAppInternal, then add 0x170 : and set that to 1, while at the same time Writing a string (filepath) - converted to bytes to the 0x178 : this would trigger a screenshot?
#6
Posté 06 mai 2014 - 05:01
Yes. I'd write the string first and the flag later to avoid a race condition, though.
#7
Posté 06 mai 2014 - 06:52
Is it 0042765B or 0x004269A0 ?
#8
Posté 06 mai 2014 - 07:25
So far not having much luck.
Tried going to
0042765B + 178 - wrote c:\mypic.tga as well as mypic.tga and mypic - in the hopes that one of them would work.
then I did 0042765B + 170 : and wrote a 01 value to that location.
The 01 didnt seem to vanish or anything, and no pictures appeared at either the screenshot directory, or my c:\
When I went in-game, and proceeded to press print screen, it did then crash my game.... :-(
#9
Posté 07 mai 2014 - 08:31
You won't get far without static or dynamic analysis - use IDA to get addresses and/or a debugger (IDA debugger is very good for that) to examine memory layout at runtime.
0x0042765B is in the code segment, it's a part of the function.
0x0092DC50 is the global variable you need, but again, read NWNCX sources and use IDA debugger: 99% of the time these things don't work at the first attempt, and without a debugger you won't know what's happening.
#10
Posté 07 mai 2014 - 09:00
Eg: like nwserver?
I will load it up in IDA now, and have a nosy.
#11
Posté 07 mai 2014 - 09:57
jumptable 00426ADC case 94
Logic would suggest that I need to trigger this case statement?
Either by directly firing it, or by changing the value of the variable being used in the switch statement, to point to case 94.
.text:00426AD4 ; ---------------------------------------------------------------------------
.text:00426AD4
.text:00426AD4 loc_426AD4: ; CODE XREF: sub_4269A0+A6j
.text:00426AD4 ; sub_4269A0+DFj
.text:00426AD4 xor ecx, ecx
.text:00426AD6 mov cl, ds:byte_428810[ebx]
.text:00426ADC
.text:00426ADC loc_426ADC: ; CODE XREF: sub_4269A0+12Dj
.text:00426ADC jmp ds:off_42870C[ecx*4
This is the switch statement.
I have no idea how to interpret this...
Is ecx the value that is being switched on?
If so: it has a value of 30 in a screenshot instance
#12
Posté 13 mai 2014 - 11:41
Any assistance would be appreciated.
I am injecting my own custom dll into the nwmain.exe process, (a c# bootstrapper dll, wrote in c++)
The c# DLL contents are as follows:
namespace Inject
{
public class MainClass
{
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
public delegate void Screenshot(uint uThis, int u1, int u2, int u3);
// 00426ADC
public static int DLLMain(string arg)
{
MessageBox.Show("Start");
try
{
Screenshot screenshot = WhiteMagic.Magic.Instance.RegisterDelegate<Screenshot>(0x004269A0);
screenshot(0x0092DC50 + 4, 0x5E, 0x80, 0);
////MessageBox.Show("Loaded DLL");
}
catch (Exception ee)
{
MessageBox.Show(ee.ToString());
}
return 1;
}
}
}
I am using a somewhat open source library called WhiteMagic which handles execution of methods from inside other programs - I believe people wrote it for the purpose of making bots and such for games like World of Warcraft.
I have ran nwmain with IDA attached, and I can confirm that this does fire the branch of the switch statement that should be making a screenshot, but for some reason, nothings appearing.
With regards to this line
screenshot(0x0092DC50 + 4, 0x5E, 0x80, 0);It being a thiscall convention, am I right in saying that 0x0092DC50 is the location of CAppManager, and the Internal is +4 into it?
So the this call should be right? right?
I think the reason its not writing the screenshot is because its missing a filename: stepping through in IDA revealed that it didnt have a filename to append onto the screenshots directory.
I am going to try writing to Internal+0x178 to see if I can give a fake name.
I will report back with my success or failure story.
#13
Posté 14 mai 2014 - 12:28
Even writing the filename to memory as an array of bytes doesnt get it to save the screenshot.
Its like it is saying the file path is invalid or something, which then causes it to fail to do fopen?
#14
Posté 14 mai 2014 - 07:32
No, 0x0092DC50 is the location of a pointer to AppManager, AppManager+0 is a pointer to CClientExoApp, and offset +0x4 in ClientExoApp is a pointer to CClientExoAppInternal, and that is the pointer you need to pass as "this" (ecx, not stack) into the function, according to thiscall calling convention.
I think it'd be easier to do this low-level stuff in C and call that C method from C#. Easier to debug C code from IDA, too.
#15
Posté 14 mai 2014 - 08:20
I get it now
I didn't realize that +0 was a pointer, I thought it was an offset.
I had it in my head that the AppManager +0 was the start of the ExoApp, and then +4 from that was the Internal.
Now that I know its a pointer, I can read the pointer address, and feed that in instead.
I will give it a go now: i think I was calling the right method, with the right arguments with exception to the 'this'.
#16
Posté 14 mai 2014 - 09:36
Got it working
finished code is as follows
Screenshot screenshot = WhiteMagic.Magic.Instance.RegisterDelegate<Screenshot>(0x004269A0);
RhunDownloader.Memory m = new RhunDownloader.Memory("nwmain");
uint uPointVal = (uint)m.ReadPointer(0x0092DC50);
uPointVal = (uint)m.ReadPointer(uPointVal);
uPointVal += 0x4;
uPointVal = (uint)m.ReadPointer(uPointVal);
screenshot(uPointVal, 0x5E, 0x80, 0);
m.Dispose();
- virusman aime ceci
#17
Posté 15 mai 2014 - 12:23
I managed to get my PW Server to now actually trigger Screenshots inside my players Neverwinter Client.
These are then uploaded to our PW Community Facebook page.
The architecture is like this.
Our PW Server has ability to invoke C# Code through nwnx_dotnet
It attempts to make a TCP Connection to the IP Address of a player (if they have upnp routers, or have port forwarding, it should work) Its connecting to a program that my players use to sync the music and resources for the server.
When it connects, it sends a message requesting a screenshot.
Before this however, when nwmain was first started: a c++ bootstrapper DLL was injected from our downloader app, which starts the .net framework within the nwmain process.
This then starts the actual DLL we are using 'RhunDLL.dll'
This is the one that contains the Screenshot functionality.
When a network request comes from our server, and if it manages to get to our Downloader app (which does the injection) - it then triggers the screenshot, and any screenshots for our Server is automatically transferred to our central server, where they are then uploaded to facebook (to a specific album)
I've given it a test run, and now I am just waiting for my players to beta test it.
- Fester Pot aime ceci
#18
Posté 17 mai 2014 - 01:42
Did you ever have compatibility issues with DLL Injection across windows xp and more recent OS's?
I have one player who is using Windows XP, but my Bootstrapper DLL crashes his Game Client.
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReeserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&StartDotNet, 0, 0, NULL);
break;
case DLL_THREAD_ATTACH:
// CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&StartDotNet, 0, 0, NULL);
break;
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
test = "loaded";
return TRUE;
}
void StartDotNet()
{
HRESULT hr;
ICLRRuntimeHost *pClrHost = NULL;
ICLRMetaHost *pMetaHost = NULL;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
//MessageBox(NULL, L"CLRCreateInstance Done.", NULL, NULL);
ICLRRuntimeInfo * lpRuntimeInfo = NULL;
hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&lpRuntimeInfo);
//MessageBox(NULL, L"pMetaHost->GetRuntime Done.", NULL, NULL);
ICLRRuntimeHost * lpRuntimeHost = NULL;
hr = lpRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID *)&lpRuntimeHost);
//MessageBox(NULL, L"lpRuntimeInfo->GetInterface Done.", NULL, NULL);
hr = lpRuntimeHost->Start();
//MessageBox(NULL, L"lpRuntimeHost->Start() Done.", NULL, NULL);
DWORD dwRet = 0;
hr = lpRuntimeHost->ExecuteInDefaultAppDomain(
L"RhunDLL.dll",
L"Inject.MainClass", L"DLLMain", L"Injection Worked", &dwRet);
lpRuntimeHost->Release();
}
He has .Net4 installed, and Visual C++ 2010 RedistI installed a VirtualBox vm with xp, and confirmed the crash.
CreateThread seems to be working for Windows 7, should I be using CreateRemoteThread instead? or Is there something specific to be done for Windows XP compatibility?
Just double checked
Using RemoteThread in the .net code that actually injects the Bootstrapper.
bool bInject(uint pToBeInjected, string sDllPath)
{
IntPtr hndProc = OpenProcess((0x2 | 0x8 | 0x10 | 0x20 | 0x400), 1, pToBeInjected);
if (hndProc == INTPTR_ZERO)
{
return false;
}
IntPtr lpLLAddress = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (lpLLAddress == INTPTR_ZERO)
{
return false;
}
IntPtr lpAddress = VirtualAllocEx(hndProc, (IntPtr)null, (IntPtr)sDllPath.Length, (0x1000 | 0x2000), 0X40);
if (lpAddress == INTPTR_ZERO)
{
return false;
}
byte[] bytes = Encoding.ASCII.GetBytes(sDllPath);
if (WriteProcessMemory(hndProc, lpAddress, bytes, (uint)bytes.Length, 0) == 0)
{
return false;
}
if (CreateRemoteThread(hndProc, (IntPtr)null, INTPTR_ZERO, lpLLAddress, lpAddress, 0, (IntPtr)null) == INTPTR_ZERO)
{
return false;
}
CloseHandle(hndProc);
return true;
}
#19
Posté 18 mai 2014 - 09:28
Where does it crash? Can you pinpoint the exact place? Is it .net part or the memory modification?
#20
Posté 18 mai 2014 - 09:45
The techical information from the error report was reporting that the bootstrapper dll was loaded correctly.
But there was no RhunDLL.dll present in the module list - suggesting that the .Net dll was not correctly loaded.
In anycase- the error seems to have resolved itself, so it might not need thinking about.





Retour en haut






