• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Ломаем инсталлятор InnoSetup

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
Если установка программы протекает совсем не так, как хочется пользователю, приходится вмешиваться в работу инсталлятора. Сегодня мы поговорим о том, как устроен инсталляционный пакет InnoSetup, и научимся менять его логику изнутри.

В статье «Ломаем инсталлятор. Как обмануть инсталлятор MSI методом для ленивых» я писал, что частенько приходится допиливать не только саму программу, но и ее инсталлятор. В той заметке мы рассматривали принципы реверс‑инжиниринга и патчинга инсталляционных пакетов, созданных при помощи инсталлятора InstallShield (MSI). Сегодня нашей целью будет другой популярный пакет — InnoSetup. Думаю, этот тип инсталляторов настолько широко распространен, что не нуждается в подробном описании, поэтому сразу перейдем к конкретной задаче.

Итак, у нас имеется установщик для некоего графического плагина, оформленный в виде самодостаточного исполняемого EXE-модуля. При запуске он просит ввести серийный номер, судя по всему, проверяет его на удаленном сервере и при неправильном вводе выдает сообщение об ошибке.

pic1.jpg

Открыв наш инсталлятор в Detect It Easy, выясняем сразу два факта: во‑первых, это наш пациент, а во‑вторых, InnoSetup насквозь писан на Delphi.

pic2.jpg

Как подсказывает опыт, открывать инсталлятор в IDA нет ни малейшего смысла. Прежде всего, это дельфи, тут скорее помог бы IDR. С другой стороны, файл чуть менее чем целиком состоит из упакованного или зашифрованного оверлея (строка Serial Number is invalid предсказуемо не находится в нем в открытом виде), то есть его загрузчик не несет ничего полезного для решения нашей проблемы.

Поэтому сразу попробуем пощупать процедуру проверки серийного номера «изнутри», в процессе работы программы. Загрузив инсталлятор в отладчик x64dbg, мы обнаруживаем, что наши предположения верны. Загрузчик порождает несколько процессов, которые далее живут собственной жизнью независимо от него. В частности, окно сообщения и все остальные диалоговые окна вызываются из процесса, порождаемого модулем, который находится во вложенной папке \is-JJ5LI.tmp каталога временных файлов системы.

При загрузке инсталлятор перво‑наперво создает этот каталог, распаковывает в него данный модуль, который потом запускает, а в конце инсталляции убирает за собой, удаляя и файл, и каталог. Рассмотрим этот модуль более детально.

Строка Serial Number is invalid отсутствует в открытом виде и здесь тоже. Detect It Easy не говорит про модуль ничего внятного, кроме того, что он тоже написан на Delphi и содержит в ресурсе еще один модуль, написанный на Microsoft Visual C.

pic3.jpg

Попробуем копнуть чуть глубже: аттачимся с помощью 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
...
Открыв модуль в IDR и найдя этот фрагмент кода, мы видим, что он является частью метода _Unit72.TApplication.MessageBox, — что ж, вполне логично. Попробуем теперь отследить, откуда было вызвано это сообщение об ошибке.

Открываем вкладку «Стек вызовов» и буквально семью вложениями выше (или ниже, кому как больше нравится) обнаруживаем интересный метод _Unit76.TPSExec.RunScript. Этот метод и по названию, и по логике работы сильно напоминает так часто встречаемый нами интерпретатор шитого байт‑кода. Легко и просто находится место выборки и расшифровки текущей команды.

pic4.jpg

На скриншоте видно, что байт‑код извлекается в регистр 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)
Если мы распакуем дистрибутив этой утилитой с ключом -m, то компилированный код Pascal Script будет сохранен в файл с капитанским названием CompiledCode.bin. Что же за код находится внутри данного файла?

По счастью, и здесь от нас не требуется изобретать велосипед — все уже придумано до нас. Слегка погуглив, находим проект 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;
...
Попробуем теперь найти это место в скомпилированном коде, чтобы поправить его. Дизассемблировав CompiledCode.bin при помощи ifpsdasm, находим ассемблерный эквивалент приведенного выше скриптового кода:
Код:
...
    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:
...
Далее нас ожидает небольшой затык: несмотря на то что и декомпилированный, и дизассемблированный листинги у нас имеются, привязать их к бинарному байт‑коду — не такая уж тривиальная задача. Дело в том, что ifpsdasm весьма специфический инструмент, в котором (как и в IDA, например) нельзя так просто взять и включить смещения и опкоды для каждой команды.

Система опкодов 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            <- Этот переход надо заменить безусловным, опкод выделен серым
pic5.jpg

Итак, для полного счастья нам необходимо всего‑навсего исправить в загруженном компилированном байт‑коде выделенный серым байт 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]
Кусок кода, который мы вставляем по смещению 6C1FAC, выглядит так:
Код:
; Команды, «позаимствованные» нами с адреса 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
Итак, еще раз резюмируем принцип работы лоадера. При загрузке инсталлятора и после нахождения процесса интерпретатора Pascal Script он патчит в нем два места: по адресу 5EB125 вставляет 7 байт, соответствующих переходу на 6C1FAC, и по адресу 6C1FAC вставляет 35 байт, соответствующих коду проверки и патча байт‑кода. В итоге у нас получился некий двухступенчатый патч: лоадер патчит интерпретатор, который, в свою очередь, патчит байт‑код. Мораль: на какие только извращения не приходится идти, если лень делать работу прямым, но тернистым путем!

Автор @МВК
Источник xakep.ru
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх