asm_text

ассемблерные головоломки\\ или может ли машина понимать естественный язык?

крис касперски ака мыщъх, no-email

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

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

x86-процессоры занимают промежуточное положение. Гибкая система команд и множество способов адресации покрывают практически всю таблицу ASCII, однако, на поиск нужной комбинации могут уйди годы.

Никаких «официальных» правил в этой игре нет. Каждый волен назначать их сам. Код может быть как 16, так и 32-разрядным. Главное, чтобы он не вешал систему и не возбуждал никаких исключений. Теперь поговорим о прочих соглашениях. В 16-разрядном режиме обычно используется com-обрамление. При этом ASCII-строка помешается в текстовой файл, который затем переименовывается в com и передается на выполнение MS-DOS. Задача: вывести что-то на экран, причем, использовать прямой доступ к портам ввода/вывода и видеопамяти нежелательно, т. к. при прогоне программы под Windows NT это приводит к проблемам. Состояние регистров на момент запуска com-файла можно найти в таблице 1.

А вот другой вариант — текстовая строка оформляется в виде массива (например, char x[]=«xxxxxx»), которому передается управление. Задача — прочитать входные аргументы и возвратить в регистре EAX результат вычислений.

Кодировка может быть любой — MS-DOS, WIN, KOI-8, но MS-DOS намного более популярна, хотя использование неанглийский символов алфавита в общем-то не приветствуется.

Для экспериментов нам понадобится: документация на ассемблер (предпочтительнее всего TECH HELP), отладчик (лучше avputil ничего не видел), HEX-редактор (например, HTE), пиво, вобла и некоторое количество свободного времени, а так же творческий настрой.

регистрзначение
AX== 00FFh, если 1-й аргумент командной строки начинается символами X:, где X соответствует букве несуществующего дисковода;
== FF00h, если 2-й аргумент командной строки начинается символами X:, где X соответствует букве несуществующего дисковода;
== FFFFh, если 1-й и 2-й аргументы командной строки ссылаются на несуществующие дисководы;
== 0000h, если 1-й и 2-й аргументы командной строки не ссылаются на несуществующие дисководы.
BX0000
DX==DS
CX00FF
SI0100
IP0100
BP0000
DIFFFE
SPFFFE
CSтекущий сегмент
DSтекущий сегмент
SSтекущий сегмент
флагиODITSZAPC
001000000 == 7202

Таблица 1 начальное состояние регистров на момент загрузки com-файла

Всякая письменность начинается с алфавита. Для кодирования в «текстовой» форме мы должны отчетливо представлять структуру машинной команды со всеми полями, префиксами и прочими превратностями судьбы, которые ее окружают. В этом нам поможет электронный справочник TECHHELP, который в частности можно найти на многих хакерских сайтах. Это настоящая библия программиста под MS-DOS в которой есть практически все!

Рисунок 1 внешний вид электронного помощника TECHHELP!

В первую очередь нас будет интересовать таблица опкодов (80×86/87 Opcodes), так же известная под именем Instruction Set Matrix или просто Матрица. На первый взгляд она выглядит ужасающее, но в действительности, пользоваться ей проще простого:

Рисунок 2 Матрица команд

Матрица представляет собой прямоугольную сетку, напичканную опокодами инструкций. По вертикали откладывается старший полубайт, а по горизонтали младший. Допустим, нас интересует какая инструкция соответствует машинной команде 41h. Откладываем по горизонтали 4x, откладываем по вертикали x1 и в точке их пересечения находит INC CX.

А теперь решим обратную задачу: по известной команде найдем соответствующей ей машинный код. Вот, например: PUSH SS. Находим такую инструкцию в таблице и видим, что она находится в клетке с координатами 1x:x6, значит, ее опкод 16h!

С однобайтовыми командами мы все понятно. Попробуем разобраться с остальными. В таблице видны сокращения: r/m, r8, r16. im8, im16. Что это? «im» это сокращения от «immediate», то есть «непосредственное значение» или «константа», а числа указывают на разрядность в битах. Вот, например, XOR AL,im8. Первый байт команды занимает опкод (34h), второй — непосредственное значение. В частности, XOR AL,69h будет выглядеть так: 34h 69h. А вот другой пример: ADD AX,im16h. Первый байт занимает опкод (05h), а два последних — непосредственное значение типа «слово», причем, младший байт располагается по меньшему адресу. Поэтому, ADD AX, 669h кодируется как 05h 69h 06h. Как видите, все предельно просто.

Сокращения r8 и r16 обозначают поля, кодирующие 8- и 16-разрядные регистры соответственно, а r/m ко всему прочему включает в себя еще и тип адресации, использующийся для доступа к памяти. Это довольно громоздка тема, даже поверхностное описание которой требует как минимум целой главы. И такая глава действительно включена в «Технику и философию хакерских атак», электронную версию которой можно найти на моем ftp-сервере (83.239.33.46). Она лежит в файле /pub/zq-disass.pdf. Добродушно настроенный TheSvin проделал большую работу по поиску ошибок, которые водились там в большом числе и ходили косяками, за что ему большое спасибо. Список исправлений оформлен в виде независимого файла, который находится там же файле /pub/phck1.buglist.chm.

Подавляющая часть r/m и r8/16 сосредоточена в нечитабельных областях таблицы ASCII (т.е. имеет код либо меньше 20h, либо больше 7Fh), поэтому пользоваться ими нам практически не придется. Приятное исключение составляют команды типа: XXX [reg16],reg8/16 и XXX [BP+im8],reg8/16, да и то далеко не со всем набором регистров. Но об этом мы еще поговорим позже, а пока, уподобившись Кириллу и Мефодию, будет составлять Азбуку.

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

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

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

Составленная нами азбука будет выглядеть так:

символкомандаопкод
&es:26h
.DDA27h
.CS:2Eh
/DAS2Fh
?AAS3Fh
@INC AX40h
[POP BX5Bh
\POP SP5Ch
]POP BP5Dh
POP SI5Eh
_POP DI5Fh
`PUSHA60h
>DS:3Eh
6ss:36h
7AAA37h
AINC CX41h
aPOPA61h
BINC DX42h
bBOUND62h
CINC BX43h
cARPL63h
DINC SP44h
dFS:64h
EINC BP45h
eGS:65h
FINC SI46h
fsize:66h
GINC DI47h
gaddr:67h
HDEC AX48h
IDEC CX49h
JDEC DX5Ah
KDEC BX4Bh
LDEC SP4Ch
MDEC BP4Dh
NDEC SI4Eh
ODEC DI4Fh
PPUSH AX50h
QPUSH CX51h
RPUSH DX52h
SPUSH BX53h
TPUSH SP54h
UPUSH BP55h
VPUSH SI56h
WPUSH DI57h
XPOP AX58h
YPOP CX59h
ZPOP DX5Ah

Таблица 2 однобайтовые команды первой группы

символкомандаопкод
$AND AL,im824h
%AND AX, im1625h
4XOR AL, im834h
5XOR AX, im1635h
,SUB AL, im82Ch
-SUB AX,im162Dh
<CMP AL, im82Ch
=CMP AX, im16 3Dh

Таблица 3 двух и трех байтовые команды второй группы

Смотрите! В первую группу попали все заглавные английские буквы, немного строчечный и значительная часть знаков препинания. То есть, закодировать можно практически все, что угодно, только бери и пиши! Компьютер не выбросит исключения и наш код будет вполне успешно исполнен. Правда, восклицательного знака здесь нет. А как же «HELLO,WORLD!». Ведь без восклицательного знака оно будет ущербным, если не сказать неполноценным. Во второй группе команд ничего подобного тоже не наблюдается. Все они начинаются с «посторонних» знаков и даже если передать восклицательный знак как непосредственное значение, получится полная ахинея. Например, AND AL,21h («$!») или CMP AL,21h («<!»). Выглядит отвратно. На самом деле, команда с опкодом 21h все-таки есть. Это, как подсказывает Матрица, AND r/m,r16. Правда, здесь возникает побочный эффект — обращение к памяти, поэтому приходится подбивать такую регистровую пару, которая бы не вызывала исключений, например, AND [SI],SP (21h 24h или «!$») в текстовом представлении. Только надо следить, чтобы SI указывал на память, не содержащую ничего интересного, иначе последствия себя не заставят ждать.

Кстати говоря, символ «$» нам очень пригодится, поскольку он служит завершителем MS-DOS строк. Это существенно отличает его от языка Си, в котором признаком конца строки является символ нуля.

Давайте для разминки наберем в hex-редакторе строку «HELLO,WORLD!$» и попробуем ее дизассемблировать:

00000000: 48 dec ax; уменьшить регистр ax на единицу

00000001: 45 inc bp; увеличить регистр bp на единицу

00000002: 4C dec sp; уменьшить регистр sp на единицу

00000003: 4C dec sp; уменьшить регистр sp на единицу

00000004: 4F dec di; уменьшить регистр di на единицу

00000005: 2C 57 sub al,057 ; отнять от регистра al 57h

00000007: 4F dec di; уменьшить регистр di на единицу

00000008: 52 push dx; затолкать в стек регистр dx

00000009: 4C dec sp; уменьшить регистр sp на единицу

0000000A: 44 inc sp; увеличить регистр sp на единицу

0000000B: 2124 and [si],sp ; *si = sp

Листинг 1 дизассемблерный листинг «HELLO,WORLD!$» с моими комментариями

Как видно, программа тасует регистры и в хвост, и в гриву. При этом, на выходе стек оказывается несбалансированным. С одной стороны мы имеем три команды DEC SP и одну команду PUSH DX (которая уменьшает SP на 2), уменьшающие указатель вершины стека на 5 байт, а с другой — одну команду INC SP. Итого, счет 5:1! Стек оказывается опущенным на 4 байта. Следовательно, далеко не всякую текстовую строку можно непосредственно запихнуть в машинный код. В данном случае, для достижения баланса к тексту требуется добавить еще четыре буквы «D» или две команды POP reg16, которым соответствуют следующие символы: «X[YZ^_]». Например, это может быть «^HELLO,WORLD!$^». А что, выглядит вполне достойно!

Теперь, разобравшись с машинным кодом, перейдем к настоящим головоломкам.

Рисунок 3 строка «HELLO,WORLD!$» и ее машинное представление

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

00000000: B4 09 MOV AH,009

00000002: BA 08 01 MOV DX,00108

00000005: CD 21 INT 021h

00000007: C3 RETN

00000008: 48 45 4C 4C-4F 2C «HELLO,

0000000E: 57 4F 52 4C 44 21-24 WORLD!$»

Листинг 2 ассемблерная программа, выводящая строку «HELLO,WORLD!» на экран и ее машинный код

Практически все символы этой программы нечитабельны, то есть не могут быть напрямую введены с клавиатуры и приходится хитрить. Начнем с «MOV AH, 09h», заносящую в регистр AH код сервисной функции, ответственный за телетайпный вывод. Заглянув в Матрицу, мы с огорчением наблюдаем, что все команды пересылки регистров MOV/LEA имеют опкод превышающий 7Fh, то есть «вылезающий» за американскую часть кодировки ASCII. Ладно, не дают нам MOV'а и не надо! Используем математические операции! В нашем распоряжении есть INC reg16/DEC reg16, SUB и XOR. Не такой уж и богатый выбор!

Поскольку, начальное значение регистра AX равно 0000h, для достижения задуманного, нам достаточно вычесть из него значение F700h, что равносильно сложением с 900h. В машинном представлении это будет выглядеть приблизительно так:

00000000: 2D 00 F7 sub ax,0F700

Листинг 3 подготовка регистра AH в работе (предварительный вариант)

Опс! Сразу два байта вылетают в штрафбат. Это 00h и F7h. Черт возьми! Как же быть? Надо подумать… А что если вычислить значение не все сразу, а по частям? Короче говоря, нужно разложить F700h на ряд слагаемых, каждое из которых находилось бы в заданном интервале. Точнее даже не интервале, а каждый байт, входящий в слово, удовлетворял бы условию 80h > x > 1Fh. Чем не головоломка? Любители математики легко найдут строгое решение, а всем остальным придется довольствоваться методом перебора. Вот, например, если от F700h шесть раз отнять по 292Ah, останется всего 4, которые можно накрутить обычным DEC AX (впрочем, в данном случае «крутить» совершенно необязательно, поскольку при AH == 9, значение регистра AL игнорируется). В общем, наш аналог MOV AX, 9 будет выглядеть так:

00000000: 2D 2A 29 sub ax,0292A

00000003: 2D 2A 29 sub ax,0292A

00000006: 2D 2A 29 sub ax,0292A

00000009: 2D 2A 29 sub ax,0292A

0000000C: 2D 2A 29 sub ax,0292A

0000000F: 2D 2A 29 sub ax,0292A

00000012: 48 dec ax

00000013: 48 dec ax

00000014: 48 dec ax

00000015: 48 dec ax

Листинг 4 подготовка регистра AH в работе (окончательный вариант)

А в текстовом виде: «-*)-*)-*)-*)-*)-*)HHHH». Для проверки работоспособности программы, запустим ее под отладчиком:

Рисунок 4 проверка работоспособности фрагменты программы под отладчиком

Ура! Получилось! Регистр AH послушно обратился в 09h и ни одного ASCII символа при этом не пострадало. Впрочем, это не единственный, и, к тому же не самый короткий, вариант. Можно, например, «подтянуть» регистр AL к 09h (в этом нам помогут команды INC AX), а затем переслать AL в AH. Стоп! Ведь команд пересылки у нас нет! Ни MOV, ни XCHG не работают! Но… зато в нашем распоряжении есть стек! А стек это могучая вещь! Команда PUSH reg16 забрасывает 16-разрядный регистр на верхушку, а POP reg16 стаскивает его оттуда. Команд для работы с 8-разярдными регистрами нет, а это, значит, что AL и AH мы никак не обменяем, во всяком случае если действовать в лобовую. Нет, тут нужен совсем другой подход! Что такое машинное слово? Совокупность двух байт, так? Причем, младший байт лежит по меньшему адресу, а за ним следует старший.

Немного медитации и… Постойте, но ведь если заслать в стек регистр AX, затем уменьшить указатель верхушки стека на единицу и извлечь регистр AX, то в AL попадет мусор, а в AH — младший байт оригинального регистра AX, в результате чего наша задача будет решена! Весь код угадывается в 0Bh байт, что на 0Ah байт короче, чем в прошлый раз. Это надо обмыть!

00000000: 40 inc ax

00000001: 40 inc ax

00000002: 40 inc ax

00000003: 40 inc ax

00000004: 40 inc ax

00000005: 40 inc ax

00000006: 40 inc ax

00000007: 40 inc ax

00000008: 40 inc ax

00000009: 50 push ax

0000000A: 4C dec sp

0000000B: 58 pop ax

Листинг 5 подготовка регистра AH в работе (улучшенный вариант)

С регистром DX мы разделываемся аналогичным образом (многократным вычитанием), а вот с «INT 21h» (CDh 21h) все обстоит значительно сложнее и без самомодифицирующегося код здесь просто никак. В нашем арсенале есть по меньшей мере две команды для работы с памятью: sub byte:[index_reg16],reg8 и sub byte:[BP+im8],reg8.

Естественно, для этого необходимо знать смещение команды «INT 21h» в машинном коде, а на данном этапе оно еще не известно, т. к. перед ним располагается самомодифицирующийся код, длину которого мы еще не готовы назвать. Хорошо, условимся считать, что «INT 21h» располагается по смещению 66h от начала файла, что соответствует 166h в памяти (базовый адрес загрузки для com-файлов равен 66h).

Начальное значение регистра SI равно 100h, что существенно упрощает нашу задачу. Остается разобраться с INT 21h (СDh 21h). Если закодировать эту команду как 23h 21, а затем отнять от нее 56h, мы добьемся того, что так долго искали. В машинном представлении это может выглядеть так:

00000000: 56 push si

00000001: 5D pop bp

00000002: 6A56 push 056

00000004: 59 pop cx

00000005: 28 4E 66 sub [bp][00066],cl

00000066: 23 21

Листинг 6 формирование инструкции INT 21h с помощью самомодифицирующегося кода

Этому соответствует следующая текстовая строка: «V]jVY(Nf…#!». Не слишком литературно, конечно, но зато целиком из печатных символов! Команда «RETN» с опкодом C3h укрощается аналогично.

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

Впрочем, это всего лишь начало. Настоящее веселье наступает потом, когда хакер пытается превратить «читабельный» текст в осмысленную фразу. Очевидно, что наш первоначальный вариант (абракадабра в стиле «-*)-*)-*)-*)-*)-*)HHHH…V]jVY(Nf…#!») ничем подобным не является.

Приходится «разлагать» числа на слагаемые так, чтобы эти сами слагаемые представляли осмысленные комбинации букв, «разбавляемые» машинными командами из первой группы (см. таблицу 2), для ликвидации подобных эффектов от которых использовать противоположные им команды… В конечном счете образуется какая-то дикая текстовая строка, с кучей посторонних символов, но зато работающая!!! Чтобы не обламывать кайф, никаких законченных решений здесь не приводится. Первую «строку» создать всегда трудно. Главное — найти идею, понять общий принцип. В готовом виде все выглядит скучно и неинтересно.

На первых порах, можно смягчить условия и расширить доступный алфавит русскими буквами и символами псевдографики, а затем, по мере накопление опыта его постепенно ужесточать. Матерые хакеры, напротив, ограничиваются _только_ заглавными английскими буквами и к тому же соревнуются одновременно по размеру кода (в байтах), скорости его выполнения (в тактах старого доброго 8086) и времени решения задачи (в часах с точностью до минуты). До международных соревнований дело, конечно, не доходит, но в локальных схватках кровь кипит только так.

Когда я говорил, что составление таких строк не имеет никакого практического значения, я слегка привирал. Ведь это замечательный защитный трюк, сбивающий хакеров с толку. Если фрагмент программы выглядит как строка и передача управления на него тщательно замаскирована, далеко не каждый хакер с лету разберется что к чему. Как вариант, «машинные» текстовые строки можно использовать как серийные номера, расшифровывающие программу или добавляющие к ней недостающую функциональность (естественно, перед запуском на выполнение необходимо проверить их CRC, вдруг пользователь ошибся при вводе). Еще можно написать своего автономного рода, блуждающего по сети и управляемого посредством вот таких текстовых строк. Еще интереснее написать MS-DOS вируса, представляющего собой текстовое послание. В общем, как говорится, главное фантазию иметь, а области применения будут!