PDA

Просмотр полной версии : Дизассемблирование программ, откомпилированных Visual C++. Часть 2.


_=(mac)=_
26.05.2009, 20:33
Дизассемблирование программ, откомпилированных Visual C++. Часть 2.
Типо аннотация…
В этом цикле статей я буду описывать идентификацию ключевых структур языка С++, каждая новая статья будет начинаться с простенького исходника на С++, демонстрирующего что-либо, затем буду приводить дизассемблерный листинг этой программы, который собственно и буду разбирать. По причине того, что я ещё можно сказать начинающий, возможны ошибки(
Идентификация массивов.
В этой статье мы рассмотрим работу программы, принимающей от нас числа – элементы одномерного массива, а затем выводящей эти числа на экран. Но сначала разберёмся, что же такое массивы.
Массив является производным типом данных и представляет собой последовательность однотипных данных, имеющих единый идентификатор и хранящихся в смежных ячейках памяти.

Количество элементов в массиве называют размером массива.

Необходимое для хранения массива количество байт оперативной памяти зависит от его типа, а также размера и вычисляется по следующей формуле:
число_байт = sizeof (тип_массива) *число_элементов

Размер массива вычисляется как частное от деления количества байт, занимаемого массивом, на количество байт, которое занимает данное этого типа. Формула вычисления имеет вид:
число_элементов = sizeof (имя_массива) / sizeof (тип_массива)

Массивы могут быть одномерными и многомерными. Доступ к элементам массива организуется с помощью индексов.

Напишем простую программу, принимающую 10 чисел, а затем выводящую их на экран

#include <iostream>
using namespace std;
const int A = 10;
int main()
{
int array [A];
for (int j = 0; j < A; j++)
{
cout<<"Enter array"<<j<<endl;
cin >>array [j];
}
cout <<"\n\nThe entered array:";

for(int j = 0; j < A; j++)
cout << array [j]<<'\t';

return 0;
}

Компилируем, загружаем в IDA Pro, переходим по адресу 401000 и начинаем разбор:


.text:00401000 sub_401000 proc near
.text:00401000
.text:00401000 var_30 = dword ptr -30h
.text:00401000 var_2C = dword ptr -2Ch
.text:00401000 var_28 = dword ptr -28h
;объявление локальных переменных
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 30h
.text:00401006 mov [ebp+var_2C], 0
.text:0040100D jmp short loc_401018
;кладём в стек ebp, затем копируем esp в ebp, после чего от esp отнимаем 30 байт. Инструкция mov [ebp+var_2C], 0 обнуляет память по указателю в первом операнде, который и является индексом нашего массива, после чего безусловный переход на 401018. Переходим по этому адресу…
.text:0040100F loc_40100F:
.text:0040100F mov eax, [ebp+var_2C]
.text:00401012 add eax, 1
.text:00401015 mov [ebp+var_2C], eax
;Фух… Мы тут впервые. Почему? Да потому, что мы не прошли проверку на 00401018, которая немного ниже находится, которая проверяет индекс цикла вывода элементов массива, равен он 10 или нет. Теперь мы тут. А тут у нас индекс сначала помещается в eax, а затем к eax прибавляем 1 (add eax, 1). Теперь индекс у нас увеличен на 1 и находится в eax, и инструкцией mov [ebp+var_2C], eax мы перемещаем его обратно в память, и переходим ниже, к проверке индекса на 10.
Вот мы и разобрали алгоритм цикла ввода массива. Когда индекс будет равен 10, мы не пройдём проверку, расположенную ниже, но в памяти у нас будет 10 чисел – элементов массива, которые надо вывести, а результат проверки нас отправит на 40105B (jge short loc_40105B). По этому адресу находится цикл вывода введённых чисел. Переходим туда….
.text:00401018 loc_401018:
.text:00401018 cmp [ebp+var_2C], 0Ah
.text:0040101C jge short loc_40105B
; Здесь мы видим сравнение ячейки памяти по указателю [ebp+var_2C] c 0Ah, что есть число 10 по нашему, а потом jge(переход, если больше или равно), но так как по указателю [ebp+var_2C] находится 0, то переходим к следующим инструкциям…
.text:0040101E push offset sub_4010B0
.text:00401023 mov ecx, [ebp+var_2C]
.text:00401026 push ecx
.text:00401027 push offset aEnterArray ; "Enter array"
.text:0040102C push offset unk_424918
.text:00401031 call sub_403350
;кладём смещение функции sub_4010B0 в стек, затем в ecx кладём содержимое памяти по указателю [ebp+var_2C], затем ecx помещаем в стек, после чего в стек помещается смещение строки, которую надо вывести, потом в стек идёт адрес 424918, по которому находится объект basic_istream, следственно тут происходит вызов функции вывода, и выводиться будет Enter array, но не забываем, что строка должна иметь вид Enter arrayJ, где j – индекс массива, и находится по [ebp+var_2C]. Смотрим ниже…
.text:00401036 add esp, 8
.text:00401039 mov ecx, eax
.text:0040103B call sub_4010F0
; а ниже у нас ещё один вызов функции вывода, на этот раз вывода индекса массива. Сначала удаляем из стека параметры, которые передавались предъидущей функции, и на вершине оказывается индекс массива (который мы клали по адресу 00401026, если кто забыл), затем копируем из eax адрес basic_istream в ecx, а теперь собственно и выводим текущий индекс.
.text:00401040 mov ecx, eaх
.text:00401042 call sub_4010D0
;копируем в ecx смещение на basic_istream, а затем вызываем функцию перехода на новую строку…
.text:00401047 mov edx, [ebp+var_2C]
.text:0040104A lea eax, [ebp+edx*4+var_28]
.text:0040104E push eax
.text:0040104F mov ecx, offset dword_42487C
.text:00401054 call sub_401640
.text:00401059 jmp short loc_40100F
; А вот тут происходит функция ввода. Сначала индекс заносим в edx, затем мы вычисляем смещение по указателю [ebp+edx*4+var_28], и кладём его в стек – это первый параметр функции и по совместительству буфер для ввода, и хочется приметить, что по мере выполнения программы индекс меняется(возрастает на один, но до этого мы ещё дойдём), а если присмотреться, то можно заметить в указателе на буфер ввода регистр edx*4, т.е. теперь можно наглядно увидеть, что каждое число будет в своей ячейке памяти. Вот например при первом проходе цикла число, которое я ввёл у себя, находится по адресу 0012FF50, второе – по 0012FF54, третье по 0012FF58 и т.д., значит адрес, в котором окажется очередное число, которое мы ввели, равен ebp + индекс*4 – 28. Так, число введено, теперь идет безусловный переход на адрес 0040100F. Переходим….
.text:0040105B
.text:0040105B
.text:0040105B loc_40105B:
.text:0040105B push offset aTheEnteredArra ; "\n\nThe entered array:"
.text:00401060 push offset unk_424918
.text:00401065 call sub_403350
.text:0040106A add esp, 8
.text:0040106D mov [ebp+var_30], 0
.text:00401074 jmp short loc_40107F
;Так… Мы здесь… Сначала тут в стек кладётся смещение строки, которую надо вывести на экран, потом в стек кидаем смещение basic_istream и выводим на экран сообщение. Дальше идёт обнуление [ebp+var_30], это наша вторая переменная j, находящаяся в цикле ВЫВОДА элементов массива. А теперь идёт безусловный переход на 40107F. Прыгаем туда.
.text:00401076
.text:00401076
.text:00401076 loc_401076:
.text:00401076 mov ecx, [ebp+var_30]
.text:00401079 add ecx, 1
.text:0040107C mov [ebp+var_30], ecx
; а вот тут мы в ecx кладём наш индекс… затем add ecx, 1 (j++) и копируем ecx обратно в ячейку памяти [ebp+var_30] (mov [ebp+var_30],ecx). Так, ну а дальше всё разжевывать я смысла не вижу. Это всё, начиная с адреса 00401076 – цикл for нашей программы, который выводит на экран элементы массива, а когда все выведены, j = 10, т.е. не удовлетворяет условию цикла, и через 00401083 jge short loc_4010A4 происходит выход из программы… Щас переходим на этот адрес…
.text:0040107F loc_40107F:
.text:0040107F cmp [ebp+var_30], 0Ah
.text:00401083 jge short loc_4010A4
;значит так… Что-то наподобие этого мы уже видели… Здесь проверяется условие цикла – наша переменная j (индекс) проверяется на 10. Если j >= 0Ah, то переходим на 4010A4, а это есть конец нашей программы. А если j != 0Ah, то идём дальше…
.text:00401085 push 9
.text:00401087 mov edx, [ebp+var_30]
.text:0040108A mov eax, [ebp+edx*4+var_28]
.text:0040108E push eax
.text:0040108F mov ecx, offset unk_424918
.text:00401094 call sub_4010F0
;вызов функции, выводящей элемент массива. Кладём в стек 9, после чего в edx заносим наше текущее значение индекса (j), находящееся по смещению [ebp+var_30], затем мы в eax помещаем число, находящееся по ebp+edx*4+var_28, точней ebp+j*4 – 28. Так, теперь в eax находится одно из элементов массива по текущему индексу. Заносим его в стек, а в ecx кладём смещение basic_istream, и вызываем функцию вывода
.text:00401099 push eax
.text:0040109A call sub_403610
.text:0040109F add esp, 8
.text:004010A2 jmp short loc_401076
; в этом отрывке мы сначала кладём в стек eax, и вызываем функцию (переход на новую строку), после чего выкидываем из стека переданные параметры (add esp, 8) и джимп на 401076… Переходим туда..
.text:004010A4
.text:004010A4
.text:004010A4 loc_4010A4:
.text:004010A4 xor eax, eax
.text:004010A6 mov esp, ebp
.text:004010A8 pop ebp
.text:004010A9 retn
;Вот и всё)) Здесь обнуляется eax, восстанавливается esp и ebp, и долгожданный reth.

Так, вот и подошли к концу все мои умозаключения по поводу одномерных массивов целого типа и попытке разобраться, как же это всё работает…
Так, ну… Что же ещё добавить… Мммм… Ээээ…
Конец

neprovad
26.05.2009, 21:06
"ну а дальше всё разжевывать я смысла не вижу"
Действительно, не вижу смысла разжежывать работу базовых команд. Если речь идет о массивах, то надо и "подниматься" с уровня команд до работы над структурами, функциями их сортировки, методами адресации, etc.
IDA вполне уверенно опознает стековые переменные, зачем вести речь о 0012FF58 и т.п.?
Можно было бы сразу указать что это начало того-то и того-то.
p.s. Имхо надо было упор сделать на структуры данных, их применение к первоначально "безликим" наборам dword'ов, байтов.

_=(mac)=_
26.05.2009, 21:45
При написании следующей статьи учту)) Просто до этого особо ничего не писал, поэтому мож в асме шарю неплохо, но вот написать на эту тему статью это реально для меня пока нелегко =) Но ничё, научусь и этому)))) Всё равно для ученика вечерней школы я наверно ещё и неплохо пишу :-D