Извлечение встроенных файлов из скомпилированного бинарника Deno
Содержание
Как изменить исполняемый файл Deno 2.7.0 для Windows, чтобы извлечь встроенные файлы VFS, и что этот способ не восстанавливает. Механизм самораспаковки (self-extraction) появился в Deno 2.7.0 (25 февраля 2026) с добавлением флага В более ранних версиях нет используемого здесь флага Deno — среда выполнения JavaScript и TypeScript от создателя Node.js Райана Дала. Команда На выходе — один Пользователь может запустить бинарник без установки Deno. Разберём, как в нём хранятся встроенные файлы и как извлечь содержимое VFS. Добавляем JSON-поле в метаданные бинарника и используем встроенный в Deno механизм самораспаковки для записи файлов VFS на диск. Это не восстановление всего исходного репозитория и не извлечение всех отдельных хранилищ модулей. Deno встраивает данные в PE-ресурс с именем При запуске скомпилированного бинарника Deno находит секцию Исполняемый файл отображён в адресное пространство процесса. В показанном ниже пути чтения VFS Этот путь чтения VFS не записывает встроенные файлы на диск. Это не утверждение о других частях рантайма или самом приложении: они могут выполнять дисковый ввод-вывод, а обращение к отображённым страницам тоже может потребовать чтения с диска. Если в метаданных есть поле Функция рекурсивно обходит VFS и записывает её файлы на диск, если маркер Директория распаковки зависит от версии рантайма, исполняемого файла, платформы, окружения и доступных для записи мест. Этот режим изначально создан для нативных модулей (FFI, Самораспаковка — не пассивная распаковка: запуск пропатченного бинарника также выполняет приложение. Для недоверенных бинарников используйте одноразовую изолированную среду без секретов и доступа к сети. Не запускайте их на своей рабочей машине. Добавляем Вставка байтов в метаданные без корректировки длин и смещений может повредить бинарник. Нужно учесть структуру данных и размер файла: Используем Получаем три значения: Зная смещение секции, читаем начальную сигнатуру (magic), длину и JSON: Если длина JSON меняется, сдвигаем следующие за ним байты. Используем Обновляем записанный размер ресурса. Код патчера: GitHub Gist Единственная внешняя зависимость. Всё остальное — стандартная библиотека Python. Изменяем метаданные, чтобы включить встроенную в Deno процедуру извлечения VFS: Этот способ извлекает встроенные файлы VFS, а не весь исходный репозиторий или все хранилища модулей. Здесь он ограничен Windows-бинарниками Deno 2.7.0; запуск изменённого бинарника также запускает приложение.--self-extracting для deno compile. Разбор ниже ограничен Deno v2.7.0 и Windows-бинарниками (PE). Python-скрипт использует pefile: это не экстрактор ELF/Mach-O, а совместимость с более поздними версиями Deno не проверена.--self-extracting, поэтому этот способ к ним неприменим. Для извлечения встроенных данных нужен отдельный подход, например разбор соответствующего бинарного формата. Это не означает, что такие рантаймы вообще никогда не записывают файлы на диск.Что такое
deno compiledeno compile собирает проект в единый самодостаточный исполняемый файл:deno compile --allow-net --allow-read src/main.ts.exe (Windows), ELF (Linux) или Mach-O (macOS), содержащий:Структура автономного исполняемого файла
Секция
d3n0l4ndD3N0L4ND (Windows) или в секцию ELF/Mach-O с именем d3n0l4nd. Встроенные данные расположены последовательно. На схеме отмечены поля длины типа u64:d3n0l4nd ← 8 байт, opening magic
┌──────────────────────────────────┐
│ u64: metadata_len │
│ metadata JSON │ ← entrypoint, permissions, настройки
├──────────────────────────────────┤
│ u64: npm_snapshot_len │
│ npm snapshot blob │ ← граф npm-зависимостей
├──────────────────────────────────┤
│ specifiers store │ ← URL всех модулей → ID
├──────────────────────────────────┤
│ redirects store │ ← перенаправления модулей
├──────────────────────────────────┤
│ remote modules store │ ← исходники HTTP-импортов
├──────────────────────────────────┤
│ u64: vfs_headers_len │
│ VFS headers JSON │ ← дерево каталогов с offset/size
├──────────────────────────────────┤
│ u64: vfs_file_data_len │
│ VFS file data blob │ ← байты всех встроенных файлов
└──────────────────────────────────┘
d3n0l4nd ← 8 байт, closing magicДва режима запуска
d3n0l4nd и десериализует содержимое. Дальше возможны два пути.Режим по умолчанию — чтение встроенной VFS
fn find_section() -> Result<&'static [u8], AnyError> {
match libsui::find_section("d3n0l4nd") {
Ok(Some(data)) => Ok(data), // прямой указатель в память процесса
...
}
}FileBackedVfs вычисляет смещение и возвращает срез встроенных байтов, а не открывает отдельный файл:let start = self.start_file_offset + file.offset;
let end = start + file.len;
Ok(Cow::Borrowed(&self.data[start..end]))
// Никакого дискового IO — просто индекс в массив байтРежим самораспаковки — распаковка на диск
"self_extracting", рантайм при запуске вызывает extract_vfs_to_disk():let root_path = if let Some(hash) = &metadata.self_extracting {
let dir = choose_and_create_extraction_dir(hash)?;
dir
} else {
std::env::temp_dir().join(format!("deno-compile-{}", current_exe_name))
};.done не заставит пропустить распаковку. Отдельные хранилища модулей в этот обход не входят:pub fn extract_vfs_to_disk(vfs: &FileBackedVfs, extraction_dir: &Path) -> Result<()> {
// Если маркер .done существует — распаковка уже была, пропускаем
let done_marker = extraction_dir.join(".done");
if done_marker.exists() {
return Ok(());
}
// Рекурсивно пишем ВСЕ файлы из VFS на диск
extract_vfs_dir(vfs, vfs.root_dir(), &mut extraction_dir.to_path_buf())?;
// Маркер: при следующем запуске распаковка не повторится
std::fs::File::create(&done_marker)?;
Ok(())
}taskflow — пример имени приложения, а не универсальная директория Deno. Чтобы определить точный путь, проверьте choose_and_create_extraction_dir и вызывающий её код в исходниках нужной версии рантайма, а затем подтвердите расположение в изолированной среде..node-аддоны), которые невозможно загрузить прямо из памяти.Патчинг: включаем self-extraction в готовом бинарнике
"self_extracting": "<hash>" в JSON метаданных внутри бинарника. При следующем запуске Deno распакует файлы VFS на диск встроенной процедурой, если маркер .done ещё не существует.Что нужно учесть при изменении бинарника
u64 с точным размером в байтах. Добавление поля увеличивает JSON на ~37 байт → значение u64 перестаёт совпадать.Size. В PE-заголовке записан размер ресурса D3N0L4ND. Вставка байт ломает и его.Алгоритм по шагам
Шаг 1. Найти ресурс
D3N0L4ND в PEpefile в режиме fast_load, затем явно разбираем каталог ресурсов:def locate_resource(exe_path: str) -> ResourceLocation:
pe = pefile.PE(exe_path, fast_load=True)
pe.parse_data_directories(
directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]]
)
for rsrc_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
for rsrc_id in rsrc_type.directory.entries:
name = rsrc_id.name.string.upper() if rsrc_id.name else b""
if name == b"D3N0L4ND":
lang = rsrc_id.directory.entries[0]
rva = lang.data.struct.OffsetToData
return ResourceLocation(
file_offset=pe.get_offset_from_rva(rva),
size=lang.data.struct.Size,
struct_file_offset=lang.data.struct.get_file_offset(),
)file_offset — где в файле начинается секция d3n0l4ndsize — текущий размер секцииstruct_file_offset — смещение структуры IMAGE_RESOURCE_DATA_ENTRY в файле; её поле Size обновляем на шаге 6Шаг 2. Прочитать JSON метаданных
def read_metadata(f, section_offset: int) -> MetadataInfo:
f.seek(section_offset)
if f.read(8) != MAGIC:
raise RuntimeError("Bad opening magic")
length = struct.unpack("<Q", f.read(8))[0]
json_offset = section_offset + 16 # 8 (magic) + 8 (u64)
raw = f.read(length)
return MetadataInfo(
json_file_offset=json_offset,
original_bytes=raw,
parsed=json.loads(raw),
)Шаг 3. Модифицировать JSON
meta.parsed["self_extracting"] = hash_value
new_bytes = json.dumps(meta.parsed, separators=(",", ":")).encode()
diff = len(new_bytes) - len(meta.original_bytes)
# diff ≈ +37 байтШаг 4. Сдвинуть хвост файла
mmap, чтобы не создавать отдельную копию всего файла:def shift_bytes(f, *, start: int, amount: int) -> None:
if amount == 0:
return
f.seek(0, 2)
old_size = f.tell()
tail = old_size - start
if amount > 0:
# Расширяем файл, затем сдвигаем блок
f.truncate(old_size + amount)
f.flush()
with mmap.mmap(f.fileno(), 0) as mm:
mm.move(start + amount, start, tail)
else:
# Сжимаем: сдвигаем, затем обрезаем
with mmap.mmap(f.fileno(), 0) as mm:
mm.move(start + amount, start, tail)
f.truncate(old_size + amount)
f.flush()mmap.move() — обёртка над memmove. Корректно обрабатывает перекрывающиеся регионы памяти. Отображение файла в память позволяет обойтись без отдельного буфера Python для всего файла, но перемещение байтов всё равно обращается к страницам памяти и может вызвать дисковый ввод-вывод.Шаг 5. Записать новые метаданные
f.seek(meta.json_file_offset - 8)
f.write(struct.pack("<Q", len(new_bytes)))
f.write(new_bytes)Шаг 6. Обновить размер PE-ресурса
IMAGE_RESOURCE_DATA_ENTRY.Size — u32 по смещению +4 от начала структуры:def patch_resource_size(path: str, struct_offset: int, new_size: int) -> None:
with open(path, "r+b") as f:
f.seek(struct_offset + 4)
f.write(struct.pack("<I", new_size))Полный код
Установка зависимостей
pip install pefileИтог
d3n0l4nd"self_extracting": "<hash>" в этот JSONmmap.move()u64) и размер PE-ресурса (u32)extract_vfs_to_disk() → встроенные файлы VFS на диске, если распаковка не пропущена из-за уже существующего .done