Русский
Русский
English
Статистика
Реклама

Reverse engineering

RATKing новая кампания с троянами удаленного доступа

26.06.2020 12:10:59 | Автор: admin
Вконце мая мыобнаружили кампанию распространения ВПО класса Remote Access Trojan (RAT) программ, которые позволяют злоумышленникам удаленно управлять зараженной системой.

Рассматриваемая нами группировка отличилась тем, что она невыбрала для заражения какое-то определенное семейство RAT. Ватаках врамках кампании были замечены сразу несколько троянов (все вшироком доступе). Этой чертой группировка напомнила нам окрысином короле мифическом животном, которое состоит изгрызунов спереплетенными хвостами.



Оригинал взят измонографии К.Н.Россикова Мыши имышевидные грызуны, наиболее важные вхозяйственном отношении (1908г.)

Вчесть этого существа мыназвали рассматриваемую нами группировку RATKing. Вэтом посте мырасскажем подробно отом, как злоумышленники проводили атаку, какие инструменты они использовали, атакже поделимся своими соображениями относительно атрибуции этой кампании.

Ход атаки


Все атаки вэтой кампании проходили последующему алгоритму:

  1. Пользователь получал фишинговое письмо соссылкой наGoogle Drive.
  2. Поссылке жертва скачивала вредоносный VBS-скрипт, который прописывал DLL-библиотеку для загрузки конечного пейлоада вреестр Windows изапускал PowerShell, чтобы исполнитьее.
  3. DLL-библиотека внедряла конечный пейлоад собственно, один изиспользуемых злоумышленниками RAT всистемный процесс ипрописывала VBS-скрипт вавтозапуск, чтобы закрепиться взараженной машине.
  4. Конечный пейлоад исполнялся всистемном процессе идавал злоумышленнику возможность управлять зараженным компьютером.

Схематически это можно представить так:



Далее мысосредоточимся напервых трех этапах, поскольку нас интересует именно механизм доставки ВПО. Мынестанем подробно описывать механизм работы самих вредоносов. Они находятся вшироком доступе либо продаются наспециализированных форумах, либо ивовсе распространяются как проекты соткрытым исходным кодом, азначит, неуникальны для группировки RATKing.

Анализ этапов атаки


Этап1. Фишинговая рассылка


Атака начиналась стого, что жертва получала вредоносное письмо (злоумышленники использовали разные шаблоны стекстом, наскриншоте ниже приведен один изпримеров). Всообщении была ссылка налегитимное хранилище drive.google.com, которая якобы вела настраницу загрузки документа вформате PDF.



Пример фишингового письма

Однако наделе загружался вовсе неPDF-документ, аVBS-скрипт.

При переходе поссылке изписьма наскриншоте выше загружался файл сименем Cargo Flight Details.vbs. Вэтом случае злоумышленники даже непытались замаскировать файл под легитимный документ.

Втоже время врамках этой кампании мыобнаружили скрипт сименем Cargo Trip Detail.pdf.vbs. Онуже мог сойти залегитимный PDF, потому что поумолчанию Windows скрывает расширение файлов. Правда, вэтом случае подозрение все еще могла вызвать его иконка, соответствовавшая VBS-скрипту.

Наэтом этапе жертва могла распознать обман: достаточно насекунду присмотреться кскачиваемым файлам. Однако втаких фишинговых кампаниях злоумышленники зачастую рассчитывают именно наневнимательного или спешащего пользователя.

Этап2. Работа VBS-скрипта


VBS-скрипт, который пользователь мог открыть понеосторожности, прописывал DLL-библиотеку вреестр Windows. Скрипт был обфусцирован: строки внем записаны ввиде байтов, разделенных произвольным символом.



Пример обфусцированного скрипта

Алгоритм деобфускации достаточно прост: изобфусцированной строки исключался каждый третий символ, после чего результат декодировался изbase16в исходную строку. Например, иззначения 57Q53s63t72s69J70r74e2El53v68m65j6CH6Ct (выделено наскриншоте выше) получалась строка WScript.Shell.

Для деобфускации строк мыиспользовали функцию наPython:

def decode_str(data_enc):       return binascii.unhexlify(''.join([data_enc[i:i+2] for i in range(0, len(data_enc), 3)]))

Ниже настроках 910 выделено значение, при деобфускации которого получался DLL-файл. Именно онзапускался наследующем этапе спомощью PowerShell.



Строка собфусцированным DLL

Каждая функция вVBS-скрипте выполнялась помере деобфускации строк.

После запуска скрипта вызывалась функция wscript.sleep сеепомощью выполнялось отложенное исполнение.

Далее скрипт работал среестром Windows. Ониспользовал для этого технологию WMI. Сеепомощью создавался уникальный ключ, ивего параметр записывалось тело исполняемого файла. Обращение креестру через WMI выполнялось спомощью следующей команды:

GetObject(winmgmts {impersonationLevel=impersonate}!\\.\root\default:StdRegProv)



Запись, сделанная вреестре VBS-скриптом

Этап3. Работа DLL-библиотеки


Натретьем этапе вредоносная DLL-библиотека загружала конечный пейлоад, внедряла его всистемный процесс иобеспечивала автозапуск VBS-скрипта при входе пользователя всистему.

Запуск через PowerShell

DLL-библиотека исполнялась спомощью следующей команды вPowerShell:

[System.Threading.Thread]::GetDomain().Load((ItemProperty HKCU:\/\/\/Software\/\/\/<rnd_sub_key_name> ).<rnd_value_name>);[GUyyvmzVhebFCw]::EhwwK('WScript.ScriptFullName', 'rWZlgEtiZr', 'WScript.ScriptName'),0

Эта команда делала следующее:

  • получала данные значения реестра сименем rnd_value_name эти данные представляли собой DLL-файл, написанный наплатформе .Net;
  • загружала полученный .Net-модуль впамять процесса powershell.exeспомощью функции [System.Threading.Thread]::GetDomain().Load()(подробное описание функции Load() доступно насайте Microsoft);
  • исполняла функциюGUyyvmzVhebFCw]::EhwwK() снее начиналось исполнение DLLбиблиотеки спараметрами vbsScriptPath, xorKey, vbsScriptName. Параметр xorKey хранил ключ для расшифровки конечного пейлоада, апараметры vbsScriptPathиvbsScriptName передавались для того, чтобы прописать VBS-скрипт вавтозапуск.

Описание DLL-библиотеки

Вдекомпилированном виде загрузчик выглядел так:



Загрузчик вдекомпилированном виде (красным подчеркнута функция, скоторой начиналось исполнение DLL-библиотеки)

Загрузчик защищен протектором .Net Reactor. Соснятием данного протектора отлично справляется утилита de4dot.

Данный загрузчик:

  • осуществлял инжект пейлоада всистемный процесс (вданном примере это svchost.exe);
  • прописывал VBS-скрипт вавтозапуск.

Инжект пейлоада

Рассмотрим функцию, которую вызывал PowerShell-скрипт.



Функция, вызываемая PowerShell-скриптом

Данная функция осуществляла следующие действия:

  • расшифровывала два массива данных (array иarray2 наскриншоте). Первоначально они были сжаты спомощью gzip изашифрованы алгоритмом XOR сключом xorKey;
  • копировала данные ввыделенные области памяти. Данные изarray вобласть памяти, накоторую указывал intPtr(payload pointer наскриншоте); данные изarray2 вобласть памяти, накоторую указывал intPtr2(shellcode pointer наскриншоте);
  • вызывала функцию CallWindowProcA(описание этой функции есть насайте Microsoft) соследующими параметрами (ниже перечислены имена параметров, наскриншоте они идут втомже порядке, носрабочими значениями):
    • lpPrevWndFuncуказатель наданные из array2;
    • hWndуказатель настроку, содержащую путь кисполняемому файлу svchost.exe;
    • Msgуказатель наданные из array;
    • wParam,lParam параметры сообщения (вданном случае эти параметры неиспользовались иимели значения 0);
  • создавала файл %AppData%\Microsoft\Windows\Start Menu\Programs\Startup\<name>.url, где <name> это первые 4символа параметра vbsScriptName(наскриншоте фрагмент кода сэтим действием начинается скоманды File.Copy). Таким образом вредонос добавлял URL-файл всписок файлов для автозапуска при входе пользователя всистему итем самым закреплялся назараженном компьютере. URL-файл содержал ссылку наскрипт:

[InternetShortcut]URL = file : ///<vbsScriptPath>

Для понимания того, как осуществлялся инжект, мырасшифровали массивы данных array и array2. Для этого мыиспользовали следующую функцию наPython:

def decrypt(data, key):    return gzip.decompress(        bytearray([data[i] ^ key[i % len(key)] for i in range(len(data))])[4:])    

Врезультате мывыяснили, что:

  • array представлял собой PE-файл это иесть конечный пейлоад;
  • array2 представлял собой шелл-код, необходимый для осуществления инжекта.

Шелл-код измассива array2 передавался вкачестве значения функции lpPrevWndFunc вфункцию CallWindowProcA. lpPrevWndFunc функция обратного вызова, еепрототип выглядит так:

LRESULT WndFunc(  HWND    hWnd,  UINT    Msg,  WPARAM  wParam,  LPARAM  lParam);

Таким образом, при запуске функции CallWindowProcA спараметрами hWnd, Msg, wParam, lParamисполняется шелл-код измассива array2 саргументами hWnd и Msg. hWnd это указатель настроку, содержащую путь кисполняемому файлу svchost.exe, а Msg указатель наконечный пейлоад.

Шелл-код получал адреса функций изkernel32.dllиntdll32.dllпозначениям хешей отихимен ивыполнял инжект конечного пейлоада впамять процесса svchost.exe, используя технику Process Hollowing (подробно оней можно прочитать вэтой статье). При инжекте шелл-код:

  • создавал процесс svchost.exe вприостановленном состоянии при помощи функции CreateProcessW;
  • затем скрывал отображение секции вадресном пространстве процесса svchost.exeпри помощи функции NtUnmapViewOfSection. Таким образом программа освобождала память оригинального процесса svchost.exe, чтобы затем поэтому адресу выделить память для пейлоада;
  • выделял память для пейлоада вадресном пространстве процесса svchost.exe при помощи функции VirtualAllocEx;



Начало процесса инжекта

  • записывал содержимое пейлоада вадресное пространство процесса svchost.exe при помощи функции WriteProcessMemory (как наскриншоте ниже);
  • возобновлял процесс svchost.exe при помощи функции ResumeThread.



Завершение процесса инжекта

Загружаемое ВПО


Врезультате описанных действий взараженной системе устанавливалась одна изнескольких вредоносных программ класса RAT. Втаблице ниже перечислены использованные ватаке вредоносы, которые мысуверенностью можем приписать одной группе злоумышленников, поскольку семплы обращались кодному итомуже серверу управления.
Название ВПО
Впервые замечено
SHA-256
C&C
Процесс, вкоторый осуществляется инжект
Darktrack
16-04-2020
ea64fe672c953adc19553ea3b9118ce4ee88a14d92fc7e75aa04972848472702
kimjoy007.dyndns[.]org:2017
svchost
Parallax
24-04-2020
b4ecd8dbbceaadd482f1b23b712bcddc5464bccaac11fe78ea5fd0ba932a4043
kimjoy007.dyndns[.]org:2019
svchost
WARZONE
18-05-2020
3786324ce3f8c1ea3784e5389f84234f81828658b22b8a502b7d48866f5aa3d3
kimjoy007.dyndns[.]org:9933
svchost
Netwire
20-05-2020
6dac218f741b022f5cad3b5ee01dbda80693f7045b42a0c70335d8a729002f2d
kimjoy007.dyndns[.]org:2000
svchost

Примеры распространяемого ВПО содним итемже сервером управления

Здесь примечательны две вещи.

Во-первых, сам факт, что злоумышленники использовали сразу несколько различных семейств RAT. Такое поведение нехарактерно для известных кибергруппировок, которые зачастую используют приблизительно одинаковый набор привычных для них инструментов.

Во-вторых, RATKing использовали вредоносы, которые либо продаются наспециализированных форумах занебольшую цену, либо ивовсе являются проектами соткрытым исходным кодом.

Более полный перечень использованного вкампании ВПО содной важной оговоркой приведен вконце статьи.

Огруппировке


Мынеможем отнести описанную вредоносную кампанию ккаким-либо известным злоумышленникам. Пока мысчитаем, что эти атаки совершила принципиально новая группировка. Как мыуже писали вначале, мыназвали ееRATKing.

Для создания VBS-скрипта группировка, вероятно, использовала инструмент, похожий наутилиту VBS-Crypter отразработчика NYAN-x-CAT. Наэто указывает схожесть скрипта, который создает эта программа, соскриптом злоумышленников. Вчастности, они оба:

  • осуществляют отложенное исполнение спомощью функции Sleep;
  • используют WMI;
  • прописывают тело исполняемого файла вкачестве параметра ключа реестра;
  • исполняют этот файл при помощи PowerShell вегоже адресном пространстве.

Для наглядности сравните команду PowerShell для запуска файла изреестра, которую использует скрипт, созданный спомощью VBS-Crypter:

((Get-ItemPropertyHKCU:\Software\NYANxCAT\).NYANxCAT);$text=-join$text[-1..-$text.Length];[AppDomain]::CurrentDomain.Load([Convert]::FromBase64String($text)).EntryPoint.Invoke($Null,$Null);

саналогичной командой, которую использовал скрипт злоумышленников:

[System.Threading.Thread]::GetDomain().Load((ItemProperty HKCU:\/\/\/Software\/\/\/<rnd_sub_key_name> ).<rnd_value_name>);[GUyyvmzVhebFCw]::EhwwK('WScript.ScriptFullName', 'rWZlgEtiZr', 'WScript.ScriptName'),0

Заметим, что вкачестве одного изпейлоадов злоумышленники использовали другую утилиту отNYAN-x-CAT LimeRAT.

Адреса C&C-серверов указывают наеще одну отличительную черту RATKing: группировка предпочитает сервисы динамического DNS (см. перечень C&Cв таблице сIoC).

IoC


Втаблице ниже приведен полный перечень VBS-скриптов, которые сбольшой вероятностью можно отнести кописанной кампании. Все эти скрипты похожи иосуществляют примерно одинаковую последовательность действий. Все они инжектят ВПО класса RAT вдоверенный процесс Windows. Увсех них адреса C&C зарегистрированы сиспользованием Dynamic DNS-сервисов.

Тем неменее, мынеможем утверждать, что все эти скрипты распространялись одними итемиже злоумышленниками, заисключением семплов содинаковыми адресами C&C (например, kimjoy007.dyndns.org).
Название ВПО
SHA-256
C&C
Процесс, вкоторый осуществляется инжект
Parallax
b4ecd8dbbceaadd482f1b23b712bcddc5464bccaac11fe78ea5fd0ba932a4043
kimjoy007.dyndns.org
svchost
00edb8200dfeee3bdd0086c5e8e07c6056d322df913679a9f22a2b00b836fd72
hope.doomdns.org
svchost
504cbae901c4b3987aa9ba458a230944cb8bd96bbf778ceb54c773b781346146
kimjoy007.dyndns.org
svchost
1487017e087b75ad930baa8b017e8388d1e99c75d26b5d1deec8b80e9333f189
kimjoy007.dyndns.org
svchost
c4160ec3c8ad01539f1c16fb35ed9c8c5a53a8fda8877f0d5e044241ea805891
franco20.dvrdns.org
svchost
515249d6813bb2dde1723d35ee8eb6eeb8775014ca629ede017c3d83a77634ce
kimjoy007.dyndns.org
svchost
1b70f6fee760bcfe0c457f0a85ca451ed66e61f0e340d830f382c5d2f7ab803f
franco20.dvrdns.org
svchost
b2bdffa5853f29c881d7d9bff91b640bc1c90e996f85406be3b36b2500f61aa1
hope.doomdns.org
svchost
c9745a8f33b3841fe7bfafd21ad4678d46fe6ea6125a8fedfcd2d5aee13f1601
kimjoy007.dyndns.org
svchost
1dfc66968527fbd4c0df2ea34c577a7ce7a2ba9b54ba00be62120cc88035fa65
franco20.dvrdns.org
svchost
c6c05f21e16e488eed3001d0d9dd9c49366779559ad77fcd233de15b1773c981
kimjoy007.dyndns.org
cmd
3b785cdcd69a96902ee62499c25138a70e81f14b6b989a2f81d82239a19a3aed
hope.doomdns.org
svchost
4d71ceb9d6c53ac356c0f5bdfd1a5b28981061be87e38e077ee3a419e4c476f9
2004para.ddns.net
svchost
00185cc085f284ece264e3263c7771073a65783c250c5fd9afc7a85ed94acc77
hope.doomdns.org
svchost
0342107c0d2a069100e87ef5415e90fd86b1b1b1c975d0eb04ab1489e198fc78
franco20.dvrdns.org
svchost
de33b7a7b059599dc62337f92ceba644ac7b09f60d06324ecf6177fff06b8d10
kimjoy007.dyndns.org
svchost
80a8114d63606e225e620c64ad8e28c9996caaa9a9e87dd602c8f920c2197007
kimjoy007.dyndns.org
svchost
acb157ba5a48631e1f9f269e6282f042666098614b66129224d213e27c1149bb
hope.doomdns.org
cmd
bf608318018dc10016b438f851aab719ea0abe6afc166c8aea6b04f2320896d3
franco20.dvrdns.org
svchost
4d0c9b8ad097d35b447d715a815c67ff3d78638b305776cde4d90bfdcb368e38
hope.doomdns.org
svchost
e7c676f5be41d49296454cd6e4280d89e37f506d84d57b22f0be0d87625568ba
kimjoy007.dyndns.org
svchost
9375d54fcda9c7d65f861dfda698e25710fda75b5ebfc7a238599f4b0d34205f
franco20.dvrdns.org
svchost
128367797fdf3c952831c2472f7a308f345ca04aa67b3f82b945cfea2ae11ce5
kimjoy007.dyndns.org
svchost
09bd720880461cb6e996046c7d6a1c937aa1c99bd19582a562053782600da79d
hope.doomdns.org
svchost
0a176164d2e1d5e2288881cc2e2d88800801001d03caedd524db365513e11276
paradickhead.homeip.net
svchost
0af5194950187fd7cbd75b1b39aab6e1e78dae7c216d08512755849c6a0d1cbe
hope.doomdns.org
svchost
Warzone
3786324ce3f8c1ea3784e5389f84234f81828658b22b8a502b7d48866f5aa3d3
kimjoy007.dyndns.org
svchost
db0d5a67a0ced6b2de3ee7d7fc845a34b9d6ca608e5fead7f16c9a640fa659eb
kimjoy007.dyndns.org
svchost
Netwire
6dac218f741b022f5cad3b5ee01dbda80693f7045b42a0c70335d8a729002f2d
kimjoy007.dyndns.org
svchost
Darktrack
ea64fe672c953adc19553ea3b9118ce4ee88a14d92fc7e75aa04972848472702
kimjoy007.dyndns.org
svchost
WSH RAT
d410ced15c848825dcf75d30808cde7784e5b208f9a57b0896e828f890faea0e
anekesolution.linkpc.net
RegAsm

Lime


896604d27d88c75a475b28e88e54104e66f480bcab89cc75b6cdc6b29f8e438b
softmy.duckdns.org
RegAsm
QuasarRAT
bd1e29e9d17edbab41c3634649da5c5d20375f055ccf968c022811cd9624be57
darkhate-23030.portmap.io
RegAsm
12044aa527742282ad5154a4de24e55c9e1fae42ef844ed6f2f890296122153b
darkhate-23030.portmap.io
RegAsm
be93cc77d864dafd7d8c21317722879b65cfbb3297416bde6ca6edbfd8166572
darkhate-23030.portmap.io
RegAsm
933a136f8969707a84a61f711018cd21ee891d5793216e063ac961b5d165f6c0
darkhate-23030.portmap.io
RegAsm
71dea554d93728cce8074dbdb4f63ceb072d4bb644f0718420f780398dafd943
chrom1.myq-see.com
RegAsm
0d344e8d72d752c06dc6a7f3abf2ff7678925fde872756bf78713027e1e332d5
darkhate-23030.portmap.io
RegAsm
0ed7f282fd242c3f2de949650c9253373265e9152c034c7df3f5f91769c6a4eb
darkhate-23030.portmap.io
RegAsm
aabb6759ce408ebfa2cc57702b14adaec933d8e4821abceaef0c1af3263b1bfa
darkhate-23030.portmap.io
RegAsm
1699a37ddcf4769111daf33b7d313cf376f47e92f6b92b2119bd0c860539f745
darkhate-23030.portmap.io
RegAsm
3472597945f3bbf84e735a778fd75c57855bb86aca9b0a4d0e4049817b508c8c
darkhate-23030.portmap.io
RegAsm
809010d8823da84cdbb2c8e6b70be725a6023c381041ebda8b125d1a6a71e9b1
darkhate-23030.portmap.io
RegAsm
4217a2da69f663f1ab42ebac61978014ec4f562501efb2e040db7ebb223a7dff
darkhate-23030.portmap.io
RegAsm
08f34b3088af792a95c49bcb9aa016d4660609409663bf1b51f4c331b87bae00
darkhate-23030.portmap.io
RegAsm
79b4efcce84e9e7a2e85df7b0327406bee0b359ad1445b4f08e390309ea0c90d
darkhate-23030.portmap.io
RegAsm
12ea7ce04e0177a71a551e6d61e4a7916b1709729b2d3e9daf7b1bdd0785f63a
darkhate-23030.portmap.io
RegAsm
d7b8eb42ae35e9cc46744f1285557423f24666db1bde92bf7679f0ce7b389af9
darkhate-23030.portmap.io
RegAsm
def09b0fed3360c457257266cb851fffd8c844bc04a623c210a2efafdf000d5c
darkhate-23030.portmap.io
RegAsm
50119497c5f919a7e816a37178d28906fb3171b07fc869961ef92601ceca4c1c
darkhate-23030.portmap.io
RegAsm
ade5a2f25f603bf4502efa800d3cf5d19d1f0d69499b0f2e9ec7c85c6dd49621
darkhate-23030.portmap.io
RegAsm
189d5813c931889190881ee34749d390e3baa80b2c67b426b10b3666c3cc64b7
darkhate-23030.portmap.io
RegAsm
c3193dd67650723753289a4aebf97d4c72a1afe73c7135bee91c77bdf1517f21
darkhate-23030.portmap.io
RegAsm
a6f814f14698141753fc6fb7850ead9af2ebcb0e32ab99236a733ddb03b9eec2
darkhate-23030.portmap.io
RegAsm
a55116253624641544175a30c956dbd0638b714ff97b9de0e24145720dcfdf74
darkhate-23030.portmap.io
RegAsm
d6e0f0fb460d9108397850169112bd90a372f66d87b028e522184682a825d213
darkhate-23030.portmap.io
RegAsm
522ba6a242c35e2bf8303e99f03a85d867496bbb0572226e226af48cc1461a86
darkhate-23030.portmap.io
RegAsm
fabfdc209b02fe522f81356680db89f8861583da89984c20273904e0cf9f4a02
darkhate-23030.portmap.io
RegAsm
08ec13b7da6e0d645e4508b19ba616e4cf4e0421aa8e26ac7f69e13dc8796691
darkhate-23030.portmap.io
RegAsm
8433c75730578f963556ec99fbc8d97fa63a522cef71933f260f385c76a8ee8d
darkhate-23030.portmap.io
RegAsm
99f6bfd9edb9bf108b11c149dd59346484c7418fc4c455401c15c8ac74b70c74
darkhate-23030.portmap.io
RegAsm
d13520e48f0ff745e31a1dfd6f15ab56c9faecb51f3d5d3d87f6f2e1abe6b5cf
darkhate-23030.portmap.io
RegAsm
9e6978b16bd52fcd9c331839545c943adc87e0fbd7b3f947bab22ffdd309f747
darkhate-23030.portmap.io
RegAsm
Подробнее..

Перевод Руткиты на основе BIOS. Часть 1

03.08.2020 16:04:44 | Автор: admin
Привет, Хабровчане!
В конце августа в OTUS запускается 2 мощных курса по обратной разработке кода (реверс-инжиниринг). В связи с этим приглашаем вас на День Открытых дверей, где Артур Пакулов (Ex-вирусный аналитик в Kaspersky Lab.) расскажет подробнее о программах, особенностях онлайн-формата, навыках, компетенциях и перспективах, которые ждут выпускников после обучения. А также приглашаем вас принять участие в бесплатных открытых уроках: Анализ буткита и Анализ банковского трояна.



Предпосылки


Все описанное здесь основано на проекте, который я завершил в начале 2011 года, спустя аж несколько лет после его начала. Принимая участие в CanSecWest в 2009 году, Анибал Сакко и Альфредо Ортега из Core Security провели презентацию Persistent BIOS Infection, где продемонстрировали, как можно пропатчить BIOS, чтобы совершить некоторые неприятные/удивительные вещи. Можете ознакомится с их докладом здесь. На то время это действительно впечатляло, но мне так и не выпал шанс попробовать это на практике. Год спустя мне нужно было подготовить групповой проект для учебы, поэтому я решил вернуться к взлому BIOS и самостоятельно реализовать что-нибудь из этого.


За последние несколько лет в мире BIOS для ПК многое изменилось, и теперь подписанный встроенный BIOS является стандартом, а архитектура UEFI внесла множество изменений по сравнению с традиционным дизайном BIOS. Менее чем через год после того, как я завершил этот проект, были обнаружены первые признаки того, что вредоносные программы действительно заражают BIOS в несколько похожей манере (ссылка). Простые меры, такие как подпись обновлений BIOS, легко предотвратили бы модификации BIOS такого типа. Также уже были проведены некоторые исследования для поиска путей решения этой проблемы (например, презентация Attacking Intel BIOS Рафаля Войчука и Александра Терешкина). Поэтому стоит отметить, что ничего из того, что описано здесь, не предназначено для представления миру какой-либо новой уязвимости, а скорее является демонстрацией концепции, которую можно легко проверить и изменить.


Моя первоначальная цель в этом проекте состояла в том, чтобы оценить реальную возможность атак и существования программ нацеленных на BIOS, и продемонстрировать их, если я что-нибудь обнаружу. Я думаю, что эта тема все еще актуальна и по сей день, несмотря на то, что новые технологии неуклонно делают этот тип атак менее релевантным. Но даже абстрагировавшись от цели, знание того, как делать интересные модификации BIOS в ассемблере, само по себе уже чего-то стоит, так что я решил опубликовать этот проект в интернете, чтобы другие тоже могли его оценить!


Если вы не горите желанием читать все это, а просто хотите поработать с результатами, жмите сюда, чтобы перейти в заключительную часть статьи, где вы найдете ссылку на образец BIOS и некоторые краткие инструкции.


Подход


В настоящее время существует весьма ограниченное количество примеров кода, подходящих для создания BIOS-руткитов. В публичном доступе можно найти только один из них, представленный миру на первой демонстрации BIOS-руткитов в марте 2009 года (насколько я знаю). Моей изначальной целью было воспроизвести находки, сделанные Core Security в 2009 году, а затем выяснить, как я могу их модифицировать. Моей конечной целью было создание своего рода BIOS-руткита, который можно было бы легко развернуть.


В 2009 году было проведено исследование в смежной области безопасности, которая занимается руткитами загрузочного сектора. В отличие от BIOS-руткитов, разработки в этой области развивались очень быстрыми темпами, что привело к созданию и выпуску ряда различных MBR(master boot record)-руткитов. Этот тип руткитов был назван Bootkit, и, подобно BIOS-руткиту, он стремится загрузить себя до загрузки ОС. Это сходство побудило многих разработчиков буткитов отметить, что данная атака должна быть осуществимой непосредственно из BIOS, а не из MBR. Несмотря на комментарии и предложения о том, что код буткита можно было перенести для выполнения в BIOS, пока не было опубликовано ни одного примера такого кода.


Первым этапом реализации этого проекта было создание среды тестирования и разработки, в которой можно было бы вносить и отлаживать модификации BIOS. В своей статье об устойчивом заражении BIOS Сакко и Ортега подробно описывают, что они обнаружили, что VMware содержит BIOS ROM, а также GDB сервер, который можно использовать для отладки приложений, начиная с самого BIOS. После успешной работы в VMware была проделана работа по переносу модификаций BIOS VMware на другие подобные BIOS это будет описано во второй половине этой статьи.


Конфигурация VMware BIOS


Ладно, достаточно предыстории, займемся!


Первый необходимый шаг извлечь BIOS из самой VMware. В Windows это можно сделать, открыв исполняемый файл vmware-vmx.exe с помощью любого экстрактора ресурсов, например, Resource Hacker. В этом приложении объединено несколько различных двоичных ресурсов, и BIOS хранится в ресурсе с идентификатором 6006 (по крайней мере, в VMware 7). Это может разниться от версии к версии, но ключевым моментом является размер файла ресурса 512 КБ. На следующем изображении показано, как это выглядит в Resource Hacker:



Хотя этот BIOS-образ связан с приложением vmware-vmx.exe, его также можно использовать отдельно, без необходимости вносить правки в исполняемый файл VMware после каждого изменения. VMware позволяет указывать ряд скрытых параметров в файле настроек образа VMX. В какой-то момент я планирую документировать некоторые их них на вкладке Tools этого сайта, потому что они действительно очень полезны! Вот те, которые пригодятся для модификации и отладки BIOS:


bios440.filename = "BIOS.ROM"debugStub.listen.guest32 = "TRUE"debugStub.hideBreakpoint = "TRUE"monitor.debugOnStartGuest32 = "TRUE"

Первая настройка позволяет BIOS ROM загружаться не из приложения vmware-vmx, а из файла. Следующие две строки подключают встроенный GDB сервер. Этот сервер прослушивает соединения через порт 8832, когда образ работает. Последняя строка указывает VMware остановить выполнение кода в первой строке гостевого образа BIOS. Это очень полезно, так как позволяет определять точки останова и проверять память до того, как произойдет какое-либо выполнение BIOS. Тестирование было выполнено с использованием IDA Pro в качестве GDB клиента. Пример гостевого образа VMware, остановленного на первой инструкции BIOS, можно увидеть на скриншоте ниже:



При первом использовании этой тестовой среды у меня были значительные проблемы с подключением IDA к GDB серверу. После долгих проб, ошибок и тестирования с различными GDB клиентами было выявлено, что виновата моя версия VMware. Версии 6 и 6.5, похоже, не очень хорошо работают с IDA, поэтому для большинства испытаний мной использовалась VMware 7. BIOS состоит из 16-битного кода, а не 32-битного кода по умолчанию для IDA, поэтому было необходимо определить Manual Memory Regions в параметрах отладки IDA. Это позволило адресам памяти быть определенным как 16-битный код, чтобы они правильно декомпилировались.


Воссоздание готовых наработок модификация VMware BIOS


Как уже отмечалось, Сакко и Ортега провели две презентации по модификации BIOS, наряду с презентацией Войчука и Терешкина. Из этих трех презентаций только Сакко и Ортега включили какой-либо исходный код или пример, демонстрирующий описанные ими методы. Поскольку это был единственный доступный пример, он использовался в качестве отправной точки для этого проекта.


Работа Сакко и Ортега довольно подробно описывает их сетап и методы тестирования. Установка VMware была завершена, как описано выше, и следующим шагом было внедрение кода модификации BIOS, который они предоставили. Предоставленный код требовал извлечения BIOS ROM в отдельные модули. BIOS ROM, входящий в комплект VMware, является Phoenix BIOS. Исследование говорит, что существует два основных инструмента для работы с этим типом BIOS, инструмент с открытым исходным кодом phxdeco, и коммерческий Phoenix BIOS Editor, который предоставляется непосредственно Phoenix. В работе Сакко и Ортега рекомендовано использовать приложение Phoenix BIOS Editor они разрабатывали свой код для использования под ним. Пробная версия была загружена мной из интернета, и, похоже, она обладает всеми функциями, необходимыми для этого проекта. Разыскивая ссылку для скачивания снова, я не могу найти ничего, что кажется даже наполовину легитимным, но Google предлагает большой список вариантов. Я просто предположу, что найти какую-нибудь легитимную пробную версию не составит труда. После того, как инструменты установлены, следующим шагом будет создание пользовательского BIOS.


Сначала я убедился, что небольшая модификация образа BIOS вступит в силу в VMware, что и произошло (изменился цвет логотипа VMware). Затем я запустил скрипт сборки на Python, предоставленный Сакко и Ортега для модификации BIOS. Помимо одной опечатки в питоновском скрипте сборки BIOS все работало отлично, и новый BIOS был сохранен на диск. Однако загрузка этого BIOS в VMware не привела к тому же самому успеху: VMware выдает сообщение о том, что в виртуальной машине что-то пошло не так и что оно закрывается. Отладка этой проблемы совершалась в IDA и GDB, но проблему было трудно отследить (плюс в IDA были проблемы с версией). В целях ускорения работы была загружена другая версия VMware, чтобы тестовая среда соответствовала среде Сакко и Ортега. После некоторых поисков была найдена и установлена точная версия VMware, которую они использовали. Это, к сожалению, не решило проблему, VMware сообщила о той же ошибке. Хотя я видел, как эта модификация BIOS работала, когда демонстрировалась как часть их презентации, теперь было ясно, что их пример кода потребует дополнительной модификации, прежде чем он сможет работать на какой-либо тестовой системе.


В результате отладки кода Сакко и Ортега было изучено много разных моментов, и в итоге проблема была в инструкции на ассемблере, которая выполняла far вызов абсолютного адреса, который не был правильным для используемой BIOS. После ввода правильного адреса код BIOS был успешно выполнен, и руткит начал искать на жестком диске файлы для изменения. Этот код требовал очень много времени для сканирования на жестком диске (который был всего 15 ГБ), и он запускался несколько раз до запуска системы. Код подтверждения концепции включал в себя возможность исправления notepad.exe, чтобы он отображал сообщение при запуске, или изменения файла /etc/passwd в системе Unix, чтобы root пароль был установлен на фиксированное значение. Это показало, что руткиты могут функционировать как в Windows системах, так и в Linux, даже если они используются только в простых целях.


Тестирование буткита


Значительно позже, в процессе работы над проектом, мной была также протестирована функциональность различных буткитов, и результаты были пересмотрены, чтобы определить, какой из них будет работать лучше не только в качестве буткита, но и как BIOS-руткит. Были исследованы четыре разных буткита: Stoned, Whistler, Vbootkit и Vbootkit2. Буткиты Stoned и Whistler были разработаны для того, чтобы функционировать в большей степени как вредоносные программы, нежели руткиты, и не отличались простотой структуры исходного кода. Vbootkit2 сильно выделялся, так как он в меньшей степени предполагал из себя вредоносное ПО и имел (относительно) хорошо документированный исходный код. Этот буткит предназначался для запуска с CD, но был протестирован только с бета-версией Windows 7. При использовании с коммерческой копией Windows 7, буткит просто не загружался, так как Windows использовала другие сигнатуры файлов. Я потратил некоторое время на определение новых сигнатур файлов, чтобы можно было протестировать этот буткит, но он все равно не загружался. В целях тестирования, я нашел бета-версию Windows 7. Когда Vbootkit2 запускался на бете Windows 7, все работало, как и ожидалось. Vbootkit2 включал в себя возможность повысить процесс до привилегий уровня системы (выше уровня администратора), чтобы захватывать нажатия клавиш и сбрасывать пароли пользователей. Все эти элементы были бы полезны для включения в руткит, но для переноса этого приложения в стандартный Windows 7 требовалась значительная работа. Далее был рассмотрен Vbootkit; он был разработан для работы с Windows 2003, XP и 2000. Хотя он не был собран так, чтобы его можно было запускать с компакт-диска, для добавления этой функциональности потребовались лишь незначительные изменения. Это программное обеспечение включало только возможность повышения привилегий процесса, но даже само по себе это уже очень ценная функция. Так этот программный пакет был выбран для использования с BIOS-руткитом, который описан в следующем разделе. NVLabs являются авторами этого буткита, который по большому счету представляет основную функциональность этого проекта, поэтому большое спасибо им за то, что они опубликовали свой код! Похоже, что исходный код больше не доступен на их веб-сайте, но его все еще можно скачать с Archive.org здесь.


Внедрение кода в BIOS посмотрим в следующей части. Следите за новостями!



Узнать подробнее о курсах: Реверс-инжиниринг. Продвинутый, Реверс-инжиниринг



Подробнее..

Работаем с Cutter основы реверса. Решение задач на реверсинг с r0от-мi. Часть 3

05.08.2020 16:23:28 | Автор: admin
image

В данной статье разберемся с декомпиляцией ELF файлов в Cutter на примере легеньких задач. Прошлые части введения в реверс:

Часть 1: C, C++ и DotNet decompile основы реверса
Часть 2: Реверсим MIPS и Golang основы реверса

Организационная информация
Специально для тех, кто хочет узнавать что-то новое и развиваться в любой из сфер информационной и компьютерной безопасности, я буду писать и рассказывать о следующих категориях:

  • PWN;
  • криптография (Crypto);
  • cетевые технологии (Network);
  • реверс (Reverse Engineering);
  • стеганография (Stegano);
  • поиск и эксплуатация WEB-уязвимостей.

Вдобавок к этому я поделюсь своим опытом в компьютерной криминалистике, анализе малвари и прошивок, атаках на беспроводные сети и локальные вычислительные сети, проведении пентестов и написании эксплоитов.

Чтобы вы могли узнавать о новых статьях, программном обеспечении и другой информации, я создал канал в Telegram и группу для обсуждения любых вопросов в области ИиКБ. Также ваши личные просьбы, вопросы, предложения и рекомендации рассмотрю лично и отвечу всем.

Вся информация представлена исключительно в образовательных целях. Автор этого документа не несёт никакой ответственности за любой ущерб, причиненный кому-либо в результате использования знаний и методов, полученных в результате изучения данного документа.


Fake Instructions




Это восьмое задание данной категории, и нам сообщают о наличии фейковых инструкций. Начнем с просмотра информации о файле.



Я открываю программу в Cutter GUI для radare2 со встроенным декомпилятором ghidra, имеющим возможность эмуляции, а с недавних пор и отладки. И сразу получаем интересный список функций.



Перейдем к функции main и декомпилируем ее.



В самом начале происходит инициализация канарейки и проверка количества аргументов. После чего выделяется 0x1f байт, куда копируется указанная константная строка, адрес сохраняется в переменную iVar3, впоследствии отдельный байты этой строки подвергаются изменению. Далее происходят преобразования со строкой s1, которая для нас не представляет интереса. Также происходит инициализация функции WPA, а веденный нами пароль записывается по адресу auStack50 и передается вместе с преобразованной строкой iVar3 в функцию WPA, после выполнения которой происходит проверка значения канарейки и завершение программы.

Давайте перейдем к функции WPA.



Заложенная заранее в программе строка снова подвергается изменению, после чего происходит сравнение двух строк. Если они равны, будет вызвана функция blowfish(), которая выведет нас верный пароль для сдачи, иначе будет вызвана функция RS4().



Так как происходит расшифрование заранее внесенных значений, мы можем отладить программу и изменить результат сравнения. Давайте перейдем к месту сравнения строк и поставим точку останова.



Теперь запустим отладку F9, и введем аргументы программы.



Нажимаем на кнопку далее на навигационной панели, пока не дойдем то своей точки останова. Результат сравнения строк с помощью функции strcmp() будет записан в регистр EAX, который потом и будет проверяться. Так как строки не равны, данный регистр содержит значение 1.



Давайте изменим его на 0, чтобы изменить ветку поведения программы и продолжим ее выполнение. После завершения в консоли Cutter мы увидим верный ответ, который следует сдавать на сайте.



Ptrace




Скачиваем программу и проверяем ее.



Ничего необычного, нужно найти пароль. Но вот есть маленкая защита от отладки (ptrace) и переходы, которые нужно править вручную. Так показывает граф в IDA Pro.



Делать это задание будем также в Cutter, он автоматически поправит все переходы.



Перейдем к декомпиляции функции main.



И верный пароль это соответствующие 4 буквы из строки, заданной в самом начале.



ARM ELF




Это бинарь для архитектуры ARM, закидываем его в Cutter и выбираем функцию main. По графу функции видим, с чем имеем дело, и скорее всего это посимвольное сравнение.



Открываем декомпилятор и анализируем программу.



Первым делом проверяется наличие аргумента программы и его длина, она должна быть равна 6.



Так переменная var_14h выступает как индекс и принимает значение 0. А далее происходят ряд сравнений, которые мы расценим как условия:
str[0] == str[5]
str[0] + 1 == str[1]
str[3] + 1 == str[0]
str[2] + 4 == str [5]
str[4] + 2 == str[2]
0 == str[3] ^ 0x72

Давайте реализуем алгоритм и получим пароль.



И получаем нужный пароль. На этом пока все.

Вы можете присоединиться к нам в Telegram. Там можно будет найти интересные материалы, слитые курсы, а также ПО. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.
Подробнее..

Тайны файла подкачки pagefile.sys полезные артефакты для компьютерного криминалиста

21.08.2020 16:19:43 | Автор: admin


В одной крупной финансовой организации произошел неприятный инцидент: злоумышленники проникли в сеть и пылесосили всю критически важную информацию копировали, а затем отправляли данные на свой удаленный ресурс. Криминалистов Group-IB призвали на помощь лишь спустя полгода после описываемых событий. К тому времени часть рабочих станций и серверов была уже выведена из работы, а следы действий злоумышленников уничтожены из-за использования ими специализированного ПО и из-за неправильного логирования. Однако на одном из серверов, участвовавших в инциденте, был обнаружен файл подкачки Windows, из которого эксперты получили критически важную информация об инциденте.

В этой статье Павел Зевахин, специалист Лаборатории компьютерной криминалистики Group-IB, рассказывает о том, какие данные можно обнаружить в ходе криминалистического исследования в файлах подкачки Windows.



Часть 1. Что скрывают pagefile.sys



Итак, pagefile.sys это файл подкачки операционной системы Windows. При нехватке оперативной памяти Windows резервирует определенное место на жестком диске и использует его для увеличения своих возможностей. Иными словами, выгружает часть данных из оперативной памяти в файл pagefile.sys. Очень часто необходимые для исследователя сведения остаются только в файле подкачки.

Выгрузка в файл подкачки происходит постранично, блоками по 4 Кб, поэтому данные могут занимать как непрерывную область в файле подкачки, так и находиться в разных его частях. Это означает, что в большинстве случаев информация, обнаруженная в этом файле, будет извлекаться с потерей целостности.

Размер pagefile.sys в файловой системе по умолчанию задается операционной системой, но пользователь всегда может отключить файл подкачки или изменить его максимальный размер. Стандартное расположение файла в корне системного раздела, но он может находиться и на любом другом логическом диске в зависимости от того, куда пользователь его поместил. Нужно помнить этот факт.

Прежде чем мы займемся извлечением pagefile.sys, надо понять, что это за файл с точки зрения файловой системы. Для этого воспользуемся ПО AccessData FTK Imager:

Hidden True Owner SID S-1-5-32-544
System True Owner Name Администраторы
Read Only False Group SID S-1-5-18
Archive True Group Name SYSTEM

Видно, что это скрытый системный файл, который так просто не скопировать.

Как тогда получить этот файл? Сделать это можно несколькими способами:

  • если вы работаете с активной операционной системой, то для извлечения используем ПО FTK Imager или KAPE Эрика Циммермана

  • если есть цифровая копия накопителя или же сам файл просто копируем файл или работаем с ним напрямую.


Не забываем, что файлы pagefile.sys могут находиться в теневых копиях (Volume Shadow Copy) и на других логических дисках. Правда, бывают случаи, когда правила теневого копирования задает сам пользователь и исключает копирование файла подкачки (в системном реестре есть ветвь HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\BackupRestore\FilesNotToSnapshot, где указываются файлы, которые будут исключены из теневого копирования).

На изображении ниже можно увидеть, как меняется объем обнаруженных данных в текущем файле подкачке (на изображении крайний левый) и файлах подкачки, которые были извлечены с этого же накопителя из теневых копий, созданных в разное время.



Важный момент, о котором стоит помнить: начиная со сборки 10525 Windows 10 используется компрессия файла подкачки. При нехватке памяти система сжимает неиспользуемые ресурсы памяти в каждом процессе, позволяя большему количеству приложений оставаться активными одновременно. Для декомпрессии такого файла необходимо использовать специализированное ПО.

Например, можно использовать для декомпрессии утилиту winmem_decompress Максима Суханова:



Это будет полезным, когда поиск в изначальном файле подкачки результатов не дал или же необходимые данные находились в сжатом виде.

Итак, когда файл pagefile.sys у нас в руках, можно приступать к его исследованию. И тут надо выделить две ситуации: первая когда мы знаем, что искать, и вторая когда не знаем. В первом случае это могут быть фрагменты файлов, следы работы того или иного ПО, какая-то пользовательская активность. Для такого поиска обычно используется шестнадцатеричный редактор X-Ways WinHEX (или любой другой). Во втором случае придется полагаться на специализированное ПО, например, MAGNET AXIOM, Belkasoft Evidence Center, утилиту strings (ее можно считать основной и наиболее часто используемой), ПО Photorec (ПО для восстановления, использующее сигнатурный метод), в некоторых случаях применять yara-правила (при условии настройки сканирования файлов большого размера) или же просто просматривать файл вручную.

А что можно найти в файле pagefile.sys, и почему мы делаем акцент на файле подкачки? Все просто: это данные, частично выгруженные из оперативной памяти, то есть процессы, файлы и прочие артефакты то, что было активно и функционировало в ОС. Это может быть часть интернет-истории и IP-адреса, информация о запуске каких-то файлов или же сами файлы, фрагменты изображений и текстов, сведения о сетевых запросах функционировавшего ранее ПО, следы работы вредоносного ПО в виде журналов нажатых клавиш, системные файлы и журналы ОС и много всего другого.



Идем в поля


Пора переходить непосредственно к реальным делам и исследованиям. Итак, что полезного можно найти в файле подкачки Windows с точки зрения цифровой криминалистики?

В одном из кейсов исследовался образ накопителя, зараженного разным вредоносным ПО, при помощи которого злоумышленники похитили деньги со счета организации.

Чтобы дать полный ответ, что произошло и как, криминалисту необходимо установить начальную точку заражения, используемый злоумышленниками инструментарий и последовательность действий. В ходе исследования удалось найти не все следы функционирования вредоносного ПО. И вот тут был проанализирован pagefile.sys. Как мы уже знаем, там можно найти страницы из памяти процессов, выгруженные из оперативной памяти в файл подкачки, которые иногда можно восстановить, например, при помощи ПО Photorec сигнатурным методом, как и было сделано в данном кейсе.

При этом нужно отметить следующее: так как в файле подкачки лежат уже выгруженные из оперативной памяти процессы (файлы), то их адресация будет отличаться от адресации оригинальных файлов. К тому же они могут быть сильно фрагментированы, поэтому запустить такой исполняемый файл зачастую нельзя, да и все другие файлы, как правило, будут иметь повреждения внутренней структуры из-за фрагментации, ведь сигнатурное восстановление не может само найти все фрагменты файла и расставить их в правильном порядке.


Выше представлен пример файлов (Photorec присвоил файлам имена, исходя из смещения относительно начала файла подкачки), выгруженных в ходе проведения этого исследования. Мы видим, что это исполняемые, графические, текстовые и иные файлы. Дальше все просто: анализируем их, исходя из необходимых критериев и задач.

В конкретном случае из файла подкачки были восстановлены dll-файлы, в которых есть вредоносный код. Ниже приведен пример их детектов на VirusTotal (поиск осуществлялся по контрольной сумме файлов):



В ходе анализа был установлен адрес удаленного сервера, с которым могли взаимодействовать эти файлы. При помощи шестнадцатеричного редактора X-Ways WinHEX в исследуемом pagefile.sys обнаружены строки, содержащие адреса удаленного сервера. Это говорит о том, что обнаруженные файлы функционировали в ОС и активно взаимодействовали со своим удаленным сервером. А вот и детекты сервиса VirusTotal за декабрь 2018 года:




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

А что еще?


Существуют порой уникальные случаи, когда в файле подкачки помимо иных следов можно найти закодированные в base64 снимки экрана. Например, такие при отправке создает банковский троян Buhtrap.

В конкретном случае начало файла было следующим: /9j/4AAQSkZJRgABAQEAYABgAAD/. Это заголовок jpeg-файла, закодированный в base64 (представлена часть изображения):



Вышеуказанный фрагмент скопировали, декодировали и добавили к нему расширение jpg. Нам повезло, и обнаруженный скриншот содержал полный снимок активного рабочего стола бухгалтерского компьютера с открытым ПО 1С: Бухгалтерия, на котором отображался финансовый баланс предприятия и прочие важные данные. Другие обнаруженные закодированные изображения из-за особенности хранения информации в файле подкачки были неполными (битыми).

Другой пример. В ходе одного из инцидентов обнаружены следы фреймворка Cobalt Strike (характерные строки в файле подкачки SMB mode, status_448, ReflectiveLoader):




И впоследствии можно попытаться выгрузить модули. На изображении выше это keylogger.dll и screenshot.dll, но могут быть и другие.

Идем дальше. Входящий в Cobalt Strike и часто используемый злоумышленниками модуль mimikatz это инструмент, реализующий функционал Windows Credentials Editor и позволяющий извлекать аутентификационные данные залогинившегося в системе пользователя в открытом виде. Именно в файле подкачки были обнаружены следы его функционирования, а именно следующие символьные строки:

  • sekurlsa::logonPasswords извлечение логинов и паролей учетной записи
  • token::elevate повышение прав доступа до SYSTEM или поиск токена администратора домена
  • lsadump::sam получение SysKey для расшифровки записей из файла реестра SAM
  • log Result.txt файл, куда записываются результаты работы ПО (не забываем поискать этот файл в файловой системе):



Следующий пример следы функционирования банковского трояна Ranbyus, который состоит из множества модулей. В ходе одного исследования в файле подкачки, который находился в теневой копии (VSS), обнаружили строки, сформированные дополнительным модулем, расширяющим функциональные возможности ПО Ranbyus. Строки содержали, помимо всего прочего, и введенные аутентификационные данные пользователя (логин и пароль) в системе клиент-банк. А в качестве примера часть сетевого запроса, включая сведения об управляющем сервере, который был обнаружен в файле pagefile.sys:



На самом деле довольно часто можно встретить примеры POST-запросов вредоносного ПО к своим управляющим серверам, а также ответы этих серверов на запросы. Ниже приведены такие случаи на примере взаимодействия ПО Buhtrap со своим управляющим сервером:



Теперь вспомним про кейс, с которого мы начинали этот пост. В крупной организации с множеством серверов и рабочих станций произошел инцидент, в ходе которого злоумышленники проникли в сеть, завладели учетными данными одного из администраторов контроллера домена и далее перемещались по сети, используя легитимное ПО. Они копировали критически важную информацию, а затем отправляли эти данные на удаленный ресурс. На момент реагирования прошло более полугода, часть рабочих станций и серверов уже были выведены из работы, а следы действий злоумышленников уничтожены благодаря использования ими специализированного ПО и из-за неправильного логирования.

В процессе реагирования мы вышли на сервер с ОС Windows Server 2012, участвовавший в инциденте. Файлы системных журналов уже не один раз перезаписаны, а свободное дисковое пространство затерто. Но там был файл подкачки! Благодаря долгой работе сервера без перезагрузки и большому объему файла подкачки в нем сохранились следы запуска ПО злоумышленников и скриптов, которые на момент исследования уже отсутствовали в файловой системе без возможности восстановления. Сохранились и сведения о каталогах и файлах (пути и имена), которые создавались, копировались и впоследствии удалялись злоумышленниками, IP-адреса рабочих станций организации, откуда копировались данные, и прочая важная информация.

Что интересно, автоматизированный анализ при помощи различного криминалистического ПО полных результатов не дал, каких-то определенных критериев поиска не было, поэтому специалисты прибегли к ручному анализу файла подкачки, используя шестнадцатеричный редактор X-Ways WinHEX.

Ниже несколько примеров того, что обнаружили специалисты:





Сведения об использовании утилит pcsp.exe и ADExplorer.exe (присутствуют и даты, и пути).

Дальше сведения об использовании vbs-скрипта (на изображении начало и конец).
Примечательно, что указаны учетные данные (логин и пароль) одного из администраторов контроллера домена, которые ранее были скомпрометированы:




В результате почти вся критически важная информация о произошедшем инциденте была обнаружена именно в файле подкачки одного из серверов. Установлен инструментарий злоумышленников и часть их действий в сети организации.

Ну и в завершение, конечно же, стоит упомянуть про остальные артефакты, такие как данные о посещении интернет-сайтов (иногда можно обнаружить сведения об использовании электронных почтовых ящиков), сведения о файлах и каталогах:




Можно обнаружить и такую информацию, как имя компьютера и серийный номер тома, где располагался файл подкачки:



А также информацию из файлов Prefetch и, конечно же, системные журналы Windows.

Итак, pagefile.sys действительно может содержать большое количество различных артефактов, которые могут помочь в анализе. Именно поэтому никогда не стоит игнорировать исследование файла подкачки. Даже если есть у вас есть все необходимые данные все равно исследуйте pagefile.sys. Практика показывает, что там может находиться что-то недостающее и важное.
Подробнее..

Укрощение Горыныча 2, или Символьное исполнение в Ghidra

01.10.2020 08:18:25 | Автор: admin


С удовольствием и даже гордостью публикуем эту статью. Во-первых, потому что автор участница нашей программы Summ3r of h4ck, Nalen98. А во-вторых, потому что это исследовательская работа с продолжением, что вдвойне интереснее. Ссылка на первую часть.


Добрый день!


Прошлогодняя стажировка в Digital Security не оставила меня равнодушной к компании и новым исследованиям, так что в этом году я взялась поработать над проектом так же охотно. Темой летней стажировки Summer of Hack 2020 для меня стала Символьное исполнение в Ghidra. Нужно было изучить существующие движки символьного исполнения, выбрать один из них и реализовать его в интерфейсе Ghidra. Казалось бы, зачем, ведь в основном движки представляют собой самостоятельные решения? Этот вопрос будет возникать до тех пор, пока не попробовать то, что автоматизирует действия и сделает их наглядными. Это и стало целью разработки.


Статья в какой-то степени является еще и продолжением статьи моего наставника, Андрея Акимова, о решении Kaos Toy Project с Triton. Только сейчас нам не придется писать ни строчки кода решить крякми можно будет практически двумя кликами.


Итак, начнем по порядку.


Пара слов о символьном исполнении


Символьное исполнение представляет собой технику анализа программного обеспечения, которая позволяет найти все наборы входных данных, способствующие выполнению каждого из его возможных путей. Если говорить обобщенно, то во время символьного исполнения производится замена переменных/регистров их символьными значениями. Зависимость между переменной и ее символьным значением называется формулой. Единичные формулы объединяются в более сложные и подаются на вход SMT-решателю. Он, в свою очередь, ищет решение к логической формуле и выдает результат утверждение удовлетворяется (satisfied) или утверждение не удовлетворяется (unsatisfied). Во время символьного исполнения ветки будут расходиться, и произойдет создание форков и новых ограничений на символьные значения. Экспоненциальный рост числа форков является одной из главных проблем в этой области, поскольку для вычислений растущего числа веток требуются большие мощности.


Если говорить об общей классификации движков символьного исполнения, то среди них выделяют статические символьные движки (SSE, emulated) и динамические символьные движки (DSE, concolic). Достоинством статических движков является поддержка эмуляции как всей программы, так и конкретной ее части. И поскольку не происходит непосредственного запуска на CPU, а лишь эмулируются инструкции, открываются возможности для анализа разнообразных архитектур. Однако, страдает масштабируемость, и могут возникнуть определенные трудности со входами в сторонние библиотеки.


DSE, в отличие от SSE, исполняет каждую ветку отдельно, и по своей сути он быстрее, поскольку символизирует не все подряд, а только входные данные пользователя (источник книга "Practical Binary Analysis" by Dennis Andriesse".


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


Выбор символьного движка


Первым наставники посоветовали изучить Triton. С ним хорошо получилось изучить теорию символьного исполнения. Движок может работать как в режиме SSE, так и в режиме DSE. Однако у него ограниченное количество поддерживаемых архитектур (x86, x86-64, ARM32, Arch64), и мне сложно было представить, каким образом можно реализовать его API в контексте интерфейса Ghidr-ы. Так что Triton пришлось отложить и ресерчить дальше.


Следующим подопытным стал KLEE. Он, несомненно, является самым мощным движком символьного исполнения, с его помощью можно работать с по-настоящему серьезными проектами и исследованиями. В контексте реализации архитектуры плагина здесь основной проблемой выступила генерация llvm-биткода. А все потому, что для полноценной работы KLEE необходимо подавать скомпилированные clang-ом в llvm-биткод (.bc) файлы исходников. Были идеи по передачи бинаря llvm-лифтеру (их ассортимент можно увидеть тут), однако ни один из этих вариантов не сработал, и KLEE выдавал ошибки. Говоря о наработках в области трансляции Pcode в llvm, есть лишь один Ghidra-to-LLVM. В рамках ресерча пришлось протестировать и его. Как оказалось, он не работает с 32-битными бинарями, и если и удавалось получить результат в виде .ll-файла, то после обработки llvm-ассемблером llvm-as и получения llvm-биткода KLEE все равно не хотел работать с подобным самопалом и выдавал ошибки. Так что KLEE также пришлось оставить, несмотря на его широкие возможности по символьному исполнению.


Наставники также посоветовали изучить движок S2E. Он расширяет возможности QEMU по трансляции бинарных инструкций в TCG и также транслирует сам TCG в LLVM. Это была заманчивая идея, однако для работы с ним требуется Python3. И, как известно, Ghidra использует старый Jython 2.x, что, казалось бы, полностью перекрывает поток возможностей по интеграции современных инструментов в Ghidr-у. Движок, который был выбран в итоге, тоже работает только с Python3, но в случае с ним возможно было придумать обходной вариант через системный интерпретатор. А поскольку S2E работает как отдельный инструмент, его использование из Ghidr-ы не представляется возможным.


На момент написания статьи вышел еще один движок символьного исполнения SymCC. Точнее, правильно его назвать оберткой компилятора для C-кода. Представьте: у вас на руках исходники, вы компилируете исполняемый файл, как в случае с KLEE и clang-ом, только здесь с SymCC. Компилятор интегрирует необходимый для символьного исполнения код и библиотеки в новоиспеченный исполняемый файл. После запуска получаем директорию с кейсами, сгенерированными во время выполнения. Все классно, но привязать такое к Ghidr-е невозможно, так как у нас на руках не исходники проекта, а результат дизассемблирования и декомпиляции.


Финальным выбором стал angr. Он популярен, у него доступный и подробно задокументированный API, и интегрировать данный движок было действительно реально. Конечно, как уже отмечалось, без Python3 никуда, в Ghidra его поддержка отсутствует, но в случае с angr-ом мне показалось возможным написать универсальный скрипт, который смог бы запускаться на системном интерпретаторе, решать заданный бинарь и передавать результат обратно в Ghidr-у. Вот такой был план.


Сердито. Костыльно. Канонично.


Реализовать получилось так: графический интерфейс плагина получает необходимую информацию от пользователя, создается буферный JSON-файл, куда записывается необходимая конфигурация для работы универсального angr-скрипта, плагин запускает скрипт на системном интерпретаторе, скрипт передает найденное решение обратно в графический интерфейс.


Если изучить принцип работы декомпилятора в Ghidra и его взаимодействие с GUI, то можно понять, что основе лежит схожий костыльный алгоритм. Дело в том, что Ghidra работает с декомпилятором через stdin и stdout потоки, используя классы DecompileProcess и DecompInterface. Так что архитектуру плагина можно считать вполне каноничной в контексте Ghidra.


На написание логики скрипта не ушло много времени. Он, по сути, собирает в себя базовые возможности angr-a по символьному исполнению для решения ctf-тасков. На графический интерфейс пришлось потратить львиную долю времени, и его разработку не могу назвать захватывающей. Как и Ghidra, графический интерфейс плагина написан на Java, в роли IDE по традиции выступил Eclipse.


Для GUI плагина было создано 4 файла:


  • AngryGhidraPlugin.java в файле указывается основная информация о плагине и происходит его инициализация.
  • AngryGhidraProvider.java самый объемный файл, который инициализирует компоненты графического интерфейса основного окна плагина; здесь прописана логика создания файла конфигурации для скрипта, происходят запуск скрипта и чтение результатов, их передача в интерфейс.
  • AngryGhidraPopupMenu.java здесь прописаны дополнительные параметры контекстного меню окна дизассемблера Ghidr-ы. Благодаря этому файлу можно задавать необходимые адреса прямиком из окна дизассемблера, а также внедрять пропатченные байты памяти в контекст работы angr-а.
  • HookCreation.java инициализирует окно создания хуков.

Итак, пара слов о функциональных возможностях плагина.


  • Auto load libs определяет работу загрузчика необходимых библиотек для исполняемого файла. Пользователь определяет, нужна ему эта опция или нет.
  • Find Address адрес, куда вы хотите попасть во время выполнения программы (например, на адрес вывода строки License key is validated!).
  • Blank State адрес, с которого вы начинаете эмуляцию. Если не добавлять дополнительных параметров в дальнейшем, то по умолчанию все регистры и память обнулены. Удобно назначать на адресе точки входа или на адресе вызова функции проверки, если вы знаете ее расположение в коде и хотите ускорить процесс работы angr-a.
  • Avoid addresses адрес/адреса, которые в ходе символьного исполнения нужно избежать. При их нахождении angr автоматически отметет соответствующие им ветки с меткой avoid и не пройдет дальше. Чем больше таких адресов указать, тем чаще angr будет отбрасывать ненужные ветки кода и найдет решение быстрее (если это решение существует).
  • Arguments аргументы, поставляемые на вход программе (argv[1], argv[2] и т.д.). Иногда значение, которое необходимо сделать символьным, передается через аргумент(-ы) к программе.
  • Hooks хуки позволяют перехватить указанные инструкции и внести определенные значения в регистры. Например, когда необходимо записать в регистры символьные вектора, это будет продемонстрировано в дальнейшем решении Kaos Toy Project.
  • Store symbolic vector если необходимо создать символьный вектор в адресном пространстве определенной длины, а потом, например, поместить его в регистр. Если плагин найдет решение, он выведет содержимое созданного символьного вектора.
    • Write to memory иногда бывает необходимо, чтобы определенные участки памяти были заполнены конкретными значениями. Например, в случае Kaos Toy Project, это значение Installation ID, которое инициализируется по адресу 0x4093a8. Это поле окна плагина можно заполнить патчингом из Ghidr-ы, для этого необходимо пропатчить нужные вам байты, выделить их и открыть контекстное меню дизассемблера AngryGhidraPlugin -> Apply patched bytes.
  • Registers те значения регистров, которые вы самостоятельно инициализируете при запуске эмуляции с заданного адреса. Здесь также можно создать и сохранить символьные вектора нужной длины.

После успешной работы универсального скрипта и нахождения решения плагин выделит цветом всю трассу инструкций, по которой прошла программа чтобы достигнуть желаемого адреса.


Продолжаем хорошую традицию


Наконец, решим Kaos Toy Project плагином AngryGhidra.



Первое, что сделаем запустим toyproject.exe в любом отладчике и отследим, по какому адресу записываются байты Installation ID.



Взглянем на байты по адресу 0x4093a8, это и есть наш Installation ID.



Нужно учитывать тот факт, что отладчик в Ghidra отсутствует, а angr осуществляет эмуляцию бинаря (за это отвечают компоненты PyVEX и SimEngine). Это значит, что значение Installation ID инициализировано не будет, нам нужно сделать это самостоятельно. Наш ход патчинг байтов в Ghidra.


Найдем адрес 0x4093a8 и запатчим нулевые значения байтами Installation ID, выделим их и выберем в контекстном меню AngryGhidraPlugin -> ApplyPatchedBytes:



Теперь решим вопрос с адресами куда хотим попасть, чего следует избегать и с чего вообще начнем.


Строка Congratulations! Now write a keygen and tutorial! однозначно для нас искомая, так что адресом для поиска станет адрес помещения в стек этой строки для вызова окна с сообщением. Выберем адрес 0x40123b, для этого можно вписать его в поле Find Address или, открыв контекстное меню, выбрать AngryGhidraPlugin -> Set -> Find Address. Теперь адрес будет перекрашен в зеленый цвет.


Строка That is just wrong. Try harder! говорит о неверном введенном ключе, так что отметим адрес 0x401250 в качестве Avoid Address. Теперь он будет красным в окне дизассемблера.


Чтобы сократить время поиска решения будет удобно выбрать начальное состояние (Blank State) по адресу, где вызывается функция проверки введенного ключа. Это функция 0x4010ec. Выделим адрес вызова этой функции в качестве Blank State Address, и он перекрасится в голубой цвет.


С назначением адресов мы закончили:



Остался последний момент. Заглянем в функцию проверки ключа по адресу 0x4010ec и изучим, каким образом нам стоит передать две части ключа в плагин.



Как можно увидеть, две части ключа передаются в функцию проверки в качестве аргументов. Поскольку наше начальное состояние не предусматривает наличия аргументов, мы должны перехватить момент записи этих аргументов в регистры EDX и EBX и внести символьные значения самостоятельно. Но что они из себя представляют? Вернемся в основную функцию программы и изучим этот момент.


Можно заметить, что первоначально введенные две части ключа обрабатываются функцией по адресу 0x40109d:



Таким образом, каждая часть ключа имеет длину 4 байта, причем второй аргумент для функции проверки будет являться результатом xor-а первой и второй части ключа.


Откроем окно AngryGhidraPlugin и создадим хук по адресу 0x4010ff, чтобы заполнить значения регистров EDX и EBX символьными векторами длиной по 4 байта.



Теперь все готово к запуску, жмем Run и получаем результат!



Но это еще не все, не забываем проксорить два найденных решения, чтобы получить вторую часть ключа.



И проверяем:



Ключ подошел! И ни строчки кода! Плагин AngryGhidra сделал все за нас.


Большое спасибо компании Digital Security за интересную стажировку, которая прошла для меня в удаленном формате, наставникам Андрею (@e13fter) и Саше (@dura_lex), отдельная благодарность за поддержку Виктору Склярову и Борису Рютину!


Плагин на Github: AngryGhidra.

Подробнее..

Из песочницы Вскрытие покажет Решаем лёгкий crackme и пишем генератор ключа

02.11.2020 16:19:26 | Автор: admin
Доброго времени суток читающий. Мне хочется рассказать тебе про алгоритм решения одного лёгкого crackme и поделиться кодом генератора. Это был один из первых crackme, который я решил.

На просторах сети найден был наш подопотный. Это сrackme. Естественно, что необходимо изучить его. Для вскрытия нам понадобиться:

  • Немного языка Assembler
  • Логика вместе с отладчиком (IDA PRO)

Лекарство изготовим из яда австралийской змеи с помощью Python. Не будем терять времени.

image



Сей crackme не очень сложный. Рассмотрим алгоритм генерации ключа на правильном ключе 8365-5794-2566-0817. В IDA Pro я добавил комментарии по поводу кода.

Осмотр пациента


Поведение на первый взгляд обычное. Расширение .exe. Не упаковано. Запустим.





Что это? Требует ключ. Нужно лечить :)

Вскрытие больного


При ошибке была надпись Fail, Serial is invalid!. Надём место, где она используется в программе.



Видим 1 функцию key_check_func перед условным переходом. Осмотрим её.

Интересное древо получается.



Ставим точку останова и начинаем отладку.

Необходимо, чтобы ключ был длиной 19 символов.



Затем программа проверяет присутствие знака тире в ключе через каждые 5 символов, входя в цикл 3 раза.





После проверки на наличие тире, программ изучает состоит ли блок ключа (1/4 ключа) из цифр. Есть предположение, чтобы понять, какая цифра была передана компилятор исполняет команду add eax, 0FFFFFFD0h



Например, сложим 8 (38h) c данным числом. Получившиеся число чрезмерно большое ( 10000008h ) и на конце располагается 8, следовательно, обрезается. Остаётся 8. Это переданная нами цифра. Так происходит 4 раза в цикле.



Что теперь? Нынче начинается сложение кодов каждой цифры проверяемого блока друг с другом, однако последняя 4 цифра складывается 3 раза подряд. Получившуюся сумму опять складывают. Код последней цифры блока + получившиеся сумма 150h. Результат добавляется в r10d. Весь этот цикл повторяется 4 раза для каждого блока ключа.



В нашем случае рассмотрим на примере первого блока ключа 8365: 38h (8) + 33h (3) + 36h (6) + 35h (5) + 35h (5) + 35h (5) = 140h + 35h 150h = 25h. 25 прибавляется в r10d и записывается в память. Пометим сие место, как A. Сумма прочих блоков ключа также равна 25h. Значит, умножаем 25h * 4 = 94.

Далее происходит побитовый сдвиг в право на 2 байта. Это место для себя пометим, как B.



У нас есть значение обозначенное, как A (25h) и B (25h). Впоследствии произойдёт сравнение данных чисел. Они должны быть одинаковые. Эта операция происходит для каждого блока ключа.



Последняя, что осуществляет программа это проверяет схожи ли цифры в блоках. Поначалу сравниваются цифры 1 блока с цифрами 2 блока. Потом проверка 2 блока с 3 блоком. Заключительная проверка 3 блока с 4 блоком. Вся это проверка происходит не сразу, а постепенно в цикле.





Анализ закончен. Больной изучен.

Время для лекарства


Для генерации ключа используем нечто необычное. Python + Библиотеку random.
Сам код находится ниже. Комментарии в коде:

import randomdef gen_key_part():    # Генерация цифр    num1 = str(random.randint(0, 9))    num2 = str(random.randint(0, 9))    num3 = str(random.randint(0, 9))    num4 = str(random.randint(0, 9))    # Генерация части ключа (1 блок)    final = num1 + num2 + num3 + num4    return finaldef sum_ord(key_part):    # Раскладываем переданную часть ключа на цифры    num1 = key_part[0]    num2 = key_part[1]    num3 = key_part[2]    num4 = key_part[3]    # Алгорит сложения в crackme    sum = ord(num1) + ord(num2) + ord(num3) + ord(num4) + ord(num4) + ord(num4)    sum_final = ord(num4) + sum - 336    return sum_finaldef shr(key):    # Разбиваем ключ на блоки    a = key[0:4]    b = key[5:9]    c = key[10:14]    d = key[15:19]    # Алгоритм сдвига по битам в crackme    x = sum_ord(a) + sum_ord(b) + sum_ord(c) + sum_ord(d)    x = x >> 2    return xdef check_key(key):    i = 0 # Счётчик    while i != 4:        # Переменная i будет увеличиваться на 1 до 4. Это будет указатель на проверяемый символ        first = 0 + i        second = 5 + i        third = 10 + i        four = 15 + i        # Проверка ключа на соответсвие сдвига по битам и суммы чисел ( Обозначали, как A и B)        if sum_ord(key[0:4]) != shr(key) or sum_ord(key[5:9]) != shr(key) or sum_ord(key[10:14]) != shr(key) or sum_ord(key[15:19]) != shr(key):            return False        # Проверка ключа на совпадающие цифры        if int(key[first]) == int(key[second]):            return False        if int(key[second]) == int(key[third]):            return False        if int(key[third]) == int(key[four]):            return False        i += 1 # # Счётчик увеличиваемdef generate_key():    # Генерация ключа    key = gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part()    # Проверяем ключ и печатаем true или false    while True: #        if check_key(key) == False:            # Генерация ключа если не верно            key = gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part()            print('Checking this key -> ' + key)        else:            # Ключ правильный            print('This is the correct key -> ' + key)            break# Генерируем ключ, вызывая функциюif __name__ == "__main__":    generate_key()

Запускаем.



Вводим ключ и видим.



Пациент излечен.

Спасибо за внимание. Жду ваших комментариев и критики. Не болейте.
Подробнее..

Меняем промежуточное представление кода на лету в Ghidra

30.04.2021 14:04:59 | Автор: admin

Когда мы разрабатывали модуль ghidra nodejs для инструмента Ghidra, мы поняли, что не всегда получается корректно реализовать опкод V8 (движка JavaScript, используемого Node.js) на языке описания ассемблерных инструкций SLEIGH. В таких средах исполнения, как V8, JVM и прочие, один опкод может выполнять достаточно сложные действия. Для решения этой проблемы в Ghidra предусмотрен механизм динамической инъекции конструкций P-code языка промежуточного представления Ghidra. Используя этот механизм, нам удалось превратить вывод декомпилятора из такого:

В такой:

Рассмотрим пример с опкодом CallRuntime. Он вызывает одну функцию из списка т.н. Runtime-функций V8 по индексу (kRuntimeId). Также данная инструкция имеет переменное число аргументов (range номер начального регистра-аргумента, rangedst число аргументов). Описание инструкции на языке SLEIGH, который Ghidra использует для определения ассемблерных инструкций, выглядит так:

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

  1. Поиск нужного названия функции в массиве Runtime-функций по индексу kRuntimeId.

  2. Поскольку аргументы передаются через регистры, необходимо сохранить их предыдущее состояние.

  3. Передача в функцию переменного количества аргументов.

  4. Вызов функции и сохранение результата вызова в аккумулятор.

  5. Восстановление предыдущего состояния регистров.

Если вы знаете, как сделать такое на SLEIGH, пожалуйста, напишите комментарий. А мы решили, что все это (а особенно работу с переменным количеством аргументов-регистров) не очень удобно (если возможно) реализовывать на языке описания процессорных инструкций, и применили механизм динамических инъекций p-code, который как раз для таких случаев реализовали разработчики Ghidra. Что это за механизм?

Можно создать в файле описания ассемблерных инструкций (slaspec) специальную пользовательскую операцию, например CallRuntimeCallOther. Далее, изменив конфигурацию вашего модуля (подробнее об этом ниже), вы можете сделать так, чтобы при нахождении в коде данной инструкции Ghidra передавала бы обработку в Java динамически, и уже на языке Java написать обработчик, который будет динамически формировать p-code для инструкции, пользуясь всей гибкостью Java.

Рассмотрим подробно, как это сделать.

Создание служебной операции SLEIGH

Опишем опкод CallRuntime следующим образом. Подробнее об описании процессорных инструкций на языке SLEIGH все можете узнать из статьи Создаем процессорный модуль под Ghidra на примере байткода v8.

Определим служебную операцию:

define pcodeop CallRuntimeCallOther;

И опишем саму инструкцию:

:CallRuntime [kRuntimeId], range^rangedst is op = 0x53; kRuntimeId; range;       rangedst {CallRuntimeCallOther(2, 0);}

Таким образом, любой опкод, начинающийся с байта 0x53, будет расшифрован как CallRuntime При попытке его декомпиляции будет вызываться обработчик операции CallRuntimeCallOtherс аргументами 2 и 0. Эти аргументы описывают тип инструкции (CallRuntime) и позволят нам написать один обработчик для нескольких похожих инструкций (CallWithSpread, CallUndefinedReceiverи т.п.).

Подготовительная работа

Добавим класс, через который будет проходить инъекция кода: V8_PcodeInjectLibrary. Этот класс мы унаследуем от ghidra.program.model.lang.PcodeInjectLibrary который реализует большую часть необходимых для инъекции p-code методов.

Начнем написание класса V8_PcodeInjectLibraryс такого шаблона:

package v8_bytecode;import public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {public V8_PcodeInjectLibrary(SleighLanguage l) {}}

V8_PcodeInjectLibraryбудет использоваться не пользовательским кодом, а движком Ghidra, поэтому нам необходимо задать значение параметра pcodeInjectLibraryClassв файле pspec, чтобы движок Ghidra знал, какой класс задействовать для инъекции p-code.

<?xml version="1.0" encoding="UTF-8"?><processor_spec>  <programcounter register="pc"/>  <properties>  <property key="pcodeInjectLibraryClass" value="v8_bytecode.V8_PcodeInjectLibrary"/>  </properties></processor_spec>

Также нам понадобится добавить нашу инструкцию CallRuntimeCallOtherв файл cspec. Ghidra будет вызывать V8_PcodeInjectLibraryтолько для инструкций, определенных таким образом в cspec-файле.

<callotherfixup targetop="CallRuntimeCallOther"><pcode dynamic="true"><input name=outsize"/> </pcode></callotherfixup>

После всех этих нехитрых процедур (которые, к слову, на момент создания нашего модуля почти не были описаны в документации) можно перейти к написанию кода.

Создадим HashSet, в котором будем хранить реализованные нами инструкции. Также мы создадим и проинициализируем член нашего класса переменную language. Данный код сохраняет операцию CallRuntimeCallOtherв наборе поддерживаемых операций, а также выполняет ряд служебных действий, в которые мы не будем подробно вдаваться.

public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {private Set<String> implementedOps;private SleighLanguage language;public V8_PcodeInjectLibrary(SleighLanguage l) {super(l);language = l;String translateSpec = language.buildTranslatorTag(language.getAddressFactory(),getUniqueBase(), language.getSymbolTable());PcodeParser parser = null;try {parser = new PcodeParser(translateSpec);}catch (JDOMException e1) {e1.printStackTrace();}implementedOps = new HashSet<>();implementedOps.add("CallRuntimeCallOther");}}

Благодаря внесенным нами изменениям Ghidra будет вызывать метод getPayloadнашего класса V8_PcodeInjectLibraryкаждый раз при попытке декомпиляции инструкции CallRuntimeCallOther Создадим данный метод, который при наличии инструкции в списке реализованных операций будет создавать объект класса V8_InjectCallVariadic(этот класс мы реализуем чуть позже) и возвращать его.

@Override/*** This method is called by DecompileCallback.getPcodeInject.*/public InjectPayload getPayload(int type, String name, Program program, String context) {if (type == InjectPayload.CALLMECHANISM_TYPE) {return null;}if (!implementedOps.contains(name)) {return super.getPayload(type, name, program, context);}V8_InjectPayload payload = null; switch (name) {case ("CallRuntimeCallOther"):payload = new V8_InjectCallVariadic("", language, 0);break;default:return super.getPayload(type, name, program, context);}return payload;}

Генерация p-code

Основная работа по динамическому созданию p-code будет происходить в классе V8_InjectCallVariadic. Давайте его создадим и опишем типы операций.

package v8_bytecode;import public class V8_InjectCallVariadic extends V8_InjectPayload {public V8_InjectCallVariadic(String sourceName, SleighLanguage language, long uniqBase) {super(sourceName, language, uniqBase);}// Типы операций. В данном примере мы рассматриваем RUNTIMETYPEint INTRINSICTYPE = 1;int RUNTIMETYPE = 2;int PROPERTYTYPE = 3;@Overridepublic PcodeOp[] getPcode(Program program, InjectContext context) {}@Overridepublic String getName() {return "InjectCallVariadic";}}

Как нетрудно догадаться, нам необходимо разработать нашу реализацию метода getPcode Для начала создадим объект pCode класса V8_PcodeOpEmitter Этот класс будет помогать нам создавать инструкции pCode (позже мы ознакомимся с ним подробнее).

V8_PcodeOpEmitter pCode = new V8_PcodeOpEmitter(language, context.baseAddr, uniqueBase); 

Далее из аргумента context (контекст инъекции кода) мы можем получить адрес инструкции, который нам пригодится в дальнейшем.

Address opAddr = context.baseAddr;

С помощью данного адреса мы получим объект текущей инструкции:

Instruction instruction = program.getListing().getInstructionAt(opAddr);

Также с помощью аргумента contextмы получим значения аргументов, которые ранее описывали на языке SLEIGH.

Integer funcType = (int) context.inputlist.get(0).getOffset();Integer receiver = (int) context.inputlist.get(1).getOffset();

Реализуем обработку инструкции и генерации Pcode.

// проверка типа инструкцииif (funcType != PROPERTYTYPE) {// получаем kRuntimeId  индекс вызываемой функцииInteger index = (int) instruction.getScalar(0).getValue();// сгенерируем Pcode для вызова инструкции cpool с помощью объекта pCode класса V8_PcodeOpEmitter. Подробнее остановимся на нем ниже.pCode.emitAssignVarnodeFromPcodeOpCall("call_target", 4, "cpool", "0", "0x" + opAddr.toString(), index.toString(), funcType.toString());}// получаем аргумент диапазон регистровObject[] tOpObjects = instruction.getOpObjects(2);// get caller args count to save only necessary onesObject[] opObjects;Register recvOp = null;if (receiver == 1) {}else {opObjects = new Object[tOpObjects.length];System.arraycopy(tOpObjects, 0, opObjects, 0, tOpObjects.length);}// получаем количество аргументов вызываемой функцииtry {callerParamsCount = program.getListing().getFunctionContaining(opAddr).getParameterCount();}catch(Exception e) {callerParamsCount = 0;}// сохраняем старые значения регистров вида aN на стеке. Это необходимо для того, чтобы Ghidra лучше распознавала количество аргументов вызываемой функцииInteger callerArgIndex = 0;for (; callerArgIndex < callerParamsCount; callerArgIndex++) {pCode.emitPushCat1Value("a" + callerArgIndex);}// сохраняем аргументы вызываемой функции в регистры вида aNInteger argIndex = opObjects.length;for (Object o: opObjects) {argIndex--;Register currentOp = (Register)o;pCode.emitAssignVarnodeFromVarnode("a" + argIndex, currentOp.toString(), 4);}// вызов функцииpCode.emitVarnodeCall("call_target", 4);// восстанавливаем старые значения регистров со стекаwhile (callerArgIndex > 0) {callerArgIndex--;pCode.emitPopCat1Value("a" + callerArgIndex);}// возвращаем массив P-Code операцийreturn pCode.getPcodeOps();

Теперь рассмотрим логику работы класса V8_PcodeOpEmitter (https://github.com/PositiveTechnologies/ghidra_nodejs/blob/main/src/main/java/v8_bytecode/V8_PcodeOpEmitter.java), который во многом основан на аналогичном классе модуля для JVM. Данный класс генерирует p-code операции с помощью ряда методов. Рассмотрим их в порядке обращения к ним в нашем коде.

emitAssignVarnodeFromPcodeOpCall(String varnodeName, int size, String pcodeop, String... args)

Для понимания работы данного метода сначала рассмотрим понятие Varnodeодин из основных элементов p-code, по сути представляющий собой любую переменную, задействованную в p-code. Регистры, локальные переменные всё это Varnode.

Вернемся к методу. Данный метод генерирует p-code для вызова функции pcodeopс аргументами argsи сохраняет результат работы функции в varnodeName То есть в итоге получается такая конструкция:

varnodeName = pcodeop(args[0], args[1], );

emitPushCat1Value(String valueName) и emitPopCat1Value (String valueName)

Генерирует p-code для аналогов ассемблерных операций push и pop соответственно с Varnode valueName.

emitAssignVarnodeFromVarnode (String varnodeOutName, String varnodeInName, int size)

Генерирует p-code для операции присвоения значения varnodeOutName = varnodeInName

emitVarnodeCall (String target, int size)

Генерирует P-Code для вызова функции target.

Заключение

Благодаря вышеизложенному механизму у нас получилось значительно улучшить вывод декомплилятора Ghidra. В итоге динамическая генерация p-code стала еще одним кирпичиком в нашем большом инструменте модуле для анализа скомпилированного bytenode скриптов Node.JS. Исходный код модуля доступен в нашем репозитории на github.com. Пользуйтесь, и удачного вам реверс-инжиниринга!

Если у вас остались какие-то вопросы, задавайте их в комментариях - буду рад ответить.

Большое спасибо за исследование особенностей Node.js и разработку модуля моим коллегам: Владимиру Кононовичу, Наталье Тляповой, Сергею Федонину.

Подробнее..

Реверс инжиниринг приборной панели Nissan Muran Z50

13.06.2021 10:20:17 | Автор: admin
Имеем Nissan Murano 2005 года выпуска. Американец. Ни блютуза ни, уж тем более, цифровой приборной панели, а хочется. Если с блютузом проблема решается прикручиванием устройств подобных этому:

то оцифровать приборную панель уже будет посложнее. Однако, глаза боятся, а руки делают.

Для экспериментов я приобрел приборку от европейца, но принципиально она ничем от американца не отличается, кроме как расположением кнопок одометра и стоп-сигнала, и настройками тока для стрелочки спидометра (а еще она не такая жирная и не пытается найти у меня нефть). Дело в том, что у европейца и американца отличается максимальная скорость на спидометре, соответственно при 240км/ч на американце, европеец покажет приблизительно 210. Таким образом, графическая накладка не взаимозаменяемая:


Далее, согласно документации на приборную панель (файл Приборка.pdf, все архивы оставлю в конце) подключаем питание к приборке. Контакты 22, 23, 24 черные провода, на схеме обозначены буквой B подключаем к минусу (один или все вместе неважно), и контакты 21Y/R желтый с красной полосой и 20O оранжевый (оба сразу) к + 12 вольтам. Контакт 15R/W красный с белой полосой это освещение приборки, то же 12 вольт, но подключать не обязательно. Важное замечание: фактический номер контакта может отличатся от указанного в схеме, ибо инженеры нисан японцы на всю голову и менуют номер контакта по японскому! То есть на плате три штекера один основной для подключения к тачке и два для подключения кнопок стоп-сигнала и сброса одометра. Так вот вместо того, чтобы обозначить в каждом штекере контакты от 1 и до максимального, они объединили номера всех трех штекеров в одну группу, таким образом первый по номеру контакт находится на первом штекере, а последний на третьем. Короче ориентируйтесь по цветам и моим картинкам. Вот фото задней части:


Вот схема из файла Приборка.pdf (страница 10) с номерами контактов и куда чего идет:


А теперь опишу как работает панель согласно документации Приборка.pdf.

  • Лампа ремня безопасности контакт 3
  • Лампа омывайки контакт 6
  • Лампа аккумулятора контакт 4
  • Лампа сигнализации контакт 5
  • Работа спидометра:

Контакт 14 V/W Фиолетовый с белой полосой. Приборная панель выдает на него +5 вольт. Сигнал с датчика скорости замыкает контакт на землю формируя на нем логическую единицу. На один оборот колеса приходится 8 импульсов на землю.

И тут начинается самое вкусное. Все остальные данные которые выводит приборная панель, приходят по шине UART, а именно: тахометр, уровень топлива, температура охлаждайки, ошибка двигателя, CVT, ABS, низкое давление в шинах, AWD, AWD Lock, сигнал поворота, дальний свет, VDC OFF, SLIP, индикаторы круиз-контроля CRUISE и SET, CVT, индикатор открытой двери, уровень масла, габариты, сигнал пищалки и режим коробки (P, D, R, S и так далее включая ручной режим и номер передачи). Пробег одометра рассчитывается согласно показаниям сигнала спидометра и пишется в память приборной панели (в том числе). Когда я подключаю чужую приборку к своему авто, показания одометра не родной приборки не сбрасываются.

Далее приведу показания приборки при подключении питания, но отсутствии управляющих сигналов (согласно файлу Приборка.pdf страница 32). UART не подключен и линии контактов 3, 4, 5 и 6 висят в воздухе:

Спидометр, тахометр, топливо, температура охлаждайки показания по нулям. Сама панель будет находиться в ночном режиме, то есть подсветка приборов включена. Дисплей Вариатора не показывает ничего. Пищалка молчит.

ABS, VDC OFF, SLIP, Ошибка тормоза все эти индикаторы должны гореть при подключении питания и отсутствии связи.

Дальний свет, не закрыта дверь, низкое давление в шинах, CRUISE, SET, AWD, AWD LOCK, масло, поворотники, ошибка двигателя, CVT эти лампы будут выключены.

Теперь нам необходимо подключиться к контактам UART и вывести данные в компьютер при помощи USB-UART моста. Я использовал вот такой:


Вы можете использовать любой другой по запросу USB to UART bridge. Упоротые могут достать из arduino UNO проц или закоротить его на ресет и использовать встроенный UART. Короче TX приборки (контакт 18R/L красный с синей полосой) подключаем к RX нашего UART преобразователя и любой из черных проводов (контакты 22, 23, 24) к контакту GND UART. Далее запускаем любой монитор COM порта, выбираем свой UART в списке, подключаемся и наблюдаем поток сознания. Это краткая версия. Теперь подробнее. Качаем, например, это и втыкаем UART в USB. В списке COM Port должен появится новый COM к нему и подключаемся. Вот теперь и наблюдаем поток сознания от приборной панели в главном окне программы. Если не наблюдаем значит вы все сделали правильно, поскольку еще необходимо подобрать скорость приема данных Baud Rate. Выбирая разные режимы, я отмел все, что выше 115200, ибо при таких настройках терминал выдавал сплошные нули. В настройках между 9600 и 19200 стала появляться хоть какая-то структура. Но дальше понятнее не становилось. Приуныв, я подпер подбородок рукой и, внезапно, обнаружил на столе осциллограф! Ого подумал я. У меня есть такая штука! Короче решил я посмотреть, что покажет эта шайтан-машинка. Кое-как подобрав настройки, я наблюдал непонятное нечто, с которым, ума не приложу, что делать. Ладно поищем что же вообще обозначает Baud Rate. Ага, википедия говорит, что это количество бит данных в секунду. Ну, думаю, дай гляну по осциллографу длину самого короткого импульса. Оказалось, что-то около 70 наносекунд. Почесав репу, решил это дело разделить на 1000000 наносекунд, а почему нет? Результат оказался близок к 14400. Есть такая скорость снова подумал я, и выставил на всякий случай такие настройки: Baud Rate 14400, Data bits 8, Parity odd, Stop bits 1, Handshaking XON/XOFF. И, черт возьми, все внезапно заработало! Ну то ест ничего не изменилось, окно выдавало все такой же странный набор символов, но чуйка подсказывала что я двигаюсь в правильном направлении. Короче снял я со спидометра такую штуку:

Это после декодирования

A0 0F 00 0F 00 14 00 00 00 00 14 A1 0B 00 0B 00 6E 00 00 00 00 6E A2 0F 00 02 00 14 10 00 00 00 09 A3 07 00 00 00 6E 05 00 00 00 6C A4 0F 00 0C 40 34 3F 00 00 00 48 A5 02 00 0E 00 6E 01 08 00 00 6B A6 0C 00 01 00 14 1F 00 00 00 06 A7 00 00 0F 00 6E 00 00 00 00 61A8 00 00 00 00 14 00 00 00 00 14 A9 00 00 00 00 6E 00 00 00 00 6E

Потыкался с нулями, не нашел ничего интересного, но, снова внезапно, обнаружил, что байт со знаком А... повторяется каждые 10 раз. Привел я к такой структуре:

(Повторяющийся кусок)

A0 0F 00 0F 00 14 00 00 00 00 14A1 0B 00 0B 00 6E 00 00 00 00 6EA2 0F 00 02 00 14 10 00 00 00 09A3 07 00 00 00 6E 05 00 00 00 6CA4 0F 00 0C 40 34 3F 00 00 00 48A5 02 00 0E 00 6E 01 08 00 00 6BA6 0C 00 01 00 14 1F 00 00 00 06A7 00 00 0F 00 6E 00 00 00 00 61A8 00 00 00 00 14 00 00 00 00 14A9 00 00 00 00 6E 00 00 00 00 6E

Предпоследние два байта всегда по нулям первые, если перевести HEX в DECIMAL растут с 160 по 169. HEX набор символов о котором речь шла выше, это ничто иное как десятичные числа в шестнадцатеричном (чуть пальцы не сломал пока писал) формате.

Стало жарко. Дай думаю скормлю эти же данные приборке через Terminal. Втыкаю TX USB-UART преобразователя в RX приборки, и она начинает пищать всеми цветами радуги. Тут я догадываюсь что не все так просто. Видимо UART физического уровня несколько отличается от того, что используется в тех же ардуинах. Снимаю накладку с приборки, туплю на плату, перерисовываю схему и ага! Действительно это дифференциальная пара. Не вдаваясь в подробности скажу лишь, что я подпаялся непосредственно к площадкам, идущим прямо к контроллеру и тогда все заработало нормально. Вот фото куда чего паять:


А теперь, самое вкусное. Несем всю эту нанотехнологию в машину, подрубаем свою модифицированную приборку, UART к буку и приборке. Причем к приборке теперь подрубаем не выход, а наоборот ВХОД (Контакт 19R/B красный с черной полосой) что бы снять данные которые шлет машина на приборную панель. Затем врубаем зажигание, запускаем на буке Terminal и он начинает записывать все, что приходит на приборку. А теперь начинаем тыкать во все кнопочки подряд в своей машине. Открываем-закрываем двери, включаем дворники, жмем газ-тормоз-реверс-с-м у кого что, свет дальний ближний, поворотники. Желательно убить мотор и вариатор, что бы мы могли снять коды ошибки мотора и вариатора. Причем мотор надо убить в разных вариантах и позах и каждый датчик по отдельности. В общем снял я данные с машины, и вот что вышло:

(Небольшой кусок от A0 до A9)A0 40 00 00 78 3F 00 00 00 00 07A1 00 00 00 10 00 00 00 00 00 10A2 40 00 00 78 3F 00 00 00 00 07A3 00 00 00 10 00 00 00 00 00 10A4 40 00 00 78 3F 00 00 00 00 07A5 00 00 00 10 00 00 00 00 00 10A6 40 00 00 78 3F 00 00 00 00 07A7 00 00 00 10 00 00 00 00 00 10A8 40 00 00 78 3F 00 00 00 00 07A9 00 00 00 10 00 00 00 00 00 10

Ого подумал я. Это же почти как у спидометра! Дай, думаю, скормлю приборке логи, что я записал с машины. Скормил. Приборка повторила все то, что я вытворял в авто с авто на авто. Эка я умен не скромничал я про себя! Решил изменить некоторые знаки и снова скормить приборке измененный файл. Ничего не произошло. Хм подумал я. Чего же делать то? И приуныл, снова подперев рукой подбородок.

.Странные цифры в конце каждой секции
Четыре пары нулей и потом какие-то цифры
Открыл я виндокалькулятор. Зачем-то переключил в режим HEX. Почему-то решил взять все числа в формате HEX исключая номер секции и последнее число и перемножить их между собой методом XOR. Внезапно! Сумма XOR чисел оказалась равна последнему числу в секции. Интересно подумал я. Видимо бит честности осенило мою голову. Изменил несколько чисел, заXORил их между собой, сумму вписал в конце секции и скормил приборке. Приборка изменила показания тахометра. Все понятно. Структура стала ясна:

A0 40 00 00 78 3F 00 00 00 00 07

Номер секции A0

Данные 40 00 00 78 3F 00

Биты конца строки 00 00 00

Бит четности 07

Короче, чтобы расшифровать чего куда какой бит изменяет на@овнокодил я такую софтину:


Управление крайне примитивное жмякаем Выбрать порт. Ждем Еще ждем Опять ждем Во всяком случае так у меня. Вылезет окошко со списком портов. По клику по имени порта, подключается к порту. Л Логика! Что бы начать передачу тыкаем кнопку Начать передачу. Л Логика! Левая часть содержит 10 секций от A0 до A9. Во время передачи эти секции последовательно грузятся в выбранный порт. Подводим указатель мыши к любой ячейке и крутим колесо. Значения меняются и на лету меняются показания на приборке. В правой части должен был выводиться ответ от приборки, но я @овнокодер, у меня все тупило, потому сейчас там просто форматированный вывод. Можно сохранить текущие настройки HEX в файл и потом его загрузить. Для этого жмякаем одноименные кнопки. Софт автоматом подсчитывает сумму и пересчитывает байт честности. Так что его не трогаем, да и нифига у вас не выйдет. Байт в синем квадратике менять можно, но скорее всего это разделитель. Он вроде как никак не влияет на данные. Для полнофункциональной работы достаточно скармливать первые две секции A0 и A1. В первой секции содержаться данные тахометра и еще что-то, во второй все лампочки-ошибки и положение ручки АКПП. Короче разберетесь. Посредине синие точки, это включатели отправки секции. Если синяя, значит эта секция отправляется в COM port. При запуске открывается дефолтная оснастка, которая 100% работает. Софтина кривая вылетает с завидной регулярностью, да и пофигу. В архивчике лежит все, что надо для самостоятельных танцев с бубнов, включая мануалы к авто и моя кривая софтина.



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

IDA Pro работа с библиотечным кодом (не WinAPI)

01.07.2020 20:13:32 | Автор: admin

Всем привет,



При работе в IDA мне, да и, наверняка, вам тоже, часто приходится иметь дело с приложениями, которые имеют достаточно большой объём кода, не имеют символьной информации и, к тому же, содержат много библиотечного кода. Зачастую, такой код нужно уметь отличать от написанного пользователем. И, если на вход библиотечного кода подаются только int, void *, да const char *, можно отделаться одними лишь сигнатурами (созданные с помощью FLAIR-утилит sig-файлы). Но, если нужны структуры, аргументы, их количество, тут без дополнительной магии не обойдёшься В качестве примера я буду работать с игрой для Sony Playstation 1, написанной с использованием PSYQ v4.7.


Дополнительная магия


Представим ситуацию: вам попалась прошивка от какой-нибудь железки. Обычный Bare-metal ROM (можно даже с RTOS). Или же ROM игры. В подобных случаях, скорее всего, при компиляции использовался какой-то SDK/DDK, у которого имеется набор LIB/H/OBJ файлов, которые вклеиваются линкером в итоговый файл.


Наш план действий будет примерно таким:


  1. Взять все lib/obj-файлы, и создать из них сигнатуры (или набор сигнатур). Это поможет нам отделить статически влинкованный библиотечный код.
  2. Взять все h-файлы и создать из них библиотеку типов. Эта библиотека хранит не только типы данных, но и информацию об именах и типах аргументов функций, в которых объявленные типы используются.
  3. Применить сигнатуры, чтобы у нас определились библиотечные функции и их имена.
  4. Применить библиотеки типов, чтобы применить прототипы функций и используемые типы данных.

Создаём sig-файлы


Для создания файла сигнатур необходимо воспользоваться набором FLAIR-утилит, доступных лицензионным пользователям IDA. Список необходимых утилит следующий:


  • pcf LIB/OBJ-parser, создаёт PAT-файл из COFF-объектных файлов
  • pelf LIB/OBJ-парсер, создаёт PAT-файл из ELF-файлов (Unix)
  • plb LIB/OBJ-parser, создаёт PAT-файл из OMF-объектных файлов
  • pmacho MACH-O-парсер, создаёт PAT-файл из MACH-O-файлов (MacOS)
  • ppsx OBJ-парсер, создаёт PAT-файл из библиотечных файлов PSYQ
  • ptmobj OBJ-парсер, создаёт PAT-файл из библиотечных файлов Trimedia
  • sigmake конвертирует ранее созданный PAT-файл в SIG-файл, перевариваемый IDA

В моём случае это ppsx. Собираю bat-файл, в котором перечисляю все lib- и obj-файлы, и добавляю к каждой строке вызов ppsx, чтобы получилось формирование итогового PAT-файла. Получилось следующее содержимое:


run_47.bat
@echo offppsx -a 2MBYTE.OBJ psyq47.patppsx -a 8MBYTE.OBJ psyq47.patppsx -a LIBAPI.LIB psyq47.patppsx -a LIBC.LIB psyq47.patppsx -a LIBC2.LIB psyq47.patppsx -a LIBCARD.LIB psyq47.patppsx -a LIBCD.LIB psyq47.patppsx -a LIBCOMB.LIB psyq47.patppsx -a LIBDS.LIB psyq47.patppsx -a LIBETC.LIB psyq47.patppsx -a LIBGPU.LIB psyq47.patppsx -a LIBGS.LIB psyq47.patppsx -a LIBGTE.LIB psyq47.patppsx -a LIBGUN.LIB psyq47.patppsx -a LIBHMD.LIB psyq47.patppsx -a LIBMATH.LIB psyq47.patppsx -a LIBMCRD.LIB psyq47.patppsx -a LIBMCX.LIB psyq47.patppsx -a LIBPAD.LIB psyq47.patppsx -a LIBPRESS.LIB psyq47.patppsx -a LIBSIO.LIB psyq47.patppsx -a ashldi3.obj psyq47.patppsx -a ashrdi3.obj psyq47.patppsx -a CACHE.OBJ psyq47.patppsx -a clear_cache.obj psyq47.patppsx -a CLOSE.OBJ psyq47.patppsx -a cmpdi2.obj psyq47.patppsx -a CREAT.OBJ psyq47.patppsx -a ctors.obj psyq47.patppsx -a divdi3.obj psyq47.patppsx -a dummy.obj psyq47.patppsx -a eh.obj psyq47.patppsx -a eh_compat.obj psyq47.patppsx -a exit.obj psyq47.patppsx -a ffsdi2.obj psyq47.patppsx -a fixdfdi.obj psyq47.patppsx -a fixsfdi.obj psyq47.patppsx -a fixtfdi.obj psyq47.patppsx -a fixunsdfdi.obj psyq47.patppsx -a fixunsdfsi.obj psyq47.patppsx -a fixunssfdi.obj psyq47.patppsx -a fixunssfsi.obj psyq47.patppsx -a fixunstfdi.obj psyq47.patppsx -a fixunsxfdi.obj psyq47.patppsx -a fixunsxfsi.obj psyq47.patppsx -a fixxfdi.obj psyq47.patppsx -a floatdidf.obj psyq47.patppsx -a floatdisf.obj psyq47.patppsx -a floatditf.obj psyq47.patppsx -a floatdixf.obj psyq47.patppsx -a FSINIT.OBJ psyq47.patppsx -a gcc_bcmp.obj psyq47.patppsx -a LSEEK.OBJ psyq47.patppsx -a lshrdi3.obj psyq47.patppsx -a moddi3.obj psyq47.patppsx -a muldi3.obj psyq47.patppsx -a negdi2.obj psyq47.patppsx -a new_handler.obj psyq47.patppsx -a op_delete.obj psyq47.patppsx -a op_new.obj psyq47.patppsx -a op_vdel.obj psyq47.patppsx -a op_vnew.obj psyq47.patppsx -a OPEN.OBJ psyq47.patppsx -a PROFILE.OBJ psyq47.patppsx -a pure.obj psyq47.patppsx -a read.obj psyq47.patppsx -a shtab.obj psyq47.patppsx -a snctors.obj psyq47.patppsx -a SNDEF.OBJ psyq47.patppsx -a SNMAIN.OBJ psyq47.patppsx -a SNREAD.OBJ psyq47.patppsx -a SNWRITE.OBJ psyq47.patppsx -a trampoline.obj psyq47.patppsx -a ucmpdi2.obj psyq47.patppsx -a udiv_w_sdiv.obj psyq47.patppsx -a udivdi3.obj psyq47.patppsx -a udivmoddi4.obj psyq47.patppsx -a umoddi3.obj psyq47.patppsx -a varargs.obj psyq47.patppsx -a write.obj psyq47.patppsx -a LIBSND.LIB psyq47.patppsx -a LIBSPU.LIB psyq47.patppsx -a LIBTAP.LIB psyq47.patppsx -a LOW.OBJ psyq47.patppsx -a MCGUI.OBJ psyq47.patppsx -a MCGUI_E.OBJ psyq47.patppsx -a NOHEAP.OBJ psyq47.patppsx -a NONE3.OBJ psyq47.patppsx -a NOPRINT.OBJ psyq47.patppsx -a POWERON.OBJ psyq47.pat

LIBSN.LIB файл имеет формат, отличный от остальных библиотек, поэтому пришлось разложить его на OBJ-файлы утилитой PSYLIB2.EXE, которая входит в комплект PSYQ. Запускаем run_47.bat. Получаем следующий выхлоп:


run_47.bat output
2MBYTE.OBJ: skipped 0, total 18MBYTE.OBJ: skipped 0, total 1LIBAPI.LIB: skipped 0, total 89LIBC.LIB: skipped 0, total 55LIBC2.LIB: skipped 0, total 50LIBCARD.LIB: skipped 0, total 18LIBCD.LIB: skipped 0, total 51LIBCOMB.LIB: skipped 0, total 3LIBDS.LIB: skipped 0, total 36LIBETC.LIB: skipped 0, total 8LIBGPU.LIB: skipped 0, total 60LIBGS.LIB: skipped 0, total 167LIBGTE.LIB: skipped 0, total 535LIBGUN.LIB: skipped 0, total 2LIBHMD.LIB: skipped 0, total 585LIBMATH.LIB: skipped 0, total 59LIBMCRD.LIB: skipped 0, total 7LIBMCX.LIB: skipped 0, total 31LIBPAD.LIB: skipped 0, total 21LIBPRESS.LIB: skipped 0, total 7LIBSIO.LIB: skipped 0, total 4ashldi3.obj: skipped 0, total 1ashrdi3.obj: skipped 0, total 1CACHE.OBJ: skipped 0, total 1clear_cache.obj: skipped 0, total 1CLOSE.OBJ: skipped 0, total 1cmpdi2.obj: skipped 0, total 1CREAT.OBJ: skipped 0, total 1ctors.obj: skipped 0, total 0divdi3.obj: skipped 0, total 1dummy.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000022Deh_compat.obj: skipped 0, total 1exit.obj: skipped 0, total 1ffsdi2.obj: skipped 0, total 1fixdfdi.obj: skipped 0, total 1fixsfdi.obj: skipped 0, total 1fixtfdi.obj: skipped 0, total 0fixunsdfdi.obj: skipped 0, total 1fixunsdfsi.obj: skipped 0, total 1fixunssfdi.obj: skipped 0, total 1fixunssfsi.obj: skipped 0, total 1fixunstfdi.obj: skipped 0, total 0fixunsxfdi.obj: skipped 0, total 0fixunsxfsi.obj: skipped 0, total 0fixxfdi.obj: skipped 0, total 0floatdidf.obj: skipped 0, total 1floatdisf.obj: skipped 0, total 1floatditf.obj: skipped 0, total 0floatdixf.obj: skipped 0, total 0FSINIT.OBJ: skipped 0, total 1gcc_bcmp.obj: skipped 0, total 1LSEEK.OBJ: skipped 0, total 1lshrdi3.obj: skipped 0, total 1moddi3.obj: skipped 0, total 1muldi3.obj: skipped 0, total 1negdi2.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000013Dop_delete.obj: skipped 0, total 1op_new.obj: skipped 0, total 1op_vdel.obj: skipped 0, total 1op_vnew.obj: skipped 0, total 1OPEN.OBJ: skipped 0, total 1PROFILE.OBJ: skipped 0, total 1pure.obj: skipped 0, total 1Fatal: Unknown record type 60 at 0000015Fshtab.obj: skipped 0, total 0Fatal: Unknown record type 60 at 000000EESNDEF.OBJ: skipped 0, total 0SNMAIN.OBJ: skipped 0, total 1SNREAD.OBJ: skipped 0, total 1SNWRITE.OBJ: skipped 0, total 1trampoline.obj: skipped 0, total 0ucmpdi2.obj: skipped 0, total 1udiv_w_sdiv.obj: skipped 0, total 1udivdi3.obj: skipped 0, total 1udivmoddi4.obj: skipped 0, total 1umoddi3.obj: skipped 0, total 1varargs.obj: skipped 0, total 1Fatal: Unknown record type 60 at 00000160LIBSND.LIB: skipped 0, total 223LIBSPU.LIB: skipped 0, total 126LIBTAP.LIB: skipped 0, total 1LOW.OBJ: skipped 0, total 1Fatal: can't find symbol F003MCGUI_E.OBJ: skipped 0, total 1NOHEAP.OBJ: skipped 0, total 1NONE3.OBJ: skipped 0, total 1NOPRINT.OBJ: skipped 0, total 1POWERON.OBJ: skipped 0, total 1

Видим некоторое количество ошибок парсинга, но, в тех файлах всего 1 сигнатура (total 1), поэтому, думаю, что это не критично. Далее преобразовываем PAT-файл в SIG-файл:


sigmake -n"PsyQ v4.7" psyq47.pat psyq47.sigpsyq47.sig: modules/leaves: 1345/2177, COLLISIONS: 21See the documentation to learn how to resolve collisions.

В итоге получаем следующий список файлов:


  • psyq47.err его не трогаем
  • psyq47.exc его нужно будет отредактировать
  • psyq47.pat его тоже не трогаем

Открываем на редактирование .exc-файл. Видим:


;--------- (delete these lines to allow sigmake to read this file); add '+' at the start of a line to select a module; add '-' if you are not sure about the selection; do nothing if you want to exclude all modules

Если удалить ---------, всё, что содержится в файле ниже, будет учитываться. Давайте взглянем на то, что там есть. Вот пример:


CdPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300DsPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300

Видим, что две функции имеют одну и ту же сигнатуру, и нам нужно выбрать, какую из них использовать. Для этого слева, рядом с именем нужной функции ставим +. Я выбираю первую. То же самое повторяем с остальными строками.


В итоге, если всё сделано правильно, получаем SIG-файл. Его нужно положить в соответствующую папку в каталоге установка IDA.


Создаём til-файлы


Эти файлы нужны для хранения информации о типах, об аргументах функций и т.п. По-умолчанию. Создаются они с помощью утилиты tilib, которую необходимо положить в каталог с Идой (ей, почему-то, нужен ida.hlp файл).


Данной утилите нужно скормить include-файлы вашего SDK/DDK. При том парсинг этой утилитой отличается от такового средством "Parse C header file..." в самой IDA. Вот описание из readme:


Its functionality overlaps with "Parse C header file..." from IDA Pro.
However, this utility is easier to use and provides more control
over the output. Also, it can handle the preprocessor symbols, while
the built-in command ignores them.

У этой утилиты есть один нюанс: она по умолчанию использует режим, когда символы заманглены, либо имеют стоящее в начале имени нижнее подчёркивание. В случае работы со статически влинкованным кодом этот режим нужно выключить флагом -Gn.


По-умолчанию, данная утилита принимает на вход только один include-файл. Если же файлов много, нужно соорудить include-файл следующего содержания:


#include "header1.h"#include "header2.h"#include "header3.h"// ...

Этот файл передаётся с помощью флага -hFileName.h. Далее, передаём путь поиска остальных header-файлов и получаем следующую командую строку:


tilib -c -Gn -I. -hpsyq47.h psyq47.til

На выходе получаем til-файл, пригодный для использования. Кладём его в соответствующий каталог IDA: sig\mips.


Проверяем результат


Закидываем ROM-файл в IDA, дожидаемся окончания анализа. Далее, необходимо указать компилятор. Для этого заходим в Options->Compiler:



Теперь просто меняем Unknown на GNU C++ (в случае PSX). Остальное оставляем как есть:



Теперь жмём Shift+F5 (либо меню View->Open subviews->Signatures), жмём Insert и выбираем нужный файл сигнатур:



Жмём OK и ждём, пока применяются сигнатуры (у меня получилось 482 распознанных функции).



Далее необходимо применить библиотеку типов (til-файл). Для этого жмём Shift+F11 (либо View->Open subviews->Type libraries) и понимаем, что IDA не может определить компилятор (не смотря на то, что мы его уже указали):



Но это нам всё равно не помешает выбрать til-файл (всё так же, через Insert):



Получаем то, что так хотели:



Теперь декомпилятор успешно подхватывает информацию о типах, и выхлоп становится куда лучше:



P.S.


Надеюсь, эта информация окажется для вас полезной. Удачного реверс-инжиниринга!

Подробнее..

Перевод Обратная разработка XC2064 первой микросхемы FPGA

29.09.2020 00:04:54 | Автор: admin
Программируемая Логическая Интегральная Схема (ПЛИС) может реализовать произвольную логику, что угодно, от микропроцессора до генератора видеосигнала или майнера криптовалюты. ПЛИС состоит из множества логических блоков, каждый из которых обычно состоит из триггера и логической функции, а также из сети проводников, соединяющей логические блоки. Что делает ПЛИС особенной, это то, что она является программируемым аппаратным обеспечением, вы можете сконфигурировать каждый логический блок и соединения между ними. В результате вы можете построить сложную цифровую схему без физического соединения каждого логического элемента и триггера, что обошлось бы вам в стоимость разработки заказной интегральной схемы.


Фотография показывает один из 64 блоков микросхемы XC2064. Слои металлизации убраны, мы видим кремний и поликремниевые транзисторы, лежащие под металлизацией. По ссылке вы можете увидеть фото в большем масштабе: siliconpr0n.


ПЛИС была изобретена Россом Фрименом, сооснователем Xilinx в 1984 году, первой микросхемой ПЛИС была XC2064. Она была гораздо проще современных ПЛИС, содержала всего лишь 64 логических блока, по сравнению с тысячами и миллионами в современных FPGA, и её создание привело к появлению отрасли стоимостью в миллиарды долларов. XC2064 настолько важна, что вошла в Зал Славы Микросхем. Я провёл обратную разработку XC2064, и в этом посте я объясняю её внутреннюю структуру в общих чертах, и как она программируется потоком бит (bitstream).
Росс Фримен
Росс Фримен (Ross Freeman) трагически погиб от пневмонии в возрасте 45 лет, через пять лет после изобретения ПЛИС. В 2009 году, Фримен был помещён в Зал Славы Изобретателей.

Xilinx
Xilinx была одной из первых фаблесс-компаний. В отличие от большинства полупроводниковых компаний, которые разрабатывали и производили полупроводники, Xilinx только разрабатывала дизайн чипа, а другая компания производила. Xilinx использовала производство Seiko Epson Semiconductor Division (часы Seiko и принтеры Epson).

XC2064
Заказные интегральные схемы имеют проблему: высокая стоимость и длительное время (месяцы или годы) на разработку и производство чипа. Одно из решений программируемое логическое устройство (Programmable Logic Devices, PLD), чип с массивом логических элементов, который может быть запрограммирован на выполнение различных функций, который был разработан примерно в 1967 году. Изначально они программировались маской, металлический слой чипа разрабатывался под требуемую функциональность, изготавливалась маска, и чип производился в соответствии со спецификациями. Позже чипы, содержащие PROM, получили возможность программирования пользователем, в них пережигались перемычки на чипе, также появились чипы с EPROM, которые можно было перепрограммировать. Программируемые логические устройства имели множество маркетинговых названий, таких, как Programmable Logic Array, Programmable Array Logic (1978), Generic Array Logic и Uncommitted Logic Array. По большей части, такие устройства состояли из логических элементов, соединённых по схеме сумма произведений, некоторые из них содержали триггеры. Главная инновация, которую внесли ПЛИС, были программируемые межсоединения между логическими блоками, а не фиксированная архитектура, а также логические блоки, содержащие триггеры. Для получения глубокого взгляда на историю ПЛИС и эффект масштабируемости, см. "Three Ages of FPGAs: A Retrospective on the First Thirty Years of FPGA Technology." Также посмотрите "A Brief History of FPGAs".



Xilinx XC2064 первая микросхема ПЛИС. Взято отсюда: siliconpr0n.

В настоящее время, ПЛИС программируются на языках описания аппаратуры, таких, как Verilog или VHDL, но в то время Xilinx предоставляла собственное ПО для разработки, приложение MS-DOS под названием XACT, с немаленькой стоимостью $12000. XACT работало на более низком уровне, нежели современные инструменты: пользователь определял функцию каждого логического блока, как показано на скриншоте ниже, и соединения между логическими блоками.
XACT разводил соединения и генерировал файл конфигурации (битстрим), который загружался в ПЛИС.


Скриншот XACT. Две таблицы F и G реализуют уравнения, показанные внизу экрана, с картой Карно, показанной выше.

ПЛИС конфигурировалась с помощью битстрима, последовательности бит, имеющей проприетарный формат. Если вы посмотрите на битстрим XC2064 (ниже), то увидите загадочную смесь паттернов, которые повторяются нерегулярным образом и разбросаны по всему битстриму. Однако, изучение физических цепей ПЛИС открывает нам структуру данных битстрима и он может быть изучен.


Часть битстрима, сгенерированного XACT.

Как работает ПЛИС?


На приведённом ниже рисунке, взятом из оригинального патента на ПЛИС, показана базовая структура ПЛИС. В этой упрощённой ПЛИС всего 9 логических блоков (обозначены голубым), и 12 портов ввода-вывода. Сеть межсоединений соединяет компоненты вместе. Установкой переключателей (диагональные линии) на соединениях, логические блоки могут быть соединены между собой и с портами ввода-вывода. Каждый логический элемент может быть запрограммирован на требуемую логическую функцию. В результате, такой программируемый чип может реализовать любое устройство, которое поместится в доступный объём.


Патент на ПЛИС, логические блоки(LE) соединённые между собой.

Конфигурируемые логические боки (CLB, Configurable Logic Block)


Несмотря на то, что на рисунке выше показаны 9 CLB, XC2064 имеет 64 CLB. На рисунке ниже показана структура CLB. CLB имеет 4 входа (A, B, C, D) и два выхода (X и Y). Между ними находится комбинационная логика, которая может быть запрограммирована на любую желаемую логическую функцию. Также CLB содержит триггер, наличие которого позволяет реализовать счётчики, регистры сдвига, конечные автоматы и другие схемы, сохраняющие состояние. Трапециями обозначены мультиплексоры, которые могут быть запрограммированы на прохождение сигнала с любого из входов. Мультиплексоры позволяют конфигурировать CLB под конкретную задачу, выбирая определённые сигналы для управления триггером и выходами.


Конфигурируемый логический блок в XC2064, взято отсюда: datasheet.

Возможно, вам интересно, как комбинационная логика реализует произвольные логические функции. Происходит ли выбор между набором элементов AND, OR, XOR и т. д.? Нет, используется хитрый трюк, называемый таблицей просмотра (lookup table, LUT), которая фактически является таблицей истинности для функции. Например, функция трёх переменных определяется таблицей из 8 строк. LUT содержит 8 бит памяти. Мы можем реализовать любую 3-входовую логическую функцию, сохраняя эти три бита.
Таблицы просмотра
Таблицы просмотра в XC2064 это нечто более сложное, чем просто таблицы. Каждый CLB содержит две трёхвходовые таблицы просмотра. Входы таблиц в XC2064 имеют программируемые мультиплексоры, позволяя выбрать 4 различных потенциальных входа. Вдобавок, две таблицы просмотра могут быть объединены для получения функции четырёх переменных и других комбинаций.


Логические функции в ПЛИС XC2064 реализованы с помощью таблиц просмотра. Из документации.


Соединения


Следующий ключевой аспект ПЛИС, это соединения, которые могут быть запрограммированы на коммутацию CLB различными способами. Соединения устроены сложно, но грубое описание состоит в том, что есть сегменты вертикальных и горизонтальных связей между всеми CLB. Можно соединять CLB с горизонтальными и вертикальными линиями, и создавать произвольные соединения. Более сложный тип соединения, это матрица переключателей. Каждая матрица имеет 8 выводов, которые могут соединяться друг с другом (почти) произвольным образом.

Рисунок внизу показывает структуру связей XC2064, которые обеспечивают соединения логических блоков (голубые) и линий ввода-вывода (жёлтые). Врезка показывает детали механизма соединения. Зелёные прямоугольники, это матрицы переключателей, имеющие 8 выводов, а маленькие квадратики, это программируемые точки соединений.


ПЛИС XC2064 имеет матрицу 8х8 CLB. Каждый CLB имеет название от AA до HH.

Соединение может коммутировать, например, выход блока DC с входом блока DE, как показано ниже. Красная линия показывает путь сигнала, а маленькие красные квадратики показывают активированные точки соединений. Выйдя из блока DC, сигнал направляется к первой точке соединений на 8-выводный переключатель (зелёный), который направляет его через две точки соединения и другой 8-входовый переключатель (неиспользуемые вертикальные и горизонтальные линии не показаны). Заметим, что соединение устроено достаточно сложно, даже такой короткий путь использует четыре точки соединения и два переключателя.


Пример разводки сигнала с выхода блока DC до блока DE.

Скриншот внизу показывает, как производится трассировка в программе XACT. Желтые линии показывают соединения между логическими блоками. По мере того, как количество сигналов увеличивается, становится сложно проводить соединения без конфликта между путями. Программа XACT производит автоматическую трассировку, но трассировка может быть также отредактирована вручную.


Скриншот программы XACT. Это программа MS-DOS, управление происходит с помощью клавиатуры и мыши.

Реализация


Дальше мы рассмотрим внутреннюю схему XC2064, проведя обратную разработку по фотографии кристалла. Предупреждаю, что это требует некоторого знакомства с XC2064.
Фотография кристалла
Для анализа XC2064 я использовал моё собственное фото кристалла XC20186, а также фото кристаллов XC2064 и XC2018 с siliconpr0n. Трудно анализировать ПЛИС по снимкам с оптического микроскопа, так как они имеют два слоя металлизации. John McMaster использовал электронный микроскоп, чтобы устранить неоднозначность, вносимую этими слоями. Фото ниже показывает, как верхний слой металлизации выглядит под электронным микроскопом.


Фото XC2064 под электронным микроскопом, с любезного разрешения John McMaster.

XC2018
ПЛИС Xilinx XC2018 (на фото ниже) имеет 100 ячеек, таких же, как в XC2064. Внутри она использует те же самые плитки, что и XC2064 с 64 ячейками, но они объединены в сетку 10х10, а не 8х8. Формат битстрима XC2018 очень похож, но больше по размеру.


ПЛИС XC2018. Справа корпус удалён, видно кремниевый кристалл. На кристалле слабо виден рисунок плиток.

На фото ниже можно видеть сравнение кристалла XC2064 и XC2018. Кристаллы очень похожи, за исключением того, что больший чип имеет две дополнительных строки и два дополнительных столбца на кристалле.


Сравнение кристаллов XC2064 и XC2018. Изображения масштабированы так, что размеры плиток совпадают, я не знаю, как соотносятся физические размеры кристаллов. Фото кристаллов взяты с siliconpr0n.

Ниже приведено фото кристалла XC2064. Главная часть ПЛИС это матрица блоков 8х8, каждый из них содержит логический блок и окружающие цепи. Хотя на схеме ПЛИС логические блоки (CLB) показаны как отдельные от соединительных схем сущности, в реальности это реализовано не так. На самом деле, каждый логический блок и его окружение реализованы в виде единого узла, плитки (tile). (Если быть точным, плитка включает в себя соединения вверху и слева от каждого CLB).


Схема размещения блоков на кристалле XC2064. Взято отсюда: siliconpr0n.

Расположенные вдоль сторон интегральной схемы, блоки ввода-вывода обеспечивают связь с внешним миром. Они подключены к выводам кристалла, которые показаны как маленькие зелёные квадратики, и подключены к выводам корпуса микросхемы. На кристалле есть буферы (зелёные): два вертикальных и два горизонтальных. Эти буферы усиливают сигнал, прошедший большое расстояние по микросхеме, и уменьшают задержку. Вертикальный регистр сдвига (розовый), и цепь выборки горизонтального столбца используются для загрузки битстрима в чип, как это будет показано ниже.

Внутри плитки


На рисунке внизу показана структура одной плитки XC2064, микросхема содержит 64 таких плитки, упакованных на одном кристалле. Около 40% каждой плитки занимают ячейки памяти (показаны зелёным цветом), которые хранят биты конфигурации. Верхняя треть (приблизительно) плитки содержит схемы соединений: две матрицы переключателей и некоторое количество отдельных переключателей линий связи. Ниже расположен логический блок. Главная часть логического блока мультиплексор входов, триггер и таблица просмотра. Плитка соединяется с соседями через горизонтальные и вертикальные линии связи, а также она подсоединена к шинам питания и земли. Биты данных конфигурации попадают в ячейки памяти горизонтально, а вертикальные сигналы выбирают конкретный столбец для загрузки.


Одна плитка FPGA, показаны важные функциональные узлы

Транзисторы


ПЛИС реализована на КМОП (CMOS) логике, построенной из NMOS и PMOS транзисторов. Транзисторы выполняют в ПЛИС две основных роли. Во-первых, из их комбинаций складываются логические элементы. Во-вторых, транзисторы используются как переключатели, через которые проходит сигнал, например, для формирования связей между блоками. Транзистор, выполняющий эту роль, называется проходным. На рисунке ниже показана базовая структура МОП-транзистора. Два участка кремния легируются примесями для создания стока и истока. Между ними расположен затвор, включающий и выключающий транзистор, и управляющий током между стоком и истоком. Затвор изготовлен из специального типа кремния, называемого поликремнием, он изолирован от кремния под ним тонким слоем оксида. Над ним находятся два слоя металлизации, обеспечивающие соединения цепей.


Структура MOSFET
На фото кристалла мы можем наблюдать, как выглядит транзистор под микроскопом. Поликремниевый затвор это змеистая линия между двумя легированными областями кремния. Круги переходные отверстия, соединяющие кремний и металлический слой (который на этом фото удалён).


Транзистор MOSFET на кристалле ПЛИС

Битовый поток и память конфигурации


Конфигурационная информация в XC2064 сохраняется в ячейках конфигурационной памяти. ПЛИС не использует для этого блочную память, вместо этого конфигурационная память распределена по кристаллу в виде решётки 160х71, каждый бит размещён рядом с той цепью, которой он управляет. На рисунке внизу показан конфигурационный битстрим, загруженный в ПЛИС. Битстрим загружается в сдвиговый регистр, который идёт посередине чипа (розовый). Как только 71 бит загружен в сдвиговый регистр, цепь выборки столбца (голубая) выбирает нужный столбец памяти, и биты загружаются в столбец параллельно. Затем следующие 71 бит загружаются в сдвиговый регистр и выбирается следующий столбец слева. Процесс повторяется для всех 160 столбцов ПЛИС, и весь битстрим загружается в ПЛИС. Использование сдвигового регистра позволяет обойтись без объёмных цепей адресации памяти.


Как битстрим загружается в ПЛИС. Биты показаны условно, реальное хранилище бит гораздо более плотное. Три столбца справа уже загружены и происходит загрузка в четвёртый. Фото кристалла взято отсюда: siliconpr0n.

Важной вещью является то, что битстрим распределён по чипу в точности в том порядке, в котором биты следуют в файле: упаковка битов в битстриме соответствует их физическому размещению на чипе. Как будет показано ниже, каждый бит хранится в ПЛИС рядом с цепью, которой он управляет. таким образом, формат файла битстрима напрямую определяется размещением аппаратных цепей. Например, если существует промежуток между плитками, и в нём расположен буфер, такой же промежуток будет и в битстриме. Структура битстрима зависит не от абстракций программного обеспечения, таких, как поля в таблицах данных или блоки конфигурации. Понимание битстрима требует мышления в аппаратных терминах, не в программных
Битстрим
Хотя битстрим напрямую отображается на аппаратные ячейки памяти, файл битстрима (.RBT) имеет некоторое форматирование, показанное ниже.


Формат данных битстрима, из документации.


Каждый бит конфигурационной памяти реализован, как показано ниже. Каждая ячейка памяти состоит из двух инвертеров, соединённых в петлю. такая цепь имеет два стабильных состояния, и может хранить один бит: либо верхний инвертер находится в состоянии 1, а нижний в состоянии 0, либо наоборот. Для записи в ячейку, проходной транзистор слева активируется, пропуская сигнал. Сигнал на линии данных просто перетягивает инвертер, записывая необходимый бит. (Можно также считать конфигурационные данные, используя ту же цепь.) Выход Q и инвертированный Q управляют определённой функцией в ПЛИС, например, замыканием цепи межсоединения, извлечения бита из таблицы просмотра, или управления триггером. В большинстве случаев, используется только выход Q.
Ячейка SRAM
Память конфигурации реализована на ячейках статической памяти (SRAM). Технически, эта память не является RAM, так как доступ производится последовательно, через регистр сдвига, но люди всё равно называют её SRAM. Эти ячейки памяти имеют пять транзисторов, и известны как 5T SRAM.

Остаётся под вопросом, есть ли в битстриме неиспользуемые биты. Вероятно, многие биты не используются. Например, каждая плитка имеет блок 18х18 бит, из которых 27 бит не используются. Просмотр кристалла показывает, что ячейки памяти, соответствующие неиспользованным битам, удалены полностью, и их место на кристалле использовано для других цепей. Фото кристалла внизу показывает 9 использованных бит и один пропущенный.


Ячейки памяти, показан промежуток, в котором одна ячейка пропущена. Взято с siliconpr0n.



Схема одного бита конфигурационной памяти, из документации.

На рисунке ниже показана физическая структура ячеек памяти. На фото слева показано 8 ячеек памяти, одна из которых выделена. Каждая горизонтальная линия данных подключена ко всем ячейкам памяти в ряду. Каждая линия выборки столбца выбирает все ячейки памяти в столбце, разрешая запись. На средней фотографии показаны участки кремния и поликремния для одной ячейки памяти. Слои металла были удалены для того, чтобы открыть находящиеся под ними транзисторы. Слои металлизации соединяют транзисторы, круги на фото, это соединения, переходные отверстия, между кремнием или поликремнием и металлом. Схема показывает, как пять транзисторов соединены, размещение элементов на схеме соответствует их размещению на фото. Две пары транзисторов формируют КМОП-инвертеры, проходной транзистор снизу слева обеспечивает доступ к ячейке.


Восемь бит конфигурационной памяти, четыре сверху и четыре снизу. Красный прямоугольник один бит. Когда линия выборки столбца активирована, линия данных строки загружает данные в соответствующую ячейку. Увеличенное фото и схема показывают один бит конфигурационной памяти. Взято отсюда: siliconpr0n.

Мультиплексоры таблицы просмотра


Как объяснялось ранее, ПЛИС реализует произвольные логические функции, используя таблицы просмотра. На рисунке ниже показано, как реализована таблица просмотра в XC2064. Восемь значений слева сохраняются в восьми ячейках памяти. Четыре мультиплексора выбирают одну из каждой пары значений, в зависимости от значения на входе А, если А = 0, выбирается верхнее из значений, если А = 1, то нижнее. Затем, большой мультиплексор выбирает одно из четырёх значений, основываясь на сигналах В и С. Результатом будет определённое значение, в данном случае A XOR B XOR C. Мы можем сделать любую логическую функцию, если будем подставлять разные значения в таблицу.


Реализация XOR в таблице просмотра.

Каждый мультиплексор реализован с помощью проходных транзисторов. В зависимости от сигналов управления, один из проходных транзисторов активируется, пропуская данные со входа на выход. Рисунок внизу показывает часть цепи LUT, с мультиплексированием двух бит. Справа находятся две ячейки памяти. Каждый бит проходит через инвертер, усиливается, и проходит через проходные транзисторы мультиплексора в середине, выбирая один из этих бит.


Вид цепей, реализующих LUT. Взято с siliconpr0n.

Триггер


Каждый блок CLB содержит триггер, позволяющий ПЛИС реализовывать защёлки, конечные автоматы, и другие цепи, сохраняющие состояние. На рисунке снизу показана (несколько необычная) реализация триггера. Она использует следующую схему. Когда тактовый сигнал в нуле, первый мультиплексор пропускает данные на первую защёлку, которая запоминает значение. Бит инвертируется дважды, так как проходит через элементы OR, NAND и инвертер, и в результате остаётся тем же. далее, мультиплексор второй защёлки принимает бит с первой защёлки, когда тактовый сигнал поднимается в 1 (отметим, что тактовый сигнал инвертирован). Это значение является выходом триггера. Когда тактовый сигнал устанавливается в 0, вторичный мультиплексор замыкает петлю, защёлкивая бит. Таким образом, триггер чувствителен к фронту сигнала, защёлкивая значение на переднем фронте тактового сигнала. Линии установки и сброса устанавливают и сбрасывают триггер.


Реализация триггера. Стрелки показывают на первый мультиплексор и два элемента OR-NAND Фото кристалла взято отсюда: siliconpr0n.

Восьмивыводная матрица переключателя


Матрица переключателя является важным элементом межсоединений. Каждый переключатель имеет 8 выводов (по два на каждой стороне) и может соединять их почти как угодно. Сигналы могут разворачиваться, разделяться, пересекаться с большей гибкостью, чем в отдельных узлах межсоединений. На рисунке снизу показана часть сети межсоединений между четырьмя CLB (голубые). Переключающие матрицы (зелёные) могут соединяться в любых комбинациях соединений с правой стороны. Отметим, что каждый вывод может соединяться с другими выводами в количестве от 5 до 7 штук. Например, вывод 1 может быть соединён с выводом 3, но не с выводами 2 и 4. Это делает матрицу почти полной, с 20 возможными соединениями вместо 28.


Взято отсюда: Xilinx Programmable Gate Array Data Book, рис. 7b.

Переключающая матрица образована рядами проходных транзисторов, управляемых ячейками памяти над ними и под ними. Две стороны транзистора представляют собой два вывода переключающей матрицы, которые могут быть соединены транзистором. Таким образом, каждая матрица имеет 20 управляющих бит, две матрицы на плитку дают нам 40 бит на плитку. На фото внизу изображена одна ячейка памяти, подсоединённая к волнистому затвору проходного транзистора под ней. Этот транзистор обеспечивает соединение между выводом 5 и выводом 1. Таким образом, бит в битстриме, соответствующий этой ячейке памяти, управляет соединением между выводами 5 и 1. Аналогично, другие ячейки памяти и связанные с ними транзисторы управляют другими соединениями. Отметим также, что порядок этих соединений не следует какому-то определённому паттерну, в результате, соответствие между битами в битстриме и выводами переключателя выглядит случайным.

Переключающая матрица
Переключающая матрица имеет 20 проходных транзисторов. Так как каждая плитка имеет память шириной 18 ячеек, два транзистора подключены к более дальним ячейкам памяти.



Реализация 8-выводной матрицы соединений. Области кремния обозначены номерами соответствующих им выводов. Слои металла, соединявшие соответствующие выводы с транзисторами, удалены. Взято с siliconpr0n.

Соединения входов


Входы CLB используют другую схему кодирования битстрима, которая объясняется его аппаратной реализацией. На рисунке ниже, восемь обведённых эллипсом узлов являются потенциальными входами в CLB под названием DD. Только один узел (максимум) может быть сконфигурирован как вход, так как подсоединение двух сигналов к одному входу может закоротить их вместе.


Выбор входа. Восемь входов, обведённых зелёным, являются потенциальными входами DD, только один из них может быть выбран.

Требуемый вход выбирается мультиплексором. Прямолинейным решением было бы использование 8-входового мультиплексора с тремя битами управления, выбирающими один из 8 сигналов. Другим прямолинейным решением было бы использование 8 проходных транзисторов, каждый со своим управляющим сигналом, один из которых выбирает требуемый сигнал. Однако, ПЛИС использует гибридный подход, который устраняет необходимость в декодере, необходимом в первом случае, но требует 5 управляющих сигналов вместо 8, требуемых при втором подходе.


ПЛИС использует мультиплексоры для выбора одного из восьми входов.

Схема на рисунке выше показывает двухуровневый мультиплексор, использованный в ПЛИС. На первой стадии активируется один из управляющих сигналов. На второй стадии выбирается верхний или нижний сигнал и подаётся на выход. Для примера, предположим, что управляющий сигнал B/F подан на первую стадию, и ABCD на вторую стадию, вход B будет единственным, который прошёл на выход. То есть, выбор одного из восьми входов требует 5 бит в битстриме и использует 5 ячеек памяти.

Входной мультиплексор CLB
Некоторые замечания о входном мультиплексоре CLB. Сигнал управления EFGH дополняет ABCD, то есть для него нужен только один управляющий сигнал в битстриме и только одна ячейка памяти. Во-вторых, другие входы в CLB имеют выбор в 6 или 10 входных сигналов, используется тот же подход с двойным мультиплексором, изменяется только количество входов и сигналов управления. Наконец, несколько сигналов управления инвертированы, возможно, потому, что инвертированный выход ячейки памяти был ближе. Всё это создаёт трудности, когда мы пытаемся понять битстрим, когда некоторые биты служат для выбора 6 входов, а не двух. Инвертирование бита, однако, восстанавливает паттерн.


Заключение


XC2064 использует различные высокооптимизированные цепи для реализации логических блоков и соединения цепей. Эти цепи требуется плотно упаковать, чтобы они вошли на кристалл. Даже несмотря на это, XC2064 был очень большим чипом, больше, чем микропроцессор того времени, и его производство было сложным, он стоил сотни долларов. По сравнению с современными ПЛИС, XC2064 имеет абсурдно малое число ячеек, но даже это послужило началом революционно новой линейке продуктов.

Две концепции являются ключевыми для понимания битстрима XC2064. Первая ПЛИС реализована на основе 64 плиток, повторяющихся блоков, которые комбинируют логические блоки и межсоединения. Несмотря на то, что ПЛИС описывается как нечто, имеющее логические блоки, окруженные межсоединениями, она реализована не так. Вторая концепция состоит в том, что битстрим не основан ни на каких абстракциях, он напрямую отображает двумерную упаковку ячеек памяти ПЛИС. Таким образом, битстрим имеет смысл только если вы рассматриваете физическую структуру ПЛИС.

Примечание
Я разобрался в том, как сконфигурирована большая часть битстрима XC2064 (см. примечание 11), и я написал программу для генерации информации CLB из файла битстрима. К сожалению, это один из тех проектов, в которых последние 20% поглощают большую часть времени, и работа до сих пор не закончена. Одна из проблем управление портами ввода-вывода, которые совершенно нерегулярны и имеют свою собственную конфигурацию межсоединений. Другой проблемой является то, что плитки по краям имеют другую конфигурацию. Объединение отдельных узлов трассировки в общий нетлист также требует некоторых громоздких вычислений на графах.

Следующая таблица обобщает смысл каждого бита в плитке, в части битстрима размером 818. Каждая строка в таблице соответствует одному биту в битстриме, и показывает, какая часть ПЛИС управляется этим битом. Пустые строки соответствуют неиспользованным битам.



Первые два столбца таблицы соответствуют переключающей матрице. Есть две переключающие матрицы, обозначенные как #1 (красная) и #2 (зелёная) на рисунке ниже. 8 выходов матрицы #1 пронумерованы от 1 до 8 по часовой стрелке. Переключатель #2 пронумерован так же, но не хватило места для номеров. Например, "#2: 1-3" обозначает, что этот бит соединяет выводы 1 и 3 на переключателе #2. Следующий столбец определяет ND, ненаправленные связи, прямоугольники ниже пурпурных цифр около переключательных матриц. каждый бит ND в таблице управляет соответствующим соединением ND.



Схема межсоединений показывает схему нумерации, которую я сделал для таблицы битстрима. Следующие две колонки описывают то, что я называю PIP соединения, сплошные прямоугольники, на линиях выше. Соединения с выхода Х (коричневые) управляются отдельными битами (Х1, Х2, С3), аналогично для соединения с выхода Y (желтый). Соединения к входу В (светло-пурпурный) устроены по-другому. Только одно из входных соединений может быть активно, и они кодируются множеством бит, с использованием мультиплексоров. Вход С (голубой), D (синий), и A (зелёный) похожи. Остальные столбцы в таблице описывают CLB, обратитесь к документации за подробностями. Биты управляют тактовым сигналом, установкой и линией сброса. Выходы X и Y могут быть выбраны из таблиц (LUT) F и G. Последние два столбца определяют LUT. Есть три входа для LUT F и три входа для LUT G, мультиплексоры управляют входами. Наконец, 8 бит для каждого LUT определяются, задавая выход для определённой комбинации трёх входов.

Я анонсировал свой последний пост в твиттере, так что подписывайтесь на kenshirriff. Также я имею RSS-фид. Благодарю John McMaster, Tim Ansell и Philip Freidin за дискуссии.
Патенты
Различные патенты на ПЛИС дают нам некоторые подробности о чипах: 4870302, 4642487, 4706216, 4758985, и RE34363. Документация на XACT раньше была на сайте Xilinx, но они, похоже, удалили её. Сейчас она здесь. У Джона МакМастера (John McMaster) есть некоторые инструментальные программы для xc2064.
Подробнее..

Сборка Open Source GTA VC и GTA III в Linux

17.02.2021 22:18:17 | Автор: admin
image
Скорее всего нет тут такого человека, который бы не играл в GTA (или хотя бы не слышал о ней). Первая 3D версия серии вышла около 20 лет назад. Это была GTA III. Через год вышла GTA: Vice City. Несмотря на это, в эти игры до сих пор не только играют, но и создают моды. Эти игры портированы на множество платформ, но к сожалению, Linux (до недавнего времени) обошли стороной. Единственный вариант поиграть в Linux был wine. Но недавно все изменилось.

Мне на глаза новость Разработчики закончили реверс-инжиниринг GTA III и Vice City и выпустили порты для разных платформ. А так как я слежу за всем, что касается исходников(утечки, открытие и т.д.), а также люблю собирать софт из исходников разумеется это я не мог проигнорировать. Из новости я понял, что энтузиасты опубликовали исходный код GTA III и GTA: VC. Моей радости не было предела. Я сразу же пошел на github за дополнительной информацией.

Основные улучшения (касательно miami):
Исправлено множество мелких и крупных багов.
Пользовательские файлы (сохранения и настройки), теперь хранятся в корневом каталоге GTA.
Настройки теперь хранятся в файле reVC.ini (в оригинале были в gta_vc.set).
Добавлено меню отладки (доступно по CTRL+M).
Свободная камера (Debug camera). CTRL+B вкл/выкл. При включении камеру можно свободно перемещать во всей карте.
Убраны экраны загрузки между островами.
И некоторые другие исправления/улучшения, включая исправления для других платформ.
Примечание: эти параметры можно настроить в файле core/config.h. Некоторые параметры можно менять прямо в игре (используя отладочное меню), а для остальных потребуется пересборка.

Перейдем к самому интересному, а именно к сборке miami (GTA: VC).
Нам потребуются:
Дистрибутив Linux (Ubuntu, Debian, Mint, etc);
Оригинальные файлы (ассеты) с дистрибутива игры VC (в случае сборки GTA III соответственно потребуются файлы GTA III).
GCC.
Некоторые библиотеки и заголовочные файлы.

В качестве ОС использую Linux Mint 19.3 (основа Ubuntu 18.04 LTS). Установить компиляторы gcc можно так (если не установлены):
sudo apt install build-essential

Установка необходимых библиотек:
sudo apt install libopenal-dev libglew-dev libglfw3-dev libmpg123-dev

Тут остановимся и рассмотрим поподробнее. Я вчера пол часа потратил на поиск решения проблемы компиляции из-за своей невнимательности. На GitHub четко указано, что версия библиотеки glfw должна быть не ниже 3.3, в то время как в репозиториях Ubuntu 18.04 3.2. Поэтому, если у вас дистрибутив построенный на Ubuntu 18.04 (или в репозитории версия библиотеки glfw ниже 3.3) то данную библиотеку необходимо собрать из исходников. Но тут нет ничего сложного. Качаем архив github.com/glfw/glfw/releases/download/3.3.2/glfw-3.3.2.zip
Распаковываем в любую папку. Заходим в распакованную папку и выполняем следующие команды:
mkdir glfw-buildcd glfw-buildcmake -DBUILD_SHARED_LIBS=ON ../make -j2sudo checkinstall

Библиотека нужной версии установлена. Теперь клонируем репозиторий:
git clone --recursive -b miami https://github.com/GTAmodding/re3.git reVC

Это много времени не займет. После клонирования размер папки reVC около 120 МБ(из которых 70 МБ папка .git).
Далее идем в папку reVC и запускаем premake5Linux:
./premake5Linux --with-librw gmake2

Генерация фалов сборки займет пару секунд.
Примечание: Если вы используете архитектуру arm/arm64 то вам нужно собрать утилиту premake5Linux из исходников, а потом запустить с параметром, указанном выше.

Теперь собственно сама сборка. Для сборки доступны следующие конфигурации:
debug_linux-x86-librw_gl3_glfw-oal
debug_linux-amd64-librw_gl3_glfw-oal
debug_linux-arm-librw_gl3_glfw-oal
debug_linux-arm64-librw_gl3_glfw-oal
release_linux-x86-librw_gl3_glfw-oal
release_linux-amd64-librw_gl3_glfw-oal
release_linux-arm-librw_gl3_glfw-oal
release_linux-arm64-librw_gl3_glfw-oal

Думаю, из названий и так все понятно. Главное не перепутайте amd64 и arm64(я вчера ночью сонный недосмотрел, потом не мог понять, почему не происходит сборка).
Переходим в директорию reVC/build и запускаем make с вашей конфигурацией. В случае с debug-версией для amd64 нужно так:
make config=debug_linux-amd64-librw_gl3_glfw-oal

Компиляция происходит достаточно быстро. На моём старом ноутбуке (Asus X55A, Pentium B970 2x2.3 ГГц и 4 Гб памяти) сборка отладочной версии заняла 2 мин. 10 сек.(выходной файл = 30.4 МБ), релизной 4 мин. 44 сек.(размер файла = 43,8 МБ). По своему опыту скажу, что во многих случаях обычно все наоборот.

Если компиляция прошла без ошибок, то в папке reVC/bin/ваша_конфигурация/Debug_или_Release/ будет бинарник reVC.

Копируем куда-нибудь оригинальную папку с GTA: VC (при желании, можно удалить все файлы из корня они нам не понадобятся). Потом в папку с игрой копируем наш бинарник reVC. Теоретически, игру уже можно запускать, и даже играть. Но текст на некоторых пунктах меню будут неправильно отображаться:
image
Чтобы это исправить, копируем папки с заменой из reVC/gamefiles в папку с игрой. Правда, в таком случае, игра будет на английском языке.

Теперь пробежимся по меню отладки:
image

Что бы включить или выключить меню отладки нажмите CTRL+M.
Cam все, что связанно с камерой.
Cheats Различные читы.
Debug Отображение разной отладочной информации, позиции игрока, и т.д.
Debug Render Можно скрывать или отображать различные объекты, транспорт, строения, пешеходов, показывать пути транспорта и пешеходов и т.д.
Game Телепортация в любое место (предварительно нужно поставить метку на карте). Также можно запустить абсолютно любую миссию.
Particle Не знаю что это такое.
Render Некоторые fix'ы, настройка fps, графические настройки
Spawn Спаун транспорта.
Time & Weather Настройка времени и погоды.

Сборка GTA III ничем не отличается, за исключением клонирования репозитория:
git clone --recursive https://github.com/GTAmodding/re3.git


Насчет модов: текстуры, скрипты, модели должны работать. А вот dll/asi, CLEO работать не будут. Некоторые возможности этих модов уже реализованы в re3, некоторые можно настроить в файле config.h.

Ссылки по теме:
github.com/GTAmodding/re3
Инструкция по сборке GTA III
Готовые бинарники reVC для Mac, Linux, Windows
Готовые бинарники re3 для Mac, Linux, Windows
Просмотр репозитория в Visual Studio Code
Lifehack: Если в ссылке на github изменить github.com на github1s.com, то репозиторий можно просматривать в удобном Visual Studio Code.
Подробнее..

Перевод Реверс-инжиниринг GPU Apple M1

05.03.2021 10:06:07 | Автор: admin
image

Новая линейка компьютеров Apple Mac содержит в себе разработанную самой компанией SOC (систему на чипе) под названием M1, имеющую специализированный GPU. Это создаёт проблему для тех, кто участвует в проекте Asahi Linux и хочет запускать на своих машинах Linux: у собственного GPU Apple нет ни открытой документации, ни драйверов в open source. Кто-то предполагает, что он может быть потомком GPU PowerVR, которые использовались в старых iPhone, другие думают, что GPU полностью создан с нуля. Но слухи и домыслы неинтересны, если мы можем сами заглянуть за кулисы!

Несколько недель назад я купила Mac Mini с GPU M1, чтобы изучить набор инструкций и поток команд, а также разобраться в архитектуре GPU на том уровне, который ранее не был публично доступен. В конечном итоге я хотела ускорить разработку драйвера Mesa для этого оборудования. Сегодня я достигла своего первого важного этапа: теперь я достаточно понимаю набор команд, чтобы можно было дизассемблировать простые шейдеры при помощи свободного и open-source тулчейна, выложенного на GitHub.

Процесс декодирования набора команд и потока команд GPU аналогичен тому процессу, который я использовала для реверс-инжиниринга GPU Mali в проекте Panfrost, изначально организованном проектами свободных драйверов Lima, Freedreno и Nouveau. Обычно для реверс-инжиниринга драйвера под Linux или Android пишется небольшая библиотека-обёртка, инъектируемая в тестовое приложение при помощи LD_PRELOAD, подключающей важные системные вызовы типа ioctl и mmap для анализа взаимодействия пользователя и ядра. После вызова передать буфер команд библиотека может выполнить дамп всей общей памяти (расширенной) для её анализа.

В целом тот же процесс подходит и для M1, но в нём есть особенности macOS, которые нужно учитывать. Во-первых, в macOS нет LD_PRELOAD; её аналогом является DYLD_INSERT_LIBRARIES, имеющая дополнительные функции защиты, которые для выполнения нашей задачи можно достаточно легко отключить. Во-вторых, хотя в macOS существуют стандартные системные вызовы Linux/BSD, они не используются для графических драйверов. Вместо них для драйверов ядра и пространства пользователя используется собственный фреймворк Apple IOKit, критической точкой входа которого является IOConnectCallMethod (аналог ioctl). Такие различия достаточно просто устранить, но они добавляют слой дистанцирования от стандартного инструментария Linux.

Более серьёзной проблемой является ориентирование в мире IOKit. Так как Linux имеет лицензию copyleft, (законные) драйверы ядра выложены в open source, то есть интерфейс ioctl открыт, хотя и зависит от производителя. Ядро macOS (XNU) имеет либеральную лицензию, не обладающую такими обязательствами; интерфейс ядра в нём проприетарен и не задокументирован. Даже после обёртывания IOConnectCallMethod пришлось попотеть, чтобы найти три важных вызова: выделение памяти, создание буфера команд и передача буфера команд. Обёртывание вызовов выделения памяти и создания буфера необходимы для отслеживаемой видимой GPU памяти (а именно это мне было интересно исследовать), а обёртывание вызова передачи необходимо для подбора времени дампа памяти.

Преодолев эти препятствия, мы можем наконец перейти к двоичным файлам шейдеров, которые сами по себе являются чёрными ящиками. Однако далее процесс становится стандартным: начинаем с самого простого фрагментного или вычислительного шейдера, вносим небольшое изменение во входящий исходный код и сравниваем получившиеся двоичные файлы. Многократное повторение этого процесса скучное занятие, но позволяет быстро выявить основные структуры, в том числе и опкоды.

Открытия, сделанные в задокументированном процессе изучения при помощи свободного дизассемблера ПО, подтверждают наличие у GPU следующих особенностей:

Во-первых, архитектура скалярна. В отличие от некоторых GPU, которые являются скалярными для 32 бит, но векторизованными для 16 бит, GPU процессора M1 скалярен при всех битовых размерах. Хотя на ресурсах по оптимизации Metal написано, что 16-битная арифметика должна быть значительно быстрее, кроме снижения использования регистров она ведёт к повышению количества потоков (занятости процессора). Исходя из этого, можно предположить, что оборудование является суперскалярным и в нём больше 16-битных, чем 32-битных АЛУ, что позволяет получать большее преимущество от графических шейдеров низкой точности, чем у чипов конкурентов, одновременно значительно снижая сложность работы компилятора.

Во-вторых, архитектура, похоже, выполняет планирование аппаратно это часто встречается среди десктопных GPU, но менее распространено во встроенных системах. Это тоже упрощает компилятор ценой большего количества оборудования. Похоже, команды минимально тратят лишние ресурсы при кодировании, в отличие от других архитектур, вынужденных перегружать команды nop для того, чтобы уместиться в сильно ограниченные наборы инструкций.

В-третьих, поддерживаются различные модификаторы. АЛУ с плавающей запятой могут выполнять модификаторы clamp (насыщенность), операций НЕ и абсолютных значений бесплатно, без лишних затрат распространённая особенность архитектуры шейдеров. Кроме того, большинство (или все?) команды позволяют бесплатно выполнять преобразование типов между 16-битными и 32-битными значениями и для адресата, и для источника, что позволяет компилятору более агрессивно использовать 16-битные операции, не рискуя тратить ресурсы на преобразования. Что касается целочисленных значений, то для некоторых команд есть различные бесплатные побитовые отрицания и сдвиги. Всё это не уникально для оборудования Apple, но заслуживает упоминания.

Наконец, не все команды АЛУ имеют одинаковые тайминги. Команды типа imad (используется для перемножения двух целых чисел и прибавления третьего) по возможности избегаются и вместо них используются многократные команды целочисленного сложения iadd. Это тоже позволяет предположить наличие суперскалярной архитектуры; оборудование с программным планированием, с которым я взаимодействую на своей повседневной работе, не может использовать различия в длине конвейеров, непреднамеренно замедляя простые команды для соответствия скорости сложных.

По своему предыдущему опыту работы с GPU я ожидала найти какой-нибудь ужас в наборе инструкций, повышающий сложность компилятора. Хотя написанное выше охватывает только небольшую площадь поверхности набора инструкций, пока всё кажется разумным и логичным. Отсутствуют сложные трюки для оптимизации, однако отказ от трюков позволяет создать простой, эффективный дизайн, выполняющий одну задачу, и справляющийся с ней хорошо. Возможно, проектировщики оборудования Apple поняли, что простота превыше всего.

Часть вторая


В первой части я начала изучать GPU Apple M1, надеясь разработать бесплатный драйвер с открытым исходным кодом. Теперь мне удалось достичь важного этапа: отрисовать треугольник с помощью собственного кода в open source. Вершинные и фрагментные шейдеры написаны вручную на машинном коде, а взаимодействие с оборудованием выполняется с помощью драйвера ядра IOKit, подобно тому, как это происходило в системном драйвере пользовательского пространства Metal.


Треугольник, отрендеренный на M1 кодом в open source

Основная часть нового кода отвечает за построение различных буферов команд и дескрипторов, находящихся в общей памяти и используемых для управления поведением GPU. Любое состояние, доступное из Metal, соответствует битам в этих буферах, поэтому следующей важной задачей будет их анализ. Пока меня больше интересует не содержимое, а связи между ними. В частности, структуры, содержащие указатели друг на друга, иногда встроенные на несколько слоёв вглубь. Процесс подготовки треугольника в моём проекте позволяет взглянуть с высоты птичьего полёта на то, как взаимодействуют все эти отдельные части в памяти.

Например, предоставляемые приложением данные вершин хранятся в собственных буферах. Внутренняя таблица в ещё одном буфере указывает на эти буферы вершин. Эта внутренняя таблица передаётся непосредственно на вход вершинного шейдера, заданного в другом буфере. На это описание вершинного шейдера, в том числе и на адрес кода в исполняемой памяти, указывает ещё один буфер, на который сам ссылается основной буфер команд, на который ссылается вызов IOKit на передачу буфера команд. Ого!

Другими словами, код демо пока не предназначен для демонстрации понимания мелких подробностей буферов команд, а скорее служит для демонстрации того, что ничто не упущено. Так как виртуальные адреса GPU меняются при каждом запуске, демо подтверждает, что идентифицированы все необходимые указатели и их можно свободно перемещать в памяти с помощью нашего собственного (тривиального) распределителя. Так как в macOS присутствует некоторая магия, связанная с распределением памяти и буферов команд, наличие этого работающего кода позволяет нам спокойно двигаться дальше.

Я использовала поэтапный процесс подготовки. Поскольку моя обёртка IOKit располагается в то же адресном пространстве, что и приложение Metal, обёртка способна модифицировать буферы команд непосредственно перед передачей в GPU. В качестве первого hello world я задала кодирование в памяти цвета очистки render target и показала, что могу изменять этот цвет. Аналогично, узнав о наборе инструкций для вывода дизассемблера, я заменила шейдеры написанными самостоятельно эквивалентами и убедилась, что могу исполнять код в GPU, доказать, что написала машинный код. Однако мы не обязаны останавливаться на этих нодах листьев системы; изменив код шейдера, я попыталась загрузить код шейдера в другую часть исполняемого буфера, модифицировав указатель командного буфера на код, чтобы компенсировать это. После этого я смогу попробовать самостоятельно загружать команды для шейдера. Проводя разработку таким образом, я смогу создать все необходимые структуры, тестируя каждую из них по отдельности.

Несмотря на затруднения, этот процесс оказался гораздо более успешным, чем попытка перейти непосредственно к построению буферов, например, при помощи воспроизведения. Я использовала этот альтернативный способ несколько лет назад для создания драйверов Mali, однако у него есть серьёзный недостаток чудовищно сложная отладка. Если в пяти сотнях строк магических чисел присутствует хотя бы одна опечатка, то никакой обратной связи, кроме ошибки GPU, вы не получите. Однако при последовательной работе можно выявлять причины ошибок и немедленно их исправлять, что оказывается быстрее и удобнее.

Однако затруднения всё-таки были! Моё временное ликование, вызванное возможностью изменения цветов очистки, пропало, когда я попыталась выделить буфер для цветов. Несмотря на то, что GPU кодировал те же биты, что и раньше, ему не удавалось корректно выполнить очистку. Думая, что где-то ошиблась в способе модификации указателя, я попыталась поместить цвет в неиспользованную часть памяти, уже созданную драйвером Metal, и это сработало. Содержимое осталось тем же, как и способ модификации указателей, но GPU почему-то не нравилось моё распределение памяти. Я думала, что как-то неправильно распределяю память, но использованные для вызова распределения памяти IOKit были побитово идентичны тем, что использовались Metal, что подтверждалось wrap. Моей последней отчаянной попыткой стала проверка, должна ли память GPU отображаться явным образом через какой-то побочный канал, например, через системный вызов mmap. У IOKit есть устройство-независимый вызов memory map, но никакие трассировки не позволили обнаружить свидетельств отображений через сторонние системные вызовы.

Появилась проблема. Утомившись от потраченного на устранение невозможного бага времени, я задалась вопросом, нет ли чего-то магического не в системном вызове, а в самой памяти GPU. Глупая теория, потому что если это так, то появляется серьёзная проблема курицы и яйца: если распределение памяти GPU должно быть одобрено другим распределением GPU, то кто одобряет первое распределение?

Чувствуя себя глупо и ощущая отчаяние, я решила протестировать теорию, вставив вызов распределения памяти посередине потока выполнения приложения, чтобы каждое последующее распределение находилось по другому адресу. Выполнив дамп памяти GPU до и после этого изменения и изучив их отличия, я обнаружила свой первый кошмар: наличие вспомогательного буфера в памяти GPU, отслеживающего все требуемые распределения. В частности, я заметила, что значения в этом буфере увеличиваются на единицу с предсказуемым смещением (через каждые 0x40 байт), и это намекает о содержании в буфере массива дескрипторов распределений. И в самом деле, эти значения точно соответствовали дескрипторам, возвращаемым из ядра при вызовах распределения памяти GPU.

Закрыв глаза на очевидные проблемы этой теории, я всё равно её протестировала, модифицировав эту таблицу и добавив в конец дескриптор моего нового распределения, а также изменив структуру данных заголовка так, чтобы увеличить количество элементов на единицу. Это не помогло. Несмотря на разочарование, это всё равно не позволяло полностью отказаться от теории. На самом деле, я заметила в элементах таблицы нечто любопытное: не все они соответствовали действительным дескрипторам. Действительными были все элементы, кроме последнего. Диспетчеры ядра имеют индексацию с 1, однако в каждом дампе памяти последний дескриптор всегда был 0, несуществующим. Вероятно, он используется как контрольное значение, аналогично NULL-terminated string в C. Однако при таком объяснении возникает вопрос: почему? Если заголовок уже содержит количество элементов, то контрольное значение избыточно.

Я продолжила разбираться дальше. Вместо добавления ещё одного элемента с моим дескриптором, я скопировал последний элемент n в дополнительный элемент n + 1 и переписала элемент n (теперь второй с конца) новым дескриптором.

Внезапно отобразился нужный мне цвет очистки.

Итак, загадка решена? Код заработал, так что в каком-то смысле да. Но едва ли это объяснение может нас удовлетворить; на каждом этапе непонятное решение будет создавать новые вопросы. Проблему курицы и яйца решить проще всего: эта таблица отображений вместе с корневым буфером команд распределяется специальным селектором IOKit, не зависящим от распределения общего буфера, а дескриптор таблицы отображений передаётся с селектором буфера команд. Более того, идея передачи требуемых дескрипторов вместе с передачей буфера команд не уникальна; подобный механизм используется в стандартных драйверах Linux. Тем не менее, обоснование для использования 64-байтных элементов таблицы в общей памяти вместо простого массива на стороне CPU остаётся совершенно непонятной.

Но даже оставив позади проблемы с распределением памяти, двигаться вперёд пришлось не без труда. Однако благодаря терпению мне удалось полностью воссоздать память GPU параллельно с Metal, используя проприетарное пространство пользователя только для инициализации устройства. Наконец, остался последний прыжок веры отказ от синхронизации с IOKit, после чего мне удалось получить свой первый треугольник.

Для второй части статьи пришлось внести в код изменения объёмом примерно 1700 строк кода, сам код выложен на GitHub. Я собрала простое демо, анимирующее на экране треугольник с помощью GPU. Интеграция с оконной системой на этом этапе практически отсутствует: требуется XQuartz, а в ПО с наивным скалярным кодом возникает detiling буфера кадров. Тем не менее, скорости CPU M1 с лихвой хватает, чтобы с этим справиться.

Теперь, когда готова начальная загрузка каждой части драйвера пространства пользователя, двигаясь дальше, мы сможем по отдельности работать над набором инструкций и буферами команд. Можно будет разобрать мелкие подробности и побитово преобразовать код из сотен необъяснимых магических констант в настоящий драйвер.
Подробнее..

NTFS Reparse Points

23.11.2020 12:04:01 | Автор: admin
Привет, Хабр. Представляю вам гайд по NTFS Reparse points (далее RP), точкам повторной обработки. Это статья для тех, кто только начинает изучать тонкости разработки ядра Windows и его окружения. В начале объясню теорию с примерами, потом дам интересную задачку.



RP является одной из ключевых фич файловой системы NTFS, которая бывает полезна в решении задач резервного копирования и восстановления. Как следствие, Acronis очень заинтересован в данной технологии.

Полезные ссылки


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


Теория


Точка повторной обработки или Reparse Point (RP) это объект заданного размера с данными, определенными программистом, и уникальным тегом. Пользовательский объект представлен структурой REPARSE_GUID_DATA_BUFFER.

typedef struct _REPARSE_GUID_DATA_BUFFER {  ULONG  ReparseTag;  USHORT ReparseDataLength;  USHORT Reserved;  GUID   ReparseGuid;  struct {    UCHAR DataBuffer[1];  } GenericReparseBuffer;} REPARSE_GUID_DATA_BUFFER, *PREPARSE_GUID_DATA_BUFFER;

Размер блока данных RP доходит до 16 килобайт.
ReparseTag 32-х разрядный тэг.
ReparseDataLength размер данных
DataBuffer указатель на пользовательские данные

RP, предоставленные Microsoft, могут быть представлены структурой REPARSE_DATA_BUFFER. Не следует её использовать для самописных RP.

Рассмотрим формат тега:



M Бит Майкрософт; Если этот бит установлен, значит тег разработан компанией Майкрософт.
L Бит задержки; Если этот бит установлен, значит данные, на которые ссылается RP расположены на носителе с низкой скоростью реакции и большой задержкой выдачи данных.
R Зарезервированный бит;
N Бит замены имени; Если этот бит установлен, значит файл или каталог представляет другую именованную сущность в файловой системе.
Значение тега Запрашивается у Microsoft;

Каждый раз, когда приложение создает или удаляет RP, NTFS обновляет файл метаданных \\$Extend\\$Reparse. Именно в этом файле хранятся записи о RP. Такое централизованное хранение позволяет сортировать и эффективно искать необходимый объект любому приложению.

Через механизм RP в Windows реализована поддержка символьных ссылок, системы удаленного хранилища, а также точек монтирования томов и каталогов.

Жесткие ссылки в системе Windows не являются фактическим объектом, а просто синонимом на один и тот же файл на диске. Это не отдельные объекты файловой системы, а просто еще одно наименование файла в таблице расположения файлов. Этим жёсткие ссылки отличаются от символьных.

Для использования технологии RP нам нужно написать:

  • Небольшое приложение с привилегиями SE_BACKUP_NAME или SE_RESTORE_NAME, которое будет создавать файл содержащий структуру RP, устанавливать обязательное поле ReparseTag и заполнять DataBuffer
  • Драйвер режима ядра, который будет читать данные буфера и обрабатывать обращения к этому файлу.

Создадим свой файл с RP


1. Получаем необходимые привилегии

void GetPrivilege(LPCTSTR priv){HANDLE hToken;TOKEN_PRIVILEGES tp;OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES, &hToken);LookupPrivilegeValue(NULL, priv, &tp.Privileges[0].Luid);tp.PrivilegeCount = 1;tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;AdjustTokenPrivileges(hToken, FALSE, &tp,sizeof(TOKEN_PRIVILEGES), NULL, NULL);CloseHandle(hToken);}GetPrivilege(SE_BACKUP_NAME);GetPrivilege(SE_RESTORE_NAME);GetPrivilege(SE_CREATE_SYMBOLIC_LINK_NAME);

2. Подготавливаем структуру REPARSE_GUID_DATA_BUFFER. Для примера в данные RP мы напишем простую строку My reparse data.

TCHAR data[] = _T("My reparse data");BYTE reparseBuffer[sizeof(REPARSE_GUID_DATA_BUFFER) + sizeof(data)];PREPARSE_GUID_DATA_BUFFER rd = (PREPARSE_GUID_DATA_BUFFER) reparseBuffer;ZeroMemory(reparseBuffer, sizeof(REPARSE_GUID_DATA_BUFFER) + sizeof(data));// {07A869CB-F647-451F-840D-964A3AF8C0B6}static const GUID my_guid = { 0x7a869cb, 0xf647, 0x451f, { 0x84, 0xd, 0x96, 0x4a, 0x3a, 0xf8, 0xc0, 0xb6 }};rd->ReparseTag = 0xFF00;rd->ReparseDataLength = sizeof(data);rd->Reserved = 0;rd->ReparseGuid = my_guid;memcpy(rd->GenericReparseBuffer.DataBuffer, &data, sizeof(data));

3. Создаем файл.

LPCTSTR name = _T("TestReparseFile");_tprintf(_T("Creating empty file\n"));HANDLE hFile = CreateFile(name,GENERIC_READ | GENERIC_WRITE, 0, NULL,CREATE_NEW, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,NULL);if (INVALID_HANDLE_VALUE == hFile){_tprintf(_T("Failed to create file\n"));return -1;}

4. Заполняем файл нашей структурой, используя метод DeviceIoControl с параметром FSCTL_SET_REPARSE_POINT.

_tprintf(_T("Creating reparse\n"));if (!DeviceIoControl(hFile, FSCTL_SET_REPARSE_POINT, rd, rd->ReparseDataLength + REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, NULL, 0, &dwLen, NULL)){CloseHandle(hFile);DeleteFile(name);_tprintf(_T("Failed to create reparse\n"));return -1;}CloseHandle(hFile);

Полный код этого приложения можно найти тут.

После сборки и запуска у нас образуется файл. Утилита fsutil поможет посмотреть на созданный нами файл и убедиться, что наши данные на месте.



Обработка RP


Пришло время взглянуть на этот файл со стороны ядреного пространства. Я не буду углубляться в детали устройства мини-фильтра драйвера. Хорошее объяснение есть официальной документации от Microsoft с примерами кода. А мы посмотрим на post callback метод.

Нужно перезапросить IRP c параметром FILE_OPEN_REPARSE_POINT. Для этого мы вызовем FltReissueSynchronousIo. Данная функция повторит запрос, но уже с обновленными Create.Options.

Внутри структуры PFLT_CALLBACK_DATA есть поле TagData. Если вызвать метод FltFsControlFile с кодом FSCTL_GET_REPARSE_POINT, то получим наш буфер с данными.

// todo конечно стоит проверить наш ли это тэг, а не только его наличиеif (Data->TagData != NULL) {if ((Data->Iopb->Parameters.Create.Options & FILE_OPEN_REPARSE_POINT) != FILE_OPEN_REPARSE_POINT)    {      Data->Iopb->Parameters.Create.Options |= FILE_OPEN_REPARSE_POINT;      FltSetCallbackDataDirty(Data);      FltReissueSynchronousIo(FltObjects->Instance, Data);    }    status = FltFsControlFile(      FltObjects->Instance,       FltObjects->FileObject,       FSCTL_GET_REPARSE_POINT,       NULL,       0,       reparseData,      reparseDataLength,      NULL    );}



Далее можно использовать эти данные в зависимости от задачи. Можно вновь перезапросить IRP. Или инициировать совершенно новый запрос. К примеру, в проекте LazyCopy в данных RP хранится путь до оригинального файла. Автор не начинает копирование в момент открытия файла, а лишь пересохраняет данные из RP в stream context данного файла. Данные начинают копироваться в момент чтения или записи файла. Вот наиболее важные моменты его проекта:

// Operations.c - PostCreateOperationCallbackNT_IF_FAIL_LEAVE(LcGetReparsePointData(FltObjects, &fileSize, &remotePath, &useCustomHandler));NT_IF_FAIL_LEAVE(LcFindOrCreateStreamContext(Data, TRUE, &fileSize, &remotePath, useCustomHandler, &streamContext, &contextCreated));// Operations.c - PreReadWriteOperationCallbackstatus = LcGetStreamContext(Data, &context);NT_IF_FAIL_LEAVE(LcGetFileLock(&nameInfo->Name, &fileLockEvent));NT_IF_FAIL_LEAVE(LcFetchRemoteFile(FltObjects, &context->RemoteFilePath, &nameInfo->Name, context->UseCustomHandler, &bytesFetched));NT_IF_FAIL_LEAVE(LcUntagFile(FltObjects, &nameInfo->Name));NT_IF_FAIL_LEAVE(FltDeleteStreamContext(FltObjects->Instance, FltObjects->FileObject, NULL));

На самом деле, RP имеют широкий спектр применения, и открывают множество возможностей для решения различных задач. Один из них разберем на решении следующей задачи.

Задачка


Игрушка Half-life умеет запускаться в двух режимах: software mode и hardware mode, которые отличаются способом отрисовки графики в игре. Немного поковыряв игрушку в IDA Pro, можно заметить, что режимы различаются загружаемой методом LoadLibrary библиотекой: sw.dll или hw.dll.



В зависимости от входных аргументов (например -soft) выбирается та или иная строка и подбрасывается в вызов функции LoadLibrary.



Суть задачи заключается в том, чтобы запретить игрушке загружаться в software mode, да так, чтобы пользователь этого не заметил. В идеале чтобы пользователь даже не понял, что вне зависимости от его выбора, он загружается в hardware mode.

Конечно, можно было бы пропатчить исполняемый файл, или заменить dll файл, да даже просто скопировать hw.dll и переименовать копию в sw.dll, но мы не ищем лёгких путей. Плюс, если игру будут обновлять или переустанавливать, эффект пропадет.

Решение


Я предлагаю следующее решение: напишем небольшой мини-фильтр драйвер. Он будет работать постоянно и на него не будут влиять переустановка и обновление игры. Зарегистрируем драйвер на операцию IRP_MJ_CREATE, ведь каждый раз, когда исполняемый файл вызывает LoadLibrary, он по сути открывает файл библиотеки. Как только мы заметим, что процесс игры пытается открыть библиотеку sw.dll, мы вернем статус STATUS_REPARSE и попросим повторить запрос, но уже на открытие hw.dll. Результат: открылась нужная нам библиотека, хоть user space и просил у нас другую.

Для начала, нам надо понять, какой процесс пытается открыть библиотеку, ведь провернуть наш трюк нам необходимо только для процесса игры. Для этого прямо в DriverEntry нам нужно будет позвать PsSetCreateProcessNotifyRoutine и зарегистрировать метод, который будет вызываться каждый раз как только в системе будет появляться новый процесс.

NT_IF_FAIL_LEAVE(PsSetCreateProcessNotifyRoutine(IMCreateProcessNotifyRoutine, FALSE));

В этом методе мы должны получить имя запускаемого файла. Для этого можно воспользоваться ZwQueryInformationProcess.

NT_IF_FAIL_LEAVE(PsLookupProcessByProcessId(ProcessId, &eProcess));NT_IF_FAIL_LEAVE(ObOpenObjectByPointer(eProcess, OBJ_KERNEL_HANDLE, NULL, 0, 0, KernelMode, &hProcess));NT_IF_FAIL_LEAVE(ZwQueryInformationProcess(hProcess,                                               ProcessImageFileName,                                               buffer,                                               returnedLength,                                               &returnedLength));

Если он совпадает с искомым, в нашем случае hl.exe, то необходимо запомнить его PID.

target = &Globals.TargetProcessInfo[i];if (RtlCompareUnicodeString(&processNameInfo->Name, &target->TargetName, TRUE) == 0){      target->NameInfo = processNameInfo;      target->isActive = TRUE;      target->ProcessId = ProcessId;      LOG(("[IM] Found process creation: %wZ\n", &processNameInfo->Name));}

Итак, теперь у нас в глобальном объекте сохранен PID процесса нашей игры. Можно переходить к pre create callback. Там мы должны получить имя открываемого файла. В этом нам поможет FltGetFileNameInformation. Данную функцию нельзя вызывать на DPC уровне прерываний (читай подробнее про IRQL), однако, мы собираемся делать вызов исключительно на pre create, что гарантирует нам уровень не выше APC.

status = FltGetFileNameInformation(Data, FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_FILESYSTEM_ONLY | FLT_FILE_NAME_ALLOW_QUERY_ON_REPARSE, &fileNameInfo);

Далее все просто, если наше имя sw.dll, то нужно заменить его в FileObject на hw.dll. И вернуть статус STATUS_REPARSE.

// may be it is swif (RtlCompareUnicodeString(&FileNameInfo->Name, &strSw, TRUE) == 0){// concatNT_IF_FAIL_LEAVE(IMConcatStrings(&replacement, &FileNameInfo->ParentDir, &strHw));// then need to changeNT_IF_FAIL_LEAVE(IoReplaceFileObjectName(FileObject, replacement.Buffer, replacement.Length));}Data->IoStatus.Status = STATUS_REPARSE;Data->IoStatus.Information = IO_REPARSE;return FLT_PREOP_COMPLETE;

Безусловно, реализация проекта целиком несколько более комплексная, но основные моменты я постарался раскрыть. Весь проект с подробностями тут.

Тестируем


Чтобы упростить наши тестовые запуски, вместо игры будем запускать небольшое приложение и библиотеки следующего содержания:

// testapp.exe#include "TestHeader.h"int main(){TestFunction();return 0;}// testdll0.dll#include "../include/TestHeader.h"#include <iostream>// This is an example of an exported function.int TestFunction(){std::cout << "hello from test dll 0" << std::endl;return 0;}// testdll1.dll#include "../include/TestHeader.h"#include <iostream>// This is an example of an exported function.int TestFunction(){std::cout << "hello from test dll 1" << std::endl;return 0;}

Соберем testapp.exe с testdll0.dll, и копируем их на виртуалку (а именно там мы планируем запускать), а также подготовим testdll1.dll. Задачей нашего драйвера будет заменить testdll0 на testdll1. Мы поймем что у нас все получилось, если в консоли мы увидим сообщение hello from test dll 1, вместо hello from test dll 0. Запустим без драйвера, чтобы убедиться, что наше тестовое приложение работает корректно:



А теперь установим и запустим драйвер:



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



Плюс в приложении, написанном для драйвера, мы видим логи, которые говорят, что мы действительно поймали наш запрос на открытие и заменили один файл на другой. Тест удался, пришло время проверить решение на самой игре. Наслаждаемся :)



Надеюсь, было полезно и интересно. Пишите ваши вопросы в комментариях, постараюсь ответить.
Подробнее..

Модифицируем Last Epoch От dnSpy до Ghidra

09.08.2020 16:21:03 | Автор: admin

Last Epoch это однопользовательская ARPG на Unity и C#. В игре присутствует система крафта игрок находит модификаторы, которые затем применяет к экипировке. С каждым модификатором накапливается "нестабильность", которая увеличивает шанс поломки предмета


Я преследовал две цели:


  • Убрать "поломку" предмета в результате применения модификаторов
  • Не расходовать модификаторы при крафте

cut


Вот так выглядит окно крафта в игре
Окно крафта в Last Epoch


Часть первая, где мы редактируем .NET код без регистрации и смс


Для начала я опишу процесс модификации старой версии игры (0.7.8)


После компиляции C# превращается в IL (Intermediate Language) код. IL-код напоминает ассемблер высокого уровня абстракции и замечательно декомпилируется. В Unity проектах IL-код игры как правило находится в <GameFolder>/Managed/Assembly-CSharp.dll


Для редактирования IL-кода мы будем использовать dnSpy лучший из всех инструментов подобного рода для .NET, что я встречал. dnSpy делает работу со скомпилированными .NET приложениями настолько легкой, что ее можно спутать с разработкой в обычной IDE


Ищем логику крафта


Итак, запускаем dnSpy и открываем Assembly-CSharp.dll


dnSpy с открытым Assembly-CSharp.dll


Мы сразу видим невероятно длинный список классов. В данном случае целью является модификация системы крафта, поэтому поступаю просто ищу классы, в названии которых встречается Craft и просматриваю их логику по диагонали


После непродолжительных поисков мы нашли искомое класс CraftingSlotManager
dnSpy x CraftingSlotManager


А именно метод Forge() в данном классе
ndSpy x CraftingSlotManager.Forge()


Полная версия кода метода для желающих
// CraftingSlotManager// Token: 0x06002552 RID: 9554 RVA: 0x0015E958 File Offset: 0x0015CB58public void Forge(){    if (!this.forging)    {        this.forging = true;        base.StartCoroutine(this.ForgeBlocker(10));        bool flag = false;        int num = -1;        if (this.main.HasContent())        {            int num2 = 0;            int num3 = 0;            if (this.debugNoFracture)            {                num3 = -10;            }            float num4 = 1f;            int num5 = -1;            bool flag2 = false;            ItemData data = this.main.GetContent()[0].data;            ItemData itemData = null;            if (this.support.HasContent())            {                itemData = this.support.GetContent()[0].data;                num5 = (int)itemData.subType;                if (itemData.subType == 0)                {                    num3--;                    flag2 = true;                }                else if (itemData.subType == 1)                {                    num4 = UnityEngine.Random.Range(0.4f, 1f);                    flag2 = true;                }            }            if (this.appliedAffixID >= 0)            {                Debug.Log("applied ID: " + this.appliedAffixID.ToString());                if (this.forgeButtonText.text == "Forge")                {                    if (data.AddAffixTier(this.appliedAffixID, Mathf.RoundToInt((float)(5 + num2) * num4), num3))                    {                        num = this.appliedAffixID;                        flag = true;                    }                    GlobalDataTracker.instance.CheckForShard(this.appliedAffixID);                    if (flag2)                    {                        this.support.Clear();                    }                    if (!GlobalDataTracker.instance.CheckForShard(this.appliedAffixID))                    {                        this.DeselectAffixID();                    }                }            }            else if (this.modifier.HasContent())            {                Debug.Log("modifier lets go");                ItemData data2 = this.modifier.GetContent()[0].data;                if (data2.itemType == 102)                {                    if (data2.subType == 0)                    {                        Debug.Log("shatter it");                        Notifications.CraftingOutcome(data.Shatter());                        if (num5 == 0)                        {                            flag2 = false;                        }                        this.main.Clear();                        flag = true;                        this.ResetAffixList();                    }                    else if (data2.subType == 1)                    {                        Debug.Log("refine it");                        if (data.AddInstability(Mathf.RoundToInt((float)(2 + num2) * num4), num3, 0))                        {                            data.ReRollAffixRolls();                        }                        flag = true;                    }                    else if (data2.subType == 2 && data.affixes.Count > 0)                    {                        Debug.Log("remove it");                        if (data.AddInstability(Mathf.RoundToInt((float)(2 + num2) * num4), num3, 0))                        {                            ItemAffix affixToRemove = data.affixes[UnityEngine.Random.Range(0, data.affixes.Count)];                            data.RemoveAffix(affixToRemove);                        }                        flag = true;                    }                    else if (data2.subType == 3 && data.affixes.Count > 0)                    {                        Debug.Log("cleanse it");                        List<ItemAffix> list = new List<ItemAffix>();                        foreach (ItemAffix item in data.affixes)                        {                            list.Add(item);                        }                        foreach (ItemAffix affixToRemove2 in list)                        {                            data.RemoveAffix(affixToRemove2);                        }                        if (num5 == 0)                        {                            flag2 = false;                        }                        data.SetInstability((int)((Mathf.Clamp(UnityEngine.Random.Range(5f, 15f), 0f, (float)data.instability) + (float)num2) * num4));                        flag = true;                    }                    else if (data2.subType == 4 && data.sockets == 0)                    {                        Debug.Log("socket it");                        data.AddSocket(1);                        data.SetInstability((int)((Mathf.Clamp(UnityEngine.Random.Range(5f, 15f), 0f, (float)data.instability) + (float)num2) * num4));                        flag = true;                    }                }            }            if (flag)            {                UISounds.playSound(UISounds.UISoundLabel.CraftingSuccess);                if (this.modifier.HasContent())                {                    ItemData data3 = this.modifier.GetContent()[0].data;                    this.modifier.Clear();                    if (num >= 0 && GlobalDataTracker.instance.CheckForShard(num))                    {                        this.PopShardToModifierSlot(num);                    }                    else if (data3.itemType == 102)                    {                        foreach (SingleSubTypeContainer singleSubTypeContainer in ItemContainersManager.instance.materials.Containers)                        {                            if (singleSubTypeContainer.CanAddItemType((int)data3.itemType) && singleSubTypeContainer.allowedSubID == (int)data3.subType && singleSubTypeContainer.HasContent())                            {                                singleSubTypeContainer.MoveItemTo(singleSubTypeContainer.GetContent()[0], 1, this.modifier, new IntVector2?(IntVector2.Zero), Context.SILENT);                                break;                            }                        }                    }                    if (num >= 0 && this.prefixTierVFXObjects.Length != 0 && this.suffixTierVFXObjects.Length != 0)                    {                        ItemData itemData2 = null;                        if (this.main.HasContent())                        {                            itemData2 = this.main.GetContent()[0].data;                        }                        if (itemData2 != null && this.main.HasContent())                        {                            List<ItemAffix> list2 = new List<ItemAffix>();                            List<ItemAffix> list3 = new List<ItemAffix>();                            foreach (ItemAffix itemAffix in itemData2.affixes)                            {                                if (itemAffix.affixType == AffixList.AffixType.PREFIX)                                {                                    list2.Add(itemAffix);                                }                                else                                {                                    list3.Add(itemAffix);                                }                            }                            for (int i = 0; i < list2.Count; i++)                            {                                if ((int)list2[i].affixId == num && this.prefixTierVFXObjects[i])                                {                                    this.prefixTierVFXObjects[i].SetActive(true);                                }                            }                            for (int j = 0; j < list3.Count; j++)                            {                                if ((int)list3[j].affixId == num && this.suffixTierVFXObjects[j])                                {                                    this.suffixTierVFXObjects[j].SetActive(true);                                }                            }                        }                    }                }                if (!flag2)                {                    goto IL_6B3;                }                this.support.Clear();                using (List<SingleSubTypeContainer>.Enumerator enumerator2 = ItemContainersManager.instance.materials.Containers.GetEnumerator())                {                    while (enumerator2.MoveNext())                    {                        SingleSubTypeContainer singleSubTypeContainer2 = enumerator2.Current;                        if (singleSubTypeContainer2.CanAddItemType((int)itemData.itemType) && singleSubTypeContainer2.allowedSubID == (int)itemData.subType && singleSubTypeContainer2.HasContent())                        {                            singleSubTypeContainer2.MoveItemTo(singleSubTypeContainer2.GetContent()[0], 1, this.support, new IntVector2?(IntVector2.Zero), Context.SILENT);                            break;                        }                    }                    goto IL_6B3;                }            }            this.modifier.Clear();            this.support.Clear();        }        IL_6B3:        if (!flag)        {            UISounds.playSound(UISounds.UISoundLabel.CraftingFailure);        }        this.slamVFX.SetActive(true);        this.UpdateItemInfo();        this.UpdateFractureChanceDisplay();        this.UpdateForgeButton();        ShardCountText.UpdateAll();    }}

С моей точки зрения данный код выглядит не так уж и плохо. Даже и не скажешь сразу, что это декомпелированная версия (выдают только названия переменных в духе num1, num2...). Есть даже логирование, которое позволяет легче понять назначение веток


После изучения кода я выяснил примерный механизм работы у нас есть несколько CraftingSlot'ов, в которые мы помещаем предмет крафта и модификаторы. CraftingSlotManager соответсвенно управляет взаимодействием этих слотов и в целом отвечает за логику крафта


Отключаем расходование ресурсов при крафте


Нас интересуют две переменные: this.modifier и this.support
Это слоты для модификаторов, которые используются во время крафта


Как оказалось уничтожение модификаторов происходит в процессе следующих вызовов:


this.modifier.Clear();this.support.Clear();

Хотя для меня не очевидно, почему очистка слота обязательно должна удалять его содержимое (а не, например, возвращать обратно в инвентарь) это вопрос наименования методов и переменных и к нашему исследованию отношения не имеет


Удаляем все вызовы this.modifier.Clear(); и this.support.Clear(); из кода функции и радуемся


Процесс редактирования в dnSpy просто фантастика просто поправили код и сохранили в .dll все изменения будут скомпилированы автоматически
dnSpy edit method


Убираем поломку предмета в процессе крафта


В игре поломка предмета в процессе крафта называется Fracture, поэтому мне сразу бросился в глаза данный кусок кода
dnSpy x CraftingSlotManager.Forge()


И действительно модификация вида int num3 = -10; полностью отключает поломку спасибо разработчикам за оставленный дебаг флаг :)


Часть вторая, где мы испытываем боль и страдания


Начиная с версии 0.7.9 разработчики начали использовать IL2CPP для того, чтобы собирать проект напрямую в нативный бинарник под нужную платформу. В результате у нас больше нет IL-кода, а есть лишь хардкорный ассемблер но кого это останавливало?


Дисклеймер

Автор статьи не является реверс-инженером, а имеет за спиной только остатки университетского курса Ассемблера и один No-CD, сделанный в OllyDbg 10 лет назад. Вполне возможно, что какие-то вещи были сделаны неправильно или их можно было сделать намного лучше и проще


Ищем иголку в стоге Гидры


Итак, из папки игры пропали все старые .dll-ки и мы взамен получили один огромный GameAssembly.dll весом в 55 мегабайт. Наши цели не изменились, но теперь все будет намного сложнее.


Первым делом загружаем dll-ку в Ghidra'у и соглашаемся на все виды анализа, которые она предлагает (анализ занимает довольно много времени и в дальнейшем я останавливал его на стадии Analyze Address Table)


Ghidra


На этом я, как начинающий реверсер, и закончил бы, ведь каким образом тут искать нужный нам код совершенно мне непонятно


К счастью погуглив на тему IL2CPP я нашел утилиту Il2CppDumper, которая позволяет частично восстановить информацию на основе метадата-файла (который обнаружился по пути <GameFolder>/il2cpp_data/Metadata/global-metadata.dat). Не знаю является ли данный файл необходимостью или разработчики просто забыли убрать его, но он сильно облегчил нашу задачу


Скармливаем утилите наши файлы dll и метадаты и получаем набор восстановленных данных
Il2CppDumper output


В папке DummyDll находятся восстановленные dll-ки с частично восстановленным IL-кодом. Загружаем восстановленный Assembly-CSharp.dll в dnSpy и идем в наш любимый CraftingSlotManager


dnSpy (restored) x CraftingSlotManager


Ну что же, кода у нас больше нет, зато у нас есть адрес! В аннотации


Address(RVA = "0x5B9FC0", Offset = "0x5B89C0", VA = "0x1805B9FC0")

Нам нужно значение VA это оффест, по которому мы найдем нашу функцию в Гидре


Ghidra forge offset


Теперь мы хотя бы нашли начало нашей функции, что уже неплохо


Можно ли сделать лучше? Вспоминаем, что Il2CppDumper генерирует данные, котрые можно импортировать в Гидру копируем скрипт в папку скриптов Гидры, запускаем ghidra.py и скармливаем ему script.json, сгенерированный из нашей метадаты. Теперь у всех функций, которые были объявлены в исходном коде, появились имена


Отключаем расходование ресурсов при крафте


Мы уже знаем, что нам достаточно убрать вызовы this.modifier.Clear(); и this.support.Clear();. Осталось найти их в коде. К счастью восстановленные имена функций помогают решить эту задачу довольно просто


Ghidra


Ломать не строить. Чтобы убрать вызов функции нам достаточно заменить все байты, участвующие в CALL на NOP


Разбиваем команду на отдельные байты (выделив ее и нажав C, или Clear Code Bytes), затем в бинарном представлении просто впечатываем 90 пять раз. Готово!
Ghidra


Такую операцию повторяем для всех вызвов OneSlotItemContainer$$Clear() из нашей функции Forge() (На самом деле это нужно делать не для всех вызовов, потому что в коде есть один вызов this.main.Clear(); Но мне было слишком лениво выискивать конкретное исключение в ассемблерной каше, поэтому я убрал все вызовы)


Убираем поломку предмета в процессе крафта


Изначально мы делали int num3 = -10; и благодарили разработчика за оставленный дебаг флаг в качестве подсказки. Теперь это не кажется такой простой задачей сложно понять, какая из ~60 локальных переменных, найденных Гидрой, является нужной. После 15 минут поиска мне наконец удалось понять, что зубодробительная строчка на скриншоте ниже является той самой проверкой дебаг флага и вычитанием из переменной


Ghidra


К сожалению моих знаний Ассемблера не хватило, чтобы понять как именно это работает (судя по Гидре этот процесс занимает 4 команды начиная с MOVZX и заканчивая на AND), поэтому деликатно изменить эту часть я не смог. Другого способа изменить эту переменную я тоже не нашел в силу своих ограниченных знаний, поэтому я изменил подход


Посмотрев еще раз в замечательный (после работы с Гидрой) код старой версии игры в dnSpy я увидел, что за накопление "нестабильности" отвечает метод AddInstability


public bool AddInstability(int addedInstability, int fractureTierModifier = 0, int affixTier = 0)    {        int num = this.RollFractureTier(fractureTierModifier, affixTier);        if (num > 0)        {            this.Fracture(num); // <----- Предмет ломается тут            return false;        }        this.instability = ((int)this.instability + addedInstability).clampToByte();        this.RebuildID();        return true;    }

Гидра радует нас относитлеьно простым кодом данной функции


Ghidra


По коду мы видим, что сначала происходит вызов CALL ItemData$$RollFractureTier, затем мы проверяем результат TEST EAX и прыгаем в нужную ветку


Ghidra


Нам нужно, чтобы мы всегда шли по ветке uVar3 < 1. Тут можно сделать разные исправления например (могу ошибаться) поменять JG(Jump short if greater) на JLE(Jump short if less or equal)


Я решил вопрос иначе просто сделаем проверяемый регистр равным нулю и тогда остальной код будет работать как надо. Меняем CALL на XOR EAX, EAX (самый просто способ обнулить регистр в Ассемблере), который занимает два байта и оставшиеся три байта заполняем NOP'ами


Ghidra


Готово! Сохраняем, заменяем существующий GameAssembly.dll (почему-то Гидра в процессе экспорта файла всегда добавляет расширение .bin и его нужно удалять) и чувствуем себя хакерами


Выводы


Для многих компиляция является "путем в один конец", а бинарные файлы представляются неуязвимыми бастионами, которые способны пасть только перед лучшими хакерами


В реальности многие популярные языки компилируются в примежуточный код, который замечательно трактуется и модифицируется профильными декомпиляторами. Для подобных модификаций зачастую хватит обычного умения программировать хоть на чем-нибудь


И, хотя нативные бинарники и могут представлять опасность для ваших глаз и мозга, повехрностных знаний о том, как работают программы на уровне, близком к железу, зачастую бывает достаточно в связке с современными open-source инструментами для небольших модификаций

Подробнее..

Как мы взломали WhatsApp

11.11.2020 14:20:14 | Автор: admin
Вопрос не в том, как взломать WhatsApp, а в том, как взломать WhatsApp таким образом, чтобы обезопасить номер от бана, исключить бан номера полностью. В принципе, чтобы организовать автоматизацию, воцап не обязательно взламывать. Можно усадить 1000 добровольцев, и они будут вручную отправлять сообщения через официальное приложение WhatsApp. Если добровольцы будут очень усердными, то при достижении определенного порога отправки номер будет заблокирован. Вопрос в том, как взломать WhatsApp таким образом, чтобы номер не был заблокирован при любых объемах отправки. Вот в чём фокус.

Аренда WhatsApp API у неофициального провайдера


Далекий 2017 год. Мы делаем сервис онлайн-записи SLONBOOK для салонов красоты. Историю создания сервиса я подробно описал в этой статье. Фишкой нашего сервиса, которая выгодно отличала нас от конкурентов, была тесная интеграция с WhatsApp. Это позволяло с одной стороны существенно экономить на SMS, а с другой было генератором повторных продаж для салона. Эти два момента делали SLONBOOK привлекательным по сравнению с конкурентами. Начали экспериментировать с WhatsApp.

Вначале у нас не было своей реализации и поэтому мы арендовали шлюз WhatsApp API у стороннего сервиса. У нас пошли первые продажи, и первые проблемы. Оказалось, что при использовании неофициального канала есть риск словить бан и потерять номер. Для салона красоты потерять номер WhatsApp, это все равно что для Интернет-магазина потерять сайт (домен). Клиенты салона общаются с администратором в основном по WhatsApp, при этом мало кто звонит или приходит без предварительной записи. Вначале всегда будет чат с администратором по WhatsApp. И вот этот номер банят. Естественным образом салон выставлял претензию на наш сервис: "- мол, подключились к вашему сервису и потеряли номер." Пошел негатив. Начали разбираться в проблеме. Пытались достучаться до нашего неофициального провайдера. Безуспешно.

Официального WhatsApp Business API в 2017 году еще не существовало, но нам нужна была интеграция. Это было нашим ключевым преимуществом и было новшеством для рынка, особенно в нашей нише.

Реализация собственного WhatsApp API через веб-обертку


Начали искать решение на своем уровне. Была поставлена задача не допустить бана номера клиента. Второстепенные задачи снизить себестоимость WhatsApp API и повысить стабильность работы.

На Github нашли исходники либы, которая реализовывала WhatsApp API через парсинг веб-версии клиента воцап. Затратили порядка 4 месяцев и выпустили на базе этого решения свою реализацию с минимальным набором функций для нашей CRM.

Начали подключать клиентов, и снова начались баны несмотря на то, что мы предпринимали целый ряд мер. Для каждого номера был отдельный IP-адрес, сообщения отправляли с рамдомными задержками, не более двух сообщений в минуту, персональное обращение по имени. Всё тщетно. Баны продолжались. Шел негатив. Продажи встали. Надо было найти кардинальное решение.

Взлом WhatsApp на уровне сокет-соединения


Решение было найдено. Один из нас как-то предложил взломать протокол и работать с сервером WhatsApp напрямую через socket, как это делает нативный веб-клиент. Задача на первый взгляд нам показалась фантастической и невыполнимой. Но нет ничего невозможного! Была поставлена задача взломать зашифрованный протокол WhatsApp. Цель отловить сигналы о скорой блокировке номера. Задачу эту мы успешно решили. Однако потребовалось нам на это 2+ года.

Следует отметить, что прежде чем изобретать свой велосипед мы порылись на Github. Нашли там либу, которая реализовывала взаимодействие с сервером воцап именно по сокету. Однако наш номер словил бан раньше, чем мы успели отправить первое сообщение.

По этой причине мы решили делать свой велосипед. Когда работаешь с воцап на уровне протокола, то надо быть очень аккуратным. Если упустить какую-либо мелочь, то можно быстро спалиться. Пришлось долго и кропотливо изучать трафик, анализировать, сегментировать. Далее пришлось также долго имитировать работу клиента таким образом, чтобы сервер воцап думал, что он работает со своим нативным веб-клиентом.

Протокол WhatsApp версионируется. Каждая версия имеет отличия от предыдущей. При выходе новой версии мы запускам автоматические проверки, которые выдают нам изменения протокола, и мы оперативно их отражаем в своей имплементации.

Надо сказать, что протокол WhatsApp надежно защищен от подобно рода взломов. После взлома протокола на уровне шифровальщика требуется дополнительно полностью повторить работу нативного веб-клиента. WhatsApp защитил свой протокол на двух уровнях: на уровне шифра, и на уровне контента. На втором уровне защиты, на уровне контента, очень много особенностей. Важно полностью повторить контент, который выдает нативный клиент. И тут разработчики воцап заложили немало пасхалочек. Например, нативный веб-клиент рамдомно в течение суток вычисляет хеш-сумму всех отправленных сообщений и отправляет хеш на сервер. На сервере, по-видимому, выполняется сверка. Если хеш не отправить, или если ошибиться со значением хеша, то бан гарантирован. И таких ловушек в протоколе накидано на каждом шагу. Вот поэтому нам и потребовалось 2+ года, чтобы добиться устойчивой работы на уровне протокола сначала взломать уровень шифровальщика, далее полностью повторить уровень контента до мельчайших деталей.

Здесь следует отметить, что существует два канала работы с воцап на уровне протокола: канал веб-клиента и канал мобильного приложения воцап.

Канал веб-клиента используется для связи веб-клиента с мобильным приложением через сервер WhatsApp. Канал мобильного приложения работает с сервером WhatsApp напрямую. В этой статье я подразумеваю взлом именно канала веб-клиента, в то время как взлом канала мобильного клиента на порядок более сложная задача. По этой причине наша имплементация WhatsApp API требует наличия телефонной трубки, на которой установлено мобильное приложение воцап. Без физического телефона наш API работать не будет.

Жёлтая карточка


При работе с сервером воцап на уровне протокола у нас появилась возможность отлавливать сигналы приближающегося бана номера. Мы умеем отлавливать предупреждения о скором бане и тем самым уберегаем номер клиента. Если раньше WhatsApp мог забанить номер одномоментно, внезапно и без предупреждения, то сейчас мы считываем из трафика управляющие сигналы и заранее приостанавливаем активность номера, чтобы сохранить его. Спустя 12 часов мы снова активируем номер. Раньше WhatsApp выдавал нам сразу Красную карточку без предупреждения. Сейчас мы научились получать из протокола Жёлтую карточку. Можно, конечно, игнорировать первую, вторую и третью Жёлтые карточки, однако на кону номер и бизнес клиента.

Благодаря такому подходу нам удалось добиться того, что номера наших клиентов перестали попадать в бан вовсе. Время показало, что на протяжении более 12 мес наши клиенты не потеряли ни одного номера. Разумеется, речь идет не о спамерах, а о нормальной работе пользователя в режиме онлайн-записи в салон красоты. Если очень хочется потерять номер, то достаточно отправить 1000+ сообщений и игнорировать предупреждения нашего шлюза.

На защиту номера от бана влияет множество факторов. Возраст номера, соотношение количества отправленных и полученных сообщений в сутки, количество отправленных жалоб на ваш номер, контент сообщений. Чем дольше номер зарегистрирован в воцап, чем активнее клиенты общаются с вами, тем лучше защищен номер от блокировки и тем большее количество сообщений в сутки номер может отправить. На официальном сайте Facebook опубликована таблица, которая наглядно показывает, как возрастает пропускная способность канала в зависимости от показателя качества номера. Примерно такие же правила мы реализовали на уровне нашего API.

Бонусы


Работа на уровне протокола имеет свои преимущества по сравнению с работой на уровне веб-обертки. Нам удалось решить вопросы безопасности, скорости, стабильности и функциональности.

Безопасность на первом месте. Важно защитить номер от бана. Об этом выше.
Скорость. Например, для чат-ботов актуальным является вопрос скорости ответа клиенту. При общении человека с ботом важно не терять фокус внимания. Если бот будет отвечать с задержкой более 5 секунд, то фокус внимания переключиться, работа с ботом прекратится. Если чат-бот ведет одновременно 10 диалогов, то скорость обработки сообщений должна быть порядка 10 сообщений в секунду.

Стабильность. Протокол WhatsApp версионируется. Это означает, что мы заранее узнаем о новых версиях протокола и своевременно адаптируем наше API под новую версию протокола.

Функциональность. На уровне протокола имеется возможность поддержать любую функцию, которая есть в WhatsApp. Поэтому потенциальные возможности API безграничны.

Рождение Сверхзелёной


Итак, решение своих задач внутри нашей CRM позволило нам создать очень качественное WhatsApp API. Во главу угла была поставлена задача защиты номера от бана. И эту задачу мы решили. Заодно решили целый ряд вспомогательных технических задач. Держать такую птичку взаперти было бы преступлением, и в начале 2020 года мы опубликовали наш проект GREEN-API.

Что дальше?


Особенностью всех существующих реализаций взлома WhatsApp является обязательное наличие телефонного аппарата. Воцап устроен таким образом, что вся переписка хранится в телефоне и всё взаимодействие выполняется через телефонный аппарат. Поэтому любая имплементация неофициального WhatsApp API требует обязательного наличия телефонного аппарата. Это в свою очередь имеет свои неудобства приходится приобретать телефон и дополнительно следить, чтобы он был всегда заряжен, подключен к Интернет. Если телефон, например, уснет, то отправка и получение сообщений прекратится. Нужен сотрудник, который будет следить за работоспособностью трубки. Это очень неудобно.

По этой причине мы параллельно разработали WhatsApp API, который работает без трубки. Мы пошли по пути использования эмулятора Android и добились того, что воцап думает, что работает с обычным телефоном. По сути это иная имплементация задачи взлома воцап на уровне реверс-инжиниринга Android-приложения. Но об этом уже в следующий раз.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru