Фундаментальные основы хакерства

Идентификация локальных стековых переменных


…общая масса бактерий гораздо больше, чем наша с вами суммарная масса. Бактерии - основа жизни на земле…

А.П. Капица

Локальные переменные размещаются в стеке

(так же называемым автоматической памятью) и удаляются оттуда вызываемой функцией по ее завершению. Рассмотрим подробнее: как это происходит. Сначала в стек затягиваются аргументы, передаваемые функции (если они есть), а сверху на них кладется адрес возврата, помещаемый туда инструкцией CALL вызывающей эту функцию. Получив управление, функция открывает кадр стека – сохраняет прежнее значение регистра EBP и устанавливает его равным регистру ESP (регистр указатель вершины стека). "Выше" (т.е. в более младших адресах) EBP находится свободная область стека, ниже – служебные данные (сохраненный EBP, адрес возврата) и аргументы.

Сохранность области стека, расположенная выше указателя вершины стека (регистра ESP), не гарантируется от затирания и искажения. Ее беспрепятственно могут использовать, например, обработчики аппаратных прерываний, вызываемые в непредсказуемом месте в непредсказуемое время. Да и использование стека самой функцией (для сохранения ль регистров или передачи аргументов) приведет к его искажению. Какой из этой ситуации выход? – принудительно переместить указатель вершины стека вверх, тем самым "занимая" данную область стека. Сохранность память, находящейся "ниже" ESP гарантируется (имеется ввиду – гарантируется от непреднамеренных искажений), - очередной вызов инструкции PUSH занесет данные на вершину стека, не затирая локальные переменные.

По окончании же своей работы, функция обязана вернуть ESP на прежнее место, иначе функция RET снимет со стека отнюдь не адрес возврата, а вообще не весь что (значение самой "верхней" локальной переменной) и передаст управление "в космос"…

Рисунок 15 0х00E Механизм размещения локальных переменных в стеке. На левой картинке показано состояние стека на момент вызова функции.
Детали технической реализации. Существует множество вариаций реализации выделения и освобождения памяти под локальные переменные. Казалось бы, чем плохо очевидное SUB ESP,xxx на входе и ADD ESP, xxx

на выходе? А вот Borland C++ (и некоторые другие компиляторы) в стремлении отличиться ото всех остальных резервируют память не уменьшением, а увеличением ESP… да, на отрицательное число (которое по умолчанию большинством дизассемблеров отображается как очень большое положительное). Оптимизирующие компиляторы при отводе небольшого количества памяти заменяют SUB

на PUSH reg, что на несколько байт короче. Последнее создает очевидные проблемы идентификации – попробуй, разберись, то ли перед нами сохранение регистров в стеке, то ли передача аргументов, то ли резервирование памяти для локальных переменных (подробнее см. "идентификация механизма выделения памяти").

Алгоритм освобождения памяти так же неоднозначен. Помимо увеличения регистра указателя вершины стека инструкцией ADD ESP, xxx



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

Действие

Варианты реализации

Резервирование памяти

SUB ESP, xxx

ADD ESP,–xxx

PUSH reg

Освобождение памяти

ADD ESP, xxx

SUB ESP,–xxx

POP reg

MOV ESP, EBP

Таблица 14 Наиболее распространенные варианты реализации резервирования памяти под локальные переменные и ее освобождение

Идентификация механизма выделения памяти. Выделение памяти инструкциями SUB и ADD

непротиворечиво и всегда интерпретируется однозначно. Если же выделение памяти осуществляется командой PUSH, а освобождение – POP, эта конструкция становится неотличима от простого освобождения/сохранения регистров в стеке.


Ситуация серьезно осложняется тем, что в функции присутствуют и "настоящие" команды сохранения регистров, сливаясь с командами выделения памяти. Как узнать: сколько байт резервируется для локальных переменных, и резервируются ли они вообще (может, в функции локальных переменных и нет вовсе)?

Ответить на этот вопрос позволяет поиск обращений к ячейкам памяти, лежащих "выше" регистра EBP, т.е. с отрицательными относительными смещениями. Рассмотрим два примера, приведенные на листинге 110.

PUSH EBP                           PUSH EBP

PUSH ECX                           PUSH ECX

xxx                                xxx

xxx                                MOV [EBP-4],0x666

xxx                                xxx

POP ECX                            POP ECX

POP EBP                            POP EBP

RET                                RET

Листинг 110

В левом из них никакого обращения к локальным переменным не происходит вообще, а в правом наличествует конструкция "MOV [EBP-4],0x666", копирующая значение 0x666 в локальную переменную var_4. А раз есть локальная переменная, для нее кем-то должна быть выделена память. Поскольку, инструкций SUB ESP, xxx

и ADD ESP, – xxx в теле функций не наблюдается – "подозрение" падает на PUSH ECX, т.к. сохраненное содержимое регистра ECX располагается в стеке на четыре байта "выше" EBP. В данном случае "подозревается" лишь одна команда – PUSH ECX, поскольку PUSH EBP на роль "резерватора" не тянет, но как быть, если "подозреваемых" несколько?

Определить количество выделенной памяти можно по смещению самой "высокой" локальной переменной, которую удается обнаружить в теле функции. То есть, отыскав все выражения типа [EBP-xxx] выберем наибольшее смещение "xxx" – в общем случае оно равно количеству байт выделенной под локальные переменные памяти. В частностях же встречаются объявленные, но не используемые локальные переменные. Им выделяется память (хотя оптимизирующие компиляторы просто выкидывают такие переменные за ненадобностью), но ни одного обращения к ним не происходит, и описанный выше алгоритм подсчета объема резервируемой памяти дает заниженный результат.


Впрочем, эта ошибка никак не сказывается на результатах анализа программы.

Инициализация локальных переменных. Существует два способа инициализации локальных переменных: присвоение необходимого значение инструкцией MOV (например, "MOV [EBP-04], 0x666") и непосредственное заталкивания значения в стек инструкцией PUSH

( например, PUSH 0x777). Последнее позволяет выгодно комбинировать выделение памяти под локальные переменные с их инициализацией (разумеется, только в том случае, если этих переменных немного).

Популярные компиляторы в подавляющем большинстве случаев выполняют операцию инициализации с помощью MOV, а PUSH

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

Размещение массивов и структур. Массивы и структуры размещаются в стеке последовательно в смежных ячейках памяти, при этом меньший индекс массива (элемент структуры) лежит по меньшему адресу, но, - внимание, - адресуется большим модулем смещения относительно регистра указателя кадра стека. Это не покажется удивительными, если вспомнить, что локальные переменные адресуются отрицательными смещениями, следовательно, [EBP-0x4] > [EBP-0x10].

Путаницу усиливает то обстоятельство, что, давая локальными переменным имена, IDA опускает знак минус. Поэтому, из двух имен, скажем, var_4 и var_10, по меньшему адресу лежит то, чей индекс больше! Если var_4 и var_10 – это два конца массива, то с непривычки возникает непроизвольное желание поместить var_4 в голову, а var_10 в "хвост" массива, хотя на самом деле все наоборот!

Выравнивание в стеке. В некоторых случаях элементы структуры, массива и даже просто отдельные переменные требуется располагать по кратным адресам. Но ведь значение указателя вершины заранее не определено и неизвестно компилятору. Как же он, не зная фактического значения указателя, сможет выполнить это требование? Да очень просто – возьмет и откинет младшие биты ESP!



Легко доказать, если младший бит равен нулю, число – четное. Чтобы быть уверенным, что значение указателя вершины стека делится на два без остатка, достаточно лишь сбросить его младший бит. Сбросив два бита, мы получим значение заведомо кратное четырем, три – восьми и т.д.

Сброс битов в подавляющем большинстве случаев осуществляется инструкцией AND. Например, "AND ESP, FFFFFFF0" дает ESP кратным шестнадцати. Как было получено это значение? Переводим "0xFFFFFFF0" в двоичный вид, получаем – "11111111 11111111 11111111 11110000". Видите четыре нуля на конце? Значит, четыре младших бита любого числа будут маскированы, и оно разделиться без остатка на 24 = 16.

___Как IDA идентифицирует локальные переменные.

Хотя с локальными переменными мы уже неоднократно встречались при изучении прошлых примеров, не помешает это сделать это еще один раз:

#include <stdio.h>

#include <stdlib.h>

int MyFunc(int a, int b)

{

int c;       // Локальная переменная типа int

char x[50]   // Массив (демонстрирует схему размещения массивов в памяти_

c=a+b;                            // Заносим в 'c' сумму аргументов 'a

и 'b'

ltoa(c,&x[0],0x10)  ;            // Переводим сумму 'a' и 'b' в строку

printf("%x == %s == ",c,&x[0]);   // Выводим строку на экран

return c;

}

main()

{

int a=0x666; // Объявляем локальные переменные 'a' и 'b' для того, чтобы

int b=0x777; // продемонстрировать механизм их иницилизации компилятором

int c[1];    // Такие извращения понадобовились для того, чтобы запретит

// отимизирующему компилятору помещать локальную переменную

// в регистр (см. "Идентификация регистровых переменных")

// Т.к. функции printf

передается указатель на 'c', а

// указатель на регистр быть передан не может, компилятор

// вынужен оставить переменную в памяти

c[0]=MyFunc(a,b);

printf("%x\n",&c[0]);



return 0;

}

Листинг 111 Демонстрация идентификации локальных переменных

Результат компиляции компилятора Microsoft Visual C++6.0 с настройками по умолчанию должен выглядеть так:

MyFunc       proc near           ; CODE XREF: main+1Cp

var_38       = byte ptr -38h

var_4        = dword      ptr –4

; Локальные переменные располагаются по отрицательному смещению относительно EBP,

; а аргументы функции – по положительному.

; Заметьте также, чем "выше" расположена переменная, тем больше модуль ее смещения

arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 38h

; Уменьшаем значение ESP на 0x38, резервируя 0x38 байт под локальные переменные

mov    eax, [ebp+arg_0]

; загружаем а EAX значение аргумента arg_0

; О том, что это аргумент, а не нечто иное, говорит его положительное

; смещение относительно регистра EBP

add    eax, [ebp+arg_4]

; складываем EAX со значением аргумента arg_0

mov    [ebp+var_4], eax

; А вот и первая локальная переменная!

; На то, что это именно локальная переменная, указывает ее отрицательное

; смещение относительно регистра EBP. Почему отрицательное? А посмотрите,

; как IDA определила "var_4"

; По моему личному мнению, было бы намного нагляднее если бы отрицательные

; смещения локальных переменных подчеркивались более явно.

push   10h          ; int

; Передаем функции ltoa значение 0x10 (тип системы исчисления)

lea    ecx, [ebp+var_38]

; Загружаем в ECX указатель на локальную переменную var_38

; Что это за переменная? Прокрутим экран дизассемблера немного вверх,

; там где содержится описание локальных переменных, распознанных IDA

; var_38     = byte ptr -38h

; var_4             = dword      ptr –4

;

; Ближайшая нижняя переменная имеет смещение –4, а var_38, соответственно, -38

; Вычитая из первого последнее получаем размер var_38



; Он, как нетрудно подсчитать, будет равен 0x34

; С другой стороны, известно, что функция ltoa

ожидает указатель на char*

; Таким образом, в комментарии к var_38 можно записать "char s[0x34]"

; Это делается так: в меню "Edit" открываем подменю "Functions", а в нем –

; пункт "Stack variables" или нажимаем "горячую" комбинацию <Ctrl-K>

; Открывается окно с перечнем всех распознанных локальных переменных.

; Подводим курсор к "var_34" и нажимаем <;> для ввода повторяемого комментария

; и пишем нечто вроде "char s[0x34]". Теперь <Ctrl-Enter> для завершения ввода

; и <Esc> для закрытия окна локальных переменных.

; Все! Теперь возле всех обращений к var_34 появляется введенный нами

; комментарий

;

push   ecx          ; char *

; Передаем функции ltoa указатель на локальный буфер var_38

mov    edx, [ebp+var_4]

; Загружаем в EDX значение локальной переменной var_4

push   edx          ; __int32

; Передаем значение локальной переменной var_38 функции ltoa

; На основании прототипа этой функции IDA

уже определила тип переменной – int

; Вновь нажмем <Ctrl-K> и прокомментируем var_4

call   __ltoa

add    esp, 0Ch

; Переводим содержимое var_4 в шестнадцатеричную систему исчисления,

; записанную в строковой форме, возвращая ответ в локальном буфере var_38

lea    eax, [ebp+var_38]          ; char s[0x34]

; Загружаем в EAX указатель на локальный буфер var_34

push   eax

; Передаем указатель на var_34 функции printf для вывода содержимого на экран

mov    ecx, [ebp+var_4]

; Копируем в ECX значение локальной переменной var_4

push   ecx

; Передаем функции printf значение локальной переменной var_4

push   offset aXS   ; "%x == %s == "

call   _printf

add    esp, 0Ch

mov    eax, [ebp+var_4]

; Возвращаем в EAX значение локальной переменной var_4

mov    esp, ebp



; Освобождаем память, занятую локальными переменными

pop    ebp

; Восстанавливаем прежнее значение EBP

retn

MyFunc       endp

main         proc near           ; CODE XREF: start+AFp

var_C        = dword      ptr -0Ch

var_8        = dword      ptr -8

var_4        = dword      ptr –4

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 0Ch

; Резервируем 0xC байт памяти для локальных переменных

mov    [ebp+var_4], 666h

; Инициализируем локальную переменную var_4, присваивая ей значение 0x666

mov    [ebp+var_8], 777h

; Инициализируем локальную переменную var_8, присваивая ей значение 0x777

; Смотрите: локальные переменные расположены в памяти в обратном порядке

; их обращения к ним! Не объявления, а именно обращения!

; Вообще-то, порядок расположения не всегда бывает именно таким, - это

; зависит от компилятора, поэтому, полагаться на него никогда не стоит!

mov    eax, [ebp+var_8]

; Копируем в регистр EAX значение локальной переменной var_8

push   eax

; Передаем функции MyFunc значение локальной переменной var_8

mov    ecx, [ebp+var_4]

; Копируем в ECX значение локальной переменной var_4

push   ecx

; Передаем MyFunc значение локальной переменной var_4

call   MyFunc

add    esp, 8

; Вызываем MyFunc

mov    [ebp+var_C], eax

; Копируем возращенное функцией значение в локальную переменную var_C

lea    edx, [ebp+var_C]

; Загружаем в EDX указатель на локальную переменную var_C

push   edx

; Передаем функции printf указатель на локальную переменную var_C

push   offset asc_406040 ; "%x\n"

call   _printf

add    esp, 8

xor    eax, eax

; Возвращаем нуль

mov    esp, ebp

; Освобожаем память, занятую локальными переменными

pop    ebp

; Закрываем кадр стека

retn

main         endp

Листинг 112

Не очень сложно, правда? Что ж, тогда рассмотрим результат компиляции этого примера компилятором Borland C++ 5.0 – это будет немного труднее!



MyFunc       proc near           ; CODE XREF: _main+14p

var_34       = byte ptr -34h

; Смотрите, - только одна локальная переменная! А ведь мы объявляли целых три...

; Куда же они подевались?! Это хитрый компилятор поместил их в регистры, а не стек

; для более быстрого к ним обращения

; (подробнее см. "Идентификация регистровых и временных переменных")

push   ebp

mov    ebp, esp

; Открываем кадр стека

add    esp, 0FFFFFFCC

; Резервируем... нажимаем <-> в IDA, превращая число в знаковое, получаем "–34"

; Резервируем 0x34 байта под локальные переменные

; Обратите внимание: на этот раз выделение памяти осуществляется не SUB, а ADD!

push   ebx

; Сохраняем EBX в стеке или выделяем память локальным переменным?

; Поскольку память уже выделена инструкцией ADD, то в данном случае

; команда PUSH действительно сохраняет регистр в стеке

lea    ebx, [edx+eax]

; А этим хитрым сложением мы получаем сумму EDX

и EAX

; Поскольку, EAX и EDX не инициализировались явно, очевидно, через них

; были переданы аргументы (см. "Идентификация аргументов функций")

push   10h

; Передаем функции ltoa выбранную систему исчисления

lea    eax, [ebp+var_34]

; Загружаем в EAX указатель на локальный буфер var_34

push   eax

; Передаем функции ltoa указатель на буфер для записи результата

push   ebx

; Передаем сумму (не указатель!) двух аргументов функции MyFunc

call   _ltoa

add    esp, 0Ch

lea    edx, [ebp+var_34]

; Загружаем в EDX указатель на локальный буфер var_34

push   edx

; Передаем функции printf указатель на локальный буфер var_34, содержащий

; результат преобразования суммы аргументов MyFunc

в строку

push   ebx

; Передаем сумму аргументов функции MyFunc

push   offset aXS   ; format

call   _printf

add    esp, 0Ch

mov    eax, ebx

; Возвращаем сумму аргументов в EAX

pop    ebx

; Выталкиваем EBX из стека, восстанавливая его прежнее значение



mov    esp, ebp

; Освобождаем память, занятную локальными переменными

pop    ebp

; Закрываем кадр стека

retn

MyFunc       endp

; int __cdecl main(int argc,const char **argv,const char *envp)

_main        proc near           ; DATA XREF: DATA:00407044o

var_4        = dword      ptr –4

; IDA

распознала по крайней мере одну локальную переменную –

; возьмем это себе на заметку.

argc         = dword      ptr  8

argv         = dword      ptr  0Ch

envp         = dword      ptr  10h

push   ebp

mov    ebp, esp

; Открываем кадр стека

push   ecx

push   ebx

push   esi

; Сохраняем регистры в стеке

mov    esi, 777h

; Помещаем в регистр ESI значение 0x777

mov    ebx, 666h

; Помещаем в регистр EBX значение 0x666

mov    edx, esi

mov    eax, ebx

; Передаем функции MyFunc аргументы через регистры

call   MyFunc

; Вызываем MyFunc

mov    [ebp+var_4], eax

; Копируем результат, возвращенный функцией MyFunc

в локальную переменную var_4

; Стоп! Какую такую локальную переменную?! А кто под нее выделял память?!

; Не иначе – как из одна команд PUSH. Только вот какая?

; Смотрим на смещение переменной – она лежит на четыре байта выше EBP, а эта

; область памяти занята содержимым регистра, сохраненного первым PUSH,

; следующим за открытием кадра стека.

; (Соответственно, второй PUSH кладет значение регистра по смещению –8 и т.д.)

; А первой была команда PUSH ECX, - следовательно, это не никакое не сохранение

; регистра в стеке, а резервирование памяти под локальную переменную

; Поскольку, обращений к локальным переменным var_8 и var_C не наблюдается,

; команды PUSH EBX и PUSH ESI, по-видимому, действительно сохраняют регистры

lea    ecx, [ebp+var_4]

; Загружаем в ECX указатель на локальную переменную var_4

push   ecx

; Передаем указатель на var_4 функции printf

push   offset asc_407081 ; format

call   _printf

add    esp, 8



xor    eax, eax

; Возвращаем в EAX нуль

pop    esi

pop    ebx

; Восстанавливаем значения регистров ESI

и EBX

pop    ecx

; Освобождаем память, выделенную локальной переменной var_4

pop    ebp

; Закрываем кадр стека

retn

_main        endp

Листинг 113

__дописать модификация локальной переменной из другого потока

FPO - Frame Pointer Omission Традиционно для адресации локальных переменных используется регистр EBP. Учитывая, что регистров общего назначения всего семь, "насовсем" отдавать один из них локальным переменным очень не хочется. Нельзя найти какое-нибудь другое, более элегантное решение?

Хорошенько подумав, мы придем к выводу, что отдельный регистр для адресации локальных переменных вообще не нужен, - достаточно (не без ухищрений, правда) одного лишь ESP – указателя стека.

Единственная проблема – плавающий кадр стека. Пусть после выделения памяти под локальные переменные ESP указывает на вершину выделенного региона. Тогда, переменная buff

(см. рис 17) окажется расположена по адресу ESP+0xC. Но стоит занести что-нибудь в стек (аргумент вызываемой функции или регистр на временное сохранение), как кадр "уползет" и buff окажется расположен уже не по ESP+0xC, а – ESP+0x10!



Рисунок 17 0х004 Адресация локальных переменных через регистр ESP приводит к образованию плавающего кадра стека

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

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


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

стремиться понять, как это работает!

Рассмотрим наш старый добрый simple.c, откомпилировав его с ключом "/O2" – оптимизация по скорости. Тогда компилятор будет стремиться использовать все регистры и адресовать локальные переменные через ESP, что нам и надо.

>cl sample.c /O2

  00401000: 83 EC 64           sub         esp,64h

Выделяем память для локальных переменных. Обратите внимание – теперь уже нет команд PUSH EBP\MOV EBP,ESP!

  00401003: A0 00 69 40 00     mov         al,[00406900] ; mov al,0

  00401008: 53                 push        ebx

  00401009: 55                 push        ebp

  0040100A: 56                 push        esi

  0040100B: 57                 push        edi

Сохраняем регистры

  0040100C: 88 44 24 10        mov         byte ptr [esp+10h],al

Заносим в локальную переменную [ESP+0x10] (назовем ее buff) значение ноль

  00401010: B9 18 00 00 00     mov         ecx,18h

  00401015: 33 C0              xor         eax,eax

  00401017: 8D 7C 24 11        lea         edi,[esp+11h]

Устанавливаем EDI на локальную переменную [ESP+0x11] (неинициализированный хвост buff)

  0040101B: 68 60 60 40 00     push        406060h ; "Enter password"

Заносим в стек смещение строки "Enter password". Внимание! Регистр ESP теперь уползает на 4 байта "вверх"

  00401020: F3 AB              rep stos    dword ptr [edi]

  00401022: 66 AB              stos        word ptr [edi]

  00401024: 33 ED              xor         ebp,ebp

  00401026: AA                 stos        byte ptr [edi]

Обнуляем буфер

  00401027: E8 F4 01 00 00     call        00401220

Вывод строки "Enter password" на экран. Внимание!

Аргументы все еще не вытолкнуты из стека!

  0040102C: 68 70 60 40 00     push        406070h



Заносим в стек смещение указателя на указатель stdin. Внимание! ESP еще уползает на четыре байта вверх.

  00401031: 8D 4C 24 18        lea         ecx,[esp+18h]

Загружаем в ECX указатель на переменную [ESP+0x18]. Еще один буфер? Да как бы не так! Это уже знакомая нам переменная [ESP+0x10], но "сменившая облик" за счет изменения ESP. Если из 0x18

вычесть 8 байт на которые уполз ESP – получим 0x10, - т.е. нашу старую знакомую – [ESP+0x10]!

Крохотную процедуру из десятка строк "проштудировать" несложно, но вот на программе в миллион строк можно и лапти скинуть! Или… воспользоваться IDA. Посмотрите на результат ее работы:

.text:00401000 main            proc near               ; CODE XREF: start+AFvp

.text:00401000

.text:00401000 var_64          = byte ptr -64h

.text:00401000 var_63          = byte ptr -63h

IDA обнаружила две локальные переменные, расположенные относительно кадра стека по смещениям 63 и 64, оттого и названных соответственно: var_64 и var_63.

.text:00401000                 sub     esp, 64h

.text:00401003                 mov     al, byte_0_406900

.text:00401008                 push    ebx

.text:00401009                 push    ebp

.text:0040100A                 push    esi

.text:0040100B                 push    edi

.text:0040100C                 mov     [esp+74h+var_64], al

IDA автоматически подставляет имя локальной переменной к ее смещению в кадре стека

.text:00401010                 mov     ecx, 18h

.text:00401015                 xor     eax, eax

.text:00401017                 lea     edi, [esp+74h+var_63]

Конечно, IDA не смогла распознать инициализацию первого байта буфера и ошибочно приняла его за отдельную переменную, – но это не ее вина, а компилятора! Разобраться – сколько переменных тут в действительности может только человек!

.text:0040101B                 push    offset aEnterPassword ; "Enter password:"

.text:00401020                 repe stosd

.text:00401022                 stosw

.text:00401024                 xor     ebp, ebp

.text:00401026                 stosb

.text:00401027                 call    sub_0_401220

.text:0040102C                 push    offset off_0_406070

.text:00401031                 lea     ecx, [esp+7Ch+var_64]

Обратите внимание – IDA правильно распознала обращение к нашей переменной, хотя ее смещение – 0x7C – отличается от 0x74!


Содержание раздела