Win32ASM: Консольный ввод, томограф IDA и скальпель SoftICE — Архив WASM.RU
В этом туториале мы напишем простенькую консольную программу, познакомимся с Идой и Сайсом и с их помощью проведем небольшое исследование на тему что такое локальные переменных и с чем их едят.
Содержание
Консольный ввод и томограф IDA
#1. В прошлый раз мы разобрались с консольным выводом. Сегодня - разберемся с консольным вводом. Для этого напишем простую программу, запрашивающую строку символов, а затем её же (строку) и выводящую. Это будет очередной маленький шажок, который для вас вполне может стать решающим, так как именно эта программа впоследтвии послужит нам жертвой для негуманных экспериментов, в которых мы впервые используем два хирургических инструмента - отладчик SoftIce (на правах скальпеля) и дизассемблер IDA Pro (на правах томографа). И если при виде окровавленных внутренностей программы вам не поплохеет, более того - если вам ЭТО понравится, значит вы имеете неплохие шансы стать либо серийным маньяком (крэкером), либо - исследователем программ (реверсером). Все же остальные профессии отныне перестанут для вас существовать ;)
Итак, открываем ASM Editor и набиваем там следующий текст:Код (Text):
.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. Вот ее описание:Код (Text):
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
. Если же это локальная переменная, то перед вызовом функции генерируется следующая последовательность инструкций:Код (Text):
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):
.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 и перемещаемся вот в это место нашего листинга:Код (Text):
.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...Что мы видим? Длиннющую простыню из байтов! Ида нам подсказывает, какой символ соответствует тому или иному шестнадцатеричному значению. Не нужно быть семи пядей во лбу, чтобы догадаться, что эта простыня соответствует следующему куску нашего исходника:
Код (Text):
.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):
.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", то бишь "переопределить в строку". В итоге получим крАсивую, и, что самое главное, сходу понятную, строчку:
Код (Text):
.data:0040301D aInputOutput db 'Input & Output',0 ; DATA XREF: start+9_oПроделаем такую же процедуру и с двумя "вышележащими" метками, получив в итоге следующее отображение секции данных:
Код (Text):
.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):
... .text:00401009 push offset unk_40301D ; lpConsoleTitle ... .text:0040102B push offset unk_403000 ; lpBuffer ... .text:0040106D push offset unk_403011 ; lpBuffer ...Когда мы переопределили данные из байтовых в строковые, то Ида переобозвала переменные в
aTypeSomething
,aYouTyped
иaInputOutput
, а обращение стало производиться уже к совершенно другим "именам собственным".
А вот если бы мы переопределили данные начиная не с последней метки, а с первой, то получилась бы строка:Код (Text):
.data:00403000 aTypeSomethingY db 'Type something > You typed > Input & Output',0А обращение к ней осуществлялось бы вот как:
Код (Text):
... .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 ;). Чтобы запустить сайс в этом режиме, необходимо ввести команду:
Код (Text):
NET START NTICEлибо запустить "Start SoftICE" из менюшки (pif на обыкновенный батник, где эта команда написана).
Все! Отладчик запущен. Только вот не увидите вы его ни в Applications, ни в Processes (CTRL+ALT+DEL). Ни даже на экране - пока не нажмете на волшебную комбинацию клавиш Ctrl+D ;).
Жмем на Ctrl+D! Если отладчик установился успешно, то всплывет приблизительно следующая "картинка":Код (Text):
-------------------------------------------------------------------------------- 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;" в самом конце - это выход из окна сайса):Код (Text):
INIT="SET FONT 2; LINES 60; WD 22; WC 25; CODE ON; COLOR A A 20 20 2; FAULTS OFF; X;"Далее раскомментируйте в winice.dat все строки наподобие
Код (Text):
EXP=\SystemRoot\System32\kernel32.dllЭто необходимо для того чтобы сайс загрузил имена экспортируемых функций, находящихся в этих биб-лиотеках. Иначе вместо понятных команд, наподобие
Код (Text):
call USER32!MessageBoxAмы увидим безобразие типа
Код (Text):
call 0044F2A1Теперь нам нужно перезапустить отладчик, чтобы проверить, подхватывает ли сайс наши настройки. Для этого нужно… В общем, как я уже говорил, сайс - прога, весьма специфическая, и выгрузить ее "из компьютера" можно только одним способом - перезагрузкой ;). Start > Shutdow > Restart…
#4. Существуют две области применения сайса - отладка собственных программ и исследование чужих, так называемый reverse ingeneering. Для начала давайте научимся использовать сайс в качестве инструмента для отладки и изучения собственных приложений.
Что мы имеем в этом случае? Исходные тексты программы, и как следствие - возможность откомпилировать ее отладочную версию, т.е. тот же экзешник, но содержащий, помимо всего прочего, еще и кучу дополнительной информации. Как в самом исполняемом файле, так и в специально для этой цели сгенерированных дополнительных файлах.
Итак, мы должны:1. Сассемблировать исходник с ключем Zi:
Код (Text):
ml /c /coff /Zi src.asmВ результате этого мы получим объектный файл, содержащий отладочную информацию для отладчика CodeView. Легко заметить, что размер этого файла намного больше, чем у его "нормального" аналога.
2. Слинковать объектный файл с ключами /DEBUG и /DEBUGTYPE:CV
Код (Text):
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], то есть "не открыт модуль". Не правда ли, не очень тонкий намек?
Теперь мы готовы свершить самое главное действо - загрузить программу под отладчик. Жмем Module > Load, и вот оно, долгожданное! У нас всплывает окно сайса с указателем, установленным на точку входа в нашу программу:
- Жмем 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), т.е. "только символическую информацию, включая локальные переменные и структуры".
Код (Text):
Main 001B:00401010 55 PUSH EBP#5. Для начала рассмотрим команду display memory (отобразить память). Ее синтаксис:
Код (Text):
D[size] [address [l length]]Если ее использовать без параметров, то просто будет отображаться следующая страница дампа. Необязательный параметр size - это размерность, в которой выводится дамп. Вот расшифровка его значений:
Код (Text):
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 - это адрес памяти, дамп которого вы хотите получить. Притом вовсе необязательно писать адрес цифрами - "составные" адреса сайс также принимает "за милую душу".
Таким образом, если мы введем команду:Код (Text):
DD EIPто увидим дамп той области памяти, в которой в настоящее время располагается секция кода нашей программы (конечно, если после всплытия отладчика на точке входа вы ничего не успели напортачить).
Если же мы начнем использовать параметр length, то данные, запрашиваемые с использованием этого параметра, начнут отображаться в командной строке. Например, командаКод (Text):
DW EIP L 1000выведет в командную строку 1000 байтов памяти, начиная с текущего значения регистра EIP, группированных пословно, и вы будете вынуждены несколько раз нажать на any key, прежде чем все это просмотрите. По большому секрету скажу, что нажатие на ESC сразу же прервет просмотр.
#6. Пожалуй, одна из главных возможностей любого отладчика - это трассировка, которая позволяет выполнять программу пошагово. Итак, загрузим нашу программу в SymbolLoader, нажмем на Load, а в появившемся окне отладчика введем команду "p" (она же - клавиша F10). В результате этого выполнится один логический шаг нашей программы (program step). Под "логическим шагом" подразумевается, своего рода "поверхностная" трассировка, без входа в процедуры, циклы и т.д.
Команда "t" (она же - клавиша F8) выполняет трассировку одной инструкции (trace one instruction), с "заходом" во все функции, в том числе и апишные. Подобная трассировка - это очень длинный путь, поэтому у данной команды существует еще и два параметра:Код (Text):
T [=start-address] [count]Первый - это адрес, с которого вы желаете начать трассировку, а count - это число инструкций, которое сайс протрассирует, прежде чем остановиться (и не забывайте о том, что он "заходит" в апишные функции!).
Конечно же, трассировочные возможности сайса намного больше, чем те немногие, которые мы рассмотрели. Впоследствии мы обязательно ознакомимся со всем их многообразием - по мере необходимости ;).#7. Еще одна полезная команда - это rs (она же F4), restore the program screen (восстановление экрана программы). А как же без этого? Ведь внешне работа программы заключается вовсе не в выполнении команд процессора и пересылках данных между регистрами ;).
Например в нашем случае программа печатает приглашение ввести строку символов. Мы можем оттрассировать программу до (включительно) строки:и затем нажать на F4, для того чтобы подсмотреть, действительно ли в консольном окне появилась строчка "Type something >". А затем нажать на any key и снова очутиться в отладчике.Код (Text):
001B:00401046 E8BD000000 CALL _WriteConsoleA
Или же, например, выполнив апишную функциюКод (Text):
001B: 00401070 E881000000 CALL _ReadConsoleAкоторая "просит" ввести строку символов, нажать на F4 и ввести необходимую строку символов, а по нажатию на Enter (типа "ввод закончен") снова оказаться в отладчике.
#8. Теперь, когда мы поверхностно ознакомились с "хакерскими" инструментами, пришло время вспомнить о цели, которую мы преследовали, начав знакомство с идой и сайсом. Напоминаю - мы хотели исследовать, каким образом происходит декларирование локальных переменных в стеке, и как происходит к ним обращение.
#1. Представьте себе картину: накачанный колесами и протеиновыми коктейлями шварценеггер бьет со всей дури по боксерской груше. Та отлетает в сторону, а потом по каким-то абсолютно нефизическим законам кинемотографа возвращается назад и бьет этому боксеру по морде лица, в результате чего шварценеггер отлетает на несколько метров, и, обязательно задев что-нибудь из мебели, размазывается соплями по стене - к неописуемому удовольствию зрителя. Посмотрев на такую картину, Станиславский бы сказал: "не верю"! А вот дZенствующей программер, увидя это безобразие, подумал бы: "ба! Да совсем как стек. Помницца, в одной из своих кулхацкерных прог я на похожие грабли как раз и напоролся".
Ранее мы уже разобрались с очередностью записи в стек и чтения из а него. Напомню, что доступ к стеку осуществляется в соответствии с принципом LIFO (Last In First Out – Последним Пришел, Первым Ушел). Однако это отнюдь не единственное, что нам нужно знать о стеке - конечно же, если мы не собираемся время от времени получать "отдачу" от "боксерской груши" ;).
Нам уже хорошо известно, что стек можно использовать для временного хранения данных – с его помощью мы выкручивались из такой проблемы, как недостаточное для полета нашей фантазии количество регистров. То есть мы временно сохраняли значения регистров в стеке, активно юзали их для наших нужд, а потом снова восстанавливали "статус кво", за исключением, в большинстве случаев, одного-единственного регистра, в котором хранился результат проделанной работы.
Также известно, что в инструкциях процессоров от Интел переменные (которые в памяти) не могут выступать в качестве приемника и источника одновременно, то есть инструкция:
Код (Text):
mov [dwVar1],[dwVar2]не проходит. А выкрутиться из такой ситуации можно двумя способами - либо использовать в качестве посредника регистр (которых, как всегда, мало):
Код (Text):
mov eax,[dwVar2] mov [dwVar1],eaxлибо задействовав стек:
Код (Text):
push [dwVar2] pop [dwVar1] Кстати, недавно в почтовой рассылке RTFM_helpers прошло обсуждение того, как можно копировать из памяти в память - там было упомянуто, например, использование movs. А если подумать, можно найти и другие нетривиальные способы.Для тех, кто ещё не понял. Вот этот кусок кода:
Код (Text):
push 1 push 2 push 3 pop eax pop ebx pop ecxделает то же самое, что и следующий код (если только не считать разницу в скорости, размере кода и побочных эффектах):
Код (Text):
mov eax,3 mov ebx,2 mov ecx,1Сомневающиеся могут проверить под отладчиком. Также попробуйте сравнить:
Код (Text):
push 1 pop eaxи психоделическое:
Код (Text):
sub esp,4 mov dword ptr [esp],1 mov eax,[esp] add esp,4Вы можете мне не поверить, но эти два куска кода тоже "делают" одно и то же, хотя последний и кажется плодом больного воображения.
- Что за esp такой? – спросите вы.
Пошире откройте глаза и слушайте – сейчас я поведаю вам страшные тайны. ;)
#2. Помните мою аналогию с блинами от штанги и вделанным в пол штырём, на который они надевались для хранения. Там еще был учитель физкультуры, который приставал к нашим неполовозрелым одноклассницам, в результате чего ему перебили нос. Так вот, адрес самого верхнего блина – это вершина стека и хранится он (адрес) в регистре esp/sp. Другими словами, вершина стека – это адрес последнего занесенного в стек элемента.
Давайте посмотрим на поведение esp при трассировке следующего кода:
Код (Text):
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:
Код (Text):
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 <источник> следующий:
- Уменьшение значения указателя стека esp/sp на 4/2.
- Запись значения источника по адресу ss:esp/sp (вершина стека).
Об алгоритме работы команды pop догадайтесь сами…
Для последователей дZена предлагаем тему для исследования: какое значение будет лежать в стеке после инструкции push esp.
ПРЕДУПРЕЖДЕНИЕ: ни в коем случае не принимайте результаты отдельных экспериментов в конкретных условиях за абсолютную истину, верную всюду и всегда!
Теперь, после вышесказанного, вы легко сообразите, какое значение примет регистр eax в следующем извращенном случае:
Код (Text):
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. То есть он может выполнять роль базы.
Напомню, что, например, следующий код:
Код (Text):
mov ebx,12FFC0h mov al,[ebx]присвоит регистру AL значение байта по адресу 12FFC0 из сегмента, задаваемого регистром DS.
Точно таким же образом можно использовать и регистр ebp/bp. Говоря другими словами, это один из немногих регистров, которые можно "брать в квадратные скобки" не увеличивая при этом размер инструкции . То есть (и это будут уже третьи слова) он позволяет работать с ячейкой памяти, адрес которой находится в регистре.
Проиллюстрирую это на простом примере. Допустим, занесли вы в стек разных параметров кучу:
Код (Text):
адрес значение 0012FFC4 77E7EB69 0012FFC8 0047E4AC 0012FFCC 0012DAB4 0012FFD0 7FFDF080И возжелалось вам в силу какой-нибудь нездоровой производственной необходимости прочитать "здесь и сейчаc", например, предпоследний элемент. В этом случае делаем вот что:
Код (Text):
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. Ну вот, мы и подошли к самой интересной части марлезонского балета. Сейчас мы готовы проанализировать нашу программу на предмет того, чего она там вытворяет с локальными переменными.
Код (Text):
: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) , во всех случаях генерит следующий код:
Код (Text):
lea eax,[ebp-X] push eaxПредвидя грабли, на которые может натолкнуться начинающий, сразу оговорюсь, эти строки принципиально отличаются от
Код (Text):
push [ebp-X]Вы не поверите, но почему-то многие новички здесь путаются... Как, вы тоже? ;))
Первое – заносит в стек указатель на. А второе – значение. Всем медитировать!
Обратите внимание, что Ида услужливо вынесла вверх листинга блок констант, каждая из которых имеет отрицательной значение. Но мы-то с вами ещё со школы умеем решать простенькие задачки на сложение отрицательных чисел и без труда высчитаем, что система уравнений:
Код (Text):
a = -84 b = x+aимеет более чем тривиальное решение:
Код (Text):
b = x-84Хе-хе... слышала бы меня сейчас моя школьная учительница математики ;)).
#7. Очевидно, что ebp – это некая "точка отсчета", относительно которой адресуются локальные переменные. А что ж у нас в ebp? Смотрим на начало процедуры, и ищем там строчки
Код (Text):
: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+-, после чего долго и упорно медитируйте.
Если подключить фантазию и пару раз протрассировать это безобразие под отладчиком, то получится такая картинка:
Распечатайте её и повесьте над своей кроватью. И пусть она время от времени напоминает вам о смерти... © Serrgio / HI-TECH
Win32ASM: Консольный ввод, томограф IDA и скальпель SoftICE
Дата публикации 22 авг 2002