Всех приветствую. Сегодня я покажу Вам, как можно сделать примитивный склейщик файлов (aka joiner). Это будет именно склейщик! Не криптор, не архиватор, а именно склейщик. Я дам вам базу, от которой вы можете отталкиваться и модифицировать данный код, как вам угодно. Сразу скажу, что база субъективна. То есть она такая, какой вижу её я. Это просто одна из возможных реализаций. Код для упаковщика будет написан на C++, а код для распаковщика будет написан на чистом C без CRT. Приступим!
Оглавление:
Давайте же разберёмся, как будет работать наш упаковщик. Чтобы разобраться, нужно понять, что мы получаем на вход и какой должен быть выход. Очевидно, что мы на вход получаем пути к файлам, которые нужно склеить, а на выход 1 файл, в котором всё склеено. Но! Мы, наверняка, хотим еще и запустить какой-то файл после распаковки. А может и не один. Так что этот момент тоже нужно учесть. Более того, мы хотим, чтобы путь распаковки не был фиксированным. А чтобы при упаковке можно было выбрать директорию, куда всё добро выкинуть.
На вход:
После сбора всех данных, мы должны записать их в стаб и выкинуть упакованный файл.
Теоретическая часть для упаковщика на этом подошла к концу.
Читаем пути.
Давай же приступим к программированию. Как было сказано, нам нужно прочитать пути до файлов, которые мы желаем упаковать.
Так как в коде упаковщика мы не ограничены ресурсами, будем использовать C++ с некоторыми его прелестями.
Давайте разберем этот код. Создаем 2 вектора, в которых мы будем хранить пути до файлов и заголовки для этих файлов.
Просто читаем пути из stdin’a пока пользовать не введёт run. Вы можете реализовать это путем чтения параметров из argv, но я решил сделать так.
Детали fh::file_header мы разберёмся чуть позже, в пункте “Сохраняем информацию о файле”.
Если пользователь не ввёл ни одного пути, то мы завершаем работу, так как паковать нам нечего!
Сохраняем информацию о файле.
Пришло время заполнить тот самый vector<fh::file_header> file_headers.
Просто пробегаемся по всему массиву и создаем экземпляр класса fh::file_header. Если что-то пошло не так, то выводим сообщение и продолжаем работу для оставшихся файлов.
Кто такой этот ваш fh::file_header?? Это такой класс
Многа кода… Но есть комментарии
. Всё же поясню в чем смысл класса file_header. При инициализации он получает путь до файла. Он должен будет заполнить структуру file_header_internal. И так же он будет реализовывать наше взаимодействие с ней. Давайте рассмотрим структуру file_header_internal. Это практически самая важная часть в нашем проекте.
Не будем заострять внимание на её полях, так как они прокомментированы. Обратим внимание на её размер. В данном случае он 100 байт. Размер её может поменяться, если Вы поменяете константу MAX_FILE_NAME_LEN или уберете #pragma pack(push, 1) с #pragma pack(pop). Эти директивы позволяют включить выравнивание по 1 байту. Если их не будет, то при текущей максимальной длине имени размер её будет 104 байта, а не 100. Из-за выравнивания! Но как Вы могли догадаться, её мы будем записывать в наш стаб, поэтому нам очень важна точность в размерах.
Обратим внимание на перечисление FOLDER_TO_UNPACK. Там просто перечислены пути, по которым есть возможность распаковаться. Так же обратите внимание на приписку : char после имени enum’a. Там мы показываем, что вес этого перечисления 1 байт! Без этого уточнения вес enum будет 4 байта. Я думаю, вам 256 значений хватит, чтобы хранить все ваши пути, поэтому сэкономим место.
Давайте же взглянем на реализацию методов класса file_header.
Метод fill_file_header заполнит поля структуры file_header_internal.
Метод free_mem освободит память, аллоцированную под структуру file_header_internal.
Конструктор класса file_header аллоцирует память под структуру.
Еще два метода отвечают за установку значений в поля для запуска и выбора пути.
Думаю с этим всё понятно. На классе error_code я не буду заострять внимание, потому что это просто класс для обработки ошибок. Это не особо касается нашей темы, Вы можете его не добавлять.
Добавляем параметры.
Окей, мы прочитали пути, проинициализировали структуры file_header_internal для этих файлов. Но мы то хотим еще как-то сказать, что мы бы хотели запустить их. Так еще и путь распаковки указать. Как сделать?)
Сначала давайте представим, как мы это реализуем. Как показывать пользователю информацию о текущем положении дел и как дать возможность редактировать эту информацию.
Выводить информацию о файлах я решил при помощи красивенькой таблицы. В этом нам поможет wprintf. Будет это выглядеть примерно так:
Первая колонка – индекс файла. Это порядок распаковки. Также служит для выбора файла, параметры которого Вы хотите изменить. Напишем функцию для вывода такой таблицы.
Ок, с выводом текущего положения дел разобрались
Теперь нужно дать возможность редактировать эти параметры. Я выдумал такой формат:
<file index> <param> <value>.
Давайте реализуем функцию, которая будет парсить эти параметры и выставлять значения. Тут идеально зашел бы scanf, но я что ли зря писал обработку ошибок???? Да и пользователь конечный может быть идиотом, так что минимальную защиту от дураков тоже надо сделать.
Принимает эта функция ввод пользователя, который мы будем парсить и ссылку на массив с file_header’ами. Как бы это странно не звучало, но она парсит параметры и устанавливает значения. Если что-то не так, выкинет ошибку. Думаю в пояснениях особо не нуждается. Разве что first--; предназначен для уменьшения индекса, так как мы в таблицу выводим индексы от 1, а в массивах от 0 считаются…
Давайте же эти две функции соберем вместе в мэйне!
Всё просто. Бесконечный цикл, который выводит таблицу, принимает команды пользователя и по команде run выходит. Если что-то пошло не так выводит ошибку в виде строки.
С заполнением данных покончено.
Сохраняем стаб.
Теперь нам нужно в конец стаба записать структуры и файлы, которые мы склеиваем. Выглядеть это будет так:
Не будем тянуть кота за яйца, давайте же напишем функцию для сохранения нового стаба.
В ней мы копируем стаб в новый файл packed.exe, аллоцируем chunk размером CHUNK_SIZE. Я выбрал 2048 байт. Ну и делаем всё, как по плану выше. Записываем структуру, за ней файл. И вот у нас появился packed.exe, в котором есть стаб и после него идут файлы.
На этом можно завершать работу нашего упаковщика. Осталось освободить память и выйти.
Глава 2. Распаковщик. Тело распаковщика.
Вес нашего распаковщика очень важен. Поэтому отключаем CRT и пишем на C с WinAPI. Разберемся, что мы должны сделать, чтобы распаковать все файлы. Мы знаем, что в конце нашего стаба лежат структуры и файлы. Нам нужно их получить. То есть нам нужно прочитать самого себя и распарсить всё, что мы там нашли.
Пишем стаб.
Для начала нужно понять, по какому смещению читать нужно. Где конец нашего стаба? Это вы не выясните до полной компиляции стаба. Поэтому пока что обозначим это таким образом:
В дебаг режиме у меня получался размер стаба 0x3400, в Release 4096 КБ.
Про singled_linked_list.h поясню сейчас. Там просто реализация односвязного списка, с дополнительной функцией, которая пробегается по всем нодам этого листа и запускает файлы через CreateProcessW. Односвязный список нам пригодится для того, чтобы туда добавлять пути файлов, которые необходимо запустить.
Итак, у нас есть смещение до конца файла. То есть мы уже можем начать читать наши структуры. Но давайте сначала их перенесем сюда.
Как вы можете заменить поле folder_to_unpack изменило свой тип на char. В комментарии я пояснил почему так. Также поле need_execute стало char. Это просто отсутствие типа bool в языке C.
Приступим к чтению.
Наша точка входа выглядит достаточно примитивно. Мы получили путь к самому себе, открыли HANDLE на чтение, переместили указатель на конец нашего стаба и начали читать.
Но остались две неизвестные функции: get_unpack_path и drop_file. В чём их смысл, думаю, ясно из названия. Давайте перейдем к их реализациям.
Выясняем путь для распаковки.
Как вы помните наша структура file_header_internal содержала поле типа char, которое позволит нам понять, куда нужно распаковать файл.
Мы должны проверить это значение и вернуть путь.
Делаем всё, как и планировали. Если что не так, то вышли.
Окей, эта функция нам вернула путь, куда нужно распаковать. Теперь нам нужно распаковать файл по этому пути. То есть перейти к реализации drop_file.
Распаковываем.
Я думаю, что комментариев в коде вам достаточно. Самое главное здесь это то, что большие файлы нужно записывать кусками, чтобы не выделять слишком много памяти. А так просто открыли файл по нужно пути и записали. Больше ничего не нужно в этой функции делать.
Запускаем, что нужно.
Мы хотели еще запускать какие-то файлы. Мы можем понять какие исходя из поля need_execute структуры file_header_internal.
В нашей основной функции entry мы записывали пути для запуска и освобождали те, которые не нужны.
После дропа всех файлов, односвязный список по имени paths_list содержит пути всех файлов, которые нужны запустить. Так давайте же их запустим.
Просто пробегаемся по односвязному списку и запускаем файлы. Закрываем хэндлы и освобождаем память. После этого можем уже завершать работу.
Мои последние слова.
На выходе стаб получился у меня 4КБ. 4096 байт. Если у вас другой вес, то измените end_of_file_offset.
Не ищете в этом коде какого-то сверх привата или каких-то сумасшедших техник. Я просто дал вам почву для размышлений, если её не хватало. Дальше эту базу Вы можете модифицировать, как Вам угодно. Можете добавить сжатие файлов/криптовку. Можете прикрутить запуск в памяти для бинарей, короче всё что считаете нужным.
Отмечу еще интересное поведение аверов. Если запаковать exe и отметить его для запуска и распаковывать в текущую папку или в %temp%, тогда всё норм. Если попробовать распаковать на рабочий стол, то детект от эмулятора скорее всего.
Пожалуйста, выдайте какой-нибудь фидбэк. Можете обосрать код, всё что угодно, но дайте какое-нибудь мнение. Любое мнение пойдет на пользу
Прикрекпил исходники. Пароль местный.
И еще пожалуй отмечу c0d3r_0f_shr0d13ng3r. Его гайд я использовал для отключения CRT. Единственное еще нужен файл crt_math.c для работы с long long числами без CRT. Он тоже есть в исходниках.
Всем спасибо за внимание!
Автор: D3buG
Написано для https://xss.pro
Оглавление:
- Глава 1. Упаковщик
- Теоретическая часть
- Читаем пути
- Сохраняем информацию о файле
- Добавляем параметры
- Сохраняем стаб
- Глава 2. Распаковщик
- Тело распаковщика
- Пишем стаб
- Выясняем путь для распаковки
- Распаковываем
- Запускаем, что нужно
- Мои последние слова
Давайте же разберёмся, как будет работать наш упаковщик. Чтобы разобраться, нужно понять, что мы получаем на вход и какой должен быть выход. Очевидно, что мы на вход получаем пути к файлам, которые нужно склеить, а на выход 1 файл, в котором всё склеено. Но! Мы, наверняка, хотим еще и запустить какой-то файл после распаковки. А может и не один. Так что этот момент тоже нужно учесть. Более того, мы хотим, чтобы путь распаковки не был фиксированным. А чтобы при упаковке можно было выбрать директорию, куда всё добро выкинуть.
На вход:
- Путь до файлов
- Хотим ли мы запускать его после дропа?
- Путь для дропа
После сбора всех данных, мы должны записать их в стаб и выкинуть упакованный файл.
Теоретическая часть для упаковщика на этом подошла к концу.
Читаем пути.
Давай же приступим к программированию. Как было сказано, нам нужно прочитать пути до файлов, которые мы желаем упаковать.
Так как в коде упаковщика мы не ограничены ресурсами, будем использовать C++ с некоторыми его прелестями.
C++:
int main() {
std::locale::global(locale("en_US.utf8"));
vector<wstring> file_paths;
vector<fh::file_header> file_headers;
while (true) {
wstring filename;
wcout << L"Enter file path or type \"run\": ";
getline(wcin, filename);
if (filename == L"run")
break;
file_paths.push_back(filename);
}
if (file_paths.size() == 0)
return 0;
wcout << endl;
}
Давайте разберем этот код. Создаем 2 вектора, в которых мы будем хранить пути до файлов и заголовки для этих файлов.
Просто читаем пути из stdin’a пока пользовать не введёт run. Вы можете реализовать это путем чтения параметров из argv, но я решил сделать так.
Детали fh::file_header мы разберёмся чуть позже, в пункте “Сохраняем информацию о файле”.
Если пользователь не ввёл ни одного пути, то мы завершаем работу, так как паковать нам нечего!
Сохраняем информацию о файле.
Пришло время заполнить тот самый vector<fh::file_header> file_headers.
C++:
for (size_t i = 0; i < file_paths.size(); ++i) {
fh::file_header fs(file_paths[i]);
if (!fs.m_error.isSuccess()) {
wcout << L"Error with " << file_paths[i] << endl << L"Error: " << fs.m_error.get_error_code_as_wstring() << endl;
continue;
}
file_headers.push_back(fs);
}
Кто такой этот ваш fh::file_header?? Это такой класс
C++:
#define MAX_FILE_NAME_LEN 45
#define MAX_PATH_VALUE 2
#include <string>
/* Некоторые перечисления и структуры, которые важны нашей программе */
enum class FOLDERS_TO_UNPACK : char
{
CURRENT_FOLDER = 0,
TEMP = 1,
DESKTOP = 2
/* добавьте другие пути, но не забудьте поменять MAX_PATH_VALUE*/
};
enum class ERROR_CODES : char
{
SUCCESS = 0, /* Всё хорошо */
FILE_NOT_FOUND = 1, /* Не найден файл, который пытаемся открыть */
BAD_UNPACK_PATH = 2, /* Выбран неверный/несуществующий путь для распаковки */
PARSE_ERROR = 3, /* Пользователь ввёл неверную команду */
FILENAME_TOO_BIG = 4, /* Имя файла превышает MAX_FILE_NAME_LEN */
INDEX_OUT_ARRAY = 5, /* Пользователь выбрал индекс вне массива */
UNDEFINED_PARAM = 6, /* Пользователь ввёл неизвестный параметр */
INVALID_VALUE = 7 /* Пользователь ввёл значение не подходящее к параметру */
};
/* Чтобы в файле её размер был без выравниваний */
#pragma pack(push, 1)
typedef struct _file_header_internal {
unsigned long long fileSize; // Размер файла
wchar_t filename[MAX_FILE_NAME_LEN]; // Буффер под имя файла. Он ограничен в 45
FOLDERS_TO_UNPACK folder_to_unpack; // Путь куда распаковать
bool need_execute; // Должны ли мы выполнить этот файл после распаковки всех файлов?
} file_header_internal, * pfile_header_internal;
#pragma pack(pop)
/* ----------------------------------------------------- */
/* Классы */
namespace fh {
class error_code {
private:
ERROR_CODES m_error;
public:
error_code();
ERROR_CODES get_error_code() const; // Получить последнюю ошибку
std::wstring get_error_code_as_wstring();
bool isSuccess() const;
void set_last_error(ERROR_CODES error);
};
class file_header {
private:
pfile_header_internal m_pFileHeader;
void fill_file_header(const std::wstring& file_path);
public:
file_header(const std::wstring& file_path); // Конструктор
error_code m_error;
const pfile_header_internal get_file_header() const; // Получить указатель на структуру file_header_internal
void set_unpack_path(FOLDERS_TO_UNPACK path);
void set_executable(bool isExecutable);
void free_mem();
~file_header();
};
}
Не будем заострять внимание на её полях, так как они прокомментированы. Обратим внимание на её размер. В данном случае он 100 байт. Размер её может поменяться, если Вы поменяете константу MAX_FILE_NAME_LEN или уберете #pragma pack(push, 1) с #pragma pack(pop). Эти директивы позволяют включить выравнивание по 1 байту. Если их не будет, то при текущей максимальной длине имени размер её будет 104 байта, а не 100. Из-за выравнивания! Но как Вы могли догадаться, её мы будем записывать в наш стаб, поэтому нам очень важна точность в размерах.
Обратим внимание на перечисление FOLDER_TO_UNPACK. Там просто перечислены пути, по которым есть возможность распаковаться. Так же обратите внимание на приписку : char после имени enum’a. Там мы показываем, что вес этого перечисления 1 байт! Без этого уточнения вес enum будет 4 байта. Я думаю, вам 256 значений хватит, чтобы хранить все ваши пути, поэтому сэкономим место.
Давайте же взглянем на реализацию методов класса file_header.
C++:
void file_header::fill_file_header(const std::wstring& file_path)
{
wstring file_name = file_path;
memset(m_pFileHeader, 0, sizeof(file_header_internal)); // Очистка структуры
HANDLE hFile = CreateFileW(file_path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE){
// Не удалось открыть
m_error.set_last_error(ERROR_CODES::FILE_NOT_FOUND);
return;
}
LARGE_INTEGER fsize;
GetFileSizeEx(hFile, &fsize);
// Записываем размер файла
m_pFileHeader->fileSize = fsize.QuadPart;
CloseHandle(hFile);
/* Ищем последний \ в пути */
while (file_name.find(L"\\") != std::wstring::npos) {
file_name = file_name.substr(file_name.find(L"\\"));
}
/* Проверяем длину имени файла */
if (file_name.size() >= MAX_FILE_NAME_LEN) {
m_error.set_last_error(ERROR_CODES::FILENAME_TOO_BIG);
return;
}
/* Копируем имя в поле структуры */
memcpy(m_pFileHeader->filename, (const void*)file_name.data(), file_name.size() * sizeof(wchar_t));
}
file_header::file_header(const std::wstring& file_path)
{
// Аллокация памяти
m_pFileHeader = new file_header_internal; // Не очень хочется на стэке хранить структуру, размер которой 100 байт
fill_file_header(file_path); // Заполняем поля
}
const pfile_header_internal file_header::get_file_header() const
{
return m_pFileHeader;
}
void file_header::set_unpack_path(FOLDERS_TO_UNPACK path)
{
m_pFileHeader->folder_to_unpack = path;
}
void file_header::set_executable(bool isExecutable)
{
m_pFileHeader->need_execute = isExecutable;
}
void file_header::free_mem()
{
delete m_pFileHeader;
}
Метод free_mem освободит память, аллоцированную под структуру file_header_internal.
Конструктор класса file_header аллоцирует память под структуру.
Еще два метода отвечают за установку значений в поля для запуска и выбора пути.
Думаю с этим всё понятно. На классе error_code я не буду заострять внимание, потому что это просто класс для обработки ошибок. Это не особо касается нашей темы, Вы можете его не добавлять.
Добавляем параметры.
Окей, мы прочитали пути, проинициализировали структуры file_header_internal для этих файлов. Но мы то хотим еще как-то сказать, что мы бы хотели запустить их. Так еще и путь распаковки указать. Как сделать?)
Сначала давайте представим, как мы это реализуем. Как показывать пользователю информацию о текущем положении дел и как дать возможность редактировать эту информацию.
Выводить информацию о файлах я решил при помощи красивенькой таблицы. В этом нам поможет wprintf. Будет это выглядеть примерно так:
Первая колонка – индекс файла. Это порядок распаковки. Также служит для выбора файла, параметры которого Вы хотите изменить. Напишем функцию для вывода такой таблицы.
C++:
void print_file_headers_in_table(const vector<fh::file_header>& file_headers) {
const wchar_t* table_header =
L"|----------------------------------------------------------------------------------------|\n"
L"| # | Filename | File size | Path (e) | Exec (e) |\n"
L"|----------------------------------------------------------------------------------------|\n";
const wchar_t* table_end = L"|----------------------------------------------------------------------------------------|";
wprintf(L"%s", table_header);
for (int i = 0; i < file_headers.size(); ++i) {
wprintf(L"|%-5d|%-45ls|%-12lld|%-11ls|%-11lc|\n",
i + 1,
file_headers[i].get_file_header()->filename,
file_headers[i].get_file_header()->fileSize,
path_to_str(file_headers[i].get_file_header()->folder_to_unpack),
(file_headers[i].get_file_header()->need_execute ? L'+' : L'-'));
}
wprintf(L"%s\n", table_end);
}
<file index> <param> <value>.
Давайте реализуем функцию, которая будет парсить эти параметры и выставлять значения. Тут идеально зашел бы scanf, но я что ли зря писал обработку ошибок???? Да и пользователь конечный может быть идиотом, так что минимальную защиту от дураков тоже надо сделать.
C++:
fh::error_code parse_user_input(const wstring& input, vector<fh::file_header>& file_headers) {
fh::error_code ecode;
ecode.set_last_error(ERROR_CODES::SUCCESS);
wstring buff = input;
int spaces_count = 0;
while (buff.find(L' ') != std::wstring::npos) {
buff = buff.substr(buff.find(L' ') + 1);
spaces_count++;
}
if (spaces_count != 2) {
ecode.set_last_error(ERROR_CODES::PARSE_ERROR);
return ecode;
}
unsigned int first = stoi(input.substr(0, input.find(L' ')));
wstring tmp = input.substr(input.find(L' ') + 1);
int second = stoi(tmp.substr(0, tmp.find(L' ')));
int value = stoi(tmp.substr(tmp.find(L' ')));
first--;
if (first >= file_headers.size()) {
ecode.set_last_error(ERROR_CODES::INDEX_OUT_ARRAY);
return ecode;
}
if (second != 1 && second != 2) {
ecode.set_last_error(ERROR_CODES::UNDEFINED_PARAM);
return ecode;
}
if (second == 1) {
if (value > MAX_PATH_VALUE || value < 0)
{
ecode.set_last_error(ERROR_CODES::INVALID_VALUE);
return ecode;
}
file_headers[first].set_unpack_path((FOLDERS_TO_UNPACK)value);
}
else {
if (value != 0 && value != 1) {
ecode.set_last_error(ERROR_CODES::INVALID_VALUE);
return ecode;
}
file_headers[first].set_executable(value);
}
return ecode;
}
Давайте же эти две функции соберем вместе в мэйне!
C++:
wstring input = L"";
while (true) {
system("cls");
print_file_headers_in_table(file_headers);
wcout << endl << L"Enter editable column and value or \"run\" (\"?\" and \"help\" are available): ";
getline(wcin, input);
if (input == L"help" || input == L"?") {
wcout << L"<file index> <param> <value>" << endl;
wcout << L"1 1 0 will select first file and set path to CURRENT_FOLDER" << endl;
wcout << L"1 1 1 will select first file and set path to TEMP" << endl;
wcout << L"1 1 2 will select first file and set path to Desktop" << endl;
wcout << L"1 2 0 will select first file and unset executable flag" << endl;
wcout << L"1 2 1 will select first file and set executable flag" << endl;
system("pause");
continue;
}
if (input == L"run")
break;
auto err = parse_user_input(input, file_headers);
if (!err.isSuccess()) {
wcout << L"Error!" << endl << err.get_error_code_as_wstring() << endl;
system("pause");
continue;
}
}
С заполнением данных покончено.
Сохраняем стаб.
Теперь нам нужно в конец стаба записать структуры и файлы, которые мы склеиваем. Выглядеть это будет так:
Не будем тянуть кота за яйца, давайте же напишем функцию для сохранения нового стаба.
C++:
void fill_stub(const vector<wstring>& file_paths, const vector<fh::file_header>& file_headers) {
if (file_headers.size() != file_paths.size())
return;
if (!CopyFileA("stub.bin", "packed.exe", TRUE))
{
DeleteFileA("packed.exe");
CopyFileA("stub.bin", "packed.exe", FALSE);
}
BYTE* chunk = new BYTE[CHUNK_SIZE];
HANDLE hStub = CreateFileA("packed.exe", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hStub == INVALID_HANDLE_VALUE)
{
delete[] chunk;
return;
}
int fsize;
fsize = GetFileSize(hStub, NULL); // инта хватит, у нас точно стаб пакера не будет больше макс значения инта. Поэтому и юзаем GetFileSize и SetFilePointer
SetFilePointer(hStub, fsize, NULL, FILE_BEGIN);
for (int i = 0; i < file_paths.size(); ++i) {
HANDLE hFile = CreateFileW(file_paths[i].c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
continue;
DWORD wrote = 0;
WriteFile(hStub, file_headers[i].get_file_header(), sizeof(file_header_internal), &wrote, NULL);
DWORD read = 1;
wrote = 0;
while (read) {
ReadFile(hFile, chunk, CHUNK_SIZE, &read, NULL);
if (read)
WriteFile(hStub, chunk, read, &wrote, NULL);
}
CloseHandle(hFile);
}
CloseHandle(hStub);
delete[] chunk;
}
На этом можно завершать работу нашего упаковщика. Осталось освободить память и выйти.
C++:
fill_stub(file_paths, file_headers);
for (int i = 0; i < file_headers.size(); ++i)
file_headers[i].free_mem();
return 0;
Глава 2. Распаковщик. Тело распаковщика.
Вес нашего распаковщика очень важен. Поэтому отключаем CRT и пишем на C с WinAPI. Разберемся, что мы должны сделать, чтобы распаковать все файлы. Мы знаем, что в конце нашего стаба лежат структуры и файлы. Нам нужно их получить. То есть нам нужно прочитать самого себя и распарсить всё, что мы там нашли.
Пишем стаб.
Для начала нужно понять, по какому смещению читать нужно. Где конец нашего стаба? Это вы не выясните до полной компиляции стаба. Поэтому пока что обозначим это таким образом:
C:
#include <Windows.h>
#include <shlobj.h>
#include "single_linked_list.h"
#define MAX_FILE_NAME_LEN 45
#define MAX_BUFFER_SIZE 1*1024*1024
#ifdef _DEBUG
#define end_of_file_offset 0x3400
#else
#define end_of_file_offset 0x1000
#endif
Про singled_linked_list.h поясню сейчас. Там просто реализация односвязного списка, с дополнительной функцией, которая пробегается по всем нодам этого листа и запускает файлы через CreateProcessW. Односвязный список нам пригодится для того, чтобы туда добавлять пути файлов, которые необходимо запустить.
Итак, у нас есть смещение до конца файла. То есть мы уже можем начать читать наши структуры. Но давайте сначала их перенесем сюда.
C:
typedef enum _FOLDERS_TO_UNPACK
{
CURRENT_FOLDER = 0,
TEMP = 1,
DESKTOP = 2
} FOLDERS_TO_UNPACK;
#pragma pack(push, 1)
typedef struct _file_header_internal {
unsigned long long fileSize;
WCHAR filename[MAX_FILE_NAME_LEN];
char folder_to_unpack; // Путь куда распаковать. Тут char. 1 байт. Мы ставили, чтобы размер этого enum был 1 байт!!!!
char need_execute;
} file_header_internal, * pfile_header_internal;
#pragma pack(pop)
Как вы можете заменить поле folder_to_unpack изменило свой тип на char. В комментарии я пояснил почему так. Также поле need_execute стало char. Это просто отсутствие типа bool в языке C.
Приступим к чтению.
C:
int entry() {
List* paths_list = makelist(); // лист, где будут пути для запуска
WCHAR exePath[MAX_PATH];
GetModuleFileNameW(GetModuleHandleW(NULL), exePath, MAX_PATH);
HANDLE hStub = CreateFileW(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hStub == INVALID_HANDLE_VALUE)
ExitProcess(0);
SetFilePointer(hStub, end_of_file_offset, NULL, FILE_BEGIN); // переместимся в конец нашего стаба. То есть в начало запакованных данных
DWORD read = 1;
while (read) {
file_header_internal fhi;
ReadFile(hStub, &fhi, sizeof(file_header_internal), &read, NULL);
if (!read)
break;
LPWSTR unpack_path = get_unpack_path(&fhi);
drop_file(&fhi, unpack_path, hStub);
if (fhi.need_execute)
add(unpack_path, paths_list);
else
VirtualFree(unpack_path, 0, MEM_FREE);
}
exec_paths(paths_list); // запустить после полной распаковки.
destroy(paths_list);
ExitProcess(0);
}
Наша точка входа выглядит достаточно примитивно. Мы получили путь к самому себе, открыли HANDLE на чтение, переместили указатель на конец нашего стаба и начали читать.
Но остались две неизвестные функции: get_unpack_path и drop_file. В чём их смысл, думаю, ясно из названия. Давайте перейдем к их реализациям.
Выясняем путь для распаковки.
Как вы помните наша структура file_header_internal содержала поле типа char, которое позволит нам понять, куда нужно распаковать файл.
Мы должны проверить это значение и вернуть путь.
C:
LPWSTR get_unpack_path(const pfile_header_internal header) {
FOLDERS_TO_UNPACK ftu = header->folder_to_unpack;
LPWSTR path = NULL;
switch (ftu)
{
case CURRENT_FOLDER:
path = VirtualAlloc(0, MAX_PATH * sizeof(WCHAR) + sizeof(header->filename), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (path) {
GetCurrentDirectoryW(MAX_PATH, path);
lstrcatW(path, L"\\");
lstrcatW(path, header->filename);
}
break;
case TEMP:
path = VirtualAlloc(0, sizeof(header->filename) + MAX_PATH + 1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (path) {
GetTempPathW(MAX_PATH + 1, path);
lstrcatW(path, header->filename);
}
break;
case DESKTOP:
path = VirtualAlloc(0, sizeof(header->filename) + MAX_PATH + 1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (path) {
SHGetFolderPathW(NULL, CSIDL_DESKTOP, NULL, 0, path);
lstrcatW(path, L"\\");
lstrcatW(path, header->filename);
}
break;
default:
path = NULL; // Можете сделать дроп по умолчанию куда-то. Но я решил выйти, потому что видимо структуру побило или ещё что-то с ней случилось
}
if (!path)
ExitProcess(0);
return path;
}
Окей, эта функция нам вернула путь, куда нужно распаковать. Теперь нам нужно распаковать файл по этому пути. То есть перейти к реализации drop_file.
Распаковываем.
C:
void drop_file(const pfile_header_internal header, LPCWSTR path_to_drop, HANDLE hStub) {
HANDLE hFile = CreateFileW(path_to_drop, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
ExitProcess(0);
// сейчас FilePointer находится на буфере. Мы знаем размер этого буфера из header.
// Нужно решить, как записывать его. Если размер слишком большой, то будем кусками читать и записывать.
// Для этого обозначим макс размер в памяти. Предположим, этот размер будет равен 1 мб. MAX_BUFFER_SIZE
if (header->fileSize <= MAX_BUFFER_SIZE) {
// можем позволить на весь буфер выделить память
LPVOID filebuffer = VirtualAlloc(0, header->fileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!filebuffer)
ExitProcess(0);
DWORD read = 0;
DWORD wrote = 0;
ReadFile(hStub, filebuffer, header->fileSize, &read, NULL); // прочитали
WriteFile(hFile, filebuffer, read, &wrote, NULL); // записали
CloseHandle(hFile); // закрыть не забыли
VirtualFree(filebuffer, 0, MEM_FREE); // Освободили
}
else {
// здесь же мы можем выделять буфер только размером в 1 МБ. Надо узнать сколько раз пробежимся и какой остаток будет
int chunks_count = header->fileSize / MAX_BUFFER_SIZE;
int ostatok = header->fileSize % MAX_BUFFER_SIZE; // это кол-во байт, которые останутся после (chunks_count * MAX_BUFFER_SIZE)
LPVOID filebuffer = VirtualAlloc(0, MAX_BUFFER_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!filebuffer)
ExitProcess(0);
DWORD read, wrote;
read = wrote = 0;
for (int i = 0; i < chunks_count; ++i) {
ReadFile(hStub, filebuffer, MAX_BUFFER_SIZE, &read, NULL); // прочитали
WriteFile(hFile, filebuffer, read, &wrote, NULL); // записали
}
// Пора остаток обработать
ReadFile(hStub, filebuffer, ostatok, &read, NULL); // прочитали
WriteFile(hFile, filebuffer, read, &wrote, NULL); // записали
CloseHandle(hFile); // закрыть не забыли
VirtualFree(filebuffer, 0, MEM_FREE); // Освободили
}
// После выполнения всех операций FilePointer находится в начале новой структуры-заголовка. Можем продолжать цикл из точки входа.
}
Запускаем, что нужно.
Мы хотели еще запускать какие-то файлы. Мы можем понять какие исходя из поля need_execute структуры file_header_internal.
В нашей основной функции entry мы записывали пути для запуска и освобождали те, которые не нужны.
C:
if (fhi.need_execute)
add(unpack_path, paths_list);
else
VirtualFree(unpack_path, 0, MEM_FREE);
C:
void exec_paths(List* list) {
Node* current = list->head;
if (list->head == NULL)
return;
for (; current != NULL; current = current->next) {
LPWSTR cur_dir = VirtualAlloc(0, lstrlenW(current->path_ptr) * sizeof(WCHAR), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
lstrcpyW(cur_dir, current->path_ptr);
for (int i = lstrlenW(current->path_ptr) - 1; i >= 0; --i)
if (cur_dir[i] == L'\\') {
cur_dir[i] = 0;
break;
}
STARTUPINFOW si;
RtlSecureZeroMemory(&si, sizeof(STARTUPINFOW)); // желательно вам подтянуть memset из ntdll. Но мне лень
si.cb = sizeof(STARTUPINFOW);
PROCESS_INFORMATION pi;
CreateProcessW(current->path_ptr, NULL, NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS, NULL, cur_dir, &si, &pi);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
VirtualFree(cur_dir, 0, MEM_FREE);
}
}
Просто пробегаемся по односвязному списку и запускаем файлы. Закрываем хэндлы и освобождаем память. После этого можем уже завершать работу.
Мои последние слова.
На выходе стаб получился у меня 4КБ. 4096 байт. Если у вас другой вес, то измените end_of_file_offset.
Не ищете в этом коде какого-то сверх привата или каких-то сумасшедших техник. Я просто дал вам почву для размышлений, если её не хватало. Дальше эту базу Вы можете модифицировать, как Вам угодно. Можете добавить сжатие файлов/криптовку. Можете прикрутить запуск в памяти для бинарей, короче всё что считаете нужным.
Отмечу еще интересное поведение аверов. Если запаковать exe и отметить его для запуска и распаковывать в текущую папку или в %temp%, тогда всё норм. Если попробовать распаковать на рабочий стол, то детект от эмулятора скорее всего.
Пожалуйста, выдайте какой-нибудь фидбэк. Можете обосрать код, всё что угодно, но дайте какое-нибудь мнение. Любое мнение пойдет на пользу
Прикрекпил исходники. Пароль местный.
И еще пожалуй отмечу c0d3r_0f_shr0d13ng3r. Его гайд я использовал для отключения CRT. Единственное еще нужен файл crt_math.c для работы с long long числами без CRT. Он тоже есть в исходниках.
Всем спасибо за внимание!
Автор: D3buG
Написано для https://xss.pro
Вложения
Последнее редактирование модератором: