Пишем анализатор PE файлов на Delphi

© 2006 Константин Панков & Hellsp@wn

Предисловие.

Уже достаточно продолжительное время для защиты коммерческого программного обеспечения производители используют, так называемые, упаковщики и протекторы PE файлов. С течением времени среди взломщиков этих продуктов стали пользоваться популярностью анализаторы PE файлов, предоставляющие полную информацию о файле - становится известным даже название и версия упаковщика, протектора, компилятора. Но цель этой статьи не только показать, как создаются подобные программы. Цели данной статьи:
  1) написать анализатор PE файлов, а вместе с тем и детектор упаковщиков\протекторов\компиляторов;
  2) показать, как, возможно, работали антивирусные программы до появления упаковщиков и протекторов;

Читателю необходимо предварительно ознакомиться хотя бы с одной из следующих утилит: PEiD, PE Tools (PE Sniffer), PEPirate или DiE, ссылки, на которые приведены в конце статьи, почитать про PE формат, а также скачать исходники первой версии PEPirate, и разбирать написанный в них код параллельно с чтением статьи.

Подготовка.

При написании программы мы будем для удобства пользоваться API функциями. На тот случай, если читатель не знаком с функциями CreateFile, CreateFileMapping, MapViewOfFile, SetFilePointer, ReadFile ему следует просмотреть их описание, приведенное ниже, в противном случае, перейти к следующей части статьи. Описание используемых функций:

Функция CreateFile открывает файл и возвращает его дескриптор, который может применяться для доступа к файлу.

CreateFile(
   FileName:PChar, // имя открываемого файла
   dwAccess:DWORD, // тип доступа к файлу
   dwShareMode:DWORD, // определяет способ совместного доступа к файлу
   SecAttributes:TSecurityAttributes, // указатель на структуру TSecurityAttributes, если хотим по умолчанию, то nil
   dwCreate:DWORD, // определяет действие, которое нужно выполнить если файл существует. Нам нужно открыть файл, значит
   //OPEN_EXISTING
   dwAttrsAndFlags:DWORD, // указывает аттрибуты и флаги для файла
   hTemplateFile:HANDLE):DWORD; // дескриптор шаблона файла

Функция CreateFileMapping создает именованный или неименованный объект отображения файла

CreateFileMapping(
   hFile:DWORD, // дескриптор файла, полученный с помощью CreateFile
   SecAttr:TSecurityAttributes // 0, если по умолчанию
   dwProtect:DWORD, // защита страницы файла для отображения
   dwMaximumSizeHigh:DWORD,// три следующих параметра 0
   dwMaximumSizeLow:DWORD,
   MapName:PChar
   ):DWORD;

Функция MapViewOfFile отображает представление файла в адресное пространство вызывающего процесса и возвращает начальный адрес представления.

MapViewOfFile(
   hMapObject:DWORD, // дескриптор полученный при помощи CreateFileMapping
   dwAccess:DWORD, // тип доступа к представлению файла
   dwOffsetHigh,dwOffsetLow,dwMap:DWORD // здесь ставим нули
   ):Pointer;

Функция SetFilePointer перемещает указатель открытого файла на указанную позицию.

SetFilePointer(
   hFile:DWORD, // хендл открытого файла
   lDistanceToMove:Integer, // число байт для перемещения
   lpDistanceToMoveHigh:Pointer, // это поле для нас не важно
   dwMoveMethod:DWORD // метод перемещения (от начала, от текущей позиции и т.д.)
   ):DWORD;
Функция ReadFile считывает фанные из файла.

ReadFile(
   hFile:DWORD, // дескриптор считываемого файла
   pBuf:pointer, // указатель на буфер куда будет помещена считанная информация
   dwBytes:DWORD, // число байт, которое надо считать из файла
   dwBytesRead:DWORD, // число реально считанных байт
   pOverlap:POVERLAPPED // нас не интересует
   ):boolean;
Функции UnMapViewOfFile и CloseHandle служат для закрытия объектов открытых при помощи CreateFile, CreateFileMapping и MapViewOfFile.

Программируем.

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

hFile:=CreateFile(pchar(filename),GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ or FILE_SHARE_WRITE,nil,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,0);
hFileMapping:=CreateFileMapping(hFile, nil, PAGE_READWRITE, 0, 0, 0);
p:=MapViewOfFile(hFileMapping,FILE_MAP_READ,0,0,0);
Следует отметить, что приведенная последовательность действий не единственная, например того же можно добиться, вызвав функцию LoadLibraryEx, т.е.
p:=LoadLibraryEx(pchar(filename),0,DONT_RESOLVE_DLL_REFERENCES);
Подразумевается, что filename типа string. Итак, что нам дает это "p", а это ни что иное как указатель на структуру IMAGE_DOS_HEADER DOS-загаловок. Полностью описывать я её не буду, а расскажу лишь о двух полях e_magic и _lfanew(так называется в Delphi, а на самом же деле это e_lfanew), т.к. они будут нужны нам в дальнейшем. e_magic сигнатура исполняемого файла должна быть равна IMAGE_DOS_SIGNATURE, если нет, то это не PE файл. _lfanew - смещение на PE-заголовок, т.е. на структуру IMAGE_NT_HEADERS. Смотрим далее:
a:=p;// сохраняем p, чтобы позже закрыть отображение
doshead:=PIMAGE_DOS_HEADER(p)^;//подразумеваем, что doshead.e_magic=IMAGE_DOS_SIGNATURE
p:=pointer(integer(p)+doshead._lfanew);//смещаемся на структуру IMAGE_NT_HEADERS
pehead:=PIMAGE_NT_HEADERS(p)^;//PEHead ни что иное как IMAGE_NT_HEADERS
и не забудьте, что Pтип = ^тип. Рассмотрим кратко структуру IMAGE_NT_HEADERS.
IMAGE_NT_HEADERS = record
   Signature:DWORD;
   FileHeader:IMAGE_FILE_HEADER;
   OptionalHeader:IMAGE_OPTIONAL_HEADER32;
end;
Если файл действительно PE, то Signature должна равняться IMAGE_NT_SIGNATURE. В IMAGE_FILE_HEADER нас интересует только поле NumberOfSection типа word, с помощью него можно узнать сколько секций в файле(это очень важно). А вот здесь все что нам нужно из IMAGE_OPTIONAL_HEADER32.

Нас интересуют поля:

AddressOfEntryPoint Это относительный адрес точки входа в программу. Именно с кода, находящегося по этому адресу начинается выполнение программы.
ImageBase Отсюда начинается отображение исполняемого файла в память
Subsystem Определяет является ли приложение GUI, Console, Native.
MajorLinkerVersion
MinorLinkerVersion
Вместе определяют версия линковщика.

Теперь мы рассмотрели необходимый минимум, если вам хочется узнать побольше,то ищите на сайте wasm.ru. Из всего изложенного мы получим:

EntryPoint:=IntToHex(PEHead.OptionalHeader.AddressOfEntryPoint);
LinkerInfo:=IntToStr(PEHead.OptionalHeader.MajorLinkerVersion)+'.'+IntToStr(PEHead.OptionalHeader.MinorLinkerVersion);
case pehead.OptionalHeader.Subsystem of
   1: subsystemstr:='Native';
   2: subsystemstr:='Win32 GUI';
   3: subsystemstr:='Win32 Console';
   else subsystemstr:='Unknown';
end;
NumOfSect:=IntToStr(PEHead.FileHeader.NumberOfSections);
ImgBase:=IntToHex(PEHead.OptionalHeader.ImageBase);
Вот мы и получили основные данные, которые показывают анализаторы. Остается найти только FileOffset, EPSection, FirstBytes и список секций. Сначала найдем FileOffset и секции. Если мы добавим к p размер структуры IMAGE_NT_HEADERS, то получим адрес структуры IMAGE_SECTION_HEADER. Адрес первой секции файла. Т.к. количество секций нам известно, то получить их список не составит труда:
p:=pointer(integer(p)+sizeof(IMAGE_NT_HEADERS));
for i:=1 to numbers do //numbers - количество секций, их мы получили ранее
   begin
     imgsection:=PIMAGE_SECTION_HEADER(p)^;//imgsection:IMAGE_SECTION_HEADER
     lstrcpyn(@buf,@imgsection.name,8);//копируем в buf название секции
     .....................................// далее следует код для определения FileOffset и EPSection
     if (EntryPoint>=imgsection.VirtualAddress)and(EntryPoint<=imgsection.VirtualAddress+imgsection.Misc.VirtualSize) then
       begin
       EPSection:=buf;
       FileOffset:=EntryPoint-imgsection.VirtualAddress+imgsection.PointerToRawData;//здесь EntryPoint уже типа dword
       end;
     p:=pointer(integer(p)+sizeof(IMAGE_SECTION_HEADER));//теперь в p адрес следующей секции
   end;
Как видите все просто, только надо будет куда-то сохранять названия секций и все что вы захотите о них узнать. И еще, если FileOffset у вас будет равен 0, то FileOffset:=EntryPoint. С FirstBytes вообще элементарно:
1) SetFilePointer(hFile,fileoffset,nil,FILE_BEGIN); //работаем с файлом
     ReadFile(hFile,firstbytesbuf,4,bytesread,nil);
     firstbytest:=strtohex(firstbytesbuf);

2) for i:=0 to 3 do //работаем с памятью
       firstbytesbuf[i]:=PChar(ImgBase+EntryPoint+i)^;
     firstbytest:=strtohex(firstbytesbuf);

Примечание: данный способ не всегда корректен. В частности, неправильные значения получаются на файлах запакованных NsPack, WinUpack v0.399, для вычисления FileOffset в этих случаях, нужно учитывать FileAlignment. Рекомендация: прочтите статью про структуру PE

Функция StrToHex переводит символьную строку в Hex. Вот как выглядет эта функция:

function StrToHex(a:array of char):string;
  var i,j:byte;s:string;
19:34 25.08.2006 begin
  j:=length(a)-1;
  for i:=0 to j do
     s:=s+inttohex(ord(a[i]),2);
  StrToHex:=s;
end;
Вот и все с основными параметрами PE файла. Далее мы рассмотрим принципиальный метод детектирования упаковщиков\ протекторов\ компиляторов.

Детектирование.

Детектирование упаковщиков, протекторов, компиляторов основано на том, что определенному компилятору(упаковщику,протектору) соответствует своя сигнатура - последовательность байт, т.о. считав её и сравнив с образцовой мы можем сказать какой компилятор использовался, все вышесказанное касается также протекторов и упаковщиков. Эталонные сигнатуры можно получить самим или использовать файл Signs.txt из PE Tools. Далее идет код детектирования:

InfoText:=SignInfo(hFile,FileOffset);//детектирование
UnMapViewOfFile(a); //все, закрываем файл
CloseHandle(hFileMapping);
CloseHandle(hFile);

function SignInfo(fs:cardinal;EntryPoint:cardinal):string;
var i:integer;
   s,stemp,sign:string;
   f:textfile;
   a,temp:array[0..1000] of char;
   Err:byte;
   bytesread:dword;
begin
  Err:=0;
  if fileexists(extractfilepath(application.exename)+'Signs.txt')=true then //если файл существует, то
     begin
     assignfile(f,extractfilepath(application.exename)+'Signs.txt'); //открываем файл и работаем с ним
     reset(f);
     while not eof(f) do
       begin
         Application.ProcessMessages;// чтобы наша программа не зависла
         readln(f,temp);
         s:=copy(temp,pos('=',temp)+1,pos(']',temp)-1);//обрабатываем строку
         SetFilePointer(fs, EntryPoint, nil, FILE_BEGIN);//перемещаемся по PE файлу
         ReadFile(fs,a,length(s)-1,bytesread,nil);//и читаем из него для нашей сигнатуры
         stemp:=StrToHex(a);//преобразуем в Hex
         for i:=0 to length(s)-1 do //далее сверяем
           begin
             Application.ProcessMessages;
             if s[i]<>':' then
               if s[i]<>stemp[i] then
                 inc(Err);
             if Err=1 then break;
           end;
         if Err=0 then
           begin
             sign:=copy(temp,pos('[',temp)+1,pos('=',temp)-2);
             break;
           end;
         Err:=0;
         s:='';
         temp:='';
         stemp:='';
       end;
     CloseFile(f);//закрываем Signs.txt
     end;
  if sign='' then
     SignInfo:='Unknown!'
  else
     SignInfo:=sign;
end;

Примечание: данный пример на больших файлах и при большом количестве сигнатур, будет работать очень медленно. Правильнее было бы применить какой-нибуть алгоритм поиска (благо в интернете много реализаций на любом языке программирования), ну и конечно отказаться от работ со строками, тогда скорость сканирования заметно возрастёт. Также, закрывать файл совсем не обязательно, можно было бы дальше работать с ним через указатель. MOVE(P,A,SizeOf(A));

Все тоже очень просто. А причем здесь антивирусы спросите вы? Да, при всем. Допустим, что в природе не существует никаких упаковщиков и протекторов, и тогда перед нами предстанет сигнатура трояна или вируса, и поискав её в своей базе сигнатур мы точно сможем утверждать, что это зловредный код. Я скачал pinch - известный троян, и результаты не заставили себя ждать. Если он не запакован, то для него характерна следующая последовательность байт:

E8::00000050E8::01000050E8::0100004883F85A7516E8DEFFFFFF6888130000E8::0000006A00E8::000000EBE2
т.е. если мы получим её, то очевидно будет, что перед нами pinch и если мы антивирус, то должны "кричать" об этом пользователю.

Заключение.

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

Примечание: а самое главное, внедряйте новые варианты детектирования. Т.е. находите уникальные особенности пакера/протектора. Детект от EntryPoint самое простое, что можно придумать. В сети много различного софта (Скрамблеры), чье предназначение скрыть информацию о том, чем реально запакован файл.

Необходимые ссылки:

Исходный код анализатора и детектора PEPirate (черновая версия) - zip или rar

DiE - hellspawn.nm.ru

Туториалы Iczelion'a о PE, документация по PE формату, PEiD, PE Tools - wasm.ru

Форум посвященный программированию детекторов, здесь ответят на все возникшие у вас вопросы - hellspawn.ucoz.ru/forum

Оригинал статьи в txt-формате

 

Copyright© 2006 Константин Панков & Hellsp@wn  Специально для Delphi Plus


Пожалуйста, оцените статью
Отлично
Хорошо
Средне
Плохо
Очень плохо