Статическое детектирование файлов: Часть 1 - Структура и данные — Архив WASM.RU

Все статьи

Статическое детектирование файлов: Часть 1 - Структура и данные — Архив WASM.RU

0000. Начало

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

Я предлагаю следующую классификацию антивирусного детектирования:

  1. статическое;
  2. динамическое;
  3. бесконтактное =)

В данном тексте речь пойдет о 1-ом пункте. Кстати, одни и те же методы детекта (эти и другие) могут применяться как для 1-ого, так и для 2-ого пунктов, но в (несколько) иной форме.

Под статическим детектированием будем понимать методы, применяемые для обнаружения малвари и производимые без реального выполнения кода малвари (методы, используемые во время проверки файлов на дисках (локальных etc), трафика («на лету») и другие).

Более подробно рассмотрим, как аверы могут палить файлы путем анализа их структуры и данных, и возможные варианты обхода этих детектов.

Вся идея заключается в том, чтобы файлы были «правильными» с точки зрения антивирусников. То есть созданные/заражённые нашим кодом файлы должны быть максимально похожими по своим структуре/коду/действиям на обычные приложения (например, на calc.exe из Windows). В результате: знания/опыт/что-то еще + красивый «чистый» файл внутренне и внешне. Для достижения поставленной цели нам достаточно взять и изучить каждую частичку хотя бы одного любого файла – все другие исследуются/делаются аналогично (за пазухой держим формат PE-файлов).

Итак, будем ориентироваться на test1.exe, собранный мной на Visual C++ 6. Это обычное Win32-приложение, его внутренности содержат всё самое необходимое (без всяких bound/delay import etc). И далее рассмотрим только те поля/структуры/etc файла, которые могут вызвать какие-либо затруднения/сомнения при их заполнении. Остальные поля обычно имеют те же значения, что и соответствующие поля в test1.exe.

Все файлы/сорцы, приложенные к данному тексту, тестировались на x86: Win XP (SP2/SP3), 7, Vista; и на x64: Win 7.

0001. Структура

Начнем с просмотра общего вида PE-файла. Вот, собственно, он самый:

Таблица 0001.0000 – общий вид PE-файла

  • IMAGE_DOS_HEADER (0x5A4D==“MZ”)
  • DOS Stub + Rich Signature
  • Signature (0x4550 == “PE\0\0”)
  • IMAGE_FILE_HEADER
  • IMAGE_OPTIONAL_HEADER (with IMAGE_DATA_DIRECTORY)
  • IMAGE_SECTION_HEADER (.text)
  • IMAGE_SECTION_HEADER (.rdata)
  • IMAGE_SECTION_HEADER (.data)
  • IMAGE_SECTION_HEADER (.rsrc)
  • Section .text
  • Section .rdata
  • Section .data
  • Section .rsrc
  • …other info…

Вперёд за подробностями =)

0010. IMAGE_DOS_HEADER

Располагается в начале файла.

DWORD e_lfanew; - Смещение структуры IMAGE_NT_HEADERS в байтах от начала файла.

Для этого поля используем наиболее часто встречаемые значения в диапазоне [0xC0..0xF8]. Должно быть кратно 8.

0011. Rich Signature

Допустим, у нас есть троян, который палится антивирусом (назовем его pizdec). В ходе различных экспериментов выясняем, что детект привязан к нескольким местам в файле: какая-то херня в коде, импорте и…что-то не в поряде с рич-сигнатурой (р-с). Причем если убрать детект по последнему признаку, файл становится чистым. После очередных опытов с р-с понимаем, что pizdec ее чекает каким-то хитрым образом, и фишка с заменой одних байтиков другими (рэндомными) не прокатит. Постоянно копировать р-с других файлов тоже не вариант. Но, как говорится, на каждую хитрую жопу найдётся хуй с винтом, и поэтому сгенерим р-с так, как она выглядит в файле после сборки в VC++. Для этого нам поможет это, а также читаем тут и тут (алгоритмы по работе с р-с из данной ссылки не изменяли и не проверяли некоторые значения, поэтому данный код мной был переписан и модернизирован).

Р-с содержит информацию о версиях компилятора/линкера и, возможно, о каких-то флагах/опциях компиляции/линковки. И не хранит идентификаторы железа и т.п., поэтому если прога будет собрана на разных машинах с одинаковым софтом (одинаковые версии, сервис-паки, длл и т.д.), то р-с этих файлов совпадут.

Графически р-с выглядит так:

Рисунок 0011.0000 - изучение р-с (test1.exe) через hex-редактор hiew

Рисунок 0011.0011 - расшифрованная р-с

Вначале идут байты (смещение 0x80) 0x12 0x6A 0xA4 0x7F. Это строка ‘DanS’, поксоренная на некоторое число (назовем его xmask).

Далее подряд следуют 3 xmask (0x56 0x0B 0xCA 0x2C). Маска представляет собой 32-битное число, вычисленное некоторым алгоритмом.

Затем идут данные (назовем их xdata), поксоренные на xmask (в диапазоне смещений [0x90..0xC7]). Для каждой р-с может быть свой набор данных и их количество.

После данных располагаются строка “Rich”, xmask и нули (в данном примере их 8, но может быть как больше, так и меньше. Кол-во нулей кратно восьми).

В некоторых случаях отсутствие р-с может улучшить положение по отношению к детектам…возможно, до следующего апдейта ав. Но зачем нам это, если есть рабочие алгосы по генерации «правильной» р-с =). Тем более что мелкомягкие всегда ее создают.

Алгоритмы проверки р-с на целостность, подмены ее данных, а также ее генерации с нуля находятся тут: frs_asm.rar и frs_C.rar.

0100. IMAGE_FILE_HEADER

Содержит самую общую информацию о файле и находится сразу после сигнатуры 0x4550 (“PE\0\0”).

WORD NumberOfSections; - Количество секций в файле.

Если строится exe-шка, то лучше создать 4 секции (.text, .rdata, .data, .rsrc). Остальные добавлять только при необходимости (например, .tls, .reloc). Для dll-ки важно включить и релоки (.reloc).

Если же мы заражаем файл, то также, действуем аккуратно. Например, жертва (обычный файл, без оверлея, никак себя не проверяет и т.п.) имеет 3 секции (.text, .rdata, .data). Можно создать +1 секцию ресурсов, засунуть в нее какую-нить иконку (не забываем тогда и про группу иконок), создать еще ресурс, в котором сохраним тело своего зверя. Ну а дальше, продумать и сделать передачу управления на наш весёлый код и скорректировать поля в файле. Таким образом, мы заинфектили файл и при этом создали вполне нормальную секцию.

Кстати, был случай, когда чистый файл палился каким-то забавным ав, а после инфекта данного файла детект исчезал. VX спасёт мир )

DWORD TimeDateStamp; - Время, когда был собран PE-файл. В этом поле хранится количество секунд, прошедших с 1 января 1970 года до времени создания файла.

Можно использовать функцию gmtime из стандартной библиотеки С (time.h), переводящую время из секунд в удобочитаемый вид:

DWORD TimeDateStamp = 0x4C9FABF5;
	struct tm *xtm = gmtime((const long*)&TimeDateStamp);
	printf("day = %d\nmonth = %d\nyear = %d\n", xtm->tm_mday, 
xtm->tm_mon, (xtm->tm_year + 1900));

В принципе, здесь можно сгенерировать любое число, например, в диапазоне [0x40000000..0x4C000000] (2004-2010 гг.).

WORD Characteristics; - Атрибуты файла.

В основном характеристики строятся из этих флагов (побитовое «ИЛИ»):

//Relocation info stripped from file.
#define IMAGE_FILE_RELOCS_STRIPPED				0x0001 

//File is executable(i.e. no unresolved externel references).
#define IMAGE_FILE_EXECUTABLE_IMAGE				0x0002  

//Line nunbers stripped from file.
#define IMAGE_FILE_LINE_NUMS_STRIPPED			0x0004  

//Local symbols stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED			0x0008  

//32 bit word machine.
#define IMAGE_FILE_32BIT_MACHINE				0x0100  

//File is a DLL.
#define IMAGE_FILE_DLL						0x2000

Для exe можно смело использовать значение 0x10F (для dll – 0x210E).

0101. IMAGE_OPTIONAL_HEADER

Эта структура является обязательной и содержит важные дополнения к общей информации в IMAGE_FILE_HEADER.

BYTE MajorLinkerVersion/MinorLinkerVersion; - Содержат версию линковщика, создавшего данный файл. Числа должны быть в десятичном виде, например, 2.56, 6.0 и т.д. Значение этого поля может быть любым, но, чтобы не провоцировать pizdec, предлагаю использовать значение 6.0.

DWORD SizeOfCode/SizeOfInitializedData/SizeOfUninitializedData; - Суммарный размер всех секций кода/с инициализированными данными/с неинициализированными данными. Значения этих полей вычисляются следующим образом:

//формула выравнивания (см. ниже)
#define ALIGN_UP ((x + (y - 1)) & (~(y - 1))) 
 
//(code_sec.Characteristics & IMAGE_SCN_CNT_CODE) == 1
SizeOfCode += ALIGN_UP(code_sec.Misc.VirtualSize, FileAlignment) 

//(init_sec.Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) == 1
SizeOfInitializedData += ALIGN_UP(init_sec.Misc.VirtualSize, FileAlignment) 

//(uninit_sec.Characteristics & // IMAGE_SCN_CNT_UNINITIALIZED_ DATA) == 1
SizeOfUninitializedData += ALIGN_UP(uninit_sec.Misc.VirtualSize, FileAlignment)

Создаем мы файл с нуля или же инфектим чужой файл, значения данных полей (да и остальных тоже) всегда должны быть правильно подсчитанными, иначе однажды по одному такому признаку pizdec сообщит: “Ёба, да тут вирус пляшет!!!”, - и вся эстетика сравняется с дерьмом.

DWORD AddressOfEntryPoint; - RVA точки входа, отсчитываемый от начала ImageBase. Во избежание приступов бешенства у pizdec лучше всего располагать точку входа (EP) в диапазоне:

	//EP != 0
(EP >= code_sec.VirtualAddress && 
 EP < (code_sec.VirtualAddress + code_sec.Misc.VirtualSize))

Если code_sec.Misc.VirtualSize == 0, тогда берем code_sec.SizeOfRawData.

DWORD BaseOfCode/BaseOfData; - RVA секции кода/данных.

	
	BaseOfCode = code_sec.VirtualAddress; //0x1000; //SectionAlignment; //!!!!! 

	// = RVA, откуда начинаются секции данных
	BaseOfData = data_sec.VirtualAddress;

DWORD ImageBase; - Базовый адрес загрузки файла в памяти. Компоновщик по дэфолту устанавливает базовый адрес 0x00400000. Вот это значение и прописываем при генерации exe-файлов (для длл берём 0x10000000). Если будет что-то отличное от данного значения, появляется большой шанс получить клеймо при следующем апдейте баз pizdec’а.

DWORD SectionAlignment/FileAlignment; - Кратность выравнивания секций в памяти/на диске. Формула выравнивания:

//для адресов секций (физических и виртуальных)
#define ALIGN_DOWN(x, y)	(x & (~(y - 1)))	

//для размеров секций
#define ALIGN_UP(x, y)		((x + (y - 1)) & (~(y - 1)))

, где x – выравниваемое значение, y– выравнивающий фактор.

Обычно SectionAlignment = 0x1000, a FileAlignment = 0x200 или 0x1000.

DWORD SizeOfImage; - Общий размер загруженного приложения в памяти, начиная от ImageBase до конца последней секции, и выровненный на величину SectionAlignment. Вычисляется по формуле:

SizeOfImage = last_sec.VirtualAddress + ALIGN_UP(last_sec.Misc.VirtualSize, IMAGE_OPTIONAL_HEADER.SectionAlignment);

DWORD SizeOfHeaders; - Суммарный размер всех заголовков, выровненный в файле на FileAlignment (в памяти на SectionAlignment). Часто равен 0x400.

DWORD CheckSum; - Контрольная сумма образа файла.

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

Чтобы получить контрольную сумму данного исполняемого файла, надо вызвать функцию CheckSumMappedFile с соответствующими параметрами. Эта функция доступна из библиотеки Imagehlp.dll.

Если мы создаем свой файл (обычный) или инфектим чужой, в котором CheckSum = 0, тогда значение этого поля оставляем равным нулю. В остальном всё по ситуации =)

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; - Массив структур типа IMAGE_DATA_DIRECTORY, количество задается полем NumberOfRvaAndSizes (обычно = 0x10). По умолчанию VC++ (6) создает в «обычном» приложении 3 директории: импорта, ресурсов, IAT (по крайней мере, для Win32 Application). Поэтому именно эти 3 директории также должны быть в нашем создаваемом файле (+ tls, релоки при необходимости).

0110. IMAGE_SECTION_HEADER

Массив структур типа IMAGE_SECTION_HEADER, количество задается полем NumberOfSection. BYTE Name[8]; - 8-байтовое ASCII-имя секции. Лучше юзать стандартные имена: .text, .rdata, .data, .rsrc, .tls, .reloc. Данную информацию используем как при создании своих, так и при инфекте чужих файлов (если выбран метод добавления новой секции, имя секции выбираем с умом).

DWORD VirtualSize; - Виртуальный размер секции, при загрузке файла в память выравнивается вверх на SectionAlignment. Причем в файле VirtualSize < SizeOfRawData для всех секций (названия которых уже не раз были написаны). Исключение для секции .data (тут наоборот).

DWORD VirtualAddress/ PointerToRawData; - Содержат RVA адрес начала секции в памяти и смещение секции относительно начала файла. Обычно first_sec.VirtualAddress = 0x1000, first_sec.PointerToRawData = 0x400 или 0x1000. Значения адресов секций выровнены вниз на SectionAlignment/FileAlignment.

DWORD SizeOfRawData; - Физический размер секции, выровненный вверх на FileAlignment.

DWORD Characteristics; - Атрибуты секции. Часто атрибуты строятся из следующих флагов (побитовое «ИЛИ»):

// Секция содержит код
#define IMAGE_SCN_CNT_CODE					0x00000020  

//Секция содержит инициализированные данные
#define IMAGE_SCN_CNT_INITIALIZED_DATA			0x00000040

// Секция содержит неинициализированные данные.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA			0x00000080  

// Эта секция отбрасывается, когда программа уже загружена. 
#define IMAGE_SCN_MEM_DISCARDABLE				0x02000000  

// Секция является исполняемой.
#define IMAGE_SCN_MEM_EXECUTE					0x20000000 
 
// Данные секции можно читать.
#define IMAGE_SCN_MEM_READ					0x40000000

// В секцию можно записывать данные.  
#define IMAGE_SCN_MEM_WRITE					0x80000000

Для «обычных» файлов дело обстоит так:

.text.Characteristics	=	0x60000020;
.rdata.Characteristics	=	0x40000040;
.data.Characteristics	=	0xC0000040;
.rsrc.Characteristics	=	0x40000040;

В качестве примера я решил прочекать test1.exe на virustotal.com до и после изменения атрибутов секции кода, и вот что получилось:

.text.Characteristics = 0x60000020;
.text.Characteristics = 0xE0000020;

Так что хышники, готовые сцапать «аномалию», всегда найдутся =)

0111. Импорт

Импорт – это механизм, позволяющий использовать функции и/или переменные из модулей, отличных от данного.

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

Таблица импорта начинается с массива структур типа IMAGE_IMPORT_DESCRIPTOR.

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

DWORD OriginalFirstThunk; - RVA массива двойных слов. Каждый элемент этого массива является объединением IMAGE_THUNK_DATA32 и соответствует одной функции, импортируемой PE-файлом.

DWORD TimeDateStamp; - Временная отметка, когда был создан данный файл. Так как юзаем стандартный механизм импорта, то пишем сюда 0.

DWORD ForwarderChain; - Данное поле связано с форвардингом функций. Пишем 0.

DWORD Name; - ASCII-имя импортируемой dll.

DWORD FirstThunk; - RVA массива двойных слов IMAGE_THUNK_DATA32. FirstThunk является таблицей адресов импорта (Import Address Table (IAT)).

IMAGE_THUNK_DATA32 - Структура представляет собой dword, который соответствует одной импортируемой функции.

DWORD AddressOfData; - Если функция импортируется по имени, то данное поле содержит RVA на структуру IMAGE_IMPORT_BY_NAME, в которой находится имя нужной функции. Если происходит импорт по номеру (ординалу), старший бит двойного слова устанавливается в 1.

IMAGE_IMPORT_BY_NAME - Содержит информацию о функции импорта.

WORD Hint; - Этот word создан для использования системным загрузчиком, чтобы лоадер мог быстро найти функцию в таблице экспорта. И содержит индекс функции в массиве экспортируемых функций, равный ординалу. Естественно, одна и та же функция имеет разные ординалы в разных версиях винды, так что универсального hint’a под все системы нет.

Предлагаю вычислять хинт так:

  1. получаем случайное число и сохраняем его в переменной cor_hint;
  2. находим нужную функцию в таблице экспорта dll и сохраняем ее ординал в переменной hint;
  3. прибавляем к hint значение cor_hint;

//это значение будет вычислено один раз и прибавляться к каждому новому хинту;
WORD cor_hint = rand()%0x100;

//для очередного хинта находим свой ординал и прибавляем cor_hint;
WORD hint = *AddrOfNameOrd + cor_hint;

Таким образом, хинт у нас будет рэндомный, но вычисленный на основе соответствующего ординала.

BYTE Name[?]; - ASCII-имя импортируемой функции.

Теперь поговорим о порядке построения импорта и прочих деталях.

Значит, внимательно рассмотрев нашу ехешку (и подобные файлы), видим следующее:

  1. весь импорт находится в секции .rdata (импорт лежит по адресу, кратному 4);
  2. в самом начале секции .rdata находится IAT (после которого могут лежать различные данные…а могут и не лежать);
  3. далее следует таблица импорта;
  4. затем идут массивы dword’ов IMAGE_THUNK_DATA32. RVA этих массивов сохранены в полях OriginalFirstThunk (lookup-таблица);
  5. после располагаются массивы структур IMAGE_IMPORT_BY_NAME и имена dll, причем порядок таков: сначала идет массив структур IMAGE_IMPORT_BY_NAME, а после имя длл – она импортирует функции, имена которых находятся в данном массиве. Затем снова идет массив структур и имя длл и т.д. И количество таких пар, естественно, равно количеству импортируемых данным файлом библиотек.

Библиотеки добавляются в импорт в порядке подключения их в проекте. Поэтому (пока что) никаких правил по порядку нет, и в импорте первой может идти как KERNEL32.dll, так и абсолютно другая длл (некоторые линкеры (Borland) вообще могут захуярить одну и ту же длл несколько раз – но там свои карусели).

WinApiшки можно размещать также в любом порядке (сортировка нужна только для экспорта).

Что касается парных апи – тут тоже нет однозначного ответа. Из всех тестов, проведенных мной, не было выявлено ни одного случая детекта по данному признаку (по крайней мере, он был не ключевым). Хотя, возможно, для каких-то функций и при определенных обстоятельствах палят. Например, в test1.exe есть LoadLibraryA, но нет FreeLibrary; имеется WriteFile, но отсутствует CreateFile (хотя нахрен эта апишка нужна, так как с помощью WriteFile можно писать не только в файл, но и в консоль =)).

Теперь о строках.

Имена длл часто такие: KERNEL32.dll, USER32.dll, GDI32.dll etc (имя пишем большими, а “dll” маленькими бук0вками).

Также замечу, что имена dll и функций (возможно и другие строки в .rdata), походу выравниваются на четное количество байтов:

Рисунок 0111.0000 – выравнивание имен длл и функций на четное количество байт

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

При создании фэйкового импорта можно юзать (только) часто встречаемые длл и функи. Тут большой плюс - отсутствие «аномальных» строк, и даже если файл палится в импорте, то, скорее всего это не главный признак, поэтому импорт долгое время может оставаться «одним и тем же» (при необходимости фильтруем его). Конечно, есть и другой путь – использовать «редкие» dll/functions. Минус на рыло – детект именно по ним. Но это и плюс: возможность быстрой чистки (для этого достаточно заменить палевную функцию на «чистую»).

У аверов существует «чёрный список» api/dll, которые, по их мнению, используются в основном в малвари. А также, возможно, на подходе и «белый список», более суровый =). Чего нет в нём – то пиздец. Следует такую нездоровую ситуацию учитывать.

Для сбора статистики апи/библиотек специально была написана тулза stait_C.rar, которая может помочь нам в этом забавном деле.

1000. Ресурсы

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

Таблица ресурсов соответствует секции ресурсов (.rsrc). Для нас важно знать, что ресурсы – это двоичное отсортированное дерево, и в венде используются только 3 первых уровня (Type, Name, Language). Запомни! Так как эта инфа в дальнейшем облегчит кодинг с ресурсами.

Обычно хватает RT_ICON, RT_GROUP_ICON и еще какой-нить хрени (строки, диалоги, картинки etc). Причем в такой хрени можно хранить важные пошифрованные данные (код). Но все, как говорится, по ситуации. RT_RCDATA – с этим товарищем следует быть аккуратней, в нем хранить только хуиту, не больше (pizdec по-особому чекает содержимое данного ресурса).

Очень хорошо, что существуют winapi, практически делающие все сами по созданию/добавлению/etc ресурсов в нужный файл. Нам остается только создать и сохранить пустую секцию/директорию ресурсов (может еще чуть скорректировать ресурсы и другие поля в PE-файле). Остальную работу выполнят BeginUpdateResource, UpdateResource, EndUpdateResource, EnumResourceNames, EnumResourceTypes и т.д. (кстати, с помощью этих фунок можно и инфектить файлы вместо использования CreateFile/CreateFileMapping/MapViewOfFile/UnmapViewOfFile).

1001. Секции

Секция - непрерывная область памяти с одинаковыми атрибутами. В этом разделе коротко рассмотрены только самые важные из них (tls & relocs может в другой раз).

.text - В эту секцию помещаем весь программный код (о нём поговорим отдельно).

.rdata - Здесь храним импорт, ну а после него дополняем секцию нулями для выравнивания (ALIGN_UP(SizeOfRawData, FileAlignment)).

.data - В данной секции размещаем различные данные: рэндомные строки, числа и т.п.

.rsrc - Тут лежат (только) ресурсы проги (смотри раздел «Ресурсы»).

Значит, секции в файле и в памяти должны идти именно в том порядке, в котором они здесь перечислены. Иначе «аномалия». Еще важно следить за размерами секций и их соотношением.

Хотя собранная статистика по файлам и показала, что каких-либо правил здесь нет (встречались файлы с абсолютно противоположными пропорциями секций (в зависимости от размера файла)), можно сделать так:

  1. если файл создаётся с нуля, то берём за эталон размеры секций и их процентные соотношения из других «чистых» файлов нужных размеров (например, если мы собираемся генерить файлы размером = 80kb, то сначала находим какой-нибудь файл, размер которого также = 80kb, и собранный на VC++ (6). И затем используем размеры его секций и их пропорции при составлении своих).
  2. если же мы заражаем файл, то стараемся придерживаться пропорций его секций. То есть, например, если у файла было 15% / 12% / 70% кода/данных/ресурсов до инфекта, то после инфекта желательно сохранить такое соотношение (но не обязательно, если убрать другие флаги детекта);
  3. что касается хранения шифрованного кода для пунктов 1 и 2, то пихаем его полностью в одну из секций (или оверлей), либо, как вариант, разбиваем код на куски и раскидываем их по секциям с сохранением пропорций (для этого нужно знать размер нашего кода и соотношение размеров секций файла). Энтропия полученного файла (его секций) должна быть нормальной (об этом ниже);
  4. всё тщательно тестим;
  5. часто встречаются файлы, у которых (.text_sec.SizeOfRawData > .rdata_sec.SizeOfRawData) && (.text_sec.SizeOfRawData > .data_sec.SizeOfRawData).

1010. Энтропия

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

Допустим, проверяется на зверьё некий файл. Эвристик ав определит, запакован/зашифрован ли файл. Если результирующий процент/коэффициент меньше заданного предела, то анализ закончен. Иначе определит, чем пожато/пошифровано. Если известно чем – вывод имени пакера/криптора. Если нет, напишет, что это какой-нить XCryptVirus. Вот и эвристика на простые вирусы. И такого рода проверки могут быть для чего угодно…не успели толком обрадоваться свеженаписанному зверьку, а на нём уже клеймо. Чтобы такую хуергу зарубить на корню, нам по-любому надо быть как минимум на шаг впереди.

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

  1. первая инструкция в точке входа pushad;
  2. нестандартные имена секций;
  3. атрибуты секции кода на запись (возможно какой-либо другой секции, изначально не предназначенной для записи);
  4. наличие оверлея;
  5. высокая энтропия (файла/отдельных его частей);
  6. среднее арифметическое;
  7. сигнатуры;
  8. статистический анализ байтов/команд;
  9. другие.

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

При правильной структуре файла, большинство пунктов сразу отсеется.

Здесь остановимся на энтропии.

Итак, кратко, информационная энтропия - мера хаотичности информации. Впервые понятия «энтропия» и «информация» связал К.Шеннон в 1948 году. Шеннон использовал бит как единицу измерения информации. Мерой количества информации Шеннон предложил считать функцию, названную им энтропией:

H(i) = -P(i) * log2(P(i));
H = H(1) + H(2) + H(i)…

H - энтропия Шеннона, P(i) - вероятность появления i-ого символа.

Подробнее об энтропии здесь и здесь.

Единица измерения информации и энтропии зависит от основания логарифма. Мы же будем измерять в битах на 1 байт с точностью до десятых.

Опытным путём установлено, что «нормальное» значение энтропии для PE-файлов (exe) лежит в диапазоне [5.5;6.8]. Если получается больше, скорее всего файл упакован.

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

На самом деле проги используют одну и ту же Шенноновскую формулу, но, как было уже написано, могут использоваться различные единицы измерения, результат можно выводить в процентах или коэффициентом. При подсчете можно откидывать выравнивающие нули, самый частый и самый редкий байты (для усреднения значения), привязать к размеру файла, считать энтропию всего файла и/или отдельных его частей (заголовков, секций etc). Если считать по секциям, то можно брать ее минимальный размер (из физического и виртуального размеров) и т.д.

Для того, чтобы нас не поймали по энтропии, очевидно, мы должны ее улучшить. Тут тоже предлагаю несколько способов, как это сделать:

  1. Перестановочные шифры. От перестановки мест слагаемых сумма не меняется =). Однако, если данные перед этим уже были сжаты, данный шифр теряет свой смысл.
  2. Коды Хаффмана.
  3. Разбавление кода «однородным мусором». Например, по некоторому алгоритму вставляем в разные места кода цепочки 0x00, 0xFF или другие байты (можно выбрать любой, либо «неслучайно»: например, вставлять тот, который имеет самую большую вероятность появления в данном коде/файле или провести анализ всех файлов нужного типа в ОС, найти «самый популярный» байт и юзать его) до тех пор, пока энтропия не «нормализуется». Этот же алгос (или другой) должен уметь и очищать от мусора наш код.
  4. Base64. Вкратце, это схема кодирования произвольного набора байт в последовательность печатных ASCII символов. В общем случае длина получаемого кода увеличивается примерно на 30%. Основное правило кодирования простое – мы умышленно сокращаем диапазон символов (байтов), которые (по)имеем на выходе. А чем меньше символов мы используем, тем меньше неопределенность их появления, а значит меньше энтропия. Как-то так =) . Для кодирования используется 64 символа (2 в 6-ой степени). В общем, читаем тут. Таким образом можно шифровать некий код и пихать его в ресурсы на «своё» место =) (RT_STRING). Base64 – это своего рода подстановочный шифр. Ничто не мешает юзать шифр (где размер символа = 6 бит, тогда энтропия не превысит 6.0 по определению) и со своим собственным алфавитом. Кстати, как вариант: берём 3 байта, у каждого из них забираем по 2 бита. И записываем после этих 3 байт новый байт, состоящий из отрезанных бит, и т.д. Получается, что у каждого байта будут «рабочими» только 6 бит. Количество различных байт сократится с 256 до 64 вариантов. И энтропия, соответственно, снизится (<= 6.0) (+ накидать несколько различных байт – следить за статистикой байтов).
  5. Разбавить код так, чтобы на выходе получить «правильный» код с низкой энтропией и «правильными частотами». Вот и вот.

В качестве примера написана прога xentr_C.rar, вычисляющая энтропию файлов (+ пара фичезов).

1011. Репутация файла

В последнее время у многих ав стало модным продвигать так называемые облачные технологии. И всё в принципе хорошо, только благодаря этим чудо-технологиям кто-то стал обезбашенным параноиком, кто-то потерял кучу бабосов, к кому-то пришел suspicious.insight. Но есть и большие плюсы…аверы знают =).

Ну вот, вступление уже есть… Далее мы рассмотрим конкретного антивирусника – symantec и его братию.

Как пишут сами симантюковцы, ими была создана «революционная» технология (кодовое название Quorum), обеспечивающая заshitу данных на основе оценки репутации. Чуть больше об этом тут. Покопавшись в различной инфе и поэкспериментировав с nis 2011, дополню описание данной технологии:

  1. norton insight – технология, обеспечивающая защиту без снижения производительности за счет исключения из проверки доверенных файлов. Это позволяет сократить время сканирования.
  2. После запуска данной технологии, вначале идет соединение с сервером Shasta-rrs.symantec.com. Это, так называемая, их облачная платформа (она самая).
  3. По защищенному соединению (протокол TLS), передаются/принимаются/проверяются различные данные. Значит, по данным: передается/принимается ~300 байт. И если верить инфе с сайта разраба – никакая личная инфа, а также сам проверяемый файл не отсылаются на их сервера (файл не сканируется). Передается только статистическая инфа о файле: sha256 хэш от файла, (возможно) его имя, размер, версия (ресурсы), цифровая подпись (может определенные ее данные).
  4. Полученный хэш с помощью специальных алгоритмов сверяется с базой данных (хранится на серверах симантека), содержащей инфу об известных доверенных файлах. Полученный результат (репутация/степень доверия) + хэш файла вероятно сохраняются в зашифрованном виде в БД на компе юзера.
  5. Если файл был изменён, полученное ранее доверие будет недействительным, и файл будет проверяться снова.
  6. а) следует отметить, что, возможно, в БД симантюка хранится и инфа о запущенных процессах/модулях/драйверах, различные следы файлов, данные о самой ОС и юзаемой файловой системе; б) наличие цифровой подписи у файла еще не означает, что файлу можно доверять. В исключении разве что мелкомягкие =).

Кстати, был найден довольно простой способ обхода знакомого многим suspicious.insight’a (о нём) (однако, против нортона не прокатит - ищем другое), который заключается в следующем:

  1. берем любой файл с подписью, например, от ms;
  2. выдираем из него цифровую подпись (все данные из директории IMAGE_DIRECTORY_ENTRY_SECURITY) и сохраняем ее на диске;
  3. файлу, который хотим защитить от суспикус-инсайта, цепляем сохраненную цифровую подпись (а) сохраняем подпись как оверлей в самом конце файла; б) прописываем адрес и размер подписи в полях VirtualAddress & Size директории IMAGE_DIRECTORY_ENTRY_SECURITY; в) пересчитываем поле IMAGE_OPTIONAL_HEADER.CheckSum).

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

//сначала перечекаем test1.exe – он без фэйковой цифровой подписи 
//после перепроверки видно, что спустя некоторое время его уже палят =)
//но сейчас о другом: нажимаем на кнопку «Show all» и видим, что 
//висит Suspicious.Insight
before_ds	//test1.exe	

//затем к test1.exe была добавлена подпись от excel.exe (XP SP3) 
//и проверим полученный файл  
//как видно, исчезли даже и другие детекты
after_ds	//si_sux.exe

Сорцы прилагаются: fakeds_C.rar.

1100. Доброключение

Следует помнить: то, что помогло в чистке одного файла, может быть непригодно в чистке другого, так как для разных файлов возможны разные детекты. Поэтому, как говорится, нормально делай – нормально будет =). И знай, что сила в простоте. Тут всё.

Ссылки на доки

  1. Antivirus Engines
  2. PE format
  3. Microsoft's Rich Signature (undocumented)
  4. About Rich Signature
  5. Fake Rich Signature
  6. Information entropy (eng)
  7. Base64 (eng)
  8. Calculation entropy (+ comments)
  9. About entropy
  10. Crypting simple instructions
  11. Symantec about technology Quorum
  12. Symantec Cloud Platform Shasta
  13. Symantec Suspicious.Insight

Исходники

  1. test1.exe & si_sux.exe
  2. frs_asm.rar & frs_C.rar
  3. stait_C.rar
  4. xentr_C.rar
  5. fakeds_C.rar

Спасибо

izee, который помогал в подготовке данного текста и в других тонких моментах. А также приветы izee, Dark Prophet, fAMINE и всем другим вирмэйкерам.

2002-2013 (c) wasm.ru