Доброго здравия, уважаемая аудитория. Данная статья писалась для конкурса статей от VLMI по Информационной Безопасности. И сегодня я расскажу вам, как писал свой велосипед с квадратными колёсиками.
Например, открывается на чтение некий ключ реестра со значением “provider”, и я хочу узнать, кто же это значение туда записал. Оказывается, что в текущем ехе – никто. И мне бы поискать эту строку “provider” по всем модулям. Да ещё бы и рекурсивно в подпапках. То есть нужно некое подобие grep/strings.
Но так как ОС у нас Windows, там такого ничего нет. Вот я и решил написать небольшую тулзу, которая будет рекурсивно обходить файловую систему и искать во всех файлах строки. Причём будет это делать в несколько потоков, т.к. у меня машина многопоточная.
Для имплементации я взял С++, и начал ваять свой велосипед.
И получится что-то такое:
Итак, мы получаем на вход вектор символов. В векторе мы последовательно ищем читаемые символы, чтобы они шли подряд и завершались нулевым байтом. Когда мы нашли такую последовательность – сравниваем её длину с нашим условием (5 и более) и либо добавляем к результату, либо нет (если условие не выполняется).
Этот класс просто открывает существующий файл.
А этот – создаёт объект ядра для файловой проекции.
Далее выполняется непосредственная загрузка файла в виртуальную память. Метод GetData() как раз и создаёт массив, с которым потенциально должен работать наш код, написанный ранее.
Работать с одним файлом – это прекрасно, но для этого у нас есть дизассемблер. Нам нужно перечислять файлы в директории и её поддиректориях. Для этого воспользуемся WinAPI:
Данный метод вернёт список полных путей либо к поддиректориям, либо к файлам в текущей директории. Если параметр files == true, то будут вам файлы. Иначе – поддиректории. Для простоты сделаны две обёртки:
Это всё хорошо, но мы должны сканировать файлы, даже если глубина вложенности неизвестна заранее. Поэтому файловая система условно представляется деревом, корнем дерева – директория, с которой начинается сканирование. Далее делаем обход дерева в ширину (можно в глубину, не важно):
Разберём этот код более детально. Экземпляр класса DirWalker имеет очень простой интерфейс:
Это то, на что нужно обратить внимание. Будучи созданным, такой объект предоставляет клиентской стороне метод NextFile(). Клиент, вызывая его, получает в ответ полный путь к следующему файлу. В объекте есть 2 списка:
В directories помещаются пути тех директорий, которые ещё предстоит просканировать. В files помещаются полные пути к файлам. И очередной полный путь к файлу извлекается из списка при каждом вызове метода NextFile().
Было бы совсем скучно вот на этом заканчивать, потому я решил сделать тулзу многопоточной. Ведь сейчас очень мало однопроцессорных одноядерных систем. Грех этим не пользоваться. Более того, мне захотелось написать какую-то lockfree структуру данных, а не юзать мьютексы. Во-первых, это должно дать выигрыш в скорости. Во-вторых, менеджить потоки так будет гораздо проще.
У нас будет lockfree односвязный список. Я приведу ниже только 2 наиболее важных метода:
Вся потокобезопаность построена std::atomic шаблоне, и возможности аппаратного обеспечения инстанцировать его реализацию как lockfree. Проще говоря, у процессора есть инструкции, которые являются неблокирующими на аппаратном уровне. В данном случае компилятор их использует.
Теперь рассмотрим самый важный метод нашего приложения – именно он запускает поиск файлов, распараллеливает поиск строк в файлах и выводит результат на экран.
Вначале вызовом hardware_concurrency() определяем количество аппаратно-независимых единиц, способных выполнять код (ядер, процессоров). Далее 1 поток отводим под парсинг файловой системы, 1 – под печать результатов. Все остальные будут парсить файлы.
Код всего проекта доступен на bitbucket.
https://bitbucket.org/KulykIevgen/strings
Вывод приложения выглядит так:
Постановка задачи
Было у меня одно приложение, точнее, программный пакет. Работал этот пакет исключительно под управлением ОС 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 класса:
Весь код для работы с файлами можно разделить на 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
Выводы
- Утилита может быть полезна при анализе больших приложений (состоящих из множества исполняемых файлов), например, игр, CAD-систем.
- Если в составе приложения есть упакованный бинарные файлы, их придётся предварительно распаковать.
- Русский язык, а также 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