Win32ASM: Консольный ввод, томограф IDA и скальпель SoftICE


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

Содержание

  • Консольный ввод и томограф IDA
  • Консольный ввод и скальпель SoftIce
  • Стек и локальные переменные

Консольный ввод и томограф IDA

  #1. В прошлый раз мы разобрались с консольным выводом. Сегодня — разберемся с консольным вводом. Для этого напишем простую программу, запрашивающую строку символов, а затем её же (строку) и выводящую. Это будет очередной маленький шажок, который для вас вполне может стать решающим, так как именно эта программа впоследтвии послужит нам жертвой для негуманных экспериментов, в которых мы впервые используем два хирургических инструмента — отладчик SoftIce (на правах скальпеля) и дизассемблер IDA Pro (на правах томографа). И если при виде окровавленных внутренностей программы вам не поплохеет, более того — если вам ЭТО понравится, значит вы имеете неплохие шансы стать либо серийным маньяком (крэкером), либо — исследователем программ (реверсером). Все же остальные профессии отныне перестанут для вас существовать 😉
Итак, открываем ASM Editor и набиваем там следующий текст:

      .386
      .model flat, stdcall
      option casemap :none   ; case sensitive

 ; ######################################################

      include \tools\masm32\include\windows.inc
      include \tools\masm32\include\kernel32.inc
      includelib \tools\masm32\lib\kernel32.lib

 ; ######################################################

    .data

      Msg1         db "Type something > "
      Msg2         db "You typed > "
      ConsoleTitle db 'Input & Output',0


 ; ######################################################

    .code

 ; ######################################################

 Main proc
   LOCAL InputBuffer[128] :BYTE  ;буффер для ввода
   LOCAL hOutPut          :DWORD ;хэндл для вывода
   LOCAL hInput           :DWORD ;хэндл для ввода
   LOCAL lpszBuffer       :DWORD ;адрес буфера
   LOCAL nRead            :DWORD ;прочитано байт
   LOCAL nWriten          :DWORD ;напечатано байт

   ;устанавливаем титл окна
   invoke SetConsoleTitle, addr ConsoleTitle

   ;получаем хэндл для вывода
   invoke GetStdHandle, STD_OUTPUT_HANDLE
   mov hOutPut, eax

   ;печатаем "Type something > "
   invoke WriteConsole, hOutPut, addr Msg1, 17, addr nWriten,NULL

   ;получаем хэндл для ввода
   invoke GetStdHandle,STD_INPUT_HANDLE
   mov hInput, eax

   ;вводим
   invoke ReadConsole, hInput, addr InputBuffer, 10, ADDR nRead, NULL

   ;печатаем "You typed > "
   invoke WriteConsole, hOutPut, addr Msg2, 12, addr nWriten, NULL

   ;печатаем то, что ввели
   invoke WriteConsole, hOutPut, addr InputBuffer, nRead, addr nWriten, NULL

   ;задержка, чтобы полюбоваться
   invoke Sleep, 2000d

   ;выход
   invoke ExitProcess,0
 Main endp


 ; ######################################################

    end Main

ПРИМЕЧАНИЕ: Ручной подсчет числа выводимых символов хорошим стилем программирования, конечно же, не назовешь :(. Немного попозже я расскажу, как делать это дело правильно ;). И да простят меня продвинутые программеры…

Строка LOCAL InputBuffer[128] :BYTE резервирует 128 байт памяти под строку символов, которую мы будем запрашивать при помощи апишной функции ReadConsole. Вот ее описание:

 BOOL ReadConsole(
   HANDLE hConsoleInput,        // handle to console input buffer
   LPVOID lpBuffer,             // data buffer
   DWORD nNumberOfCharsToRead,  // number of characters to read
   LPDWORD lpNumberOfCharsRead, // number of characters read
   LPVOID lpReserved            // reserved
 );

Попутно даю урок английского языка, который сам знаю хреново (академиев не кончал, но высшее образование вам даду): «number of characters to read» переводится как «число символов, подлежащих чтению», а «number of characters read» — как «число прочитанных символов». Наверное. Согласитесь, это существенная разница ;).

#2. Далее обратите внимание, что адрес переменной мы получаем не при помощи offset, а при помощи addr. Все их различие заключается в том, что addr может работать с локальными переменными, а вот offset — нет.
Открою вам страшную тайну! На самом деле локальная переменная — это всего лишь зарезервированное место в стеке. Когда компилятор встречает addr, он сначала проверяет локальная это переменная или глобальная. Если глобальная, он помещает адрес этой переменной в объектный файл, то есть работает аналогично offset. Если же это локальная переменная, то перед вызовом функции генерируется следующая последовательность инструкций:

 lea eax, LocalVar
 push eax

Как обычно, я надеюсь на то, что вы не поверили мне на слово. Мы обязательно разберемся с тем, как происходит резервирование места в стеке и обращение к локальным переменным, но сделаем немного позже. Для начала нам нужно хотя бы чуть-чуть ознакомиться с инструментарием. Начнем мы, пожалуй, с IDA — The Interactive Disassembler.

#3. IDA относится и интенсивно развивающимся продуктам, то есть вследствие постоянного совершенствования даже близкие версии могут вести себя по-разному. А по сему оговорюсь: описываемая мной последовательность действий рассчитана на версию 4.1.5.520, самым честным образом купленную. Если у вас другая версия, и мои советы «не проходят», то: во-первых, я не виноват, а во-вторых — для разнообразия попробуйте не только тупо следовать руководству, но еще и головой думать (сорри за грубость).
Что касается дизассемблирования вообще, то тут четко необходимо уяснить для себя одну вещь: вследствие того, что ассемблирование — это однонаправленный процесс с потерями, автоматическое восстановление исходного текста невозможно. Хотя, казалось бы, чего тут сложного — перевод двоичного кода процессора в удобночитаемые мнемоники… а фиг вам, задачка еще та!
Существуют две категории дизассемблеров: автономные и интерактивные. Автономные требуют у юзверя все необходимые им указания до начала процесса дизассемблирования и не позволяют вмешиваться в сам процесс. Соответственно, если результат нас не устраивает или мы желаете попробовать какую-либо дополнительную фичу из предоставляемых дизассеблером, то весь процесс (а для больших программ он может длиться часами!) придется повторять, и, скорее всего, не один раз.
А вот интерактивные позволяют «вручную» управлять процессом «препарирования» программы. В любой момент мы можем сказать дизассемблеру «парень, ты гонишь» и помочь этому парню, например, отличить адреса от констант либо определить границы инструкций и т. д. Соответственно, интерактивные дизассемблеры имеют хорошо развитый пользовательский интерфейс, а некоторые (не буду показывать пальцем, все наверняка уже догадались, какой дизассемблер я имею в виду) имеют даже собственный си-подобный язык скриптов! И более даже более того — являются виртуальной программируемой машиной!

#4. Итак, давайте проведем первое знакомство с этим плодом человеческого гения… Запускаем ИДУ и озадачиваем ее нашим исполнимым файлом (File > Open).
Вот что мы увидим:

Я не менял настройки, оставил все дефолтом. Но посмотрите, например, на список Processor type, разве он не впечатляет? Жмем на ОК и получаем дизассемблированный листинг нашей программы.
Наверняка вы некоторое время потягаете вверх-вниз вертикальный скроллбар и помедитируете над полученным результатом ;). И только потом начнете читать дальше… Что ж, совершенно правильное поведение ;)) Именно это я называю «медитацией» 😉
Теперь пойдем дальше…

Лезем в пункт меню Views, и знакомимся с некоторыми его подпунктами. Сразу же совет: запоминайте горячие клавиши того или иного пункта меню. Это сэкономит вам кучу времени 😉
Итак, View > Toggle dump view переключит режим отображения из дизассемблированного листинга в режим дампа, то есть мы увидим простыню шестнадцатеричных циферек:

 .text:00401000  55 8B EC 81 C4 6C FF FF-FF 68 1D 30 40 00 E8 A7 "UEuA-l   h_0@.oc"
 .text:00401010  00 00 00 6A F5 E8 94 00-00 00 89 85 7C FF FF FF "...j?oO...EA|   "
 .text:00401020  6A 00 8D 85 6C FF FF FF-50 6A 11 68 00 30 40 00 "j.IAl   Pj_h.0@."
 .text:00401030  FF B5 7C FF FF FF E8 8B-00 00 00 6A F6 E8 6C 00 " ¦|   oE...j?ol."
 ...

Не правда ли, до боли знакомо? Переключаемся назад в режим листинга нажатием клавиши F4.

View > Open subview > Functions выдаст нам окошко со всеми имеющимися в программе функциями:

 

Изображение отсутсвует

 

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

Кликаем View > Open subviews > Segments и видим следующую картинку:

Как вы уже, должно быть, догадались, это окошко показывает нам, из каких секций состоит дизассемблированная программа.
.text — эта секция содержит исполняемый код. Благодаря 32-битной плоской адресации содержимое аналогичных секций всех объектных файлов, подаваемых на вход линкера, собирается в одной секции .text исполняемого PE-файла.
.idata — содержит данные об импортируемых приложением функциях, то бишь таблица импорта. Эта таблица состоит из 1) массива с описанием используемых DLL’ок, 2) двух массивов с адресами импортируемых функций и 3) массива имен импортируемых функций. Страшно?! Не переживайте, эту секцию мы еще рассмотрим детально… пока что просто знайте, что такая есть 😉
.rdata — содержит данные, доступные только для чтения, как-то: литеральные строки, константы, отладочную информацию… Это тоже не берите в голову, попозже мы копнем глубже и эту секцию ;).
.data — содержит инициализированные и глобальные переменные. Как и для секции .text одержимое .data-секций всех объектных файлов, подаваемых на вход линкера, собирается в одной секции .data исполняемого PE. На всякий случай напомню, что локальные переменные в этой секции вы не найдете.

#5. Двойной щелчок по той или иной секции переместит указатель на то место дизассемблированного листинга, где эта секция начинается.
Делаем кликаем .data и перемещаемся вот в это место нашего листинга:

 .data:00403000 ; Segment type: Pure data
 .data:00403000 _data           segment para public 'DATA' use32
 .data:00403000                 assume cs:_data
 .data:00403000                 ;org 403000h
 .data:00403000 unk_403000      db  54h ; T     ; DATA XREF: start+2B^o
 .data:00403001                 db  79h ; y
 .data:00403002                 db  70h ; p
 .data:00403003                 db  65h ; e
 .data:00403004                 db  20h ;
 .data:00403005                 db  73h ; s
 .data:00403006                 db  6Fh ; o
 .data:00403007                 db  6Dh ; m
 .data:00403008                 db  65h ; e
 .data:00403009                 db  74h ; t
 .data:0040300A                 db  68h ; h
 .data:0040300B                 db  69h ; i
 .data:0040300C                 db  6Eh ; n
 .data:0040300D                 db  67h ; g
 .data:0040300E                 db  20h ;
 .data:0040300F                 db  3Eh ; >
 .data:00403010                 db  20h ;
 .data:00403011 unk_403011      db  59h ; Y      ; DATA XREF: start+6D^o
 .data:00403012                 db  6Fh ; o
 .data:00403013                 db  75h ; u
 .data:00403014                 db  20h ;
 .data:00403015                 db  74h ; t
 .data:00403016                 db  79h ; y
 .data:00403017                 db  70h ; p
 .data:00403018                 db  65h ; e
 .data:00403019                 db  64h ; d
 .data:0040301A                 db  20h ;
 .data:0040301B                 db  3Eh ; >
 .data:0040301C                 db  20h ;
 .data:0040301D unk_40301D      db  49h ; I       ; DATA XREF: start+9^o
 .data:0040301E                 db  6Eh ; n
 .data:0040301F                 db  70h ; p
 .data:00403020                 db  75h ; u
 .data:00403021                 db  74h ; t
 .data:00403022                 db  20h ;
 .data:00403023                 db  26h ; &
 .data:00403024                 db  20h ;
 .data:00403025                 db  4Fh ; O
 .data:00403026                 db  75h ; u
 .data:00403027                 db  74h ; t
 .data:00403028                 db  70h ; p
 .data:00403029                 db  75h ; u
 .data:0040302A                 db  74h ; t
 .data:0040302B                 db    0 ;
 .data:0040302C                 align 200h
 .data:0040302C _data           ends
 .data:0040302C
 .data:0040302C
 .data:0040302C                 end start...

Что мы видим? Длиннющую простыню из байтов! Ида нам подсказывает, какой символ соответствует тому или иному шестнадцатеричному значению. Не нужно быть семи пядей во лбу, чтобы догадаться, что эта простыня соответствует следующему куску нашего исходника:

 .data

      Msg1         db "Type something > "
      Msg2         db "You typed > "
      ConsoleTitle db 'Input & Output',0

То есть unk_403000 (смотрим на дизассемблированный листинг) — это глобальная переменная Msg1 (смотрим на исходник нашей программы), unk_403011 — это Msg2, а unk_40301D — это ConsoleTitle.
Теперь посмотрим на нашу дизассемблированную секцию данных. Напротив каждой метки имеется комментарий наподобие DATA XREF: start+2B^o. Но это не просто комментарий — это перекрестная ссылка, которая свидетельствует о том, что к текущему адресу произошло обращение из процедуры start. Более того, указывается и адрес, по которому происходит обращение — смещение в 2Bh он начала процедуры start. Стрелка указывает на относительное расположение источника перекрестной ссылки, а буква «о» cвидетельствует о том, что обращение произошло по смещению (offset).
Теперь о главном. Если есть ссылка, то по ней можно (и нужно!) куда-нибудь проследовать, как по обычной html-ной гиперссылке, 😉 Итак, перемещаем указатель на слово start+2B^o в комментарии и жмем на Enter!
Перепрыгиваем на следующую строчку:

 .text:0040102B  push    offset unk_403000 ; lpBuffer

И в самом деле, мы видим, что обращение к переменной (пихание оной в стек) происходит по смещениию 2B при помощи префикса offset. И тут же видим ну вообще потрясающую вещь — IDA смекнула, что череда пушей перед call WriteConsoleA — это передача параметров соответствующей апишной функции, проанализировала там чего-то… и решила, что эта переменная — lpBuffer, совсем как в MSDN’овском описании функции WriteConsole! А ниже и хорошо знакомые нам переменные lpReserved, lpNumberOfCharsWritten, nNumberOfCharsToWrite, hConsoleOutput. Не правда ли, впечатляет? Сравните с листингами, генерируемыми другими дизассемблерами и вы поймете, что Иду не зря называют седьмым чудом света 😉
Перемещаем указатель на unk_403000, жмем на Enter и перепрыгиваем назад в секцию данных.

#6. Честно говоря, мне не нравится то, какой простыней Ида дизассемблировала блок данных. Хотелось бы лицезреть его в таком виде, каков он был в исходном тексте ;). Для этого ставим указатель на последний db в секции данных и жмем на правую кнопку мыши, а вывалившейся контекстной менюшке

выбираем «s», то бишь «переопределить в строку». В итоге получим крАсивую, и, что самое главное, сходу понятную, строчку:

 .data:0040301D aInputOutput db 'Input & Output',0   ; DATA XREF: start+9_o

Проделаем такую же процедуру и с двумя «вышележащими» метками, получив в итоге следующее отображение секции данных:

 .data:00403000 _data           segment para public 'DATA' use32
 .data:00403000                 assume cs:_data
 .data:00403000                 ;org 403000h
 .data:00403000 aTypeSomething  db 'Type something > '  ; DATA XREF: start+2B_o
 .data:00403011 aYouTyped       db 'You typed > '       ; DATA XREF: start+6D_o
 .data:0040301D aInputOutput    db 'Input & Output',0   ; DATA XREF: start+9_o
 .data:0040302C                 align 200h
 .data:0040302C _data           ends

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

  И можно вводить любые ругательные слова, которые только придут на ум 😉

#7. Обратите внимание вот на что:
Когда переменные были помечены как unk_403000, unk_403011 и unk_40301D, то обращение к ним осуществлялось следующим образом:

 ...
 .text:00401009 push offset unk_40301D ; lpConsoleTitle
 ...
 .text:0040102B push offset unk_403000 ; lpBuffer
 ...
 .text:0040106D push offset unk_403011 ; lpBuffer
 ...

Когда мы переопределили данные из байтовых в строковые, то Ида переобозвала переменные в aTypeSomething, aYouTyped и aInputOutput, а обращение стало производиться уже к совершенно другим «именам собственным».
А вот если бы мы переопределили данные начиная не с последней метки, а с первой, то получилась бы строка:

 .data:00403000 aTypeSomethingY db 'Type something > You typed > Input & Output',0

А обращение к ней осуществлялось бы вот как:

 ...
 .text:00401009  push    (offset aTypeSomethingY+1Dh) ; lpConsoleTitle
 ...
 .text:0040102B  push    offset aTypeSomethingY ; lpBuffer
 ...
 .text:0040106D  push    (offset aTypeSomethingY+11h) ; lpBuffer
 ...

Как я уже говорил, на сегодняшний день не существует ни одного полностью автоматического дизассемблера, способного генерировать безупречно работоспособный листинг (вырожденые примеры наподобие нашего — не в счет), поэтому в той или иной мере, но доводить его готовности приходится человеку. Что мы помаленьку и будем учиться делать.

За сим будем считать что первое знакомство с Идой состоялось, а в качестве домашнего задания — попробуйте диассемблировать нашу программу Sourcer’ом и WinDASM’ом, чтобы, как говориться, почувствовать разницу 😉
Медитируйте!

Консольный ввод и скальпель SoftICE

  #1. Сначала был 8086-й процессор, операционная система DOS и отладчик debug фирмы Microsoft. Отладчик неудобный, со скромными возможностями — он был (однако, есть и будет!) пригоден разве что для разного рода низкоуровневых забав и изучения ассемблера ;). В то время отладчики росли подобно грибам после мягкого кислотного дождика. И с такой же скоростью уходили в забвение — ибо от своего мелкомягкого прототипа отличались лишь интерфейсом. Это было золотое время для разработчиков всемозможных защит, так как достаточно было «запереть» клавиатуру, запретить прерывания, сбросить флаг трассировки, и у незадачливого хакера надолго отпадала охота копаться в чужом исполняемом коде…
Потом был 80286-й, такие шедевры программирования как отладчики AFD Pro и Turbo Debugger… Золотое время разработчиков защит плавно перетекло в серебряное. А затем, с выходом на рынок фирмы NuMega и ее отладчика SoftIce, их жизнь вовсе превратилась в адский, неблагодарный, и, что самое важное, малоэффективный труд… Было вот как — разработчики защит были отдельными личностями либо небольшими фирмами, а за разработчиками SoftIce стояла намного более многочисленная группа людей, да не абы каких, а хакеров ;). Притом хакеров не в (увы!) современном понимании этого слова (т.е. прыщавых компутерных вандалов, постоянных покупателей клирасила), а в единственно верном его толковании, т.е. людей в первую очередь высокоинтеллектуальных и стремящихся во всем разобраться до мелочей… Поэтому нисколько не удивительно, что на появление 80386-го, который имел специальные механизмы, обеспечивающие контроль за исполнением кода на аппаратном уровне, NuMega отр! еагировала намного быстрее, чем противоположный лагерь, который «разобрался» с этими новыми возможностями только годы спустя; и как очевидно, при этом безнадежно проиграл «хакерским средствам».
А потом был Прозорливый Билли и его «принципиально новая архитектура», перед которой пасовали все существующие отладчики. Взамен им мелкософт предлагал свои, но ориентированы эти неповоротливые монстры были на программиста, отлаживающего собственный код, но никак не исполняемый файл, избавленный от отладочной информации. При этом документацию, необходимую для написания отладчиков под Windows, мелкомягкие не распространяли… некоторое время. И несмотря на то что конкурирующие фирмы все-таки вытянули ее посредством каленых клещей US’овской судебной системы, легче им от этого не стало — их отладчики все равно не превосходили мелкософтовский. Ибо это были отладчики под Windows. Неповоротливые отладчики под такой же неповоротливый виндовс.
NuMega же пошла своим путем, и повторить ее шедевр до сих пор никто не решается. Если операционная система не позволяет отлаживать программы — «забиваем» на операционную систему! Если стену не перепрыгнуть и не обойти, то почему бы ее не подкопать? Результат такого подхода оказался выше всяких похвал. «Виндозный» отладчик нумеги ни в чем не полагался на операционную систему, а опирался исключительно на аппаратное обеспечение и, вследствие этого, позволял отлаживать практически любую программу, в том числе и ядро операционной системы…
Что мы имеем в результате всего этого? Фирму NuMega — единственного «поставщика» высококачественных хакер-ориетированных иструментов под Windows, оперативно реагирующего на все изменения операционной системы от мелкософта…
Подробнее обо всем этом вы можете прочитать в книге К.Касперски «Техника и философия хакерских атак», настоятельно рекомендую (при всем своем уважении к автору, не могу не отметить, что второе издание этой книги, выпущенное издательством Солон, имеет отвратительный переплет — по первом же прочтении книга буквально развалилась на отдельные страницы).

#2. Как вы, должно быть, уже поняли, способ запуска сайса (именно так мы будем назвать SoftIce) несколько сложнее, чем всенародно любимого пасьянса «косынка». В Windows 9X для этого нужно было редактировать autoexec.bat и делать мультиконфигурацию, но в NТ с этим дела обстоят немного проще. Всю последовательность действий я привожу из предположения, что у читателя установлена операционная система Windows 2000 (которая, как известно, «build on NT technology»), английская, а сайс берется из пакета DriverStudio версии 2.5. При этом желающие «сходу» произвести полную установку этого пакета должны иметь в виду, что программа инсталляции попросит, помимо всего прочего, указать пути к Driver Development Kit ;).
Итак, после установки (и появления на панеле задач соответствующей ветки меню), первое, что мы должны сделать, это выбрать тип его загрузки. Для этого запускаем программу конфигурирования (Start > NuMega DriverStudio > SoftIce > Settings) и видим следующее окошко:

  Честно говоря, меня вполне устраивает ручной режим загрузки, то есть Manual — наверное, это потому что я еще не сталкивался с отладкой «core device driver» ;). Поэтому насчет остальных режимов ничего сказать не могу. Итак, ставим Manual ;).   Чтобы запустить сайс в этом режиме, необходимо ввести команду:

NET START NTICE

либо запустить «Start SoftICE» из менюшки (pif на обыкновенный батник, где эта команда написана).
Все! Отладчик запущен. Только вот не увидите вы его ни в Applications, ни в Processes (CTRL+ALT+DEL). Ни даже на экране — пока не нажмете на волшебную комбинацию клавиш Ctrl+D ;).
Жмем на Ctrl+D! Если отладчик установился успешно, то всплывет приблизительно следующая «картинка»:

--------------------------------------------------------------------------------
EAX=00005305   EBX=C4920074   ECX=C14698E4   EDX=00000000   ESI=C1476EC0
EDI=C49202B0   EBP=67890000   ESP=C4687E2C   EIP=000080D2   o d I s z a P c
CS=0128   DS=0030   SS=0030   ES=0030   FS=0078   GS=0030
--------------------------------------------------byte-------------------PROT16-
0030:00000000 9E 0F C9 D8 65 04 70 00-16 00 C9 09 65 04 70 00 ....e.p.....e.p.
0030:00000010 65 04 70 00 54 00 FF F0-58 7F 00 F0 FF E7 00 F0 e.p.T...........
0030:00000020 00 00 00 C9 D2 08 A3 0A-6F EF 00 F0 6F 00 F0 00 .........o...o..
0030:00000030 6F EF 00 F0 6F EF 00 F0-9A 00 C9 09 65 04 70 00 o...o.......e.p.
-----Cancel_Call_When_Idle+002C------------------------------------------PROT16-
0128:80D1  POPF
0128:80D2  CLS
0128:80D3  RETF
0128:80D4  POPF
0128:80D5  STC
0128:80D6  RETF
0128:80D7  CMP     AL,13
0128:80D9  NOP
0128:80DA  NOP
0128:80DB  JBE     80E1
--------------------------------------------------------------------------------
:rs
:g
WINICE: Free32  Obj=01 Mod=NOTEPAD
WINICE: Free32  Obj=02 Mod=NOTEPAD
WINICE: Free32  Obj=03 Mod=NOTEPAD
WINICE: Free32  Obj=04 Mod=NOTEPAD
WINICE: Free32  Obj=05 Mod=NOTEPAD
WINICE: Free16  Sel=351F
:X
--------------------------------------------------------------------------------
X, XFRAME, XG, XP, XRSET, XT                                            KERNEL32
--------------------------------------------------------------------------------

Сверху, как вы уже, должно быть, догадались, регистры. Чуть ниже — дамп памяти. Еще ниже — дизассемблированные команды процессора. Далее следуют окно диспетчера, в котором мы можем вводить команды и читать различные матюгальники, и контекстная подсказка.

#3. Не знаю, как на вашем дисплее, но на моем 17-дюймовом с разpешением 1024х768 окошко отладчика получилось больно уж маленьким. Чтобы не напрягать глаза, его можно немножко «под себя» настроить. Ок! Давайте попробуем ввести несколько команд, которые позволяют выполнить подобную настройку. Для этого пишем следующие команды:
SET FONT 2 — и шрифт в окошке немного увеличивается, как и сам размер окна;
LINES 60 — увеличиваем число строк в окне отладчика;
WD 22 — задаем число строк под дамп;
WC 25 — задаем число строк под код;
CODE ON — разрешить отображать байты инструкций.;
COLOR A A 20 20 2 — устанавливаем «извращенную» цветовую схему (подробнее о параметрах смотрите в SoftIce Command Reference, битовое кодирование цвета мы рассматривали).
Еще одна команда (я настоятельно рекомендую ее использовать, особенно пользователям W9X) — это FAULTS OFF, которая предотвращает «всплытие» отладчика при возникновении GPF — General Protection Fault, в просторечии также известную как «ваши ручки выполнили недопустимую операцию и будут ампутированы».

Теперь, когда мы настроили «под себя» внешний вид отладчика, давайте позаботимся о том, чтобы нам не нужно было при его запуске каждый раз вводить эти семь команд. Т.е. пропишем все эти команды в строку инициализации — простыню команд, которые автоматически будут выполняться при загрузке отладчика. Для этого ищем конфигурационный файл winice.dat (в 2000 я его нашел в Windows\system32\drivers\; в 9X, насколько я помню, он находился в том же каталоге, что и проинсталлированный SoftICE) и дополняем строчку INIT="X;" нашими командами («X;» в самом конце — это выход из окна сайса):

INIT="SET FONT 2; LINES 60; WD 22; WC 25; CODE ON; COLOR A A 20 20 2; FAULTS OFF; X;"

Далее раскомментируйте в winice.dat все строки наподобие

EXP=\SystemRoot\System32\kernel32.dll

Это необходимо для того чтобы сайс загрузил имена экспортируемых функций, находящихся в этих биб-лиотеках. Иначе вместо понятных команд, наподобие

call USER32!MessageBoxA

мы увидим безобразие типа

call 0044F2A1

Теперь нам нужно перезапустить отладчик, чтобы проверить, подхватывает ли сайс наши настройки. Для этого нужно… В общем, как я уже говорил, сайс — прога, весьма специфическая, и выгрузить ее «из компьютера» можно только одним способом — перезагрузкой ;). Start > Shutdow > Restart…

#4. Существуют две области применения сайса — отладка собственных программ и исследование чужих, так называемый reverse ingeneering. Для начала давайте научимся использовать сайс в качестве инструмента для отладки и изучения собственных приложений.
Что мы имеем в этом случае? Исходные тексты программы, и как следствие — возможность откомпилировать ее отладочную версию, т.е. тот же экзешник, но содержащий, помимо всего прочего, еще и кучу дополнительной информации. Как в самом исполняемом файле, так и в специально для этой цели сгенерированных дополнительных файлах.
Итак, мы должны:

1. Сассемблировать исходник с ключем Zi:

ml /c /coff /Zi src.asm

В результате этого мы получим объектный файл, содержащий отладочную информацию для отладчика CodeView. Легко заметить, что размер этого файла намного больше, чем у его «нормального» аналога.

2. Слинковать объектный файл с ключами /DEBUG и /DEBUGTYPE:CV

link.exe /SUBSYSTEM:CONSOLE /DEBUG /DEBUGTYPE:CV src.obj

После этого, помимо экзешника, мы получим отладочные файлы src.ilk и simple.pdb, Microsoft Linker Database и Microsoft C/C++ program database соответственно.

Теперь загружаем нашу отладочную версию программы в отладчик. Для этого запускаем Symbol Loader:

  В левом окне мы видим, из каких файлов подгружена символическая информация. Зеленая лампочка в строке статуса свидетельствует о том, что отладчик подгружен (конечно, если вы это сделали после перезагрузки). Правое окно — это окно отчета, там появляется информация о выполненных действиях, ошибках и т.д. Ах да…, еще есть заголовок окна, там помимо названия программы есть еще и надпись в скобках [No Module Opened], то есть «не открыт модуль». Не правда ли, не очень тонкий намек?

  • Жмем File > Open и открываем наш src.exe (отладочные файлы, как, желательно, и исходник, должны находиться в этом же каталоге). На первый взгляд ничего не изменилось, но посмотрите внимательно на заголовок программы — надпись «не открыт модуль» заменилась на «src.exe». Значит, что-то все-таки произошло ;).
  • Далее жмем Module > Setting, и всплывает диалоговое окно, в котором можно настроить все, что мы желаем сделать с модулем, перед тем как он будет загружен в отладчик. Настраиваем:
  • Из закладки General нам ничего не нужно — все поля оставляем пустыми, никаких галочек не ставим.
  • В Debugging выбираем Load Executeable («загрузить экзешник») и ставим галочку на Stop at WinMain, Main, DllMain, etc… т.е. «остановиться на точке входа».
  • В Translation выбираем Symbols only (included locals and structures), т.е. «только символическую информацию, включая локальные переменные и структуры».

Теперь мы готовы свершить самое главное действо — загрузить программу под отладчик. Жмем Module > Load, и вот оно, долгожданное! У нас всплывает окно сайса с указателем, установленным на точку входа в нашу программу:

 Main
 001B:00401010  55               PUSH EBP

#5. Для начала рассмотрим команду display memory (отобразить память). Ее синтаксис:

D[size] [address [l length]]

Если ее использовать без параметров, то просто будет отображаться следующая страница дампа. Необязательный параметр size — это размерность, в которой выводится дамп. Вот расшифровка его значений:

 b      Byte
 W      Word
 D      Double Word
 S      Short Real
 L      Long Real
 T      10-Byte Real

Размерности byte, word и double word вам, конечно же, хорошо знакомы. А вот загадочные short long и 10-byte real мы рассмотрим немного позже.
Обратите внимание на то, что первый параметр (размерность) мы должны писать «в одно слово» c, собственно, самой командой «d», т.е. — «db», «dw», «dd» и т.д.
Необязательный параметр address — это адрес памяти, дамп которого вы хотите получить. Притом вовсе необязательно писать адрес цифрами — «составные» адреса сайс также принимает «за милую душу».
Таким образом, если мы введем команду:

 DD EIP

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

 DW EIP L 1000

выведет в командную строку 1000 байтов памяти, начиная с текущего значения регистра EIP, группированных пословно, и вы будете вынуждены несколько раз нажать на any key, прежде чем все это просмотрите. По большому секрету скажу, что нажатие на ESC сразу же прервет просмотр.

#6. Пожалуй, одна из главных возможностей любого отладчика — это трассировка, которая позволяет выполнять программу пошагово. Итак, загрузим нашу программу в SymbolLoader, нажмем на Load, а в появившемся окне отладчика введем команду «p» (она же — клавиша F10). В результате этого выполнится один логический шаг нашей программы (program step). Под «логическим шагом» подразумевается, своего рода «поверхностная» трассировка, без входа в процедуры, циклы и т.д.
Команда «t» (она же — клавиша F8) выполняет трассировку одной инструкции (trace one instruction), с «заходом» во все функции, в том числе и апишные. Подобная трассировка — это очень длинный путь, поэтому у данной команды существует еще и два параметра:

 T [=start-address] [count]

Первый — это адрес, с которого вы желаете начать трассировку, а count — это число инструкций, которое сайс протрассирует, прежде чем остановиться (и не забывайте о том, что он «заходит» в апишные функции!).
Конечно же, трассировочные возможности сайса намного больше, чем те немногие, которые мы рассмотрели. Впоследствии мы обязательно ознакомимся со всем их многообразием — по мере необходимости ;).

#7. Еще одна полезная команда — это rs (она же F4), restore the program screen (восстановление экрана программы). А как же без этого? Ведь внешне работа программы заключается вовсе не в выполнении команд процессора и пересылках данных между регистрами ;).
Например в нашем случае программа печатает приглашение ввести строку символов. Мы можем оттрассировать программу до (включительно) строки:

001B:00401046   E8BD000000 CALL _WriteConsoleA

и затем нажать на F4, для того чтобы подсмотреть, действительно ли в консольном окне появилась строчка «Type something >». А затем нажать на any key и снова очутиться в отладчике.
Или же, например, выполнив апишную функцию

001B: 00401070 E881000000 CALL _ReadConsoleA

которая «просит» ввести строку символов, нажать на F4 и ввести необходимую строку символов, а по нажатию на Enter (типа «ввод закончен») снова оказаться в отладчике.

#8. Теперь, когда мы поверхностно ознакомились с «хакерскими» инструментами, пришло время вспомнить о цели, которую мы преследовали, начав знакомство с идой и сайсом. Напоминаю — мы хотели исследовать, каким образом происходит декларирование локальных переменных в стеке, и как происходит к ним обращение.

Стек и локальные переменные

  #1. Представьте себе картину: накачанный колесами и протеиновыми коктейлями шварценеггер бьет со всей дури по боксерской груше. Та отлетает в сторону, а потом по каким-то абсолютно нефизическим законам кинемотографа возвращается назад и бьет этому боксеру по морде лица, в результате чего шварценеггер отлетает на несколько метров, и, обязательно задев что-нибудь из мебели, размазывается соплями по стене — к неописуемому удовольствию зрителя. Посмотрев на такую картину, Станиславский бы сказал: «не верю»! А вот дZенствующей программер, увидя это безобразие, подумал бы: «ба! Да совсем как стек. Помницца, в одной из своих кулхацкерных прог я на похожие грабли как раз и напоролся».

Ранее мы уже разобрались с очередностью записи в стек и чтения из а него. Напомню, что доступ к стеку осуществляется в соответствии с принципом LIFO (Last In First Out – Последним Пришел, Первым Ушел). Однако это отнюдь не единственное, что нам нужно знать о стеке — конечно же, если мы не собираемся время от времени получать «отдачу» от «боксерской груши» ;).

Нам уже хорошо известно, что стек можно использовать для временного хранения данных – с его помощью мы выкручивались из такой проблемы, как недостаточное для полета нашей фантазии количество регистров. То есть мы временно сохраняли значения регистров в стеке, активно юзали их для наших нужд, а потом снова восстанавливали «статус кво», за исключением, в большинстве случаев, одного-единственного регистра, в котором хранился результат проделанной работы.

Также известно, что в инструкциях процессоров от Интел переменные (которые в памяти) не могут выступать в качестве приемника и источника одновременно, то есть инструкция:

 mov  [dwVar1],[dwVar2]

не проходит. А выкрутиться из такой ситуации можно двумя способами — либо использовать в качестве посредника регистр (которых, как всегда, мало):

 mov eax,[dwVar2]
 mov [dwVar1],eax

либо задействовав стек:

 push [dwVar2]
 pop  [dwVar1]

Кстати, недавно в почтовой рассылке RTFM_helpers прошло обсуждение того, как можно копировать из памяти в память — там было упомянуто, например, использование movs. А если подумать, можно найти и другие нетривиальные способы.

Для тех, кто ещё не понял. Вот этот кусок кода:

 push 1
 push 2
 push 3
 pop  eax
 pop  ebx
 pop  ecx

делает то же самое, что и следующий код (если только не считать разницу в скорости, размере кода и побочных эффектах):

 mov  eax,3
 mov  ebx,2
 mov  ecx,1

Сомневающиеся могут проверить под отладчиком:). Также попробуйте сравнить:

 push 1
 pop  eax

и психоделическое:

 sub  esp,4
 mov  dword ptr [esp],1
 mov  eax,[esp]
 add  esp,4

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

Что за esp такой? – спросите вы.

Пошире откройте глаза и слушайте – сейчас я поведаю вам страшные тайны. 😉

#2. Помните мою аналогию с блинами от штанги и вделанным в пол штырём, на который они надевались для хранения. Там еще был учитель физкультуры, который приставал к нашим неполовозрелым одноклассницам, в результате чего ему перебили нос. Так вот, адрес самого верхнего блина – это вершина стека и хранится он (адрес) в регистре esp/sp. Другими словами, вершина стека – это адрес последнего занесенного в стек элемента.

Давайте посмотрим на поведение esp при трассировке следующего кода:

 Main proc
  push 1
  push 2
  push 3
  pop  eax
  pop  ebx
  pop  ecx
  invoke ExitProcess,0
 Main endp

При загрузке мы видим, что регистр esp инициализирован значением 12FFC4 (в других условиях стартовое значение может быть другим, но суть от этого не меняется). Давайте выполним один шаг (команда «t» — trace). В результате этого в стек ляжет 1, а значение регистра esp поменяется на 12FFC0. Трассируем дальше и наблюдаем, как изменяется esp:

  push 1    ;esp=12FFC0
  push 2    ;esp=12FFBC
  push 3    ;esp=12FFB8
  pop  eax  ;esp=12FFBC
  pop  ebx  ;esp=12FFC0
  pop  ecx  ;esp=12FFC4

Протрассировав первые три строчки, мы можем сделать вывод, что стек «растет» в сторону младших адресов (12FFC0 > 12FFBC > 12FFB8, логично?), а шагом изменения регистра esp является 4. И это правильно, так как при 32-битном режиме адресации в стеке сохраняются двойные слова, они же — 4 байта (хотя допускается класть в стек также и 2-байтные слова — при этом шаг равен 2). К слову сказать: если бы мы писали под DOS (точнее, в реальном или виртуальном режиме), то стек у нас адресовался бы регистром sp и изменялся бы он на плюс-минус 2.

Обобщаем. Алгоритм работы команды push <источник> следующий:

  1. Уменьшение значения указателя стека esp/sp на 4/2.
  2. Запись значения источника по адресу ss:esp/sp (вершина стека).

Об алгоритме работы команды pop догадайтесь сами…

Для последователей дZена предлагаем тему для исследования: какое значение будет лежать в стеке после инструкции push esp. 🙂

ПРЕДУПРЕЖДЕНИЕ: ни в коем случае не принимайте результаты отдельных экспериментов в конкретных условиях за абсолютную истину, верную всюду и всегда!

Теперь, после вышесказанного, вы легко сообразите, какое значение примет регистр eax в следующем извращенном случае:

 push 1    ;esp=12FFC0
 push 2    ;esp=12FFBC
 push 3    ;esp=12FFB8
 add  esp,4
 pop  eax  ;esp=12FFC0
 pop  ebx  ;esp=12FFC4

[Правильный ответ – 2. :)]

#3. Есть ещё один регистр, ассоциируемый со стеком — ebp/bp, и описывается его функция так, что выговорить страшно – указатель базы кадра стека. Такое название этого регистра я нашел в книжке Юрова & Хорошенко «ASSEMBLER, учебный курс». Нет, конечно же, можно назвать калоши «мокроступами», а bitmap «двоично-точечной картинкой», но… «У меня нет слов, у меня есть только выражения в адрес того, кто заворачивает такие коленца» (C) Аркадий Белоусов. А по сему давайте заменим словосочетание «кадр стека» простым народным 🙂 словом «фрейм», а «указатель базы» заменим на просто «база» (или «указатель» — по вкусу).

В результате подобных терминологических «подстановок» получается следующая картина: есть у нас в компьютере некие «фреймы», располагаются они в стеке и адреса этих пока что непонятных нам штук завязаны с регистром ebp/bp.

«Дело в следующем». Любая процедура/функция в терминах любого процедурного языка имеет (может иметь) нуль и больше параметров и локальных переменных. Область памяти, создаваемая (выделяемая) при вызове процедур для аргументов и локальных переменных, и называется фреймом. А чтобы процедура могла быть рекусивной (т.е. могла вызывать саму себя или вызываться из других процедур, которые она вызывает) или реентерабельной (т.е. чтобы код процедуры мог использоваться в параллельных процессах), фреймы для неё должны размещаться в стекоподобной структуре данных — «стеке». А поскольку процедурные языки ближе к «естественному» мышлению, то в наборы инструкций процессоров и ассемблеры вводят поддержку подобных высокоуровневых конструкций.

Существуют нюансы и различия в реализациях, например:

  • Писюковые сишные компиляторы генерят код, в котором часть фрейма с аргументами после вызова процедуры удаляется вызывающим кодом, а в паскалевских соглашениях о вызове фрейм всегда удаляется самой процедурой (что экономит на размере кода, поэтому это соглашение было принято как стандартное в Windows).
  • В Паскале существует понятие локальных процедур, которые могут обращаться к параметрам и локальным переменным всех родительских (в смысле статического размещения, а не динамического порядка вызовов) процедур и при этом могут быть рекурсивными (родительские процедуры тоже могут быть рекурсивными, и доступ к переменным должен идти к последнему, активному экземпляру). Для поддержки этой идеи во фреймы добавляются указатели на родительские фреймы, обычно организованные в виде списка.
  • Фортран вообще язык не рекурсивный, поэтому IBM в реализации Фортрана на IBM/360 (где, кстати, нет поддержки стека) для каждой процедуры заводила фрейм статически, во время компиляции.

Подобные «нюансы» сущности фрейма, конечно же, не меняют. Приведены они по одной единственной причине – чтобы вы поняли некоторую условность такого термина как «фрейм».

Теперь, собсна, про ebp/ep. В случае стекового фрейма его адрес не фиксирован, поэтому адресация параметров и переменных в нём должна быть «базисно-индексной», относительно начала фрейма. На писюке под это идеально подошёл (или изначально проектировался) BP/EBP — в отличие от SP, он может служить базой, и также адресуется относительно SS.

Я знаю, что вы мало что поняли из всего этого бреда, а по сему будем познавать истину путем долгой и продолжительной медитации.

#4. Мы привыкли к тому, что извлекать данные из стека можно только повинуясь очередности LIFO. А что, если нам понадобилось обратиться к произвольному элементу стека? Один из возможных способов мы уже рассмотрели: это изменение значения регистра esp/sp плюс команда pop. Это далеко не совсем хорошая идея, и вам не стоит издеваться над стеком таким изощренным способом. Если, конечно же, вы не хотите уподобляться Штирлицу и Мюллеру, которые стреляли по очереди…

Напомню: регистр ebp/bp ведет себя приблизительно таким же образом, как и хорошо нам знакомый ebx/bx. То есть он может выполнять роль базы.

Напомню, что, например, следующий код:

 mov ebx,12FFC0h
 mov al,[ebx]

присвоит регистру AL значение байта по адресу 12FFC0 из сегмента, задаваемого регистром DS.

Точно таким же образом можно использовать и регистр ebp/bp. Говоря другими словами, это один из немногих регистров, которые можно «брать в квадратные скобки» не увеличивая при этом размер инструкции :). То есть (и это будут уже третьи слова) он позволяет работать с ячейкой памяти, адрес которой находится в регистре.

Проиллюстрирую это на простом примере. Допустим, занесли вы в стек разных параметров кучу:

 адрес     значение
 0012FFC4  77E7EB69
 0012FFC8  0047E4AC
 0012FFCC  0012DAB4
 0012FFD0  7FFDF080

И возжелалось вам в силу какой-нибудь нездоровой производственной необходимости прочитать «здесь и сейчаc», например, предпоследний элемент. В этом случае делаем вот что:

 mov ebp,esp
 mov eax,[ebp+4]

Расшифровываю. Мы принимаем адрес самого последнего из записанных в стек элемента за точку отсчета, и путем прибавления к этой «точке отсчета» четвёрок можно легко получить доступ к тому элементу стека, который нашей программерской душеньке возжелался. В принципе, в данном примере можно было бы использовать ESP вместо EBP, но, во-первых, должны же мы были показать использование EBP, 🙂 а, во-вторых, в больших фрагментах кода использование ESP непосредственно имеет свои недостатки (больший размер инструкций и необходимость отслеживать изменение вершины стека).

Вот именно такое безобразие и называется «организация произвольного доступа к данным внутри стека».

#5. Перед тем, как мы пойдем дальше и разберемся-таки с локальными переменными – пара слов для особо продвинутых:

В защищённом режиме (или в реальном режиме с префиксами смены разрядности) базой может служить любой регистр, поэтому, если мы точно знаем состояние регистра ESP (т.е. мы точно знаем, сколько раз мы делали push, а сколько pop), то для доступа к фрейму можно использовать ESP (при этом индексы одной и той же перменной в разных местах процедуры могут отличаться из-за промежуточных push/pop). Собсно, подобного рода оптимизацией занимаются, насколько я знаю, последние версии BC и VC. А в их «асмах» появилась директива «фраме-поин-оммисинс» как раз для таких извратов и предназначенная.

Однако, здесь есть недостатки по сравнению с использованием более-менее статичного EBP, как об этом было упомянуто выше: во-первых, с [ESP] инструкции длиннее, во-вторых, нужно быть очень аккуратным в подсчёте промежуточных push/pop, чтобы верно подставлять смещение до аргумента или переменной в [ESP+xx] (а ведь есть относительно непредсказуемые инструкции вида push [esp+xxx]). Наконец, поскольку индекс xx может постоянно меняться, поэтому использовать встроенные директивы типа ARGS или даже вручную раставленные EQU становится малореальным. Поэтому возможность использования ESP в качестве базы (при ручной кодогенерации – глюкалово полное) отнюдь не умаляет полезности EBP.

#6. Ну вот, мы и подошли к самой интересной части марлезонского балета. Сейчас мы готовы проанализировать нашу программу на предмет того, чего она там вытворяет с локальными переменными.

 :00401000 NumberOfCharsWritten= dword ptr -90h
 :00401000 nNumberOfCharsToWrite= dword ptr -8Ch
 :00401000 hConsoleInput   = dword ptr -88h
 :00401000 hConsoleOutput  = dword ptr -84h
 :00401000 Buffer          = byte ptr -80h
 :00401000
 :00401000 push    ebp
 :00401001 mov     ebp, esp
 :00401003 add     esp, 0FFFFFF70h
 :00401009 push    offset aInputOutput ; lpConsoleTitle
 :0040100E call    SetConsoleTitleA
 :00401013 push    0FFFFFFF5h      ; nStdHandle
 :00401015 call    GetStdHandle
 :0040101A mov     [ebp+hConsoleOutput], eax
 :00401020 push    0               ; lpReserved
 :00401022 lea     eax, [ebp+NumberOfCharsWritten]
 :00401028 push    eax             ; lpNumberOfCharsWritten
 :00401029 push    11h             ; nNumberOfCharsToWrite
 :0040102B push    offset aTypeSomething ; lpBuffer
 :00401030 push    [ebp+hConsoleOutput] ; hConsoleOutput
 :00401036 call    WriteConsoleA
 :0040103B push    0FFFFFFF6h      ; nStdHandle
 :0040103D call    GetStdHandle
 :00401042 mov     [ebp+hConsoleInput], eax
 :00401048 push    0               ; lpReserved
 :0040104A lea     eax, [ebp+nNumberOfCharsToWrite]
 :00401050 push    eax             ; lpNumberOfCharsRead
 :00401051 push    80h             ; nNumberOfCharsToRead
 :00401056 lea     eax, [ebp+Buffer]
 :00401059 push    eax             ; lpBuffer
 :0040105A push    [ebp+hConsoleInput] ; hConsoleInput
 :00401060 call    ReadConsoleA
 :00401065 push    0               ; lpReserved
 :00401067 lea     eax, [ebp+NumberOfCharsWritten]
 :0040106D push    eax             ; lpNumberOfCharsWritten
 :0040106E push    0Ch             ; nNumberOfCharsToWrite
 :00401070 push    offset aYouTyped ; lpBuffer
 :00401075 push    [ebp+hConsoleOutput] ; hConsoleOutput
 :0040107B call    WriteConsoleA
 :00401080 push    0               ; lpReserved
 :00401082 lea     eax, [ebp+NumberOfCharsWritten]
 :00401088 push    eax             ; lpNumberOfCharsWritten
 :00401089 push    [ebp+nNumberOfCharsToWrite] ; nNumberOfCharsToWrite
 :0040108F lea     eax, [ebp+Buffer]
 :00401092 push    eax             ; lpBuffer
 :00401093 push    [ebp+hConsoleOutput] ; hConsoleOutput
 :00401099 call    WriteConsoleA
 :0040109E push    7D0h            ; dwMilliseconds
 :004010A3 call    Sleep
 :004010A8 push    0               ; uExitCode
 :004010AA call    ExitProcess

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

 lea eax,[ebp-X]
 push eax

Предвидя грабли, на которые может натолкнуться начинающий, сразу оговорюсь, эти строки принципиально отличаются от

 push [ebp-X]

Вы не поверите, но почему-то многие новички здесь путаются… Как, вы тоже? ;))

Первое – заносит в стек указатель на. А второе – значение. Всем медитировать!

Обратите внимание, что Ида услужливо вынесла вверх листинга блок констант, каждая из которых имеет отрицательной значение. Но мы-то с вами ещё со школы умеем решать простенькие задачки на сложение отрицательных чисел и без труда высчитаем, что система уравнений:

 a = -84
 b = x+a

имеет более чем тривиальное решение:

 b = x-84

Хе-хе… слышала бы меня сейчас моя школьная учительница математики ;)).

#7. Очевидно, что ebp – это некая «точка отсчета», относительно которой адресуются локальные переменные. А что ж у нас в ebp? Смотрим на начало процедуры, и ищем там строчки

 :00401001 mov     ebp, esp
 :00401003 add     esp, 0FFFFFF70h

И медленно («брюки превращаются… брюки превращаются…») приоткрываем завесу над тайной. «Дело в следующем»: ассемблер смотрит, сколько мы задекларировали локальных переменных и какова размерность каждой. На основании этой информации определяется размер фрейма. В нашем случае задекларировано (смотрим исходник):

LOCAL InputBuffer[128] :BYTE  ;буффер для ввода
 LOCAL hOutPut          :DWORD ;хэндл для вывода
 LOCAL hInput           :DWORD ;хэндл для ввода
 LOCAL nRead            :DWORD ;прочитано байт
 LOCAL nWriten          :DWORD ;напечатано байт

То есть 128 штук байт плюс 4 двойных слова по 4 байта в каждом. Итого для этого всего богатства нужно выделить 144 байт памяти.

Ну и замечательно! Сохраняем адрес вершины стека (память под переменные еще не отведена!) в регистре ebp. Теперь это у нас вовсе не вершина, а «точка отсчёта» для локальных переменных. А саму вершину стека передвигаем на 144 байта «вверх», в сторону младших адресов (там у нас будет область для хранения локальных переменных). Как видите, все очень просто. 🙂

Задание для медитации: чем будут отличаться генеримые инструкции для директивы addr в случае, если addr будет применяться к аргументам процедуры, а не к локальным переменным.

Если вас смущает то, что для «поднятия планки» используется инструкция add и столь большое число, то вернитесь к выпуску о кодировании отрицательных чисел и особенностях дополнительного кода. Либо поставьте в Иде указатель на смущающее вас число 0FFFFFF70h и нажмите на Ctrl+-, после чего долго и упорно медитируйте. 🙂

Если подключить фантазию и пару раз протрассировать это безобразие под отладчиком, то получится такая картинка:

Изображение отсутсвует

  Распечатайте её и повесьте над своей кроватью. И пусть она время от времени напоминает вам о смерти…

[C] Serrgio / HI-TECH

 

Источнк WASM.RU /22.08.2002/

Внимание — некоторые изображения отсутствуют по причине отсутствия таковых на странице-источнике


Поделиться в соц сетях

Подписаться
Уведомить о
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+