UNetworkHandler::AddNetworkQueue
inside engine.dll:VirtualProtect
and VirtualAlloc
worked out with an error, and without them, the ray cannot be climbed into protected memory. Why is this happening? I never found out, there was no time. But I can say that S protection intercepts NtProtectVirtualMemory
and does something there. Then I began to build an ingenious plan to deceive the defense, but my laziness got the better of it and I stupidly did this: HANDLE hMain = OpenProcess(PROCESS_VM_OPERATION, FALSE, GetCurrentProcessId()); VirtualProtectEx(hMain, ... );
add esi, 0x3c push 0x1
jmp ...
EnterCriticalSection
or elsewhere. We go further. The structure of the package, which is passed to the AddNetworkQueue
function, back in 2010, was published by the respected GoldFinch: struct NetworkPacket { unsigned char id, _padding1, exid, _padding2; unsigned short size, _padding3; unsigned char* data; }
id
and data
fields. As well as the contents of the ecx
. Why ecx
? Everything is simple: we are dealing with the __thiscall
and to call any function of the UNetworkHandler
class UNetworkHandler
we must have a pointer to our object with us. It is transmitted exactly in ecx
. Why do we have to call something? Next, you will understand everything, but for now I am providing the ready code: BYTE *AddNetworkQueue = (BYTE *)GetProcAddress(hEngine, "?AddNetworkQueue@UNetworkHandler@@UAEHPAUNetworkPacket@@@Z"); AddNetworkQueue += *(DWORD *)(AddNetworkQueue + 1) + 5; retAddr_AddNetworkQueue = (DWORD)AddNetworkQueue + 0x19; trmpAddr = (DWORD)wrapper_AddNetworkQueue - ((DWORD)AddNetworkQueue + 0x14 + 5); VirtualProtectEx(hMain, AddNetworkQueue + 0x14, 1, PAGE_EXECUTE_READWRITE, &tmpProtect); *(AddNetworkQueue + 0x14) = 0xE9; *(DWORD *)(AddNetworkQueue + +0x14 + 1) = trmpAddr; VirtualProtectEx(hMain, AddNetworkQueue + 0x14, 1, PAGE_EXECUTE, &tmpProtect); while (!unh) Sleep(100);
AddNetworkQueue += *(DWORD *)(AddNetworkQueue + 1) + 5;
just transitions from the jmp wrapper to the real AddNetworkQueue function. What is unh
? This is the same ecx
value that we push into a variable in our handler: void __declspec(naked) wrapper_AddNetworkQueue() { __asm { pushad pushfd sub [unh], 0 jnz L1 mov [unh], ecx L1: lea eax, [esp + 44] //32 (pushad) + 4 (pushfd) + 4 (push 4) + 4 (ret addr) push eax call [handler_AddNetworkQueue] popfd popad add esi, 0x3c //see disasm push 0x1 jmp [retAddr_AddNetworkQueue] } } void __stdcall handler_AddNetworkQueue(DWORD *stack) { NetworkPacket_t *pck = (NetworkPacket_t *)*stack; if (ShowServerPck) { printf("s -> c | %02hhX ", pck->id); for (int i = 0; i < pck->size; i++) printf("%02hhX ", pck->data[i]); printf("\n"); } }
wrapper_AddNetworkQueue
function saves the values ​​of all registers, gets the value unh
and calls our handler. In it, we comfortably process the packet without fear for the stack, and return control back to the wrapper. He, in turn, restores the jammed instructions and jumps to the place where we interrupted the original code. Nous, one problem less.SendPacket
: UNetworkHandler::SendPacket(char* msk, ...)
ecx
: SendPacket = (BYTE *)*(DWORD *)(**(DWORD **)(unh + 0x48) + 0x68); SendPacket += *(DWORD *)(SendPacket + 1) + 5;
AddNetworkQueue
, like this:SendPacket
, the most common hook in which we spawn a jmp near entry at the address of the garbage instruction obfuscated code. To understand this, in my words, is not very simple, but the scheme is as follows:SendPacket
function.SendPacket
procedure SendPacket
(in fact, I decided to change the attribute of the memory page where the protection handler S is located, more on that later). It sounds difficult, but in fact we will need to execute the following code: VirtualProtectEx(hMain, SendPacket, 1, PAGE_EXECUTE | PAGE_GUARD, &tmpProtect);
SendPacket
function, an exception will be generated that we have to handle. I really don't want to write about tib, so we’ll do everything quite simply AddVectoredExceptionHandler(1, wrapper_SendPacket);
SendPacket
we’ll go to wrapper_SendPacket
: long __stdcall wrapper_SendPacket(PEXCEPTION_POINTERS exInfo) { if (exInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) { VirtualProtectEx(hMain, SendPacket, 1, PAGE_EXECUTE, &tmpProtect); if (exInfo->ContextRecord->Eip == (DWORD)SendPacket) { handler_SendPacket((DWORD *)exInfo->ContextRecord->Esp + 3); //4 (ret addr) + 4 (ret addr) + 4 (1 arg) } return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; }
wrapper_SendPacket
function, wrapper_SendPacket
is called, which normalizes the page attributes and returns control back. But normalize page attributes == remove the hook. We will use the second method above, described under the heading “Head-on”, and reinstall it by intercepting the end of the SendPacket
function (the function has two ret, so we will install two hooks): trmpAddr = (DWORD)wrapper_SendPacketEnd - ((DWORD)SendPacket + 0xb5 + 5); //first ret inside SendPacket VirtualProtectEx(hMain, SendPacket + 0xb5, 1, PAGE_EXECUTE_READWRITE, &tmpProtect); *(SendPacket + 0xb5) = 0xE9; *(DWORD *)(SendPacket + 0xb5 + 1) = trmpAddr; trmpAddr = (DWORD)wrapper_SendPacketEnd - ((DWORD)SendPacket + 0xc5 + 5); //second ret inside SendPacket *(SendPacket + 0xc5) = 0xE9; *(DWORD *)(SendPacket + 0xc5 + 1) = trmpAddr; VirtualProtectEx(hMain, SendPacket + 0xc5, 1, PAGE_EXECUTE, &tmpProtect);
wrapper_SendPacketEnd
: void __declspec(naked) wrapper_SendPacketEnd() { __asm { pushad pushfd call [handler_SendPacketEnd] popfd popad add esp, 0x2000 //see disasm ret } } void __stdcall handler_SendPacketEnd() { if (ShowClientPck) VirtualProtectEx(hMain, SendPacket, 1, PAGE_EXECUTE | PAGE_GUARD, &tmpProtect); }
PAGE_GUARD
attribute and return, not to the end of SendPacket, but to its calling function.wrapper_SendPacket
. Do not forget? Pay attention to the check. if (exInfo->ContextRecord->Eip == (DWORD)SendPacket)) { ... }
VirtualProtectEx
, we change the attribute of at least a whole page of memory. Those at least 4 kilobytes of code are not available. And there may be, and they are there, other procedures. Those exceptions are not necessarily generated when calling SendPacket. This is the main drawback of this method (the handler removes the hook when calling any procedures, at the end of which the hook is not restored), but it is solved. There are several options to fix it. We will use the fastest and not the highest quality. We will stupidly spawn VirtualProtectEx
with the argument PAGE_GUARD
. For this purpose (spoiler: not only for it), the exported function FPlayerSceneNode::Render(FRenderInterface *)
was selected, which is called by the main thread in the loopVirtualProtectEx
. Does this give a 100% guarantee of how our hook works? Of course not. Only 95%. That was enough for me. I did not bother and roll crutches. Above, I wrote that I set the hook not in the engine.dll
address space, but at the address of the S protection handler. Why? Just there, the percentage of response if (exInfo->ContextRecord->Eip == (DWORD)SendPacket)) { ... }
SendPacket
, the output of a certain indicator line that will be displayed 100% after the package has been sent, we will see the following picture:#pck
lines tell us that the hook did not work (the same 5%). Summarize the above porridge:SendPacket
address, we can call our own handlerSendPacket
function, at the end of which is our second hook.SendPacket
Render
procedure, the installation of the same attributes on the same memory will spawn.engine.dll
address space - we get it in the forehead. We are trying to slip the return address not from the engine.dll
address space - we get it in the ear. We are trying to call the function not from the main thread, but directly from our dll`ki - we get through the liver. In the end, the recipe is:engine.dll
functions is engine.dll
, which calls SendPacket
(but in vain!)SendPacket
called from (the return address must be inside engine.dll
, the call must come from the main threadSendPacket
functionSendPacket
functionengine.dll
(it will fit perfectly from alignment) and place one springboard + small buffer there. Let's move from words to deeds: BYTE *Remove = (BYTE *)GetProcAddress(hEngine, "?Remove@?$TArray@E@@QAEXHH@Z"); Remove += *(DWORD *)(Remove + 1) + 5; pckMsk = (char *)Remove + 0x74; //max 44 chars with zero (43 without). You can find more. VirtualProtectEx(hMain, pckMsk, 1, PAGE_EXECUTE_READWRITE, &tmpProtect); //
SendPacket
first (in fact, second) argument.engine.dll
onto our handler (after the call to SendPacket
control passes to our springboard, and from there to our handler). What does this look like? Like this: BYTE* RequestRestart = (BYTE *)GetProcAddress(hEngine, "?RequestRestart@UNetworkHandler@@UAEXAAVL2ParamStack@@@Z"); RequestRestart += *(DWORD *)(RequestRestart + 1) + 5; retAddr_handler_Render = RequestRestart + 0x2b; trmpAddr = (DWORD)fixupStack_Render - ((DWORD)retAddr_handler_Render + 5); VirtualProtectEx(hMain, retAddr_handler_Render, 1, PAGE_READWRITE, &tmpProtect); *retAddr_handler_Render = 0xE9; *(DWORD *)(retAddr_handler_Render + 1) = trmpAddr; VirtualProtectEx(hMain, retAddr_handler_Render, 1, PAGE_EXECUTE, &tmpProtect);
fixupStack_Render
itself: void __declspec(naked) fixupStack_Render() { __asm { add esp, [fixupSize] //SendPacket has cdecl convention mov esp, ebp //prolog of pop ebp ///////handler_Render ret //ret to the end of wrapper_Render } }
SendPacket
fixupSize = 12; //4 (push eax) + 4 (push [pckMsk]) + 4 (push 0x46) __asm { mov ecx, [unh] mov eax, [ecx + 0x48] mov ecx, [eax] mov edx, [ecx + 0x68] //SendPacket push 0x46 push [pckMsk] push eax push [retAddr_handler_Render] //trampoline to fixupStack_Render jmp edx }
fixupStack_Render
procedure fixupStack_Render
this. Of course, SendPacket
itself must be called from the main thread, the aforementioned exported Render
function for such a purpose will fit.Source: https://habr.com/ru/post/336842/
All Articles