
                        О PE ФАЙЛАХ И ДЛИНАХ СЕКЦИЙ
                        ===========================

                              (x) 2001 Z0MBiE
                           http://z0mbie.host.sk

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

     Вроде  бы  ясная  и простая вещь. Однако, практически КАЖДЫЙ задает на
 эту  тему  вопросы,  причем -- одни и те же. В связи с этим остается одно:
 или  подробно  и  публично  осветить  эту  проблему, или сдохнуть. Выберем
 трудный путь.

     Рассмотрим некий PE файл, лучше всего CALC.EXE.


     1.  В  PE хеадере хранится число секций в файле, это WORD, назовем его
 pe_numofobjects, оффсет в PE заголовке равен +06.

     Примечание:  обычно число секций не равно нулю, хотя маздаю (win9x) на
 это  насрать,  и даже если в файле нет ни одной секции, он все равно будет
 работать,  особенно  если  после  PE  заголовка  всунуть какой-нибудь код.
 Однако,  если  в  файле совсем не будет импортов, то возникнут трудности с
 вызовом  win32  api-шек. Но и это не проблема, если вместо апишек вызывать
 процедуру kernel@int21 по адресу BFF712B9.

     Примечание:  сразу  после  WORD pe_numofobjects идет DWORD pe_datetime
 (оффсет  +08), то есть датавремя создания файла. Если работать с WORD'ом в
 падлу,   то  можно  предварительно  занулить  pe_datetime  и  принять  что
 pe_numofobjects это DWORD.


     2.  Сразу  после PE заголовка идет таблица секций (==таблица объектов,
 object  table),  элементы  (записи, object entry) которой описывают секции
 файла.  Сколько  записей,  столько  и  секций, всего pe_numofobjects штук.
 Длина  PE  заголовка в абсолютном большинстве случаев равна 0F8h байт. Но,
 поскольку всякое бывает, то берут WORD по +14h, прибавляют 18h, и получают
 точную длину PE заголовка.

        Формат object entry (одной записи таблицы секций):

oe_struc                struc
oe_name                 db      8 dup (?);00 01 02 03 04 05 06 07
oe_virtsize             dd      ?       ; 08 09 0a 0b
oe_virtrva              dd      ?       ; 0c 0d 0e 0f  need objectalign
oe_physsize             dd      ?       ; 10 11 12 13
oe_physoffs             dd      ?       ; 14 15 16 17  need filealign
oe_xxx                  dd      ?       ; for obj file
                        dd      ?       ; --//--
                        dd      ?       ; --//--
oe_flags                dd      ?        ; 24 25 26 27
; ---- total size == 0x28 ---------
oe_struc                ends

     Теперь  отметим  САМЫЙ  ВАЖНЫЙ  МОМЕНТ:  В  таблице  объектов хранятся
 _НЕ_ВЫРОВНЕННЫЕ_  значения  физической  и виртуальной длин секций. Что это
 значит?  Это  значит, что чтобы получить настоящие длины секций надо взять
 эти  значения  из  соответствующих  записей  в таблице секций и ВЫРОВНЯТЬ:
 физическую длину на pe_filealign, а виртуальную длину на pe_objectalign.

     Смещения  же  секций (физическое в файле и виртуальное (rva) в памяти)
 выровнены всегда.
     Примечание:   из   выравнивания   смещений  следует,  что  после  всех
 заголовков  но  до  начала  первой секции может быть пустое неиспользуемое
 место. Например туда записывался CIH.

     Поля  pe_filealign  и  pe_objectalign  (DWORD'ы, смещения +3Ch и +39h)
 суть   степени   двойки,   причем   pe_filealign  кратно  512  (сектор), а
 pe_objectalign кратно 4096 (страница в памяти).

     Поэтому процесс выравнивания для одной секции выглядит так: (на C)

#define ALIGN(x,y)      (((x)+(y)-1)&(~((y)-1)))

     // oe=таблица секций
     // i=номер секции
     oe[i].oe_physsize = ALIGN(oe[i].oe_physsize, pe->pe_filealign);
     oe[i].oe_virtsize = ALIGN(oe[i].oe_virtsize, pe->pe_objectalign);

     или на asm'е:

     ; esi=PE-заголовк
     ; edi=элемент таблицы секций
     ; 1. выровняем физическу длину
     mov eax, [esi].pe_filealign
     dec eax
     add [edi].oe_physsize, eax
     not eax
     and [edi].oe_physsize, eax
     ; 2. выровняем виртуальную длину
     mov eax, [esi].pe_objectalign
     dec eax
     add [edi].oe_virtsize, eax
     not eax
     and [edi].oe_virtsize, eax

     Кроме  того,  бывает,  что виртуальная длина у всех секций == 0. Такую
 дрянь   производит   watcom.  И  при  этом,  оно  работает.  Откуда  брать
 виртуальную  длину  в  этом  случае?  Я брал вместо нее физическую длину и
 выравнивал ее на objectalign.

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

     Важно:  далее  в  этом  тексте  мы  имеем  в  виду,  что  физическая и
 виртуальная  длины секций уже выровнены; "выровненная" перед "длина" далее
 подразумевается само собой.


     3.  Для  чего  PE  файл  разбит на секции? Для того, чтобы в образе PE
 файла  в  памяти  (который  обычно не соответствует образу на диске) могли
 чередоваться инициализированные и неинициализированные данные. Кроме того,
 есть  секции  со  специальным  назначением,  в них обычно хранятся таблицы
 импортов, экспортов, ресурсов, фиксапов, отладочной информации и некоторые
 другие.

     Каждая  секция  файла  имеет <физическую длину> и <виртуальную длину>.
 Физическая   длина   --  это  то,  сколько  секция  занимает  на  диске. А
 виртуальная  длина  --  это  то, сколько секция занимает в памяти. Разница
 между  этими  длинами на ассемблере представляется как DB ?, то есть это и
 есть неинициализированные данные. Неинициализированные данные могут быть у
 любой секции. В результате получается такая ситуация:

  ФАЙЛ НА ДИСКЕ                 ПРОГРАММА В ПАМЯТИ

   +--------+                       +--------+
   |MZxxxxxx| <---- заголовки ----> |MZxxxxxx|
   +--------+                       |00000000|<--alignment
   |xxxxxxxx|                       +--------+
   |xxxxxxxx| <---- cекция#1 -----> |xxxxxxxx|\~~~~~~~\
   |xxxxxxxx|                       |xxxxxxxx| }-физ.   \
   +--------+                       |xxxxxxxx|/ длина    }-виртуальная длина
   | ...    |             неиниц.  /|00000000|  секции  /   секции
                          данные~~~\|00000000| _______/
                                    +--------+
                                    | ...    |

  Несмотря на кажущуюся простоту, возможны такие варианты:

     -  Физическая длина секции равна виртуальной. Идеальный вариант, мечта
 поэта. Обычно встречается когда filealign == objectalign.
     -  Физическая  длина меньше виртуальной. Это значит, что у секции есть
 неинициализированные данные.
     -  Физическая  длина  больше виртуальной. Это значит что у секции есть
 оверлей   или  alignment.  Помните,  что  если  виртуальная  длина  меньше
 физической,  то  некоторая  (скорее всего состоящая из нулей) часть образа
 секции  с  диска  не загрузится -- это будет небольшой файловый алигнмент.
 Хотя  никто  не  запрещает  таким образом вставить не загружаемый в память
 "оверлей" не в конец файла, а между его секциями.

     Alignment -- это разница между физической и виртуальной длинами. Можно
 понимать  его  по  разному:  для секции -- (A) если длины выровнены, и (B)
 если  длины  НЕ  выровнены;  и  для файла, если это (C) заполненный нулями
 оверлей  длиной  меньше pe_objectalign. Причем в случаях (A) и (B) разница
 длин  у  одной  и  той  же  секции может иметь разный знак. Например, если
 невыровенная   physsize=512,   невыровненная   virtsize=100,   выровненная
 physsize=512, выровненная virtsize=4096, то в случае (A) alignment=3584, а
 в  случае  (B) alignment=412. Причем один из них -- в файле, а другой -- в
 памяти.  Так  что решите для себя, о каком из алигнментов вы думаете и/или
 говорите.

     Замечу,  что  секции не всегда идут "впритык" друг к другу. Между ними
 возможны   неиспользуемые  странички  памяти,  хотя  бывает  такое  редко.
 Например  бывает,  что оффсеты всех секций выровнены на 64k, а виртуальные
 их длины всего по несколько страниц.


     4. Лирическое отступление в область теории

     ImageBase -- это адрес в памяти, куда должен быть загружен PE файл. По
 умолчанию  большинство  адресов  в  файле  настроены  на  указанный  в  PE
 заголовке  ImageBase  (DWORD,  смещение  +34h).  Обычно выровнен на 64k, и
 навряд ли линкер даст сделать меньше; однако маздайный загрузчик проглотит
 многие нестандартные значения с самым разным результатом.
     Если  файл -- это DLL, и загрузить его по указанному адресу нельзя, то
 происходит  перенастройка  на  другой  ImageBase,  для  этого используется
 таблица  настроек  (==фиксапов,  релокаций).  Если  же в этом случае ее не
 окажется,  то файл загружен не будет. Поните об этом, заражая PE DLL'ки, и
 при отдаче управления в файл вместо PUSH OFFSET/RETN делайте JMP.

     ImageSize (DWORD, оффсет +50h) -- это виртуальная длина файла, то есть
 размер  образа  файла  в  памяти,  вычисляется как виртуальный адрес (rva)
 последней  секции + виртуальная длина последней секции. Обычно выровнен на
 pe_objectalign.

     BaseOfCode -- это RVA первой кодовой секции, просто копируется сюда из
 object table при линковке.
     BaseOfData -- та же фигня, для второй, обычно секции данных.
     SizeOfCode -- длина кода, обычно выставлена корректно и  равна
                   вирт. длине первой секции
     SizeOfInitData  &  SizeOfUninitData  --  полная  херь,  выставлено как
 попало и кое-где. Каким бы ни был метод заражения, менять их нет смысла.

     pe_subsystem  (WORD по смещению +5Ch) -- равно 2 для GUI приложений, и
 3  для  консольных  аппликух.  Иногда встречаются другие значения, и тогда
 файл заражать не следует.


     5. Оверлеи

     Оффсет оверлея вычисляется как физический оффсет последней секции плюс
 физическая длина последней секции.
     Длина оверлея вычисляется как длина файла минус оффсет оверлея.
     Оверлей  в  память  вместе  с PE файлом не грузится, но pe checksum по
 нему считается.

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

     Если  у  оверлея  первый  DWORD=00000001h,  а  чуть дальше где-то идет
 '.dbg', то это дебаговая инфа. Есть и другие ее виды.

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

     Замечу,  что  в  NT'ях  и  далее  -- большинство файлов с оверлеями, в
 основном с дебаговой инфой в них.


     6. Собственно, что нужно чтобы дописаться к последней секции.

        - проверить файл на валидность: alredy-infected, subsystem=2/3, ...
        - выровнять длины последней секции
        - передвинуть overlay если он есть или поскипать дебаговую инфу
        - записать вирус в конец последней секции
        - увеличить длины последней секции
          на соответственно выровненные длины вируса
        - увеличить imagesize на выровненную-на-objectalign длину вируса
        - проапдейтить заголовки

     Пример добавления к последней секции: win9X.Examplo


     7. Некоторые другие методы заражения PE файлов:

         - запись в алигнмент
                  - хеадера
                  - секции
         - добавление новой секции
         - добавление нескольких секций, возможно фэйковых
         - запись на место секции .reloc (с убиванием фиксапов)
         - запись в ресурсы
         - запись в начало/в конец какой-нибудь (обычно кодовой) секции,
           с ее увеличением (раздвигаем файл);
           необходима таблица фиксапов, придется перепатчить весь файл,
           но это не сложно
         - запись в конец кодовой секции с предварительной ее упаковкой,
           длина файла не меняется
         - нахождение в секции данных области нулей и запись в нее
         - нахождение в кодовой секции неиспользуемых "пятен" и запись
           по кусочкам в них
         - выдирание из кодовой секции целой процедуры и запись вместо нее

                                   * * *
