Разработка функций RvaToRaw и RawToRva

Цель статьи


Целью этой статьи является желание автора показать некоторые нюансы по разработке функций RvaToRaw/RawToRva, которые являются важными для системных утилит работающих с исполнимыми файлами формата PE.

На кого нацелена статья?


  • Читатель знаком с форматом файлов «Portable Executable»
  • Читатель >= 1 раза писал парсер этого файла
  • Читатель отлично знает что такое «RvaToRaw»


Терминология



RVA — Это аббревиатура англ. слов Relative Virtual Address и означает смещение в байтах от начала реального или предполагаемого адреса загрузки модуля.
RAW — Файловое смещение от начала файла. Другое удачное название «File offset».

Разработка


При поиске в гугле по ключевым словам «rva to raw» или «rva to offset» можно наткнуться на следующий код:
DWORD RVAToOffset(IMAGE_NT_HEADERS32* pNtHdr, DWORD dwRVA)
 {
    int i;
    WORD wSections;
    PIMAGE_SECTION_HEADER pSectionHdr;
 
    /* Map first section */
    pSectionHdr = IMAGE_FIRST_SECTION(pNtHdr);
    wSections = pNtHdr->FileHeader.NumberOfSections;
 
    for (i = 0; i < wSections; i++)
    {
        if (pSectionHdr->VirtualAddress <= dwRVA)
            if ((pSectionHdr->VirtualAddress + pSectionHdr->Misc.VirtualSize) > dwRVA)
            {
                dwRVA -= pSectionHdr->VirtualAddress;
                dwRVA += pSectionHdr->PointerToRawData;
 
                return (dwRVA);
            }
        pSectionHdr++;
    }
    return (-1);
 }


Код имеет имеет несколько грубых ошибок и при этом весьма популярен.

В нем не учитывается:
  • Значение rva может быть меньше чем OptionalHeader.SizeOfHeaders, т.е. значение может указывать внутрь заголовка
  • Значения VirtualAddress, VirtualSize, PointerToRawData никак не выравниваются
  • Используется макрос IMAGE_FIRST_SECTION , который не учитывает 64-битных файлов, об этом честно сказано в winnt.h


Более корректный код, с точки зрения автора:

Код класса выравнивающего оффсеты и адреса
inline uint32_t alignDown(uint32_t value_, uint32_t factor)
{
    return value_ & ~(factor-1);
}

inline uint32_t alignUp(uint32_t value_, uint32_t factor)
{
    return alignDown(value_ - 1, factor) + factor;
}

class Aligner
{
public:
    const uint32_t FORCED_FILE_ALIGNMENT = 0x200;
    const uint32_t MIN_SECTION_ALIGNMENT = 0x1000;

public:
    Aligner(uint32_t fileAlignment_, uint32_t sectionAlignement_)
        : fileAlignment(fileAlignment_)
        , sectionAlignement(sectionAlignement_)
    {
    }

    uint32_t getVirtualSize(uint32_t size)
    {
        return needAlign(sectionAlignement) ?
            alignUp(size, sectionAlignement) :
            size;
    }

    uint32_t getVirtualAddress(uint32_t address)
    {
        return needAlign(sectionAlignement) ?
            alignDown(address, sectionAlignement) :
            address;
    }

    uint32_t getFileOffset(uint32_t offset)
    {
        return needAlign(sectionAlignement) ?
            alignDown(offset, FORCED_FILE_ALIGNMENT) :
            offset;
    }

    uint32_t getSectionSize(const ImgSectionHeader& header)
    {
        uint32_t fileSize = header.SizeOfRawData;
        uint32_t virtualSize = header.Misc.VirtualSize;
        if (needAlign(sectionAlignement)) {
            fileSize = alignUp(fileSize, fileAlignment);
            virtualSize = alignUp(virtualSize, sectionAlignement);
        }
        return std::min(fileSize, virtualSize);
    }
private:
    uint32_t fileAlignment;
    uint32_t sectionAlignement;

    bool needAlign(uint32_t sectionAlignement)
    {
        return sectionAlignement >= MIN_SECTION_ALIGNMENT;
    }
};



Код метода для конвертации Rva в Raw
const uint32_t numInvalidRaw = (uint32_t)( -1 );

uint32_t PeUtils::RvaToRaw( const PeImage& peImage, uint32_t rva )
{
    uint32_t result = INVALID_RAW;

    const auto& optionalHeader = peImage.NtHeaders.OptionalHeader;

    if (rva < optionalHeader.SizeOfHeaders)
        return rva;

    Aligner aligner(optionalHeader.FileAlignment, optionalHeader.SectionAlignment);

    if (peImage.NtHeaders.FileHeader.NumberOfSections > 0) {
        for (const auto& section : peImage.Sections) {
            if (section.PointerToRawData == 0)
                continue;

            auto sectionStart = aligner.getVirtualAddress(section.VirtualAddress);
            auto sectionSize = aligner.getSectionSize(section);
            auto sectionEnd = sectionStart + sectionSize;

            if (sectionStart <= rva && rva < sectionEnd) {
                auto sectionOffset = aligner.getFileOffset(section.PointerToRawData);
                sectionOffset += (rva - sectionStart);
                if (sectionOffset < peImage.SizeOfFileImage)
                    result =  sectionOffset;
            }
        } // for
    } 
    else if (rva < aligner.getVirtualSize(optionalHeader.SizeOfImage)) {
        result = rva;
    }
    return result;
}



Что улучшено:
  • Учитывается факт того, что не всегда нужно выравнивать(см. метод needAlign)
  • Учитывается факт того, что количество секций может быть нулевым. И такой файл может быть загружен!
  • Когда требуется выравниваются значения PointerToRawData и VirtualAddress в меньшую сторону
  • Более корректное вычисление размера секции


Nota bene:
На самом деле даже это не окончательный вариант, но он уже ближе к тому, как системный загрузчик понимает такие файлы.

Тестирование


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

Пути получения тестовых файлов:
  1. Применить упаковщик исполнимых файлов, который может создать не совсем стандартные значения в заголовках. Примером такого упаковщика является Upack.exe, но существует и множество других
  2. Периодически пополнять свою коллекцию новых образцов зловредных файлов, к примеру с MDL(см. доп. источники)


Nota bene:
Прошу простить за напоминание, но любой сомнительный файл лучше всего запускать в гостевой виртуальной машине, к примеру на базе VMWare или VirtualBox.

Также корректность своего кода можно проверить с помощью Hiew, IDA Pro или отладчика.

Дополнительная источники:


  1. #include <winnt.h>. Хидер входит в MSVC и не только. Этот хидер должен быть настольным справочником для любого кто пишет парсер этого файла!
  2. Мэтт Питрек. «Форматы РЕ и COFF объектных файлов». rsdn.ru/article/baseserv/pe_coff.xml
  3. Максим М. Гумеров. «Загрузчик PE-файлов». rsdn.ru/article/baseserv/peloader.xml
  4. Forums >> IDA Pro >> # new PE loader bug and new crack-me. www.openrce.org/forums/posts/969
  5. MDL. www.malwaredomainlist.com/mdl.php. Ресурс на котором можно скачать образцы сомнительных файлов


Post scriptum:
  • Подчеркну что автор не ставил перед собой цель кого-либо удивить или кого-нибудь высмеять. Автор имеет огромное желание повысить качество системного кода. Очень надеюсь что в будущем «великий гугл» будет выдавать ссылки на достаточно корректный код RvaToRaw\RawToRva
  • Автор также обрадуется любым вопросам, любой критике и любым пожеланиям
Share post

Similar posts

Comments 3

    +1
    У автора мания говорить о себе в 3-м лице?
      +2
      >>У автора мания говорить о себе в 3-м лице?
      Любая техническая литература, в том числе и статьи пишут не от первого лица.
        0
        В топике были сделаны изменения.

        Что сделано:
        1) Найденный файловый офсет не проверялся на вхождение в файл
        2) Не учитывалось, что системный загрузчик берет минимальное между файловым и виртуальным размерами

        Что еще нужно сделать:
        1) Файловое выравнивание может быть меньше чем 0x200.

        Only users with full accounts can post comments. Log in, please.