• VLMI - форум по обмену информацией. На форуме можете найти способы заработка, разнообразную информацию по интернет-безопасности, обмен знаниями, курсы/сливы.

    После регистрации будут доступны основные разделы.

    Контент форума создают пользователи, администрация за действия пользователей не несёт ответственности, отказ от ответственности. Так же перед использованием форума необходимо ознакомиться с правилами ресурса. Продолжая использовать ресурс вы соглашаетесь с правилами.
  • Подпишись на наш канал в Telegram для информации о актуальных зеркалах форума: https://t.me/vlmiclub

Авторская статья Написание софта для reverse engineering задач

ARCHANGEL

Участник
Сообщения
27
Реакции
81
0 руб.
Telegram
Доброго здравия, уважаемая аудитория. Данная статья писалась для конкурса статей от VLMI по Информационной Безопасности. И сегодня я расскажу вам, как писал свой велосипед с квадратными колёсиками.

Постановка задачи
Было у меня одно приложение, точнее, программный пакет. Работал этот пакет исключительно под управлением ОС Windows, но как именно – непонятно. Из себя он представлял порядка 10 различных exe-файлов, и порядка 15-20 dll. Некоторые процессы были запущены постоянно, некоторые запускались при наступлении какого-то события. Начав реверсить один такой ехе, я очень скоро столкнулся с тем, что все эти бинарники тесно взаимодействуют друг с другом посредством файлов, реестра, сообщений.

Например, открывается на чтение некий ключ реестра со значением “provider”, и я хочу узнать, кто же это значение туда записал. Оказывается, что в текущем ехе – никто. И мне бы поискать эту строку “provider” по всем модулям. Да ещё бы и рекурсивно в подпапках. То есть нужно некое подобие grep/strings.

Но так как ОС у нас Windows, там такого ничего нет. Вот я и решил написать небольшую тулзу, которая будет рекурсивно обходить файловую систему и искать во всех файлах строки. Причём будет это делать в несколько потоков, т.к. у меня машина многопоточная.

Для имплементации я взял С++, и начал ваять свой велосипед.


Этапы решения задачи
Для начала рассмотрим более простую задачу – поиск строки в массиве. Предположим, что нам нужно определить, может ли один отдельной взятый символ быть символом строки. Для этого мы воспользуемся библиотечной функцией isprint. Потом предположим, что читаемая строка должна быть длиннее 4 символов (5 и более). Идея взята из дизассемблеров, которые руководствуются таким же принципом. Правда, там этот параметр может быть настраиваемым. Также не будем забывать, что строка в стиле С должна заканчиваться нулевым байтом.

И получится что-то такое:

Код:
bool AsciDetector::IsValidSymbol(char symbol) const
{
    return isprint(static_cast<int> (symbol));
}

std::vector<std::string> AsciDetector::ExtractStrings(const std::vector<char> &data) const
{
    std::vector<std::string> result;
    size_t length = 0;

    for (auto it = data.cbegin();it != data.cend();++it)
    {
        if (*it == 0)
        {
            if (length >= 5)
            {
                result.emplace_back(it - length,it - 1);
            }
            length = 0;
        }
        else if (IsValidSymbol(*it))
        {
            ++length;
        }
    }

    return result;
}

Итак, мы получаем на вход вектор символов. В векторе мы последовательно ищем читаемые символы, чтобы они шли подряд и завершались нулевым байтом. Когда мы нашли такую последовательность – сравниваем её длину с нашим условием (5 и более) и либо добавляем к результату, либо нет (если условие не выполняется).

File Mapping
Очевидно, что нам нужно работать не с массивами, а с файлами на диске. Значит нужно как-то от файлов перейти к массивам. Для этого воспользуемся файловым маппингом. Далее будет код, который наверняка покажется вам знакомым. Единственное, что в нём необычно – идиома RAII, а не просто один цикл открытия файла, создания маппинга и проецирования файла в память.

Весь код для работы с файлами можно разделить на 3 класса:​
Код:
FileOpener::FileOpener(const std::string &name)
{
    fileHandle = CreateFileA(name.c_str(),GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ |
                                                                       FILE_SHARE_WRITE, nullptr,OPEN_EXISTING,0, nullptr);
    if (fileHandle == INVALID_HANDLE_VALUE)
    {
        throw std::logic_error("Unable to open file " + name);
    }
}

FileOpener::~FileOpener()
{
    CloseHandle(fileHandle);
}

HANDLE FileOpener::GetFileHandle() const
{
    return fileHandle;
}

Этот класс просто открывает существующий файл.
Код:
OpenMapping::OpenMapping(const std::string &name):
        fileopener(name)
{
    mapHandle = CreateFileMappingA(fileopener.GetFileHandle(),nullptr,PAGE_READONLY,0,0, nullptr);
    if (mapHandle == nullptr)
    {
        throw std::logic_error("Unable to map file " + name);
    }
}

OpenMapping::~OpenMapping()
{
    CloseHandle(mapHandle);
}

HANDLE OpenMapping::GetMapHandle() const
{
    return mapHandle;
}

А этот – создаёт объект ядра для файловой проекции.
Код:
FileMapping::FileMapping(const std::string &name):mapping(name)
{
    mapStart = MapViewOfFile(mapping.GetMapHandle(),FILE_MAP_READ,0,0,0);
    if (mapStart == nullptr)
    {
        throw std::logic_error("Unable to load file mapping for the " + name);
    }
}

FileMapping::~FileMapping()
{
    UnmapViewOfFile(mapStart);
}

size_t FileMapping::GetMapSize() const
{
    MEMORY_BASIC_INFORMATION info = {0};

    if (VirtualQuery(mapStart,&info,sizeof(info)))
    {
        return info.RegionSize;
    }
    return 0;
}

std::vector<char> FileMapping::GetData() const
{
    size_t mappingLength = GetMapSize();
    std::vector<char> result(mappingLength);

    char* begin = reinterpret_cast<char*> (mapStart);
    for (size_t i = 0;i < mappingLength;i++)
    {
        result[i] = begin[i];
    }

    return result;
}

Далее выполняется непосредственная загрузка файла в виртуальную память. Метод GetData() как раз и создаёт массив, с которым потенциально должен работать наш код, написанный ранее.

Перечисление файлов

Работать с одним файлом – это прекрасно, но для этого у нас есть дизассемблер. Нам нужно перечислять файлы в директории и её поддиректориях. Для этого воспользуемся WinAPI:
Код:
std::list<std::string> DirWalker::GetElements(const std::string &dir, bool files)
{
    std::list<std::string> result;
    WIN32_FIND_DATA data = {0};

    std::string Template = dir + "\\*.*";

    HANDLE searchHandle = FindFirstFileA(Template.c_str(),&data);
    if (searchHandle != INVALID_HANDLE_VALUE)
    {
        do{
            std::string ShortName = data.cFileName;
            if (ShortName == "." || ShortName == "..")
                continue;

            auto condition = data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
            condition = (files) ? !condition : condition;

            if (condition)
            {
                result.emplace_back(dir + std::string("\\") + data.cFileName);
            }
        }while (FindNextFileA(searchHandle,&data));
        FindClose(searchHandle);
    }

    return result;
}

Данный метод вернёт список полных путей либо к поддиректориям, либо к файлам в текущей директории. Если параметр files == true, то будут вам файлы. Иначе – поддиректории. Для простоты сделаны две обёртки:
Код:
std::list<std::string> DirWalker::GetSubdirs(const std::string &dir)
{
    return GetElements(dir,false);
}

std::list<std::string> DirWalker::GetFilesInDir(const std::string &dir)
{
    return GetElements(dir,true);
}

Это всё хорошо, но мы должны сканировать файлы, даже если глубина вложенности неизвестна заранее. Поэтому файловая система условно представляется деревом, корнем дерева – директория, с которой начинается сканирование. Далее делаем обход дерева в ширину (можно в глубину, не важно):

Код:
std::string DirWalker::NextFile()
{
    std::string currentDir;
    std::string result;

    if (!directories.empty())
    {
        currentDir = directories.front();
        directories.pop_front();
    }

    if (!currentDir.empty())
    {
        auto filesInCurrentDir = GetFilesInDir(currentDir);
        files.insert(files.begin(),filesInCurrentDir.begin(),filesInCurrentDir.end());
        auto subDirs = GetSubdirs(currentDir);
        directories.insert(directories.begin(),subDirs.begin(),subDirs.end());
    }

    if (!files.empty())
    {
        result = files.front();
        files.pop_front();
    }

    return result;
}

Разберём этот код более детально. Экземпляр класса DirWalker имеет очень простой интерфейс:
Код:
public:
    explicit DirWalker(const std::string& CurrentDir);
    std::string NextFile();

Это то, на что нужно обратить внимание. Будучи созданным, такой объект предоставляет клиентской стороне метод NextFile(). Клиент, вызывая его, получает в ответ полный путь к следующему файлу. В объекте есть 2 списка:

Код:
private:
    std::list<std::string> directories;
    std::list<std::string> files;

В directories помещаются пути тех директорий, которые ещё предстоит просканировать. В files помещаются полные пути к файлам. И очередной полный путь к файлу извлекается из списка при каждом вызове метода NextFile().

Многопоточность

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

У нас будет lockfree односвязный список. Я приведу ниже только 2 наиболее важных метода:
Код:
void push_back(T element)
{
    link* zero = nullptr;
    link* Link = new link(element);
    if (first.compare_exchange_strong(zero,Link))
        return;

    zero = nullptr;
    if (last.compare_exchange_strong(zero,Link))
    {
        auto pointer = first.load();
        pointer->next = last;
        return;
    }

    auto pointer = last.exchange(Link);
    pointer->next = Link;
}
T pop_front()
{
    if (first != nullptr)
    {
        auto temp = first.exchange(first.load()->next);
        auto backup = temp;
        T result = temp->value;
        link* zero = nullptr;
        last.compare_exchange_strong(temp,zero);
        delete backup;
        return result;
    }
    else
        throw std::logic_error("No more elements");
}

Вся потокобезопаность построена std::atomic шаблоне, и возможности аппаратного обеспечения инстанцировать его реализацию как lockfree. Проще говоря, у процессора есть инструкции, которые являются неблокирующими на аппаратном уровне. В данном случае компилятор их использует.

Запуск сканирования

Теперь рассмотрим самый важный метод нашего приложения – именно он запускает поиск файлов, распараллеливает поиск строк в файлах и выводит результат на экран.

Код:
void Processor::ManagingWork()
{
    auto coreNumber = std::thread::hardware_concurrency();
    std::vector<std::thread> fileParsers;

    std::thread walker{&Processor::DirList,this};
    std::thread printer{&Processor::PrintStrings,this};
    for (unsigned int i = 0;i < coreNumber - 2;i++)
    {
        fileParsers.emplace_back(&Processor::handleFileProcessing,this);
    }

    walker.join();
    for(auto& parser:fileParsers)
    {
        parser.join();
    }
    IsReadingFinished = true;
    printer.join();
}

Вначале вызовом hardware_concurrency() определяем количество аппаратно-независимых единиц, способных выполнять код (ядер, процессоров). Далее 1 поток отводим под парсинг файловой системы, 1 – под печать результатов. Все остальные будут парсить файлы.

Код всего проекта доступен на bitbucket.
https://bitbucket.org/KulykIevgen/strings

Выводы

  1. Утилита может быть полезна при анализе больших приложений (состоящих из множества исполняемых файлов), например, игр, CAD-систем.
  2. Если в составе приложения есть упакованный бинарные файлы, их придётся предварительно распаковать.
  3. Русский язык, а также Unicode не поддерживаются. Можете сами добавить.

Вывод приложения выглядит так:
Код:
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _RtlLeaveCriticalSection@
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _SetErrorMode@
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _RaiseException@1
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _SetEnvironmentVariableW@
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _FlushViewOfFile@
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _GetCommModemStatus@
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _WaitForSingleObject@
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _GetSystemWow64DirectoryW@
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _FindNextVolumeW@1
C:\WORK\SOFT\aircrack-ng-1.2-win\aircrack-ng-1.2-win\bin\32bit\cygwin1.dll ==> _WideCharToMultiByte@3
 
Сверху Снизу