На этом шаге мы приведем пример анализа кода исполняемого файла.
Большинство исполняемых модулей были написаны на языках высокого уровня и не содержат отладочной информации. Однако анализировать код программы и в этом случае приходится. Для того чтобы ускорить процесс анализа, необходимо знать или, по крайней мере, иметь перед глазами некоторые стандартные ассемблерные структуры, соответствующие определенным структурам языков высокого уровня. Заметим, что речь, разумеется, будет идти о 32-битных приложениях.
Современные компиляторы довольно эффективно оптимизируют исходный код, поэтому не всегда просто разобраться, где какая переменная работает. В первую очередь это касается того, что для хранения части переменных, насколько это возможно, компилятор использует регистры. Если ему не хватит регистров, он начнет использовать память.
Для примера возьмем простую консольную программу, написанную на Borland C++. В текстовом варианте программа занимает полтора десятка строк. Корректно справился с задачей, т. е. корректно выявил точку входа - метку _main, только один дизассемблер - IDA PRO. To есть, конечно, реально работающий участок программы дизассемблировали все, но выявить, как происходит переход на участок, смог только упомянутый дизассемблер. Приятно также и то, что аккуратно были распознаны функции printf и getch(). В следующем листинге приведен фрагмент дизассемблированной программы, соответствующей основной процедуре main.
Рис.1. Функция main консольного приложения. Дизассемблер с программы на С, IDA PRO
С другой стороны, в данном случае нет никаких видимых возможностей быстрого поиска данного фрагмента в отладчике. Отсюда наглядно можно понять полезность совместного использования отладчика и дизассемблера.
Попытаемся теперь понять, какая программа на С была исходным источником данного фрагмента. Начнем с рассмотрения стандартных структур. Собственно, налицо только одна структура - цикл. Команда
CODE:00410093 jl short loc_410079
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
Рассмотрим фрагмент в целом:
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.