Идентификация локальных стековых переменных
…общая масса бактерий гораздо больше, чем наша с вами суммарная масса. Бактерии - основа жизни на земле…
А.П. Капица
Локальные переменные размещаются в стеке
(так же называемым автоматической памятью) и удаляются оттуда вызываемой функцией по ее завершению. Рассмотрим подробнее: как это происходит. Сначала в стек затягиваются аргументы, передаваемые функции (если они есть), а сверху на них кладется адрес возврата, помещаемый туда инструкцией 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 |
Идентификация механизма выделения памяти. Выделение памяти инструкциями 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!