№26. Смешались вместе: оптимизация, консольный ввод...

Очередной выпуск... порция бреда от Толстой Луны (издранно из RTFM_Helpers) и две порции бреда от Serrgio (издранно из воспаленного и измученного сендмейлом мозга).

Return to sources, или DZен после вечернего пива

Сразу сорри за возможно внетематическую мессагу, прямо к RTFM возможно и не относящуюся. Если в лом читать всю эту лажу, делетьте сразу...

1. О рассылке "Низкоуровневое программирование для дзенствующих".

Мужики! (ну и девки тож, ведь есть наверняка!) Серрджио нас по ходу кинул! Обещал писать выпуски, холить и лелеять, учить премудростям и иметь Матрицу. И что же? Рассылка даже не еже- (двух-) недельная, а хорошо если ежемесячная. А какие были планы! По понедельникам по теме, по воскресеньям юмор, ответы на вопросы, переход рассылки в "серебряные" (не знаю, что это значит, но что-то явно хорошее :))... Разочарован, господа. Сейчас он мне ответит, что готовит следующий выпуск по сайсу, и усе будет, только жди... Не верьте! Напишет один выпуск и опять кинет вариться в собственном соку. Серрджио! Душа просит...

На этом бредисловие считаем закрытым.

2. Вернёмся к истокам, или что было вначале (когда меня не было :) )

( :-) я знал, что не делетнете, программеры народ любопытный. Вам же хуже :)))

От нечего делать, вчера решил посмотреть архив конференции. Начал, естессно с начала... Если б я это сделал раньше :)... Не задавал бы вопросов о переходе в protected mode - весьма обстоятельно это было разъяснено. Если и не совсем подробно, то по крайней мере для меня достаточно. Но и не узнал бы о такой штуке, как Menuet OS, спасибо Алексею. Это ж надо, народ вовсю программит, а я сижу и только дзенствую над простенькими прогами под 286! Эмпирическим путем выясняю, что за фигня код 66h перед вроде бы нормальными командами в дебуге, а люди свободно пишут 'xor eax,eax'! Не, надо вылезать из берлоги! Тем более такие темы обсуждаются, такими девченками :)). Заря этой конфы была что надо! Парень спросил - как ему call loop'om завернуть...:))) Наивный! Уже завернули! Сначала выкинули call, потом loop, и умяли в 28 байт. Не, мисс (чёрт, или миссис? пусть будет так: DZ Единорожка :)) White Unicorn программит что надо - чувствуется родственная душа... Тока зачем цельных 29 байт на такую простую штуку отводить? Я бы больше 26 не дал :) Посему часть 3:

3. НИЗКОУРОВНЕВАЯ ОПТИМИЗАЦИЯ

...Машина довольно безграмотно объяснила,
что такое неонка, и понеслось...
(С) А. и Б. Стругацкие

3.1. Что это такое?

Оптимизация, понимашь, это такая штука... Это когда ненормальный программер издевается над кодом и вместо того, чтобы писать толковые вещи или гаматься, тратит свое бесценное время на подсчет либо байтов в проге, либо числа тактов, за которое она исполнится. В результате получаются маленькие шустрые программы, за которые никто никогда не заплатит - см. "Вредные советы" Серрджио. Оптимизация бывает по времени исполнения и по размеру проги. Как показывает опыт, почти всегда маленькие проги исполняются быстрее. Но иногда (когда? рассчеты запуска ракеты-носителя с Бураном? контроль ядерных реакторов?) есть резон убрать циклы, вытянуть прогу в длину, и надеяться, что выгадаем пару тактов в скорости :) На сём мое бестолковое объяснение, что же такое оптимизация, заканчиваю. Кто чего не понял - сам виноват!

3.2. Для чего это нужно?

А теперь открою страшную тайну. Оптимизация никому не нужна! Это имело смысл на старых компах, когда были сурьёзнейшие траблы с оперативной памятью и / или с местом на _дискете_! Нет смысла ужимать байты из 400-байтовой проги, поскольку на диске она занимает кластер. А сколько у вас размер кластера? Не, если вы сидите за Win-2000 или NT, то к вам это не относится. У прочих же кластер занимает от 4 до 32 кил (или больше...) - ну и зачем ужимать байты? Практически то же самое относится и к памяти. Время выполнения? Выровняй критичный по времени цикл по размеру кеша процессора и хватит! А можно и этого не делать - поднимите руки, у кого тактовая меньше 100 MHz. Опустите руки. На ваших тачках никто критичные по времени рассчеты делать не собирается. Все прочие могут отдыхать и не забивать себе голову ерундой - на Целероне-700 разница между оптимизированной по времени прогой и равноценной неоптимальной будет... не больше 30 секунд за час работы. Зачем же этим заниматься? А заниматься оптимизацией кода надо затем, что это чисто DZенское занятие, развивает башку, дает чувство глубокого морального удовлетворения, позволяет запихнуть свой микрокод в самые неожиданные места вроде бут-сектора и вообще - мало ли чего я там говорил - нужна, не нужна... Всем дебаг с ТАСМом в зубы и если в вашей проге будут лишние 10-20- и т.д. байт - покусаю! Мастдай! (западная школа, (С) Serrgio)

3.3. Как это делается?

Начнем с того, что оптимизация - чисто интеллектуальное занятие. Если бы был алгоритм, его давно бы во все компиляторы засовали. А заодно заставили бы эти компиляторы сами программы писать, компилить и на диски записывать. И настала бы полная МАТРИЦА! Посему и я алгоритма не дам, а лишь несколько советов да примеров. Для начала, чтобы въехать, о чем вообще я тут толкую, гляньте на начало конфы - там DZ Единорожка явила собой хороший пример дзенствующего оптимизатора :). По времени я оптимизировать не люблю - см выше. Чем короче код, тем он быстрее, а пройгрыш нескольких секунд за час работы я переживу. Так что всё малоразумное нижеприведенное относится к оптимизации места.

1. Это все опытные кодеры делают сразу. Все обнуления регистров меняются на XOR, никаких LEA - только MOV Reg, offset Var. Для счетчиков стараемся использовать CX - он классно закручивается LOOP'ом, экономим на каждой замене 1 байт.

2. Используем только необходимое. Если работаем с 16-разрядными регистрами, то никаких EAX ! Каждая буква Е в коде увеличивает прогу на байт (шутка! ...на 2 байта :))). Если для прерывания нужен 8-разрядный - его и используем. Если требуется сделать что-то вроде

     MOV CH, 0Eh
     MOV CL, 1Fh,

заменяем на

     MOV CX, 0E1Fh

и выйгрыш еще одного байта обеспечен. Активно юзаем строковые операции - ничто так не экономит место, как структуры типа REP MOVSW! Нужно засунуть что-то вроде A000h в ES?

     PUSH A000h
     POP ES

и готово! Уже громадный плюс, что большинство компиляторов при арифметике с байтовыми числами генерит код типа ADD CX,+3Eh - это еще байт экономии.

3. Окинув оптимизированное во 2-м приближении безобразие критическим взором, иногда удается полностью или частично поменять алгоритм, сокращая результат раза в 2. Тогда еще раз повторяем шаг 2. Попутно примеряем нестандартные способы кодирования. Пример - надо к BX прибавить 05EFCh и сохранить результат в DX. Что я там говорил насчет LEA? Не, бесполезных команд не бывает - LEA DX, [BX+05EFCh] и все!

4. Для совсем ненормальных - отслеживаем в уме или в отладчике значения регистров. Если, к примеру, CX=021Eh, а надо занести в него 0220h - зачем тратить на пересылку 3 байта, когда можно дважды сделать INC CX? Кстати, INC и DEC с 16-разрядными регистрами занимают 1 байт, поэтому - если точно знаем, что переполнения или заема при операции не предвидится - юзаем INC Reg16 вместо INC Reg8. Убираем все процедуры, если они вызываются один раз. Если два - пытаемся сделать рекурсию. Ну и для окончательно свихнувшихся есть еще

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

Используя все вышеперечисленное в любом порядке (замена пунктов и добавление новых приветствуется), прогу любого халявщика можно сократить в 2 раза минимум. Сложнее, когда код ваял профессионал... Надеюсь, в архив все уже заглянули :). Последний результат White Unicorn'a был таким:

     CODESG segment
     assume cs:CODESG
     org 100h
     MAIN proc
            mov     di,0101h
            mov     ax,0600h
            mov     bh,70h
            mov     dx,0184fh
            xor     cx,cx
            mov     si,0dh
         NextWin:
            int     10h
            sub     bh,10h
            add     cx,di
            sub     dx,di
            dec     si
            jnz     NextWin
            ret
     MAIN    endp
     CODESG  ends
     end     MAIN

Ни добавить ни убавить, кажись - 29 байт.

Ну, я так не умею, посему напишу как могу - в debug'e:

     :0100    MOV    DI,0101
     :0103    MOV    AH,06
     :0105    MOV    BH,70
     :0107    MOV    DX,184F
     :010A    XOR    CX,CX
     :010C    INT    10
     :010E    SUB    BH,10
     :0111    ADD    CX,DI
     :0113    SUB    DX,DI
     :0115    CMP    DH,CH
     :0117    JNC    10C
     :0119    RET

Шестерку заносим в AH - на AL по барабану. Там либо 0, либо 0FFh - 3-го не дано. И то, и другое прокрутит нам не только весь экран, но еще и много строк пробелами. А цикл делаем через сравнение - когда выполняется 0C-ый раз, и и идем на 0D-ый, в CX 0c0ch, в DX - 0c43. При последнем проходе DH=0bh, CH=0dh - то есть при сравнении будет перенос. Получаем 26 байт. Что ни говори, а три байта у мастера урвать приятно!

An Taro Adun,
перепивший пива и при этом переDZенствоваший Толстая Луна
(не путать с полной!), он же FatMoon.

PS: я еще вторую сотню из архива не прочитал. Вот ищо продвинусь и тогда-то мы и похохочем! (Если меня раньше не отпишут :))

Издранное из RTFM_Helpers ;)

F> Сейчас он мне ответит, что готовит следующий выпуск по сайсу, и усе будет,
F> только жди... Не верьте! Напишет один выпуск и опять кинет вариться в
F> собственном соку. Серрджио! Душа просит...

С трясущимися от страха коленками, роняя слюну на клавиатуру, сажусь лабать очередной выпуск. Авось успею, прежде чем разочарованный народ (под предводительством FatMoon'а) устроит мне джихад (с повешиванием вниз головой, нежным поглаживанием утюгом и пилением зубов ржавым напильником)...
(C)Serrgio

Консольный ввод и томограф 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),10,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

Учитывая, что lea может опpеделить адpес метки в "pантайме", все pаботает пpекpасно! (С) Iczelion.

Как обычно, я надеюсь на то, что вы не поверили мне на слово. Мы обязательно разберемся с тем, как происходит резервирование места в стеке и обращение к локальным переменным, но сделаем немного позже. Для начала нам нужно хотя бы чуть-чуть ознакомиться с инструментарием. Начнем мы, пожалуй, с 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. Теперь, когда мы поверхностно ознакомились с "хакерскими" инструментами, пришло время вспомнить о цели, которую мы преследовали, начав знакомство с идой и сайсом. Напоминаю - мы хотели исследовать, каким образом происходит декларирование локальных переменных в стеке, и как происходит к ним обращение. Что же, у вас есть целый месяц, чтобы исследовать это самостоятельно ;). Свою версию исследования я предоставлю в следующем выпуске...

И да пребудет с вами сила!

© 2014-2017 Сергей Воробьев

0.02