Запись в архивы RAR, ZIP и ARJ без помощи архиватора

Решил вспомнить свою юность, когда я занимался разработкой "самоходного программного обеспечения". Одной из особенностей моих творений было распространение не только через исполняемые файлы, но и через архивы различных форматов. Технология внедрения в архивные файлы не нова, и использовалась в разных вирусах еще со времен MS-DOS. Так как готовых решений на тот момент у меня не было, пришлось доходить до всего самому. Никакого вредоносного кода на этом сайте не появится, а вот некоторыми своими наработками по внедрению в архивы я с удовольствием поделюсь. Поскольку полноценно продублировать алгоритмы сжатия архиваторов в столь малом размере файла не представляется возможным, внедряться в архивы мы будем по методу "Store". Это означает, что файл в архив добавлен с опцией "без сжатия". Внутренние форматы различных архивных файлов, естественно, различаются, но у всех обязательно присутствуют служебные заголовки, используемые архиваторами, и, собственно, сами упакованные данные.

Самый простой для внедрения - формат RAR, используемый одноименным архиватором. Подробную спецификацию формата RAR можно почитать здесь.
;---------------------------------------------
; RAR Header
;---------------------------------------------
rhcrc   dw      ?     ; --> Low-word CRC32 of fields in header
rhtype  db      ?     ; Header type: 0x74
rhflag  dw      ?     ; Bit flags
rhsize  dw      ?     ; File header full size
rcsize  dd      ?     ; Compressed file size
rosize  dd      ?     ; Uncompressed file size
rhoss   db      ?     ; Target OS version
rfcrc   dd      ?     ; --> File CRC32
rdtm    dd      ?     ; File Time/Date
runp    db      ?     ; Archive version to extract
rmeth   db      ?     ; Packing method (store)
rnsize  dw      ?     ; File name size
rfattr  dd      ?     ; File attributes
rfname  rb      (?)   ; File name

Мнемокода "(?)" в FASM нет, просто таким образом я обозначил текстовую строку для записи имени файла неопределенной длины. В реальном проекте будет достаточно MAX_PATH или вообще фиксированного размера.
; "Хвост" архива - признак окончания данных
tail    db      0C4h,03Dh,07Bh,00h,040h,07h,00h
tail_length     = $-tail

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

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

1. rhtype = 74h - тип заголовка (файл);
2. rhflag = 8000h - флаги;
3. runp = 14h - версия архиватора, необходимая для распаковки файла;
4. rmeth = 30h - метод упаковки файла (store);
5. rfattr = 20h - атрибуты файла (архивный);
6. rhoss = 2 - целевая ОС (Windows);
7. rdtm = время и дата создания файла в формате MS-DOS;
8. rfcrc = CRC32 оригинального файла;
9. rosize = размер оригинального файла;
10. rcsize = размер сжатого файла, равен оригинальному размеру;
11. rfname = имя файла;
12. rnsize = длина имени файла;

После этого считаем CRC32 заголовка, начиная от поля rhtype и до rfname включительно, затем надо взять младшее слово от результата расчета CRC32 и записать его в поле rhcrc. Все, заголовок заполнен. Открываем целевой архив для записи, и устанавливаем указатель на позицию 7 байт от конца файла, чтобы удалить "хвост" архива. Записываем наш заголовок, следом записываем внедряемый файл. После этого записываем 7-байтовый "хвост" архива. Точно таким же способом можно прицепляться к RAR-SFX архивам. На этом теорию внедрения в архивы формата RAR можно считать освоенной.

Архиватор ARJ во времена MS-DOS фактически являлся стандартом архивирования, но сейчас утратил актуальность. Однако наработки по внедрению в него остались. Есть официальная коммерческая версия и бесплатная с открытым кодом, обе они совместимы между собой и используют одинаковый формат архива. Техническая спецификация формата ARJ есть в файлеTECHNOTE.TXT из дистрибутива коммерческой версии. Дописывание файла к архиву ARJ делается немного сложнее, чем к RAR, но тоже не представляет больших трудностей.
;---------------------------------------------
; ARJ Header
;---------------------------------------------
marker  dw      ?     ; Header ID
bhsize  dw      ?     ; --> Basic header size (acrc-fhsize)
fhsize  db      ?     ; --> First header size (afname-fhsize)
anum    db      ?     ; Archive version number
anum2   db      ?     ; Archive version to extract
osver   db      ?     ; Target OS version
aflag   db      ?     ; No any flags
ameth   db      ?     ; Archive method (0 - stored)
aftype  db      ?     ; File type (0 - binary)
ares    db      ?     ; Reserved
dtm     dd      ?     ; Date/Time last modification
csize   dd      ?     ; Compressed size
osize   dd      ?     ; Original size
crc     dd      ?     ; --> Original file CRC32
fspec   dw      ?     ; Filespec position in filename
faccess dw      ?     ; File access mode
hstdata dw      ?     ; Host data
extra1  dd      ?     ; Extended file position
edtma   dd      ?     ; Date-time accessed
edtmc   dd      ?     ; Date-time created
extra2  dd      ?     ; Original file size even for volumes
afname  rb      (?)   ; File name (ASCIIZ)
acomm   db      ?     ; File comment (ASCIIZ)
acrc    dd      ?     ; --> Basic header CRC32
ehsize  dw      ?     ; Extended header size

; "Хвост" архива - признак окончания данных
tail    db      060h,0EAh,00h,00h
tail_length     = $-tail

Записываемый файл также предварительно надо загрузить в память, после этого заполняем поля заголовка.

1. marker = 0EA60h - маркер заголовка архива;
2. anum = 6 - версия архиватора;
3. anum2 = 1 - версия архиватора для извлечения файла;
4. osver = 11 - ОС для запуска файла (Windows);
5. fhsize = 2Eh - размер первого заголовка от поля fhsize до afname;
6. dtm = время и дата создания файла в формате MS-DOS;
7. crc = CRC32 оригинального файла;
8. osize = размер оригинального файла;
9. csize = размер сжатого файла, равен оригинальному размеру;
10. fname = имя файла в формате ASCIIZ;

После этого надо посчитать размер полученного заголовка от поля fhsize до acommвключительно и вычислить его CRC32. Размер записывается в поле bhsize, а CRC32 в полеacrc. Остальные поля для нас значения не имеют и заполняются нулевыми значениями. Если интересно, можете почитать про них в документации. Открываем целевой архив для записи, и устанавливаем указатель на позицию 4 байта от конца файла, чтобы удалить "хвост" архива, как и в случае с форматом RAR. Записываем наш заголовок, за ним внедряемый файл и 4-байтовый "хвост" архива. Все, с форматом ARJ разобрались. Также не забывайте проверять флаги в главном заголовке архива, чтобы не повредить запароленные, многотомные и другие архивы, в которые нельзя добавлять файлы таким способом. Эти проверки сделайте самостоятельно.

Наиболее сложным для внедрения является формат ZIP. Здесь информация о сжатом файле хранится в нескольких местах: локальном заголовке и так называемой "центральной директории". Это сделано специально, чтобы можно было максимально быстро получить доступ к структуре архива, не просматривая целиком его содержимое. И именно из-за этой особенности при внедрении нам придется обрабатывать весь архив. Для больших архивов придется создавать временный файл, а в нашем случае вполне можно прочитать архив целиком в память. Спецификацию формата ZIP можно почитать на офсайте разработчиков. Этот же формат имеют архивы JAR, по сути это просто переименованные ZIP-файлы.
;---------------------------------------------
; ZIP-header (local)
;---------------------------------------------
zlid    dw      ?     ; Header Id
zlsig   dw      ?     ; Signature
zlvneed dw      ?     ; Version Need
zlflags dw      ?     ; Flags
zlmeth  dw      ?     ; Method
zldtm   dd      ?     ; DateTime
zlfcrc  dd      ?     ; --> CRC32
zlcsize dd      ?     ; Compressed Size
zlosize dd      ?     ; Uncompressed Size
zlnsiz  dw      ?     ; Size of Filename
zlefild dw      ?     ; Size of Extra Field
zlfname rb      (?)   ; Filename

;---------------------------------------------
; ZIP-header (central directory)
;---------------------------------------------
zid     dw      ?     ; Header Id
zsig    dw      ?     ; Signature
zverm   dw      ?     ; Version Made
zvneed  dw      ?     ; Version Need
zflags  dw      ?     ; Flags
zmeth   dw      ?     ; Method
zdtm    dd      ?     ; TimeDate
zfcrc   dd      ?     ; --> CRC32
zcsize  dd      ?     ; Compressed Size
zosize  dd      ?     ; Uncompressed Size
znsiz   dw      ?     ; Size of Filename
zefield dw      ?     ; Size of Extra Field
zcomm   dw      ?     ; Comment Size
zdnumb  dw      ?     ; Disk Number
ziattr  dw      ?     ; Internal Attributes
zeattr  dd      ?     ; External Attributes
zohead  dd      ?     ; Offset Header
zfname  rb      (?)   ; Filename

;---------------------------------------------
; ZIP-header (End of central directory)
;---------------------------------------------
zeid    dw      ?     ; Header Id
zesig   dw      ?     ; Signature
zenumd  dw      ?     ; Number of this disk
zenume  dw      ?     ; Number of this disk
zetotal dw      ?     ; Total number of entries of disk
zedir   dw      ?     ; Total number of entries of the central directory
zesizec dd      ?     ; Size of the central directory
zeoffs  dd      ?     ; Offset of start of central directory
zecomm  dw      ?     ; ZIP file comment length

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

1. zlid = 'PK' - сигнатура заголовка;
2. zlsig = 0403h - тип заголовка - локальный;
3. zlvneed = 10h - версия архиватора для извлечения файла;
4. zlflags = 80h - флаги файла (см. в документации);
5. zlmeth = 0 - метод упаковки без сжатия;
6. zldtm = время и дата создания файла в формате MS-DOS;
7. zlfcrc = CRC32 оригинального файла;
8. zlcsize = размер сжатого файла, равен оригинальному размеру;
9. zlosize = размер оригинального файла;
10. zlnsiz = длина имени файла;
11. zlfname = имя файла;

Центральная директория:

1. zid = 'PK' - сигнатура заголовка;
2. zsig = 0201h - тип заголовка - центральная директория;
3. zverm = 10h - версия архиватора для извлечения файла
4. zvneed = 10h - версия архиватора для извлечения файла;
5. zflags = 80h - флаги файла (см. в документации);
6. zmeth = 0 - метод упаковки без сжатия;
7. zdtm = время и дата создания файла в формате MS-DOS;
8. zfcrc = CRC32 оригинального файла;
9. zcsize = размер сжатого файла, равен оригинальному размеру;
10. zosize = размер оригинального файла;
11. znsiz = длина имени файла;
12. zeattr = 20h - атрибут файла (архивный)
13. zfname = имя файла;

После заполнения заголовков переносим упакованные файлы из исходного архива в новый. Для этого в цикле проверяем сигнатуру локальных заголовков с самого начала архива. Если она равна 0x04034b50, то вычисляем размер блока по сумме размера локального заголовка от поляzlid до zlfname, длины имени файла zlnsiz и длины дополнительных полей zlefild. Когда все упакованные файлы из исходного архива будут перенесены в новый, можно записывать в новый архив наш заполненный локальный заголовок и тело файла. Перед этим надо запомнить абсолютное смещение нашего локального заголовка файла относительно начала нового архива и заполнить этим значением поле zohead заголовка нашего файла в центральной директории. Архив может содержать дополнительные данные (Archive Extra Data), они расположены после файлов и оформлены своим заголовком с сигнатурой 0x08064b50. Формат и значения полей заголовка описаны в документации. Их также надо записать в новый архив. Теперь надо перенести все записи из центральной директории исходного архива в новый. Каждый блок идентифицируется по заголовку 0x02014b50, размер блока равен сумме размера заголовка центральной директории от поля zid до поля zfname, длины имени файла znsiz и длины дополнительных данных zefield. Перед переносом центральной директории надо также запомнить ее абсолютное смещение относительно начала нового архива, это значение потребуется при заполнении поля zeoffs завершающей структуры центральной директории (End of central directory record). Когда центральная директория исходного файла перенесена в новый архив, можно дописывать к ней заголовок центральной директории нашего файла. После этого в новый архив надо перенести завершающую структуру центральной директории, она начинается с сигнатуры 0x06054b50. Предварительно в ней надо увеличить на 1 значения полей, обозначающих количество файлов в архиве (zenumd и zenume) и размер центральной директории (увеличить текущее значение zesizec на размер заголовка нашего файла). Полный размер блока завершающей структуры центральной директории равен сумме размера ее заголовка от поля zeid до поля zecomm включительно и размера комментария архива zecomm. Вот, вроде бы и все. Если из описания процесс обработки ZIP-архивов не очень понятен, то смотрите исходники. Как вариант, можно обрабатывать содержимое архива не с начала файла, а сразу анализируя заголовки центральной директории. Такой способ вполне имеет право на существование и, наверное, может даже считаться более надежным.

UPD: При написании статьи я столкнулся с некоторыми JAR-архивами, у которых в локальном заголовке размеры файла и CRC32 были просто обнулены, в результате этого невозможно посчитать размер блока файла. Видимо программы, работающие с ними, ориентируются на данные центральной директории. Попытка внедриться в такие архивы описанным выше способом приведет к их безвозвратному повреждению, поэтому при этом способе внедрения такие файлы надо пропускать. Я разобрался со способом внедрения в архив через центральную директорию. Здесь алгоритм немного другой. Сперва надо найти End of central directory record. Она имеет фиксированный размер, но архив также может содержать комментарий, который записан ПОСЛЕ End of central directory record в конце файла, а размер комментария записан в нее же. Так что я не придумал ничего лучше, чем просто сканировать файл с конца по сигнатуре0x06054b50, благо что максимальный размер комментария не может превышать значения WORD, то есть 65535 байт. Из найденной структуры мы извлекаем данные о местоположении центральной директории относительно начала файла. После этого начинаем по очереди перебирать записи из нее, а именно смещение упакованных файлов и их размеры, записывая их в новый архив. Здесь важно учитывать следующий момент: если в локальном заголовке файла в поле zlflags установлен 3-й бит, то информация о контрольной сумме, а также о размерах упакованного и оригинального файла содержатся в 16-байтном блоке data descriptor, который записан сразу же после упакованного файла и начинается с сигнатуры 0x08074b50. Причем в архиве могут одновременно быть файлы как с заполненными локальными заголовками, так и с неполными заголовками + data descriptor. Таким образом, мы через центральную директорию находим файл в архиве, записываем его в новый архив, проверяем наличие data descriptor, в случае его наличия также переносим в новый архив сразу после файла. Когда будут перенесены все файлы, записываем наш файл. После этого записываем оригинальную центральную директорию, данные нашего файла из центральной директории и скорректированную End of central directory record с файловым комментарием (при его наличии). Смотрите исходники, там понятнее. Таким образом можно корректно прицепляться к обычным архивам, JAR-архивам и даже документам OpenOffice (odt, ods) и MS Office 2010 (docx, xlsx), которые по сути также являются ZIP-архивами. Кроме того, при обработке центральной директории можно внедряться не только в конец, но и в середину архива, а также подменять собой уже имеющиеся в архиве файлы. Но это уже переходит границу добрых дел, поэтому останется только идеей.

Не забывайте обрабатывать многотомные и защищенные архивы (данные о шифровании хранятся в отдельном блоке). Еще есть 64-битные версии ZIP-архивов, они имеют несколько другой формат заголовков и не могут быть обработаны как обычные ZIP-архивы. Повторюсь, что подробное описание всех служебных заголовков и форматов можно найти в официальной документации.

К сожалению, современные архиваторы типа 7zip, WinRK, KuaiZip, WinArchiver и WinUHA имеют более сложный внутренний формат, там даже имена файлов подвергаются сжатию или хранятся в отдельном блоке, а KGB Archiver вообще не подразумевает возможность модификации своих архивов.
Теги:
RAR, ZIP, ARJ, Запись в архив
Добавлено: 10 Апреля 2018 09:15:51 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...