ZF

                    ЗАРАЖЕНИЕ ВО БЛАГО
       (Опыт полезного применения вирусных технологий)

            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


(C) NF, 1998-2004