leecher1337 / ntvdmx64

Run Microsoft Windows NTVDM (DOS) on 64bit Editions
784 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)

ghotik commented 2 years ago

Another bit of the mystery is revealed: this morning I started moving the new, working code on Win7 with some GUI update to make testing more comfortable. With my despair, nothing was working, not even the old procedures for the few games that seemed to accept APC injection. Then, a bit of insight: I turned the AV off and things started to work again. So, it seems that APC injection is so alike viral code that the AV stops it. I was used to the AV setting DxWnd.exe as unsafe and to add exceptions, but apparently the exception doesn't prevent Avast to make some run-time checks and kill the suspicious activity. This is quite an annoyance, because making tests with the AV protections turned off is not healthy, considering that together with DxWnd code we're going to execute old crapped games hacked by who knows who!

leecher1337 commented 2 years ago

Honestly, I consider all this Antivirus-Crap just snakeoil that doesn't have any real use. It slows down the machine with all its realtime scans, it gives loads of false positives (my personal best was a simple C program that I wrote to enumerate network shares and reconnect them, as Windows often suffers from a problem that network drives are not reconnected on startup, only on first use, so it was just a useful simple utility - 50% (!!) of all AV-scanners on virustotal.com flagged it as malware - unbelievable!), it wrongly flags cracks and keygens as malware with partly obscure names (Kaspersky doesn't do that kind of crap, but i.e. Avira is notorious for making intentionally wrong signatures), partly has buggy filter drivers that cause system hangs, etc. I'm OK with offline signature based scanners, but all this realtime protection crap is just a burden that can and should be avoided imho. Just my personal opinion.

ghotik commented 2 years ago

If you are curious, here https://sourceforge.net/p/dxwnd/discussion/general/thread/678da4be84/#a262 I uploaded a DxWnd upgrade (it should work also with these two files alone) with the new inject modes that, with little fantasy, I called "Inject ACP" and "Inject ACP2". Today (I say today because I'm now used to see test results changing in all possible ways ...) on Win7 the inject ACP works pretty well with the AV turned off, while the inject ACP2 doesn't work at all (despite the fact that yesterday, on Win11 and the code fix it seemed to work in most cases!). I will attach here also the source code of the two procedures. As you may see, I didn't change very much. Question: could the use of libc calls (like fopen , sprintf used to write logs) inhibit the loading of dxwnd.dll in ACP injection? I am skeptical, because the random behavior, if that was a problem why should the ACP2 work on Win11? InjectAPC2.zip

ghotik commented 2 years ago

Some better (and less bugged) files here: https://sourceforge.net/p/dxwnd/discussion/general/thread/678da4be84/?page=1#7fcb But maybe it's better that we stabilize the situation a little better, I fear that because of some bugs of mine I made some mess here.

I got a few doubts and I humbly ask your opinion: I see that the ACP2 shellcode block uses some registers (ecx, eax and others) but it doesn't preserve their value before the shellcode execution with some push & pop instructions. Is it possible that depending on the OS or the target program there registers could be vital? You overwrite the DWORD 0xAAAAAAAA in the shellcode with the address of the LoadLibraryW system call that you retrieve in the DxWnd.exe address space. But the shellcode is going to be executed in the target address space, so are we sure that the LoadLibraryW address so retrieved is valid in the new context?

ghotik commented 2 years ago

Usually, a task hooked with ACP2 dies immediately, so it is impossible to attach it to a debugger (I use OllyDBG) but launching it repeatedly you can get multiple instances locking each other that can be inspected. This happens now on my Win7 computer. I hope that the peculiar test condition don't invalidate some possible conclusions. This is what I saw attaching one of them: acp2dump2 Here it is possible to see what you would expect, the WIDECHAR full path of dxwnd.dll and the shellcode with 0xaaaaaaaa replaced by the address 0x76D548F3. That is supposed to be the address of kernel32 LoadLibraryW, but trying to follow the pointer I get into a unqualified procedure that leads to a bad address. The memory dump shows that there is no kernel32 text segment (yet). BTW (unseen) the debugger declares a unhandled exception accessing address 0x80000008, which is in the kernel memory segment. acp2dump3 Could it be that the problem for this early hook is that it just runs too early?

leecher1337 commented 2 years ago

Ah, good analysis, so I'd say you are right with your conclusion that at the time the code runs, there is no kernel32.dll yet. So maybe the APC fires in the loader at a position where DLLs are not bound yet (call stack would be useful to see that). In APC1, we create a thread so the code will run after next task switch, so probability that it runs too early may be lower, so that may be the reason why it is more reliable. The crash can easily be fixed by using NTDLL's LdrLoad... function, but it wouldn't be of much use for your usecase, I guess, because DXWND.DLL should run AFTER imports were bound by the loader, otherwise you wouldn't find what you are looking for. Maybe it could be rewritten so that it checks the current state of progress of the loader and requeue the APC if it runs too early, but I guess I'm lacking reliable test cases for that, because it may be random when the APC gets executed.

Another method that could work pretty reliably is registering yourself as a debugger and launch the process with debug flags, then you get called on application entry point reliably. I didn't use this method for ldntvdm, because it would possibly prevent a real debugger from working, but for your usecase, it may be a good method, because you only inject into 1 target. Should I try to write some demo code for that method?

ghotik commented 2 years ago

I wrote something that resembles what you proposed, a debugger skeleton just to inject the dxwnd.dll code. It works, but it is a little less reliable that the other methods, maybe because I didn't handle all the many events that could happen during a debug session. If you want, you may start from this file belonging to DxWnd project already, maybe you can see some flaw here ... InjectDebug.zip

leecher1337 commented 2 years ago

Ah, I see, I must have overlooked it that this is already implemented. You put an infinite loop at EIP and then check when target has reached it. Just out of couriosity: Is there an advantage over placing an INT 3 there? With other methods, INT 3 would have undesirable effects, but as a debugger, you would get notified about it, right? But generally, acting as a debugger should be pretty robust that makes me wonder why it is a bit unreliable. There is no timing issue as it can happen with APCs as you always reach a pre-defined point as a debugger. Just makes me wonder in which cases this can fail.

ghotik commented 2 years ago

Well, now on Win7 it seems pretty good in this release, but after a few attempts with a dozen different games I found a problem in "Premier Manager 97". The game gets hooked, but some desktop resolution setting operation escapes my hooks and the video mode changes, while this doesn't happen with the ISP injection, nor with your ACP method. This is the DxWnd GUI log about two attempts, the first with ISP and the second with ACP

isp vs debug.txt

As you can see there's a lot of events to process. The dxwnd.dll should happen (unlogged) after the process creation, you may notice a second load of dxwnd.dll later at [00000:718], but this should be the redundant window hook that gets executed when it's too late.

Update: I repeated the tests disabling the redundant window hook and - surprise - the debug hook is practically useless, it seems to always fail doing its job. The only difference with ACP2 is that it doesn't crash the program, so you don't notice. Ach!

ghotik commented 2 years ago

Uhm... I repeated the test with window hook disabled and the load dll message about dxwnd.dll disappeared. But there should be one right after the process creation, I wonder why it's not there. It seems that the target program never loaded this lib! debug_alone.txt Sadly, one eerie characteristic of this method is that you can't have two debuggers attached to the same process, so if you inject via debug mode you can't also debug to see what's going on :(

ghotik commented 2 years ago

I got it! The problem was in the Inject function within dxwnd.dll where I freed (with VirtualFreeEx) the allocated buffer where the dxwnd.dll path was copied. The operation was deferred by 500mSec that is enough or not depending on the case, so I decided to swipe that call away and everything works much better. The inject procedure is used by many hook modes, so I revised quickly some results. With the fixed dxwnd.dll:

In the new log yu can see that now dxwnd.dll is effectively loaded, but the method can't force the loading sequence, so it is possible that one of the previously loaded dlls has a DllMain procedure that sets a video mode and the toy is broken. In effect I got a suspect that proved to be right: in this case the culprit and unwanted code comes from a Microsoft shim 640x480 applied without being asked for! I renamed the executable filename (with the DxWnd noshim flag, but it doesn't matter how) and the debug mode worked also in this case. Well, it's a difficult world. pm97_alone.txt

ghotik commented 2 years ago

If you are still interested in injection problems and difficult cases, I just found one with "Jane's Fleet Command" (discussion also here: https://sourceforge.net/p/dxwnd/discussion/general/thread/678da4be84/?page=1#c0e4 ). The game seems to load preferably glide2x.dll for rendering, withdrawing on ddraw.dll if a 3Dfx card (or a glide emulator) can't be found. DxWnd has a flag to mimic a failure when trying to make a LoadLibrary("glide2x.dll") operation, but the game apparently succeeds loading it because the loading happens before that dxwnd.dll is injected and run. Since no static linkage can be found, I believe this is because a LoadLibrary in buried within the DllMain procedure of some of its linked dlls. Running the game with Debugger injection, in effect you can see that the glide2x.dll module was loaded before dxwnd.dll, and I can't think of a way to prevent this to happen! dxwnd.gui.fleet.log

ghotik commented 2 years ago

Another curiosity about ACP injection: in my Win11 the difficulties to make an early hook seem increased, also turning the AV off I got games that refused the hook. Then, by chance, I put a MessageBox in the middle of the ACP injection steps and it worked. Maybe the MessageBox provided the necessary suspended state that is necessary for the injected thread to be scheduled. Since the MessageBox call is based on a timed-out inner routine MessageBoxTimeout it could be possible to increase the effectiveness of ACP injection by simply adding a timed-out message like "Wait 1 second ..." and then going on.

update: tested & working. This is the code added just before the ResumeThread call in the ACP injection routine. I set a 500 mSec timeout, but I saw it working also with smaller delays. HMODULE hUser32 = GetModuleHandle("user32.dll"); if (hUser32){ typedef int (WINAPI *MessageBoxTimeoutA_Type)(HWND, LPCSTR, LPCSTR, UINT, WORD, DWORD); MessageBoxTimeoutA_Type pMessageBoxTimeoutA; pMessageBoxTimeoutA = (MessageBoxTimeoutA_Type)GetProcAddress(hUser32, "MessageBoxTimeoutA"); if(pMessageBoxTimeoutA) (*pMessageBoxTimeoutA)(NULL, "wait ...", "DxWnd", 0, 0, 500); }

update 2: after writing this code I felt a little stupid and thought: "if all we need is a small delay, why don't do that silently with a Sleep(500) call?". Well, as a matter o fact there is a good reason: the Sleep doesn't fix the problem. Probably it is some obscure reason about context switching or entering kernel mode, anyway MessageBoxTimeoutA worked and Sleep didn't!

leecher1337 commented 2 years ago

Hmm, SleepEx just does a NtDelayExecution and indeed would trigger a Task switch. Maybe it's GetMessage inside MsgBox that helps? Just as a stupid idea: Did you try with PeekMessage / GetMessage ?

ghotik commented 2 years ago

I tried right now with this code replacing the timed message-box:

    MSG msg;
    GetMessage(&msg, 0, 0, 0);
    DWORD tick0 = GetTickCount();
    while((GetTickCount() - tick0) < 500)  GetMessage(&msg, 0, 0, 0);

The result is puzzling: depending on who knows what, the trick works or doesn't work. The feeling is that it works better at first game start, but as soon as you feel you got a schema, next time the behavior is completely different. The thing doesn't change if I turn the AV off or I increase the 500 mSec timeout.

BEENNath58 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.

Hi @leecher1337 it's been some time and I didn't want to interrupt this discussion of ACP. I tried different applications in this time and it seems no DOS application work in Program Files folder in any partition. I want to follow your instructions but I didn't manage to figure out how to do them. Can you please specify better?

leecher1337 commented 2 years ago

First of all, you can check for potential file access errors using Sysinternals ProcessMonitor and setting process to ntvdm.exe, maybe its results lead you to some wrong path access or something like that.

Otherwise, to debug NTVDM: 1) Ensure that you installed checked build 2) Run inst-sym.cmd from release so that debug symbols get installed. 3) There is a YODA.reg file in reg\ folder of the release. Merge it into your registry. From now on, NTVDM will not start up normally but break into YODA debugger. If you check the contents of the registry file or the docs, this has to do with setting YODA=1, so as soon as you don't want YODA to break anymore, you have to remove the key YODA=1 from CpuEnv in registry again! Also be aware that if no vdmdbg is running, ntvdm will be cought in an endless loop with 99%, if that happen (spamming debugvie output with Yoda> prompts), so you have to kill ntvdm.exe via task manager. 4) Startup vdmdebug.exe 5) Start your target application You will see a Yoda> prompt in vdmdebug window. 6) Attach a debugger to ntvdm.exe, i.e. x32dbg 7) Now you can continue in Yoda by entering c command 8) If x32dbg breaks with some notifications like STATUS_SEGMENT_NOTIFICATION, you can skip them with SHIFT+F9 9) if ntvdm.exe crashes, you will end up in x32dbg at the point of crash. If it just silently quits, you may have to set various breakpoints in ntvdm.exe in order to check the flow. This would require you to study NTVDM sourcecode in order to know where you cna place breakpoints to catch certain error conditions. There is no generic hint I can give you, unfortunately. Maybe you can also spot something in the YODA window. Generally: If you want to debug code INSIDE the NTVDM, so DOS-code, you have to get used to YODA and use it for that. If you want to debug NTVDM, use x32dbg. As it's a path issue, you may want to set breakpoints at KERNE32.CreateFile API, but it's just a guess.