Пишем свой анализатор исполняемых файлов
Анализатор исполняемых файлов – программа, которая может определить чем
скомпилирован или упакован исполняемый файл. Одной из таковых является PEid.
Давайте попробуем написать свой.
Создайте проект Standart EXE. Добавьте модуль, в котором будут структуры и
функции для работы с PE-заголовком:
DOS-заголовок – проверяет, если программа запущена из под DOS’a, то
запуститься DOS stub, который отобразит строку "This рrogram cannot run in DOS
mode". Здесь нам понадобятся поля: Magic – должен содержать "4D5Ah", что равно "MZ",
следовательно файл – EXE; lfanew – длина DOS заголовка, чтобы узнать где
начинается РE-заголовок.
Option Explicit
'DOS Header
'Совместимый заголовок (форматированная часть)
Public Type IMAGE_DOS_HEADER
Magic As Integer
cblp As Integer
cp As Integer
crlc As Integer
cparhdr As Integer
minalloc As Integer
maxalloc As Integer
ss As Integer
sp As Integer
csum As Integer
ip As Integer
cs As Integer
lfarlc As Integer
ovno As Integer
res(3) As Integer
oemid As Integer
oeminfo As Integer
res2(9) As Integer
lfanew As Long 'длина заголовка
End Type
РE-заголовок – содержит много основных полей. Нужные нам: Signature –
должна быть PE за которыми следуют два нуля, иначе это не PE файл, можно
закрываться; NumObjects – количество секций в файле; EntryPointRVA – адрес,
относительно Image Base по которому передается управление при запуске программы;
ImageBase – виртуальный начальный адрес загрузки программы (ее первого байта).
'PE Header
Public Type PE_HEADER
Signature As String * 4
CPU_Type As Integer 'тип процессора
'CPU Type имеет следующие значения:
'14Ch -i386
'014Dh - i486
'014Eh - i586
'0162h - MIPS Mark I (R2000, R3000)
'0163h - MIPS Mark II (R6000)
'0166h - MIPS Mark III (R4000)
NumObjects As Integer 'Число секций
TimeDateStamp As Long
pCOFFTable As Long
COFFTableSize As Long
NTHeaderSize As Integer
Flags As Integer
Magic As Integer
LinkMajor As Byte
LinkMinor As Byte
SizeOfCode As Long
SizeOfInitData As Long
SizeOfUnInitData As Long
EntryPointRVA As Long
BaseOfCode As Long
BaseOfData As Long
ImageBase As Long
ObjectAlign As Long
FileAlign As Long
OSMajor As Integer
OSMinor As Integer
USERMajor As Integer
USERMinor As Integer
SubSysMajor As Integer
SubSysMinor As Integer
Reserved1 As Long
ImageSize As Long
HeaderSize As Long
FileCheckSum As Long
SubSytem As Integer
'SubSystem имеет следующие значения:
'0001h - Native
'0002h - Windows GUI, т.е окошечная
'0003h - Windows Character
' (консольное приложение)
'0005h - OS/2 Character
'0007h - Posix Character
DLLFlags As Integer
StackReserveSize As Long
StackCommitSize As Long
HeapReserveSize As Long
HeapComitSize As Long
LoaderFlags As Long
NumOfRVAandSizes As Long
ExportTableRVA As Long
ExportDataSize As Long
ImportTableRVA As Long
ImportDataSize As Long
ResourceTableRVA As Long
ResourceDataSize As Long
ExceptionTableRVA As Long
ExceptionDataSize As Long
SecurityTableRVA As Long
SecurityDataSize As Long
FixTableRVA As Long
FixDataSize As Long
DebugTableRVA As Long
DebugDataSize As Long
ImageDescriptionRVA As Long
DescriptionDataSize As Long
MachineSpecificRVA As Long
MachnineDataSize As Long
TLSRVA As Long
TLSDataSize As Long
LoadConfigRVA As Long
LoadConfigDataSize As Long
Reserved2(39) As Byte
End Type
Таблица секций - это массив структур. Число входов в таблице
объектов (секций) определяется полем Num of Objects заголовка PE Header.
Рассмотрим некоторые поля: SectionName – имя секции, максимальная длина поля 8
байтов. Может быть пустым; VirtualSize - виртуальный размер секции, именно
столько памяти будет отведено под секцию; VirtualAddress – виртуальный адрес
секции, размещение секции в памяти; PointerToRawData - файловое смещение на
начало секции.
'Object Table (таблица секций)
Public Type IMAGE_SECTION_HEADER
SectionName As String * 6
PhisicalAddress As Integer
VirtualSize As Long
VirtualAddress As Long
SizeOfRawData As Long
PointerToRawData As Long
PointerToRelocations As Long
PointerToLinenumbers As Long
NumberOfRelocations As Integer
NumberOfLinenumbers As Integer
Characteristics As Long
End Type
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(pDst As Any, pSrc As Any, ByVal ByteLen As Long)
Public Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" _
(ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal _
dwShareMode As Long, lpSecurityAttributes As Any, ByVal _
dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As Long) As Long
Public Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, _
lpBuffer As Any, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead _
As Long, lpOverlapped As Any) As Long
Public Declare Function SetFilePointer Lib "kernel32" (ByVal hFile As _
Long, ByVal lDistanceToMove As Long, lpDistanceToMoveHigh As Long, ByVal _
dwMoveMethod As Long) As Long
Public Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Public tDos As IMAGE_DOS_HEADER
Public tFile As PE_HEADER
Public tSection() As IMAGE_SECTION_HEADER
Dim cFileOffset As Long
Dim PackBytes As String
Давайте рассмотрим функцию, которая будет проверять, действительно ли файл
является исполняемым:
Public Function PEorNotPE(hFile As Long) As Boolean
Dim Buffer(4) As Byte
Dim lngBytesRead As Long
Dim tDosHeader As IMAGE_DOS_HEADER
If (hFile > 0) Then 'если есть файл
' читаю DOS Header
ReadFile hFile, tDosHeader, ByVal Len(tDosHeader), lngBytesRead, ByVal 0&
CopyMemory Buffer(0), tDosHeader.Magic, 2 ' два байта в память
'если они равны "MZ". полдела есть.
If (Chr(Buffer(0)) & Chr(Buffer(1)) = "MZ") Then
SetFilePointer hFile, tDosHeader.lfanew, 0, 0
' Прыгаю в конец заголовка и читаю 4! Байта в буфер.
ReadFile hFile, Buffer(0), 4, lngBytesRead, ByVal 0&
If (Chr(Buffer(0)) = "P") And (Chr(Buffer(1)) = "E") And _
(Buffer(2) = 0) And (Buffer(3) = 0) Then
' Должно быть "PE" и два байта равных 0
PEorNotPE = True 'Тогда это настоящий Portable Executable!
Exit Function
End If
End If
End If
PEorNotPE = False
End Function
Итак, мы проверяем DOS-заголовок, сравнивая первое слово с "MZ". Если у файла
верный DOS-заголовок находим PE-заголовок, используя lfanew. Сравниваем первое
слово с "PE". Если совпало – файл является Portable Executable.
Следующая функция находит адрес секции в файле, с которой ей следует начинаться.
Если мы сместимся от начала файла на число байт полученное этой функцией, то
получим первые байты программы, так называемую сигнатуру, сравнив которую с
имеющейся у нас базой мы можем узнать имя компилятора/упаковщика.
Public Function GetFileOffset(sFile As String) As String
Dim PointerToRaw As Long
Dim SizeOfRaw As Long
Dim VirtualAdr As Long
Dim EPoint As Long
Dim sTemp As Long
Dim sData() As Byte
Dim sU As Integer
' Открываю файл и читаю его в массив
Open sFile For Binary As #1
ReDim sData(LOF(1) - 1)
Get #1, , sData
Close #1
Dim sTemp1 As Long
Dim sTemp2 As Long
cFileOffset = 0 'обнуляю offset
CopyMemory tDos, sData(sTemp1), Len(tDos)
' добираюсь до таблицы секции файла.
' они находятся после сразу PE заголовка
CopyMemory tFile, sData(tDos.lfanew), Len(tFile)
sTemp1 = sTemp1 + tDos.lfanew + Len(tFile)
' заполняю массив секций данными
ReDim tSection(tFile.NumObjects - 1)
For sTemp2 = 0 To UBound(tSection)
CopyMemory tSection(sTemp2), sData(sTemp1), Len(tSection(0))
sTemp1 = sTemp1 + Len(tSection(0))
Next sTemp2
' получаю "точку входа" по которой передается управление при запуске программы
EPoint = tFile.EntryPointRVA
' из всех секции путем сравнения виртуального адреса секции и ее
' размера с "точкой входа" получаю виртуальную первую секцию так
' называемую EP section.
For sU = 0 To UBound(tSection)
sTemp1 = tSection(sU).VirtualAddress
sTemp2 = sTemp1 + tSection(sU).VirtualSize
If EPoint >= sTemp1 And EPoint <= sTemp2 Then GoTo sNex
'если нашел выхожу из цикла
Next sU
sNex:
' отнимаю от виртуального адреса найденной секции файловое
' смещение на начало секции.
sTemp = tSection(sU).VirtualAddress - tSection(sU).PointerToRawData
' И теперь все отнимаю от "точки входа"
cFileOffset = EPoint - sTemp
' Здесь и будут первые байты программы.
GetFileOffset = cFileOffset
End Function
Функция пропускает определенное кол-во байт полученных функцией GetFileOffset и
считывает в переменную PackBytes следующие 30 байт.
Public Function Get1stBytes(sFile As String) As String
Dim sX As Integer
Dim sBytes As String * 30' кол-во считываемых байт
Dim sTemp As String
PackBytes = ""
' открываю файл
Open sFile For Binary Access Read As #1
Seek #1, cFileOffset + 1'пропускаю число байт
Get #1, , sBytes 'получаю число байт
Close #1
' перевожу в шестнадцатеричную систему каждый байтик
For sX = 1 To Len(sBytes)
sTemp = Hex(Asc(Mid(sBytes, sX, 1)))
If sTemp = "0" Then
sTemp = "00"
ElseIf Len(sTemp) = 1 Then
sTemp = "0" & sTemp
End If
PackBytes = PackBytes & sTemp
Get1stBytes = Get1stBytes & sTemp & " "
Next sX
End Function
Вот здесь мы и проводим сравнение сигнатур. Если какая то сигнатура совпадет с
байтами из переменной PackBytes, то мы увидим имя компилятора или упаковщика
выбранной нами программы.
Public Function PackerName() As String
Dim sTemp As String
'сигнатурка VB
'Like - оператор для проверки строки String на маску Pattern
' ? Любой одиночный символ
' * Ноль или более символов
' # Любая одиночная цифра (0–9).
If Trim(PackBytes) Like _
Trim("68????????E8????????0000??00000030000000????????????????????") _
Then
PackerName = "Microsoft Visual Basic v5.0/v6.0"
ElseIf Trim(PackBytes) Like _
Trim("60BE00????008DBE00????FF5783CDFFEB10????????????8A0646880747") _
Then
PackerName = "UPX 0.89.6 - 1.02 / 1.05 - 1.24 - 1.92beta -> Markus & Laszlo"
'Ничего
Else
PackerName = "Nothing found!"
End If
End Function
Прыгаем на форму и вставляем следующий код:
Private Sub Form_Load()
Dim hDest As Long
Dim fName As String
fName = "C:\1.exe" ' имя исследуемого файла
'открываю файл
hDest = CreateFile(fName, ByVal (&H80000000 Or &H40000000), 0, ByVal 0&, 3, 0, ByVal 0)
If PEorNotPE(hDest) Then ' если PE
GetFileOffset fName 'FileOffset
Text1.Text = PackerName ' в Text отобразится имя пакера
CloseHandle hDest 'закрываю файл
End If
CloseHandle hDest
End Sub
Остается добавить побольше сигнатур разных упаковщиков, компиляторов в
GetPakerName и можно сказать, что анализатор исполняемых файлов готов. Конечно,
здесь далеко не все, что содержит в себе формат исполняемого файла, но Вы можете
доработать исходник, добавить таблицы экспорта, импорта, получить ресурсы и
многое другое. Читайте информацию о PE файлах – это интересно.
Автор: Артём Курсанов
В функции PackerName настоятельно
рекомендуется использовать синтаксис Select ... Case,
который специально предназначен для сравнения значений "одно ко многим". -
ГМ