| Пишем анализатор PE файлов на Delphi. | ----------------------------------------------------- Авторы: Панков К.(kosfiz), 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 вообще элементарно: //* Примечание: данный способ не всегда корректен. В частности, неправильные значения получаются на файлах запакованных NsPack, WinUpack v0.399, для вычисления FileOffset в этих случаях, нужно учитывать FileAlignment. Рекомендация: прочтите статью про структуру PE *// 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); Функция StrToHex переводит символьную строку в Hex. Вот как выглядет эта функция: function StrToHex(a:array of char):string; var i,j:byte;s:string; begin j:=length(a)-1; for i:=0 to j do begin s:=s+inttohex(ord(a[i]),2); end; 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(черновая версия) - kosfiz.narod.ru/sources/pein.rar DiE - hellspawn.nm.ru Туториалы Iczelion'a о PE, документация по PE формату,PEiD,PE Tools - wasm.ru Форум посвященный программированию детекторов, здесь ответят на все возникшие у вас вопросы - hellspawn.ucoz.ru/forum/ Оригинал статьи: kosfiz.narod.ru/articles.html