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

       

Идентификация литералов и строк


Уже давно

Утихло поле боя,

Но сорок тысяч

Воинов Китая

Погибли здесь,

Пожертвовав собою...

Ду Фо "Оплакиваю поражение при Чэньтао"

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

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

1) строка состоит из ограниченного ассортимента символов. В грубом приближении это – цифры, буквы алфавита (включая проблел), знаки препинания и служебные символы наподобие табуляции или возврата каретки;

2) строка должна состоять по крайней мере из нескольких символов.

Условимся считать минимальную длину строки равной N байтам, тогда для автоматического выявления всех строк достаточно отыскать все последовательности из N и более "строковых" символов. Весь вопрос в том, чему должна быть равна N, и какие символы включать в "строковые".

Если N мало, порядка трех-четырех байт, то мы получим очень большое количество ложных срабатываний. Напротив, когда N велико, порядка шести-восьми байт, число ложных срабатываний близко к нулю и ими можно пренебречь, но все короткие строки, например "OK", "YES", "NO" окажутся нераспознаны! Другая проблема – помимо знакоцифровых символов в строках встречаются и элементы псевдографики (особенно часты они в консольных приложениях), и всякие там "мордашки", "стрелки", "карапузики" – словом почти вся таблица ASCII.
Чем же тогда строка отличается от случайной последовательности байт? Частотный анализ бессилен – ему для нормальной работы требуется как минимум сотня байт текста, а мы говорим о строках из двух-трех символов!

Зайдем с другого конца – если в программе есть строка, значит, на нее кто-нибудь да ссылается. А раз так – можно поискать среди непосредственных значений указатель на распознанную строку. И, если он будет найден, шансы на то, что это действительно именно строка, а не случайная последовательность байт резко возрастают. Все просто, не так ли?



Просто, да не совсем! Рассмотрим следующим пример:

BEGIN

WriteLn('Hello, Sailor!');

END.

Листинг 131

Откомпилирует его любым подходящим Pascal-компилятором (например, Delphi или Free Pascal) и, загрузив откомпилированный файл в дизассемблер, пройдемся вдоль сегмента данных. Вскоре на глаза попадется следующее:

.data:00404040 unk_404040      db  0Eh ;

.data:00404041                 db  48h ; H

.data:00404042                 db  65h ; e

.data:00404043                 db  6Ch ; l

.data:00404044                 db  6Ch ; l

.data:00404045                 db  6Fh ; o

.data:00404046                 db  2Ch ; ,

.data:00404047                 db  20h ;

.data:00404048                 db  53h ; S

.data:00404049                 db  61h ; a

.data:0040404A                 db  69h ; i

.data:0040404B                 db  6Ch ; l

.data:0040404C                 db  6Fh ; o

.data:0040404D                 db  72h ; r

.data:0040404E                 db  21h ; !

.data:0040404F                 db    0 ;

.data:00404050 word_404050     dw 1332h

Листинг 132

Вот она, искомая строка! (В том, что это строка – у нас никаких сомнений нет). Попробуем найти: кто на нее ссылается? В IDA Pro для этого следует нажать <ALT-I> и в поле поиска ввести смещение начала строки – "0x404041"…

Как это "ничего не найдено – Search Failed"? А что же тогда передается функции WriteLn? Может быть, это глюк IDA? Просматриваем дизассемблерный текст вручную – результат вновь нулевой.



Причина нашей неудачи в том, что в начале Pascal- строк идет байт, содержащий длину этой строки. Действительно, в дампе по смещению 0x404040

находится значение 0xE

(четырнадцать в десятичной системе исчисления). А сколько символов строке "Hello, Sailor!"? Считаем: один, два, три… четырнадцать! Вновь нажимаем <ALT-I> и ищем непосредственный операнд, равный 0x404040. И, в самом деле, находим:

.text:00401033                 push    404040h

.text:00401038                 push    [ebp+var_4]

.text:0040103B                 push    0

.text:0040103D                 call    FPC_WRITE_TEXT_SHORTSTR

.text:00401042                 push    [ebp+var_4]

.text:00401045                 call    FPC_WRITELN_END

.text:0040104A                 push    offset loc_40102A

.text:0040104F                 call    FPC_IOCHECK

.text:00401054                 call    FPC_DO_EXIT

.text:00401059                 leave

.text:0040105A                 retn

Листинг 133

Отказывается, мало идентифицировать строку – еще, как минимум, требуется определить ее границы.

Наиболее популярны следующие типы строк: Си-строки, завершающиеся нулем; DOS-строки, завершающиеся символом "$"; Pascal-строки, предваряемые одним-, двух- или четырехбайтным полем, содержащим длину строки. Рассмотрим каждый из этих типов подробнее:

::Си-строки, так же именуемые ASCIIZ-строками (от Zero – нуль на конце) – весьма распространенный тип строк, широко использующийся в операционных системах семейств Windows и UNIX. Символ "\0" (не путать с "0") имеет специальное предназначение и трактуется по-особому – как завершитель строки. Длина ASCIIZ-строк практически ничем не ограничена – ну разве что размером адресного пространства, выделенного процессу или протяженностью сегмента. Соответственно, в Windows 9x\NT максимальный размер ASCIIZ-строки лишь немногим менее 2 гигабайт, а в Windows 3.1 и MS-DOS – около 64 килобайт. Фактическая длина ASCIIZ-строк лишь на байт длиннее исходной ASCII-строки.


Несмотря на перечисленные выше достоинства, Си-строкам присущи и некоторые недостатки. Во-первых, ASCIIZ-строка не может содержать нулевых байт, и поэтому, она не пригодна для обработки бинарных данных. Во-вторых, операции копирования, сравнения и контакции Си-строк сопряжены со значительными накладными расходами – современным процессорам не выгодно работать с отдельными байтами, – им желательно иметь дело с двойными словами. Но, увы, длина ASCIIZ-строк наперед неизвестна и ее приходится вычислять "на лету", проверяя каждый байт на символ завершения. Правда, разработчики некоторых компиляторов идут на хитрость – они завершают строку семью

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

::DOS-строки. В MS-DOS функция вывода строки воспринимает знак '$' как символ завершения, поэтому в программистских кулуарах такие строки называют "DOS-строками". Термин не совсем корректен – все остальные функции MS-DOS работают исключительно с ASCIIZ-строками! Причина выбора столь странного выбора символа-разделителя восходит к тем древнейшим временам, когда никакого графического интерфейса еще и в помине не существовало, а консольный терминал считался весьма продвинутой системой взаимодействия с пользователем. Клавиша <Enter> не могла служить завершителем строки, т.к. под час приходилось вводить в программу несколько строк сразу. Комбинации <Ctrl-Z>, или <Alt-000> так же не годились – на многих клавиатурах тех лет отсутствовали такие регистры! С другой стороны, компьютеры использовались главным образом для инженерных, а не бухгалтерских расчетов, и символ "бакса" был самым мало употребляемым символом – вот и решили использовать его для сигнализации о завершении пользователем ввода и как символ-завершитель строки. (Да, символ завершитель вводился пользователем, а не добавлялся программой, как это происходит с ASCIIZ-строками).


В настоящее время DOS- строки практически вышли из употребления и  читатель вряд ли с ними столкнется…

::Pascal-строки. Pascal-строки не имеют завершающего символа, - вместо этого они предваряются специальным полем, содержащим длину этой строки. Достоинства этого подхода: – возможность хранения любых символов в строке (в том числе и нулевых байт!) и высокая скорость обработки строковых переменных. Вместо постоянной проверки каждого байта на завершающий символ, происходит лишь одно обращение к памяти – загрузка длины строки. Ну, а раз длина строки известна, можно работать не с байтами, а двойными словами – "родным" типом данных 32-разрядных процессоров. Весь вопрос в том – сколько байт отвести под поле размера. Один? Что ж, экономно, но тогда максимальная длина строки будет ограничена 255 символами, что во многих случаях оказывается явно недостаточно! Этот тип строк используют практически все Pascal-компиляторы (например, Borland Turbo Pascal, Free Pascal), поэтому-то такие строки и называют "Pascal-строками" или, если более точно, "короткими Pascal-строками".

::Delphi-строки. Осознавая очевидную смехотворность ограничения длины Pascal-строк 255 символами, разработчики Delphi расширили поле размера до двух байт, увеличив, тем самым максимально возможную длину до 65.535 символов. Хотя, такой тип строк поддерживают и другие компиляторы (тот же Free Pascal к примеру), в силу сложившейся традиции их принято именовать Delphi-строками или "Pascal-строками с двухбайтным полем размера – двухбайтными Pascal-строками".

Ограничение в шестьдесят с гаком килобайт и "ограничением" язык назвать не поворачивается. Большинство строк имеют гораздо меньшую длину, а для обработки больших массивов данных (текстовых файлов, к примеру) если куча (динамическая память) и ряд специализированных функций. Накладные же расходы (два служебных байта на каждую строковую переменную) не столь велики, чтобы их брать в расчет. Словом, Delphi-строки, сочетая в себе лучше стороны Си- и Pascal-строк (практически неограниченную длину и высокую скорость обработки соответственно), представляются самым удобным и практичным типом.



::Wide-Pascal строки. "Широкие" Pascal- строки отводят на поле размера аж четыре байта, "ограничивая" максимально возможную длину 4.294.967.295 символами или 4 гигабайтами, что даже больше того количества памяти, которое Windows NT\9x выделяют в "личное пользование" прикладному процессу! Однако за эту роскошь приходится дорого платить, отдавая каждой строке четыре "лишние" байта, три из которых в большинстве случаев будут попросту пустовать. Накладные расходы на коротких строках становятся весьма велики, поэтому, тип Wide-Pascal практически не используется.

::Комбинированные типы. Некоторые компиляторы используют комбинированный Си+Pascal тип, что позволяет им с одной стороны, достичь высокой скорости обработки строк и хранить в строках любые символы, а с другой – обеспечить совместимость с огромным количеством Си-библиотек, "заточенных" под ASCIIZ-строки. Каждая комбинированная строка принудительно завершается нулем, но этот нуль в саму строку не входит и штатные библиотеки (операторы) языка работают с ней как с Pascal-строкой. При вызове же функций Си-библиотек, компилятор передает им указатель не на истинное начало строки, а на первый символ строки.

__::Другие завершающие символы.



Рисунок 21 0х014 Осиновые типы строк

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

Грубо тип строки определяется по роду компилятора (Си или Pascal), а точно – по алгоритму обработки этой строки (т.е. анализом манипулирующего с ней кода). Рассмотрим следующий пример:

VAR

s0, s1 : String;

BEGIN

s0 :='Hello, Sailor!';

s1 :='Hello, World!';



IF s0=s1 THEN WriteLN('OK') ELSE Writeln('Woozl');

END.

Листинг 134 Пример, демонстрирующий идентификацию типа строк

Откомпилировав его компилятором Free Pascal, заглянем в сегмент данных. Там мы найдем следующую строку:

.data:00404050 aHelloWorld     db 0Dh,'Hello, World!',0 ; DATA XREF: _main+2B^o

Не правда ли, она очень похожа на ASCIIZ-строку? Кому не известен используемый компилятор, тому и на ум не придет, что 0xD – это поле длины, а не символ переноса! Чтобы проверить нашу гипотезу на счет типа, перейдем по перекрестной ссылке, любезно обнаруженной IDA Pro, или самостоятельно найдем в дизассемблированном тексте непосредственный операнд 0x404050

(смещение строки).

push   offset _S1                        ; Передаем указатель на строку-приемник

push   offset aHelloWorld ;"\rHello, World!"    Передаем указатель на строку-источник

push   0FFh                              ; Макс. длина строки

call   FPC_SHORTSTR_COPY

Так-с, указатель на строку передается функции FPC_SHORTSTR_COPY. Из прилагаемой к Free Pascal документации можно узнать, что эта функция работает с короткими Pascal - строками, стало быть, байт 0xD

никакой не символ переноса, а длина строки. А чтобы мы делали, если бы у нас отсутствовала документация на Free Pascal? (В самом же деле, невозможно раздобыть все-все-все компиляторы!). Кстати, штатная поставка IDA Pro, вплоть до версии 4.17 включительно, не содержит сигнатур FPP-библиотек и их приходится создавать самостоятельно.

В тех случаях, когда строковая функция неопознана или отсутствует ее описание, путь один – исследовать код на предмет выяснения алгоритма его работы. Ну что, засучим рукава и приступим?

FPC_SHORTSTR_COPY   proc near           ; CODE XREF: sub_401018+21p

arg_0        = dword      ptr  8              ; Макс. длина строки

arg_4        = dword      ptr  0Ch            ; Исходная строка

arg_8        = dword      ptr  10h            ; Целевая строка

push   ebp



mov    ebp, esp

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

push   eax

push   ecx

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

cld

; Сбрасываем флаг направления

; т.е. заставляем команды LODS, STOS, MOVS

инкрементировать регистр-указатель

mov    edi, [ebp+arg_8]

; Загружаем в регистр EDI значение аргумента arg_8 (смещение целевого буфера)

mov    esi, [ebp+arg_4]

; Загружаем в регистр ESI значение аргумента arg_4 (смещение исходной строки)

xor    eax, eax

; Обнуляем регистр EAX

mov    ecx, [ebp+arg_0]

; Загружаем в ECX значение аргумента arg_0 (макс. допустимая длина строки)

lodsb

; Загружаем в AL первый байт исходной строки, на которую указывает регистр ESI

; и увеличиваем ESI на единицу

cmp    eax, ecx

; Сравниваем первый символ строки с макс. возможной длиной строки

; Уже ясно, что первой символ строки – длина, однако, притворимся, что мы

; не знаем назначения аргумента arg_0, и продолжим анализ

jbe    short loc_401168

; if (ESI[0] <= arg_0) goto loc_401168

mov    eax, ecx

; Копируем в EAX значение ECX

loc_401168:                       ; CODE XREF: sub_401150+14j

stosb

; Записываем первый байт исходной строки в целевой буфер

; и увеличиваем EDI на единицу

cmp    eax, 7

; Сравниваем длину строки с константой 0x7

jl     short loc_401183

; Длина строки меньше семи байт?

; Тогда и копируем ее побайтно!

mov    ecx, edi

; Загружаем в ECX значение указателя на целевой буфер, увеличенный на единицу

; (его увеличила команда STOSB при записи байта)

neg    ecx

; Дополняем ECX до нуля, NEG(0xFFFF) = 1;

; ECX :=1

and    ecx, 3

; Оставляем в ECX три младший бита, остальные – сбрасываем

; ECX :=1

sub    eax, ecx

; Отнимаем от EAX (содержит первый байт строки) "кастрированный" ECX

repe movsb

; Копируем ECX байт из исходной строки в целевой буфер, передвигая ESI

и EDI

; В нашем случае мы копируем 1 байт

mov    ecx, eax



; Теперь ECX содержит значение первого байта строки, уменьшенное на единицу

and    eax, 3

; Оставляем в EAX три младший бита, остальные – сбрасываем

shr    ecx, 2

; Циклическим сдвигом, делим ECX на четыре (22=4)

repe movsd

; Копируем ECX двойных байтов из ESI в EDI

; Теперь становится ясно, что ECX

– содержит длину строки, а, поскольку,

; в ECX загружается значение первого байта строки, можно с полной уверенностью

; сказать, что первый байт строки (причем именно, байт, а не слово) содержит

; длину этой строки

; Таким образом, это – короткая Pascal - строка

;

loc_401183:                       ; CODE XREF: sub_401150+1Cj

mov    ecx, eax

; Если длина строки менее семи байт, то EAX

содержит длину строки для ее

; побайтного копирования (см. условный переход jbe short loc_401168)

; В противном случае EAX содержит остаток "хвоста" строки, который не смог

; заполнить собой последнее двойное слово

; В общем, так или иначе, в ECX загружается количество байт для копирования

repe movsb

; Копируем ECX байт из ESI в EDI

pop    ecx

pop    eax

; Восстанавливаем регистры

leave

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

retn   0Ch

FPC_SHORTSTR_COPY   endp

Листинг 135

А теперь познакомимся с Си-строками, для чего нам пригодится следующий пример:

#include <stdio.h>

#include <string.h>

main()

{

char s0[]="Hello, World!";

char s1[]="Hello, Sailor!";

if (strcmp(&s0[0],&s1[0])) printf("Woozl\n"); else printf("OK\n");

}

Листинг 136

Откомпилируем его любым подходящим Си-компилятором, например, Borland C++ 5.0 (внимание – Microsoft Visual C++ для этой цели не подходит, см. "Turbo-инициализация строковых переменных"), и поищем наши строки в сегменте данных.

Долго искать не приходится – вот они:

DATA:00407074 aHelloWorld     db 'Hello, World!',0    ; DATA XREF: _main+16^o

DATA:00407082 aHelloSailor    db 'Hello, Sailor!',0   ; DATA XREF: _main+22^o



DATA:00407091 aWoozl          db 'Woozl',0Ah,0        ; DATA XREF: _main+4F^o

DATA:00407098 aOk             db 'OK',0Ah,0           ; DATA XREF: _main+5C^o

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

_main        proc near           ; DATA XREF: DATA:00407044o

var_20       = byte ptr -20h

var_10       = byte ptr -10h

push   ebp

mov    ebp, esp

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

add    esp, 0FFFFFFE0h

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

mov    ecx, 3

; Заносим в регистр ECX значение 0x3

lea    eax, [ebp+var_10]

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

lea    edx, [ebp+var_20]

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

push   esi

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

; Именно сохраняем, а не передаем функции, т.к. ESI

еще не был инициализирован!

push   edi

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

lea    edi, [ebp+var_10]

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

mov    esi, offset aHelloWorld    ; "Hello, World!"

; IDA

распознала в непосредственном операнде смещение строки "Hello,World!"

; А если бы и не распознала – это бы сделали мы сами, основываясь на том, что:

; 1) непосредственный операнд совпадает со смещением строки

; 2) следующая команда неявно использует ESI

для косвенной адресации памяти,

;    следовательно, в ESI загружается указатель

repe movsd

; Копируем ECX двойных слов из ESI в EDI

; Чему равно ECX? Оно равно 0x3

; Для перевода из двойных слов в байты умножаем 0x3 на 0x4 и получаем 0xC,

; что на байт короче копируемой строки "Hello,World!", на которую указывает ESI

movsw

; Копируем последний байт строки "Hello, World!" вместе с завершающим нулем



lea    edi, [ebp+var_20]

; Загружаем в регистр EDI указатель на локальный буфер var_20

mov    esi, offset aHelloSailor ; "Hello, Sailor!"

; Загружаем в регистр ESI указатель на строку "Hello, Sailor!"

mov    ecx, 3

; Загружаем в ECX количество полных двойных слов в строке "Hello, Sailor!"

repe movsd

; Копируем 0x3 двойных слова

movsw

; Копируем слово

movsb

; Копируем последний завершающий байт

; // Функция сравнения строк

loc_4010AD:                       ; CODE XREF: _main+4Bj

mov    cl, [eax]

; Загружаем в CL содержимое очередного байта строки "Hello, World!"

cmp    cl, [edx]

; CL

равен содержимому очередного байта строки "Hello, Sailor!"?

jnz    short loc_4010C9

; Если символы обоих строк не равны, переходим к метке loc_4010C9

test   cl, cl

jz     short loc_4010D8

; Регистр CL равен нулю? (В строке встретился нулевой символ?)

; если так, то прыгаем на loc_4010D8

; Теперь мы можем безошибочно определить тип строки –

; во-первых, первый байт строки содержит первый символ строки,

; а не хранит ее длину,

; во-вторых, каждый байт строки проверяется на завершающий нулевой символ

; Значит, это ASCIIZ-строки!

mov    cl, [eax+1]

; Загружаем в CL следующий символ строки "Hello, World!"

cmp    cl, [edx+1]

; Сравниваем его со следующим символом "Hello, Sailor!"

jnz    short loc_4010C9

; Если символы не равны – закончить сравнение

add    eax, 2

; Переместить указатель строки "Hello, World!" на два символа вперед

add    edx, 2

; Переместить указатель строки "Hello, Sailor!" на два символа вперед

test   cl, cl

jnz    short loc_4010AD

; Повторять сравнение пока не будет достигнут символ-завершитель строки

loc_4010C9:                       ; CODE XREF: _main+35j    _main+41j

jz     short loc_4010D8

; см. "Идентификация if – then - else"



; // Вывод строки "Woozl"

push   offset aWoozl ; format

call   _printf

pop    ecx

jmp    short loc_4010E3

loc_4010D8:                       ; CODE XREF: _main+39j    _main+4Dj

; // Вывод строки "OK"

push   offset aOk   ; format

call   _printf

pop    ecx

loc_4010E3:                       ; CODE XREF: _main+5Aj

xor    eax, eax

; Функция возвращает ноль

pop    edi

pop    esi

; Восстанавливаем регистры

mov    esp, ebp

pop    ebp

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

retn

_main        endp

Листинг 137

___строки одного типа

Turbo- инициализация строковых переменных. Не всегда, однако, различить строки так просто. Чтобы убедиться в этом, достаточно откомпилировать предыдущий пример компилятором Microsoft Visual C++, и заглянуть в полученный файл любым подходящим дизассемблером, скажем IDA Pro.

Так, переходим в секцию данных, прокручиваем ее вниз то тех пор, пока не устанет рука (а когда устанет – кирпич на Page Down!) и… Woozl! – никаких следов присутствия строк "Hello, Sailor!" и  "Hello, World!". Зато обращает на себя внимание какая-то странная гряда двойных слов – смотрите:

.data:00406030 dword_406030    dd 6C6C6548h            ; DATA XREF: main+6^r

.data:00406034 dword_406034    dd 57202C6Fh            ; DATA XREF: main +E^r

.data:00406038 dword_406038    dd 646C726Fh            ; DATA XREF: main +17^r

.data:0040603C word_40603C     dw 21h                  ; DATA XREF: main +20^r

.data:0040603E                 align 4

.data:00406040 dword_406040    dd 6C6C6548h            ; DATA XREF: main +2A^r

.data:00406044 dword_406044    dd 53202C6Fh            ; DATA XREF: main +33^r

.data:00406048 dword_406048    dd 6F6C6961h            ; DATA XREF: main +3C^r

.data:0040604C word_40604C     dw 2172h                ; DATA XREF: main +44^r

.data:0040604E byte_40604E     db 0                    ; DATA XREF: main +4F^r

Чтобы это значило? Это не указатели – они никуда не указывают, это не переменные типа int – мы не объявляли таких в программе.


Жмем <F4> для перехода в hex-режим и что мы видим? Вот они наши строки, вот они родимые:

.data:00406030  48 65 6C 6C 6F 2C 20 57-6F 72 6C 64 21 00 00 00 "Hello, World!..."

.data:00406040  48 65 6C 6C 6F 2C 20 53-61 69 6C 6F 72 21 00 00 "Hello, Sailor!.."

.data:00406050  57 6F 6F 7A 6C 0A 00 00-4F 4B 0A 00 00 00 00 00 "Woozl0..OK0....."

Хм, почему же тогда IDA Pro их посчитала двойными словами? Ответить на вопрос поможет анализ манипулирующего со строкой кода, но прежде чем приступить к его исследованию, превратим эти двойные слова в нормальную ASCIIZ - строку. (<U> для преобразования двойных слов в цепочку бестиповых байт и <A> для преобразования ее в строку). Затем подведем курсор к первой перекрестной ссылке и, нажмем <Enter>:

main   proc near           ; CODE XREF: start+AFp

var_20       = byte ptr -20h

var_1C       = dword      ptr -1Ch

var_18       = dword      ptr -18h

var_14       = word ptr -14h

var_12       = byte ptr -12h

var_10       = byte ptr -10h

var_C        = dword      ptr -0Ch

var_8        = dword      ptr -8

var_4        = word ptr -4

; Откуда взялось столько локальных переменных?!

push   ebp

mov    ebp, esp

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

sub    esp, 20h

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

mov    eax, dword ptr aHelloWorld ; "Hello, World!"

; Загружаем в EAX... нет, не указатель на строку "Hello, World!", а

; четыре первых байта этой строки! Теперь понятно, почему ошиблась IDA Pro

; и оригинальный код (до преобразования строки в строку) выглядел так:

; mov  eax, dword_406030

; Не правда ли, не очень наглядно? И если бы, мы изучали не свою, а чужую

; программу, этот трюк дизассемблера ввел бы нас в заблуждение!

mov    dword ptr [ebp+var_10],    eax

; Копируем четыре первых байта строки в локальную переменную var_10

mov    ecx, dword ptr aHelloWorld+4

; Загружаем байты с четвертого по восьмой строки "Hello, World!" в ECX



mov    [ebp+var_C], ecx

; Копируем их в локальную переменную var_C. Но мы-то уже знаем, что это

; никакая не переменная var_C, а часть строкового буфера

mov    edx, dword ptr aHelloWorld+8

; Загружаем байты с восьмого по двенадцатый строки "Hello, World!" в EDX

mov    [ebp+var_8], edx

; Копируем их в локальную переменную var_8, точнее – в строковой буфер

mov    ax, word ptr aHelloWorld+0Ch

; Загружаем оставшийся двух-байтовый хвост строки в AX

mov    [ebp+var_4], ax

; Записываем его в локальную переменную var_4

; Итак, строка копируется по частям в следующие локальные переменные:

; int var_10; int var_0C; int var_8; short int var_4

; следовательно, на самом деле есть только одна локальная переменная –

; char var_10[14]

mov    ecx, dword ptr aHelloSailor ; "Hello, Sailor!"

; Проделываем ту же самую операцию копирования над строкой "Hello, Sailor!"

mov    dword ptr [ebp+var_20],    ecx

mov    edx, dword ptr aHelloSailor+4

mov    [ebp+var_1C], edx

mov    eax, dword ptr aHelloSailor+8

mov    [ebp+var_18], eax

mov    cx, word ptr aHelloSailor+0Ch

mov    [ebp+var_14], cx

mov    dl, byte_40604E

mov    [ebp+var_12], dl

; Копируем строку "Hello, Sailor!" в локальную переменную char var_20[14]

lea    eax, [ebp+var_20]

; Загружаем в регистр EAX указатель на локальную переменную var_20

; которая (как мы помним) содержит строку "Hello, Sailor!"

push   eax          ; const      char *

; Передаем ее функции strcmp

; Из этого можно заключить, что var_20 – действительно хранит строку,

; а не значение типа int

lea    ecx, [ebp+var_10]

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

; хранящую строку "Hello, World!"

push   ecx          ; const      char *

; Передаем ее функции srtcmp

call   _strcmp

add    esp, 8

; strcmp("Hello, World!", "Hello, Sailor!")



test   eax, eax

jz     short loc_40107B

; Строки равны?

; // Вывод на экран строки "Woozl"

push   offset aWoozl ; "Woozl\n"

call   _printf

add    esp, 4

jmp    short loc_401088

;      // Вывод на экран строки "OK"

loc_40107B:                       ; CODE XREF: sub_401000+6Aj

push   offset aOk   ; "OK\n"

call   _printf

add    esp, 4

loc_401088:                       ; CODE XREF: sub_401000+79j

mov    esp, ebp

pop    ebp

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

retn

main   endp

Листинг 138

___о поддержке строк IDA

___"\r\n\a\v\b\t\x1B"

" !\"#$%&'()*+,-./0123456789:;<=>?"

"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"

"`abcdefghijklmnopqrstuvwxyz{|}~"

"АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"

"абвгдежзийклмноп---¦+¦¦¬¬¦¦¬---¬"

"L+T+-+¦¦Lг¦T¦=+¦¦TTLL-г++----¦¦-"

"рстуфхцчшщъыьэюя";

___обработка строк операторами и функцими

___строки фиксированной длины

___паскль пихает строки в сегмента кода


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