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

         

Алфавит


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



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


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

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



Извращения начинаются


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

 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!$"



ассемблерная программа




Практически все символы этой программы нечитабельны, то есть не могут быть напрямую введены с клавиатуры и приходится хитрить. Начнем с "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



подготовка регистра 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: 6A 56                        push      056

 00000004: 59                           pop       cx

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

 00000066: 23 21



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


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

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



подготовка регистра 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



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


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



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


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

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

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

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

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



внешний вид электронного помощника TECH HELP!


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



Матрица команд


Матрица представляет собой прямоугольную сетку, напичканную опокодами инструкций. По вертикали откладывается старший полубайт, а по горизонтали младший. Допустим, нас интересует какая инструкция соответствует машинной команде 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. Добродушно настроенный The Svin проделал большую работу по поиску ошибок, которые водились там в большом числе и ходили косяками, за что ему большое спасибо. Список исправлений оформлен в виде независимого файла, который находится там же файле /pub/phck1.buglist.chm.


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

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

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

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

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

символ

команда

опкод

&

es:

26h

.

DDA

27h

.

CS:

2Eh

/

DAS

2Fh

?

AAS

3Fh

@

INC AX

40h

[

POP BX

5Bh

\

POP SP

5Ch

]

POP BP

5Dh

^

POP SI

5Eh

_

POP DI

5Fh

`

PUSHA

60h



DS:

3Eh

6

ss:

36h

7

AAA

37h

A

INC CX

41h

a

POPA

61h

B

INC DX

42h

b

BOUND

62h

C

INC BX

43h

c

ARPL

63h

D

INC SP

44h

d

FS:

64h

E

INC BP

45h

e

GS:

65h

F

INC SI

46h

f

size:

66h

G

INC DI

47h

g

addr:

67h

H

DEC AX

48h

I

DEC CX

49h

J

DEC DX

5Ah

K

DEC BX

4Bh

L

DEC SP

4Ch

M

DEC BP

4Dh

N

DEC SI

4Eh

O

DEC DI

4Fh

P

PUSH AX

50h

Q

PUSH CX

51h

R

PUSH DX

52h

S

PUSH BX

53h

T

PUSH SP

54h

U

PUSH BP

55h

V

PUSH SI

56h

W

PUSH DI

57h

X

POP AX

58h

Y

POP CX

59h

Z

POP DX

5Ah


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


Ура! Получилось! Регистр 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



однобайтовые команды первой группы


символ

команда

опкод

$

AND AL,im8

24h

%

AND AX, im16

25h

4

XOR AL, im8

34h

5

XOR AX, im16

35h

,

SUB AL, im8

2Ch

-

SUB AX,im16

2Dh

CMP AL, im8

2Ch

=

CMP AX, im16

3Dh



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


Смотрите! В первую группу попали все заглавные английские буквы, немного строчечный и значительная часть знаков препинания. То есть, закодировать можно практически все, что угодно, только бери и пиши! Компьютер не выбросит исключения и наш код будет вполне успешно исполнен. Правда, восклицательного знака здесь нет. А как же "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



Поиск текстовых строк, интерпретируемых как


Поиск текстовых строк, интерпретируемых как осмысленный код, — очень древнее увлечение, которым "болели" еще во времена "динозавров". В зависимости от структуры машинной команды, сложность решения задачи варьируются в очень широких пределах. Некоторые платформы вообще не позволяют написать ничего осмысленного, некоторые делают это настолько тривиальным, что пропадает весь интерес.
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-й аргументы командной строки не ссылаются на несуществующие дисководы.
BX
0000
DX
==DS
CX
00FF
SI
0100
IP
0100
BP
0000
DI
FFFE
SP
FFFE
CS
текущий сегмент
DS
текущий сегмент
SS
текущий сегмент
флаги
ODITSZAPC
001000000 == 7202

я говорил, что составление таких


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