Реверс-інжиніринг «Козаків», частина друга: Збільшення черги



У більшості випадків слово «чергу» не викликає позитивних емоцій, тим більше в поєднанні зі словом «збільшити». Але якщо ви любите грати з мільйонами одиниць ресурсів до початку гри, щоб на десятій хвилині кинути в бій тисячі солдатів, то стандартного замовлення по п'ять бойових одиниць одиниць за допомогою клавіші Shift вам буде мало. От якщо б можна було замовляти по 20 або 50 солдатів, або ще краще – мати кілька різних клавіш-модифікаторів…

Вступ

Після публікації попередній статті і виниклого інтересу з боку співтовариства LCN мене запитали, чи я зможу збільшити обсяг черги замовлення бойових одиниць з п'яти до 20 або до 50. «Чому б і ні», подумав я, «та й взагалі – якщо пощастить, то потрібно буде тільки один байт з 0x05 на 0x14 замінити, і все».

Якби я знав тоді, чим це обернеться… Але я не знав, так що поїхали!

З чого почнемо?

Природно, з пошуку потрібного ділянки машинного коду. Тут є вибір: Можна шукати місце, в якому обробляються натискання кнопок миші і спробувати відстежити код до гілки, що відповідає за замовлення бойових одиниць. Або ж можна обійти всі місця, в яких перевіряється стан клавіші Shift. Мені другий варіант здався більш перспективним. Запускаємо всіма улюблену для таких справ програму і дивимося в список імпортованих функцій.

Хм, GetKeyState звучить багатообіцяюче. Що там у нас з перехресним посиланням? Сто вісімнадцять викликів? Забагато, потрібно відсіяти ті виклики, в яких не перевіряється клавіша Shift. Функція GetKeyState приймає тільки один параметр, а саме код клавіші, який для Shift дорівнює 0x10. В моєму dmcr.exe це відповідає такому шматку машинного коду:

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

Пошук цієї послідовності байт видав 38 адрес, на яких викликається GetKeyState(VK_SHIFT). Розставляємо з точки зупину, на кожен з них, запускаємо відладчик і знімаємо зайві, поки не доберемося до потрібної процедури. Якщо бути точним, то процедур дві: Одна для замовлення бойових одиниць і одна для скасування. Але так як вони розрізняються тільки адресою викликається в них функції, то далі ми будемо розглядати їх як одну процедуру.

Ось що нас там чекає:



Ну звичайно. Що робить компілятор, коли бачить маленький цикл з невеликим, але постійною кількістю виконань? Правильно, розгортає його повторювану послідовність інструкцій тіла циклу. Надія на однобайтний патч невимушено помахала ручкою.

Патч перший або зациклюємося на асемблері

Для циклу нам потрібні лічильник, инкрементирование, порівняння і умовний стрибок. Тіло циклу залишимо без змін, за винятком поправки зсуву виклику функції відповідно новою адресою інструкції call.

Після деякого куріння мінлива вивчення документації був створений наступний нарис машинного коду:

; Зберігаємо регістр перед змінами
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

Невеликий відступ про cx і 66hРегістр cx вважається «регістром циклу» і використовується разом з інструкцією loop. Хоч я і вирішив використовувати замість loop звичайну комбінацію з inc, cmp і jl, як регістра-лічильника я все одно залишив cx. Однак при підборі машинних команд у мене виникла проблема: Що б я не робив, у результаті завжди виходили операції з регістром ecx замість його молодшого брата. Довелося вдатися до допомоги онлайн асемблера. Якого ж було моє здивування, коли у відповідь на мій нарис він видав здебільшого ті ж самі операційні коди, але з префіксом 0x66. В документації операційний код 66h описується як «Operand-size override prefix. Reserved and may result in unpredictable behavior». При такому описі не дивно, що він не кинувся мені в очі раніше. Префікс 0x66 змушує машинні коди, які оперують 32-бітними регістрами переключитися на їх 16-бітних побратимів і навпаки.

Незважаючи на те, що цей патч призводить до бажаного результату, він має один великий недолік: Не втручаючись в логіку гри можна змінити розмір черги виробництва, створюваного за допомогою клавіші-модифікатора Shift, але не більше того. Патчити чергу перед кожною грою в залежності від умов гри не дуже приваблива перспектива, тому в співтоваристві досить швидко було озвучено бажання мати різні модифікатори черзі на клавішах Shift, Alt, і ~. Що ж, виклик прийнятий!

Патч другий або «програма максимум»

Замінивши послідовність з п'яти повторюваних блоків одним циклом, ми звільнили пристойну кількість байт. Але як в утворилося простір вбудувати перевірку декількох клавіш і регулювання циклу в залежності від результату? Найпростіше на мій погляд рішення це послідовні виклики GetKeyState, що чергуються з присвоєннями регістру відповідного значення, з яким буде порівнюватись лічильник в циклі. Якщо виклик GetKeyState показує, що клавіша не натиснута, то інструкція присвоєння перепрыгивается. Таким чином у нас замість розвилок в залежності від стану клавіш буде ряд послідовних перевірок і присвоєнь, що завершується одним циклом:

; В тому випадку, якщо жодна клавіша не натиснута, цикл виконається один раз
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
[...]

В цей раз я вирішив використовувати регістр ebx для збереження кількості виконань циклу і регістр esi як лічильник циклу. Для цього є дві причини. Слідуючи угоди про виклик функцій ці регістри є «постійними», тобто якщо в тілі функції в них вносяться зміни, то функція зобов'язана зберегти їх значення в стеку і відновити їх перед завершенням. Це звільняє мене від необхідності самому виконувати push і pop перед кожним виконанням циклу. Друга причина в тому, що на відміну від регістра cx мені більше не потрібно префікс 0x66, а це економія одного байта на кожній операції з регістрами крім mov.

У результаті ми маємо клавіші-модифікатори Shift, Alt, TAB, F1 і F2. Від клавіші ~ довелося відмовитися, так як на різних розкладках їй відповідають різні идентификаторынаприклад VK_OEM_3 і VK_OEM_5.

Фінальний код патча
; Зберігаємо регістр перед зміною
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

Післямова

На цьому місці можна сказати, що завдання виконане і йти умивати руки. Чи ж можна написати мініатюрний патчер, що дозволяє гравцям самим встановлювати розмір черзі для кожної з клавіш-модифікаторів… Але про це в наступній статті.

Посилання


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.