UpdateResource повреждает исполняемый файл

У меня есть EXE-файл, сгенерированный из скрипта Python с помощью PyInstaller, и я добавил текстовый файл в качестве ресурса в EXE-файл с помощью Resource Hacker. Я пытаюсь программно изменить текстовый файл ресурса с помощью функции WinAPI UpdateResource. Модификация проходит успешно, но вызывает повреждение EXE-файла. После модификации размер EXE-файла уменьшается с 75 МБ до 271 КБ, и я больше не могу его запускать. Когда я пытаюсь запустить поврежденный EXE-файл, я получаю сообщение об ошибке «Не удается открыть PyInstaller из исполняемого файла».

Я пытался реализовать модификацию, используя как Python, так и C++, но ни один из них не работает. Вот код Python, который я использовал:

Код Python:

exe_file = r"\path\to\exe_file.exe"
handle = win32api.BeginUpdateResource(exe_file, False)
resource_type = win32con.RT_RCDATA
new_data = "modified text".encode()
try:
    win32api.UpdateResource(handle, resource_type, 110, new_data)

    # Save the changes and close the file
    win32api.EndUpdateResource(handle, False)
except:
    win32api.EndUpdateResource(handle, True)

Это код C++ (я обрабатывал в своем реальном коде случаи, когда дескрипторы недействительны, но если я поместил все это здесь, код стал намного длиннее):

#include <windows.h>
#include <winbase.h>
#include <winuser.h>


int modify_resource(const LPCWSTR new_text, const LPCWSTR filename)
{
    const LPCWSTR res_type = MAKEINTRESOURCE(RT_RCDATA);
    const LPCWSTR res_name = MAKEINTRESOURCE(110); // resource ID

    HMODULE module = LoadLibraryEx(filename, NULL, LOAD_LIBRARY_AS_DATAFILE);
    HRSRC res_info = FindResource(module, res_name, res_type);

    DWORD res_size = SizeofResource(module, res_info);
    HGLOBAL res_data = LoadResource(module, res_info);

    LPVOID res_ptr = LockResource(res_data);
    FreeResource(res_data);
    HANDLE update_handle = BeginUpdateResource(filename, FALSE);

    BOOL result = UpdateResource(update_handle, res_type, res_name, MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), (LPVOID)new_text, (DWORD)(wcslen(new_text) + 1) * sizeof(wchar_t));
    if (!result)
    {
        EndUpdateResource(update_handle, TRUE);
        FreeLibrary(module);
        return 1;
    }

    result = EndUpdateResource(update_handle, FALSE);
    FreeLibrary(module);
    if (!result)
    {
        return 1;
    }
    return 0;
}

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

Обновлено: Вот минимальные шаги воспроизведения: конвертируйте следующую программу Python в exe:

input("Press enter to exit")

С помощью команды pyinstaller: pyinstaller --onefile --console <script_name.py>

и запустите следующий код С++:

# include <windows.h>
# define RES_ID 110

int main();
{
    const LPCWSTR res_type = MAKEINTRESOURCE(RT_RCDATA);
    const LPCWSTR res_name = MAKEINTRESOURCE(RES_ID);
    HMODULE module = LoadLibrary(L"exe\\to\\copy\\resource\\from.exe");
    HRSRC res_info = FindResource(module, res_name, res_type);
    HGLOBAL res_load = LoadResource(module, res_info);
    LPVOID res_ptr = LockResource(res_load);

    HANDLE update_handle = BeginUpdateResource(L"path\\to\\python\\exe.exe", FALSE);
    DWORD res_size = SizeofResource(module, res_info);

    BOOL result = UpdateResource(update_handle,
        res_type,
        res_name,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),
        res_ptr,
        res_size);

    EndUpdateResource(update_handle, FALSE);
    FreeLibrary(module);
    return 0;
}

Может быть, PE-файл, который создает PyInstaller, не на 100% правильный или не соответствует спецификации?

Jonathan Potter 16.05.2023 12:37

Невозможно воспроизвести с помощью Обновление ресурсов. Может ли имя вашего исполняемого файла быть загружено LoadLibraryEx? Почему ваш исполняемый файл пытался открыть PyInstaller?

YangXiaoPo-MSFT 17.05.2023 07:57

В некоторых случаях Pyinstaller открывается при запуске исполняемого файла (например, при отсутствии импорта). Я не уверен, что исполняемый файл предназначен для открытия с помощью LoadLibraryEx (когда я открываю исполняемый файл с помощью LoadLibrary, я получаю сообщение об ошибке ERROR_BAD_EXE_FORMAT: %1 не является допустимым приложением Win32)

Roey_MR 18.05.2023 09:47

В примере кода в Обновление ресурсов они используют LoadLibrary, но поскольку я загружаю исполняемый файл, я использую LoadLibraryEx с флагом LOAD_LIBRARY_AS_DATAFILE. Я предполагаю, что тот факт, что ему удается изменить ресурс, означает, что он действительно загружает исполняемый файл.

Roey_MR 18.05.2023 10:22

Я подозревал, что это был большой размер исполняемого файла (75 МБ), поэтому я попробовал его с Microsoft Team (100 МБ), и все прошло нормально. Я также взял еще один exe-файл меньшего размера (15 МБ), который также был сделан с помощью Pyinstaller - и он также был сжат (до 270 КБ) и поврежден. Так что на данный момент подозрение падает на Pyinstaller. Есть ли известная проблема с Pyinstaller?

Roey_MR 18.05.2023 22:03

НЕТ. BeginUpdateResource не загружается, а открывается. Принимая во внимание ERROR_BAD_EXE_FORMAT, соответствует ли архитектура вашего приложения, которое вызывает LoadLibraryEx, архитектуре the executable для загрузки? Может ли Resource Hacker модифицировать безопасно? Может быть, вам нужно обратиться в поддержку Pyinstaller?

YangXiaoPo-MSFT 19.05.2023 03:58

Хакер ресурсов каким-то образом безопасно модифицирует ресурс. Использует ли он для этого другой метод?

Roey_MR 19.05.2023 15:32

Я добавил минимальные шаги воспроизведения

Roey_MR 19.05.2023 19:13

Я не могу найти контакт поддержки Pyinstaller. Кто-нибудь знает, к кому можно обратиться по этой проблеме?

Roey_MR 20.05.2023 14:46
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
9
167
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Фактический исполняемый файл PyInstaller имеет размер менее 300 КБ. Он присутствует в репозитории Git и находится в подкаталоге PyInstaller/bootloader/[операционная система].

Что делает PyInstaller при создании установщика, так это то, что он выполняет всевозможные сложные действия, чтобы выяснить, что нужно упаковать, создает архив этих файлов и добавляет архив к самому исполняемому файлу.

При вызове исполняемого файла он распаковывает архив, который является частью собственного исполняемого файла, во временный каталог, а затем запускает фактическую реализацию PyInstaller (которая сама написана на Python) из этого временного каталога. Сам исполняемый файл называется загрузчиком людьми PyInstaller. Что именно делает загрузчик, объясняется в соответствующей документации PyInstaller.

Если теперь вы возьмете сгенерированный таким образом исполняемый файл и воспользуетесь стандартными функциями Windows API для его редактирования, эти функции будут видеть только начальную загрузочную часть исполняемого файла, а не архив, который был добавлен. И по этой причине, когда они перезаписывают исполняемый файл обратно на диск, они перезаписывают только исполняемую часть, а не архив.

Поскольку архив содержит как код PyInstaller, так и код, который должен был быть упакован, а загрузчик — это просто очень простая программа (преднамеренно), загрузчик выйдет из строя, потому что он не может найти ничего общего с самим собой.

Чтобы изменить ресурсы в файле PyInstaller, вам нужно будет добавить эти ресурсы в процессе сборки PyInstaller, прежде чем сборщик добавит архив в загрузчик.

К счастью для вас, люди из PyInstaller уже подумали об этом и добавили параметр командной строки -r в установщик, чтобы вы могли легко использовать

pyinstaller -r file_with_resources.res your_script.py

вместо того, чтобы просто

pyinstaller your_script.py

Это автоматически добавит ресурсы из указанного файла ресурсов в сгенерированный установщик и должно достичь того, что вы хотите.

Честно говоря, я не мог в это поверить, поэтому пролистал исходники PyInstaller. И, кажется, вы правы: PyInstaller генерирует недопустимые PE-образы, и при использовании официальных вызовов системного API все просто ломается. Это ужасно, как ни крути. Интересно, как бы вы подписали EXE-файл, сгенерированный PyInstaller, учитывая, что нет возможности встроить сертификат, не нарушая предположений загрузчика. Я все еще очень надеюсь, что просто неверно истолковал код, который видел.

IInspectable 24.05.2023 10:52

@Iinspectable В этом нет ничего нового. Поскольку, насколько я помню, в 90-х годах таким образом генерировались самораспаковывающиеся установщики - подумайте, например. WinZIP, WinRAR и т. д. Я не пробовал, но подозреваю, что такие программы, как 7zip, все еще работают таким образом. Если вы попытаетесь отредактировать исполняемые файлы таким же образом, произойдет то же самое, что и в случае с PyInstaller. Как ни странно, signtool работает таким образом, что может обрабатывать эти дополнительные данные — я только что попытался подписать файл, сгенерированный PyInstaller, и это сработало, а изменение неисполняемой части (шестнадцатеричный редактор) привело к недопустимой подписи.

chris_se 24.05.2023 17:16

Ух ты! Я впечатлен тем, как вы нашли причину этой проблемы! Я связался с создателем Resource Hacker, и он сказал, что модифицировал ресурс, вручную пересобрав раздел ресурсов (но точно не помнит, как он это сделал). Означает ли это, что нет способа (кроме изменения всего раздела ресурсов) изменить исполняемый файл Pyinstaller?

Roey_MR 26.05.2023 13:00

@Roey_MR Что вы имеете в виду под «изменением всего раздела ресурсов»? Опция '-r для PyInstaller будет копировать любые ресурсы из указанного файла (также может быть другим .exe), но, насколько я помню из исходного кода, она не удаляет существующие ресурсы, а просто добавляет/обновляет новые те. Так что это должно работать для вашего варианта использования, если я что-то не упустил и/или не забыл?

chris_se 26.05.2023 13:09
Ответ принят как подходящий

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

Это должно быть возможно, если сначала отделить чистую PE-часть исполняемого файла от остальной части (то есть архива, который был добавлен к нему), затем изменить чистую PE-часть, а затем повторно добавить остальные.

Я написал небольшую функцию Python, которая сделает именно это:

import os, time
import pefile
import win32api, win32con
from PyInstaller.utils.win32 import winutils

def modify_pyinstaller(exe_file):
    if exe_file.endswith('.exe'):
        modified_name = exe_file[:-4] + '_modified.exe'
    else:
        modified_name = exe_file + '_modified.exe'

    size = 0
    with pefile.PE(exe_file, fast_load=True) as pe:
        size = pe.OPTIONAL_HEADER.SizeOfHeaders
        for section in pe.sections:
            size += section.SizeOfRawData

    # Copy just the initial .exe part
    with open(exe_file, 'rb') as fin, open(modified_name, 'wb') as fout:
        fout.write(fin.read(size))

    # This part of the code is from what you wanted to do with
    # the executable...
    handle = win32api.BeginUpdateResource(modified_name, False)
    try:
        win32api.UpdateResource(handle, win32con.RT_RCDATA, 110, "modified text".encode())
        win32api.EndUpdateResource(handle, False)
    except:
        win32api.EndUpdateResource(handle, True)
        raise
    # End of the part that should be modified...

    # Now that we've modified the exe, copy the archive back (the rest of the original file)
    # (note the 'ab' mode to append data)
    with open(exe_file, 'rb') as fin, open(modified_name, 'ab') as fout:
        fin.seek(size, os.SEEK_SET)
        fout.write(fin.read())

    # Now that we're done also keep virus scanners happy and update
    # the timestamps and PE checksums. Re-use PyInstaller's
    # implementation, because they already figured this out.
    # (Note that since we're now using PyInstaller's functionality
    # here, this will technically make this code GPL-2+, because
    # that's the license of PyInstaller!)
    winutils.set_exe_build_timestamp(modified_name, int(time.time()))
    winutils.update_exe_pe_checksum(modified_name)

Общая логика следующая:

  • Он использует пакет pefile для извлечения размера исполняемой части файла без встроенного архива (сохраняет его в size)
  • Затем он создает копию исходного файла, но только первые size байт (чтобы скопировать только исполняемый файл)
  • Затем он выполняет изменения, которые вы хотите сделать (пожалуйста, замените эту часть кода)
  • Затем он добавляет остальную часть содержимого исходного файла (начиная со смещения size байтов в исходном файле) в измененный файл.
  • Поскольку PyInstaller также делает это, он обновляет временную метку и контрольную сумму PE вновь созданного файла (чтобы сканеры вирусов были довольны). (технически вы можете пропустить эту часть, если вам нужен только работающий .exe)
  • Вам вообще не обязательно разбираться в каком формате архив, достаточно просто скопировать данные

По крайней мере, с тривиальным скриптом hello world Python это работает для меня, и оно должно работать в общем случае (на самом деле с любым самораспаковывающимся исполняемым файлом PE, а не только с теми, которые сгенерированы PyInstaller), но ваш пробег может отличаться.

Вероятно, вы можете оптимизировать это немного больше (в настоящее время я читаю все данные в память, прежде чем записывать их снова), но код должен иллюстрировать принцип.

Отказ от ответственности: делайте это только в том случае, если вы не можете самостоятельно воссоздать файл PyInstaller. Если вы все равно создаете файл установщика, разработчики PyInstaller уже добавили необходимые функции для добавления ресурсов в процессе сборки. (Подробности см. в моем другом ответе на этот вопрос.)

Удивительный!! это работает как по волшебству! Большое спасибо за вашу самоотверженность и помощь! Я ищу эквивалент С++ для этого. Знаете ли вы о библиотеке C++, которую я могу использовать для загрузки PE-файла и извлечения размера исполняемого файла (например, модуль pefile в python)?

Roey_MR 27.05.2023 20:41

Другие вопросы по теме