leecher1337 / ntvdmx64

Run Microsoft Windows NTVDM (DOS) on 64bit Editions
823 stars 81 forks source link

Extreme Paintbrawl crash and DxWnd APC injection #182

Open BEENNath58 opened 2 years ago

BEENNath58 commented 2 years ago

This game is hilarious, from a gameplay POV. On the technical side of things it is a mixture of Windows and DOS. On Windows 11 it crashes too just like XP. Can you look into getting this game run? On XP this appears: extremepaintbrawlcrash

I am not sure if that's the situation with NTVDM too, but it'll be cool if you can the game to work. Since the game scaled to my entire desktop resolution, I actually had to use DxWnd to run the game. I posted my .dxw profile too pblaunch.zip (You may want too add the 640x400 resolution to your driver, or use the Run in Window setting of DxWnd)

leecher1337 commented 2 years ago

Not really sure, how that one works. Tried your dxwnd profile, then said "Single Player" and then it shows an overview screen, where I can i.e. click "Practice" or "Exit game", but whatever I try, it doesn't react to any click.

So I tried starting game.exe in engine subdirectory, with NTVDMx64 it starts up normally, but of course the performance is inferior, completely unusable (not really surprising, as NTVDMx64 is not really useful for games due to the performance of the emulated CCPU and with VT-x accelleration on the other hand, you would have a good CPU performance but suffer from the slow video performance, which is technically impossible to speed up due to the VGA architecture). image

Anyway, to me - at least in my version - it looks like this is just some DOS game that has a Windows GUI for setup, so why not directly run it in DosBox? Or do I have the wrong version? If so, can you provide me with a download link to the proper version?

My guess is that this should be some hybrid with a Windows graphics where just the game engine is on DOS, like PowerChess? If so, how do I get this damn thing working?

BEENNath58 commented 2 years ago

The game is based on the Build engine, popular for Duke Nukem 3D. Thus it's engine is clearly DOS, but it's initial setup of buying and customizing things is in Win32, so that's a problem for dosbox.

Regarding clicks, did you get a white screen? If so you might want to run it in a window. The game should run in a very low resolution, so if the game for some reason decides to render at a very high resolution, a problem like this should occur.

Since the game works here from the Win32 "setup", I can buy weapons easily, access other options but when I am about to launch a mission, it just crashes without any type of error!

And I got the game from old games ru. Probably you have the same version, it has an InstallShield that displays a "headgames" video at the start.

If I try to run "game.exe" I get "Cannot execute D:......\ENGINE\game.exe"

so why not directly run it in DosBox?

I tried running game.exe from there, all I got was a revolving "Extreme Paintbrawl" logo.

Below, you can see how the game looks on my PC, the Win32 part: paintbrawlingame

leecher1337 commented 2 years ago

I got this version https://www.myabandonware.com/game/extreme-paintbrawl-cl3 and game.exe also runs fine in DOSBox.

My Win32 part looks the same (at least with your DxWnd profile), when I go over the various elements of the picture with the mouse, I also get the correct descriptive text, but when I click on one of these items, nothing happens.

leecher1337 commented 2 years ago

https://www.old-games.ru/game/download/5644.html This RIP also works fine with dosbox

leecher1337 commented 2 years ago

Ok, I managed to find a point where I can click to "Practice" on this Overview map, it's a tiny spot but I found it. What it basically does is to modify the config files and the run the game via GO.BAT Good thing is that you can rewrite Engine\GO.BAT and i.e. let the gam run in DosBox. One annoying bug is a use-after-free bug. It unloads some DLLs but leaves pointers to functions in memory leading to a crash upon launching the target executable, see here:

image

image

This is upon handling the Window-Procedure request WM_COMMAND for pushing the "Play" button:

image

leecher1337 commented 2 years ago

FreeLibrary(hDirectDraw):

image

But DirectDraw still on callstack, so crashes on return to nonexisting address:

image

leecher1337 commented 2 years ago

Even tough this seems to be a bug in Extreme Paintbrawl launcher, it was enough for me to edit go.bat and lat it launch via dosbox:

ECHO OFF
"c:\Program Files (x86)\DOSBox-0.74-3\dosbox.exe" -c "mount C '%CD%'" -C C: -c game.exe -c exit
CD ..
PAINTBRAWL
leecher1337 commented 2 years ago

Forgot that I circumvented dxwnd when testing this, so there also is a bug in dxwnd preventing you to run GO.BAT:

dll\createproc.cpp:

    res=(*pCreateProcessA)(
        lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, false, 
        dwCreationFlags|CREATE_SUSPENDED, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
    if (!res){
        OutTraceE("%s(CREATE_SUSPENDED) ERROR: err=%d\n", ApiRef, GetLastError());
        res=(*pCreateProcessA)(NULL, lpCommandLine, 0, 0, false, dwCreationFlags, NULL, lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
        if(!res){ 
            OutTraceE("%s ERROR: err=%d\n", ApiRef, GetLastError());
        }
        return res;
    }

    while(TRUE){ // fake loop
        bKillProcess = TRUE;

        // locate the entry point
        TargetHandle = OpenProcess(
            PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_SUSPEND_RESUME, 
            FALSE, 
            lpProcessInformation->dwProcessId);

        fExe = fopen(lpExePath, "rb");
        if(fExe == NULL){
            OutTraceE("%s: ERROR fopen(\"%s\") err=%d\n", ApiRef, lpExePath, GetLastError());
            break;
        }

        bKillProcess = !InjectSon(ApiRef, fExe, lpProcessInformation, TargetHandle);
        break; // exit fake loop
    }

    // cleanup ....
    if(fExe) fclose(fExe);
    if(TargetHandle) CloseHandle(TargetHandle);
    // terminate the newly spawned process
    if(bKillProcess){
        OutTraceDW("%s: Kill son process hproc=%#x pid=%#x\n", ApiRef, lpProcessInformation->hProcess, lpProcessInformation->dwProcessId);
        if(!TerminateProcess( lpProcessInformation->hProcess, -1 )){
            OutTraceE("%s: failed to kill hproc=%#x err=%d\n", ApiRef, lpProcessInformation->hProcess, GetLastError());
        }
    }

When launching GO.BAT, this is a .BAT file, thus it launches cmd.exe /C GO.BAT cmd.exe is a console process. However, it opens the lpExePath with fopen, so it opens the BAT file, but in InjectSon, it tries to parse the .bat file as .exe, which of course fails and thus, it kills the process instead of leaving it alone. Why it reads the exe from disk and not in memory is onknown to me, this doesn't make sense. I don't know why it just kills the target process when it's unable to inject, this is plain stupid behaviour to me.

So in order to run it properly, you need 3 things: 1) Fix dxwnd to launch GO.BAT 2) Modify GO.BAT like mentioned above to run it in DosBox 3) Maybe optionally patch the use-after-free bug in PaintBrawl.exe

I can take a look at 3), if necessary, but first dxwnd author needs to fix his bug.

leecher1337 commented 2 years ago

I changed above line

    if(bKillProcess){

to:

if(bKillProcess && ResumeThread(lpProcessInformation->hThread)==(DWORD)-1) {

and recompiled dxwnd.dll (which was a real pain). Find attached an updated dxwnd.dll: dxwnd.zip

So:

  1. Copy updated dxwnd.dll to dxwnd directory
  2. Modify GO.BAT as described above
  3. Profit

Hope that helps

BEENNath58 commented 2 years ago

Ok I got some time and tested it. I tested the change in DxWnd and now it no longer crashes directly. But still executing go.bat through NTVDMx64 doesn't run anything. As soon as I execute it, it goes back to the game GUI with errors (that apparently has to be fixed by DxWnd, the game loses the hooked task). But NTVDMx64 never launches the game here.

So I took the DOSBox route, in fact I tried both DOSBox as well as DOSBox-X but all I get it: paintbrawl rotating

I tried this game in PCem Win98 and the rotating icon stops spinning to launch the game.

BEENNath58 commented 2 years ago

A weird situation here. While testing this game I had PCem on. The strange thing was DOSBox ran the game some times, but when it ran, PCem Win98 received a BSOD. It is like if there's no BSOD on PCem Win98, the mission doesn't launch on DOSBox

ghotik commented 2 years ago

Hi, thank for your help. The reason for killing the son process is that the procedure is supposed to inject code in the son process, so it first overwrites a few bytes of the suspended process with an infinite loop, then injects the hooking code and finally resumes the bytes at the start address to let the program go. If anything fails in this operation, you would have a process looping forever and overheating your CPU, so it is wise to kill it after the timeout. The procedure is intended for Win32 executable only, you should be able to run a .BAT file by picking a DxWnd different son hooking mode (the default one) but of course this way you won't have the automatic "extended hook" mode. Please, let me know if my reasoning makes sense, I'll be happy to apply the fix to improve DxWnd if I was wrong.

leecher1337 commented 2 years ago

Hi, thank for your help. The reason for killing the son process is that the procedure is supposed to inject code in the son process, so it first overwrites a few bytes of the suspended process with an infinite loop, then injects the hooking code and finally resumes the bytes at the start address to let the program go. If anything fails in this operation, you would have a process looping forever and overheating your CPU, so it is wise to kill it after the timeout.

So you can either return a different return value for non-PE executables (your PE-header parser will notice it) then and let these applications resume instead of killing it, then it should work with your injector too (as the checked PE-header is not used anywhere, I guess the intention of it is to just check if it's a PE file). Your injector may even work fine, because a .bat is just a cmd.exe /c process, so it's an .exe, but you have to take HMODULE load addres for checking then and not open the file on disk (That's what I found odd, you have it mapped in memory already including PE-Header and everything, but you do a seperate read and check if ondisk. Is there a reason for this?)

You can also use APC injection, as NtTestAlert() should fire just in time. Btw., if you are interested in different injection methods, you can have a look at my [injector32.c] (https://github.com/leecher1337/ntvdmx64/blob/master/ntvdmpatch/src/ldntvdm/ldntvdm/injector32.c) so that your loader can offer even more hooking methods, just in case.

The procedure is intended for Win32 executable only, you should be able to run a .BAT file by picking a DxWnd different son hooking mode (the default one) but of course this way you won't have the automatic "extended hook" mode. Please, let me know if my reasoning makes sense, I'll be happy to apply the fix to improve DxWnd if I was wrong.

I tried different combinations before writing (and trying to compile the source myself), because I thought, maybe this is enough, but I didn't get the nworking, most of them just crashed the original .exe. Btw.: How is the compilation supposed to work? I opened the dll project in VS2019 and got thousands of errors on compiling, because there are some MinGW headers in the "include" directory. I had to remove libloaderapi.h and apiset.h to be able to compile it and even then, I had to do some fixes (which may be due to using VS2019 instead of VS2008). There also was a declaration with C++ auto somewhere, which the VS2019 C++ compiler didn't understand.

ghotik commented 2 years ago

Thank you for the reply. It's quite a lot of stuff to make my brains boiling, so I'm replying now just to some easy questions, though I assure you that I'll consider all your suggestions very carefully. DxWnd is easily compiled with VS2008. This was not really a choice, the original Japanese code was compiled this way and, during so many years, I never found a very good reason for changing compiler, so I presume that I could say the culprit was my laziness. In addition, DxWnd is targeted to old Win32 programs, and in some cases it was possible to find leaked include files that were better fitting to the old tool rather than the modern ones. Finally, I found quite annoying some Microsoft claim to release free versions of Visual Studio just to discover after some time that I needed to register someplace or do other unpleasant stuff. I like my freedom. In any case, it's something I could try to change in near future. I will appreciate comments and suggestions about the best, free available tool.

You said that your compilations crashed the original exe. Did you mean the DxWnd.exe GUI? That sounds reasonable: DxWnd.exe and dxwnd.dll use shared memory sections with C structures inside, so it is necessary to make sure they both compile the struct objects with the same spacing. Probably some #pragma pack directive here and there could fix the problem, as well as using the same compiler.

I will surely read and take advantage of your injector32.c source. I was happy enough with mine, but I must admit that in some cases the process shows some flaw, like failures on first attempt or unreliable results. Usually with a little patience and some retry the problems are bypassed, but I agree there could be something that doesn't work properly in there.

Well, that's even too much for now. I hope to hear you again with more intriguing questions. And congrats for your wonderful work!

leecher1337 commented 2 years ago

Thank you for the reply. It's quite a lot of stuff to make my brains boiling, so I'm replying now just to some easy questions, though I assure you that I'll consider all your suggestions very carefully. DxWnd is easily compiled with VS2008. This was not really a choice, the original Japanese code was compiled this way and, during so many years, I

Hehe, I still prefer Visual C 6, that's my IDE, it has a small memory footprint and links everything to msvcrt.dll, so no additional runtime includes, so I fully understand your Compiler choice. I was just in doubt that it would compile with VS 2008, because of these 2 strange header files I mentioned. They include i.e. an inexistant _mingw.h, so I thought that this also cannot work with VS 2008, i.e. because of conflicts with Windows original headers. When I removed these 2 header files, then the other errors can be attributed to the VS version. But what I found funny is that a newer compiler doesn't know the "auto" keyword, but an older one would be OK with it, so that got me even more confused. As a matter of fact, I have a few different VS Versions installed, but VS 2008 is the one I'm missing. Bad luck for me, I guess ;-)

You said that your compilations crashed the original exe. Did you mean the DxWnd.exe GUI? That sounds reasonable: DxWnd.exe and dxwnd.dll use shared memory sections with C structures inside, so it is necessary to make sure they both compile the struct objects with the same spacing. Probably some #pragma pack directive here and there could fix the problem, as well as using the same compiler.

Oh, no, that's a misunderstanding. Before I even tried to recompile the source, I first checked if I cannot fix the problem with the .bat file by choosing different hooking methods in the "advanced" GUI, but a lot of combinations mostly crashed the target executable. That's why I gave up on that and finally patched dxwnd. Maybe I was missing a working combination. You may want to try it out yourself with the "RIP" version from oldgames.ru, it is only a few MB in size.

I will surely read and take advantage of your injector32.c source. I was happy enough with mine, but I must admit that in some cases the process shows some flaw, like failures on first attempt or unreliable results. Usually with a little patience and some retry the problems are bypassed, but I agree there could be something that doesn't work properly in there.

Some of the methods in injector32.c are a bit unreliable too depending on the target, but as you have this nice selection dialog, the user can try out different methods. As I went through and fixed some common problems when writing them, you may profit from it.

Well, that's even too much for now. I hope to hear you again with more intriguing questions. And congrats for your wonderful work!

Thank you :)

leecher1337 commented 2 years ago

Ok I got some time and tested it. I tested the change in DxWnd and now it no longer crashes directly. But still executing go.bat through NTVDMx64 doesn't run anything. As soon as I execute it, it goes back to the game GUI with errors (that apparently has to be fixed by DxWnd, the game loses the hooked task). But NTVDMx64 never launches the game here.

Forget about NTVDMx64 for that game, performance is inferior, so it is completely unusable.

So I took the DOSBox route, in fact I tried both DOSBox as well as DOSBox-X but all I get it:

Which version of DOSbox? With latest DOSBox 0.74-3 directly from the homepage, I don't have any problems so far. Here is my PaintB.cfg in case it is a problem with your settings: PaintB.cfg

BEENNath58 commented 2 years ago

Ok I got some time and tested it. I tested the change in DxWnd and now it no longer crashes directly. But still executing go.bat through NTVDMx64 doesn't run anything. As soon as I execute it, it goes back to the game GUI with errors (that apparently has to be fixed by DxWnd, the game loses the hooked task). But NTVDMx64 never launches the game here.

Forget about NTVDMx64 for that game, performance is inferior, so it is completely unusable.

So I took the DOSBox route, in fact I tried both DOSBox as well as DOSBox-X but all I get it:

Which version of DOSbox? With latest DOSBox 0.74-3 directly from the homepage, I don't have any problems so far. Here is my PaintB.cfg in case it is a problem with your settings: PaintB.cfg

I tried your configuration as well and it doesn't seem to help. It's still a infinite rotating "Extreme Paintbrwal" logo and I no longer to achieve what I describe here.

Can you describe what kind of problem I am facing with launching with NTVDM because as I said it can't execute the file (according to cmd output).

leecher1337 commented 2 years ago

Check DebugView output, ensure that 32bit loader is injected properly. No problems with starting it in NTVDMx64 via GO.BAT here. You are running it on Win11 as host OS?

BEENNath58 commented 2 years ago

Check DebugView output, ensure that 32bit loader is injected properly. No problems with starting it in NTVDMx64 via GO.BAT here. You are running it on Win11 as host OS?

Yes I am using Windows 11. And here's the log: extremepaintbrwal.LOG

leecher1337 commented 2 years ago

Looks like NTVDM is starting up normally as it should:

00000016    0.07511840  [5716] LDNTVDM is running inside ntvdm.exe  

So you don't see any window with the game in it?

BEENNath58 commented 2 years ago

Looks like NTVDM is starting up normally as it should:

00000016  0.07511840  [5716] LDNTVDM is running inside ntvdm.exe  

So you don't see any window with the game in it?

No as i double click on game exe it starts and crashes

leecher1337 commented 2 years ago

Your game.exe is 993.885 bytes dated 02.11.1998 ? paintb.grp is 20.704.495 bytes dated 02.11.1998 ?

As your game.exe also crashes in DosBox and other emulators, I suspect that you may have a damaged version? Here is works fine in DosBox, ntvdmx64 (altough unusable due to slowness) and other emulators without any issues.

BEENNath58 commented 2 years ago

Your game.exe is 993.885 bytes dated 02.11.1998 ? paintb.grp is 20.704.495 bytes dated 02.11.1998 ?

Size: 970 KB (9,93,855 bytes) ‎Modified: 02 ‎November ‎1998, ‏‎12:11:22

As your game.exe also crashes in DosBox and other emulators, I suspect that you may have a damaged version? Here is works fine in DosBox, ntvdmx64 (altough unusable due to slowness) and other emulators without any issues.

The same game, from same media installs and run on Windows 98SE

leecher1337 commented 2 years ago

I tried that RIP from oldgames.ru which indeed does not work, but only leaves you with a black screen. As said, I used this ISO and installed it: https://www.myabandonware.com/game/extreme-paintbrawl-cl3 and it works fine. File size of your version seems to match my version (I think I installed Extreme_Paintbrawl_11_patch.rar , but it shouldn't matter), but maybe something different is damaged? So did you use the ISO from myabandonware? Tested on Win 7 x64 and Windows 11 x64, no issues.

image

But for god's sake, don't run this with NTVDMx64, it's not designed to run any games that use graphics. Better check with DosBox authors why your version crashes within DosBox.

BEENNath58 commented 2 years ago

As said, I used this ISO and installed it: https://www.myabandonware.com/game/extreme-paintbrawl-cl3 and it works fine. File size of your version seems to match my version (I think I installed Extreme_Paintbrawl_11_patch.rar , but it shouldn't matter), but maybe something different is damaged? So did you use the ISO from myabandonware? Tested on Win 7 x64 and Windows 11 x64, no issues.

The ISO from myabandonware and oldgames ru are identical.

But for god's sake, don't run this with NTVDMx64, it's not designed to run any games that use graphics. Better check with DosBox authors why your version crashes within DosBox.

Since DOSBox X didn't help then I came here and while your DOSBox worked, I gave it a go and it didn't work here. Looks like there isn't anything else to try, is it? The most frustrating thing is it doesn't work on Windows 7/10 VM either.

leecher1337 commented 2 years ago

Did you try DOS32A DOS etender as a DOS4GW replacement? http://www.r-t-c-m.com/knowledge-base/downloads-rtcm/general-tools-dosxp/ At least for native Windows XP, that worked.

BEENNath58 commented 2 years ago

Did you try DOS32A DOS etender as a DOS4GW replacement? http://www.r-t-c-m.com/knowledge-base/downloads-rtcm/general-tools-dosxp/ At least for native Windows XP, that worked.

There's a lot of complications. Even with this the game doesn't work on Windows XP. BUT...

Your NTVDM started working, only after I removed (x86) from the path. It seems NTVDM doesn't like special characters on my PC. Can you check this?

Regarding DOSBox IDK what's wrong, it's something else I'll have to figure out!

leecher1337 commented 2 years ago

I can run game.exe with NTVDMx64 from "c:\Program Files (x86)\HeadGames\PaintBrawl\engine" without any issues.

BEENNath58 commented 2 years ago

I can run game.exe with NTVDMx64 from "c:\Program Files (x86)\HeadGames\PaintBrawl\engine" without any issues.

I now installed it on "D:\adfhuiahednf\dsafhjmnok e8qu9rhna[\aaaaaaaaaaaaaaaaaaaaaaaaaaaaa(x86)\PaintBrawl" and it runs without issues. Program Files (x86) and Program Files are definitely the culprit! I can't understand why.

leecher1337 commented 2 years ago

Maybe it has to do with your file system, I don't know. Not reproducable here.

You can enable YODA, start vdmdebug, set registry key so that NTVDM breaks on startup into YODA, start the applicationn, attach a debugger (x32dbg) to ntvdm.exe, grab ntvdm.pdb symbols, continue execution in yoda and step through ntvdm.exe to see where it quits.

ghotik commented 2 years ago

Hi, leecher1337 I was intrigued by your suggestions and started to put them in practice, but I fear I may need some help. As you wrote, I started trying to implement a Inject schema of APC type that seemed quite promising. Instead of using it in the target client/son relationship, I started directly using APC injection between DxWnd.exe and the direct target. If I understood correctly, this goes through several steps (here summarized): 1) DxWnd creates a process in a suspended state with CreateProcess(... CREATE_SUSPENDED, ..., &pinfo); 2) then gets the injected dll full path this way: GetFullPathNameW(L"dxwnd.dll", MAX_PATH, dllpath, NULL); 3) then calls that injects injectLdrLoadDLL(pinfo.hProcess, pinfo.hThread, dllpath); 4) in turn, injectLdrLoadDLL calls inject_via_apcthread that should do the trick 5) finally, on successful completion, DxWnd should resume the process with ResumeThread(pinfo.hThread);

The procedure is finished, it compiles and runs with no errors, but it doesn't seem able to perform an early hook how it is supposed to. Please, consider that dxwnd.dll performs a double hook activity: at program startup if run with code injection and later, at the first window creation, through a SetWindowsHook callback, so it's not so easy to understand whether the procedures were run through injection. It takes some target that needs injection to tell the difference, as a game that checks for the OS version before creating its first window.

All this said, I have a few doubts: a) DxWnd work start by the invocation of the DLLMain() entry, which should happen automatically when loading the dll. The APC injection seems to require (if I understood correctly) to define the address of the procedure to be called, but I didn't find trace of that in the code. Is APC injection starting DLLMain too? b) did I create the process with the correct capabilities for APC injection? (see my attached code) c) Somewhere in the web I read that the injected thread must be put in a alertable suspended state to allow APC to run the injected thread first and this is easily made with a Sleep() call. I saw no Sleep calls in your code (at least, in the tiny part that I grabbed) and I didn't put a Sleep in mine. Maybe is this the flaw in my code? d) more in general, how comes that I got no errors whatsoever (the return codes of your calls were always TRUE) and nothing happens?

Please, forgive my laziness. Surely if I downloaded and studied all your project I would (more later than sooner) get some answer, but it's really quite a lot of coding! Here in attach the procedure that I am now using instead of the DxWnd InjectSuspended for testing (should I upload also the source code of InjectSuspended for a comparison?). It can't be compiled alone, but I think it should clear enough to follow the logic by reading the code. I hope you'll be so kind to have a look at it and help me finding what's possibly wrong.

InjectAPC.zip

ghotik commented 2 years ago

A late addition: browsing the web I found an interesting set of articles about code injection in cocomelonc.github.io . For instance, the code injection used currently in DxWnd maps this example: [Classic DLL injection into the process](https://cocomelonc.github.io/tutorial/2021/09/20/malware-injection-2.html). Is the suggested APC injection method described in some of the related pages, like (perhaps) [APC injection via NtTestAlert](https://cocomelonc.github.io/tutorial/2021/11/20/malware-injection-4.html) ? There seems to be so many different ways, I could get lost ...

leecher1337 commented 2 years ago

Hi ghotik,

I tested your code and here it works fine. Fixed some obvious errors that prevented proper testing: MessageBoxEx -> MessageBoxExA CreateProcess -> CreateProcessA Added CloseHandle() for the event

Then I wrote test code to launch notepad.exe and inject dxwnd.dll into it:

int main(int argc, char** argv)
{
    InjectAPC("C:\\WINDOWS\\SYSTEM32\\notepad.exe", NULL, TRUE, TRUE);
}

Then verified with a debugger: image

LdrLoadDLL success: image

DLL loaded: image

The trick why it works is that on starting a task, Windows does NtTestAlert() to start the main thread of the target application, as this is of course also just an APC. As your APC for injection is also queued, it gets executed as soon as you resume the main thread.

As for your questions: a) You pass an address (in the target thread, of course) of a routine that gets executed on APC. That's the Shellcode we inject (apc_stub_x86). The APC stub then loads the read loading routine (APCProcx86) which does the LdrLoadDLL() of your call. OrigEIP parameter is not used in our case, as you said, loading the DLL is enough to execute DllMain(). b) Yes, it works fine here c) True, but as you suspended the process, NtTestAlert() will get executed as soon as you resume the main thread executing your APC. d) Here it works, DLL is injected in target process with test program.

Maybe you also want to execute it as standalone like I did in my tests and check if it also works for you. You can do the same as me with the debugger in order to check possible problems.

ghotik commented 2 years ago

You are right, the code works! I got another prove by disabling the SetWindowsHookEx hook in DxWnd so that the APC injection works alone, and it works. So now, hoping not to abuse of your patience, I'll explain what is my trouble. DxWnd should intercept system calls and inject some wrapper code to ensure compatibility, windowing, time stretching etc., pretty much like Microsoft shims do. But unlike shims, in some case timing is critical and DxWnd hooks partially fail. For instance, a game may start and check the OS release as first thing, just to tell that the OS is not supported and terminate. Another nasty case is when the game first commands a video screen resolution change to set 8bit color depth. So, it is vital for DxWnd to hook some system calls before they gets executed, and it seems that most injection schemas may run in parallel with the game main thread, so that the hook may work or not depending on timing. With this goal in mind, the current "Classic DLL injection" used in DxWnd works apparently a little better that the "APC Injection" because I tested the two modes with a game that calls GetVersionExA to detect the OS and the APC injection failed in most cases. I say "in most cases" because repeating the same test more times, sometimes the APC hook was fast enough to intercept the call, but this happened only a few times. I suppose that this means that the injected dll and the game were running in parallel and the fastest won! So, I will add APC injection to the DxWnd set of hook modes, you never know that it may have better performances in specific cases, but it doesn't seem to fix my fundamental concern. I whish to ask you if in your opinion there is some stronger method (I see you implemented many) that could ensure that ALL the injection code is executed BEFORE the program threads are freed and let to run. I add that there is a very hard case that so far had no solutions in DxWnd, that is when the system call to be intercepted is within the DLLMain section of some linked library. In this case, the code is executed before that the hooking dll is run, so there seems to be no hope, but I was intrigued by the source code of some leaked Microsoft shims that showed the possibility to inject some code in the DLLMain section before it was executed. So, to summarize, would you suggest some stronger hook method to fully execute the injected dll before that the target program starts its execution?

p.s. I would have many more questions for you. Should I open a dedicated thread for this?

leecher1337 commented 2 years ago

I think this thread is fine, it has something to do with DxWnd at least ;-)

I was facing the very same problem with ldntvdm that it needs to get executed prior to all other calls, otherwise some fixes may just be too late. Especially for processes like cmd.exe, that don't have an alertable Wait in their main routine, this was always a problem. The "strongest" that ldntvdm now uses is METHOD_HOOKLDR, I came up with it when disassembling the loader desperately searching for a point where I can inject before main DLL code gets executed. As described in the header file, this is the same as hooking NtCurrentPeb()->PostProcessInitRoutine pointer, but unfortunately, user32.dll sets this pointer to NULL, so I have to write my METHOD_HOOKLDR which just hooks the private loader routine LdrpInitializeProcess. The disadvantage of METHOD_HOOKLDR is that you need debug symbols for lookup of LdrpInitializeProcesswhich may not be desirable for your case, but NTVDMx64 just has the availability of MS Symbol server as a requirement anyway, so we can hook private symbols, as quite some private functions need to get intercepted to fix bugs in them (there is at least 1 different bug in every Windows version, even Windows 11 lol Does M$ even do some testing before putting out a new Windows version..? ).

You said that Shims are too "slow", I don't really know why there should be a performance issue when writing an AppCompat-Shim. can you elaborate on that? There is an example for writing custom shims: https://github.com/Fleex255/CustomShim I also wrote my own shim engine DLL, beacuse I didn't like the example being in C++ and wrote a plain C example. If you are interested, I can upload the source for the Demo-Shim. But unfortunately there can only be 1 custom shim DLL on the system making it hard to place multiple different Shim-DLLs on the same machine without kicking out each other due to the naming restrictions for the DLLs that get accepted as Shim DLLs.

ghotik commented 2 years ago

Thank you again. Well, no, I didn't mean that shims are "slow", quite the opposite: shims are surely executed before anything else, if you define a shim to wrap a system call there is no risk that the call could be run unwrapped and with no shim execution. Probably this is because shims are coordinated by the loader, after all M$ wrote both the loader and the shims engine, and Bill G. can do whatever it likes in its own OS. The problem instead is that my DxWnd wrappers are hooked while some part of the program is in execution, so if the hooking process is not fast enough it is possible that some calls could run unwrapped.

If the METHOD_HOOKLDR is more reliable, it could be worth adding it to DxWnd hooking set even if it could work only on a limited number of cases, you never know that it could become handy. I'll have some more interesting reading in your source code.

The idea of writing a custom shim is interesting. Since shims have a good control of operation timing, I was wondering if it was possible to write a shim that, on its execution, would load dxwnd.dll to do the rest of the job while the process is stopped. That would be the final weapon, the DxWnd "shim injection" mode. Am I too optimistic?

Good, that's all for now, I'll have to read and get prepared for next round!

P.s. as in all other contributions, I'd like to add a reference for yours in DxWnd thanks list. Would that be ok for you? Is the nick "Leecher1337" good for you or would you propose some other name?

leecher1337 commented 2 years ago

Shims - as far as I know - are designed to hook certain functions that you also have to define in the sdb so that it gets loaded, but you could try to use the SN_STATIC_DLLS_INITIALIZED notification hook of the NotifyShims callback for your purposes. Here was some coding attempt by me to write a shim for the pchess program, for instance, may be interesting for reference purposes: pchessshim.zip

Regarding APC injection, I suppose the timing problem is due to the fact that the Shellcode just does a CreateProcess in the target thread, but does not do LoadLibrary directly. I wrote some simple code that does LoadLibrary directly, you can try it directly if you also have timing problems with this piece of code:

// Needed on newer Windows versions (>Vista) to initialize Activation Context 
static __declspec(naked) __declspec(code_seg(".text$1")) void  shellcode(ULONG_PTR parameter)
{
    _asm {
        xor ecx, ecx                        ; zero ECX
        mov eax, dword ptr fs:[24]          ; Get the current TEB
        cmp dword ptr [eax+424], ecx        ; Is the TEB ActivationContextStackPointer pointer NULL ?
        jne cont                            ; If there already is an ActivationContext structure setup, just continue
        call goon                           ; Push current addr on stack
        goon:
        pop ebx                             ; get current address 
        lea edx, dword ptr [ebx+16]         ; calculate the address of our dummy ActivationContext
        mov dword ptr [eax+424], edx        ; and set the address of our dummy ActivationContext in the current TEB
        cont:
        push 0xAAAAAAAA                     ; Set LoadLibraryW address
        ret
        ActCtx:                             ; Empty activation context 
        _emit 0x00 
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
        _emit 0x00
    }
}

#define SHELLCODE_SIZE  0x3D
#define SHELLCODE_LOADLIB_OFFS 0x20

#pragma warning(disable : 4996)
void InjectAPC(char* exepath, char* dirpath, BOOL bSuspended, BOOL bCommitPage)
{
    STARTUPINFO sinfo;
    PROCESS_INFORMATION pinfo;
    char DebugMessage[1024];
    BOOL bKillProcess = FALSE;
    HANDLE hEvent;
    char* eventName = "DxWndInjectCompleted";
    DWORD t0 = GetTickCount();
    LPSTR lpCommandLine = NULL;
    BOOL APCret;

    OutTrace("InjectAPC: exe=\"%s\" dir=\"%s\" commit=%x\n", exepath, dirpath, bCommitPage);

    hEvent = CreateEvent(NULL, FALSE, FALSE, eventName);
    if (!hEvent) {
        if (ERROR_ALREADY_EXISTS == GetLastError()) {
            hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, eventName);
        }
        if (!hEvent) {
            OutTrace("CreateEvent %s failed: err=%d\n", eventName, GetLastError());
        }
    }

    ZeroMemory(&sinfo, sizeof(sinfo));
    sinfo.cb = sizeof(sinfo);
    // attempt to load the specified target
    // v2.05.21: setlpCommandLine together with lpApplicationName for games that requires it,
    // for instance "Star Wars Rogue Squadron 3D"
    // v2.05.23: made this configurable with SETCMDLINE flag
    // v2.05.53: made configurable by field copy and fixed argument order!!
#if 0
    if (pm->cmdline[0])
        lpCommandLine = pm->cmdline;
#endif

    if (!CreateProcessA(exepath, lpCommandLine, 0, 0, FALSE, CREATE_SUSPENDED, NULL, dirpath, &sinfo, &pinfo)) {
        int err = GetLastError();
        char* errmsg;
        switch (err) {
        case ERROR_DIRECTORY:
            errmsg = "(invalid directory name)";
            break;
        case ERROR_FILE_NOT_FOUND:
            errmsg = "(file not found)";
            break;
        default:
            errmsg = "";
            break;
        }
        sprintf(DebugMessage, "CreateProcess \"%s\" \nerror=%d %s", exepath, err, errmsg);
        MessageBoxExA(0, DebugMessage, "Injection", MB_ICONEXCLAMATION | MB_OK, NULL);
        OutTrace("%s\n", DebugMessage);
        CloseHandle(hEvent);
        return;
    }

    // Injection code start
    WCHAR dllpath[MAX_PATH];
    LPVOID lpTgtDllName;
    DWORD dwLenDll = GetFullPathNameW(L"dxwnd.dll", MAX_PATH, dllpath, NULL);
    DWORD dwVersion;
    BOOL fSuccess = FALSE;
    if (dwLenDll)
    {
        HMODULE hKrnl32 = GetModuleHandleA("kernel32.dll");
        LPVOID lpLoadLibrary = GetProcAddress(hKrnl32, "LoadLibraryW");
        DWORD dwReserve, dwPerm = PAGE_READWRITE;

        dwReserve = dwLenDll = (dwLenDll + 1) * sizeof(WCHAR);
        dwVersion = GetVersion();
        if (LOBYTE(LOWORD(dwVersion)) >= 6)
        {
            dwReserve += SHELLCODE_SIZE;
            dwPerm = PAGE_EXECUTE_READWRITE;
        }
        if (lpTgtDllName = VirtualAllocEx(pinfo.hProcess, NULL, dwReserve,
            MEM_RESERVE | MEM_COMMIT, dwPerm))
        {
            if (WriteProcessMemory(pinfo.hProcess, lpTgtDllName, dllpath, dwLenDll, NULL))
            {
                if (LOBYTE(LOWORD(dwVersion)) >= 6)
                {
                    ULONG_PTR ulAddrShellcode = (ULONG_PTR)lpTgtDllName + dwLenDll;
                    if (WriteProcessMemory(pinfo.hProcess, (PVOID)ulAddrShellcode, &shellcode,
                        SHELLCODE_SIZE, NULL) &&
                        WriteProcessMemory(pinfo.hProcess, 
                            (PVOID)((ULONG_PTR)ulAddrShellcode + SHELLCODE_LOADLIB_OFFS),
                            &lpLoadLibrary, sizeof(DWORD), NULL))
                    {
                        fSuccess = QueueUserAPC(ulAddrShellcode, pinfo.hThread, lpTgtDllName);
                    }
                }
                else fSuccess = QueueUserAPC(lpLoadLibrary, pinfo.hThread, lpTgtDllName);
            }
            if (!fSuccess) VirtualFreeEx(pinfo.hProcess, lpTgtDllName, dwReserve, MEM_RELEASE);
        }
    }
    // Injection code end

    if (ResumeThread(pinfo.hThread) == (DWORD)-1) {
        sprintf(DebugMessage, "ResumeThread error=%d at:%d", GetLastError(), __LINE__);
        MessageBoxExA(0, DebugMessage, "Injection", MB_ICONEXCLAMATION | MB_OK, NULL);
        OutTrace("%s\n", DebugMessage);
    }

    CloseHandle(pinfo.hThread);
    CloseHandle(pinfo.hProcess);

    CloseHandle(hEvent);
}

Adding the nick leecher1337 to the list of contributors is fine, as I also go by this nick as author for NTVDMx64 ;-)

ghotik commented 2 years ago

Some quick and short comments ...

1) I tried to compile and replace the current InjectAPC code with this new interesting release, but I got two problems: on VS2008 the statement __declspec(code_seg(".text$1")) is not recognized and without it the procedure doesn't work. The effect is that the target program seems blocked. I don't know if the missing code_seq is the culprit, maybe not because I was on Win7, so in an earlier OS than Vista. Anyway, it's really too early to draw conclusions.

2) the first InjectAPC is working quite well, so far we found only an interesting case where (sometimes) the original injection was better. The old game "South Park" (a really bad beast!) fails at startup with APC because it fails when trying to allocate a memory segment at 0x10010000 that is probably taken by the APC stub. I was just wondering if it could be possible to free the stub memory before loading the dxwnd.dll that, in turn, will fire DllMain with the hooking routines.

If you are curious, you can read more at https://sourceforge.net/p/dxwnd/discussion/general/thread/678da4be84/#983a

update on issue 1) - it was necessary to uncomment these lines if (pm->cmdline[0]) lpCommandLine = pm->cmdline; this way (and with deleted __declspec(code_seg) some target works on Win7, but many others stay locked for a while and then terminate. This should mean that the logic is sound, but probably there's some event not handled properly. I'll try to find what.

leecher1337 commented 2 years ago

Without the code_seg declaration, it could happen that it copies some crap to the taget intead of the real code. I suggest you replace the shellcode() procedure with:

static BYTE shellcode[] = {
    0x33,0xc9,0x64,0xa1,0x18,0x00,0x00,0x00,
    0x39,0x88,0xa8,0x01,0x00,0x00,0x75,0x0f,
    0xe8,0x00,0x00,0x00,0x00,0x5b,0x8d,0x53,
    0x10,0x89,0x90,0xa8,0x01,0x00,0x00,0x68,
    0xaa,0xaa,0xaa,0xaa,0xc3,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00 };
leecher1337 commented 2 years ago

Regarding the proposal to free the memory block, I fear this isn't possible, because it could be that the thread's activation context lies within the allocated memory block and would become an invalid pointer once you free the allocated memory block (which is a challenge on its own). However you can control the address of the allocated memory block in the target process (second parameter of VirtualAllocEx), so you could make it configurable to ensure that you are not taking an address that the game expects to allocate. The Method I presented in the previous post has the advantage that it only allocates one memory block and not multiple blocks, so lowers the risk of collissions.

ghotik commented 2 years ago

changing the shellcode array doesn't seem to give any benefit (even a task that was working before is now failing). I was wondering if it was possible to change the segment attributes on the fly after it was allocated, making a programmatic "code_seg" equivalent. Anyway, all these are details, the original schema works better than expected!

leecher1337 commented 2 years ago

You mean VirtualProtectEx ?

Aha, interesting, do you have any simple test cases where the code above with direct loadlibrary call fails? I'm just curious.

ghotik commented 2 years ago

Sure, as a matter of fact I got a single case where the new procedure works (Soccer Manager '97) so I think you can run any program (or video game) with no risk to miss the trouble. Here in attach the InjectACP.cpp file modified by me. I did the following: 1) #if-ed the old working code to have all available just changing one #if 0 int #if 1 statement 2) recovered the pm and tm arguments, necessary for DxWnd interface. You can revert this change for your tests 3) added a VirtualProtectEx to get PAGE_EXECUTE_READWRITE attributes (was that right? The call throws no error, but ...) 4) added some redundant error messages, just in case. For standalone test you can delete them of #define OutTrace as printf

But I'd like also to ask your opinion about another "thick" problem. Some games are obfuscated, that is they show a stripped PE table with minimal entries that, once run, unzip themselves and populate the actual PE table before running the code. In this case, hooking by PE editing fails and I have to use what I call a "hot patching" method by patching a jump/call in the target function' prologue (using MinHook). The drawback is that this way I have to blindly patch every call without knowing if it was referenced or not. Have you ever had such a problem? Is there some way to tell when the code deflating is finished to defer the PE analysis until when the scene is complete?

InjectAPC (2).zip

leecher1337 commented 2 years ago

I don't really understand the use of VirtualProtect in your modified code:

        DWORD dwReserve, dwPerm = PAGE_READWRITE;

So I default to PAGE_READWRITE on Windows XP and below, because there is no ActivationContext to fix there, so we don't need the shellcode and can simply inject LoadLibraryW as APC

        if (LOBYTE(LOWORD(dwVersion)) >= 6)
        {
            dwReserve += SHELLCODE_SIZE;
            dwPerm = PAGE_EXECUTE_READWRITE;
        }

On Vista and above, we need the shellcode, so the page doesn't just contain the DLL name (where READWRITE would be enough), but also the shellcode, therefore the page permission is changes to EXECUTE_READWRITE.

Now the page gets allocated with the given permissions:

        if (lpTgtDllName = VirtualAllocEx(pinfo.hProcess, NULL, dwReserve,
            MEM_RESERVE | MEM_COMMIT, dwPerm))
        {

And now, I don't understand this. The page is already allocated with the necessary permissions, why do you change them afterwards (most likely to the same permissions the page already has)? I don't get it...

            DWORD OldProtect;
            //if(!VirtualProtectEx(pinfo.hProcess, lpTgtDllName, SHELLCODE_SIZE, PAGE_EXECUTE_READ, &OldProtect)){
            if(!VirtualProtectEx(pinfo.hProcess, lpTgtDllName, dwReserve, PAGE_EXECUTE_READWRITE, &OldProtect)){
                OutTrace("InjectACP: VirtualProtectEx ERROR err=%d\n", GetLastError());
            }

Can you explain the reason why you do this?

leecher1337 commented 2 years ago

Hm, I tried the copy of your code, added a starter to it and ran it as a standalone starter for various applications (i.e.:

test.exe c:\windows\system32\calc.exe test.exe c:\windows\system32\notepad.exe test.exe c:\windows\system32\taskmgr.exe

and dxwnd.dll from same directory was always inected on my Windows 7 machine: test.zip

So providing an example (preferably something that doesn't need to get installed) would still be interesting.

leecher1337 commented 2 years ago

Regarding OEP detection of packed/crypted eexecutables, I guess that inline hooking (that you described as "hotpatching") isn't a bad idea, as various packers have a different workflow, so it's not really clear on how to detect when OEP is reached. There are various OEP finders out there that can be of help, but imho it's too specific for the used exe packer / crypter. You can try to rely on seomething that happens in the entry point, i.e. entering an alertable wait state so that a queued APC would get executed then, or using WaitForInputIdle() works so that hook fires on first GetMessage() call, but that may be too late for you and not every application does it. An unpacker most likely will not enter an alertable wait state, as it is busy with unpacking, I'd assume. But I don't think that there is a reliable generic way to find the OEP.

ghotik commented 2 years ago

About my improper use of VirtualProtectEx, my bad, it was a wrong attempt to make a programmatic "code_seg" directive, though I must admit that it's not clear to me what is the effect of that __declspec mode and how would it be possible to implement it by code. Please, forget it. About a test case, I'll try to suggest something, but today is going to be another busy day (sic!) ... Probably some game RIP (like those in old-games.ru) should let you control both size and registry activities.

About OEP detection of packed/crypted executables, I feared that and you just confirm my suspects.

leecher1337 commented 2 years ago

The code_seg directive is just because, at least in debug builds, it sometimes copies bullshit when specifying the address of the function to copy, so keeping it in a seperate segment of the executable remedys this problem. Of course, if you just use the shellcode as BYTE[] array, this is not necessary at all.

ghotik commented 2 years ago

Oh, my! All of the sudden your code started to work .... after this small fix:

    //dwReserve = dwLenDll = (dwLenDll + 1) * sizeof(WCHAR);
    dwLenDll = (dwLenDll + 1) * sizeof(WCHAR);
    dwReserve = dwLenDll;

I think the reason is clear: your compiler interprets the statements right to left, while my vs2008 probably does it left to right. This way dwReserve is assigned to dwLen before than dwLen is incremented and doubled, so the valued space is not enough to hold everything. Separating the statements in two lines of code makes it unambiguous.

Now it is possible to compare the features of the two APC methods. This last method was proposed as a method able to hook at an earlier stage which makes it quite interesting. But it seems that it doesn't work with all games, there are many of them that don't get run as if the procedure could crash the game in certain circumstances. I am planning to modify the Dxwnd GUI so that it will be easier to add or remove hook procedures, something like the current renderer selection.

leecher1337 commented 2 years ago

Ah, interesting! I didn't check K&R about that, but I guess, behaviour is indeed undefined in this case, so compiler implementation dependent. Good to know that such constructs should be avoided then.

For testing on why games crash on startup, you could use the launcher code, remove "ResumeThread", attach x32dbg to the target game, and then resume thread from the debugger to check why it crashes there (or what bad things it does). I restricted ldntvdm to only link with kernel32.dll and ntdll.dll, as they both should be already loaded at the time of injection, in order to not pull in any other DLL dependencies at this early stage of injection. Not sure if this really helps against potential problems, but I thought that it couldn't hurt. Anyway, I think the debugger will help identifying potential problems with early injection.