Программизм
 
Программизм
На главную | Графомания | Программизм | Книги | Всячина | Скачать | Ъ?  

UUencode, BASE64 и все-все-все

Давным-давно, когда дискеты были большими,
а программы — маленькими...

Сказка про Колобка и первый компьютер

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

Какой файл считается подозрительным? Если подумать, то разумным решением можно назвать запрет на заражённые исполняемые модули. Например, *.exe, *.dll, *.vbs, *.js, *.doc, *.xls, ну, и ряд других расширений. Правда, такой подход требует своевременного обновления антивирусной базы, тщательного отбора типов файлов, ну, и вообще порядка в сети, который обеспечивать тяжело. Поэтому администраторы фирмы, где работает мой корреспондент, режут всё — исполняемые модули вообще без проверки, все архивы и даже такие «страшные» файлы, как *.mht — формат Microsoft для сохранения web-страницы вместе с картинками, стилями, скриптами и т.д.

Казалось бы, можно переименовать файл — например, из file.zip в file.gif, но файлы известных форматов легко опознаются по содержимому — в начале файла обязательно стоит сигнатура. Для архивов Zip это PK, для исполняемых файлов DOS/Windows — MZ, ну, и так далее. Всякие приёмы типа «архив в архиве» и прочие мелкие хитрости также бесполезны...

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

Давным-давно, когда дискеты... то есть модемы могли передавать только 7-битные байты, а символы с кодами до 0x19 использовались в некоторых протоколах для синхронизации, необходимо было придумать способ передавать 8-битные данные, при том, что у нас всего 128 – 32 = 96 допустимых символов. Решение, в общем-то, достаточно очевидно — каждые три восьмибитных байта кодируются четырьмя шестибитными байтами:

0xA00xEB0xD2
 1010 0000  1110 1011  1101 0010 
 1010 0000  1110 1011  1101 0010 
0x280x0E 0x2F0x12

Теперь вопрос в том, как выбрать диапазон печатных символов, чтобы отобразить множество полученных шестибитных чисел.

Первый стандарт назывался Unix-to-Unix encode или, сокращённо, uuencode. Там для получения печатного символа к числу от 0 до 64 добавлялось 32, а 0 кодировался не пробелом, а знаком обратного апострофа (`). Если написать таблицу преобразования, то она выглядела бы так:

`!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_

Как видим, здесь используются только знаки препинания, цифры и заглавные буквы, так что даже если регистр букв не при передаче не сохраняется (e-mail по телеграфному аппарату), файл всё равно дойдёт. Есть, правда, один нюанс: первый символ строки, содержащий uu-кодированные данные, содержит информацию о том, сколько байт закодировано в этой строке. Если вы посмотрите на файл, сделанный любым стандартным uu-кодировщиком, то увидите, что первый символ каждой строки, кроме последней, — 'M', т.е. в строке 45 байт, а длина строки — 61 символ.

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

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Итак, робот, призванный следить за чистотой корпоративной почты, легко опознаёт вложенный файл по заголовку MIME или uuencode и вырезает этот файл из письма. Чтобы обмануть его, можно попробовать применить нестандартную таблицу кодирования и не писать стандартных заголовков. То есть превращаем «подозрительный» файл в текст и методом copy/paste вставляем текст в письмо.

Проблема состоит в том, что у корреспондента должен быть декодер, а переслать ему исполняемый модуль я не могу. Возникает естественное желание написать декодер на каком-нибудь встроенном языке Windows, то есть CScript или VBScript, но оказывается, что там нет функций для работы с двоичными файлами — объект FileSystemObject позволяет работать только с текстом.

К счастью, у 99.9% пользователей Windows установлен пакет Microsoft Office, а возможности встроенного в него языка VBA существенно превосходят возможности VBScript. Значит, используем VBA и Excel (последнее — исключительно дело привычки, подойдут также Word, Access или PowerPoint). Ниже приводится текст декодера:

Option Explicit
Option Base 0


Private Type OPENFILENAME
  lStructSize As Long
  hwndOwner As Long
  hInstance As Long
  lpstrFilter As String
  lpstrCustomFilter As String
  nMaxCustFilter As Long
  nFilterIndex As Long
  lpstrFile As String
  nMaxFile As Long
  lpstrFileTitle As String
  nMaxFileTitle As Long
  lpstrInitialDir As String
  lpstrTitle As String
  flags As Long
  nFileOffset As Integer
  nFileExtension As Integer
  lpstrDefExt As String
  lCustData As Long
  lpfnHook As Long
  lpTemplateName As String
End Type

Private Declare Function GetOpenFileName Lib "comdlg32.dll" _
  Alias "GetOpenFileNameA" (pOpenfilename As OPENFILENAME) As Long


Private Const CodeChars As String = _
  "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя"


Private Function c2i(ByVal c As String) As Integer
  c2i = InStr(1, CodeChars, c) - 1
End Function


Private Sub Decode(ByVal pStr As String, ByRef pBuf() As Byte)
  Dim count As Integer, count0 As Integer, l As Integer, i As Integer
  pStr = Trim$(pStr)
  l = Len(pStr): i = 1
  count0 = Fix(l / 4) * 3
  If l Mod 4 > 0 Then count0 = count0 + l Mod 4 - 1
  ReDim pBuf(count0 - 1) As Byte
  Do While l > 0
    pBuf(count) = CByte((c2i(Mid$(pStr, i, 1))) * 4 Mod 256 + _
      (c2i(Mid$(pStr, i + 1, 1))) \ 16)
    count = count + 1
    l = l - 2: If l = 0 Then Exit Do
    pBuf(count) = CByte((c2i(Mid$(pStr, i + 1, 1))) * 16 Mod 256 + _
      (c2i(Mid$(pStr, i + 2, 1))) \ 4)
    count = count + 1
    l = l - 1: If l = 0 Then Exit Do
    pBuf(count) = CByte((c2i(Mid$(pStr, i + 2, 1))) * 64 Mod 256 + _
      (c2i(Mid$(pStr, i + 3, 1))))
    count = count + 1
    l = l - 1
    i = i + 4
  Loop
End Sub


Sub DecodeFile()
  Dim FileName As OPENFILENAME
  Dim c As String
  Dim Buf() As Byte
  Dim n As Integer
  With FileName
    .lStructSize = 76
    .lpstrFilter = _
      "Текстовые файлы (*.txt)" & Chr$(0) & "*.txt" & Chr$(0) & _
      "Все файлы (*.*)" & Chr$(0) & "*.*" & Chr$(0)
    .nFilterIndex = 1
    .lpstrFile = Chr$(0) + Space(255)
    .nMaxFile = 256
    .lpstrTitle = "Выберите файл для декодировки"
    .flags = &H881804
  End With
  If GetOpenFileName(FileName) = 0 Then Exit Sub
  Open FileName.lpstrFile For Input Access Read As #1
  Line Input #1, c
  Open Left$(FileName.lpstrFile, FileName.nFileOffset) & c _
    For Binary Access Write As #2
  Do While Not EOF(1)
    Line Input #1, c
    If Len(c) = 0 Then Exit Do
    Decode c, Buf
    Put #2, , Buf
  Loop
  Close #2
  Close #1
End Sub

Формат текстового файла прост — в первой строке идёт имя двоичного файла, который должен получиться в результате декодировки, а затем — собственно данные. Для того, чтобы декодировать файл, надо (сюрприз!) запустить макрос DecodeFile(). Ну, а если вы хотите заставить этот макрос декодировать BASE64 или uu-кодированные файлы, замените таблицу и начинайте декодировку uu не с первого, а со второго символа. Всё.

Теперь возникает следующая сложность — нужен кодер. На чём его писать? Хотелось бы на том же самом VBA, но оказывается, что функции работы с двоичными файлами реализованы там криво, и если при записи эта кривизна обходится, то при чтении — нет. Можно писать на C, но это не спортивно. К счастью, под руками у меня всегда есть интерпретатор REXX, язык, используемый с тех пор, как я работал под OS/2. Ниже приводится код программы:

# regina "%~f0" %* & exit

CodeChars = "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя"

"del a.out 2>nul"

parse arg inf
call lineout "a.out", inf
do while chars(inf)>0
  l = min(chars(inf),54)
  c = charin(inf,,l)
  call lineout "a.out", encode(c)
end
exit

encode :procedure expose CodeChars
  parse arg c
  buf = ""
  i = 1
  do while i<=length(c)%3*3
    c1 = c2d(substr(c,i,1))%4
    c2 = (c2d(substr(c,i,1))*16+c2d(substr(c,i+1,1))%16)//64
    c3 = ((c2d(substr(c,i+1,1))*4)+(c2d(substr(c,i+2,1))%64))//64
    c4 = c2d(substr(c,i+2,1))//64
    buf = buf || substr(CodeChars, 1+c1, 1)
    buf = buf || substr(CodeChars, 1+c2 ,1)
    buf = buf || substr(CodeChars, 1+c3 ,1)
    buf = buf || substr(CodeChars, 1+c4 ,1)
    i = i+3
  end
  select
    when length(c)-i=0 then do
      c1 = c2d(substr(c,i,1))%4
      c2 = (c2d(substr(c,i,1))*16)//64
      buf = buf || substr(CodeChars, 1+c1, 1)
      buf = buf || substr(CodeChars, 1+c2 ,1)
    end
    when length(c)-i=1 then do
      c1 = c2d(substr(c,i,1))%4
      c2 = (c2d(substr(c,i,1))*16+c2d(substr(c,i+1,1))%16)//64
      c3 = ((c2d(substr(c,i+1,1))*4))//64
      buf = buf || substr(CodeChars, 1+c1, 1)
      buf = buf || substr(CodeChars, 1+c2 ,1)
      buf = buf || substr(CodeChars, 1+c3 ,1)
    end
    otherwise
  end
  return buf

Зная, что символы в строке нумеруются с 1, после ключевого слова expose перечисляются глобальные переменные, видимые в процедуре, функция c2d() возвращает ASCII-код символа, операция % представляет собой целочисленное деление, а // — остаток от деления, вы без труда переведёте эту программу на понятный вам язык, например, Perl.

В общем, удачи вам, быстрого интернета и правильно настроенных почтовых систем!

20.01.2004

Поиск
См. также

Надо запустить несколько процессов, дождаться их окончания и двигаться дальше... »»»

Очевидно, что char поместится в int. Во многих моделях памяти размер указателя также равен размеру int. Следовательно, структуру можно заменить массивом... »»»

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

Рекомендую

e.g.Orius’
Игорь Иртеньев
Вячеслав Шевченко

Copyright notice

ъ) Все материалы, размещённые на странице, являются неотъемлемой собственностью автора с вытекающими отсюда правами, как ©, так и (ъ). Некоммерческое их распространение всячески приветствуется, разумеется, при условии сохранения ссылки на оригинал. Что касается коммерческого использования — пишите письма, договориться можно всегда.

Удивительное рядом

lj userhardsign
Закладки Карта Королёва

Пишите письма

Счётчики

XPEHOMETP™ Рейтинг@Mail.ru