Хакер - Препарируем P-Code. Как реверсить старый софт на Visual Basic
hacker_frei
МВК
Чтобы немного разнообразить свою жизнь, сегодня мы снова займемся разборкой антиквариата. Многие уже забыли о существовании такого инструмента, как давно похороненный P-Code, в который транслировались программы на Visual Basic. Тем не менее созданные с его помощью приложения до сих пор существуют, и сегодня мы с тобой аккуратно сломаем одно из них.
WARNING
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
К сожалению (или счастью?), Microsoft поэтапно похоронила P-Code, сперва перейдя на компиляцию пи‑кода в натив, от чего, на мой взгляд, он стал сильно более неуклюжим, а затем и вовсе полностью перевела VB на платформу .NET. Тем не менее из‑за своей простоты и популярности в былые времена созданные на P-Code проекты весьма многочисленны и продолжают работать по сей день.
Чтобы ты не испугался, если вдруг в твои руки попадет подобный раритет, знал, что с ним делать и с какой стороны к нему подойти, рассмотрим специфику и внутреннее устройство P-Code на примере доработки небольшой графической программы. Условия задачи таковы: у нас есть софтина, в текущей поставке которой отключены некоторые полезные функции (нет связанных с ними пунктов меню). Но сама черная кошка в темной комнате присутствует, то есть в модуле имеются и нужные нам пункты меню, и код отвечающих за них функций. Просто они невидимы для нашей лицензии, и открыть их — наша задача.
Для начала определяем тип программы и ее защиту при помощи DetectItEasy.

Попытка загрузить исследуемую программу в IDA, к сожалению, не принесла никакого полезного результата.

Настало время для нашего любимого отладчика x64dbg. Немного повозившись в нем, обнаруживаем, что основной код крутится внутри модуля msvbvm60.dll.

Налицо классическая виртуальная машина. В ней интерпретируется поток данных, текущий указатель команды находится в регистре esi. Из потока выбирается байт кода операции, и по его индексу из таблицы адресов обработчиков (6909AA24 в дампе) выбирается адрес обработчика следующей команды, на которую делается переход. По счастью, это не злобный протектор типа Enigma или Themida, никаких обфускаторов и антиотладчиков в коде нет. Более того, нам даже не требуется изобретать велосипед, разбирая виртуальную машину самостоятельно — за время существования VB за нас это сделали другие добрые люди.
Итак, мы наконец вплотную подошли к понятию P-Code. Этот термин был придуман еще полвека назад одним из основоположников современного программирования Никлаусом Виртом для любого аппаратно‑независимого псевдокода виртуальной машины. Однако с легкой руки Microsoft прижилось оно применительно к теперь уже мертвому байт‑коду Visual Basic, преимущественно 6-й версии.
Схема функционирования P-Code предельно проста и видна на скриншоте выше. Из странных особенностей можно отметить наличие целых шести таблиц обработчиков байт‑кодов. Это значит, что не все опкоды псевдокоманд однобайтовые, каждый из последних пяти опкодов главной таблицы (FB-FF) — «составной» двухбайтовый. То есть обработчик каждого из этих кодов считывает следующий байт и передает управление по его индексу на обработчик из другой таблицы адресов, в которой тоже содержится 256 возможных значений. Непонятно, зачем используется такой хитрый способ, а главное — для чего предусмотрен такой запас опкодов, ведь из 1531 возможного задействовано всего 822, причем некоторые дублируют друг друга.
Я не буду приводить здесь полную таблицу опкодов P-Code, желающие могут найти ее на сайте DotFix или даже в более информативном виде в исходниках проекта Semi-VB-Decompiler. Там же приводятся аргументы и примерный смысл каждого опкода.
Вооружившись данной информацией, гораздо продуктивнее разбирать и отлаживать программу даже в x64dbg. Однако существуют и еще более продвинутые инструменты для отладки и декомпиляции пи‑кода. Два из них ты уже, наверное, увидел по приведенным выше ссылкам: свободный декомпилятор Semi-VB-Decompiler и коммерческий VB Decompiler. Еще один незаменимый инструмент для работы с пи‑кодом — свободный дизассемблер‑отладчик VBDec.
Надо сказать, что эти проекты весьма сырые и имеют массу недочетов (к примеру, Semi-VB-Decompiler у меня вообще не декомпилировал ни одного файла и не смог найти валидную сигнатуру), но их возможности дополняют друг друга. К примеру, VB Decompiler умеет с более‑менее переменным успехом не только дизассемблировать, но и восстанавливать VB-код. Причем не только из P-Code, но и из скомпилированного из него натива, и даже из .NET. А хоть VBDec этого и не умеет, зато его дизассемблер на несколько порядков более прямой и безглючный, а главное, у него есть полнофункциональный дебаггер с возможностью трассировки P-Code.
Вернемся к нашей задаче и попробуем решить ее уже с помощью этих чудесных инструментов. Для начала открываем исследуемую программу в VB Decompiler и ищем главное окно. В VB Decompiler для этого имеется инструмент Tools-Search string (он, правда, временами подглючивает, но потерпеть можно). Хоть программа и обфусцирована, но мы находим и главное окно MDIForm1, и первый пункт меню из скрытых из‑за отсутствия лицензии.

Теперь, когда нам известно имя идентификатора нужного пункта меню, мы можем при помощи того же инструмента (поиск строк) поискать его уже по коду. Очень быстро мы обнаруживаем его упоминание в нужном нам контексте:
loc_930D48: var_19C(&HFF).Visible = var_1BC
loc_930D50: GoTo loc_930D97
loc_930D53: End If
loc_930D63: var_D0(&HFF).Visible = MemVar_1619316
loc_930D79: var_15C(&HFF).Visible = var_B0
loc_930D8F: var_C0(&HFF).Visible = var_16C
loc_930D97: ' Referenced from: 930D50
loc_930DA9: If (MemVar_161C794(&HCA) = "1") Then
loc_930DBA: xy0000.Visible = var_E0 <-------------------------
loc_930DC2: GoTo loc_930DDD
loc_930DC5: End If
loc_930DD5: xy0000.Visible = var_19C <-------------------------
loc_930DDD: ' Referenced from: 930DC2
loc_930DEF: If (MemVar_161C794(&HAA) = "1") Then
loc_930E00: var_D0(&HFF).Visible = MemVar_16182F8
loc_930E08: GoTo loc_930E23
loc_930E0B: End If
Надо сказать, код из‑за обфускации получается довольно‑таки безумный, поэтому нашему декомпилятору срывает крышу. Чтобы прояснить ситуацию, открываем закладку Disassembler (при этом, к нашей великой досаде, сбивается текущее смещение в файле, и приходится его заново искать в процедуре). Логика становится чуть яснее:
loc_930D9B: F5CA000000 LitI4 &HCA
loc_930DA0: 051100 ImpAdLdRf MemVar_161C794
loc_930DA3: 9E Ary1LdI4 <-------------- MemVar_161C794(&HCA)
loc_930DA4: 1B6B00 LitStr "1"
loc_930DA7: FB30 EqStr
loc_930DA9: 1C3D1A BranchF loc_930DC5 <----- Если не равно 1, то переход на 930DC5
loc_930DAE: F4FF LitI2_Byte &HFF <----- FF = True
loc_930DB0: 21 FLdPrThis
loc_930DB1: 0F0004 VCallAd Control_ID_
loc_930DB4: 1978FF FStAdFunc var_88
loc_930DB7: 0878FF FLdPr var_88
loc_930DBA: 0D5C008B00 Me.Visible = <----- xy0000.Visible = True
loc_930DBF: 1A78FF FFree1Ad var_88
loc_930DC2: 1E551A Branch loc_930DDD
loc_930DC5: End If
loc_930DC9: F4FF LitI2_Byte &H00 <----- 00 = False
loc_930DCB: 21 FLdPrThis
loc_930DCC: 0F0004 VCallAd Control_ID_
loc_930DCF: 1978FF FStAdFunc var_88
loc_930DD2: 0878FF FLdPr var_88
loc_930DD5: 0D5C008B00 Me.Visible = <----- xy0000.Visible = False
loc_930DDA: 1A78FF FFree1Ad var_88
Вообще говоря, если внимательно присмотреться, мы увидим, что даже в дизассемблировании этот декомпилятор слегка лажает. К примеру, заметно, что по смещению 930DC5 он сожрал целых 4 байта кода. Возможно, это мне попадались глючные версии (я проверял всего пару штук), но с VB Decompiler нужно держать ухо востро: такие мелкие баги попадаются на каждом шагу и сильно раздражают. Другой декомпилятор, VBDec, с дизассемблированием справляется гораздо лучше. Но в нем нет восстановления исходного кода, ибо нет в мире совершенства. Вот тот же участок кода, восстановленный через VDec: явно видно, что VB Decompiler нагло халявит, пропуская все инструкции BoS (LargeBos).
930D9B F5 CA000000 LitI4 0xCA
930DA0 05 1100 ImpAdLdRf unk_161C794
930DA3 9E Ary1LdI4
930DA4 1B 6B00 LitStr str_4CC410='1'
930DA7 FB30 EqStr
930DA9 1C 3D1A BranchF loc_930DC5
930DAC 00 16 BoS loc_930DC2
930DAE F4 FF LitI2_Byte 255
930DB0 21 FLdPrThis
930DB1 0F 0004 VCallAd aym00000.xy0000
930DB4 19 78FF FStAdFunc var_88
930DB7 08 78FF FLdPr var_88
930DBA 0D 5C008B00 VCallHresult _Menu Let Visible
930DBF 1A 78FF FFree1Ad var_88
930DC2 1E 551A Branch loc_930DDD
930DC5 00 02 BoS loc_930DC7
930DC7 00 16 BoS loc_930DDD
930DC9 F4 FF LitI2_Byte 0
930DCB 21 FLdPrThis
930DCC 0F 0004 VCallAd aym00000.xy0000
930DCF 19 78FF FStAdFunc var_88
930DD2 08 78FF FLdPr var_88
930DD5 0D 5C008B00 VCallHresult _Menu Let Visible
930DDA 1A 78FF FFree1Ad var_88
Итак, мы нашли искомое место в коде, теперь его неплохо бы пропатчить. Здесь у нас имеется два пути: либо поправить на True аргумент команды LitI2_Byte по адресу 930DC9, дабы пункт меню оставался видимым при любом результате проверки, либо убрать саму проверку. К нашему ужасу, и здесь мы оказываемся в большой беде: у пи‑кода внезапно нет однобайтовой команды NOP, которой мы привыкли забивать убираемый код в других системах. Придется выкручиваться окольными путями, отдельные из которых могут быть весьма оригинальными. К примеру, David Zimmer предлагает самому сконструировать NOP из команды InvalidExcode (опкод 1), заменив ее обработчик в таблице чуть модифицированным адресом обработчика StAry:
call ___vbaAryMove@8 ; Оригинальный обработчик StAry
xor eax, eax ; Новый адрес обработчика для псевдокоманды InvalidExcode, который превращает ее в NOP
mov al, [esi]
inc esi
jmp ds:_tblByteDisp[eax*4]
К слову сказать, отладчик VBDec использует именно такой метод для занопливания кода, называя новую инструкцию vbDecNop.
При всей своей оригинальности описанный выше способ предполагает патчинг системной библиотеки msvbvm60.dll, что крайне нежелательно делать ради такой мелочи, как создание однобайтового NOP. По счастью, однобайтовый NOP и не требуется, ведь существует двухбайтовый NOP — это псевдокоманда CI2UI1 (код FC14). B качестве же трехбайтового NOP можно использовать команду безусловного перехода Branch (1EXXXX, где XXXX — смещение до следующей инструкции относительно начала текущей функции).
В нашем случае достаточно поправить смещение условного перехода по адресу 930DA9. Еще одна весьма специфичная особенность пи‑кода — отсчет смещений в псевдокомандах перехода относительно не текущего адреса псевдокоманды, а, как я уже говорил, начала текущей функции. В нашем примере 92F389 — начало процедуры, аргумент псевдокоманды BranchF 1A3D. В итоге мы получим адрес перехода 92F388+1A3D=930DС5, нам же нужен адрес следующей за BranchF команды 930DAC: 930DAC-92F388=1A24.
Для патча можно использовать обычный hiew, в нем есть возможность указывать не только смещение в файле, но и виртуальный адрес байта при загрузке в память (у нас указаны именно они). Меняем байт 3D по адресу 930DAA на 24 и убеждаемся, что искомый пункт меню теперь виден и даже активен. Таким же способом находим и меняем код для остальных пунктов — задача решена.
Как ты уже понял, за свою долгую историю VB и его пи‑код очень неплохо исследован. Для его ковыряния создано множество разных инструментов. Поэтому в заключение я хочу упомянуть об одном из них, который, как мне кажется, наиболее востребован.
Частенько случается, что надо поменять в программе текстовую строку или подправить элемент интерфейса. Хорошо, если строки или описания элементов содержатся в стандартных ресурсах Windows, для которых существует множество редакторов. Однако в EXE-файле, скомпилированном из VB, среди обычных ресурсов имеется только иконка и версия. Собственные же ресурсы VB имеют свою специфику и отображаются только в описанных выше декомпиляторах‑дизассемблерах. Если же тебе надо что‑либо в них подправить и неохота возиться с подгонкой строк в hiew/winhex, то на помощь придет утилита VBLocalize. Хотя она довольно старая, но при желании ее вполне можно найти на просторах интернета и использовать для правки отдельных строк или даже для полного перевода интерфейса программы, если, конечно, на это хватит твоего энтузиазма.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei