Дисклеймер: Статья носит исключительно исследовательский характер и посвящена анализу архитектуры сервисного ПО с помощью LLM. Автор не призывает к взлому, пиратству или нарушению EULA. Вмешательство в работу сервисных утилит и изменение параметров принтера может привести к его поломке и потере гарантии. Автор не несет ответственности за любые последствия, возникшие в результате повторения описанных действий.
У мамы долгое время был струйный принтер epson xp-332. Он переоборудован системой непрерывной подачи чернил (СНПЧ) и также сбором отходов в баночку. С самими чернилами проблем нет, но вот с отводом отходов есть. В принтере стоит счетчик, который считает сколько чернил утекло в контейнер-абсорбер, куда собирается по умолчанию с завода. Вывод сделали и абсорбер не используется и не заполняется, но счетчик то никуда не делся и при достижении лимита выдается ошибка, что надо бы обратиться в сервисный центр для замены “памперса” (так реально в тексте ошибки, жаль скрина не сохранилось). Первое время сбрасывали там же где и ставили СНПЧ, каждая итерация за деньги. Потом сервисник показал как он это делает с помощью программы и покупки кода сброса, и стали делать сами. Так продолжалось какое-то время, но в прошлом году вариант с покупкой перестал работать из-за проблем с оплатой.
Сервисная тулза Adjustment Program
Была найдена сервисная программа Adjustment Program, там можно было поделать всякое с принтером, но самое главное можно работать с “waste inc counter”, тем самым счетчиком из-за которого выдается ошибка переполнения абсорбера. Но позволяет сбросить только до 97% от лимита. Да этого вцелом достаточно, чтобы произвести какие-то работы на принтере в сервисе, попечатать там что-нибудь, и сделать это без замены абсорбера, пока клиент видимо не заплатит за замену.
Какое-то время тулза использовалась в таком режиме, но после сброса эти 3% накапливаются крайне быстро, бывало нужно сбрасывать пару раз за одну распечатку. Благо к тому времени необходимость в цветном принтере отпала и был куплен обычный лазерник, а этот уехал со мной, чтобы использовать его как сканер, ну или пока не будет продан. Когда забирал его, уже прикидывал, что перед продажей хорошо бы сбросить счетчик, заодно набраться опыта в декомпиле.
Подготовка к декомпиляции
Итак руки дошли через полгода. За это время LLM-ки стали лучше и подрос ghidra-mcp и обвязки вокруг него. Еще в инфополе было несколько знаковых новостей об использовании этой комбинации для декомпила сложных вещей. Например, восстановление и правка драйверов для windows и полный рестор кода для GTA SA с возможностью последующей сборки и нормальной играбельностью (https://www.youtube.com/watch?v=zBQJYMKmwAs).
Хотелось попробовать самому на сколько эта связка хороша и на сколько упрощает работу. А поскольку уже был достаточный опыт по декомпилу экзешников и дллок, но старыми дебагерами и IDA с частичной поддержкой восстановления С кода - и это было тяжело как для мозга, так и для глаз. Опыт работы с гидрой (https://github.com/nationalsecurityagency/ghidra) хоть и малый, но показал что в ней в целом поприятнее работать.
Модельку запускал локально в lm studio qwen3.6-35b-a3b (Q4) с 200к контекстом - достаточно шустро работает, точности хватает для простого кода и как раз надо было потестить как справится с декомпилом.
Обновил гидру и далее склонил https://github.com/bethington/ghidra-mcp (тулзы и интеграция с mcp самой гидры). Далее по инструкции из ридми настраивается тривиально интеграция. Добавил mcp провайдер в lm studio. Из важного - в lm studio указывается mcp не самой гидры (по умолчанию запускается на 8089 порту), а прокси тулсета (порт 8081). Прокся запускается как (из того же ридми)
uv run bridge-mcp-ghidra --transport streamable-http --mcp-host 127.0.0.1 --mcp-port 8081
Использовать чисто mcp гидры не пробовал, может и необходимости в тулинге и не было никакой.
Начало
В гидре создал по новой проект. Импортировал экзешник тулзы, запустил по нему анализ и дождался пока он закончится и появится декомпильнутый код.
Счетчик уже был сброшен, а при повторной попытке это сделать выдавалось предупреждение, что лимит еще не достигнут и сброс недоступен

Текст есть, поиск по гидре вручную быстро навел на нужное место в коде, там был if на несколько проверок и если проверка не пройдена, то выдается окно, по закрытии которого сброс останавливается.


Без участия ллм по старой памяти были инвертированы jz и jnz. До этого в гидре только смотрел код, но патчингом не занимался, нашел видео о том как делается. Прямо скажу не удобно менять через не очень удобный шорткат, который надо жать на каждую asm инструкцию, и ввод, который странно работает требует ввод имени инструкции и именно заглавными буквами, что прямо бред (буду рад советам как делать поудобнее).
Поменяв условия, экспортнул в “Original File” формате (ранее был отдельно экспорт в “PE”, что было как-то понятнее). Запускаю, приложение стартует, но все кнопки не работают. Точнее они нажиимаются, но ничего не проиходит.

Думаю, что может чего напортачил, проделываю несколько подходов поправить по новой, подключаю уже к работе ллмку, она напоминает про простой jmp, который тут же вношу - все попытки заканчиваются одинаково как и первый экспорт. Но jmp освободил кучу байт, которые были заNOPлены, это поможет позже.
Неработающие кнопки
Измененная логика выполняется только на третьем экране, т.е. надо на стартовом нажать одну кнопку, на новом выбрать меню и провалиться в него, и уже там нажать кнопку, с измененной логикой. А кнопки не работают уже на первом стартовом экране. Вполне очевидно, что тут была какая-то проверка, и скорее всего на целостность. Но не понятно, почему тогда приложение стартует, а не как обычно выдает просто ошибку на запуске, если чексумма где-то не сошлась.
Еще до этого момента при разборе декомпила, где вносились изменения, обратил внимание, что в вызывающей функции был странный блок с вычислением какой-то чексуммы, но тогда подумал, что это какой-то хитрый способ работы с внутренней “бд” в дата секции, чтобы проверить что она цела и не побита. Проигнорировал в первую очередь из-за того, что думал что это заинлайненная проверка от ембедед движка такой “бд” - то что заинлайнено позже оказалось правильной мыслью, а вот про “бд” совсем мимо.
Тут уже полноценно подключается чат с моделькой, и начинаю просить разобрать конкретные функции и сделать предположения по коду. Ллм хорошо справляется с тем, чтобы взять декомпильнутый гидрой код и сделать из него человекочитаемый - выделяет структуры, именует переменные, функции, в циклах счетчики проявляет. Но надо проверять, т.к. ллмка например местами просто в виде коммента писала что делает код, но сам этот код она убирала, оставив только коммент. Еще упрятывала куски кода под выдуманные функции, вызова которых не было в исходном коде. Поразбиравшись понял, что она прикидывает что вот этот блок кода скорее всего заинлайненная функция и она называлась бы вот так - да читабельнее, но приходится ее явно просить этого не делать.
С таким кодом уже было проще работать, хоть и потребность в просмотре и кода из гидры и ассемблера никуда не делась.
Итак, найден какой-то код валидации, но он далек от места вызова первых кнопок. Вспоминаю, что в папке с тулзой есть еще всякие dat файлы и может быть там, чтото есть. Ищу Process Monitor утилиту от # Sysinternals для отслеживания обращений программ к диску/реестру/сети. Запускаю, настраиваю фильтрацию, запускаю мой экспортнутый бинарь, жму в нем кнопку и вижу как происходит чтение всего экзешника. И это происходит на каждое нажатие любой из кнопок на экране - очевидно происходит вычисление чексумм. Вопрос только где они хранятся и как вычисляются, чтобы подставить новые значения. Или вообще убрать эти проверки.
Где считаются чексуммы
Чтобы это выяснить дал ллмке задание найти места, где читается сам экзешник. Оно покряхтело и выдало кучу функций, где действительно вызывалось чтение файла, но не обязательно экзешника. Понять что читается и тем более откуда инициируется достаточно сложно, т.к. приклад это MFC и найти вызываемые функции по нажатию кнопки ллмке было сложно. Искать отправлял по тексту на кнопке - саму кнопку находило, ид находило, а вот дальше раскопать не выходило.
Пока это происходило, увидел, что в mcp есть возможность переименования функций, и ллмка была запряжена пройтись по всем сгенеренным гидрой FUN_XXXXXX функциям, понять что делает код в них и переименовать. И так делать рекурсивно. Каких-то пару-тройку часов контроля в полглаза и вот все функции переименованы. И т.к. это сделано для всех таких функций, то и в коде они отбражались с осмысленными именами и даже ориентироваться стало значительно проще.


Далее по поиском по именам связанным с файлами нашел тонну функций и ничего конкретного. Ллмка тоже ничего не нашла сходу и было принято решение просто глянуть в дебагере откуда идет вызов чтения файлов. В гидре почему-то дебагер не завелся, поэтому расчехлил старенькую IDA, в которой с полпинка, поставив брейкпоинт на вызовы получения пути запущенного экзешника, был найден адрес функции и уже в гидре найдена сама функция от кнопки Select.
Кусок декомпила с валидацией целостности
local_fde8 = &DAT_00613830; local_1dc = uVar1; DVar2 = GetModuleFileNameA((HMODULE)0x0,aCStack_10090,0x104); if (DVar2 == 0) { local_1d8 = 1; } if ((local_1d8 == 0) && (uStack_10094 = FID_conflict:__open(aCStack_10090,0x8000,0,uVar1), (int)uStack_10094 < 0)) { local_1d8 = 2; } if ((local_1d8 == 0) && (iVar3 = FUN_004ebff6(uStack_10094,local_1c8), iVar3 != 0)) { local_1d8 = 3; } if (local_1d8 == 0) { local_198 = 0; local_1c9 = local_fde8[1] & 0xf; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_198 = local_198 << 8 | (uint)(byte)local_fde8[(uint)local_1c9 + local_18c]; } local_fde4 = 0; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_fde4 = local_fde4 << 8 | (uint)(byte)local_fde8[local_18c + 0x18]; } if (local_fde4 != local_1b4 + local_198) { local_1d8 = 4; } } if (local_1d8 == 0) { local_198 = 0; local_1c9 = local_fde8[3] & 0xf; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_198 = local_198 << 8 | (uint)(byte)local_fde8[(uint)local_1c9 + local_18c]; } local_fde4 = 0; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_fde4 = local_fde4 << 8 | (uint)(byte)local_fde8[local_18c + 0x14]; } local_194 = local_fde4 - local_198; if (100 < local_194) { local_1d8 = 5; } } if (local_1d8 == 0) { local_198 = 0; local_1c9 = local_fde8[4] & 0xf; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_198 = local_198 << 8 | (uint)(byte)local_fde8[(uint)local_1c9 + local_18c]; } for (local_fdec = 0; local_fdec < local_194; local_fdec = local_fdec + 1) { local_fde4 = 0; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_fde4 = local_fde4 << 8 | (uint)(byte)local_fde8[local_18c + 0x20 + local_fdec * 4]; } aiStack_ff80[local_fdec] = local_fde4 - local_198; } } if (local_1d8 == 0) { local_198 = 0; local_1c9 = local_fde8[2] & 0xf; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_198 = local_198 << 8 | (uint)(byte)local_fde8[(uint)local_1c9 + local_18c]; } local_fde4 = 0; for (local_18c = 0; local_18c < 4; local_18c = local_18c + 1) { local_fde4 = local_fde4 << 8 | (uint)(byte)local_fde8[local_18c + 0x1c]; } local_190 = 0; local_1d4 = 0; local_188 = 0; local_1d0 = 0; while ((local_ff84 = AdjGetUnicodeStr(uStack_10094,local_fde0,(LPWSTR)0xfc00), local_ff84 != -1 && (0 < local_ff84))) { for (local_18c = 0; local_18c < local_ff84; local_18c = local_18c + 1) { if ((local_1d4 < local_194) && (local_188 == aiStack_ff80[local_1d4] + 0x1c)) { local_1d0 = 4; local_1d4 = local_1d4 + 1; } if (local_1d0 == 0) { local_190 = (uint)*(byte *)((int)local_fde0 + local_18c) + local_190; } else { local_1d0 = local_1d0 + -1; } local_188 = local_188 + 1; } } AdjFileDone(uStack_10094); if (local_ff84 < 0) { local_1d8 = 6; } else if (local_fde4 != local_190 + local_198) { local_1d8 = 7; } } if (local_1d8 == 0) { DAT_006a0838 = 1; } else { GetObjectInstance(&iStack_10098); ClearAdjustValue(&iStack_10098); }
Вариант ллмки
/** * Проверяет целостность конфигурационного файла принтера * * @param filePath Путь к конфигурационному файлу * @param fileData Указатель на данные из файла * @param fileHandle Дескриптор открытого файла * @return 0 если проверка успешна, код ошибки иначе */ int CheckConfigFileIntegrity(const char* filePath, void* fileData, int fileHandle) { int errorFlag = 0; // 0 = ошибок нет uint32_t checksum; uint32_t expectedValue; int dataCount; // ========================================== // Проверка 1: Получение пути к файлу // ========================================== char modulePath[268]; DWORD pathLength = GetModuleFileNameA(NULL, modulePath, sizeof(modulePath)); if (pathLength == 0) { errorFlag = 1; // Не удалось получить путь return errorFlag; } // ========================================== // Проверка 2: Открытие файла // ========================================== int fileDescriptor = _open(modulePath, _O_RDONLY, _S_IREAD); if (fileDescriptor < 0) { errorFlag = 2; // Не удалось открыть файл return errorFlag; } // ========================================== // Проверка 3: Парсинг файла // ========================================== uint32_t parseResult[5]; int parseStatus = ParseConfigFile(fileDescriptor, parseResult); if (parseStatus != 0) { errorFlag = 3; // Ошибка парсинга return errorFlag; } // ========================================== // Проверка 4: Проверка контрольной суммы заголовка // ========================================== // Извлекаем базовое значение из байта[1] & 0xF int baseOffset = ((uint8_t*)fileData)[1] & 0xF; uint32_t baseValue = 0; for (int i = 0; i < 4; i++) { baseValue = (baseValue << 8) | ((uint8_t*)fileData)[baseOffset + i]; } // Извлекаем ожидаемую контрольную сумму из смещения 0x18 uint32_t expectedChecksum = 0; for (int i = 0; i < 4; i++) { expectedChecksum = (expectedChecksum << 8) | ((uint8_t*)fileData)[0x18 + i]; } // Проверяем совпадение if (expectedChecksum != parseResult[0] + baseValue) { errorFlag = 4; // Неверная контрольная сумма return errorFlag; } // ========================================== // Проверка 5: Проверка размера данных // ========================================== // Извлекаем базовое значение из байта[3] & 0xF baseOffset = ((uint8_t*)fileData)[3] & 0xF; baseValue = 0; for (int i = 0; i < 4; i++) { baseValue = (baseValue << 8) | ((uint8_t*)fileData)[baseOffset + i]; } // Извлекаем ожидаемый размер из смещения 0x14 uint32_t expectedSize = 0; for (int i = 0; i < 4; i++) { expectedSize = (expectedSize << 8) | ((uint8_t*)fileData)[0x14 + i]; } // Вычисляем фактическое количество элементов dataCount = expectedSize - baseValue; // Проверяем ограничение (должно быть <= 100) if (dataCount > 100) { errorFlag = 5; // Слишком большой размер данных return errorFlag; } // ========================================== // Проверка 6: Обработка массива смещений // ========================================== int offsets[101]; // Максимум 100 элементов + 1 для безопасности // Извлекаем базовое значение из байта[4] & 0xF baseOffset = ((uint8_t*)fileData)[4] & 0xF; baseValue = 0; for (int i = 0; i < 4; i++) { baseValue = (baseValue << 8) | ((uint8_t*)fileData)[baseOffset + i]; } // Обрабатываем массив смещений for (int i = 0; i < dataCount; i++) { uint32_t offsetValue = 0; for (int j = 0; j < 4; j++) { offsetValue = (offsetValue << 8) | ((uint8_t*)fileData)[0x20 + i * 4 + j]; } offsets[i] = offsetValue - baseValue; } // ========================================== // Проверка 7: Проверка Unicode-строк // ========================================== // Извлекаем базовое значение из байта[2] & 0xF baseOffset = ((uint8_t*)fileData)[2] & 0xF; baseValue = 0; for (int i = 0; i < 4; i++) { baseValue = (baseValue << 8) | ((uint8_t*)fileData)[baseOffset + i]; } // Извлекаем ожидаемую контрольную сумму из смещения 0x1C uint32_t stringChecksum = 0; for (int i = 0; i < 4; i++) { stringChecksum = (stringChecksum << 8) | ((uint8_t*)fileData)[0x1C + i]; } // Читаем и обрабатываем Unicode-строки wchar_t unicodeBuffer[32258]; int stringOffset = 0; int stringIndex = 0; int skipCounter = 0; uint32_t calculatedChecksum = 0; while ((stringLength = ReadUnicodeString(fileDescriptor, unicodeBuffer, sizeof(unicodeBuffer))) > 0) { for (int i = 0; i < stringLength; i++) { // Проверяем, нужно ли пропускать байты if ((stringIndex < dataCount) && (stringOffset == offsets[stringIndex] + 0x1C)) { skipCounter = 4; stringIndex++; } if (skipCounter == 0) { // Добавляем байт в контрольную сумму calculatedChecksum += (uint8_t)unicodeBuffer[i]; } else { skipCounter--; } stringOffset++; } } // Закрываем файл _close(fileDescriptor); // Проверяем ошибки чтения if (stringLength < 0) { errorFlag = 6; // Ошибка чтения Unicode-строк return errorFlag; } // Проверяем контрольную сумму строк if (stringChecksum != calculatedChecksum + baseValue) { errorFlag = 7; // Неверная контрольная сумма строк return errorFlag; } return errorFlag; // 0 если все проверки пройдены }
В этой функции был блок вычисления чексуммы, и если она сходилась с записанной в том же экзешнике, то работа логики кнопки выполнялась дальше. Блок вычисления чексуммы был такой же как и рядом с тем местом, где вносил правки - код выглядел как заинлайненный судя по всему и тут и там. Потом я увидел, что и еще много где. Из-за того что мест много, не было никакого желания проверять реальное ли полное это дублирование, т.к. в каждом месте есть какие-то базовые смещения и уверенности не было.
Разобрал код вычисления чексуммы и откуда конкретно там берется чексумма для сверки. Чексумма считается простым побайтовым суммированием и сумма сравнивается с тем что записано в экзешнике. Однако не все суммируется, в датасекции есть таблица из нескольких десятков значений, которые являются смещениями внутри экзешника, значения по которым надо пропустить при вычислении. Там еще сами смещения вычисляются через базовое, от чего добавляется еще подготовка оффсетов.
По первому смещению, как раз хранится чексумма, по второму - просто часть какой-то строки, по остальным не проверял. В общем легкая заморочка есть, но самое главное принцип понятен, числа есть, где лежат понятно. В дебагере убедился, что число именно то что надо и какое высчитывается на текущем измененном файле.
Менять чексумму не стал, т.к. у меня был заNOPленный блок и решил просто в нем вписать подходящие значения. Для измененного блока байт взял исходные, взял новые, попросил ллмку сгенерить суммы и какая дельта - это была вполне ожидаемая ошибка, ллмка выдумала числа и сборка не завелась. После просьбы сделать скрипт для вычисления сумм и его запуска получил нужную дельту, поправил пару байт, экпортнул - и теперь кнопки работают. И нет проверки, что лимит уже достигнуты.
Как ллмка считала суммы

Сброс счетчиков до нуля
Пол дела сделано, осталось самое главное - сбрасывать счетчики в ноль, а не на 97% и 95%. Функция уже известна, вопрос только где эти проценты хранятся и что патчить. Ллмка была отправлена декомпилить код и искать где там есть 97% или идет вычисление 3% и вычитание. Чуда не случилось, ллмка только расписала логику и указала, что ведется работа с максимальными и текущими значениями, и что вот пример структуры и вон там примерно начало.
Пример того что выдавало
// Описание структуры данных, которая хранится в глобальной памяти. // Эти данные загружаются при инициализации драйвера. // Адрес: DAT_00681d98 (примерный базовый адрес в памяти) struct CounterConfigData { int current_value; // Смещение 0x00: Текущее значение счетчика int max_value; // Смещение 0x04: МАКСИМАЛЬНОЕ значение (здесь лежат 97 и 95) int unknown_1; // Смещение 0x08 int unknown_2; // Смещение 0x0C int control_id; // Смещение 0x10: ID элемента управления в диалоге int text_control_id; // Смещение 0x14: ID текстового поля // ... остальные поля структуры ... }; // Глобальный массив конфигураций для 3-х цветных головок (Cyan, Magenta, Black) // Размер каждого элемента 0x78 (120) байт. extern CounterConfigData g_CounterConfigs[3]; void __fastcall ProcessCounterDialog(HWND hParent, int param_2) { int status_code = 0; CWnd* pDialog = nullptr; int loop_index = 0; // param_2 == 1 означает, что нужно запустить процесс настройки счетчиков if (param_2 == 1) { // 1. Инициализация прогресс-бара AdjSetStatusTxt(hParent, "Initializing now..."); AdjSetProgressRange(hParent, 0, 5); // Диапазон 0-5 AdjAutoProgressOn(hParent, 1); AdjUpdateProgress(hParent); // Получаем дескриптор экземпляра драйвера void* hDriverInstance = GetObjectInstance(); // Блокируем интерфейс status_code = 3; // 2. Цикл по всем трем цветным каналам (0, 1, 2) // Именно здесь мы обращаемся к значениям 97 и 95 while (loop_index < 3) { // Указатель на текущую конфигурацию счетчика CounterConfigData* current_config = &g_CounterConfigs[loop_index]; // Получаем указатель на родительское окно CWnd* pParent = CWnd::GetDlgItem(hParent, current_config->control_id); // Читаем текущее значение из UI (если пользователь что-то ввел) int ui_value = AdjGetControlVal(pParent); // Если в UI стоит галочка или значение активно (== 1) if (ui_value == 1) { // Запрашиваем состояние приложения (печатает ли оно сейчас?) int app_state = QueryAppState(hDriverInstance); // Логика обработки состояния: // Если приложение готово (state == 2) и мы еще не начали печать (param_2 == 0) if ((app_state == 2) && (param_2 == 0)) { // Получаем окно для отображения статуса CWnd* pStatusWnd = CWnd::GetDlgItem(hParent, current_config->text_control_id); // ... дополнительная логика обновления статусов ... } // Вычисление процента выполнения или корректировка значений // Если статус -1 (ошибка или сброс), пересчитываем if (app_state == -1) { // Формула пересчета на основе адресов памяти драйвера int calculated_percent = ((current_config->end_addr - current_config->start_addr) * 100) / (current_config->end_addr - current_config->start_addr); // Сохраняем результат current_config->current_percent = calculated_percent; } else { // Иначе берем значение из param_2 (если оно передается как процент) current_config->current_percent = (int)param_2; // Корректировка текущего значения счетчика current_config->current_value = ((current_config->end_addr - current_config->start_addr) * current_config->current_percent) / 100 + current_config->start_addr; } // Проверка на соответствие текущих значений допустимым пределам // current_config->max_value - это и есть то самое значение (97 или 95) if ((app_state == 2) && (current_config->current_value == 0) && (current_config->start_addr < current_config->current_value) && (current_config->current_value < current_config->end_addr)) { // Ошибка: значения не сходятся AdjShowErrorMsg(hParent, 0x210006a4, nullptr); status_code = 2; ClearAdjustValue(hDriverInstance); status_code = 1; AdjDestroyProgressDone(hParent); status_code = 0; FUN_ReleaseRefurbishmentBuffer(); status_code = 0xFFFFFFFF; ClearAdjustValue(param_2); goto LAB_004ed965; // Переход к очистке } // Поиск записи в конфигурации печати int* pConfigEntry = nullptr; int fetch_result = FetchPrintPositionConfig(hParent); // Ищем совпадающую запись while (loop_index < fetch_result) { if (pConfigEntry != nullptr) { // Проверка совпадения ID и индекса if ((*pConfigEntry == fetch_result) && (*(pConfigEntry + 0x13) == loop_index)) { // 0x4c / 4 = 0x13 // Если найдена запись, проверяем тип настройки int setting_type = *(pConfigEntry + 0x10); // Смещение 4 от начала записи // Если тип 0x22 (94 в десятичной) - специфичная логика if (setting_type == 0x22) { // Корректировка значения current_config->current_value = *(pConfigEntry + 0x03); while (current_config->current_value <= current_config->current_percent) { current_config->current_value += 5; } } // Если тип 0x1B (27 в десятичной) или 0x07 (7) - другая логика else if ((setting_type != 7) && (setting_type != 0x1B)) { // ... обработка других типов ... } else { // Базовое значение current_config->current_value = *(pConfigEntry + 0x03); } // Применение отрефурбленной (настроенной) стоимости int apply_result = ApplyRefurbishmentValue( hDriverInstance, *(pConfigEntry + 0x02), current_config->current_value ); if (apply_result != 1) { // Ошибка применения AdjShowErrorMsg(hParent, apply_result, nullptr); status_code = 2; ClearAdjustValue(hDriverInstance); status_code = 1; AdjDestroyProgressDone(hParent); status_code = 0; FUN_ReleaseRefurbishmentBuffer(); status_code = 0xFFFFFFFF; ClearAdjustValue(param_2); goto LAB_004ed965; } } } // Переход к следующей записи в конфигурации loop_index++; pConfigEntry += 0x1E; // Смещение на следующую запись в структуре (0x78 / 4 = 0x1E слов) } // Обновление текстовых полей в диалоге // Здесь используется значение current_config->current_value, которое зависит от максимума (97/95) SetWideString(hParent, L"Печать..."); // Или другой статус int buffer_size = FUN_GetBufferSize(hParent); LPCSTR text = FUN_GetBufferOffset(hParent, buffer_size); SetControlText(hParent, current_config->text_control_id, text); // Обновление прогресс-бара AdjUpdateProgress(hParent); } // Переход к следующему счетчику loop_index++; } // Завершение диалога CWnd::UpdateData(hParent, 0); FUN_DestroyProgressDialog(hParent); // Цикл ожидания выключения принтера while (true) { MessageBoxA(hParent, "Please turn off the printer.", "Information", 0x40); int dev_id = AdjGetDeviceId(hDriverInstance); if (dev_id != 1) break; Sleep(500); } // Завершение работы с драйвером AdjDriverTerm(hDriverInstance); AdjDriverPortDone(hDriverInstance); // Показать сообщение об успехе std::string path = "Please reboot the printer."; FUN_ShowSuccessMessage(hParent); // Финальная очистка status_code = 2; ClearAdjustValue(hDriverInstance); status_code = 1; AdjDestroyProgressDone(hParent); status_code = 0; FUN_ReleaseRefurbishmentBuffer(); status_code = 0xFFFFFFFF; ClearAdjustValue(param_2); } else { // Если param_2 != 1, показываем ошибку AdjShowErrorMsg(hParent, *(uint*)(param_2 - 0x148), nullptr); status_code = 1; AdjDestroyProgressDone(hParent); status_code = 0; FUN_ReleaseRefurbishmentBuffer(); status_code = 0xFFFFFFFF; ClearAdjustValue(param_2); } LAB_004ed965: // Очистка исключений и возврат ExceptionList = *(void**)(param_2 - 0xC); SecurityCheckCookie(*(uint*)(param_2 - 0x1D8) ^ param_2); return; }
Тут можно увидеть как раз “коммент вместо кода” // ... обработка других типов .... И в целом видно как именованные функции упрощают понимание логики.
По указанному адресу действительно было максимальное и минимальное значение, но никаких намеков на 97% или значение ему соответствующее. В коде была секция вычисления процентов по значению и значения по процентам - оба применялись в ифе и дальше никуда не шли, т.е. в принтер в они не отправлялись.
Были попытки разные промты позакидывать, использовать qwen3.6-27b (тоже q4, хотя можно было и пожирнее), но пользы не было, хотя разок поймал забавное:

В общем надо было самому разбираться в коде или уже кидать в облачные ллмки. Решил копаться дальше с тем что есть, чтобы не смазывать чистоту эксперимента. А код скажем так странный, да еще и переплетен goto. Notepad++ в руки и погнали приводить декомпиленный код в читаемый, расставляя комменты параллельно глядя в ответ ллмки. Когда плюс минус стала понятна работа с процентами и что они ни на что не влияют, взял дебагер в руки и погнал идти по коду и отмечая всякие значения переменных. Ждал появления нескольких конкретных значений, отображаемых на экране:
максимальные (уже найдено было где хранятся)
текущие - проставленные после предыдущего удачного сброса (0x0F0F (3855) для main и 0x0C0C (3084) для platen)
проценты - они тоже рисовались на экране
Не удивительно, что не удавалось найти простым способом нужные цифры. Если мин и макс значения были фиксированы и их можно было найти по базовому смещению в коде, то вот значение для сброса искалось по какой внутренней таблице с данными, в которой 63 (0x3F) записи и по которой идет линейный поиск, пока не совпадет нечто вроде id или модели или хз что там по сути. И вот как совпадание случится, то относительно найденного смещения читается значение, которое используется для сброса. Т.е. дело было не 97% или 3% как я думал ранее, а в прошитых значениях.
Далее уже тривиальный поиск значения в экзешнике, патч чтобы сбрасывало на околонулевое, не рискнул прям на ноль ставить. Потом естественно экпортнул без фикса чексуммы. Потыкал в неработающие кнопки. Быстренько подправил пару байт, экпорт и вуаля - счетчик сброшен.

Для сброса оставлены 0x0F (15) и 0x0C (12) соответсвенно.
Итоги
На все про все по времени вышло часов на 15 в течение трех дней. Где-то параллельно смотря видосики и занимаясь другими делами, а где-то плотненько работая головой и ломая глаза.
По ллм:
ллм-ка множество раз зацикливалась, видимо специфика декомпильнутого кода ну и локальные модели подвержены этим больше. Тут просто надо понимать, что надо бы время от времени поглядывать что именно оно делает
да код ллмка переписывает и делает читаемее, но иногда убирает из него значимые куски
высказывая идеи и предположения, ллмка скатывалась до используйте уже тулзу, которую собственно я и анализировал, что больше забавляло, чем мешало
локальной модели для задачи именования функций за глаза, но вот ограничения гидры, что имена не должны пересекаться вызывали у модели приступы генерации супермегадлинных названий, которые тоже нельзя было использовать из-за длины и она уходила в бесконечный цикл подбора нового имени
Общее впечатление от mcp гидры в связке даже с простой ллмкой строго положительное, на похожую операцию в ковид ушло пару-тройку недель плотного засиживания за компом, а здесь результат фактически за три дня.