📜 ⬆️ ⬇️

We bypass the commercial protection using the black box method and write packet hack for lineage 2

Prologue


It all started a year ago, when one of my comrades from the forum T proposed to rewrite the l2phx program known to the whole cheat world for the authorship of the highly respected xkor.
The l2phx itself (l2 packet hack, packet bag, hlapa) is a sniffer of incoming and outgoing packets (all implemented via LSP) of the client lineage 2 (there are versions for other mmorpg), with the ability to send / replace individual packets. Xkor tried as followed: implemented encryption bypass methods, nice gui and so on. But the malicious administrators of the freezers did not like this application: it substantially killed their income at the start of the next one-day packages. Yes, there were times when any noname could go to any server and arrange a complete orgy with this tool. At the same time, all sorts of commercial protections appeared, which unsuccessfully blocked the use of the sachet, and the most cunning ones still encrypted traffic. One of these protections lives on its last gasp and to this day: meet, protection S. Today, protection S stands on all top lineage 2 servers. By the way, xkor provided such an outcome and realized the ability to write a packet decryption module (newxor.dll) on its own. Yes, just write it was not rational: new server == new newxor. The cheating on l2 gradually began to die, because the newbies were not able to send packets using the client memory modification methods (HxD, cheat engine, etc.).

Then I took this undertaking not very seriously: I wrote a client -> server packet interception module and abandoned it. Why? Because. But just 3 days ago, I decided to resume work on this project and publish this article. Why? The cheater l2 community is currently dead. All the bugs and laundering to them are in the hands of 10 people who communicate with each other on Skype and on the T forum. I also decided to leave. And if you leave, it is only beautiful)) Two years ago I dreamed of a working bag, and today I do not need it.

Disclaimer


September burns in view of my age very soon, namely on September 1, I will meet my last year in one of Moscow schools. The list of school literature did not even open, and books to prepare for the exam are gathering dust in the closet. There is no time at all. Part of the code is whipped in a hurry, not quite pleasant news was found out only after the work was completed, but more on that later. The article is also written not entirely in literary language. But what is, that is.

Server Capture Server → Client


All the packets that the client receives from the server can eventually be caught by calling the exported UNetworkHandler::AddNetworkQueue inside engine.dll:
')
image

It is a wrapper, inside which is a jump to the original function:

image

It is quite obvious that somewhere around here the tricky S protection and decrypts packets that were additionally encrypted on the server. If you look at how this code looks in memory, we will see the following:

image

As corny, this is the most common jmp near to some handler. The handler itself is not interesting to us, let it work for itself. We just put the hook after this hook and get the packet in the decoded form. Then there was the first problem. Using a scientific method, it was revealed that functions like 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, ... ); 

Of course, it is not beautiful, considering that we are inside the process (I forgot to mention, we are writing exactly the dll); but it works. We return to the hook ... and the second problem pops up: the protection checks the first 10-20 bytes of this function. It turns out right away, because the nk comes out of the window where we are scolded with obscenities. What to do? That's right, put the hook further. I chose the offset 0x14 (see picture above). jmp near takes 5 bytes, we will overwrite those

 add esi, 0x3c push 0x1 

On

 jmp ... 

Do not forget this, at the end of our handler you will have to restore them. By the way. The hook can be placed inside the imported 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; } 

We are interested in the 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); 

An unprepared person will want to die at this moment. In fact, everything is simple. I’ll just point out that 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"); } } 

Here the naked 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.

Capturing client → server packets


Honestly, these are the most delicious packages. 70% of all dups are based on them. For sending these packages is responsible non-exported function, which is called SendPacket :

 UNetworkHandler::SendPacket(char* msk, ...) 

The function has a variable number of parameters that come from the stack based on the first argument (mask). How to receive the address of this here, nor exported? It's simple, just look at how it is called. The article does not claim to be a tutorial on api lineage 2, so I’ll just give you a specific example of a call:

image

Now it should be clear why we needed the register value.

ecx :

 SendPacket = (BYTE *)*(DWORD *)(**(DWORD **)(unh + 0x48) + 0x68); SendPacket += *(DWORD *)(SendPacket + 1) + 5; 

SendPacket is also a wrapper and inside it is the usual jmp on the main function. Her beginning looks like this:

image

And in memory, by analogy with AddNetworkQueue , like this:

image

Again, a banal leap for a certain handler, but in this case we cannot ignore it - it performs packet encryption. What to do? If you try to overwrite it on your jump - the protection of S swears. And if you go through this jump?

image

Eee, one more jump. I'll spoil: there will be 5 more of them (jmp / call near alternation). We are dealing with obfuscation, cool. What if we are too lazy to restore the flow of control?

Method in the forehead


Why not to overwrite one of these 5 jmp / near on your own? At first, I did, and it was my fatal mistake. As it turned out, S protection checks the integrity of the code in these places and, if it does not coincide with the original, it swears. But! not right away, Carl! Only 15 minutes later. Of course, at the development stage, I couldn’t afford to test performance for such a time. At the end of the work on the whole project, I was pleasantly surprised. But I did not wilt, but ... made the second fatal mistake. Having tried the technique of the inline patch over the obfuscated code aka the self-erasing hooks (unfortunately, the source code of that option was not left to the question of the gita). How it works: we are looking for any garbage instructions and overwrite it on the jmp near to our handler. In it, we quickly restore the original bytes (we write them into memory, where we put jmp, and not just perform overwritten bytes in the handler), do our own thing, return control to the original function. But this option will work only once? That is, until we set the hook again. We recall that the protection S only checks the first bytes of the function and if we put the hook at the end, then it will not say a word. We put the second hook at the end of 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:

  1. Set the hook at the place of the garbage instructions of the protection processor S. In it, at the end, we restore these garbage instructions in memory and jump on them. This way we erase our hook.
  2. Security handler S executes and transfers control to the original SendPacket function.
  3. At the end of it we put the second hook, which reinstalls the first one.

Why did I make this scheme a fatal error # 2? The fact is that this approach will work only if the protection checks the integrity of the code from the current thread. Those, if we have somewhere a second thread weighs, which checks the bytes of the first, then such a trick will not work. So it happened, I just spent time. What to do? We can not change bytes in memory! How to live?! Get out the window?

Method behind


In fact, in such a situation there are a couple of options for installing a hook. I chose one of them: the hook method of changing the rights of the memory page. Yes, this is not the best option, but the terms were burning (I remind you that this was done at the very end, right before writing this article). Here it is worth making a reference to the remarkable series of articles from Broken Sword "Intel processor in protected mode." Read, do not be lazy. As well as a reference to the series of articles by Matt Pietrek ʻa "Win32 SEH from the inside". Googling is quite simple. Now, I hope you understand what the whole point is. We will change the attribute of the page where our 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); 

Now, after the client calls the 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 and not aesthetically

 AddVectoredExceptionHandler(1, wrapper_SendPacket); 

Ok, now when we call 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; } 

As you can see, in the 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); 

Himself 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); } 

There is nothing complicated here, just set the PAGE_GUARD attribute and return, not to the end of SendPacket, but to its calling function.

Let's go back to wrapper_SendPacket . Do not forget? Pay attention to the check.

 if (exInfo->ContextRecord->Eip == (DWORD)SendPacket)) { ... } 

Could it be otherwise? Fortunately, but in our case, unfortunately, yes. When we execute 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

image

Protection S does not swear if you intercept it at the very beginning. We intercept and spawn 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)) { ... } 

much more (empirically tested). If we add to the hook that we installed at the end of 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:

image

The consecutive #pck lines tell us that the hook did not work (the same 5%). Summarize the above porridge:

  1. Change the attributes of the memory page and set an exception handler.
  2. Inside it, we restore the original attributes and, if an exception occurred at our SendPacket address, we can call our own handler
  3. Ultimately, control returns to the original SendPacket function, at the end of which is our second hook.
  4. He, in turn, re-sets the attributes of the memory page and transfers control to the code that caused SendPacket
  5. And at this time, in the Render procedure, the installation of the same attributes on the same memory will spawn.

Sending packages to the server


The most delicious, but after all the dances with a tambourine around packet interception the client -> server is quite simple. We learned how to receive the SendPacket address above, we also saw an example of passing arguments to this function. What to do? Try to call! And quite a lot. We are trying to slip the arguments not from the 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:

  1. S protection do not care where one of the exported engine.dll functions is engine.dll , which calls SendPacket (but in vain!)
  2. Protection S does not care about where SendPacket called from (the return address must be inside engine.dll , the call must come from the main thread
  3. Protection S does not spit on the address space of the SendPacket function

And here is the medicine:

  1. Forge return address when calling the SendPacket function
  2. Forge the address space of the arguments passed to it
  3. Make a call from the main thread

How to do it? Very simple! It is enough to find a free space inside the engine.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); // 

It was found the first available place with a length of 44 bytes (you can search and more). There is a buffer in which the string will be written, which is passed to SendPacket first (in fact, second) argument.

What to do with the return address? Simply slip the springboard inside the 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 } } 

What fixupSize? When calling 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 } 

we give him a variable number of parameters, hence the stack cleaning lies on us. The code in the 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.

Sending packages to the client


Implemented in the same way.

Substitution of incoming and outgoing packets


It is enough to change the arguments of the functions that we have successfully learned to intercept above without success.

Completely forgot


  1. Servers on which everything was tested - the Pythas
  2. The application was written under the Interlude chronicle.
  3. Protection S swears if you change the export table

Epilogue


I express my gratitude to each participant of the forum T, who has helped me at least once; with which we were looking for bugs and dump servers. As well as the developer of protection S: thank you for giving me a reason to write this vraytap. And of course, Habr users who read the article to the end.

Full source included: clack

Video demonstration of the bag work:


I say goodbye to this.

Source: https://habr.com/ru/post/336842/


All Articles