📜 ⬆️ ⬇️

Reverse engineering "Kazakov", part two: increase the queue



In most cases, the word "turn" does not cause positive emotions, especially in combination with the word "enlarge". But if you like to play with millions of resources at the beginning of the game, to throw thousands of soldiers into battle in the tenth minute, then the standard order for five units of combat using the Shift key will not be enough. Now, if it were possible to order for 20 or 50 soldiers, or even better - to have several different modifier keys ...

Introduction


After the publication of the previous article and the interest from the LCN community, I was asked if I could increase the queue number of order of combat units from five to 20 or 50. “Why not,” I thought, “and in general, if you're lucky, you only need one byte from 0x05 to 0x14 to replace, and that's it. ”
')
If I knew then what it would turn out ... But I did not know, so let's go!

Where do we start?


Naturally, with the search for the desired part of the machine code. There is a choice: you can search for a place where mouse clicks are processed and try to track the code to the branch responsible for ordering combat units. Or you can bypass all the places where the state of the Shift key is checked. The second option seemed to me more promising. We start the program, which is our favorite for such cases, and look at the list of imported functions.

Hmm, GetKeyState sounds promising. What we have on cross-references? One hundred and eighteen calls? Too much, we need to weed out those calls in which the Shift key is not checked. The GetKeyState function takes only one parameter, namely the keycode , which for Shift is 0x10. In my dmcr.exe, this corresponds to such a piece of machine code:

push 10h 6A 10 call GetKeyState FF 15 EC C1 5C 00 

The search for this byte sequence yielded 38 addresses where GetKeyState (VK_SHIFT) is called. We arrange the breakpoint on each of them, launch the debugger and remove the extra ones, until we get to the necessary procedure. To be precise, there are two procedures: one to order combat units and one to cancel. But since they differ only in the address of the function called in them, then we will consider them as one procedure.

This is what awaits us there:



Well, of course. What does the compiler do when it sees a small loop with a small but constant number of executions? That's right, it unfolds it into a repeating sequence of instructions for the loop body. Hope for a single-byte patch waved a pen at ease.

Patch one or dwell in assembly language


For the cycle, we need a counter, an increment, a comparison, and a conditional jump. The body of the cycle will remain unchanged except for editing the offset of the function call according to the new address of the call instruction.

After some smoking of the manuals for studying the documentation , the following machine code sketch was created:

 ;     push cx 66 51 ;   xor cx, cx 66 31 C9 ;   ;  -     push cx 66 51 ;  ,      mov dx, word ptr [ebp+arg_0] 66 8B 55 F0 push edx 52 xor eax, eax 33 C0 mov al, byte_10FC290 A0 90 C2 0F 01 xor eax, 85h 35 85 00 00 00 push eax 50 call sub_4FD01E E8 .. .. .. .. add esp, 8 83 C4 08 ; ,    - pop cx 66 59 inc cx 66 41 cmp cx, 14h 66 83 F9 14 ;    ,    20 jl 7C DA ;      pop cx 66 59 

A small digression about cx and 66h
The cx register is considered a “loop register” and is used with the loop instruction. Although I decided to use the usual combination of inc, cmp and jl instead of loop, I still left cx as a register counter. However, when selecting machine commands, I had a problem: whatever I did, in the end, operations with the ecx register always came out instead of his younger brother. I had to resort to the help of online assembler . What was my surprise when, in response to my sketch, he issued mostly the same operational codes, but with the prefix 0x66. The documentation describes operational code 66h as “Operand-size override prefix. Reserved and may result in unpredictable behavior ". With this description it is not surprising that he did not rush to my eyes before. The prefix 0x66 causes the machine codes that operate with 32-bit registers to switch to their 16-bit counterparts and vice versa.

Despite the fact that this patch leads to the desired result, it has one major drawback: without interfering with the logic of the game, you can override the size of the production queue created using the Shift modifier key, but no more. Patching the line before each game, depending on the conditions of the game, is not a very attractive prospect, so the community quite quickly voiced the desire to have different queue modifiers on the Shift, Alt, and ~ keys. Well, the challenge is accepted!

Patch two or “program maximum”


Replacing a sequence of five repeating blocks with one cycle, we freed a decent amount of bytes. But how to integrate the test of several keys and the adjustment of the cycle depending on the result into the resulting space? The simplest solution in my opinion is the successive calls to GetKeyState, alternating with assigning the corresponding value to the register, with which the counter in the loop will be compared. If a call to GetKeyState indicates that the key is not pressed, the assignment instruction is jumped over. Thus, instead of forks, depending on the state of the keys, we will have a series of consecutive checks and assignments, ending in one cycle:

 ;   ,      ,     mov ebx, 01h BB 01 00 00 00 ;   push 10h 6A 10 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 ;      mov,      jz 74 05 mov ebx, 05h BB 05 00 00 00 ;   push 12h 6A 12 call GetKeyState FF 15 EC C1 5C 00 [...] 

This time I decided to use the ebx register to save the number of loop executions and the esi register as a loop counter. There are two reasons for this. Following the convention on calling functions, these registers are “permanent”, i.e. if changes are made in the body of the function, the function must save their values ​​on the stack and restore them before completion. This frees me from having to push and pop myself before each execution of the loop. The second reason is that, unlike the cx register, I no longer need the 0x66 prefix, and this is saving one byte on every register operation except mov.

As a result, we have the modifier keys Shift, Alt, TAB, F1 and F2. The ~ key had to be abandoned, since different identifiers correspond to it on different layouts, for example VK_OEM_3 and VK_OEM_5.

Final Patch Code
 ;     push ebx 53 ;   ,      ,     mov ebx, 01h BB 01 00 00 00 ; Shift: 5   push 10h 6A 10 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 05h BB 05 00 00 00 ; Alt: 20   push 12h 6A 12 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 14h BB 14 00 00 00 ; TAB: 50   push 09h 6A 09 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 32h BB 32 00 00 00 ; F1: 15   push 70h 6A 70 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 0Fh BB 0F 00 00 00 ; F2: 36   push 71h 6A 71 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 24h BB 24 00 00 00 ;    -  push esi 56 xor esi, esi 31 F6 ;   mov dx, word ptr [ebp+arg_0] 66 8B 55 F0 push edx 52 xor eax, eax 33 C0 mov al, byte_10FC290 A0 90 C2 0F 01 xor eax, 85h 35 85 00 00 00 push eax 50 call sub_4FD01E E8 .. .. .. .. add esp, 8 83 C4 08 ; , ,     inc esi 46 cmp esi, ebx 39 DE jl 7C E1 ;   pop esi 5E pop ebx 5B 

Afterword


At this point, we can say that the task is completed and go to wash our hands. Or you can write a miniature patcher that allows the players to set the queue size for each of the modifier keys ... But this is about in the next article.

Links

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


All Articles