Оригинальный DVD-ROM: eXeL@B DVD !
eXeL@B ВИДЕОКУРС !

ВИДЕОКУРС ВЗЛОМ
выпущен 1 марта!


УЗНАТЬ БОЛЬШЕ >>
Домой | Статьи | RAR-cтатьи | FAQ | Форум | Скачать | Видеокурс
Новичку | Ссылки | Программирование | Интервью | Архив | Связь

ПРОГРАММИРОВАНИЕ НА C и С++



Давно заметил, что всё-таки языки С/C++ это не самый лучший вариант программирования под Windows. Сейчас появилась масса более современных и удобных языков, например тот же Python - кроссплатформенный язык, очень легок в изучение. Я его изучил буквально за несколько дней по этому курсу - ссылка. Автор постарался, там видеоуроки на удивление легкие и понятные.
.. Подробно о функциях и CALL'ах  //Trance_C

Данная статья предполагает знание ассемблера начального уровня (регистры, основные комманды, стек).

В давние времена программы писались на чистом ассемблере, и никто не мечтал о компиляторах, что оптимизируют программу, транслируют ее в машинный код, а в завершение связывают отдельные модули в один исполняемый файл. Сейчас, для разработки ПО достаточно бывает знать какой-то один язык программирования, даже Visual Basic. Так что сейчас об асме, как о языке разработки прикладного ПО, даже не вспоминают. Тем не менее, знание ассемблера реально помогает при отладке программ, модификации и восстановлению кода. До сих пор я не видел декомпиляторов, способных из машинного кода создать что-то, хотя бы отдаленно напоминающее программу. А это значит, что провести грамотный reverse-engineering можно только вручную, а для этого нужно понимание смысла не только отдельных инструкций, но и их комбинаций.

Вызов Процедур
Что такое процедура? Не вдаваясь в подробности, можно сказать, что это кусок кода, который выполняет какую-то определенную задачу. На ассемблере вызов осуществляет команда CALL.

 CALL 00482568
 CALL [00401234]
 CALL ESI
 CALL [EAX+24]
 
Имеет смысл проиллюстрировать примеры. Первый вызов передает управление в процедуру внутри исполняемого модуля, ровно по адресу 482568. Второй вызов сложнее, он передает управление в тот адрес, что записан в ячейке памяти [00401234], обычно дизассемблеры дают подсказку, если такого рода вызов обращается к системным функциям. Третий CALL работает с регистром и доставляет массу неудобств - чтобы узнать, куда будет передано управление, нужно следить за регистром, что в него было записано до вызова. Последний вызов обращается к десятой фунции в таблице адресов, что расположена по адресу EAX. То есть отследить ее можно выяснив значение EAX в момент вызова или отыскав саму эту таблицу в исполняемом файле (или еще где?). Любой вызов записывает в стек значение EIP - указатель на следующую за CALL команду, и после этого передает управление (путем изменения значения того же EIP). Для выхода из процедуры используется команда RET. Она переключает процессор на выполнение программы с того адреса, что записан в стеке. Вместе с RET может стоять параметр (напр:. RET 8 - выход + очистка стека от 2 парам.), который указывает, на сколько нужно увеличить ESP (указатель вершины стека). Используется такая конструкция, например, чтобы очистить стек от входных данных процедуры. Иногда этот параметр и есть число входных значений, принятых процедурой (Следует помнить, что для передачи каждого параметра отводится двойное слово - 4 байта). В общем случае, RET может вернуться совсем в другую часть программы, а не в ту, что вызвала его процедуру. Это следует иметь это ввиду и следить за стеком.

Передача параметров и выходные значения
Процедура не просто выполняется сама по себе, но в нее передаются некоторые входные данные, то есть параметры. Форма, в которой будут переданы эти параметры зависит уже от самой процедуры и языка, на котором она была написана. Для одного только Visual C++ MSDN описывает 4 способа вызова функции. В любом случае параметры передаются через стек и/или регистры.
Чаще всего в дизасcемблированном коде встречаются вызовы такого вида:

 push <param_N>
 push <param_N-1>
 . . .
 push <param_2>
 push <param_1>
 call <function>
 
В функцию передается столько параметров, сколько PUSH'ев стоит подряд перед вызовом. "Подряд" в смысле того, что их могут разделять разного рода пересылки (например LEA eax, [esi+0000012c] / PUSH eax), но при этом результат направляется в стек. Особенность такой передачи в том, что первый параметр будет занесен последним, то есть сразу перед вызовом. Возвращаясь к приведенному выше примеру, функция могла иметь такое описание:
 __stdcall function(param_1, param_2, ..., param_N-1, param_N);
 
Такой вызов не зря назван стандартным - он используется системными функциями Windows.
Бывает, что помимо стека для передачи параметров привлекаются регистры ECX и EDX - такой вызов в MS VС++ назван __fastcall. Когда дело касается объектно-ориентированного программирования и вызывается какой-то метод класса, то указатель на объект передается в регистре ECX, а стек может и вовсе не использоваться, если метод класса не принимает параметров. При выходе из процедуры, стек должен быть очищен - обычно это поручается вызываемой процедуре. Однако, в MS VC++ есть вызовы типа __cdecl, и это значит, что код, вызваший такую функцию, очищает стек. Так объявлены стандартные функции языка Си, вроде printf.
По завершении процедуры в регистре EAX находится код возврата. Это то число, что обычно стоит после return в С++ или Result:= в паскале.

Что происходит внутри процедуры
Многие процедуры имеют стандартное начало (благодаря стандартным компиляторам). Одним из варинтов может быть такой код:

 PUSH EBP
 MOV  EBP, ESP
 SUB  ESP, 24		; 24 байта под локальные переменные
 
Такая операция называется содание стекового фрейма. Происходит следующее: в стеке сохраняется старое значение EBP, в сам регистр сохраняется указатель на вершину стека (независимо от того, есть ли в нем параметры), а потом на стеке выделяется место для локальных переменных - 24 байта в данном примере. Получается, что обращаться к параметрам функция может через [EBP + смещение], а к локальным переменным либо как [EBP - смещение] или [ESP + смещение]. В результате функция получает область стека, на которой она обрабатывает локальные данные. Такой же эффект имеет команда ENTER 24, 0. На совремнных процессорах она редко используется, потому что работает медленнее, чем PUSH, MOV, SUB, несмотря на то что была призвана заменить собой эту последовательность.

Выход из процедуры с таким началом содержит нечто обратное по смыслу и эффекту:

 MOV ESP,EBP
 POP EBP
 
Это уничтожение стекового фрейма - восстанавливаются прежние значения вершины стека и регистра EBP. В последний заносится нижняя граница стекового фрейма вызывающей функции (в случае если вызывающая функция не использовала EBP для других целей). Аналогом такой связки является команда LEAVE. В отличие от ENTER, она короче записывается и выполняется быстрее, так что компиляторы предпочитают именно ее.

Другим способом работы с локальными данными является использование регистров общего назначения, так процедура может начинаться с

 PUSH EBX
 PUSH ESI
 PUSH EDI
 
Такой подход может оказаться более выигрышным, чем хранение данных в памяти. EBX, ESI и EDI чаще всего используются для регистровых переменных. К тому же, через эти регистры можно передавать параметры в процедуру (если после PUSH идет занесение нового значения в регистр, то, это никакой не параметр), и коль скоро в задачи компилятора входит сохранять регистры, которые он меняет, появляется такой код в начале, и соответственно
 POP EDI
 POP ESI
 POP EBX
 
в конце, чтобы восстановить значения праметров при выходе из процедуры.

Доступ к переменным
Все переменные в программе после компиляции превращаются в безликие адреса памяти. По дизасму восстановить названия переменных вряд ли удасться, но характер переменной установить достаточно просто. Для примера в коде

 MOV EAX,[00401234]
 MOV [00401238],ESI
 PUSH [77852432]
 ADD [00620428],00001000
 
команды обращаются к переменным по их эффективному адресу в контексте программы. Адрес таких переменных изветстен уже на этапе связывания, следовательно такие переменные были объявлены глобально или статически. Код
 MOV ESI,[EBP+14]
 MOV [ESP+30],EAX
 ADD [EBP+0C],2
 OR  [ESP+20],00000010
 
работает с параметрами процедуры, локальными данными. Подразумевается, что после создания стекового фрейма параметры функции располагаются в ячейках по адресу [EBP+смещение], а локальные переменые по адресам [ESP+смещение]. В коде, подвергшемуся оптимизации, все параметры и переменные могут адресоваться относительно ESP. Неудобно это тем, что ESP может изменяться по ходу процедуры, и параметры имеют переменные адреса. Например, если параметр был в стеке по адресу [ESP+14], то после двух команд PUSH, адрес параметра станет [ESP+1C]. Кроме того, относительная адресация со смещением относительно регистра общего назначения может описывать поля структур или члены классов.

Часто внутри процедуры встречаются команды вроде LEA EAX,[ESP+14]. Несмотря на квадратные скобки, такая команда не считывает память. По эффекту это все равно что EAX = ESP + 14, а по смыслу это может быть получение адреса локальной переменной/параметра/поля структуры. Другим применением этой команды может быть хитрое умножение на 3, 5, 9 примерно так: LEA EAX,[EAX*4+EAX].


Приложение - декомпиляция процедуры
Вот фрагмент кода из одной игры (Пиво первому, кто угадает из какой именно, и кто издатель ;). Попробуем провести ДЕКОМПИЛЯЦИЮ.

 * Referenced by a CALL at Addresses:
 |:00434DF4   , :00435BA2   , :004724AE
 |
 :004504D5 6683795001              cmp word ptr [ecx+50], 0001
 :004504DA 7468                    je 00450544
 :004504DC 8B8190000000            mov eax, dword ptr [ecx+00000090]
 :004504E2 8B542408                mov edx, dword ptr [esp+08]
 :004504E6 53                      push ebx
 :004504E7 56                      push esi
 :004504E8 8BF0                    mov esi, eax
 :004504EA 57                      push edi
 :004504EB 2BF2                    sub esi, edx
 :004504ED 03B194000000            add esi, dword ptr [ecx+00000094]
 :004504F3 8BFE                    mov edi, esi
 :004504F5 2BF8                    sub edi, eax
 :004504F7 8B442410                mov eax, dword ptr [esp+10]
 :004504FB 89B994000000            mov dword ptr [ecx+00000094], edi
 :00450501 BF10270000              mov edi, 00002710
 :00450506 0194816C010000          add dword ptr [ecx+4*eax+0000016C], edx
 :0045050D 8D84816C010000          lea eax, dword ptr [ecx+4*eax+0000016C]
 :00450514 8BC6                    mov eax, esi
 :00450516 8BDF                    mov ebx, edi
 :00450518 99                      cdq
 :00450519 F7FB                    idiv ebx
 :0045051B 6689412C                mov word ptr [ecx+2C], ax
 :0045051F 8BC6                    mov eax, esi
 :00450521 99                      cdq
 :00450522 F7FF                    idiv edi
 :00450524 8B8190000000            mov eax, dword ptr [ecx+00000090]
 :0045052A 038194000000            add eax, dword ptr [ecx+00000094]
 :00450530 50                      push eax
 :00450531 68DA000000              push 000000DA
 :00450536 6689512A                mov word ptr [ecx+2A], dx
 :0045053A E82653FBFF              call 00405865
 :0045053F 59                      pop ecx
 :00450540 59                      pop ecx
 :00450541 5F                      pop edi
 :00450542 5E                      pop esi
 :00450543 5B                      pop ebx
 :00450544 С20008                  ret 0008
 
Итак, раз ее вызывает CALL, то мы получили процедуру в чистом виде. Процедура эта, надо сказать, полна глюков, сейчас при "разбирании" все прояснится. Начнем:
 * Referenced by a CALL at Addresses:
 |:00434DF4   , :00435BA2   , :004724AE
 |
 :004504D5 6683795001              cmp word ptr [ecx+50], 0001
 :004504DA 7468                    je 00450544
 
Сравнивается память в [ecx+50] с единицей, если совпало - переход на RET 8. Если фукция уменьшает стек при выходе на 8, то параметров было два - (обычно они передаются как 4-байтовые DWORD'ы). Значит был такой прототип и начало:
 void <class ?>::func_004504D5 (param_1, param_2)   {    // ex. 004504D5
     if (WORD(this->0x50) == 1) return;                    // ex. 004504DA
 
Тип void выбран потому, что функция не заботится о возвращаемом значении - ничего не записывает в EAX. Запись this->0x50 означает, что осуществляется доступ к полю класса, смещенном на 80 байт относительно начала описания объекта. Обычно именно в ECX записывается указатели вроде this или self. Адресация через ECX + смещение - верный признак работы с классами и объектами в процедуре. Стало известно, что функция описывает какой-то метод какого-то класса... туманно, однако. Дальше можно прокомментировать код таким образом:
 :004504DC mov eax, dword ptr [ecx+00000090] ; eax = this->0x90
 :004504E2 mov edx, dword ptr [esp+08]       ; edx = param_2
 :004504E6 push ebx                          ; сохранили ebx
 :004504E7 push esi                          ; туда же esi
 :004504E8 mov esi, eax                      ; esi = this->0x90
 :004504EA push edi                          ; edi на стеке сохранили
 :004504EB sub esi, edx                      ; esi = this->0x90 - param_2
 :004504ED add esi, dword ptr [ecx+00000094] ; esi = this->0x90 - param_2 + this->0x94
 :004504F3 mov edi, esi                      ; edi = this->0x90 - param_2 + this->0x94
 :004504F5 sub edi, eax                      ; edi = this->0x94 - param_2
 :004504F7 mov eax, dword ptr [esp+10]       ; eax = param_1
 :004504FB mov dword ptr [ecx+00000094], edi ; this->0x94 = this->0x94 - param_2
 
Здесь важно в комментариях имена регистров заменять их значениями, когда те известны. Важно отметить, что первый параметр был изначально по адресу esp+4, а после трех PUSH его положение изменилось на esp+10, то есть "watch the stack, sir". Такая вот ручная трассировка покажет, какие инструкции действительно важны при восстановлении исходного кода. Так, в этих строках все команды являются вспомогательными, кроме одной, которую мы запишем в восстановленный исходник.
     this->0x94 = this->0x94 - param_2 ;                  // ex. 004504FB
 
В продолжении листинга пойдет
 :00450501 mov edi, 00002710                 ; edi = 10000 (десятичное)
 :00450506 add dword ptr [ecx+4*eax+0000016C], edx
                                             ; this->0x16C[param_1] += param_2
 :0045050D lea eax, dword ptr [ecx+4*eax+0000016C]
                                             ; eax = &( this->0x16C[param_1] )
 :00450514 mov eax, esi             ; old: eax = this->0x90 - param_2 + this->0x94
                                    ; fix: eax = this->0x90 + this->0x94
 :00450516 mov ebx, edi                      ; ebx = 10000 (десятичное)
 :00450518 cdq                               ; edx = sign(this->0x90 + this->0x94)
 :00450519 idiv ebx                          ; eax = (this->0x90 + this->0x94) / 10000 
                                             ; edx = (this->0x90 + this->0x94) % 10000 
 :0045051B mov word ptr [ecx+2C], ax         ; this->0x2C = LOWORD( (this->0x90 +
                                             ;this->0x94) / 10000)
 :0045051F mov eax, esi                      ; eax = this->0x90 + this->0x94
 :00450521 cdq                               ; edx = sign(this->0x90 + this->0x94)
 :00450522 idiv edi                          ; eax = (this->0x90 + this->0x94) / 10000 
                                             ; edx = (this->0x90 + this->0x94) % 10000 
 :00450524 mov eax, dword ptr [ecx+00000090] ; eax = this->0x90
 :0045052A add eax, dword ptr [ecx+00000094] ; eax = this->0x90 + this->0x94
 :00450530 push eax                          ; готовимся к вызову - передача второго
                                             ; параметра
 :00450531 push 000000DA                     ; передача первого параметра
 :00450536 mov word ptr [ecx+2A], dx         ; this->0x2A = (this->0x90
                                             ; + this->0x94) % 10000)
 :0045053A call 00405865                     ; func_00405865 (0xDA, this->0x90
                                             ; + this->0x94) 
 :0045053F pop ecx                           ; очистка стека после вызова
 :00450540 pop ecx                           ; два POP'а от вдух параметров
 :00450541 pop edi                           ; вернуть прежнее значение EDI
 :00450542 pop esi                           ; восстановить ESI
 :00450543 pop ebx                           ; вернуть EBX
 :00450544 ret 0008                          ; выход + убрать со стека два
                                             ; своих парметра
 
Это все может казаться сложным, но только поначалу. Более того, существует более простой способ выполнять те операции, что должна делать функция. Вот, к примеру, команда LEA заносит в EAX число - адрес элемента массива, куда добавили параметр_2, а следующая команда перезаписывает этот адрес. Для чего тогда понадобилось делать LEA, мне лично непонятно. Потом два раза производится деление, отдельно для нахождения частного и остатка. В строке 00450514 стоит два комментария. Здесь предстоит решать вопрос о том, что делать с переменной this->0x94 : ее новое значение записано в память, а в регистрах хранится старое. Так, в дальнейшем коде использовалось уже исправленное значение, то есть под this->0x94 понималось значение в RAM. Из всего что приведено выше, можно восстановить только четыре строки кода:
     this->0x16C[param_1] += param_2;                       // ex. 00450506
     this->0x2C = LOWORD((this->0x90 + this->0x94)/10000);  // ex. 0045051B
     this->0x2A = (this->0x90 + this->0x94) % 10000;        // ex. 0045051B
     func_00405865 (0xDA, this->0x90+this->0x94);          // ex. 0045053A
     }
 
Прежде чем собирать воедино куски исходного кода, надо отметить, что такая декомпиляция - процесс долгий и утомительный. Поэтому полностью ее проводить не рекомендуется, особенно если неизвестны назначения переменных и параметров. Мне были известны некоторые назначения и сейчас я приведу восстановленную функцию с (мной придуманными) именами переменных.
 void Player::SpendMoney (DWORD param_1, long price)
 {
     if (CanBuyForFree == 1) return;                          // this опущен
     this->Value -= price ;                                   // дальше без this
 	long new_funds = (this->Money + this->Value);
     this->array_16C[param_1] += price;                       // хз что это и зачем
     this->var_2A = new_funds % 10000;                        //  ? ? ?
     this->var_2C = LOWORD(new_funds) / 10000;                // Тоже непонятно
     func_00405865 (0xDA, new_funds);                         // Вызов куда-то...
 }
Это все, что удалось найти за три часа взлома, написания статьи, копания в доках. Никто не поверит, что для создания патча нужно декомпилировать процедуру. Потому и сделал я этот декомп. Патч сам по себе был написан за 20 минут от начала исследования. Если кому это интересно - надо убрать LEA, сдвинуть код от начала процедуры на 7 байт, чтобы заполнить пустое место. В начале процедуры, около "cmp" можно вставить что-нибудь вроде CMP [ESI+8],0 // jns <туда, где RET>. Радость от такого патча в том, что покупка предметов в игре будет халявной, а при продаже, стоимость будет возвращаться. А вот будь такой код написан вручную, никаких бесполезных LEA и не было бы, так что и патч вписать некуда будет.

<< ВЕРНУТЬСЯ В ПОДРАЗДЕЛ

<< ВЕРНУТЬСЯ В ОГЛАВЛЕНИЕ




Материалы находятся на сайте https://exelab.ru/pro/



Оригинальный DVD-ROM: eXeL@B DVD !


Вы находитесь на EXELAB.rU
Проект ReactOS