Если установка программы протекает совсем не так, как хочется пользователю, приходится вмешиваться в работу инсталлятора. Сегодня мы поговорим о том, как устроен инсталляционный пакет InnoSetup, и научимся менять его логику изнутри.
В статье «Ломаем инсталлятор. Как обмануть инсталлятор MSI методом для ленивых» я писал, что частенько приходится допиливать не только саму программу, но и ее инсталлятор. В той заметке мы рассматривали принципы реверс‑инжиниринга и патчинга инсталляционных пакетов, созданных при помощи инсталлятора InstallShield (MSI). Сегодня нашей целью будет другой популярный пакет — InnoSetup. Думаю, этот тип инсталляторов настолько широко распространен, что не нуждается в подробном описании, поэтому сразу перейдем к конкретной задаче.
Итак, у нас имеется установщик для некоего графического плагина, оформленный в виде самодостаточного исполняемого EXE-модуля. При запуске он просит ввести серийный номер, судя по всему, проверяет его на удаленном сервере и при неправильном вводе выдает сообщение об ошибке.
Открыв наш инсталлятор в Detect It Easy, выясняем сразу два факта: во‑первых, это наш пациент, а во‑вторых, InnoSetup насквозь писан на Delphi.
Как подсказывает опыт, открывать инсталлятор в IDA нет ни малейшего смысла. Прежде всего, это дельфи, тут скорее помог бы IDR. С другой стороны, файл чуть менее чем целиком состоит из упакованного или зашифрованного оверлея (строка Serial Number is invalid предсказуемо не находится в нем в открытом виде), то есть его загрузчик не несет ничего полезного для решения нашей проблемы.
Поэтому сразу попробуем пощупать процедуру проверки серийного номера «изнутри», в процессе работы программы. Загрузив инсталлятор в отладчик x64dbg, мы обнаруживаем, что наши предположения верны. Загрузчик порождает несколько процессов, которые далее живут собственной жизнью независимо от него. В частности, окно сообщения и все остальные диалоговые окна вызываются из процесса, порождаемого модулем, который находится во вложенной папке
При загрузке инсталлятор перво‑наперво создает этот каталог, распаковывает в него данный модуль, который потом запускает, а в конце инсталляции убирает за собой, удаляя и файл, и каталог. Рассмотрим этот модуль более детально.
Строка Serial Number is invalid отсутствует в открытом виде и здесь тоже. Detect It Easy не говорит про модуль ничего внятного, кроме того, что он тоже написан на Delphi и содержит в ресурсе еще один модуль, написанный на Microsoft Visual C.
Попробуем копнуть чуть глубже: аттачимся с помощью x64dbg к процессу в момент появления диалогового окна "Serial Number is invalid". Код вызова MessageBox выглядит примерно так:
Открыв модуль в IDR и найдя этот фрагмент кода, мы видим, что он является частью метода
Открываем вкладку «Стек вызовов» и буквально семью вложениями выше (или ниже, кому как больше нравится) обнаруживаем интересный метод _Unit76.TPSExec.RunScript. Этот метод и по названию, и по логике работы сильно напоминает так часто встречаемый нами интерпретатор шитого байт‑кода. Легко и просто находится место выборки и расшифровки текущей команды.
На скриншоте видно, что байт‑код извлекается в регистр esi из потока по адресу [edx+eax], где edx — базовый адрес текущей процедуры, а eax — текущее смещение относительно него. Что же это за скрипты такие и какой байт‑код им соответствует?
Погуглив по названию класса TPSExec, мы сразу натыкаемся на термин Pascal Script. В двух словах — это паскалеподобный скриптовый язык, используемый, в частности, в сценариях InnoSetup.
Как только мы разобрались, с чем имеем дело, дальнейший путь превращается в скоростное шоссе. Для начала попробуем вытащить скомпилированный байт‑код скрипта из инсталлятора. Оказывается, для этого вовсе не обязательно танцевать с бубном, дампя скомпилированный байт‑код из памяти отладчика. Специально обученные энтузиасты создали несколько проектов распаковщиков дистрибутивов InnoSetup, причем с открытым кодом. Например, innoextract и innounp. Запустив innounp.exe из последнего пакета с ключом -m, мы получаем информацию о встроенном в него Pascal-скрипте (не путать с инсталляционным скриптом .iss, представляющим собой список файлов устанавливаемого дистрибутива):
Если мы распакуем дистрибутив этой утилитой с ключом -m, то компилированный код Pascal Script будет сохранен в файл с капитанским названием CompiledCode.bin. Что же за код находится внутри данного файла?
По счастью, и здесь от нас не требуется изобретать велосипед — все уже придумано до нас. Слегка погуглив, находим проект IFPSTools, включающий в себя дизассемблер Pascal Script ifpsdasm. Существует даже весьма толковый декомпилятор CompiledCode в исходный паскалевский код Inno Setup Decompiler. К сожалению, проект, похоже, мертв, однако сам декомпилятор все еще можно скачать по ссылке. С него мы и начнем исследовать наш код. Довольно быстро мы находим в нем вызов окна сообщения:
Попробуем теперь найти это место в скомпилированном коде, чтобы поправить его. Дизассемблировав CompiledCode.bin при помощи ifpsdasm, находим ассемблерный эквивалент приведенного выше скриптового кода:
Далее нас ожидает небольшой затык: несмотря на то что и декомпилированный, и дизассемблированный листинги у нас имеются, привязать их к бинарному байт‑коду — не такая уж тривиальная задача. Дело в том, что ifpsdasm весьма специфический инструмент, в котором (как и в IDA, например) нельзя так просто взять и включить смещения и опкоды для каждой команды.
Система опкодов PascalScript столь специфична, что в открытом доступе таблицы опкодов не найти, как мы это делали для IL или JVM. Немного выручает то, что мы располагаем исходниками ifpsdasm, и, будь у нас чуть больше усидчивости, мы бы, конечно, добавили требуемые функции в наш проект. Но мы, как обычно, пойдем интуитивным путем наименьшего сопротивления. Поискав по исходному коду проекта IFPSTools, мы находим пару файлов, содержащих комментированную информацию о системе команд и опкодах этого интерпретатора:
Поразмыслив логически, мы находим место, напоминающее нам нужный фрагмент кода:
Итак, для полного счастья нам необходимо всего‑навсего исправить в загруженном компилированном байт‑коде выделенный серым байт 0x12 на 06. Проверим эту гипотезу прямо в отладчике, который мы предусмотрительно оставили открытым на интерпретируемом байт‑коде в окне дампа.
Прямо в нем меняем требуемый байт с 12 на 6 и нажимаем кнопку Next. Бинго, любой введенный код принимается, не вызывая ошибки!
Но, как обычно, самая веселая часть работы только начинается.
Мы разобрались, что именно надо поменять в скомпилированном скрипте, чтобы он принимал любой код. Однако как поменять исправленный код в инсталляторе? Поправить аналогично тому, как мы это делали в InstallShield, не получится — алгоритм криптования не сводится к простому XOR по маске, тут полноценный алгоритм сжатия, причем такой экзотический, как LZMA. Вообще говоря, по уму следовало бы перепаковать инсталлятор, хотя это очень непросто.
Возможно, я когда‑нибудь напишу отдельную статью об этом, сейчас же я просто в двух словах обрисую направление, в котором желающие могут самостоятельно попробовать свои силы. Формат организации инсталляционных архивов InnoSetup нигде не документирован, однако, как я уже говорил, есть проекты с открытым кодом innounp и innoextract. Внимательно разобравшись с кодом этих проектов (особенно полезен в этом плане innounp), можно написать свой собственный перепаковщик. Можно найти обходной путь и для основной сложности этого процесса — отсутствия в публичном доступе исходных кодов специфического для InnoSetup компрессора LZMA (да, к сожалению, во всех этих проектах есть только декомпрессор, алгоритм компрессии нигде не документирован). Но формат сжатия подразумевает использование нескольких алгоритмов, помимо LZMA (zlib, bzip или вообще без компрессии). Собственно, самый простой выход — оставить результирующий стрим вообще без сжатия.
А напоследок, как обычно, я раскрою самый ленивый и быстрый способ патча дистрибутива без его пересборки. В этом нам снова поможет лоадер. Для тех, кто прогуливал посвященные лоадерам уроки, я расскажу в двух словах общий принцип этой технологии.
Ее суть заключается в том, что если невозможно поправить программу на диске (при загрузке она динамически расшифровывается или распаковывается в оперативную память компьютера), то для нее создается лоадер — загрузочная программа, которая запускает ее из себя, а потом ищет и заменяет нужные байты прямо в распакованном образе в памяти. Существует множество стандартных готовых лоадеров, но нам нужен специфический, работающий по определенным принципам.
Наш лоадер должен искать в памяти процесс (точнее, все процессы), соответствующий интерпретаторам PascalScript, и патчить его таким образом, чтобы в нужной процедуре интерпретируемого скрипта байт со значением 0x12 по смещению 0x4f4 относительно начала текущей процедуры патчился на 6.
По здравом размышлении мы приходим к выводу, что самый правильный и надежный способ — чтобы интерпретатор сам же и патчил код во время интерпретации. Поэтому встраиваем проверку с патчем прямо в место выборки текущего опкода. Естественно, места для требуемого действия там нет, поэтому ищем в секции свободное место и делаем переход на него с последующим возвратом на следующие команды. Переход длинный, поэтому придется пожертвовать двумя командами.
Было:
Стало:
Кусок кода, который мы вставляем по смещению 6C1FAC, выглядит так:
Итак, еще раз резюмируем принцип работы лоадера. При загрузке инсталлятора и после нахождения процесса интерпретатора Pascal Script он патчит в нем два места: по адресу 5EB125 вставляет 7 байт, соответствующих переходу на 6C1FAC, и по адресу 6C1FAC вставляет 35 байт, соответствующих коду проверки и патча байт‑кода. В итоге у нас получился некий двухступенчатый патч: лоадер патчит интерпретатор, который, в свою очередь, патчит байт‑код. Мораль: на какие только извращения не приходится идти, если лень делать работу прямым, но тернистым путем!
Автор @МВК
Источник xakep.ru
В статье «Ломаем инсталлятор. Как обмануть инсталлятор MSI методом для ленивых» я писал, что частенько приходится допиливать не только саму программу, но и ее инсталлятор. В той заметке мы рассматривали принципы реверс‑инжиниринга и патчинга инсталляционных пакетов, созданных при помощи инсталлятора InstallShield (MSI). Сегодня нашей целью будет другой популярный пакет — InnoSetup. Думаю, этот тип инсталляторов настолько широко распространен, что не нуждается в подробном описании, поэтому сразу перейдем к конкретной задаче.
Итак, у нас имеется установщик для некоего графического плагина, оформленный в виде самодостаточного исполняемого EXE-модуля. При запуске он просит ввести серийный номер, судя по всему, проверяет его на удаленном сервере и при неправильном вводе выдает сообщение об ошибке.
Открыв наш инсталлятор в Detect It Easy, выясняем сразу два факта: во‑первых, это наш пациент, а во‑вторых, InnoSetup насквозь писан на Delphi.
Как подсказывает опыт, открывать инсталлятор в IDA нет ни малейшего смысла. Прежде всего, это дельфи, тут скорее помог бы IDR. С другой стороны, файл чуть менее чем целиком состоит из упакованного или зашифрованного оверлея (строка Serial Number is invalid предсказуемо не находится в нем в открытом виде), то есть его загрузчик не несет ничего полезного для решения нашей проблемы.
Поэтому сразу попробуем пощупать процедуру проверки серийного номера «изнутри», в процессе работы программы. Загрузив инсталлятор в отладчик x64dbg, мы обнаруживаем, что наши предположения верны. Загрузчик порождает несколько процессов, которые далее живут собственной жизнью независимо от него. В частности, окно сообщения и все остальные диалоговые окна вызываются из процесса, порождаемого модулем, который находится во вложенной папке
\is-JJ5LI.tmp каталога временных файлов системы.При загрузке инсталлятор перво‑наперво создает этот каталог, распаковывает в него данный модуль, который потом запускает, а в конце инсталляции убирает за собой, удаляя и файл, и каталог. Рассмотрим этот модуль более детально.
Строка Serial Number is invalid отсутствует в открытом виде и здесь тоже. Detect It Easy не говорит про модуль ничего внятного, кроме того, что он тоже написан на Delphi и содержит в ресурсе еще один модуль, написанный на Microsoft Visual C.
Попробуем копнуть чуть глубже: аттачимся с помощью x64dbg к процессу в момент появления диалогового окна "Serial Number is invalid". Код вызова MessageBox выглядит примерно так:
Код:
...
005B8CBB | mov dword ptr fs:[ecx],esp
005B8CBE | push esi
; [ebp-8]:L"Setup"
005B8CBF | mov eax,dword ptr ss:[ebp-8]
005B8CC2 | push eax
; edi:L"Serial Number is invalid. Please enter valid license you received or contact support"
005B8CC3 | push edi
005B8CC4 | push ebx
005B8CC5 | call <JMP.&MessageBoxW>
005B8CCA | mov dword ptr ss:[ebp-C],eax
005B8CCD | xor eax,eax
005B8CCF | pop edx
005B8CD0 | pop ecx
...
_Unit72.TApplication.MessageBox, — что ж, вполне логично. Попробуем теперь отследить, откуда было вызвано это сообщение об ошибке.Открываем вкладку «Стек вызовов» и буквально семью вложениями выше (или ниже, кому как больше нравится) обнаруживаем интересный метод _Unit76.TPSExec.RunScript. Этот метод и по названию, и по логике работы сильно напоминает так часто встречаемый нами интерпретатор шитого байт‑кода. Легко и просто находится место выборки и расшифровки текущей команды.
На скриншоте видно, что байт‑код извлекается в регистр esi из потока по адресу [edx+eax], где edx — базовый адрес текущей процедуры, а eax — текущее смещение относительно него. Что же это за скрипты такие и какой байт‑код им соответствует?
Погуглив по названию класса TPSExec, мы сразу натыкаемся на термин Pascal Script. В двух словах — это паскалеподобный скриптовый язык, используемый, в частности, в сценариях InnoSetup.
Как только мы разобрались, с чем имеем дело, дальнейший путь превращается в скоростное шоссе. Для начала попробуем вытащить скомпилированный байт‑код скрипта из инсталлятора. Оказывается, для этого вовсе не обязательно танцевать с бубном, дампя скомпилированный байт‑код из памяти отладчика. Специально обученные энтузиасты создали несколько проектов распаковщиков дистрибутивов InnoSetup, причем с открытым кодом. Например, innoextract и innounp. Запустив innounp.exe из последнего пакета с ключом -m, мы получаем информацию о встроенном в него Pascal-скрипте (не путать с инсталляционным скриптом .iss, представляющим собой список файлов устанавливаемого дистрибутива):
Код:
; Version detected: 6100 (Unicode)
Compression used: lzma
Files: 457 ; Bytes: 218186809
Compiled Pascal script: 10715 byte(s)
По счастью, и здесь от нас не требуется изобретать велосипед — все уже придумано до нас. Слегка погуглив, находим проект IFPSTools, включающий в себя дизассемблер Pascal Script ifpsdasm. Существует даже весьма толковый декомпилятор CompiledCode в исходный паскалевский код Inno Setup Decompiler. К сожалению, проект, похоже, мертв, однако сам декомпилятор все еще можно скачать по ссылке. С него мы и начнем исследовать наш код. Довольно быстро мы находим в нем вызов окна сообщения:
Код:
...
v_58 := 'Status';
v_59 := 0;
v_60 := v_1;
v_54 := IDISPATCHINVOKE(v_60, v_59, v_58, v_55);
v_53 := v_54 < 500;
v_45 := v_45 and v_53;
label_8570:
flag := not v_45;
if flag then goto label_8846; Этот переход надо заменить безусловным
label_8583:
v_62 := 0;
v_63 := 2;
v_64 := 'Serial Number is invalid. Please enter valid license you received or contact support';
v_61 := MSGBOX(v_64, v_63, v_62);
result := 0;
goto label_8858;
label_8846:
result := 1;
label_8858:
goto label_9271;
...
Код:
...
lt Var3, Var4, S32(500)
pop ; StackCount = 3
and Var2, Var3
pop ; StackCount = 2
loc_4ec:
sfz Var2
pop ; StackCount = 1
jf loc_600 ; Этот переход надо заменить безусловным
pushtype S32 ; StackCount = 2
pushtype S32 ; StackCount = 3
assign Var3, S32(0)
pushtype TMSGBOXTYPE ; StackCount = 4
assign Var4, TMSGBOXTYPE(2)
pushtype UnicodeString_2 ; StackCount = 5
assign Var5, UnicodeString_3("Serial Number is invalid. Please enter valid license you received or contact support")
pushvar Var2 ; StackCount = 6
call MSGBOX
pop ; StackCount = 5
pop ; StackCount = 4
pop ; StackCount = 3
pop ; StackCount = 2
pop ; StackCount = 1
assign RetVal, BOOLEAN(0)
jump loc_60c
loc_600:
assign RetVal, BOOLEAN(1)
loc_60c:
...
Система опкодов PascalScript столь специфична, что в открытом доступе таблицы опкодов не найти, как мы это делали для IL или JVM. Немного выручает то, что мы располагаем исходниками ifpsdasm, и, будь у нас чуть больше усидчивости, мы бы, конечно, добавили требуемые функции в наш проект. Но мы, как обычно, пойдем интуитивным путем наименьшего сопротивления. Поискав по исходному коду проекта IFPSTools, мы находим пару файлов, содержащих комментированную информацию о системе команд и опкодах этого интерпретатора:
\IFPSLib\Emit\OpCodes.cs и \IFPSLib\Emit\Code.cs. В частности, последний файл содержит нечто, напоминающее таблицу опкодов:
Код:
...
public enum Code : ushort
{
Assign,
Calculate,
Push,
PushVar,
Pop, // =4 pop
Call,
Jump, // =6 opcode безусловный jump
JumpNZ,
JumpZ,
Ret,
SetStackType,
PushType,
Compare,
CallVar,
SetPtr, // Removed between 2003 and 2005
SetZ,
Neg,
SetFlag, // =0x11 opcode sfz
JumpF, // =0x12 opcode jf
StartEH,
PopEH,
Not,
...
SetFlagNZ = (SetFlag << 8) | (SetFlagOpCode.NotZero),
SetFlagZ = (SetFlag << 8) | (SetFlagOpCode.Zero), // 0x11 00 sfz
...
Код:
pop ; StackCount = 2 <- Выделено красным
loc_4ec:
sfz Var2 <- Выделено желтым
pop ; StackCount = 1 <- Выделено красным
jf loc_600 <- Этот переход надо заменить безусловным, опкод выделен серым
Итак, для полного счастья нам необходимо всего‑навсего исправить в загруженном компилированном байт‑коде выделенный серым байт 0x12 на 06. Проверим эту гипотезу прямо в отладчике, который мы предусмотрительно оставили открытым на интерпретируемом байт‑коде в окне дампа.
Прямо в нем меняем требуемый байт с 12 на 6 и нажимаем кнопку Next. Бинго, любой введенный код принимается, не вызывая ошибки!
Но, как обычно, самая веселая часть работы только начинается.
Мы разобрались, что именно надо поменять в скомпилированном скрипте, чтобы он принимал любой код. Однако как поменять исправленный код в инсталляторе? Поправить аналогично тому, как мы это делали в InstallShield, не получится — алгоритм криптования не сводится к простому XOR по маске, тут полноценный алгоритм сжатия, причем такой экзотический, как LZMA. Вообще говоря, по уму следовало бы перепаковать инсталлятор, хотя это очень непросто.
Возможно, я когда‑нибудь напишу отдельную статью об этом, сейчас же я просто в двух словах обрисую направление, в котором желающие могут самостоятельно попробовать свои силы. Формат организации инсталляционных архивов InnoSetup нигде не документирован, однако, как я уже говорил, есть проекты с открытым кодом innounp и innoextract. Внимательно разобравшись с кодом этих проектов (особенно полезен в этом плане innounp), можно написать свой собственный перепаковщик. Можно найти обходной путь и для основной сложности этого процесса — отсутствия в публичном доступе исходных кодов специфического для InnoSetup компрессора LZMA (да, к сожалению, во всех этих проектах есть только декомпрессор, алгоритм компрессии нигде не документирован). Но формат сжатия подразумевает использование нескольких алгоритмов, помимо LZMA (zlib, bzip или вообще без компрессии). Собственно, самый простой выход — оставить результирующий стрим вообще без сжатия.
А напоследок, как обычно, я раскрою самый ленивый и быстрый способ патча дистрибутива без его пересборки. В этом нам снова поможет лоадер. Для тех, кто прогуливал посвященные лоадерам уроки, я расскажу в двух словах общий принцип этой технологии.
Ее суть заключается в том, что если невозможно поправить программу на диске (при загрузке она динамически расшифровывается или распаковывается в оперативную память компьютера), то для нее создается лоадер — загрузочная программа, которая запускает ее из себя, а потом ищет и заменяет нужные байты прямо в распакованном образе в памяти. Существует множество стандартных готовых лоадеров, но нам нужен специфический, работающий по определенным принципам.
Наш лоадер должен искать в памяти процесс (точнее, все процессы), соответствующий интерпретаторам PascalScript, и патчить его таким образом, чтобы в нужной процедуре интерпретируемого скрипта байт со значением 0x12 по смещению 0x4f4 относительно начала текущей процедуры патчился на 6.
По здравом размышлении мы приходим к выводу, что самый правильный и надежный способ — чтобы интерпретатор сам же и патчил код во время интерпретации. Поэтому встраиваем проверку с патчем прямо в место выборки текущего опкода. Естественно, места для требуемого действия там нет, поэтому ищем в секции свободное место и делаем переход на него с последующим возвратом на следующие команды. Переход длинный, поэтому придется пожертвовать двумя командами.
Было:
Код:
005EB119 | mov eax,dword ptr ss:[ebp-4]
005EB11C | mov eax,dword ptr ds:[eax+5C]
005EB11F | mov edx,dword ptr ss:[ebp-4]
005EB122 | mov edx,dword ptr ds:[edx+54]
005EB125 | movzx esi,byte ptr ds:[edx+eax]
005EB129 | mov eax,dword ptr ss:[ebp-4]
005EB12C | inc dword ptr ds:[eax+5C]
Код:
005EB119 | mov eax,dword ptr ss:[ebp-4]
005EB11C | mov eax,dword ptr ds:[eax+5C]
005EB11F | mov edx,dword ptr ss:[ebp-4]
005EB122 | mov edx,dword ptr ds:[edx+54]
005EB125 | jmp 6C1FAC
005EB12A | nop
005EB12B | nop
005EB12C | inc dword ptr ds:[eax+5C]
Код:
; Команды, «позаимствованные» нами с адреса 5EB125
006C1FAC | movzx esi,byte ptr ds:[edx+eax]
006C1FB0 | mov eax,dword ptr ss:[ebp-4]
; Проверка нужной процедуры: по смещению 4F4 от ее начала должна стоять команда условного перехода 12 07 01 00
006C1FB3 | cmp dword ptr ds:[edx+4F4],10712
; Если нет, то возврат
006C1FBD | jne 5EB12C
; Если да, меняем на безусловный переход, опкод 6
006C1FC3 | mov byte ptr ds:[edx+4F4],6
; Спокойно возвращаемся назад
006C1FCA | jmp 5EB12C
Автор @МВК
Источник xakep.ru