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 loop
VirtualProtectEx . 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.SendPacketRender 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