• Познакомьтесь с пентестом веб-приложений на практике в нашем новом бесплатном курсе

    «Анализ защищенности веб-приложений»

    🔥 Записаться бесплатно!

  • CTF с учебными материалами Codeby Games

    Обучение кибербезопасности в игровой форме. Более 200 заданий по Active Directory, OSINT, PWN, Веб, Стеганографии, Реверс-инжинирингу, Форензике и Криптографии. Школа CTF с бесплатными курсами по всем категориям.

Статья ASM – анатомия кодировки "Base64"

Традиционные способы передачи данных по протоколу HTTP имеют существенный недостаток – это транспорт исключительно для текстовой информации, о чём и свидетельствует его название "Hyper-Text Transfer Protocol" (протокол передачи гипер-текста). Он находится на самом верхнем уровне(7) модели OSI, а потому пользователям в нашем лице приходится взаимодействовать именно с ним. Это создаёт определённые трудности, когда нужно передать по e-mail не текстовые, а бинарные данные ..например картинки, аудио/видео-файлы, или зипнутые архивы. Вопрос встал остро с приходом графических интерфейсов, и чтобы организовать обмен двоичной информацией по текстовым каналам, было решено кодировать BIN в читаемые символы ASCII. Решать проблемы именно такого характера выпало на долю алгоритма Base64 – рассмотрим его реализацию.

Оглавление:

1. MIME – общие сведения;
2. Принцип ручного кодирования Base64;
3. Алгоритм декодирования;
4. Родственный алгоритм Base32;
5. Эпилог.
----------------------------------------------------


1. Общие сведения

Перед тем-как положить Base64 на операционный стол, не помешает рассмотреть область её применения. Этот раздел можно смело пропустить без ущерба понимания основ кодирования информации, однако новичкам в данной области настоятельно рекомендуется к прочтению.

В общем случае, кодировка Base64 живёт внутри сетевого трафика, частью которого является MIME – Multipurpose Internet Mail Extension, или просто зоопарк расширений почты. Стандарт MIME описывает спецификация
(предшественником является ARPA, rfc-822), где в красках расписываются способы передачи по сети файлов различного типа: изображений, музыки, текста и т.д. Фактически, благодаря именно MIME мы имеем возможность пересылать по почте не только текст, но и другие виды данных.

Идея передачи BIN по текстовым каналам с помощью MIME применяется не только для электронных вложений. Например в браузерах, стандарт используется для определения типа контента, что позволяет ему открывать в своём окне файлы *.doc/pdf/mp3 и т.д, при помощи соответствующих программ. Названия типов указываются в разделе "Content-Type" заголовка данных, в формате "тип/подтип". Среди
MIME-типов стоит отметить следующие:

• text/plain - обычный текст (тип по умолчанию);
• text/html - гипертекст;
• image/jpg - изображение;
• audio/mp3 - аудиозапись;
• video/mp4 - видеофайл;
• application/zip - ZIP-архив;
• multipart/mixed - текст с разнообразным вложением;
• message/rfc822 – сообщение в сообщении.

На самом деле их огромное кол-во, а система хранит поддерживаемый ею лист в разделе реестра [HKEY_CLASSES_ROOT]. Когда браузер встречает в контексте принятых данных незнакомый ему MIME-тип, то в надежде найти его он лезет именно в этот раздел и сопоставив значение ключа "Content-Type" с расширением (в левом окне на рис.ниже), пытается привязать к нему соответствующий софт. Если найденное приложение удовлетворяет системе безопасности браузера, он запустит контент прямо в своей тушке:

Mime_Reg.png


Чтобы пролить свет на вышесказанное, я провёл такой эксперимент..
С одной своей почты на яндексе, отправил себе-же письмо на mail.ru со-вложенной картинкой(png) и архивом(zip). Теперь зайдя на мэйл, сохранил письмо на диск в формате *.eml, после чего открыл его в обычном текстовом редакторе. В результате, получил все технические сведения об отправителе, но в данном случае мне интересны лишь MIME-заголовок и тело сообщения в том виде, в котором оформил его почтовый сервер яндекса:


Код:
;//------------8<---------- Вырезано ----------------8<-------------

MIME-Version:    1.0
   X-Mailer:     Yamail [ http://yandex.ru ] 5.0
   Message-Id:   <952291608987613@sas1-941a8864505d.qloud-c.yandex.net>
   Content-Type: multipart/mixed;
   boundary="----==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net"

------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset=utf-8

Привет

------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net
Content-Type:        application/zip; name="123.zip"
Content-Disposition: attachment; filename="123.zip"
Content-Transfer-Encoding: base64

UEsDBBQAAgAIAFqVeVATGlqDwAMAAL8IAAAJAAAAQ1JDMzIuYXNtrVVda9tIFH0P+D/cxy7ITdO+
BJdC2jRhH1K6ZHehEEqRJXfdxbGM7TTp3wu7j2Up2x8wVj3xWNKM9VHbLQrrvTMjybIr+lSBGI1m
aqCykWIctgLH8NLlUn+aS5GW0PutIc5aXbtX2/kfUEsBAhQAFAACAAgAWpV5UBMaWoPAAwAAvwgA
AAkAAAAAAAAAAAAgAAAAAAAAAENSQzMyLmFzbVBLBQYAAAAAAQABADcAAADnAwAAAAA=

------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net
Content-Type:        image/png; name="lilu.png"
Content-Disposition: attachment; filename="lilu.png"
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSUhEUgAAASwAAADsCAMAAAD5PGtHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
bWFnZVJlYWR5ccllPAAAAMBQTFRFzGkxzqyX1tTR14ldso511Lan0HZHtXBJuGYzs0sSbSIL+fv6
c5wmi0WXSySLXkPbPTc2EQ/e8mFPj4hGtgysISRLMaF1pjUXIu/p+Ma2f2MunAgJK3Y/B+sckNRz
V1hqtIybxza1/jxrNNDAwmZVCXx5UmpD0HDBYEe4lKY6wdqg1jSx7I4pEvh8LAGjCZdJU9uo0Br3
030ZBE8zyaqEQcUFMT3rYFkvS8/zTsui0vp/AgwAbOvpPweEKAAAAAAASUVORK5CYII=

------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net--

Код:
MIME-Version:      1.0
   X-Mailer:       Yamail [ http://yandex.ru ] 5.0
   Content-Type:   multipart/mixed;
   boundary="----==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net"

MIME-заголовок начинается со-строки с указанием версии стандарта(1.0).
Строка "X-Mailer" сообщает, каким софтом сервер создал и отправил письмо – здесь это "Yamail ver.5.0" (видимо почтовый клиент "Yahoo Mail"). Далее указывается тип письма "multipart/mixed", что характеризует его как "письмо со-смешанными данными". Аргумент последней строки "boundary" информирует, какую последовательность символов нужно воспринимать в качестве "разделителя частей" одного письма. Сервер генерит его от-балды, а потому в следующий раз он будет уже иным.

На этом, интересующий нас контент в MIME-заголовке заканчивается и дальше идут непосредственно части электронного письма:


Код:
------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit

Привет

Как видим, перед нами текстовая часть послания типа "text/plain" – обычный текст. О том, что это действительно отдельный фрагмент содержимого, свидетельствует разделитель. Если обратить внимание, он дополняется вначале двумя дефисами, а не в точности как предписывал "boundary" в MIME-заголовке.

Поскольку в тексте письма было всего одно слово кириллицей "Привет", то серверу пришлось перекодировать его в UTF-8 (Unicode Trans-Format), алфавит которого требует как-минимум все/256 символов расширенной таблицы ASCII. Агент оповещает нас об этом строкой "Content-Transfer-Encoding: 8bit", т.е. 8-битная кодировка, т.к. помимо латиницы, UTF использует и символы кириллицы из верхней половины таблицы с 80h до FFh.


ASCII.png


Чтобы продемонстрировать разницу, в повторном письме я отправил текст латиницей, буквально все печатные символы которой лежат уже в нижней половине таблицы 00..7Fh. Для кодирования этого диапазона достаточно 7-бит, поэтому сервер пропустил текст "в чём мать родила", не забыв отметить сей факт в заголовке части. Таким образом, если в хидере фрагмента мы видим "Encoding: 7bit", значит данные не требуют преобразования на приёмном узле. Это касается не только текста, но и любого содержимого (см.спеку):

Код:
------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net
Content-Type:  text/plain
Content-Transfer-Encoding: 7bit

Hello World!

Теперь, дадим "верблюду по-почкам" и двинемся дальше..
Здесь мы натыкаемся на следующие две части письма, по заголовкам которых можно сделать вывод, что это вложенные архив и картинка. Как и в предыдущем случае, обе части начинаются с разделителей, с указанием типа их содержимого "Content-Type: application/zip", а в нижнем фрагменте "image/png":


Код:
------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net
Content-Type:              application/zip; name="123.zip"
Content-Disposition:       attachment; filename="123.zip"
Content-Transfer-Encoding: base64

UEsDBBQAAgAIAFqVeVATGlqDwAMAAL8IAAAJAAAAQ1JDMzIuYXNtrVVda9tIFH0P+D/cxy7ITdO+
BJdC2jRhH1K6ZHehEEqRJXfdxbGM7TTp3wu7j2Up2x8wVj3xWNKM9VHbLQrrvTMjybIr+lSBGI1m
aqCykWIctgLH8NLlUn+aS5GW0PutIc5aXbtX2/kfUEsBAhQAFAACAAgAWpV5UBMaWoPAAwAAvwgA
AAkAAAAAAAAAAAAgAAAAAAAAAENSQzMyLmFzbVBLBQYAAAAAAQABADcAAADnAwAAAAA=

------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net
Content-Type:              image/png; name="lilu.png"
Content-Disposition:       attachment; filename="lilu.png"
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSUhEUgAAASwAAADsCAMAAAD5PGtHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
bWFnZVJlYWR5ccllPAAAAMBQTFRFzGkxzqyX1tTR14ldso511Lan0HZHtXBJuGYzs0sSbSIL+fv6
c5wmi0WXSySLXkPbPTc2EQ/e8mFPj4hGtgysISRLMaF1pjUXIu/p+Ma2f2MunAgJK3Y/B+sckNRz
V1hqtIybxza1/jxrNNDAwmZVCXx5UmpD0HDBYEe4lKY6wdqg1jSx7I4pEvh8LAGjCZdJU9uo0Br3
030ZBE8zyaqEQcUFMT3rYFkvS8/zTsui0vp/AgwAbOvpPweEKAAAAAAASUVORK5CYII=

------==--bound.94002.sas2-4fe1bb3c0a49.qloud-c.yandex.net--

Вот мы и подобрались к кодированию бинарной информации в текст..
Поле "Encoding: base64" прямым текстом сообщает, что содержимое находится под алгоритмом Base64, т.к. передача его в явном (двоичном) виде не поддерживается текущим почтовым сервером. Есть и другой важный момент – спека MIME требует, чтобы в каждой (не считая последней) строке закодированных данных было не более 76-символов, после которых должен следовать перенос в виде пары CR/LF = 0D/0Ah. Обратите внимание, что маркером информационного конца является тот-же разделитель "boundary", но дополненный в хвосте двумя терминальными символами(--).

Разбор заголовков сырых писем весьма увлекательное занятие. Например учитывая, что каждый из серверов в HTTP-заголовках оставляет свой айпи, можно вычислять по ним спамеров. Или если на Ваш адрес пришла корреспонденция из какой-нибудь корпоративной сети, можно узнать расположение отправителя, вплоть до его кабинета. В общем есть чем позабавиться..


2. Алгоритм кодирования "Base64"

Чтобы из бинарного потока сделать печатную строку, нужно 8-битный байт превратить в 6-битный индекс, который будет указывать на конкретный элемент в "таблице-символов" Base64. Причём алгоритм обязательно должен быть обратимым, чтобы на приёмном узле можно было восстановить переданную информацию. Ясно, что решить проблему без потерь никак не получится, поэтому в Base64 имеем соотношение 4/3. То-есть из поступающего конвейера берём пачками по 3-двоичных байта, а на выход отправляем по 4 символа. В результате получаем лишний вес ~33%.

Забегая вперёд скажу, что это неплохой показатель. К примеру у алгоритма Base32 расход вообще 60% с соотношением 8/5, а у Base16 – все 100%, когда каждый бинарный байт кодируется двумя печатными символами. Именно поэтому повсеместно используют исключительно 64-символьный алфавит, а не 32 или 16. В таблицы ниже представлены наборы символов, которые разрешено использовать в соответствующих алгоритмах кодирования:


Alphabet.png


Ниже представлена схема кодирования Base64..
Суть в том, что рассматривая каждые 3х8=24 бит как одно/целое, на выходе преобразуем их в форму 4х6=24 бит. Это средний блок рисунка ниже. Поскольку в алфавите Base64 всего 64 символа, то 6-битного индекса как-раз хватит, чтобы выбрать любой из имеющихся в нём символов: 2^6=64.

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


EnCoder.png


Как видим, алгоритм довольно прост, зато решает вполне серьёзные проблемы.
При его реализации мы должны учитывать следующие моменты:

1. Поскольку читать из источника нужно строго 3-байтными блоками, то и размер входных данных должен быть кратен(3). Если это не так, на подготовительном этапе просто дополняем хвост необходимым кол-вом нулей, и обязательно запоминаем их число. По окончании, вместо этих нулей в выходной поток нужно будет вставить символы(=), которые послужат "маркерами лишних байт" декодеру Base64. Например если на входе имеем 10-байт, значит один лишний и добавляем пару символов(==). Таким образом, при кодировании информации входной поток должен быть всегда кратен(3), а выходной будет кратен(4).
2. Процессоры х86 устроены так, что при чтении "из памяти в регистр" получим обратный порядок байт. К примеру, если в памяти лежит слово со-значением 0х2548, то в регистр оно попадёт в виде 4825h (см.отладчик). Значит после чтения очередной партии данных, нужно непременно восстанавливать оригинальный порядок, для чего предусмотрена инструкция BSWAP (Byte Swap).
3. Будем считать, что взяли дворд данных в регистр EDX – этот этап представлен на рис.выше самым первым/синим блоком. Теперь, если в какой-нибудь пустой регистр из EDX вытолкнуть влево 6-бит, то как-раз получим готовый к употреблению индекс. С одного регистра в другой выдвигает биты инструкция SHLD (Double Precision Shift Left). У неё три операнда – приёмник, источник, и счётчик бит для сдвига. Например, команда SHLD EAX,EDX,6 волшебным образом сбросит индекс в регистр AL, избавляя нас от возни с отдельными битами.
4. Очередная инструкция XLATB залезет в алфавит-кодировки Base64, и используя значение регистра AL в качестве индекса, вытащит из него соответствующий символ. Всё.. вот и весь алгоритм, на который потратили всего-то три инструкции: BSWAP, SHLD и XLATB.

Система команд современных процессоров поражает своим разнообразием. В наборе есть как простые, так и составные команды – по сути этим и отличаются процессоры архитектуры CISC (Complex Instruction Set Computing) от процессоров с одиночными инструкциями RISC. Не смотря на то, что большинство из этих команд специфичны и нужны далеко не всегда, для каждого шага кодирования Base64 всё-же нашлась отдельная инструкция. В результате, можно реализовать на ассемблере такую процедуру, которая будет летать на реактивной скорости, и не нужны никакие библиотеки высокого уровня.

Код ниже демонстрирует, как в 28-ми байтах запрограммировать полноценный кодер Base64. Такой размер без зазрения совести можно таскать с собой, задействуя его в токсических зонах по мере необходимости.


C-подобный:
          xor     eax,eax         ;// очистить EAX
@encode:  mov     edx,dword[esi]  ;// EDX = очередные 4-байта данных (нам нужны только 3 из них)
          bswap   edx             ;// восстановить в нём порядок байт
          mov     ecx,4           ;// счётчик внутреннего цикла LOOP (4-раз по 6-бит)

@next:    shld    eax,edx,6       ;// выдвинуть 6-бит из EDX в EAX (они попадут в AL)
          shl     edx,6           ;// удалить их из EDX (shld сам не удаляет)
          xlatb                   ;// взять в AL символ из алфавита, по индексу в AL
          stosb                   ;// сохранить его в приёмнике!
          xor     al,al           ;// очистить AL
          loop    @next           ;// промотать внутр.цикл ECX-раз..

          add     esi,3           ;// указатель в источнике на сл.триаду байт
          dec     [counter]       ;// счётчик по длине файла -1
          jnz     @encode         ;// на повтор, пока он > 0

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


C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;//-----------
.data
Base64:  ;//<----------------- Алфавит 64-символов
times 26   db  % +('A'-1)    ;// ABCDEFGHIJKLMNOPQRSTUVWXYZ
times 26   db  % +('a'-1)    ;// abcdefghijklmnopqrstuvwxyz
times 10   db  % +('0'-1)    ;// 0123456789
           db  '+/',0        ;// +/

title      db  '*** Base64 Coder v0.1 ***',0
outFile    db  'BinTo64.txt',0

fileName   rb  128    ;// под имя входного файла с данными
fileHndl   dd  0      ;//   ..его дескриптор,
fileSize   dd  0      ;//       ..и размер.

readData   dd  0      ;// указатель для чтения файла
writeData  dd  0      ;// линк на память с закодированными данными
writeSize  dd  0      ;//   ..размер готовых данных, для записи в файл

shlCycle   dd  0      ;// кол-во триад байтов в исходном файле
complete   dd  0      ;//
buff       db  0      ;//
;//-----------

.code
start:    invoke  SetConsoleTitle,title
;//====== Запросим у юзера файл для открытия
         cinvoke  printf,<10,'   Open file name:  ',0>
         cinvoke  scanf,<'%s',0>,fileName

;//====== Попытка его открыть..
          invoke  _lopen,fileName,OF_READ
          mov     [fileHndl],eax
          cmp     eax,-1
          jne     @f
         cinvoke  printf,<10,'*** ERROR! File not found! ***',0>
          jmp     @exit

;//====== ОК! Размер файла, и боксим его на консоль
@@:       invoke  GetFileSize,eax,0
          mov     [fileSize],eax
         cinvoke  printf,<'  Input file size:  %i byte',0>,eax

;//====== Выделить память, и считать туда файл.
;//====== адрес сохраняем в переменную "readData"
          invoke  VirtualAlloc,0,[fileSize], MEM_COMMIT, PAGE_READWRITE
          mov     [readData],eax
          invoke  _lread,[fileHndl],eax,[fileSize]
          invoke  _lclose,[fileHndl] ;// данные считали, файл больше не нужен

;//====== Получить кол-во триад (длину цикла),
;//====== и остаток для дополнения символом(=)
;//====== если остаток(1), то делаем его двойкой, иначе - наоборот.
          mov     eax,[fileSize]     ;// размер файла
          mov     ebx,3              ;// делитель
          xor     edx,edx            ;// очистить место под остаток
          div     ebx                ;// разделить EAX на EBX
          or      edx,edx            ;// проверить остаток на нуль
          je      @okey              ;// если нет остатка, значит размер кратен(3)
          inc     eax                ;// иначе: увеличить кол-во триад

          cmp     edx,1              ;// проверить остаток на 1
          jne     @f                 ;// если нет, значит остаток=2, и отнимаем(1)
          inc     edx                ;// иначе: увеличить на 1                  |
          jmp     @okey              ;//                                        |
@@:       dec     edx                ;// <--------------<<----------------------+

@okey:    add     [fileSize],edx     ;// сделать размер кратным трём
          mov     [complete],edx     ;// запомнить остаток для добавления(=)
          mov     [shlCycle],eax     ;// запомнить число проходов (триад)

;//====== Выделить память, куда будем сбрасывать закодированные данные
          invoke  VirtualAlloc,0,[fileSize], MEM_COMMIT, PAGE_READWRITE
          mov     [writeData],eax    ;// запомнить указатель

;//====================================================================
;//====== КОДИРОВАНИЕ BASE64 ==========================================
;//====================================================================
          mov     ebx,Base64         ;// адрес алфавита для XLATB
          mov     esi,[readData]     ;// источник данных,
          mov     edi,[writeData]    ;//   ..и их приёмник для записи в файл.
          mov     ebp,[shlCycle]     ;// счётчик чтения по 3-байта
          xor     eax,eax            ;// очистить EAX

@encode:  mov     edx,dword[esi]     ;// EDX = очередные 4-байта из файла
          bswap   edx                ;// восстановить в нём порядок байт
          mov     ecx,4              ;// счётчик внутреннего цикла для LOOP
@@:       shld    eax,edx,6          ;// выдвинуть 6-бит из EDX в EAX
          shl     edx,6              ;// удалить их из EDX (shld не удаляет)
          xlatb                      ;// взять в AL символ из алфавита, по индексу в AL
          stosb                      ;// сохранить его в приёмнике!
          xor     al,al              ;// очистить AL
          loop    @b                 ;// промотать внутр.цикл ECX-раз..
          add     esi,3              ;// указатель в источнике на сл.триаду байт
          dec     ebp                ;// число проходов -1
          jnz     @encode            ;// повторить, пока EBP > 0
;//=====================================================================
          mov     ecx,[complete]     ;// кол-во дополнителей(=)
          sub     edi,ecx            ;// отсечь хвост (если он есть) в выходном потоке
          mov     al,'='             ;// что вместо них вставить
          rep     stosb              ;// замена!

;//====== Вычисляем размер закодированных "Base64" данных
;//====== для этого будем искать нуль, которым заканчиваются данные.
          mov     edi,[writeData]    ;// EDI = адрес начала данных
          xor     ecx,ecx            ;// ставим счётчик ECX,
          dec     ecx                ;//   ..на макс = FFFFFFFFh
          xor     al,al              ;// AL=0 (что искать)
          repne   scasb              ;// поиск AL по всей строке EDI
          not     ecx                ;// инверсия счётчика
          dec     ecx                ;// ECX = размер данных!
          push    ecx                ;//
         cinvoke  printf,<10,' Output file size:  %i byte  (BinTo64.txt)',0>,ecx

;//====== Сбросить закодированные данные на консоль, и в файл "BinTo64.txt"
         cinvoke  printf,<10,10,'%s',0>,[writeData]
          invoke  _lcreat,outFile,0   ;// создать файл: 0=RW, 1=R, 2=Hidden, 3=System.
          mov     [fileHndl],eax      ;// запомнить его дескриптор
          pop     ebx                 ;// EBX = размер данных для записи
          invoke  _lwrite,eax,[writeData],ebx  ;// куда, от куда, сколько байт!
          invoke  _lclose,[fileHndl]  ;// прихлопнуть файл

@exit:   cinvoke  scanf,<'%s',0>,buff ;// GAME OVER <<--------------------------------
         cinvoke  exit,0              ;//

;//-----------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
include 'api\kernel32.inc'

BaseResult.png


Алгоритму Base64 без разницы, какого типа данные поступают на вход – хоть двоичные, хоть текст. В этом примере нет переноса строки на каждом 76-ом символе, поскольку отформатировать текст можно и после кодирования. Хотя без проблем можно было и дополнить код этим функционалом. Тут всё зависит от задачи – если собираемся программно передавать бинарный файл по почте, то нужно вставить разрывы строк, т.к. декодер удалённого браузера рассчитывает на это (см.вводную часть). В данном случае я просто перекодировал текстовый файл, чтобы спрятать его содержимое от глаз любопытных юзверов. Учитывая, что Base64 обратимый, можно применять и рекурсивное кодирование, например одни данные кодировать 2-3 раза подряд. Только потом и разворачивать придётся столько-же раз.


3. Обратное преобразование – декодер "Base64"

Теперь поменяем вектор на противоположный и рассмотрим, как можно восстановить закодированные алгоритмом Base64 данные. В этом деле, основную роль играет правильно оформленная "таблица-преобразований". Если при кодировании мы делали ставку на алфавит из 64-х символов латиницы, то теперь эти символы сами будут выступать в роли индексов в таблице декодера. Соответственно размер данной таблицы должен быть 128-байт, и значения в ней должны отражать порядковый номер текущего символа в "алфавите кодирования".

На рисунке ниже я представил содержимое этой таблицы, где значения 0xFF являются просто заполнителями, чтобы необходимые декодеру числа встали по своим индексам. Например, алфавит Base64 начинался с заглавного символа(А) с ascii-кодом 41h, и заканчивался символом(/) с кодом 2Fh. При этом внутри алфавита они имели свои индексы 00 и 63 соответственно. Выстраивая "таблицу декодера" мы просто проецируем алфавит на ASCII-таблицу. В результате должны получить такую картину:


DecoderTable.png


Чтобы программно реализовать декодер, нужно включить реверс и двигаться назад. Для начала, разбиваем строку на блоки по 4 символа, и используя каждый из них в качестве индекса, вытягиваем значения из "таблицы-декодера". Затем, отсекаем у каждого значения старшие 2-бита "массовки" и получаем единую группу из 24-бит. На заключительном этапе, необходимо раскроить 24 на три части по 8-бит, которые и отправляем на выход. Вот демо-пример, который автоматизирует этот процесс.

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


C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;//-----------
.data
decodeTable:
 db  43    dup(-1)
 db  0x3E, -1, -1, -1, 0x3F
 db  0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, -1,-1,-1,-1,-1,-1,-1
 db  0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E
 db  0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, -1,-1,-1,-1,-1,-1
 db  0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28
 db  0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, -1,-1,-1,-1,-1

title      db  '*** Base64 Decoder v0.1 ***',0
outFile    db  '64toBin.txt',0

fileName   rb  128     ;// под имя файла,
fileHndl   dd  0       ;//   ..его дескриптор,
fileSize   dd  0       ;//       ..и размер.

readData   dd  0       ;// будет адресом буфера для чтения файла
writeData  dd  0       ;// указатель на буфер записи
shrCycle   dd  0       ;// количество тетрад (длина цикла)
buff       db  0       ;//
;//-----------

.code
start:    invoke  SetConsoleTitle,title
;//====== Запросим у юзера файл с данными
         cinvoke  printf,<10,'   Open file name: ',0>
         cinvoke  scanf,<'%s',0>,fileName

;//====== Попытка его открыть..
          invoke  _lopen,fileName,OF_READ
          mov     [fileHndl],eax     ;//
          cmp     eax,-1             ;//
          jne     @f                 ;//
         cinvoke  printf,<10,'*** ERROR! File not found! ***',0>
          jmp     @exit              ;//

;//====== ОК! Размер файла, и вычисляем кол-во тетрад в нём
@@:       invoke  GetFileSize,eax,0  ;// EAX = дескриптор
          mov     [fileSize],eax     ;// в EAX вернулся размер файла
          mov     ebx,4              ;// делитель
          xor     edx,edx            ;// очистить место под остаток
          div     ebx                ;// EAX / EBX
          mov     [shrCycle],eax     ;// запомнить кол-во тетрад (целая часть)
          or      edx,edx            ;// проверить остаток на нуль
          jz      @f                 ;// пропустить, если ОК!
                                     ;// иначе: можно вывести ошибку
;//         cinvoke  printf,<10,'*** ERROR! This is not "Base64" correct file! ***',0>
;//          jmp     @exit

;//====== Выделить память, и считать туда файл
@@:       invoke  VirtualAlloc,0,[fileSize], MEM_COMMIT, PAGE_READWRITE
          mov     [readData],eax     ;// EAX = адрес выделенной памяти
          invoke  _lread,[fileHndl],eax,[fileSize]
          invoke  _lclose,[fileHndl]

;//====== Выделить память для записи результата
          invoke  VirtualAlloc,0,[fileSize], MEM_COMMIT, PAGE_READWRITE
          mov     [writeData],eax    ;// адрес памяти

;//=========================================================
;//====== ДЕКОДEР BASE64 ===================================
;//=========================================================
          mov     edi,eax            ;// приёмник данных
          mov     esi,[readData]     ;// источник с данными
          mov     ebp,[shrCycle]     ;// кол-во проходов
          mov     ebx,decodeTable    ;// адрес таблицы для XLATB

@decode:  mov     edx,dword[esi]     ;// очередные 4-символа
          bswap   edx                ;// восстановить порядок байт
          mov     ecx,4              ;// счётчик внутреннего цикла LOOP
@@:       shld    eax,edx,8          ;// выдвинуть в AL очередной байт
          shl     edx,8              ;// удалить его из EDX
          xlatb                      ;// взять по нему число из таблицы
          loop    @b                 ;// собираем дворд в EAX

          xchg    eax,edx            ;// обменять! EAX=0, EDX = полученный дворд
          mov     ecx,4              ;// счётчик внутреннего цикла LOOP
@@:       shl     edx,2              ;// убрать из EDX 2-бита "массовки"
          shld    eax,edx,6          ;// выдвинуть в EAX следующие 6-бит
          shl     edx,6              ;// (удалить их из источника)
          loop    @b                 ;// прокрутить внутренний цикл..

          bswap   eax                ;// восстановить порядок байт
          shr     eax,8              ;// удалить лишний (младший) байт
          stosd                      ;// сохранить триаду двоичных!

          dec     edi                ;// коррекция указателя в приёмнике
          add     esi,4              ;// указатель на сл.дворд в источнике
          dec     ebp                ;// внешний цикл -1
          jnz     @decode            ;// повторить по длине EBP..

;//====== КОНЕЦ ДЕКОДЕРА ====================================//

;//====== Вычисляем размер раскодированных данных.
;//====== Cейчас EDI указывает на хвост буфера - отнимем от него начало.
          sub     edi,[writeData]    ;// EDI = размер закодированных данных!
          push    edi                ;// запомнить

;//====== Сбросить раскодированные данные на консоль, и в файл "64toBin.txt"
;//====== Если декодировали двоичный файл, то fn.printf() покажет его не весь, а до первого нуля.
;//====== Сохранения в файл это не касается, т.к. указываем реальный размер данных.
         cinvoke  printf,<10,10,'%s',0>,[writeData]
          invoke  _lcreat,outFile,0
          pop     ebx                ;// размер данных
          push    eax                ;// дескриптор файла
          invoke  _lwrite,eax,[writeData],ebx
          pop     eax                ;//
          invoke  _lclose,eax        ;// закрыть файл

@exit:   cinvoke  scanf,<'%s',0>,buff
         cinvoke  exit,0
;//-----------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
include 'api\kernel32.inc'


4. Родственный алгоритм Base32

Под занавес рассмотрим принцип кодирования Base32. Здесь всё аналогично, только алфавит включает в себя 32 печатных символа (без символов нижнего регистра латиницы), и соответственно для выбора одного из них достаточно 5-битного индекса, поскольку 2^5=32. Рисунок ниже показывает, сколько нужно собрать таких индексов, чтобы суммарная разрядность была в пределах байта. Пенты во-второй строке рисунка или не дотягивают до границы байта, или вылетают за его пределы. И только на 5-ом байте входных данных мы попали в диапазон. Именно такого размера требуются входные блоки, чтобы можно было корректно превратить их в индексы. В конечном счёте, это увеличивает выходной поток аж на 60%, с соотношением 8/5. Это слишком расточительно, поэтому алго Base32 применяется крайне редко.


Base32.png



5. Эпилог

Для работы с кодировкой Base64, на вооружении Win имеются специально натренированные функции – это CryptBinaryToString(), которая выступает в роли кодера, и её кузен для обратного преобразования CryptStringToBinary(). Они живут в системной библиотеке crypt32.dll и в силу своего веса уже давно понижены в звании программистами. Приведённый здесь пример хоть и исповедует идентичные концепции, но детали реализации совершенно разные. Я пробовал трейсить эту CryptBinaryToString() в отладчике, так он повёл меня за собой в непроходимые джунгли кода, и даже после 100-шагов так и не дождавшись полезной нагрузки, я решил прекратить этот балаган. Системе конечно-же видней, как правильно нужно делать, но в большинстве случаях для нас это не приемлемо и проще написать свою короткую процедуру, что я и попытался сделать в этой статье.

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

Вложения

  • Base64.zip
    1,8 КБ · Просмотры: 329
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!