Дата публикации статьи: 29.01.2006 00:55

Пишем свой анализатор исполняемых файлов

Анализатор исполняемых файлов – программа, которая может определить чем скомпилирован или упакован исполняемый файл. Одной из таковых является 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

PE or not PE

Давайте рассмотрим функцию, которая будет проверять, действительно ли файл является исполняемым:

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.

GetFileOffset

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

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
GET1stBytes

Функция пропускает определенное кол-во байт полученных функцией 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
PakerName

Вот здесь мы и проводим сравнение сигнатур. Если какая то сигнатура совпадет с байтами из переменной 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, который специально предназначен для сравнения значений "одно ко многим". - ГМ