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