ЗАРАЖЕНИЕ ВО БЛАГО (Опыт полезного применения вирусных технологий) by Constantin E. Climentieff aka DrMad ВВЕДЕНИЕ 1. ЗАЩИТА COM-ПРОГРАММ 1.1. ОБЩАЯ ХАРАКТЕРИСТИКА ФОРМАТА 1.2. КОД COM-ИНФЕКТОРА 1.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ COM-ПРОГРАММУ 2. ЗАЩИТА EXE-ПРОГРАММ 2.1. ОБЩАЯ ХАРАКТЕРИСТИКа ФОРМАТА 2.2. КОД EXE-ИНФЕКТОРА 2.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ EXE-ПРОГРАММУ 3. ЗАЩИТА NE-ПРОГРАММ 3.1. ОБЩАЯ ХАРАКТЕРИСТИКА ФОРМАТА 3.1.1. КАК ОТЛИЧИТЬ NE-ПРОГРАММУ ОТ EXE-ПРОГРАММЫ 3.1.2. СПЕЦИФИЧЕСКИЙ NE-ЗАГОЛОВОК 3.1.3. ОРГАНИЗАЦИЯ МЕЖСЕГМЕНТНЫХ ССЫЛОК 3.2. КОД NE-ИНФЕКТОРА 3.2.1. ПОЛУЧЕНИЕ ИНФЕКТОРОМ УПРАВЛЕНИЯ 3.2.2. ВОЗВРАТ УПРАВЛЕНИЯ В NE-ПРОГРАММУ 3.2.3. ДОСТУП К АБСОЛЮТНЫМ АДРЕСАМ 3.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ NE-ПРОГРАММУ 4. ЗАЩИТА PE-ПРОГРАММ 4.1. ОБЩАЯ ХАРАКТЕРИСТИКА ФОРМАТА 4.1.1. КАК ОТЛИЧИТЬ PE-ПРОГРАММУ ОТ ДРУГИХ ТИПОВ ПРОГРАММ 4.1.2. СПЕЦИФИЧЕСКИЙ PE-ЗАГОЛОВОК 4.1.3. PE-ПРОГРАММЫ НА ДИСКЕ И В ПАМЯТИ 4.1.4. ТАБЛИЦА ОПИСАНИЯ СЕКЦИЙ 4.2. КОД PE-ИНФЕКТОРА 4.2.1. ПРОБЛЕМА ТОЧКИ ВХОДА 4.2.2. ДОСТУП К ФИЗИЧЕСКИМ АДРЕСАМ ПАМЯТИ 4.2.3. ДОСТУП К СИСТЕМНОМУ СЕРВИСУ WINDOWS 4.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ PE-ПРОГРАММУ ЗАКЛЮЧЕНИЕ ЛИТЕРАТУРА ...информации, приведенной в статье, заведомо мало для написания вирусов; это сделано специально... ВВЕДЕНИЕ Дано: Исполняемый файл какой-либо программы (COM, EXE, NE или PE). Требуется: Обеспечить нормальную работу на своем (или каком-нибудь заранее выбранном другом) компьютере и неработоспособность на других компьютерах. Дополнительные условия: a) привязка к компьютеру должна осуществляться подсчетом контрольной суммы байтов BIOS и сравнением полученного значения с эталонным; б) процедура проверки должна прикрепляться к защищаемой программе по вирусному принципу. Это классическая задача из области "Защита от несанкционированного копирования (ЗНСК)" /3-6/. В первой половине 90-х годов XX века немало электронной крови было пролито на полях виртуальных сражений между теми, кто старался защитить софт от воровства и теми, кто эти защиты ломал. Как корректно "заразить" программу? Как защититься от дизассемблирования и отладки защитного кода? К чему привязываться - к BIOS-у, к аппаратной заглушке типа HASP или к нестандартно отформатированной ключевой дискете? Это были великие битвы! Они продолжаются и сейчас, но в связи с доминированием операционных систем, работающих в защищенном режиме (Windows), с распространением отладчиков типа SoftICE/WinICE, с появлением интеллектуальных копировщиков дискет типа FDA, они переместились несколько в другую плоскость /2/. Цель данной статьи - дать минимальное представление о способах внедрения в исполняемые файлы навесных защитных модулей. Конечно, большая часть информации, которая будет рассматриваться далее, не просто хорошо известна, а считается просто "арифметикой" вирусологии и ЗНСК. Но тем не менее, последние годы внесли в правила этой "арифметики" ряд тонких нюансов. Кроме того, когда у меня возникла проблема защитить чужую Win-программу от копирования, я не смог найти ни одной мало-мальски пригодной навесной защиты. Пришлось делать самому. Разумеется, все эти "защиты" отламываются "легким движением руки", но ведь они направлены в -основном против мирных юзеров, которым достаточен сам факт защищенности программы, а также против ламеров, которые кроме GetWindowText больше ничего и не знают. Наконец, статья поможет разобраться, как же конкретно вирусы прикрепляются к относительно новым форматам исполняемых файлов, и как их оттуда "отдирать". Обратное неверно: информации, приведенной в статье, заведомо мало для написания вирусов; это сделано специально. Так что, как хочется надеяться, мои разработки имеют хоть и крохотную, но ненулевую практическую ценность. 1. ЗАЩИТА COM-ПРОГРАММ 1.1. ОБЩАЯ ХАРАКТЕРИСТИКА ФОРМАТА СОМ-программа - это фрагмент машинного кода, начинающийся сразу с исполняемой команды. Формат появился еще в CP/M, долго существовал в DOS и успешно дожил до наших дней. По крайней мере, даже Windows 2000 и Windows NT эмулируют его исполнение. Идея заражения крайне проста - инфектор должен получить управление первым, выполнить свои задачи, а потом восстановить в памяти код COM-программы в исходном виде и вернуть ему управление. Стандартный метод "заражения" /1/. Инфектор приписывается к концу файла, а в начало вписывается команда перехода на его (инфектора) тело. По завершении работы инфектор восстанавливает (заранее сохраненные на этапе инфицирования где-то внутри себя) байты, затертые командой перехода, и передает управление на адрес CS:100h (это стартовый адрес COMпрограммы в памяти). Нюансы. При внедрении навесного кода необходимо отслеживать фактический формат программы, поскольку встречаются файлы с расширением COM, первые байты которых равны 'MZ' - признак EXE -программы. Необходимо отслеживать, чтобы суммарная длина программы с добавленным "привоем" не превышала 64 Кб, иначе "гибрид" не будет работать. Некоторые COM-программы (системные утилиты DOS7) имеют сложный формат, рассчитанный на подключение разноязыких наборов текстовых сообщений. Взаимное расположение отдельных фрагментов таких программ обеспечивается структурой вида: ; байты программы ... Lang db 'ENU' ; Или 'RUS','FRA', etc. для разных языков Sign db 'NS' ; Уникальная сигнатурка Offs dw ? ; Смещение После дописывания инфектора в конец файла необходимо перенести туда же и хххNS-байты, причем смещение в них надо скорректировать, прибавив к нему длину инфектора и длину дополнительной копии хххNS-структуры (т. к. старую мы не удаляли). 1.2. КОД COM-ИНФЕКТОРА Собственно говоря, здесь все крайне примитивно: - адресуемся на BIOS (DS:SI:=0F000h:0), суммируем первые 256 байтов без переноса - это будет контрольная сумма BIOS; - сравниваем рассчитанную контрольную сумму BIOS с эталоном, зашитым по смещению 0х16; - если они не совпадают, то просто завершаем работу, иначе восстанавливаем по абсолютным адресам CS:100h, CS:101h и CS:102h байты, сохраненные ранее в пристыкованном модуле по смещениям 0x1F, 0x24 и 0x29 и отдаем управление программе-носителю, втолкнув в стек адрес 100h и выполнив "короткий" ret. ; (c) Климентьев К., Самара 2000 00 60 pusha 01 1E push ds 02 6800F0 push F000 05 1F pop ds 06 2BC0 sub ax,ax 08 8BF0 mov si,ax 0A B90001 mov cx,0100 0D FC cld 0E AC lodsb 0F 02E0 add ah,al 11 E2FB loop 0000000E 13 1F pop ds 14 80FC dw 0FC80h ; cmp ah, ?? 16 ?? db ?? ; <- контрольная сумма BIOS 17 61 popa 18 7401 je 0000001B 1A C3 retn 1B C6060001 dd 010006C6 ; mov b,[00100], ?? 1F ?? db ?? ; <- 1-й сохраненный байт 20 C6060101 dd 010006C6 ; mov b,[00101], ?? 24 ?? db ?? ; <- 2-й сохраненный байт 25 C6060201 dd 010006C6 ; mov b,[00102], ?? 29 ?? db ?? ; <- 3-й сохраненный байт 2A 680001 push 0100 2D C3 retn 1.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ COM-ПРОГРАММУ Переменные байты защитного кода должны быть проинициализированы программой - генератором защиты, причем она же должна: - приписать этот код к концу защищаемой программы; - организовать переход на нее в защищаемой программе; - если потребуется, дописать хххNS-байты. Приведем откомментированный текст на языке Си для процедуры, отвечающей за "заражение" COM -программ. Примечание. Считается, что: - ранее уже был распознан тип "заражаемой" программы; - эталонная контрольная сумма BIOS для данной машины уже подсчитана ранее и хранится в переменной BIOS_CSUM. struct enuns { unsigned char en[5]; unsigned cs; }; struct jump { unsigned char jmp; unsigned int ofs; }; /* Длина COM-"инфектора" */ #define C_LEN 46 unsigned char Module_COM[C_LEN] = { 0x60, 0x1E, 0x68, 0x00, 0xF0, 0x1F, 0x2B, 0xC0, 0x8B, 0xF0, 0xB9, 0x00, 0x01, 0xFC, 0xAC, 0x02, 0xE0, 0xE2, 0xFB, 0x1F, 0x80, 0xFC, 0x00, 0x61, 0x74, 0x01, 0xC3, 0xC6, 0x06, 0x00, 0x01, 0x00, 0xC6, 0x06, 0x01, 0x01, 0x00, 0xC6, 0x06, 0x02, 0x01, 0x00, 0x68, 0x00, 0x01, 0xC3 }; // (c) Климентьев К., Самара 2000 int Infect_COM(char *fn) { int f; /* Хэндл */ long l; /* Длина файла */ unsigned char b1,b2,b3; /* Сохраненные байты */ struct enuns e; /* Буфер под ENUNS */ struct jump j; /* Команда передачи управления на модуль */ /* Открываем файл */ f = my_open(fn,O_RDWR|O_BINARY); /* Читаем стартовые байты */ my_read(f,&b1,1); my_read(f,&b2,1); my_read(f,&b3,1); /* Переходим на позицию ENUNS и читаем 7 байтов */ my_seek(f,-7,SEEK_END); my_read(f,&e,7); /* Переходим на конец файла, определяя его длину */ l=my_seek(f,0,SEEK_END); /* Сохраняем в инфекторе стартовые байты */ Module_COM[0x1F] = b1; Module_COM[0x24] = b2; Module_COM[0x29] = b3; /* Сохраняем в инфекторе контрольную сумму BIOS */ Module_COM[0x16] = BIOS_CSUM; /* Генерируем команду перехода на инфектор вида JMP <code> */ j.jmp = 0xE9; j.ofs = (unsigned) (l-3); /* Приписываем инфектор к концу файла */ my_write(f, Module_COM, C_LEN); /* Если присутствует ENUNS, то корректируем смещение и приписываем все это к концу файла */ if ((e.en[3]=='N')&&(e.en[4]=='S')) { e.cs += (unsigned) C_LEN; e.cs += (unsigned) 7; /* !!! */ my_write(f,&e,7); } /* Переходим на начало файла и вписываем команду JMP */ my_seek(f,0,SEEK_SET); my_write(f,&j,3); /* Закрываем файл */ my_close(f); return 0; } Следует отметить также способ записи инфектора в начало COM-программы, при этом проблема с хххNS-байтами практически отсутствует. 2. ЗАЩИТА EXE-ПРОГРАММ 2.1. ОБЩАЯ ХАРАКТЕРИСТИКа ФОРМАТА Формат EXE-программ появился позже. Он предусматривает в общем случае размещение кода, данных и стека в разных сегментах, а также переносимость адресных ссылок (т.е. независимость их от сегмента, в который загружена программа). Но программа по-прежнему работает только в реальном режиме, и максимальный адрес памяти, до которого она может "дотянуться", составляет 0F000h:0FFFFh (или 0FFFFh:0FFFFh для 80286). Соответственно, файл EXE-программы представляет собой набор кода и данных, предваряемый: - общим заголовком (Exe Header); - таблицей настройки ссылок (Relocation Table). Заголовок имеет следующий формат: struct EXEhdr { WORD MZ; // +0 'MZ' WORD PartPag; // +2 длина неполной последней страницы WORD PageCnt; // +4 длина образа (+заголовок) в 512-байтниках WORD ReloCnt; // +6 число элементов в Relocation Table WORD HdrSize; // +8 длина заголовка в 16-байтниках WORD MinMem; // +0aH минимум требуемой памяти WORD MaxMem; // +0cH максимум требуемой памяти WORD ReloSS; // +0eH сегмент стека (относительно RootS) WORD ExeSP; // +10H указатель стека WORD ChkSum; // +12H контрольная сумма WORD ExeIP; // +14H счетчик команд WORD ReloCS; // +16H сегмент кода (относительно RootS) WORD TablOff; // +18H позиция в файле 1-го элемента Relocation Table WORD Overlay; // +1aH номер оверлея WORD r1; // +1cH зарезервировано WORD r2; // +1eH зарезервировано }; Поле TablOff содержит позицию в файле для Relocation Table, которая имеет длину ReloCnt элементов вида: 0 Смещение 1-го слова отностельно его сегмента 2 Сегмент 1-го слова (относительно RootS) 4 Смещение 2-го слова отностельно его сегмента 6 Сегмент 2-го слова (относительно RootS) и т.д. ... Сегмент настраиваемых слов отсчитывается от RootS - "корневого" сегмента загруженной программы, т.е. от сегмента, лежащего сразу за PSP. Сам сегментный адрес PSP можно определить при помощи функции 51h прерывания 21h, кроме того, на него сразу после загрузки программы в память указывают сегментные регистры ES и DS. Рассмотрим способы пристыковки внешних фрагментов (инфекторов) к EXE-программам /1,5,6/. Стандартный метод /1/. Код "инфектора" приписывается к концу EXE-файла. При этом в его теле сохраняются прежние значения полей заголовка ReloCS и ExeIP. Эти же поля в заголовке заменяются новыми значениями ReloCS' и ExeIP' так, чтобы управление было передано сразу на код "инфектора". Пусть длина EXE-файла составляет L байт, а модифицированная длина L':=L+VirLen байт. Тогда: ReloCS' := (L-HdrSize*16)/16 (или ReloCS+L/16); ExeIP' := (L-HdrSize*16)%16 (или VirLen%16). Также модифицируются значения полей PartPag и PageCnt, чтобы по ним можно было правильно рассчитать новую, увеличенную длину образа задачи: PageCnt := L'/512; PartPag := L'%512. Иногда, по мере необходимости, модифицируются также значения полей ReloSS и ExeSP, - дабы стек не налезал на код "инфектора". Чтобы корректно передать управление программе-носителю, достаточно выполнить из "инфектора" длинный переход по адресу (PSP+16+ReloCS):ExeIP. Нюансы. Пожалуй, самый главный нюанс связан с так называемыми сегментированными или оверлейными программами. Суть проблемы заключается в том, что некоторые EXE-файлы могут состоять как бы из двух (или более) частей. 1. Первая содержит заголовок ExeHeader, таблицу Relocation Table, код и данные. Она носит наименование "корневого сегмента". Значения полей PartPag и PageCnt соответствуют длине этой первой части. 2. Вторая часть имеет произвольную структуру и просто приписана к файлу сразу после конца корневого сегмента. Программа, загружаемая в память из корневого сегмента, по мере необходимости считывает из этого "довеска" некую необходимую ей информацию. Характерный признак таких EXE-программ - несоответствие реальной длины файла и длины программы, рассчитанной по значениям полей PartPag и PageCnt. Такими программами являются, например, различные информационные системы, написанные на CLIPPER-е. Если формально приписать инфектор к концу такого файла, то возможны ситуации, когда: - инфектор не будет загружен в память вместе с программой, в результате точка входа в инфектор, рассчитываемая по значениям полей ReloCS и ExeIP, будет указывать "в никуда"... результат очевиден; - программа, находящаяся в корневом сегменте, прочитает из конца файла не то, что ей требуется, а "мусор", что также приведет к краху программы. Решние проблемы. Проще всего - сдвинуть образ программы (код и данные) вниз и вписать инфектор между Relocation Table и программой, естественно, перенастроив ссылки в программе. Прием, очень редко используемый авторами вирусов ввиду его трудоемкости и низкой скорости. Но у нас задача другая - нам требуется повышенная надежность нашего "привоя". При этом придется: - установить ReloCS=ExeIP=0; - без дополнительных проверок просто пересчитать PartPag и PageCnt с учетом новой длины файла; - модифицировать (с учетом сдвига) поля Relocation Table и слова, на которые они ссылаются (если этот сдвиг будет кратен минимальному размеру сегмента, - 16 байтам, - то эта операция крайне упростится). 2.2. КОД EXE-ИНФЕКТОРА Отличия от процедуры-инфектора COM-программы небольшие. Основное отличие - способ возврата в программу-носитель: в стек заносятся поочередно смещение (ExeIP) и сегмент (16+ReloCS+3) старой точки входа. Последнее число получается следующим образом. Поскольку реальная длина инфектора 40 байт, целесообразно взять ее "с запасом" - в размере 48 байт: это число кратно 16 (длине параграфа), значит код программы придется сдвигать на целое число параграфов и, соответственно, к сегментам точки входа и стека достаточно прибавить 48/16=3, а смещения точки входа и стека можно оставить прежними. ; (c) Климентьев К., Самара 2000 00 60 pusha 01 1E push ds 02 6800F0 push 0F000h 05 1F pop ds 06 2BC0 sub ax,ax 08 8BF0 mov si,ax 0A B90001 mov cx,0100 0D FC cld 0E AC lodsb 0F 02E0 add ah,al 11 E2FB loop 0000000E 13 1F pop ds 14 80FC dw 0FC80h ; cmp ah, ?? 16 ?? db ?? ; <- контрольная сумма BIOS 17 61 popa 18 7404 je 0000001E 1A B44C mov ah,4C 1C CD21 int 21 1E 8CC0 mov ax,es 20 50 db 50h ; add ax, ReloCS+16+3 21 ???? dw ???? ; <- 16 на PSP, 3 на инфектор 23 50 push ax 24 68 db 68h ; push ExeIP 25 ???? dw ???? ; <- сохраненное значение ExeIP 27 CB retf 2.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ EXE-ПРОГРАММУ Переменные байты защитного кода должны быть проинициализированы программой - генератором защиты, причем она должна "раздвинуть" код программы так, чтобы после заголовка образовалось пустое место, потом перенастроить ссылки в Relocation Table с учетом сдвига программного кода и модифицировать нужные поля в EXE-заголовке. Приведем откомментированный текст на языке Си для процедуры, отвечающей за "заражение" EXE-программ. Примечание. Считается, что: - ранее уже был распознан тип "заражаемой" программы; - эталонная контрольная сумма BIOS для данной машины уже подсчитана ранее и хранится в переменной BIOS_CSUM. #define E_LEN 40 unsigned char Module_EXE[E_LEN] = { 0x060,0x01E,0x068,0x000,0x0F0,0x01F,0x02B,0x0C0, 0x08B,0x0F0,0x0B9,0x000,0x001,0x0FC,0x0AC,0x002, 0x0E0,0x0E2,0x0FB,0x01F,0x080,0x0FC,0x000,0x061, 0x074,0x004,0x0B4,0x04C,0x0CD,0x021,0x08C,0x0C0, 0x005,0x000,0x000,0x050,0x068,0x000,0x000,0x0CB }; struct ReloTab { unsigned r_off; // Смещение unsigned r_seg; // Сегмент }; #define BUFLEN 512 // (c) Климентьев К., Самара 2000 int Infect_EXE(char *fn) { int f; /* Хэндл */ long curr_p; /* Текущая позиция элемента Relocation */ long relo_p; /* Текущая позиция настраиваемого слова */ long base_a; /* Базовый адрес образа задачи */ long read_p, write_p; /* Позиции чтения/записи */ unsigned w; /* Буфер под настраиваемое слово */ unsigned char b1[BUFLEN]; /* Буфер #1 */ unsigned char b2[BUFLEN]; /* Буфер #2 */ struct ReloTab rt; struct EXEhdr eh; int i, q1, q2; /* Открываем файл */ f = my_open(fn,O_RDWR|O_BINARY); /* Читаем заголовок */ my_read(f,&eh,sizeof(eh)); /* Сохраняем в инфекторе ReloCS и ExeIP */ *((unsigned *)(&Module_EXE[0x21])) = eh.ReloCS+0x10+3; *((unsigned *)(&Module_EXE[0x25])) = eh.ExeIP; /* Сохраняем в инфекторе контрольную сумму BIOS */ Module_EXE[0x16] = BIOS_CSUM; /* Видоизменяем точку входа в заголовке */ eh.ReloCS=0; eh.ExeIP=0; /* Сдвигаем сегмент стека */ eh.ReloSS+=3; /* Видоизменяем длину программы в заголовке */ eh.PartPag+=E_LEN; if (eh.PartPag>511) { eh.PartPag=eh.PartPag%BUFLEN; eh.PageCnt++; } /* Записываем заголовок на место */ my_seek(f,0,SEEK_SET); my_write(f,&eh,sizeof(eh)); /* Базовая позиция в файле для образа задачи */ base_a = eh.HdrSize*16; /* Настраиаем Relocation Table */ if (eh.ReloCnt>0) { curr_p = eh.TablOff; for (i=0;i<eh.ReloCnt;i++) { my_seek(f,curr_p,SEEK_SET); my_read(f,&rt,4); relo_p = rt.r_seg*16 + rt.r_off; rt.r_seg+=3; my_seek(f,curr_p,SEEK_SET); my_write(f,&rt,4); my_seek(f,base_a+relo_p,SEEK_SET); my_read(f,&w,2); w+=3; my_seek(f,base_a+relo_p,SEEK_SET); my_write(f,&w,2); curr_p+=4; } } /* Хитрый цикл - освобождаем место для инфектора */ read_p = eh.HdrSize*16; write_p = read_p+48; my_seek(f,read_p,SEEK_SET); q2 = my_read(f,b2,BUFLEN); read_p+=BUFLEN; do { my_seek(f,read_p,SEEK_SET); q1 = my_read(f,b1,BUFLEN); my_seek(f,write_p,SEEK_SET); my_write(f,b2,q2); q2=q1; for(i=0;i<q1;i++) b2[i]=b1[i]; write_p+=BUFLEN; read_p+=BUFLEN; } while (q1==BUFLEN); my_seek(f,write_p,SEEK_SET); my_write(f,&b2,q1); /* Записываем инфектор в "каверну" */ my_seek(f,(unsigned long)(eh.HdrSize*16),SEEK_SET); my_write(f,&Module_EXE,E_LEN); my_close(f); } Конечно, даже и таким образом не удастся "заразить" все 100% программ: есть программы, проверяющие свою длину или использующие взаимное месторасположение своих отдельных фрагментов... в-общем, сделано главное - проиллюстрирована сама идея размещения инфектора между заголовком и кодом программы. 3. ЗАЩИТА NE-ПРОГРАММ 3.1. ОБЩАЯ ХАРАКТЕРИСТИКА ФОРМАТА Формат появился где-то в середине 80-х годов как способ организации программных файлов, общий для Windows и OS/2. Программы в этом формате представляют собой весьма сложную смесь различных типов кода, данных и ресурсов /7,9/. До сих пор Windows-программ в NE-формате немало, они присутствуют даже среди стандартных утилит Windows 9X. 3.1.1. КАК ОТЛИЧИТЬ NE-ПРОГРАММУ ОТ EXE-ПРОГРАММЫ 1. Первые 32 байта файла занимает традиционный заголовок EXE-программы, начинающийся с 'MZ' (см. п. 2.1). В этом заголовке ReloCS:ExeIP указывает на маленькую "заглушку" ("stub") - обычную EXE-программу примерно следующего содержания: mov dx, offset M push cs pop ds mov ah, 09h int 21 ; Стандартное mov ax, 4C01h ; завершение программы int 21 M db 'This program must be run under Microsoft Windows$' Возможны варианты: 'This program requires Microsoft Windows', 'This program can't run in DOS mode', 'Кретин, эта программа будет выполняться только из-под Форточек' и пр. В EXE-заголовке поле TablOff, - это cлово по смещению 0x18, - для NE-программ содержит значение, большее или равное 0x40. Такое значение - признак NE-программы или PE-программы (см. далее). 3.1.2. СПЕЦИФИЧЕСКИЙ NE-ЗАГОЛОВОК Для NE-программ по адресу 0х3С располагается слово winInfoOffset - указатель на специфический заголовок NE-файла. Очень часто (но не всегда) NE-заголовок размещается в файле по смещению 400h, причем пространство после EXE-заголовка с заглушкой до NE-заголовка - пусто и никем не неиспользуется. NE-заголовок, адресуемый полем winInfoOffset, имеет в длину 64 байта и соответствует следующей структуре: struct NEhdr { WORD NE; // +0 0x454E = 'NE' BYTE linkerVersion; // +2 версия компоновщика BYTE linkerRevision; // +3 ревизия компоновщика WORD entryTabOffset; // +4 смещение таблицы точек входа WORD entryTabLen; // +6 длина таблицы точек входа DWORD reserved1; // +8 ??? WORD exeFlags; // +0CH биты описания исполняемого кода WORD dataSegNum; // +0EH число сегментов "автоданных" WORD localHeapSize; // +10H исходный размер локального хипа WORD stackSize; // +12H -"- стека WORD NE_IP; // +14H смещение в сегменте точки входа WORD NE_CS; // +16H индекс сегмента точки входа WORD NE_SP; // +18H смещение в стековом сегменте WORD NE_SS; // +1AH индекс стекового сегмента (с 1) WORD segTabEntries; // +1CH к-во элементов в таблице сегментов WORD modTabEntries; // +1EH -"- в таблице вхождений WORD nonResTabSize; // +20H -"- в таблице нерезидентов WORD segTabOffset; // +22H смещ. до табл. сегментов от NEHdr WORD resTabOffset; // +24H -"- до таблицы ресурсов WORD resNameTabOffset; // +26H -"- до таблицы имен ресурсов WORD modTabOffset; // +28H -"- до таблицы модулей WORD impTabOffset; // +2AH -"- до таблицы импорта WORD nonResTabOffset; // +2CH -"- до таблицы нерезидентов WORD reserved2; // +2EH ??? WORD numEntryPoints; // +30H к-во перемещаемых точек входа WORD shiftCount; // +32H Log(SegSiz,2) WORD numResourceSegs; // +34H число ресурсных сегментов BYTE targetOS; // +36H код операционной системы BYTE miscFlags; // +37H прочие биты описания программы WORD fastLoadOffset; // +38H смещение области быстрой загрузки WORD fastLoadSize; // +3AH размер области быстрой загрузки WORD reserved3; // +3CH ??? BYTE winRevision; // +3EH текущая версия Форток BYTE winVersion; // +3FH текущая ревизия Форток }; Центральное место в структуре NE-заголовка занимают поля, описывающие смещения _от_начала_NE_заголовка различных таблиц: таблицы сегментов, таблицы ресурсов, таблицы модулей и пр. В частности, слово segTabOffset помогает получить доступ к таблице сегментов, где под сегментом понимается фрагмент программы, хранящий определенный тип информации: код или данные. (Обычно segTabOffset=40h, т.е. таблица сегментов лежит сразу за NE-заголовком). Эта таблица содержит segTabEntries записей 8-байтового размера и следующего вида: struct tagTBSEGMENT { WORD segDataOffset; // +00 смещение сегмента в логических // секторах _от_начала_файла_ !!! WORD segLen; // +02 длина сегмента в байтах; WORD segFlags; // +04 слово описания сегмента; WORD segMinSize; // +06 резервируемая под сегмент память, // 0 означает максимум: 64Кб. }; Логический сектор - это единица измерения внутри NE-программы. Размер логического сектора может быть разным, двоичный логарифм значения размера хранится в поле shiftCount NE-заголовка. Обычно shiftCount=9, и это значит, что длина логического сектора составляет 2^ 9=512 байт. Позиции всех сегментов (см. поле segDataOffset) в NE -файле _выровнены_по_логическим_секторам! О поле segFlags этой таблицы можно сказать чуть-чуть подробней: - бит 0: установлен для сегмента данных и сброшен для сегмента кода; - бит 7: если установлен, то означает признаки "только для чтения" (если это сегмент данных) и "только для исполнения" (если это сегмент кода); - бит 8: если установлен, то в сегменте присутствуют поля, которые требуют настройки загрузчиком программ по таблице перемещаемых ссылок Relocation Table (примерно как это было в обычных EXE). Поля NE_IP и NE_CS в NE-заголовке описывают положение точки входа в NE-программу. В отличие от обычного EXE-файла в данном случае содержимое NE_CS представляет собой индекс строки в вышеописанной таблице tagTBSEGMENT, причем первая запись имеет номер 1, вторая 2 и т. д. Смысл NE_IP традиционен. Разумеется, кодовых сегментов может быть несколько, но точка входа - одна. 3.1.3. ОРГАНИЗАЦИЯ МЕЖСЕГМЕНТНЫХ ССЫЛОК Сразу после кодового сегмента иногда располагается Relocation Table - таблица настройки перемещаемых ссылок. Наличие или отсутствие этой таблицы определяется битом 8 поля segFlags таблицы tagTBSEGMENT. Положение таблицы легко вычислить, зная местоположение сегмента (поле segDataOffset таблицы tagTBSEGMENT) и его длину (поле segLen таблицы tagTBSEGMENT). Структура элемента Relocation Table: struct tagRELOCATEITEM { BYTE addressType; // +00 способ задания адреса ссылки BYTE relocationType; // +01 тип настроечной ссылки WORD itemOffset; // +02 смещение в сегменте до релокейшена WORD index; // +04 индекс в таблице ссылок, // либо номер сегмента WORD extra; // +06 порядковый номер функции, // либо смещение в сегменте } Непосредственно перед Relocation Table располагается слово, содержащее количество записей в этой таблице. Очень важную роль настроечные ссылки играют при межсегментных переходах. Поскольку абсолютный адрес точки перехода становится известен только после загрузки программы в память, то код команд перехода db 0EAh ; JMP dw ???? ; Неизвестное смещение dw ???? : Неизвестный сегмент db 09Ah ; CALL dw ???? ; Неизвестное смещение dw ???? : Неизвестный сегмент нуждается в дополнительной настройке - в процессе загрузки необходимо поместить на место неизвестных смещений и сегментов конкретные числовые значения. Чтобы не раздувать размеры Relocation Table, авторы Windows решили сэкономить на ссылках. Например, если в сегменте присутствует несколько вызовов одной и той же внешней (располагающейся в другом сегменте) процедуры, то в Relocation Table имеется строка только для описания одной, самой первой такой ссылки на внешнюю процедуру. Но зато слово смещения в этой ссылке указывает на следующую ссылку, соответствующую той же внешней процедуре, та в свою очередь - на следующую... и так далее, пока в слове смещения не встретится признак конца цепочки - слово 0FFFFh: .100F1 9AE8010000 call KERNEL.89 .101E7 9AFB010000 call KERNEL.89 .101FA 9AFFFF0000 call KERNEL.89 ^^^^ Таким образом, при запуске программы Windows по одной строке Relocation Table единым махом настраивает несколько (иногда очень много!) ссылок. Разумеется, если в файле присутствует единственное обращение по какой-то внешней ссылке, то значение смещения сразу должно быть 0FFFFh. 3.2. КОД NE-ИНФЕКТОРА 3.2.1. ПОЛУЧЕНИЕ ИНФЕКТОРОМ УПРАВЛЕНИЯ Идея. Необходимо создать для инфектора еще один кодовый сегмент. Для этого NE-заголовок и расположенная _сразу_за_ним_ таблица сегментов простым копированием смещаются на 8 байтов в сторону начала программы. Это возможно (но не всегда!), поскольку между MZ-заголовком и NE -заголовком обычно имеется довольно большой неиспользуемый фрагмент. При этом не забудем скорректировать адрес NE-заголовка: winInfoOffset:= winInfoOffset - 8. После этой операции сразу освободится место для дополнительной строки в конце таблицы сегментов. Шаг 1: модификация NE-заголовка. Поскольку NE-заголовок сместится на 8 байтов в сторону начала программы, а всякие таблицы (кроме таблицы сегментов) останутся на прежнем месте, то необходимо отразить этот факт в NE-заголовке увеличением на 8 относительных адресов (смещений) этих таблиц. Для пяти это делается обязательно: entryTabOffset+=8; // Адрес таблицы точек входа resTabOffset+=8; // Адрес таблицы ресурсов resNameTabOffset+=8; // Адрес таблицы имен ресурсов modTabOffset+=8; // Адрес таблицы модулей impTabOffset+=8; // Адрес таблицы импорта а для таблицы нерезидентов только в том случае, если она существует и располагается после NE-заголовка: nonResTabOffset+=8; // Адрес таблицы нерезидентов Также отметим, что размер таблицы сегментов увеличится на 1: segTabEntries +=1. Шаг 2: расширение таблицы сегментов. Создадим в освободившемся месте строку - описатель для нового кодового сегмента. В нее запишем: segLen := Длина_инфектора_в_байтах; segFlags := 180h; // См. п. 3.1.2. segMinSize := Длина_инфектора_в_байтах. Значение поля segDataOffset в этой строке рассчитывается несколько сложнее. Предполагается, что сегмент инфектора будет располагаться в конце программы. Значит, нужно выровнять старую длину файла так, чтобы она образовывала целое число логических секторов, и тогда уже segDataOffset := (Выровненная_длина_файла) div shiftCount. Шаг 3: изменение точки входа. Сохраним внутри кода инфектора (внутри команды JMP OOOO:SSSS) старые значения NE_IP и NE_ CS, а в NE-заголовке пропишем новую точку входа: - поскольку новая точка входа будет располагаться в сегменте, описатель которого мы поместили в конец таблицы сегментов, то в поле NE _CS нужно внести номер последней строки этой таблицы (вспомним, что нумерация идет с единицы!): NE_CS := segTabEntries; - предположив, что код инфектора начнет исполняться с первого же (точнее, нулевого) своего байта, заполним поле NE_IP нулем: NE_OP := 0; Шаг 4: создание Relocation Table. Для того, чтобы инфектор смог передать управление программе-носителю, внутри инфектора должна быть по крайней мере одна команда межсегментного перехода. Таблица описания межсегментных ссылок (Relocation Table) должна располагаться сразу после кода инфектора, т.е. в конце файла. Точнее, сначала должно располагаться слово, содержащее количество строк в этой таблице (в данном случае это 1), nrelocs := 1; а лишь затем сама таблица (состоящая из одной строки): addressType := 3; // Тип адреса релокейшена // есть Segm:Off relocationType :=4; // Флаги релокейшена // (недокументировано!) itemOffset := offset GoHome; // Cмещение релокейшена index := старый NE_CS; extra := старый NE_IP; Шаг 5: обновление файла. Ну а теперь можно записать все на свои места внутри NE-файла: - модифицированный (только слово winInfoOffset по адресу 0 x3С) EXE -заголовок на свое старое место; - модифицированный NE-заголовок и расширенную (на одну строку) таблицу сегментов - на 8 байтов ближе (чем старое местоположение) к началу файла; - код инфектора - в конец файла (не непосредственно в конец, а в позицию, выровненную на целое число логических секторов); - Relocation Table - сразу после кода инфектора. 3.2.2. ВОЗВРАТ УПРАВЛЕНИЯ В NE-ПРОГРАММУ Инфектор должен завершаться командой межсегментного перехода: db 0EAh ;\ OLD_IP dw 0000h ; > JMP 0FFFFh:0 OLD_CS dw FFFFh ;/ Т.к. в поле itemOffset таблицы Relocation Table указан адрес сохраненного значения точки входа, то именно к этим сохраненным значениям загрузчиком будут прибавлены нужные смещения, и переход произойдет в нужное место - в старую точку входа. 3.2.3. ДОСТУП К АБСОЛЮТНЫМ АДРЕСАМ Все программы Windows работают в защищенном режиме процессоров 80х86 /11,12/. В этом режиме все адресное пространство компьютера разделено на множество независимых сегментов, содержащих код или данные. Каждой программе доступны только те сегменты, которые были ей назначены на этапе загрузки в память. Адресация к тому или иному сегменту памяти (из числа доступных), осуществляется через механизм селекторов и дескрипторов. Дескриптор - это "паспорт" сегмента, содержащий данные о начальном линейном адресе его начала (база) и длине (лимит), о правах доступа к сегменту и некоторую другую информацию. Дескрипторы объединяются в таблицы дескрипторов - в одну глобальную (GDT), которой ведает операционная система, и множество локальных (LDT), принадлежащих прикладным программам (кстати, и операционной системе тоже). Индекс (номер строки) в таблице дескрипторов носит наименование селектора. Изначально программа снабжается несколькими сегментами: для кода, для данных, для стека и пр. Область оперативной памяти, в которой размещается BIOS, по умолчанию программе недоступна. Точнее, программы могут запросить и получить селектор этой области у ядра Windows (в модуле Kernel) /10/. Но наш инфектор к стандартным процедурам Windows (например, к GetModuleHandle из библиотеки ToolHelp) обращаться не может. Самый простой способ получить доступ к области памяти по адресу 0F000h:0 - использовать сервис DPMI, работающий как в Windows 3.x, так и в Windows 9X через прерывание 31h. Нам понадобятся следующие функции DPMI /8,11/: 1. Создать новый селектор в текущей LDT Вход: ax = 0 cx = кол-во дескрипторов Выход: ax - новый селектор (c "пустым" дескриптором) 2. Установить в дескрипторе селектора базу Вход: ax = 7 bx = селектор cx:dx = 32-разрядный "плоский" адрес базы 3. Установить в дескрипторе селектора лимит Вход: ax = 8 bx = селектор cx:dx = 32-разрядный "плоский" адрес лимита 4. Освободить селектор и дескриптор Вход: ax = 1 bx = селектор Все функции при ошибке возращают бит C=1. Код NE-инфектора выглядит следующим образом: ; (c) Климентьев К., Самара 2000 00 60 pusha 01 1E push ds 02 29C0 sub ax,ax ; Создать селектор 04 B90100 mov cx,0001 07 CD31 int 31 09 8BD8 mov bx,ax 0B B80700 mov ax,0007 ; Установить базу 0E B90F00 mov cx,000F 11 BA0000 mov dx,0000 14 CD31 int 31 16 B80800 mov ax,0008 ; Указать лимит 19 B90F00 mov cx,000F 1C BA0001 mov dx,0100 1F CD31 int 31 21 8EDB mov ds,bx 23 29F6 sub si,si 25 8BC6 mov ax,si 27 B90001 mov cx,0100 2A FC cld 2B AC lodsb 2C 00C4 add ah,al 2E E2FB loop 0000002B 30 50 push ax 31 B80100 mov ax,0001 ; Освободить селектор 34 CD31 int 31 36 58 pop ax 37 80FC dw 0FC80h ; cmp ah, ?? 39 ?? db ?? ; <- контрольная сумма BIOS 3A 1F pop ds 3B 61 popa 3C 7505 jne 00000043 3E EA db 0EA ; JMP FFFF:0000 3F 0000 dw 0000 41 FFFF dw FFFF 43 B44C mov ah,4Ch 45 CD21 int 21 Этот код прекрасно работает как под Windows 3.x, так и под Windows 9x, т.к. специально для 16-разрядных NE-программ моделируется "комфортная" среда: защищенный режим типа segmentation /8/, обработчик прерывания Int21h и пр. 3.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ NE-ПРОГРАММУ Процедура работает в соответствии с алгоритмом, описанным в п. 3.2. 1. Примечание. Считается, что: - ранее уже был распознан тип "заражаемой" программы; - эталонная контрольная сумма BIOS для данной машины уже подсчитана ранее и хранится в переменной BIOS_CSUM. #define N_LEN 71 unsigned char Module_NE[N_LEN] = { 0x60, 0x1E, 0x29, 0xC0, 0xB9, 0x01, 0x00, 0xCD, 0x31, 0x8B, 0xD8, 0xB8, 0x07, 0x00, 0xB9, 0x0F, 0x00, 0xBA, 0x00, 0x00, 0xCD, 0x31, 0xB8, 0x08, 0x00, 0xB9, 0x0F, 0x00, 0xBA, 0x00, 0x01, 0xCD, 0x31, 0x8E, 0xDB, 0x29, 0xF6, 0x8B, 0xC6, 0xB9, 0x00, 0x01, 0xFC, 0xAC, 0x00, 0xC4, 0xE2, 0xFB, 0x50, 0xB8, 0x01, 0x00, 0xCD, 0x31, 0x58, 0x80, 0xFC, 0x00, 0x1F, 0x61, 0x75, 0x05, 0xEA, 0x00, 0x00, 0xFF, 0xFF, 0xB4, 0x4C, 0xCD, 0x21 }; // (c) Климентьев К., Самара 2000 int Infect_NE(char *fn) { int f; /* Хэндл */ long l; /* Длина файла */ struct WINhdr wh; /* Обобщенный заголовок Win-программы */ struct NEhdr nh; /* NE-заголовок */ struct tagTBSEGMENT *st; /* Указатель на таблицу сегментов */ struct tagRELOCATEITEM rt; /* Строка Relocation Table */ int _one=1; /* Сохраняем в инфекторе контрольную сумму BIOS */ Module_NE[0x39] = BIOS_CSUM; /* Открываем файл */ f = my_open(fn,O_RDWR|O_BINARY); /* Читаем обобщенный заголовок Win-программы */ my_read(f, &wh, sizeof(wh)); /* Определяем старую длину NE-программы */ l=my_seek( f, 0, SEEK_END); /* Переходим на NE-заголовок */ my_seek(f, (long) wh.winInfoOffset, SEEK_SET); /* Считываем NE-заголовок */ my_read(f, &nh, sizeof(nh)); /* Переходим на таблицу сегментов */ my_seek(f, (long) (wh.winInfoOffset + nh.segTabOffset), SEEK_SET); /* Распределяем память под таблицу сегментов (с учетом пустой строки) */ st = (struct tagTBSEGMENT *) my_alloc((nh.segTabEntries+1)*sizeof(struct tagTBSEGMENT)); /* Считываем таблицу сегментов полностью */ my_read(f, st, nh.segTabEntries*sizeof(struct tagTBSEGMENT) ); /* Модифицируем кол-во элементов таблицы сегментов в NE-заголовке */ nh.segTabEntries++; /* Модифицируем необходимые смещения в NE-заголовке */ nh.entryTabOffset+=8; nh.resTabOffset+=8; nh.resNameTabOffset+=8; nh.modTabOffset+=8; nh.impTabOffset+=8; if (nh.nonResTabOffset) nh.nonResTabOffset+=8; /* Сохраняем в Relocation Table старые значения NE_IP и NE_CS */ rt.index = nh.NE_CS; rt.extra = nh.NE_IP; /* Модифицируем значения точки входа в NE-заголовке */ nh.NE_IP=0; // Смещение первой команды инфектора nh.NE_CS=nh.segTabEntries; // Индекс кодового сегмента в таблице сегментов /* Исправляем файловый адрес NE-заголовка в обобщенном заголовке */ wh.winInfoOffset-=8; /* Рассчитываем длину файла, выровненную по лог. секторам */ if (l%(1<<nh.shiftCount)) l=(l/(1<<nh.shiftCount)+1)*(1<<nh.shiftCount); /* Сформируем новую строку в таблице сегментов */ st[nh.segTabEntries-1].segDataOffset = (int) (l/(1<<nh.shiftCount)); st[nh.segTabEntries-1].segLen = N_LEN; st[nh.segTabEntries-1].segFlags = 0x180; st[nh.segTabEntries-1].segMinSize = N_LEN; /* Сформируем новую строку в Relocation Table */ rt.addressType=3; // Тип адреса релокейшена есть Segm:Off rt.relocationType=4; // Флаги релокейшена (недокументировано!) rt.itemOffset = 63; // Смещение релокейшена в инфекторе /* Переходим на начало файла и записываем модифицированный обобщенный заголовок */ my_seek(f, 0, SEEK_SET); my_write(f, &wh, sizeof(wh)); /* Переходим в новую позицию NE-заголовка и записываем модифицированные NE-заголовок и таблицу сегментов */ my_seek(f, (long) wh.winInfoOffset, SEEK_SET); my_write(f, &nh, sizeof(nh)); my_write(f, st, nh.segTabEntries*sizeof(struct tagTBSEGMENT)); /* Переходим на конец файла, выровненный по лог. секторам */ my_seek(f, l, SEEK_SET); /* Записываем код инфектора */ my_write(f, Module_NE, N_LEN); /* Вписываем количество строк в Relocation Table */ my_write(f, &_one, 2); /* Вписываем саму Relocation Table */ my_write(f, &rt, sizeof(rt)); /* Закрываем файл */ my_close(f); } 4. ЗАЩИТА PE-ПРОГРАММ 4.1. ОБЩАЯ ХАРАКТЕРИСТИКА ФОРМАТА Формат появился в первой половине 90-х годов XX века. Фирма MicroSoft планировала использовать его как базовый способ организации программ для своей разрабатывававшейся универсальной 32-разрядной операционной системы. Но "универсальной" ОС в то время так и не получилось, вместо нее были выпущены в свет: - ветвь 16-разрядных операционных систем Windows 3.1/3.11 c ограниченной возможностью поддержки 32-разрядных приложений посредством Win32s; - ветвь 16/32-разрядных операционных систем для индивидуальных пользователей - Windows 95/98/ME; - ветвь полностью 32-разрядных операционных систем для корпоративных пользователей - Windows NT 3.5/4.0/5.0/2000. Все эти ОС в той или иной мере поддерживали PE-формат исполнимых файлов. Но в связи с тем, что на момент своего обнародования формат не был четко зафиксирован и документирован (этого не произошло и до сих пор!), в настоящий момент существует большое количество толкований этого формата со стороны разных производителей программного обеспечения. Программы в этом формате представляют собой весьма сложную смесь различных типов кода, данных и ресурсов /13-16/. 4.1.1. КАК ОТЛИЧИТЬ PE-ПРОГРАММУ ОТ ДРУГИХ ТИПОВ ПРОГРАММ Все, сказанное о NE-формате (см. 3.1.1) полностью справедливо и для PE-формата исполнимых файлов: - в поле TablOfff MZ-заголовка располагается значение, большее или равное 40h - признак не-DOS-программы; - поле ReloCS:ReloIP MZ-заголовка указывает на программу-заглушку; - по адресу 0х3С в файле располагается слово winInfoOffset - указатель в файле на специфический заголовок PE-файла. Главное отличие PE-программ от NE-программ: специфический заголовок PE-файла содержит в начале сигнатуру 'PE', а не 'NE'. 4.1.2. СПЕЦИФИЧЕСКИЙ PE-ЗАГОЛОВОК Существует несколько вариантов PE-заголовка. Отдельные его фрагменты могут присутствовать или отсутствовать в зависимости от того, какие компилятор и линкер были использованы для создания PE-программы, и на какую версию Windows она ориентирована /13/: struct PEhdr { // Постоянная часть DWORD PE; // +00 Сигнатура WORD MachType; // +04 Тип процессора WORD NOfSections; // +06 Количество секций DWORD TimDat; // +08 Время/дата создания DWORD PSymTable; // +0СH Адрес таблицы символов DWORD NOfSymbols; // +10H К-во строк в таблице символов WORD SzOfOptHdr; // +14H Размер переменной части WORD Flags; // +16H Флаги // Переменная часть WORD R1; // +18H ??? BYTE MajorLnkV; // +1AH Старшая версия линкера BYTE MinorLnkV; // +1BH Младшая версия линкера DWORD SizeOfCode; // +1CH Размер исполняемого кода DWORD SizeOfInD; // +20H Размер иниц-ных данных DWORD SizeOfUnInD;// +24H Размер неиниц-ных данных DWORD EntryPoint; // +28H Адрес точки входа DWORD BaseOfCode; // +2CH Смещ. кода в памяти DWORD BaseOfData; // +30H Смещ. иниц-ных данных в памяти // NT-часть DWORD ImBase; // +34H RVA отобpажения файла в память DWORD SectAlign; // +38H Фактор выравнивания объектов в ОЗУ DWORD FileAlign; // +3CH Фактор выравнивания объектов в файле WORD MajorOSV; // +40H | WORD MinorOSV; // +42H | WORD MajorImV; // +44H | WORD MinorImV; // +46H +> Версии и субверсии компонентов WORD MajorSSV; // +48H | WORD MinorSSV; // +4AH | DWORD Win32Vers; // +4CH | DWORD SizeOfIm; // +50H Размер образа программы в ОЗУ DWORD SizeOfHd; // +54H Размеp заголовка и таблицы объектов DWORD CSum; // +58H Контpольная сумма WORD SubS; // +5CH WORD ProcFlags; // +5EH DWORD SizeOfStR; // +60H DWORD SizeOfStC; // +64H DWORD SizeOfHpR; // +68H DWORD SizeOfHpC; // +6CH DWORD LoaderFlags;// +70H DWORD NrOfRVAs; // +74H // Описано только у Hard Wisdom DWORD ExRVA; // +78h RVA таблицы экспорта DWORD ExSize; // +7Ch размер таблицы экспорта DWORD ImRVA; // +80h RVA таблицы импорта DWORD ImSize; // +84h размер таблицы импорта DWORD RsRVA; // +88h RVA таблицы ресурсов DWORD RsSize; // +8Ch размер таблицы ресурсов DWORD ExcRVA; // +90h RVA таблицы исключений DWORD ExcSize; // +94h размер таблицы исключений DWORD ScRVA; // +98h RVA таблицы безопасности DWORD ScSize; // +9Ch размер таблицы безопасности DWORD FURVA; // +A0h RVA таблицы настроек DWORD FUSize; // +A4h размер таблицы настроек DWORD DbRVA; // +A8h RVA таблицы отладочной инф. DWORD DbSize; // +ACh размер таблицы отладочной инф. DWORD IDRVA; // +B0h RVA строки описани модуля DWORD IDSize; // +B4h размер строки описания модуля DWORD MhRVA; // +B8h RVA таблицы описания процессора DWORD MhSize; // +BCh размер таблицы описания процессора DWORD TLRVA; // +C0h RVA области данных цепочек DWORD TLSize; // +C4h размер области данных цепочек DWORD LCRVA; // +C8h RVA таблицы параметров загрузки DWORD LCSize; // +CCh размер таблицы параметров загрузки DWORD R2[2]; // +D0h ??? DWORD IARVA; // +D8h RVA ??? DWORD IASize; // +DCh размер ??? DWORD R3[2]; // +E0h ??? DWORD R4[2]; // +E8h ??? DWORD R5[2]; // +F0h ??? }; 4.1.3. PE-ПРОГРАММЫ НА ДИСКЕ И В ПАМЯТИ PE-программы и на диске, и в оперативной памяти компьютера представляют собой набор секций (еще их называют "сегментами" или "объектами"), среди которых можно отметить: - псевдосекцию "заголовков"; - секцию кода; - секцию данных; - секцию инициализированных данных; - секцию отладочной информации и пр. Как правило, компиляторы/линкеры строят PE-программы так, что каждая секция содержит однородную информацию: или только код, или только данные и т.п. Для дифференциации типов данных каждой секции ставятся в соответствие битовые флаги свойств: разрешение чтения, разрешение записи, разрешение исполнения кода и пр. Тем не менее, ничто не мешает иметь всего одну секцию, со всевозможными установленными флагами, содержащую одновременно и код, и данные, и стек, и т.п. Но распространеные компиляторы/линкеры так не поступают. Наоборот, имеется тенденция введения "внутрифирменных" стандартов. Например, Microsoft выделяет 9 типов данных, хранящихся в секциях: тип ".text" для исполнимого кода (почему не .muzik или .picture?), .data для данных, .idata для констант и т.п. А Borland строит секции с именами CODE для исполнимого кода, DATA для данных и пр. С точки зрения загрузчика Windows-программ эти символические имена ничего не значат. Каждая секция на диске занимает целое число блоков размером по FileAlign (смещение +3СH в PE-заголовке) байтов. Разумеется, реальное количество полезной информации внутри секции может быть (и чаще всего бывает!) меньше, чем распределенное под него на диске место. Первая "псевдосекция" заголовков имеет в файле адрес 0, остальные имеют начальные адреса, кратные FileAlign. Каждая секция в оперативной памяти занимает целое число блоков размером по SectAlign ( смещение +38H в PE-заголовке) байтов. Первая "псевдосекция" заголовков имеет в оперативной памяти адрес ImBase, остальные имеют начальные адреса, кратные SectAlign. Все секции на диске и в памяти располагаются в одной и той же последовательности, только в памяти несколько "попросторней": 0 +---------+ ImBase +---------+ |Заголовки| |Заголовки| |.........| ----------->|.........| +---------+ |.........| |Секция1..| -----+ +---------+ |.........| | |Секция1..| +---------+ +----->|.........| ... |.........| +---------+ +---------+ ... |СекцияN..| -----+ |.........| | +---------+ +---------+ | |СекцияN..| +----->|.........| |.........| +---------+ Загрузчик Windows чисто формально берет секции на диске и помещает их в оперативную память, модифицируя только отдельные адреса. Это означает, что в оперативной памяти по адресу ImBase можно обнаружить сигнатуру 'MZ', потом код "заглушки", потом PE-заголовок и пр.! С другой стороны, это означает, что справочные таблицы и структуры, актуальные при старте и выполнении PE-программ, присутствуют и в файле! Параметр FileAlign, как правило, равен 200h=512 - длине сектора на диске. Параметр SectAlign, как правило, равен 1000h =4096 - длине страницы памяти в защищенном режиме типа pagination. В качестве исключения из правил можно привести пример значения SectAlign из PE-программ, собранных при помощи Tasm32/Tlink32 v4.0: там оно равно 10000h... что, впрочем, тоже кратно 1000h. 4.1.4. ТАБЛИЦА ОПИСАНИЯ СЕКЦИЙ Для доступа к секциям внутри PE-программы (и на диске и в памяти) имеется таблица описания секций. Таблица располагается сразу после PE-заголовка. Ее местоположение в файле может быть вычисленно как сумма значений: - адреса PE-заголовка (поле winInfoOffset в MZ-заголовке по смещению 3Сh от начала файла); - длины постоянной части PE-заголовка (18h байтов); - длины переменной части PE-заголовка (поле SzOfOptHdr в PE-заголовке по смещению 14h). Количество описанных секций (строк в таблице) хранится в поле NOfSections (смещение +06 в PE-заголовке). Таблица состоит из строк следующей структуры: struct PEObjTbl { BYTE ObjName[8]; // Символьное имя секции DWORD VirtSize; // Размер секции в памяти DWORD VirtRVA; // Смещение секции от ImBase DWORD PhSize; // Размер секции на диске DWORD PhOffset; // Смещение секции от начала файла DWORD R1[3]; // ??? DWORD ObjFlags; // Флаги свойств секции }; В поле ObjName хранится справочное символическое имя секции, напимер '.text' или 'CODE'. В поле VirtSize хранится истинное значение полезной информации внутри секции. В поле VirtRVA хранится адрес начала секции в памяти, взятый относительно ImBase, т.е. абсолютное значение этого адреса легко вычисляется как ImBase+VirtRVA. Разумеется, значение VirtRVA кратно SectAlign (см. 4.1.3). В поле PhSize хранится размер секции на диске. Разумеется, он кратен FileAlign (см. SectAlign). В поле PhOffset хранится адрес начала секции в файле. Разумеется, он кратен FileAlign. В поле ObjFlags хранятся битовые флаги свойств секции: 20h - код, 40h - инициализированные данные (константы), 80h - неициализированные данные, 20000000h - исполняемый код, 40000000 - читабельная, 80000000 - изменяемая и пр. Рассмотрим типичные задачи, которые приходится решать при навигации по PE-файлу. Задача 1. Известен RVA (смещение в памяти относительно ImBase) какого-либо объекта, например, EntryPoint - точки входа. Найти местоположение этого же объекта в файле. Решение. Просканировать таблицу объектов до тех пор, пока не будет выполнено условие: (EntryPoint >= VirtRVA) && (EntryPoint < VirtRVA+VirtSize) Это даст информацию о том, в какой именно секции располагается объект. (Важно: некоторые объекты могут располагаться вне секций, например, внутри "псевдосекции" заголовков). По параметрам этой секции вычисляем позицию объекта на диске: EntryPoint - VirtRVA + PhSize Задача 2 (обратная). Известно местоположение объекта на диске, найти его относительный (RVA) или абсолютный адрес в памяти. Решение. Просканировать таблицу объектов до тех пор, пока не будет выполнено условие: (Позиция >= PhOffset) && (Позиция < PhOffset+PhSize) Это даст информацию о том, в какой именно секции располагается объект. По параметрам этой секции вычисляем адрес объекта в памяти: Позиция - PhOffset + VirtRVA (* RVA *) Позиция - PhOffset + VirtRVA + ImBase (* абсолютный *) Задача 3. Известно VirtSize секции, рассчитать ее размер. Решение. Эта задача часто возникает при модификации содержимого секций или добавлении к программе новых секций. Необходимо посчитать, во сколько целых блоков известной длины уложится VirtSize байтов. Для этого может быть использован Си-макрос вида: #define Align(x,y) ((x)%(y)?((x)/(y)+1)*(y)Kx)) или #define Align(x,y) (((x)+(y)-1)&(~((y)-1))) (* см. /15/ *) Тогда размер вычисляется просто: Align(VirtSize, FileAlign) (* В файле *) Align(VirtSize, SectAlign) (* В памяти *) Примечание. В таблице секций могут находиться "странные" значения. Цитируем Z0mbIE /15/: : Кроме того, бывает, что виртуальная длина всех : секций = 0. Такую дрянь производит Watcom... Я брал : вместо нее физическую длину и выравнивал... 4.2. КОД PE-ИНФЕКТОРА 4.2.1. ПРОБЛЕМА ТОЧКИ ВХОДА Загрузчик Windows не проверяет, какой именно секции принадлежит точка входа и располагается ли она в области секций вообще. Поэтому возможно легкое внедрение в PE-программу собственного исполнимого кода разными способами /15/. Вот некоторые из них. Способ 1 (вирус Bizatch/Boza). В конец файла дописывается дополнительная секция с кодом вируса. К таблице секций добавляется еще одна запись с именем .vlad и флагом свойств 0C0000040h. Модифицируется PE-заголовок: - инкрементируется значение поля NOfSections; - увеличивается значение поля SizeOfHd (суммарная длина заголовка и таблицы секций); - увеличивается значение поля SizeOfIm (размер образа программы); - точка входа EntryPoint теперь указывает внутрь новой секции. Недостаток /15/: в конце файла может располагаться оверлей - данные, которые не принадлежат ни одной секции, но важны для работы программы. Дописывание кода в конец файла может исказить взаимное расположение фрагментов программы. Способ 2 (вирус Jacky). В конец файла дописывается код вируса. Считая, что код вируса принадлежит последней секции, значение ее размеров в таблице секций увеличивается (с учетом выравнивания на FileAlign и SectAlign). Модифицируются флаги свойств секции: 0F0000040h. PE-заголовок модифицируется так же, как и в Способе 1 (разумеется, SizeOfHd и NOfSections трогать не стоит). Недостаток также аналогичен недостатку Способа 1. Способ 3 (вирус CIH - Чернобыльский). Начало кода вируса записывается в позицию между концом PE-заголовков и началом первой секции. Туда же указывает EntryPoint. По мере возможности куски кода вируса записываются в пустые "хвосты" секций, а остаток - в конец последней секции (см. Способ 2). В этом случае недостаток также аналогичен недостатку Способа 1. Если остатка не имеется, то возникает интересный эффект: в таблице секций достаточно модифицировать только длины секций, а в PE-заголовке - только поле EntryPoint. Длина файла при этом не изменяется. Но за легкость инфицирования приходится платить сложностями исполнения вирусного кода: CIHу требуется собирать себя в памяти из отдельных кусков. Способ 4. Код инфектора непрерывным куском помещается в неиспользуемый "хвост" кодовой секции. Достаточно модифицировать только значение виртуальной длины этой секции в таблице секций и точку входа. Длина файла не изменяется. Недостаток: инфектор может не поместиться в пустое пространство, если оно невелико. Поэтому данный способ, рассматриваемый "в чистом виде", практически никогда не рассматривается вирусописателями (иногда предлагают сложные рецепты с "раздвиганием" секций и модификацией множества системных таблиц /15/), но... но вполне пригоден для решения нашей конкретной задачи! 4.2.2. ДОСТУП К ФИЗИЧЕСКИМ АДРЕСАМ ПАМЯТИ Для 32-битных прикладных программ Windows создает специальную "плоскую" модель памяти (типа flat). Программа свободно видит таблицу векторов прерываний по линейному 32-разрядному адресу [00000000h], области BIOS по адресу [000F0000h], самое себя по адресу ImBase, а также области ОЗУ с более "высокими" адресами. Примечание. На самом деле i80x86 при этом работает в защищенном режиме типа pagination /8/, и вся физическая память разбивается на фрагменты длиной по 1000h=4096 байт. Программе предоставляется лишь некоторое подмножество этих фрагментов, незаметно "склеенных" в непрерывную "ленту" адресов и заполненную "правильным" содержимым. Благодаря этому одна программа не видит другую, но видит копии системных областей памяти. 4.2.3. ДОСТУП К СИСТЕМНОМУ СЕРВИСУ WINDOWS Любая корректная программа Windows обращается по крайней мере к одной системной API-функции: KERNEL32.ExitProcess, и, значит, как минимум, ей обязательно требуется библиотека KERNEL32.DLL. В общем случае копия KERNEL32 располагается в разных местах памяти. Поэтому PE-программа содержит внутури себя таблицы адресов API-функций, которые заполняются конкретными значениями загрузчиком Windows-программ. Все эти таблицы доступны через одну общую таблицу импорта. Ее местоположение определяется значением поля ImRVA (смещение +80h) PE-заголовка. Размер таблицы хранится в поле ImSize (смещение +84h), но сканировать ее удобней до тех пор, пока все поля не станут нулевыми. Каждая строка таблицы описывает адреса, импортируемые из какой-нибудь одной динамической библиотеки, и имеет формат /9/: struct PEITbl { DWORD Chars; // +00 RVA имен функций DWORD TimeDate; // +04 Время/дата создания DWORD Forward; // +08 ??? DWORD NamePtr; // +0Ch RVA имени библиотеки DWORD Thunk; // +10h RVA массива RVA функций // (также адрес массива RVA // имен функций) }; Поле NamePtr представляет собой RVA имени динамической библиотеки. Поле Chars представляет собой RVA массива, в котором последовательно хранятся 4-байтовые RVA имен функций. Точнее, первые два байта перед именами заняты какими-то числами (может, это номер функции в библиотеке?), а сама строка располагается, начиная с 3-го байта. Поле Thunk представляет собой RVA массива, содержащего RVA кода этих функций внутри KERNEL32. Картина выглядит примерно так: IT[0].Chars --> АдресСтр1 -> ??GetProcAddress,0 IT[0].TimeDate АдресСтр2 -> ??ExitProcess,0 IT[0].Forward ... ... IT[0].NamePtr -----------> KERNEL32.dll, 0 IT[0].Thunk -----------------------------> АдресAPI1 АдресAPI2 ... IT[1].Chars --> АдресСтр1 -> ??GlobalLock,0 IT[1].TimeDate АдресСтр2 -> ??GlobalUnlock,0 IT[1].Forward ... ... IT[1].NamePtr -----------> SHELL32.dll, 0 IT[1].Thunk -----------------------------> АдресAPI1 АдресAPI2 ... ... Примечание: в PE-программах от Borland поле Chars может быть пустым, зато поле Thunk несет двойную нагрузку: в файле на диске оно содержит указатель на массив RVA имен функций (т. е. выполняет роль поля NamePtr), а после загрузки в память - указатель на массив RVA их адресов. Таким образом, алгоритм поиска адреса нужной функции (нам потребуется только ExitProcess) заключается в следующем. Шаг 1. Таблица импорта сканируется до тех пор, пока ее поле NamePtr не будет указывать на строку 'KERNEL32.DLL'. Шаг 2. Для этой строки выбираются значения полей Thunk и Chars, и они используются в качестве указателей на два массива адресов. Шаг 3. Оба этих массива синхронно сканируются до тех пор, пока в массиве адресов строк не встретится адрес строки 'ExitProcess'. Тогда в другом массиве адрес с этим же индексом - искомый! Примечание: в вирусах эта технология чаще всего используется для поиска адреса функции GetProcAddress /17/. Дело в том, что эта функция - справочная, и позволяет получить адрес любой другой API-функции, в том числе и той, которая не импортируется программой. Примечание: для обращения к API функциям с успехом может быть также применен "метод предопределенных адресов". Дело в том, что для Windows 95/98 (но не для NT!) адрес расположения копии KERNEL32 в памяти фиксирован и равен 0BFF70000h /17/. Таким образом, точка входа в обработчик GetProcAddress может быть найдена по сигнатуре 02B226A57h. Итак, код PE-инфектора выглядит следующим образом. ; (c) Климентьев К., Самара 2001 00 60 pushad 01 BE00000F00 mov esi,000F0000 06 2BC0 sub eax,eax 08 B900010000 mov ecx,00000100 0D FC cld 0E AC lodsb 0F 02E0 add ah,al 11 E2FB loop 0000000E 13 80FC dw 0FC80h ; cmp ah, ?? 15 ?? db ?? ; <- контрольная сумма BIOS 16 0F8409000000 je 00000025 1C 61 popad 1D 6A00 push 00 1F FF15 dw 15FF ; call [????????] 21 ???????? dd ???????? ; 25 61 popad 26 E9 db 0E9h ; jmp ???????? 27 ???????? dd ???????? ; <- смещение до OldEntryPoint 4.3. ТЕКСТ ПРОЦЕДУРЫ, ИНФИЦИРУЮЩЕЙ PE-ПРОГРАММУ Примечание. Считается, что: - ранее уже был распознан тип "заражаемой" программы; - эталонная контрольная сумма BIOS для данной машины уже подсчитана ранее и хранится в переменной BIOS_CSUM. #define Align(x,y) ((x)%(y)?((x)/(y)+1)*(y)Kx)) #define P_LEN 43 unsigned char Module_PE[P_LEN] = { 0x060, 0x0BE, 0x000, 0x000, 0x00F, 0x000, 0x02B, 0x0C0, 0x0B9, 0x000, 0x001, 0x000, 0x000, 0x0FC, 0x0AC, 0x002, 0x0E0, 0x0E2, 0x0FB, 0x080, 0x0FC, 0x000, 0x00F, 0x084, 0x009, 0x000, 0x000, 0x000, 0x061, 0x06A, 0x000, 0x0FF, 0x015, 0x000, 0x000, 0x000, 0x000, 0x061, 0x0E9, 0x000, 0x000, 0x000, 0x000 }; // (c) Климентьев К., Самара 2001 int Infect_PE(char *fn) { int f; /* Хэндл */ long po; /* Позиция строки кодового сегмента */ long pp; long p; long p1; long p2; long a1; long a2; struct WINhdr wh; /* Обобщенный заголовок Win-программы */ struct PEhdr ph; /* PE-заголовок */ struct PEObjTbl ot, /* Текущая строка таблицы секций */ cd, /* Описатель секции с точкой входа */ im; /* Описатель секции с таблицей импорта */ struct PEITbl tf; /* Текущая строка таблицы импорта */ unsigned char buf[32]; int i, j; /* Открываем файл */ f = my_open(fn,O_RDWR|O_BINARY); if (f==-1) return -1; /* Читаем обобщенный заголовок Win-программы */ my_read(f, &wh, sizeof(wh)); /* Переходим на PE-заголовок */ my_seek(f, (long) wh.winInfoOffset, SEEK_SET); /* Считываем PE-заголовок целиком */ my_read(f, &ph, sizeof(ph)); /* Инициализируем исходные данные для секции заголовков */ ot.VirtRVA=0; ot.VirtSize=ph.SectAlign; ot.PhOffset=0; ot.PhSize=ph.FileAlign; /* Переходим на таблицу секций */ my_seek(f, (long) ph.SzOfOptHdr+0x18+wh.winInfoOffset, SEEK_SET); /* Сканируем таблицу секций */ for (j=0;j<ph.NOfSections;j++) { pp = my_seek(f, 0, SEEK_CUR); my_read(f, &ot, sizeof(ot)); /* Ищем секцию, внутри которой точка входа */ if ((ph.EntryPoint >= ot.VirtRVA) && (ph.EntryPoint < (ot.VirtRVA + ot.VirtSize))) { cd=ot; // Описатель кодового сегмента po=pp; // Запоминаем файловую позицию } /* Ищем секцию, внутри которой таблица импорта */ if ((ph.ImRVA >= ot.VirtRVA) && (ph.ImRVA < (ot.VirtRVA + ot.VirtSize))) im = ot; // Описатель сегмента импорта } /* Определяем, достаточно ли пустого места в хвосте секции? */ if (Align(ot.PhSize, ph.FileAlign)-cd.VirtSize<P_LEN) { my_close(f); return -1; } // Слишком мало пустого места. /* Переходим на таблицу импорта */ p = my_seek( f, ph.ImRVA-im.VirtRVA+im.PhOffset, SEEK_SET ); /* Сканируем таблицу импорта */ tf.NamePtr=-1; while (tf.NamePtr) { my_seek( f, p, SEEK_SET ); my_read( f, &tf, sizeof(tf)); // Очередная строка my_seek( f, tf.NamePtr-im.VirtRVA+im.PhOffset, SEEK_SET ); my_read( f, buf, 32); // Читаем имя библиотеки strupr(buf); // Все буквы -> заглавные if (!strcmp( buf, "KERNEL32.DLL")) // Это KERNEL32 ? goto f1; p+=sizeof(tf); } my_close(f); return -1; // KERNEL32.DLL не найден. f1: /* Устанавливаем указатели на адреса массивов имен и адресов */ p2 = lseek(f, tf.Thunk-im.VirtRVA+im.PhOffset, SEEK_SET); if (tf.Chars) // Microsoft p1 = lseek(f, tf.Chars-im.VirtRVA+im.PhOffset, SEEK_SET); else // Borland p1 = p2; /* Синхронно сканируем оба массива */ a1=-1; while(a1) { my_seek(f, p1, SEEK_SET); my_read(f, &a1, 4); // Адрес очередного имени my_seek(f, a1-im.VirtRVA+im.PhOffset+2, SEEK_SET); my_read(f, buf, 32); // Читаем очередное имя API-функции if (!strcmp(buf,"ExitProcess")) // Это ExitProcess? goto f2; // Нашли ! p1+=4; // Синхронно p2+=4; // смещаемся по масивам } my_close(f); return -1; // ExitProcess не найден. f2: /* Рассчитываем адрес в памяти для ссылки на KERNEL32.ExitProcess */ a2 = p2 - im.PhOffset + im.VirtRVA + ph.ImBase; /* Сохраняем его внутри инфектора */ *((long *)(&Module_PE[0x21])) = a2; /* Рассчитываем адрес для JMP на старую точку входа */ a2 = ph.EntryPoint - (cd.VirtRVA+cd.VirtSize+P_LEN+1); /* Сохраняем его внутри инфектора */ *((long *)(&Module_PE[0x27])) = a2; /* Сохраняем в инфекторе контрольную сумму BIOS */ Module_PE[0x15] = BIOS_CSUM; /* Рассчитываем новую позицию точки входа в PE-заголовке */ ph.EntryPoint = cd.VirtRVA+cd.VirtSize+1; /* Переходим на позицию PE-заголовка и записываем его на старое место */ my_seek(f, (long) wh.winInfoOffset, SEEK_SET); my_write( f, &(ph), sizeof(ph) ); /* Переходим на пустой хвост внутри секции и записываем код инфектора */ my_seek(f, cd.PhOffset+cd.VirtSize+1, SEEK_SET); my_write( f, Module_PE, P_LEN ); /* Рассчитываем новую виртуальную длину секции в таблице объектов */ cd.VirtSize+=P_LEN+1; /* Переходим на "кодовую" строку в таблице объектов и обновляем ее */ my_seek(f, po, SEEK_SET); my_write( f, &(cd), sizeof(cd) ); close(f); return 0; } ЗАКЛЮЧЕНИЕ В статье описаны алгоритмы, лежащие в основе очень простой демонстрационной программы для защиты от НСК. Разумеется, ее невозможно рекомендовать в качестве серьезного средства защиты данных: обработанные ей программы могут работать некорректно; кроме того, защита очень легко нейтрализуется. В то же время в статье затронуты сложные и разнообразные вопросы системного программирования под DOS и Windows. Предполагалось дать интересующимся минимальное преставление о рассматриваемой тематике и побудить к дальнейшим исследованиям. Также статья может рассматриваться как справочный материал по теме "Поиск и обезвреживание инфицированных вирусами программ". Автор выражает благодарность: - Олександру Отенко aka Sassa за помощь при изучении механизма межсегментных ссылок в NE-программах и за информацию о ENUNS/RUSNS; - Владимиру Н. Кокареву aka CrkV за информацию о ENUNS/RUSNS. ЛИТЕРАТУРА 1. Касперский Е. Компьютерные вирусы в MSDOS - М.: "Эдель", 1992. - 174 с. 2. Касперски К. Техника и философия хакерских атак - М.: "Солон-Р", 1999. - 272 с. 3. Правиков Д.И. Ключевые дискеты - М.: "Радио и связь", 1995. - 127 с. 4. Расторгуев С.П., Дмитриевский Н.Н. Искусство защиты и "раздевания" программ. - М.: "Совмаркет", 1991. - 94 с. 5. Щербаков А. Защита от копирования - М.: "Эдель", 1992. - 80 с. 6. Защита информации в персональных ЭВМ / Спесивцев А.В., Вегенер В.А., Крутяков А.Ю. и др. - М.: "Радио и связь", 1992. - 192 с. 7. Сван Т. Форматы файлов Windows.- М.: "Бином", 1994. - 286 с. 8. Зубков С.В. Assemler для DOS, Windows и Unix. М.: ДМК, 1999. - 637 с. 9. Федоров А. DEPEND, или Какие библиотеки используют Windows-программы // Компьютер Пресс. -1995. - #8.- C. 83-86. 10. Федоров А. Windows 3.1. Доступ к памяти в первом мегабайте.- 1994.-#8.- C. 5-7. 11. Фролов А.В., Фролов Г.В. Защищенный режим процессоров Intel 80286/80386/80486. - М.: "Диалог-Мифи", 1993. - 234 с. 12. Плотников В. Использование возможностей защищенного режима работы процессора в Windows-приложениях // Монитор.- 1995.-#2.- С. 26-37. 13. Hard Wisdom. Формат исполняемых файлов Portable Executables (PE). 14. Randy Kath. Portable Executables format from top to bottom. 1995. 15. Z0mbIE. О PE-файлах и длинах секций. 2001. 16. Lord Julus. The PE-file layout. 1999. 17. Lord Julus. Accessing the Windows 95 APIs by scanning PE tables. 1998. -------------------------------------------------------------- (с) Constantin E. Сlimentieff, 2000-2001 mailto: drmad@chat.ru * http://www.chat.ru/~drmad |