О 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 (с убиванием фиксапов) - запись в ресурсы - запись в начало/в конец какой-нибудь (обычно кодовой) секции, с ее увеличением (раздвигаем файл); необходима таблица фиксапов, придется перепатчить весь файл, но это не сложно - запись в конец кодовой секции с предварительной ее упаковкой, длина файла не меняется - нахождение в секции данных области нулей и запись в нее - нахождение в кодовой секции неиспользуемых "пятен" и запись по кусочкам в них - выдирание из кодовой секции целой процедуры и запись вместо нее * * *