У меня есть 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;
}
Невозможно воспроизвести с помощью Обновление ресурсов. Может ли имя вашего исполняемого файла быть загружено LoadLibraryEx
? Почему ваш исполняемый файл пытался открыть PyInstaller
?
В некоторых случаях Pyinstaller
открывается при запуске исполняемого файла (например, при отсутствии импорта). Я не уверен, что исполняемый файл предназначен для открытия с помощью LoadLibraryEx
(когда я открываю исполняемый файл с помощью LoadLibrary
, я получаю сообщение об ошибке ERROR_BAD_EXE_FORMAT: %1 не является допустимым приложением Win32)
В примере кода в Обновление ресурсов они используют LoadLibrary, но поскольку я загружаю исполняемый файл, я использую LoadLibraryEx с флагом LOAD_LIBRARY_AS_DATAFILE. Я предполагаю, что тот факт, что ему удается изменить ресурс, означает, что он действительно загружает исполняемый файл.
Я подозревал, что это был большой размер исполняемого файла (75 МБ), поэтому я попробовал его с Microsoft Team (100 МБ), и все прошло нормально. Я также взял еще один exe-файл меньшего размера (15 МБ), который также был сделан с помощью Pyinstaller
- и он также был сжат (до 270 КБ) и поврежден. Так что на данный момент подозрение падает на Pyinstaller
. Есть ли известная проблема с Pyinstaller
?
НЕТ. BeginUpdateResource не загружается, а открывается. Принимая во внимание ERROR_BAD_EXE_FORMAT
, соответствует ли архитектура вашего приложения, которое вызывает LoadLibraryEx
, архитектуре the executable
для загрузки? Может ли Resource Hacker модифицировать безопасно? Может быть, вам нужно обратиться в поддержку Pyinstaller
?
Хакер ресурсов каким-то образом безопасно модифицирует ресурс. Использует ли он для этого другой метод?
Я добавил минимальные шаги воспроизведения
Я не могу найти контакт поддержки Pyinstaller. Кто-нибудь знает, к кому можно обратиться по этой проблеме?
Фактический исполняемый файл 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 В этом нет ничего нового. Поскольку, насколько я помню, в 90-х годах таким образом генерировались самораспаковывающиеся установщики - подумайте, например. WinZIP, WinRAR и т. д. Я не пробовал, но подозреваю, что такие программы, как 7zip, все еще работают таким образом. Если вы попытаетесь отредактировать исполняемые файлы таким же образом, произойдет то же самое, что и в случае с PyInstaller. Как ни странно, signtool работает таким образом, что может обрабатывать эти дополнительные данные — я только что попытался подписать файл, сгенерированный PyInstaller, и это сработало, а изменение неисполняемой части (шестнадцатеричный редактор) привело к недопустимой подписи.
Ух ты! Я впечатлен тем, как вы нашли причину этой проблемы! Я связался с создателем Resource Hacker, и он сказал, что модифицировал ресурс, вручную пересобрав раздел ресурсов (но точно не помнит, как он это сделал). Означает ли это, что нет способа (кроме изменения всего раздела ресурсов) изменить исполняемый файл Pyinstaller?
@Roey_MR Что вы имеете в виду под «изменением всего раздела ресурсов»? Опция '-r
для PyInstaller будет копировать любые ресурсы из указанного файла (также может быть другим .exe
), но, насколько я помню из исходного кода, она не удаляет существующие ресурсы, а просто добавляет/обновляет новые те. Так что это должно работать для вашего варианта использования, если я что-то не упустил и/или не забыл?
Перечитывая ваш вопрос, я теперь понимаю, что вы хотите сделать: у вас уже есть файл 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
байтов в исходном файле) в измененный файл.По крайней мере, с тривиальным скриптом hello world Python это работает для меня, и оно должно работать в общем случае (на самом деле с любым самораспаковывающимся исполняемым файлом PE, а не только с теми, которые сгенерированы PyInstaller), но ваш пробег может отличаться.
Вероятно, вы можете оптимизировать это немного больше (в настоящее время я читаю все данные в память, прежде чем записывать их снова), но код должен иллюстрировать принцип.
Отказ от ответственности: делайте это только в том случае, если вы не можете самостоятельно воссоздать файл PyInstaller. Если вы все равно создаете файл установщика, разработчики PyInstaller уже добавили необходимые функции для добавления ресурсов в процессе сборки. (Подробности см. в моем другом ответе на этот вопрос.)
Удивительный!! это работает как по волшебству! Большое спасибо за вашу самоотверженность и помощь! Я ищу эквивалент С++ для этого. Знаете ли вы о библиотеке C++, которую я могу использовать для загрузки PE-файла и извлечения размера исполняемого файла (например, модуль pefile в python)?
Может быть, PE-файл, который создает PyInstaller, не на 100% правильный или не соответствует спецификации?