Шаг 116.
Основы анализа кода программ. Переменные и константы

    На этом шаге мы приведем пример анализа кода исполняемого файла.

    Большинство исполняемых модулей были написаны на языках высокого уровня и не содержат отладочной информации. Однако анализировать код программы и в этом случае приходится. Для того чтобы ускорить процесс анализа, необходимо знать или, по крайней мере, иметь перед глазами некоторые стандартные ассемблерные структуры, соответствующие определенным структурам языков высокого уровня. Заметим, что речь, разумеется, будет идти о 32-битных приложениях.

    Современные компиляторы довольно эффективно оптимизируют исходный код, поэтому не всегда просто разобраться, где какая переменная работает. В первую очередь это касается того, что для хранения части переменных, насколько это возможно, компилятор использует регистры. Если ему не хватит регистров, он начнет использовать память.

    Для примера возьмем простую консольную программу, написанную на Borland C++. В текстовом варианте программа занимает полтора десятка строк. Корректно справился с задачей, т. е. корректно выявил точку входа - метку _main, только один дизассемблер - IDA PRO. To есть, конечно, реально работающий участок программы дизассемблировали все, но выявить, как происходит переход на участок, смог только упомянутый дизассемблер. Приятно также и то, что аккуратно были распознаны функции printf и getch(). В следующем листинге приведен фрагмент дизассемблированной программы, соответствующей основной процедуре main.


Рис.1. Функция main консольного приложения. Дизассемблер с программы на С, IDA PRO

    С другой стороны, в данном случае нет никаких видимых возможностей быстрого поиска данного фрагмента в отладчике. Отсюда наглядно можно понять полезность совместного использования отладчика и дизассемблера.

    Попытаемся теперь понять, какая программа на С была исходным источником данного фрагмента. Начнем с рассмотрения стандартных структур. Собственно, налицо только одна структура - цикл. Команда

    CODE:00410093       jl short loc_410079
и является ключевой в организации цикла. Команда же inc eax, очевидно, инкрементирует параметр цикла. Таким образом, в ЕАХ хранится некая переменная, играющая роль параметра цикла. Назовем эту переменную i. Вышесказанное подтверждается и наличием команды хог еах,еах перед началом цикла. Команда эта, разумеется, эквивалентна просто i=0. Команда inc eax означает просто i++. Попытаемся выявить еще переменные. Обратим внимание на команду
    CODE:00410080       mov ecx, ds:off_420070

    Команда весьма примечательна, т. к. в регистр ECX помещается некий адрес, адрес чего-то. Проследим далее, как используется регистр ECX. Команды:

    CODE:00410086                 mov     dl, [ecx+edx]
    CODE:00410089                 mov     byte ptr ds:unk_420178[eax], dl

убеждают нас, что СDX играет роль указателя на строку, массив или запись. Это нам предстоит сейчас выяснить. Тут примечательны две команды:

    CODE:00410095                 mov     ds:byte_420182, 0
    CODE:0041009C                 push    offset unk_420178

    Первая команда убеждает нас, что мы имеем дело со строкой, т.к. именно строка должна содержать в конце символ 0. Вторая команда выполняет передачу второго параметра в функцию printf. Исходя из этого, а также комментария IDA PRO (отладчик раньше нас понял, что это такое), заключаем, что СDX представляет собой указатель на некую строку. Заметьте, что мы не обратились к просмотру блока данных, что, несомненно, ускорило бы наше исследование. Обозначим этот указатель как s1. В этой связи выражение [ecx+edx] можно трактовать как s1[i] или как *(s1 + i).

    Что означает последовательность команд

    CODE:00410086                 mov     dl, [ecx+edx]
    CODE:00410089                 mov     byte ptr ds:unk_420178[eax], dl
    CODE:0041008F                 inc     eax

    Эта последовательность может означать только одно: на каждом шаге цикла в DL помещается значение из [ecx+edx], которое затем помещается по адресу byte ptr ds:unk_420178[eax]. Причем обратите внимание, что ЕAХ на каждой итерации увеличивается, а EDX - уменьшается. Выражение [ecx+edx] тогда будет эквивалентно s2[9-i] или *(s2+9-i).

    В результате получается, что строки:

    CODE:00410086                 mov     dl, [ecx+edx]
    CODE:00410089                 mov     byte ptr ds:unk_420178[eax], dl
можно заменить на s1[i]=s2[9-i].

    Рассмотрим фрагмент в целом:

    CODE:00410077                 xor     eax, eax
    CODE:00410079 
    CODE:00410079 loc_410079:                             ; CODE XREF: _main+1Fj
    CODE:00410079                 mov     edx, 9
    CODE:0041007E                 sub     edx, eax
    CODE:00410080                 mov     ecx, ds:off_420070
    CODE:00410086                 mov     dl, [ecx+edx]
    CODE:00410089                 mov     byte ptr ds:unk_420178[eax], dl
    CODE:0041008F                 inc     eax
    CODE:00410090                 cmp     eax, 0Bh
    CODE:00410093                 jl      short loc_410079

    По нашему мнению, он соответствует следующему С-фрагменту:

  i=0;
  do {
	 s1[i]=s2[9-i];
	 i++;
  } while(i<11);

    Еще один вопрос: где хранятся строки s1 и s2? В этом можно разобраться достаточно быстро. Main является самой обычной процедурой, и если бы строки являлись локальными переменными, то для них была бы зарезервирована область в стеке процедуры путем вставки команды SUB ESP,N (или ADD ESP,-N). Таким образом, строковые переменные s1 и s2 являются глобальными. Интересно, что другие переменные, хотя они тоже локальные, хранятся в регистрах, в полном соответствии с принципом, что по мере возможности переменные следует хранить в регистрах.

    Окончательный результат анализа представлен ниже

#include<stdio.h>
#include<conio.h>
char s1[11];
char *s2="0123456789";
void main()
{
  int i;
  i=0;
  do {
	 s1[i]=s2[9-i];
	 i++;
  } while(i<11);
  s1[10]='\0';
  printf("%s\n",s1);
  getch();
}
Текст этой программы можно взять здесь.

    Со следующего шага мы начнем рассматривать управляюще конструкции языка C.




Предыдущий шаг Содержание Следующий шаг