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

Reverse-engineering

Дизассемблируем код на Си switch() case assembler disass()

19.11.2020 20:15:16 | Автор: admin

Доброго времени суток.

Сегодня мы будем смотреть дизассемблированный код инструкций if, for, while, swich, которые написаны на языке Си.

1605795529033.png1605795529033.png

Инcтрукция if

Данную инструкцию довольно просто отличить в дизассемблированном виде от других инструкций. Её отличительное свойство - одиночные инструкции условного перехода je, jne и другие команды jump.

1605790962062.png1605790962062.png1605790989407.png1605790989407.png

Напишем небольшую программу на языке Си и дизассемблируем её с помощью radare2. Разницы между IDA PRO и radare2 при дизассемблировании этих программ не было обнаружено, поэтому я воспользуюсь radare2. Вы можете использовать IDA PRO.

IDA PRO

2020-11-17_08-22.png2020-11-17_08-22.png2020-11-17_08-23.png2020-11-17_08-23.png

radare2

1605792900362.png1605792900362.png

Код на Си

#include <stdio.h>void main() {    int x = 1;    int y = 2;    if(x == y) {        printf("x = y\n");    }    else{        printf("x != y\n");    }}

Компилируем при помощи gcc. Команда gcc -m32 prog_if.c -o prog_if. -m32 озночает, что компилироваться код будет под архитектуру x86.

Чтобы посмотреть на код в radare2, напишем команду r2 prog_if. Далее прописываем aaa для анализа кода и переходим к функции main s main. Посмотрим на код с помощью команды pdf.

Дизассемблированный вариант в radare2

1605792900362.png1605792900362.png

Первым делом в программе происходит объявление переменных ( int x; int y ), а затем значение 1 перемещается в varch (это переменная x) и значение 2 в var10h (это переменная y). Далее идёт сравнение (cmp) 1 и 2 (cmp edx, dword [var_10h]). Эти значения не равны. Значит jne ( jump if noe equal) перейдёт по адресу 0x000011e1. Проще всего инструкцию if запомнить и опрелелить в режиме графов (команда VV для для radare2 или клавиша пробел для IDA).

Режим графов

1605792914861.png1605792914861.png

Немного усложним задачу. Добавим вложенные инструкции. Попробуйте проанализировать этот код.

Код на Си

#include <stdio.h>void main() {    int x = 0;    int y = 1;    int z = 2;    if(x == y) {        if(z == 0) {        printf("z = 0; x = y\n");        }        else{            printf("z = 0; x != y\n");        }    }    else {        if(z == 0) {            printf("z = zero and x != y.\n");        } else {            printf("z non-zero and x != y.\n");        }    }}

Дизассемблированный вариант в radare2

1605792935104.png1605792935104.png

Режим графов

1605792950680.png1605792950680.png

В режиме графов это воспринимать намного проще.

Инструкция for

Циклы for всегда состоят из четырех этапов: инициализации, сравнения, выполнения инструкций и инкремента/декремента. По этим четырём этапом мы будем распозновать for в ассемблерном коде.

Код на Си

#include <stdio.h>void main() {    int x;    for(x = 0; x < 100; x++) {        printf("x = %d", x);    }}

Дизассемблированный вариант в radare2

1605792973718.png1605792973718.png

1 - инициализации переменной var_ch (x = 0)
2 - сравнение, а затем jle. ( пока x не будет меньше или равен 2, выполнять цикл.)
3 - выполнения инструкций (printf)
4 - инкрмент переменной var_
ch (++x)

Режим графов

1605792987678.png1605792987678.png

Инструкция while

Цикл while часто используется при ожидании, пока не будет выполнено какое-то условие, например получение команды или пакета. В ассемблере циклы while похожи на for, но их легче понять. В ассемблере это выражение похоже на цикл for, но инкремента может и не быть.

Код на Си

#include <stdio.h>int func_1(int x);int change_status();int main() {    int status = 0;    while(status == 0) {        printf("int e = %d", func_1(5) );        status = change_status();    }    return 0;}int change_status() {    return 1;}int func_1(int x) {    int c;    int e;    int l;    c = 1 + 2;    e = x / 5;    l = 4 - 2;    return e;}

Дизассемблированный вариант в radare2

1605793002542.png1605793002542.png

1 - инициализации переменной var_4h (status = 0)
2 - сравнение, а затем je. ( пока x равен 0, выполнять цикл.)
3 - выполнения инструкций (func
1, printf, change_status)

Режим графов

1605793019409.png1605793019409.png

Инструкция switch

Конструкция switch обычно компилируется двумя способами: по примеру условного выражения или как таблица переходов.

Компиляция по примеру условного выражения

Код на Си

#include <stdio.h>int main() {    int i = 3;    switch(i) {        case 1:            printf("CASE_1 i = %d", i+4);            break;        case 2:            printf("CASE_2 i = %d", i+9);            break;        case 3:            printf("CASE_3 i = %d", i+14);            break;    }    return 0;}

Дизассемблированный вариант в radare2

1605793045836.png1605793045836.png

1 - инициализации переменной var_4h (i = 3)
2 - выполнения инструкций (add, printf)

Чтобы понять какой "case" выбран, происходит сравниение (cmp, а затем je, jne) переменной i с значением case.

Режим графов

1605793347813.png1605793347813.pngscreen13.pngscreen13.pngscreen_13_2.pngscreen_13_2.png

Глядя на этот код, сложно (если вообще возможно) сказать, что представлял собой оригинальный исходный текст конструкцию switch или последовательность выражений if . В обоих случаях код выглядит одинаково, поскольку оба выражения используют множество инструкций cmp и je или jne.

Таблица переходов

Следующий пример ассемблерного кода часто можно встретить в больших смежных выражениях switch. Мы добавим case 4 и инструкцию по умолчанию.

Код на Си

#include <stdio.h>int main() {    int i = 3;    switch(i) {        case 1:            printf("CASE_1 i = %d", i+4);            break;        case 2:            printf("CASE_2 i = %d", i+9);            break;        case 3:            printf("CASE_3 i = %d", i+14);            break;        case 4:            printf("CASE_3 i = %d", i+19);            break;        default:            break;    }    return 0;}

Дизассемблированный вариант в radare2

1605793648917.png1605793648917.png1605793656018.png1605793656018.png

1 - инициализации переменной var_4h (i = 3)
2 - выполнения инструкций (add, printf)

Вот этот дизасcемблированный код довольно сложно быстро отличить от if и вообще понять что и как тут. В режиме графов всё будет более понятно.

Режим графов

1605793750977.png1605793750977.png1605793673414.png1605793673414.png1605793684884.png1605793684884.png1605793691266.png1605793691266.png

Режим графов - ваш друг в дизасcемблировании :)

На этом всё. Рекомендую попробовать самому написать программы на Си, скомпилировать и изучить дизасcемблированный код. Практика и ещё раз практика!

Спасибо за внимание. Не болейте.

Подробнее..

Kremlin RATs история одной мистификации

15.02.2021 12:06:03 | Автор: admin

Этим постом мы начинаем двухсерийный технодетектив, в котором встретились "священная триада" доменов: putin, kremlin, crimea и "крысы" программы удаленного доступа (RAT), а также шпион AgentTesla. Началась история с того, что в конце мая 2020 года сетевой граф Group-IB, наша автоматизированная система анализа инфраструктуры, начал детектировать домены с интересным паттерном *kremlin*.duckdns.org, к которым подключались различные вредоносные файлы. Аналитики Group-IB Threat Intelligence & Attribution исследовали эти домены и установили три кампании по распространению различных RAT. Они шли с 2019 года и были нацелены на пользователей из Польши, Турции, Италии, Украины, России, Казахстана, Болгарии, Беларуси, Греции и Чехии. В ходе расследования была установлена связь между обнаруженными доменами и остальной используемой инфраструктурой, а заодно и с конкретным человеком, который стоит за распространением AgentTesla и других вредоносных программ. Итак, обо всем по-порядку.

Кампания лета 2020 года

В начале список доменов, который привлёк наше внимание, выглядел так:

  • crimea-kremlin.duckdns.org

  • kremlin-afghan.duckdns.org

  • kremlin-crimea.duckdns.org

  • kremlin-turbo.duckdns.org

Данные домены были зарегистрированы на один IP-адрес 79.134.225.43 15 июня 2020 года. По данным сетевого графа Group-IB, только с этими четырьмя доменами связанно порядка 30 различных вредоносных файлов. Судя по документам-приманкам, данная кампания была нацелена на пользователей из Польши, Турции, Италии, Германии и Болгарии.

Связанная инфраструктураСвязанная инфраструктура

Дальнейший анализ показал, что в основном файлы были залиты в публичные источники, начиная с 25 июня 2020 года. Самые распространенные имена Potwierdzenie transakcji.xls, lem makbuzu, WACKER - 000160847.xls, Potwierdzenie operacji.xls. Один из таких файлов, SHA1: 95A6A416F682A9D254E76EC38ADE01CE241B3366, является документом-приманкой на польском языке и якобы отправлен от Bank Polski.

Изображения документа-приманки SHA1: 95A6A416F682A9D254E76EC38ADE01CE241B3366Изображения документа-приманки SHA1: 95A6A416F682A9D254E76EC38ADE01CE241B3366

Заражение

После активации макросов в этом документе выполняется PS-скрипт для извлечения команды второго этапа из файла lab.jpg, размещенном на удаленном сервере:

Исполняемый PS-скрипт из макросаИсполняемый PS-скрипт из макроса

В файле lab.jpg содержится обфусцированная в BASE64 команда, которая после декодирования выглядит следующим образом:

Деобфусцированное содержимое lab.jpgДеобфусцированное содержимое lab.jpg

Данный код считывает содержимое файла http://officeservicecorp[.]biz/rnp.txt, в котором и находится полезная нагрузка.

В результате выполнения данной последовательности PS-скриптов загружается и выполняется популярный NetWire RAT, который и производит подключение к своему C&C-серверу kremlin-crimea[.]duckdns.org на порт 3396.

Конфигурация NetWire RATКонфигурация NetWire RAT

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

Граф с шагом 2. Связанная инфраструктураГраф с шагом 2. Связанная инфраструктура

Интересно, что те файлы, которые подключались к office-service-tech[.]info, также производили сетевое подключение к ahjuric[.]si. Пример таких файлов SHA1 a3816c37d0fbe26a87d1cc7beff91ce5816039e7. Это документ-приманка на турецком языке с логотипом государственного банка Турции.

Документ-приманка на пользователей Турции. SHA1: a3816c37d0fbe26a87d1cc7beff91ce5816039e7Документ-приманка на пользователей Турции. SHA1: a3816c37d0fbe26a87d1cc7beff91ce5816039e7

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

Исполняемый PS-скрипт из макросаИсполняемый PS-скрипт из макросаСодержимое ahjuric[.]si/code.txtСодержимое ahjuric[.]si/code.txt

Результатом выполнения обфусцированного PS-скрипта будет выполнение еще одного обфусцированного в Base64 скрипта, который в конечном счете и выполнит полезную нагрузку в виде Netwire Rat из office-service-tech[.]info/pld.txt.

Содержимое office-service-tech[.]info/pld.txtСодержимое office-service-tech[.]info/pld.txt

C&C-сервером данного образца является crimea-kremlin.duckdns[.]org.

Также мы обнаружили файлы, которые производят сетевое подключение одновременно к kremlin-turbo.duckdns[.]org и wshsoft[.]company. Название домена относит нас к WSH RAT, который основан на коде Houdini. Один из таких файлов SHA1: b42a3b8c6d53a28a2dc84042d95ce9ca6e09cbcf. Данный образец RAT отправляет на C&C-сервер kremlin-turbo.duckdns[.]org:3397 запросы вида /is-ready, а в качестве UA у него указан WSHRAT.

Сетевые запросы файла SHA1: b42a3b8c6d53a28a2dc84042d95ce9ca6e09cbcfСетевые запросы файла SHA1: b42a3b8c6d53a28a2dc84042d95ce9ca6e09cbcf

На этом этапе важно отметить, что часть используемых доменов в этой кампании была зарегистрирована на почту tetragulf@yahoo.com.

Кампания весны 2020 года

Изучая всю остальную связанную инфраструктуру, мы обратили внимание на домены, зарегистрированные на asetonly@yahoo.com. С начала 2020 года на эту почту были зарегистрированы следующие домены:

  1. nitro-malwrhunterteams.com

  2. office-data-labs.com

  3. putin-malwrhunterteams.com

  4. kremlin-malwrhunterteam.info

  5. skidware-malwrhunterteams.com

  6. screw-malwrhunterteams.com

  7. screw-malwrhunterteam.com

  8. office-services-labs.com

  9. office-cloud-reserve.com

  10. office-clean-index.com

  11. office-cleaner-indexes.com

Мы собрали более 130 различных образцов вредоносных программ из различных источников, связанных только с этими доменами. Судя по названиям и содержимому данных образцов, кампания весны 2020 года была нацелена на пользователей из Европы и стран СНГ мы обнаружили документы-приманки на украинском, белорусском, казахском, русском и греческом языках.

Первые файлы данной кампании были загружены на публичные песочницы 23 марта 2020 года. Один из таких файлов Аналз проекту.docx SHA1-d8826efc7c0865c873330a25d805c95c9e64ad05 распространялся в качестве вложения к письму Електронна розсилка_ Змнене замовлення.eml SHA1-7f1fdf605e00323c055341919173a7448e3641fb, которое было загружено на VirusTotal через веб-интерфейс из Украины.

Содержимое письма Електронна розсилка_ Змнене замовлення.emlСодержимое письма Електронна розсилка_ Змнене замовлення.eml

Заражение

Содержимое самого документа не вызывает интереса и выглядит как отсканированный лист со счетом. Однако сам документ во время запуска эксплуатирует уязвимость CVE-2017-0199. В результате выполняется команда, которая загружает полезную нагрузку в виде http://office-cloud-reserve[.]com/hydro.exe.

Исполняемый PS-скриптИсполняемый PS-скрипт

Загружаемой полезной нагрузкой является программа-шпион AgentTesla (почитать о ней вы можете тут, тут и тут). В качестве сервера для эксфильтрации данных используется ftp.centredebeautenellycettier[.]fr легитимный домен, который, по всей видимости, был скомпрометирован.

Установка FTP-соединения.Установка FTP-соединения.

Другой исследуемый файл SHA1- 19324fc16f99a92e737660c4737a41df044ecc54, который называется Байланыс орталытары.img, распространялся в качестве вложения через электронное письмо SHA1- 403c0f9a210f917e88d20d97392d9b1b14cbe310 на казахском языке c темой, относящейся к COVID-19.

Содержимое письма 403c0f9a210f917e88d20d97392d9b1b14cbe310Содержимое письма 403c0f9a210f917e88d20d97392d9b1b14cbe310

Данное вложение является .iso-образом и в некоторых случаях называется Байланыс орталытары.img. Файл монтируется в систему как образ, в котором находится лишь один обфусцированный VBS-файл SHA1: fd274f57e59c8ae3e69e0a4eb59a06ee8fd74f91 под названием Денсаулы сатау бойынша анытамалы жне деректер базасы.vbs. Данный файл по сути является загрузчиком, который выполняет обфусцированный PS-код. При его открытии происходит считывание файла http://office-cleaner-indexes[.]com/loud.jpg.

Содержимое сбрасываемого файла SHA1:fd274f57e59c8ae3e69e0a4eb59a06ee8fd74f91Содержимое сбрасываемого файла SHA1:fd274f57e59c8ae3e69e0a4eb59a06ee8fd74f91

В результате происходит загрузка и выполнение AgentTesla, который также производит эксфильтрацию данных через ftp.centredebeautenellycettier[.]fr.

Другой документ SHA1: c992e0a46185bf0b089b3c4261e4faff15a5bc15 под названием 060520.xls распространялся через письмо на греческом языке, а его содержимое выглядит так же, как и все другие в этой кампании, только на греческом языке. Его полезная нагрузка в виде NanoCore Rat подключается к screw-malwrhunterteams[.]com.

Содержимое документа-приманки 060520.xlsСодержимое документа-приманки 060520.xls

Кампания 2019 года

Продолжая исследовать инфраструктуру, связанную с tetragulf@yahoo.com, мы обнаружили, что в 2019 году на эту почту было зарегистрировано всего четыре домена, два из которых были зарегистрированы в конце февраля и участвовали в одной кампании по распространению вредоносных документов.

Список зарегистрированных доменов (подчеркнутые точно вредоносные):

  • east-ge.com

  • mariotkitchens.com

  • sommernph.com

  • kingtexs-tvv.com

Первые файлы, связанные с этими доменами, начали загружаться в публичные песочницы 18 июня 2019 года.

Список вредоносных файлов, связанных с кампанией 2019 годаСписок вредоносных файлов, связанных с кампанией 2019 года

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

Заражение

Один из первых документов этой кампании распространялся через электронную почту под разными названиями: CNC 0247.doc, ЧПУ 0247.doc SHA1:443c079b24d65d7fd74392b90c0eac4aab67060c.

Содержимое письма SHA1: b6ff3e87ab7d6bd8c7abd3ee30af24b4e3709601Содержимое письма SHA1: b6ff3e87ab7d6bd8c7abd3ee30af24b4e3709601

Согласно данным из нашего графа, этот документ устанавливает подключение к http://68.235.38[.]157/ava.hta и kingtexs-tvv[.]com.

Сетевое взаимодействие файла SHA1: 443c079b24d65d7fd74392b90c0eac4aab67060cСетевое взаимодействие файла SHA1: 443c079b24d65d7fd74392b90c0eac4aab67060c

Мы заинтересовались этим хостом и обнаружили дополнительные файлы, которые устанавливали сетевое подключение к http://68.235.38[.]157. Одни из таких файлов, Estos son los documentos adjuntos de junio.doc SHA1: 02799b41c97b6205f1999a72cef8b8991d4b8092 и New Order.doc SHA1: 25abf0f75c56516134436c1f836d9db1e770ff30, эксплуатируют уязвимость CVE-2017-11882. Во время запуска они устанавливают подключение к http://68.235.38[.]157/oyii.hta.

Содержимое http://68.235.38[.]157/oyii.htaСодержимое http://68.235.38[.]157/oyii.hta

Этот файл содержит код на Visual Basic, который выполняет обфусцированную в Base64 PS-команду на загрузку полезной нагрузки из общедоступного файлового хранилища https://m.put[.]re/Qm8He5E4.exe - SHA1: 523c5e0a1c9bc6d28f08500e96319571b57e4ba7 и сохраняет в директорию temp под именем avantfirewall.exe.

Исполняемый PS-скриптИсполняемый PS-скрипт

Загружаемая полезная нагрузка считывает содержимое из https://paste[.]ee/r/rSrae, вследствие чего выполняется Async RAT, который устанавливает подключение к своему C&C-серверу kizzoyi.duckdns[.]org на порт 8808.

Другой документ из данной кампании SHA1-1230acfd1f6f5b13a218ff8658a835997d1f0774 под названием таблиц.doc распространялся через письмо на украинском языке.

Из-за критически опасной уязвимости CVE-2017-11882, позволяющей выполнить вредоносный код без взаимодействия с пользователем, во время запуска этого документа происходит выполнение кода, содержащегося в OLE-объекте wd32PrvSE.wmf.

Ole объекты содержащиеся в SHA1:1230acfd1f6f5b13a218ff8658a835997d1f0774Ole объекты содержащиеся в SHA1:1230acfd1f6f5b13a218ff8658a835997d1f0774

В результате выполнения кода из OLE-объектов загружается и выполняется Async RAT.

Заключение

На этой ноте мы заканчиваем первую часть исследования. Мы понимаем, что этот детектив должен закончиться чем-то логичным, и в следующей части вы с новыми силами окунетесь в развязку данной истории. Пока же можете изучить наше ежегодное исследование Hi-Tech Crime Trends или взглянуть на наши вакансии.

Рекомендации

Ниже техники атакующего и защитные техники в соответствии с MITRE ATT&CK и MITRE Shield, которые мы рекомендуем использовать для защиты и предотвращения инцидентов.

Все защитные техники реализованы в продуктах Group-IB для защиты на разных этапах атаки. Если у вас будут вопросы или подозрения на инцидент обращайтесь на response@cert-gib.com.

Tactics

Techniques of adversaries

Mitigations & Active Defense Techniques

Group-IB mitigation & protection products

Resource Development

T1583. Acquire Infrastructure

ID: T1588.005. Obtain Capabilities: Exploits

ID: T1588.001. Obtain Capabilities: Malware

M1056. Pre-compromise

M1016. Vulnerability Scanning

Security Assessment

Threat Intelligence & Attribution

Initial Access

ID: T1566.001. Phishing: Spearphishing Attachment

M1049. Antivirus/Antimalware

M1031. Network Intrusion Prevention

M1017. User Training

M1050. Exploit Protection

M1051. Update Software

DTE0035. User Training

DTE0019. Email Manipulation

DTE0027. Network Monitoring

Threat Hunting Framework

Threat Intelligence & Attribution

Cyber Education

Red Teaming

Execution

T1059. Command and Scripting Interpreter

T1204. User Execution

T1203. Exploitation for Client Execution

M1049. Antivirus/Antimalware

M1038. Execution Prevention

M1021. Restrict Web-Based Content

M1026. Privileged Account Management

DTE0035. User Training

DTE0021. Hunting

DTE0018. Detonate Malware

DTE0007. Behavioral Analytics

DTE0003. API Monitoring

DTE0034. System Activity Monitoring

Threat Hunting Framework

Red Teaming

Incident Response

Fraud Hunting Platform

Persistence

T1053. Scheduled Task/Job

Defense Evasion

T1036. Masquerading

T1027. Obfuscated Files or Information

Credential Access

T1555. Credentials from Password Stores

T1552. Unsecured Credentials


M1049. Antivirus/Antimalware

DTE0007. Behavioral Analytics

DTE0003. API Monitoring

DTE0034. System Activity Monitoring

Threat Hunting Framework

Collection

T1005. Data from Local System

Command and Control

T1071. Application Layer Protocol

T1573. Encrypted Channel

M1038. Execution Prevention

M1031. Network Intrusion Prevention

DTE0021. Hunting

DTE0022. Isolation

DTE0027. Network Monitoring

DTE0003. API Monitoring

DTE0034. System Activity Monitoring

DTE0031. Protocol Decoder

Threat Hunting Framework

Подробнее..

Reverse engineering обратная разработка приложений для самых маленьких

05.05.2021 14:10:44 | Автор: admin

Обратная разработка (англ. Reverse Engineering) - метод исследования устройств или программного обеспечения с целью понять принцип его работы или обнаружить недокументированные возможности. В информационной безопасности она занимает значительную роль, благодаря ей специалисты в области ИБ могут исследовать вредоносные приложения, разбираться как они работают для последующего, например, составления сигнатур в базы антивирусов и защиты других пользователей от предстоящей цифровой угрозы.

Выделяют 3 методики проведения обратной разработки:

  • анализ обмена данными приложения, с помощью различных анализаторов трафика;

  • дизассемблирование машинного кода программы (изучение требует довольно много времени);

  • декомпиляция кода программы для создания исходного кода программы на языке программирования высокого уровня.

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

В качестве подопытного возьмем задание, которое использовалось в Test lab 15. Но для большей наглядности программа была скомпилирована без отладочной информации и без использования оптимизаций (gcc -O0). После чего к ней была применена утилита strip с параметром -s, который удаляет всю лишнюю символьную информацию из исполняемого файла, вроде имён переменных и функций.

Рабочим инструментом выступит Radare2 - кроссплатформенный набор утилит для обратной разработки и отладки. Включает в себя инструменты для анализа, дизассемблирования, декомпилирования и патчинга. В отличие от популярного IDA Pro распространяется бесплатно.

Установка

Рекомендуемым разработчиками способом установки и обновления Radare2 является установка из официального git-репозитория. Предварительно в системе должны присутствовать установленные пакеты git, build-essential и make.

$ sudo apt install git build-essential make$ git clone https://github.com/radareorg/radare2$ cd radare2$ sys/install.sh

Запуск установки рекомендуется производить не из под пользователя root, иначе скрипт сам произведёт понижение привилегий.

Далее устанавливаем графическую оболочку для Radare2. Мы будет устанавливать официальный GUI под названием Iaito. Установим пакеты, необходимые для установки Iaito:

$ sudo apt install qttools5-dev-tools qt5-default libqt5svg5-dev

Для дистрибутивов Linux на базе Debian, есть готовые пакеты, ссылки на которые можно взять тут. Скачаем и установим нужную версию пакета:

$ wget https://github.com/radareorg/iaito/releases/download/5.2.2/iaito_5.2.2_amd64.deb$ sudo dpkg -i iaito_5.2.2_amd64.deb

Теперь установим плагин r2ghidra, который является интеграцией декомпилятора Ghidra для Radare2. Плагин не требует отдельной установки Ghidra, так как содержит в себе всё необходимое. Для установки плагин доступен в качестве r2pm пакета:

$ r2pm update$ r2pm -ci r2ghidra

Установленный плагин автоматически интегрируется в GUI Iaito. После установки запускаем графическую оболочку и если все сделали правильно, то видим стартовый экран:

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

Первый запуск программы-примера

Открываем файл в Iaito, оставляем настройки анализа по умолчанию:

Настройки

После того, как Radare2 проанализирует файл, смотрим результат, открывшийся во вкладке Dashboard:

Программа скомпилирована под 64-битную версию Linux, написана на языке C. Слева мы видим список функций, которые Radare2 смог обнаружить. Среди них импортируемые из библиотеки libc функции printf, puts и putchar, выводящие на экран строку по формату и символ.

Функция main это главная функция программы. Выполнение начинается с неё. Кликнув два раза по её названию, открывается вкладка Disassembly с результатом её дизассемблирования:

Результат
^^

Немного про Ассемблер

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

Команды ассемблера

Каждая команда Ассемблера это команда для процессора. Синтаксис команды состоит из нескольких частей:

Команда - означает какую операцию необходимо выполнить. Например:

  • mov - команда пересылки данных. Копирует содержимое одного операнда в другой;

  • lea - вычисляет эффективный адрес операнда-источника и сохраняет его в регистре;

  • cmp - сравнение двух операндов;

  • условные и безусловные переходы (jmp, jne, je, jle, ) - безусловные и условные (требуется выполнение условия) переходы к меткам. Например, jump @exit выполняет безусловный переход к метке exit;

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

  • и тд

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

Комментарий - понятно из названия, что это комментарий для удобства чтения кода. Пишется после точки с запятой.

Метки - обозначение участка кода. Также улучшает читаемость кода, но нужны еще и для перехода к помеченному им участку.

mov ax, 0 ; Помещаем значение 0 в регистр ax

где:

  • mov - команда (перемещение значения из одного операнда в другой);

  • ax, 0 - операнды (регистр и значение);

  • ; <текст> - комментарий

Или рассмотрим другой пример как выглядит возведение числа в степень в Ассемблере:

mov ax,2 ; Помещаем значение 2 в регистр AXmov bx,ax ; Помещаем значение регистра AX в BXmul bx ; Выполняем дважды командой mul возведение в степень числа 2mul bx

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

pow (2, 3);

Вернемся к нашему заданию

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

Масштабирование на этой вкладке выполняется сочетаниями клавиш Ctrl+"-" и Ctrl+"+". Можно было бы начать разбираться в работе программы уже с этого места, но есть возможность посмотреть на программу в ещё более читаемом виде. Переключаемся на вкладку Decompiler, внизу окна и видим псевдокод, полученный в результате декомпиляции (восстановление до кода на языке, на котором программа была написана, в нашем случае язык C) средствами встроенного декомпилятора Radare2.

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

Теперь код программы стал практически полностью читаем, за исключением имён переменных.

undefined8 main(void){    uint32_t uVar1;    uint32_t uVar2;    uint32_t var_ch;    undefined8 var_8h;        puts("Token:");    fcn.00001189(0x5020, 0x5090);    for (var_8h._0_4_ = 0; (int32_t)var_8h < 0xf; var_8h._0_4_ = (int32_t)var_8h + 1) {        putchar(*(undefined *)((int64_t)(int32_t)var_8h * 8 + 0x5020));    }    puts(0x2c78);    puts("Key:");    printf("\n-----BEGIN RSA PRIVATE KEY-----");    var_ch = 0;    for (var_8h._4_4_ = 0; (int32_t)var_8h._4_4_ < 0xc68; var_8h._4_4_ = var_8h._4_4_ + 2) {        if ((var_8h._4_4_ & 0x7f) == 0) {            putchar(10);        }        uVar1 = (*(uint8_t *)((int32_t)var_8h._4_4_ + *(int64_t *)0x5098) & 0x1f) + 9;        uVar2 = (*(uint8_t *)(*(int64_t *)0x5098 + (int64_t)(int32_t)var_8h._4_4_ + 1) & 0x1f) + 9;        putchar((char)uVar2 + (char)(uVar2 / 0x19) * -0x19 + ((char)uVar1 + (char)(uVar1 / 0x19) * -0x19) * '\x10' ^                *(uint8_t *)((int64_t)(int32_t)var_ch * 8 + 0x5020));        var_ch = var_ch + 1;        if (var_ch == 0xf) {            var_ch = 0;        }    }    putchar(10);    puts("-----END RSA PRIVATE KEY-----");    return 0;}

В коде мы видим, что сначала выводится строка Token:, после чего происходит вызов некой функции с двумя параметрами, после которого идёт цикл с переменной var_8h, которая проходит значения от 0 до 14 включительно и выводит что-то посимвольно, основываясь на адресе памяти 0x5020 и счётчике с множителем 8. Из этого можно сделать вывод, что в памяти, начиная с адреса 0x5020, расположен массив структур из 15 значений размером 8 байт. Также стоит обратить внимание, что адрес 0x5020 передавался в качестве первого параметра в функцию, вызываемую перед этим циклом. Будем для простоты далее называть его токен. Далее по коду выводятся строки начала закрытого ключа и в цикле выводится посимвольно закрытый ключ. Внутри цикла вывода ключа идёт повторяющийся цикл по обнаруженному нами ранее массиву структур, используя переменную var_ch. Перед выводом на экран над каждым символом закрытого ключа производится операция исключающего ИЛИ (XOR) с текущим символом токена. После цикла выводится строка, завершающая закрытый SSH ключ. Исходя из того, что выводимый программой токен не является правильным, можно сделать вывод, что что-то происходит не так в ранее обнаруженной нами функции с двумя параметрами fcn.00001189, вызываемой перед выводом токена на экран. Перейдём на неё, дважды кликнув по названию функции в списке слева.

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

_var_18h < (undefined8 *)(arg2 + -8)

Получается, нужно это исправить. Переключимся на вкладку дизассемблера:

В полученном коде мы видим только одну команду вычитания 8:

0x00001211 sub rax, 8

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

Представление в виде графов

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

Теперь нам нужно убрать команду sub rax, 8. Наиболее просто это можно сделать переписав поверх нее команду nop (No Operation) - то есть, заменив команду вычитания командой "ничего не делать".

Для этого, находясь на вкладке дизассемблера, поставим курсор на эту команду и переключимся на вкладку Hexdump:

По относительному адресу команды 0x00001211 убеждаемся, что курсор стоит там, где необходимо. Выделяем 4 байта, начиная с адреса 0x00001211 и справа выберем вкладку Parsing. Увидим результат дизассемблирования выделенных байт.

Теперь нужно заменить выделенные байты на 4 байта со значением 90 (шестнадцатиричное значение машинного кода команды nop), но тут мы сталкиваемся с тем, что в Iaito нельзя просто так отредактировать шестнадцатиричное значение по адресу. Список доступных действий мы можем увидеть, нажав на выделенных байтах правую кнопку мыши.

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

Сначала выберем Write zeros. Iaito напомнит нам, что файл открыт в режиме только для чтения и предложит переоткрыть его либо в режиме для записи, либо включить режим кэширования. В режиме кэширования все изменения к исходному файлу будут применяться только после выбора пункта меню File Commit changes.

Предупреждение

Выберем режим кэширования, после чего снова попытаемся записать нули. И теперь это у нас получается. На каждом из четырёх байт выберем из контекстного меню пункт Edit Increment/Decrement и добавим значение 144 (десятичную запись шестнадцатиричного числа 90).

Смотрим на получившийся результат:

После внесения изменений не забываем нажать File Commit Changes. Запускаем ещё раз программу dechip, чтобы посмотреть результат наших действий:

Было
Стало

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

Заключение

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

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

Подробнее..

Binary Coverage для Reverse Engeneering

09.06.2021 18:07:37 | Автор: admin

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

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

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

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

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

На картинке в качестве базовых единиц используется количество базовых блоков из dll библиотек ОС Windows. Также можно брать в качестве базовых единиц:

  • строку дизассемблированного листинга

  • базовый блок алгоритма, обычно это строки дизассемблированного листинга между условными переходами (джампами)

  • блоки между инструкциями вызовов функций - call

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

Описание стенда и набора инструментов

Для исследований приложений будем пользоваться:

  1. IDA Pro в качестве навигатора по дизассемблированному коду

  2. Проект DinamoRIO как инструмент построения покрытия кода

  3. Плагин IDA Pro LightHouse как инструмент визуализации покрытия кода для исследования алгоритма

  4. Виртуальная машина Windows 10

  5. Virtual Box в качестве среды виртуализации

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

Практика

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

  • Открыть исследуемый файл в IDA Pro

  • В строке IDAPython набрать команду: idaapi.get_user_idadir()

  • Полученный путь скопировать в проводник, если в результирующей директории нет директории plugins, создать её

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

  • перезапустить IDA Pro

После успешной установки, должно появиться дополнительная опция в меню File:

Теперь можно приступать к сбору данных о приложении. Для сбора информации о покрытии кода, будем использовать инструмент drrun.exe с плагином drcov. Результирующая командная строка будет выглядеть так:

drrun -t drcov -logdir ./ --  KeygenMe.exe

Выбирать инструмент drrun нужно в соответствии с разрядностью исследуемого файла, так как в релизной версии DinamoRIO есть несколько версий приложения. В нашем случае берем инструмент из bin32. В результате в директории будет создан файл с расширением ".proc". Это файл с информацией о покрытии кода. Его нужно загузить в IDA Pro через опцию, котрая появилась после установки плагина. Результат представлен ниже на снике:

Цетовая схема показывает какие участки кода работали чтобы алгоритм мог обработать подаваемые данные.

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

Выводы

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


Статья подготовлена экспертом OTUS - Александром Колесниковым в преддверии старта курса Reverse-Engineering. Professional.

Всех желающих приглашаем принять участие в бесплатном двухдневном интенсиве по теме: "Пишем дампер процессов"


Подробнее..

Использование Windbg для обратной разработки

16.06.2021 00:20:01 | Автор: admin

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

Установка

Установка возможна только при использовании Windows SDK. Версию для Windows 10 можно найти здесь. Для работы с отладчиком необходимо запустить процесс установки SDK и выбрать только опции с набором инструментов для отладки. Пример выбора представлен на снимке ниже.

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

  • Установить директорию и сервер для отладочных символов Проще всего это сделать можно через меню: File->Symbol File Path. В открывшемся меню нужно прописать вот эту строку: "SRVC:\symbolshttp://msdl.microsoft.com/download/symbols". Затем создать директорию C:\symbols;

  • Установить WorkPlace с удобной раскладкой рабочих панелей. Взять готовый Workspace можно отсюда. В итоге, если запустить для теста notepad.exe в отладчике, он будет выглядеть вот так:

Теперь можно перейти к исследованию команд. Откроем в отладчике файл и приступим.

Набор команд и анализ приложения

Полный справочник по всем командам отладчика можно найти по команде ".hh". Появится справка, как на рисунке ниже. Здесь можно вводить описание или конкретную команду.

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

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

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

Главные команды, которые станут глазами и ушами при исследовании данных в оперативной памяти - d? (b,c,a,p,w,q). Команда показывает дамп памяти по указанному адресу. Можно использовать конкретный адрес или регистр. Например, можно посмотреть как выглядит часть заголовка файла в памяти:

Команда !dh разбирает файл и показывает заголовки. Нам нужен файловый заголовок, поэтому добавим флаг -f. Если необходимо показать все данные о файловых и секционных заголовках, то можно не дополнять команду.

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

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

Расчет адреса.

Дизассемблирование функции от адреса до ret команды.

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

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

Как только алгоритм загрузчика ОС выполнит все подготовительные действия мы увидим в командной строке следующие данные:

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

Для поиска данных будем использовать команду s. Эта команда проводит поиск по определенному в команде объему данных. Соответственно чтобы получить данные о местоположении приглашения к вводу ключа, нужно указать всё адресное пространство исследуемого приложения. Так же необходимо указать тип данных, которые нужно искать.

Теперь, когда мы знаем адрес данных, которые используются, мы можем поставить точку останова, которая будет следить за доступом к данным. Сделать это можно с помощью команды ba. Для такой точки останова нужно указывать размер данных за которыми отладчик будет наблюдать, а там же адрес и тип доступа. Адрес должен быть выровнен по границе в 4 байта. После становки снова нужно запустить приложение через команду g. На рисунке ниже показан вариант команды:

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

Из рисунка видно, что копирование строки для работы с ней выполняется библиотечной функцией, а её вызов был сделан из "For_Crackme+0x15f2".

2. Локализуем функцию проверки ключа. Функция проверки будет недалеко от предложения ввести данные пользователя. В прошлом этапе мы нашли смещение внутри функции до этой операции. Введем можифицированную команду uf - u для того чтобы посмотреть несколько команд после адреса "For_Crackme+0x15f":

Фрагмент кода не содержит дополнительных отладочных символов, поэтому просто просмотрим данные рядом:

  • offset For_Crackme+0x40a2

  • offset For_Crackme+0x40bb

Чтобы это сделать, используем команду db:

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

...00401612 c744241c30372f31 mov     dword ptr [esp+1Ch],312F3730h0040161a c7442420302f3937 mov     dword ptr [esp+20h],37392F30h...

Если раскодировать константы, то мы получим вот такое значение: "07/10/97". Выполнить раскодирование может помочь команда .formats 312F3730h. Из списка форматов нас интересует Char или символьное представление. Стоит помнить, что данные в памяти хранятся в LittleEndian формате, поэтому если прочитать наоборот, то получатся данные необходимые для прохождения валидации.

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


Статья написана в преддверии старта курса "Reverse-Engineering. Professional". Напоминаем о том, что завтра пройдет второй день бесплатного интенсива по теме "Пишем дампер процессов". Записаться на интенсив можно по ссылке ниже:

Подробнее..

Никаких секретов или Frida для Windows

01.12.2020 16:10:27 | Автор: admin

Эксперт OTUS - Александр Колесников поделился с нами полезной статьёй, которую написал специально для студентов курса Reverse-Engineering. Basic.


Приглашаем вас записаться на demo day курса, в рамках которого наши преподаватели подробно расскажут о программе курса, а также ответят на интересующие вас вопросы.


Доброго времени суток! В этой статье будет рассказано, как знание JavaScript и концепций работы современного программного обеспечения могут помочь в reverse engineering, а так же продемонстрировано, как можно использовать тулзу для динамической инструментации для анализа любого ПО на Linux, Windows, Android и iOS.

Frida

Вероятно, многие читали уже про феноменальную космическую мощь сабжевого фреймворка, но вся его слава обычно распространялась на Android и iOS. Об использовании для мобильных платформах можно прочитать здесь, а здесь можно найти даже интересные примеры для конкретных задач. Что же под капотом? На официальном сайте есть вот такая картинка:

Что это значит для конечного пользователя? Для фреймворка можно использовать скрипт для взаимодействия с функциями, которые находятся в движке V8, каким-то способом загруженного в адресное пространство исследуемого процесса, причем загрузка может быть как в процессе исполнения бинарного файла, так и добавление механизма загрузки уровне исходного кода. Касательно способов загрузки в память процесса, Frida может быть использована в 3-х видах:

  • Injected - запуск приложения и автоматическая загрузка V8 движка в память процесса;

  • Embedded - вид загрузки, который используется при патчинге исследуемого приложения. Frida предоставляет библиотеку - frida-gadget, которая может быть использоваться для загрузки в процесс на этапе запуска самим приложением;

  • Preloaded - использование распространенного в Linux подобных операционных системах механизма - операционная система может позволить загрузить сторонний код в память процесса и запустить его на старте.

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

Windows

Начнем практическую часть с изучения приложения под операционную систему Windows. Задание взято отсюда. Приложение представляет собой исполняемый файл и предлагает пройти 5 уровней защиты от отладки. Если запустим, то получаем вот такой вывод:

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

Найдем функцию через перекрестные ссылки. Сначала данные по строкам, которые были на экране. Посмотрим, что есть внутри функций, что должно остановить использование отладчика:

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

Frida-Trace

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

Итак, frida-trace создала файлы с расширением js, которые имеют одинаковые названия с функцией WinAPI и находятся в директории __handlers__. Посмотрим, что находится внутри:

Файл генерируется автоматически и содержит 2 функции, которые синхронно будут выполняться, как только будет вызвана функция WriteFile. Таким образом, можно просматривать параметры, которые передаются в функцию, и данные, которые функция возвращает. Чтобы разобраться, сколько есть переменных, которые передаются в функцию, можно воспользоваться официальной документацией или прибегнуть к дизассемблеру. Воспользуемся первой опцией. Официальная документация содержит следующее определение для функции:

Итого 5 переменных. Мы можем их в общем виде представить как шестнадцатеричные значения с помощью функции log или send. В итоге скрипт приобретет следующий вид:

В результате перехват будет выглядеть так:

Хороший результат, таким образом, мы можем изучить любые данные передаваемые в WinAPI, но что, если нужно просмотреть информацию из функции, которая относится только к приложению? К сожалению, в этом случае frida-trace нам помочь не сможет, но есть и другие инструменты. Так как приложение быстро завершает свою работу, то единственный инструмент, который нам может помочь - frida.

Frida

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

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

Базовые операции с помощью Frida выполняются за счет модулей:

  • Process - функции, которые используются для взаимодействия с характеристиками процессов;

  • Memory - функции, которые позволяют взаимодействовать с объектами в памяти;

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

Используем модули для создания перехвата функции печати строки Your flag is at [address]. Для этого необходимо по перекрестным ссылкам найти адрес, откуда вызывается функция:

На первых строках функции radare2 проставляет информацию об перекрестных ссылках - все 5 ссылок ведут к каждому из функций, которые используются для генерации флагов. Следовательно, можно попробовать получить данные именно из этой функции. Нужный адрес будет передаваться в качестве парного параметра с шаблоном Your flag is at [address]. Для этого найдем смещение для функции, которая принимает в качестве параметров шаблон и адрес.

Итак, нас интересует смещение 0x57с4 от начала исполняемого файла. Чтобы провести мониторинг функции по смещению, нам необходимо узнать базовый адрес исполняемого файла и написать перехват для просмотра данных. Исходный код перехватчика будет следующим:

Запустим frida и продолжим исполнение приложения:

В итоге - мы видим 3 флага, которые были получены даже без анализа каждого из уровней приложения. Неплохой результат за 1 действие. Остальные 2 можно решить используя дополнительные функциии frida.

Native call

Заключительная часть статьи продемонстрирует, как можно получить все флаги, кроме 4го (этот будет на самостоятельное изучение). Frida предоставляет возможность вызывать любые функции исследуемого приложения. Делает она это за счет использования объекта NativeFunction. Чтобы им воспользоваться, достаточно просто определить прототип функции, которую мы хотим вызвать и далее согласно прототипу передать в нее значения. Давайте попробуем вызвать функцию для 1го уровня. Для этого возьмем адрес функции и определим ее прототип. Все вместе это будет выглядеть так:

Запустим код:

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

Итог

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


Узнать о курсе подробнее.


Подробнее..

Frida изучаем эксплуатацию алгоритмов Heap

08.12.2020 18:19:49 | Автор: admin

Эксперт OTUS Александр Колесников поделился с нами полезной статьёй, которую написал специально для студентов курсаReverse-Engineering. Basic


Приглашаем васпосмотреть demo day курса, в рамках которого наши преподаватели подробно рассказали о программе курса и ответили на вопросы.


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

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

Heap

Heap или куча область памяти, которая используется для динамического выделения памяти под большие объекты. Обычно, обращение к этой области осуществляется через вызов функции malloc. Функция занимается запросами на выделение памяти и по сути запускает цель алгоритмов, которые память предоставляют наиболее оптимальным способом и в наиболее оптимальном объеме. Алгоритмы памяти модифицируются с каждой новой версией ядра, поэтому единого алгоритма для действий по выделению памяти нет.

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

  1. heap grooming attack

  2. fastbin attack

  3. tcache attack

  4. unlink

  5. largebin

  6. unsortedbin attack

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

Frida

В наборе инструментов frida нам понадобятся как минимум 2 инструмента: frida-trace и MemoryMonitor. Использовать их будем по мере необходимости, в большинстве случаев хватит и одного инструмента. Изучение будем проводить сначала на примерах, которые будут демонстрировать работу атаки или механизма, и следом будем решать задание из уже завершенных CTF.

Heap Grooming

Атака, которая работает с минимум версии libc 2.23. Атака заключается в том, что можно выделить одинаковые кусочки памяти, высвободить их, и когда аналогичные кусочки будут запрошены, то они будут предоставляться кучей в обратном порядке. Ограничение этой атаки заключается в том, что количество этих кусочков ограничено. В наших зданиях это количество не превышает 7. Для демо этой методики будем использовать следующий код:

#include <stdio.h>#include <stdlib.h>int main(void){  unsigned long int *mem0, *mem1, *mem2;    mem0 = malloc(0x10);  mem1 = malloc(0x10);  mem2 = malloc(0x10);    printf("Allocated memory on:\nptr0: %p\nptr1: %p\nptr2: %p\n\n", mem0, mem1, mem2);    free(mem0);  free(mem1);  free(mem2);    printf("mem0 after free and alloc again: %p\n", malloc(0x10));  printf("mem1 after free and alloc again: %p\n", malloc(0x10));  printf("mem2 after free and alloc again: %p\n\n", malloc(0x10));}

Для компиляции кода не требуется дополнительных опций. И так вывод от скомпилированной версии будет следующим:

В тестовом примере, безусловно все супер, выводятся данные по адресам и мы можем их видеть, но что если в приложении не будет printf? Попробуем сделать такие printf за счет frida-trace. Запустим команду:

Frida-trace -f test -i malloc

После первого прогона нужно модифицировать handler. Он находится в директории handlers откуда был запущен инструмент frida-trace. И файле malloc.js нужно в разделе OnLeave прописать строку, как показано на экране:

А теперь к реальной проблеме. В качестве примера будем использовать задание с pico CTF Are you root. Запустим исследуемое приложение через frida-trace:

Мы не открывали приложение в дизассемблере, а уже можем сказать, что для функции ввода логина используется куча. Судя по значениям, которые выделяются у нас на куче хранятся последовательно значения login; Auth level. Почему так? После ввода данных о логине мы видим последовательный вызов malloc с размером 0x10 и 0x7. Так как минимальное количество данных, которое выделяемся на куче может быть больше, то второе значение помещается в кусочек размера 0x10, просто остаток места не будет содержать полезные данные в результате у нас есть 2 ячейки - с адресами 0x1514eb0 и 0x1514ed0. Написание остального кода эксплойта становится тривиальным.

TCACHE

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

#include <stdio.h>#include <stdlib.h>int main(void){    unsigned long int *mem0, *mem1;  int target;      mem0 = malloc(0x10);    mem1 = malloc(0x10);    target = 0xdead;        printf("mem0: %p\n", mem0);  printf("mem1: %p\n", mem1);  printf("int: %p\n\n", &target);        free(mem0);    free(mem1);     printf("Next pointer for mem1: %p\n\n", (unsigned long int *)mem1);     *mem1 = (unsigned long int)&target; printf("Next pointer for mem1: %p\n\n", (unsigned long int )mem1);         printf("Malloc Allocated: %p\n\n", malloc(0x10));  printf("Malloc Allocated: %p\n\n", malloc(0x10));}

Скомпилируем и посмотрим, что может показать frida-trace с модификациями, которые мы вносили в предыдущий раз:

Несмотря на кажущуюся нереальность атаки, мы видим, что действительно удается выделить память по адресу, который используется для хранения значения переменной на стеке. Об этом говорит последний адрес, который был возвращен функцией malloc. Стоит только упомянуть, что подобные трюки возможны только если в приложении есть уязвимость типа Use-After-Free. Попробуем изучить задание с Plaid CTF cpp. Модифицируем скрипт для трассировки:

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

Теперь будет проще определить откуда вызывается функция и где, потенциально есть уязвимость.

Выводы

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


Узнать подробнее о курсе "Reverse-Engineering. Basic"

Читать ещё:

Подробнее..

Modern Reverse Engineering TTD

14.12.2020 18:12:07 | Автор: admin

Будущих учащихся на курсе "Reverse-Engineering. Basic" и всех желающих приглашаем посмотреть открытый урок на тему "Анализ шелкода".

Также делимся авторской статьей.


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

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

Go

Python

Rust

Опишем некоторые проблемы обратной разработки связанных с каждым из перечисленных языков программирования.

Проблемы RE Go

Язык программирования, который стал популярен по ряду причин. Разработан компанией Google, по синтаксису очень похож на Python и Swift. Используется для написания приложений для Cloud приложений. Используют его в первую очередь из-за того что он заявлен как типобезопасный язык программирования и он частично лишен проблем, которые присущи С++ и С. Вот так выглядит традиционный Hello World:

А вот так он выглядит собранный исполняемый файл, открытый в дизассемблере:

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

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

Python

Язык программирования, который до сегодняшнего дня так и не определился какая версия популярнее ветка 2 или 3. Спор идет в первую очередь потому что большое количество программного обеспечения и инструментов написаны с использованием Python 2. Язык программирования в отличии от всех, которые будут описаны в статье, использует собственный подход для выполнения команд. Он работает на собственной виртуальной машине. Продемонстрируем это на примере:

Дизасемблирование теперь не поможет, так как исполняемый файл состоит сразу из нескольких упакованных файлов, список файлов представим на снимке экрана:

Основной файл с кодом, который писал программист это одноименный файл с расширением pyc. pyc это скомпилированная версия Python скрипта, который был показан выше. Дезассемблированного листинга с нативными командами не представить, поэтому файл main.pyc в шестнадцатеричном редакторе:

Rust

Язык программирования, который призван заменить C/C++. Позиционируется как типобезопасный. Исходник Hello World:

Количество функций меньше, чем в Go:

Всё создается сразу в нативных командах:

Кстати, несмотря на то, что это main функция программист её не пишет. Эту функцию генерирует компилятор, а вот функция, которая в исходном виде была представлена выше, находится по выделенному на снимке адресу:

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

TTD

Time Travel Debugging маховик времени для реверс инженера, все действия приложения записываются и можно просто отматывать на тот момент, который хочется проанализировать и увидеть что конкретно происходило в каждую команду приложения. Самый доступный из всех представленных на сегодняшний день отладчиков, которые могут продемонстрировать эту функцию WinDBG Preview. Для использования этой чудной функции будем использовать следующие основные команды:

!tt- команда Time Travel позволяет двигаться по всей записи

g/g- go или выполнить приложение дальше

t/t- trace или выполнить по шагам, входя в функции и выполняя каждую команду

Начнем анализ с Crackme на языке программирования Go. Внешний вид приложения:

Ничего необычного, консольная версия Crackme. Файл не содержит отладочных данных, а значит исследовать придется в дефиците данных о функциях, которые выполняются для работы приложения. Запустим файл и приаттачимся из отладчика:

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

Crackme Go

Записанную последовательность команд помещаем в отладчик и нас приветствует вот такой интерфейс:

Начнем путешествие во времени и перейдём к точке !tt 40. При изучении алгоритмов с использованием TTD стоит сначала просмотреть середину трассировки и ее окончание, обычно именно там есть что-то интересное. Начало и конец пропускается на первых этапах так как там очень много системных вызовов работы операционной системы. Чтобы определить где находится настоящая main функция, которая включает код программиста нужно:

1. Выяснить базовый адрес запущенного файла

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

Пункт 1 можно ввести команду lm покажет список модулей и их базовые адреса:

Теперь воспользовавшись функцией Step Out 3 раза можно найти вот такую функцию:

Для решения можно использовать тот факт, что ABI Windows x64 предполагает, что данные, которые передаются в качестве аргументов функции, должны находиться в регистрах общего назначения. Если просматривать их до вызова функции, то можно предположить какие данные используются для проверки ключа и, если повезет, то можно найти сам ключ. Чтобы не искать по функциям вручную, поставим точки останова на каждую инструкцию call внутри функции main. При использовании команд g/g- точки останова будут срабатывать как в одну так и в другую сторону, поэтому если случайно потеряетесь в функциях, всегда можно набрать команду g- чтобы остановиться на последней точке останова.

Результат наших исследований в итоге обнаруживается по смещению 0x1c303:

Попробуем ввести это значение:

Crackme Python

Для анализа используем вот это CrackMe. Как было уже сказано выше, анализ приложения, которое написано на Python усложнено тем, что все команды выполняет виртуальная машина. Попробуем записать и ее действия через TTD:

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

Crackme Rust

Для анализа будем использовать вот это Crackme. В этом случае повезло больше, так как Rust все же много берет от шаблонов, которые использует C и C++ при создании исполняемого файла. В частности, найти его main функцию не так уж и сложно. Основная точка входа находится здесь: !tt 3C:12

Команды которые используются на точке входа типичны для компилятора С++ от Microsoft. Поэтому дальше приложение на Rust можно анализировать как и любое другое приложение написанное на C++: !tt 124:133. Теперь можно повторить те же операции, которые мы выполняли для Crackme на Go проставить точки останова на интересные функции и узнать ключ:

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

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

Вывод

TTD весьма упрощает процесс анализа даже самого запутанного алгоритма. Не вообразить сколько часов он может помочь сэкономить при анализе какой-нибудь сложной функции. Но ничего не бывает бесплатно и просто, что стоит помнить, что:

A. Запись возможна только для User Mode

B. Изменять ничего в процессе исследования нельзя

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


Узнать подробнее о курсе "Reverse-Engineering. Basic"

Посмотреть открытый урок на тему "Анализ шелкода".


ЗАБРАТЬ СКИДКУ

Подробнее..

Создаем процессорный модуль под Ghidra на примере байткода v8

23.04.2021 18:05:42 | Автор: admin

В прошлом году наша команда столкнулась с необходимостью анализа байткода V8. Тогда еще не существовало готовых инструментов, позволявших восстановить такой код и обеспечить удобную навигацию по нему. Было принято решение попробовать написать процессорный модуль под фреймворк Ghidra. Благодаря особенностям используемого языка описания инструкций на выходе мы получили не только читаемый набор инструкций, но и C-подобный декомпилятор. Эта статья продолжение серии материалов (1, 2) о нашем плагине для Ghidra.

Между написанием процессорного модуля и статьи прошло несколько месяцев. За это время спецификация SLEIGH не изменилась, и описанный модуль работает на версиях 9.1.29.2.2, которые были выпущены за последние полгода.

Сейчас на ghidra.re и в приложенной к Ghidra документации есть достаточно хорошее описание возможностей языка эти материалы стоит почитать перед написанием своих модулей. Отличными примерами могут быть уже готовые процессорные модули разработчиков фреймворка, особенно если вы знаете описываемую в них архитектуру.

В документации можно прочесть, что процессорные модули для Ghidra пишутся на языке SLEIGH, который произошел от языка SLED (Specification Language for Encoding and Decoding) и разрабатывался целенаправленно под Ghidra. Он транслирует машинный код в p-code (промежуточный язык, используемый Ghidra для построения декомпилированного кода). Как у языка, предназначенного для описания инструкций процессора, у него достаточно много ограничений, которые, однако, можно купировать за счет механизма внедрения p-code в java-коде.

Исходный код созданного процессорного модуля представлен на github. В этой статье будут рассматриваться принципы и ключевые понятия, которые использовались при разработке процессорного модуля на чистом SLEIGH на примере некоторых инструкций. Работа с пулом констант, инъекции p-code, анализатор и загрузчик будут или были рассмотрены в других статьях. Также про анализаторы и загрузчики можно почитать в книге The Ghidra Book: The Definitive Guide.

С чего начать

Для работы понадобится установленная среда разработки Eclipse, в которую нужно добавить плагины, поставляемые с Ghidra: GhidraDev и GhidraSleighEditor. Далее создается Ghidra Module Project с именем v8_bytecode. Созданный проект содержит шаблоны важных для процессорного модуля файлов, которые мы будем модифицировать под свои нужды.

Чтобы получить общее представление о файлах, с которыми предстоит работать, обратимся к официальной документации либо вышедшей недавно книге Криса Игла и Кары Нанс The Ghidra Book: The Definitive Guide. Вот описание этих файлов.

  • *.сspec спецификация компилятора.

  • *.ldefs определение языка. Содержит отображаемые в интерфейсе параметры процессорного модуля. Также содержит ссылки на файлы *.sla, спецификацию процессора и спецификации компилятора.

  • *.pspec спецификация процессора.

  • *.opinion конфигурации для загрузчика; поскольку мы будем описывать только один вид файлов, файл opinion можно оставить пустым: он не пригодится.

  • *.slaspec, *.sinc файлы, описывающие регистры и инструкции процессора на языке SLEIGH.

Также после первого запуска вашего проекта появится файл с расширением .sla, он генерируется на основании slaspec-файла.

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

О регистрах V8

Jsc-файл, который нас интересовал, был собран c использованием среды выполнения JavaScript Node.Js 8.16.0 через bytenode (этот модуль либо будет в поставке Node.Js, либо нужно будет доставить его через npm). По сути, bytenode использует документированный функционал Node.js для создания скомпилированного файла. Вот исходный код функции, компилирующей jsc файлы из js:

Node.js можно скачать как в собранном виде, так и в виде исходников. При детальном изучении исходных файлов и примеров инструкций становится ясно, как кодируются регистры в байткоде (для понимания расчета индекса будут полезны файлы bytecode-register.cc, bytecode-register.h). Примеры инструкций v8 с расчетами индексов регистров в соответствии с Node.js:

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

Тут Х количество аргументов текущей функции без учета передаваемого <this>, aX регистры, содержащие аргументы функции, а rN регистры, используемые как локальные переменные. Регистры могут кодироваться 1-байтовыми значениями для обычных инструкций, 2-байтовыми для инструкций с пометкой Wide- и 4-байтовыми для инструкций с пометкой ExtraWide-. Пример кодировки Wide-инструкции с пояснениями:

Более подробно о Node.js и v8 можно почитать в статье Сергея Федонина.

Стоит заметить, что SLEIGH не совсем подходит для описания подобных интерпретируемых байткодов, поэтому у написанного процессорного модуля есть некоторые ограничения. Например, определена работа не более чем с 124регистрами rN и 125регистрами aX. Была попытка решить эту проблему через стековую модель взаимодействия с регистрами, так как она больше соответствовала концепции. Однако в этом случае дизассемблированный байткод тяжелее читался:

Также без введения дополнительных псевдоинструкций, регистров или областей памяти не представляется возможным высчитывать название регистра аргумента в соответствии с Node.js из-за отсутствия информации о количестве аргументов. В связи с этим нами было принято решение проставлять номера в названии регистров аргументов функций (X в aX) в обратном порядке. Это не мешает разбору кода, что было для нас важным критерием, однако может смущать при сравнении результатов вывода инструкций файла в разных инструментах.

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

CSPEC

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

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

Также становится понятно, что теги используются для следующих целей:

  • Compiler Specific P-code Interpretation;

  • Compiler Datatype Organization (у нас использовался <data_organization>);

  • Compiler Scoping and Memory Access (у нас использовался <global>);

  • Compiler Special Purpose Registers (у нас использовался <stackpointer>);

  • Parameter Passing (у нас использовался <default_proto>).

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

Теги <data_organization> и <stackpointer> достаточно типовые; разберем тег <prototype> в <default_proto>, частично описывающий соглашение о вызове функций. Для него определим: <input>, <output>, <unaffected>.

Как говорилось выше, аргументы в функцию передаются через регистры aX. В модуле регистры должны быть определены как непрерывная последовательность байтов по смещению в некотором пространстве. Как правило, в таких случаях используется специально придуманное для этого пространство register. Однако теоретически не запрещено использовать любое другое. В случае наличия большого количества регистров, выполняющих примерно одни функции, проще всего не прописывать каждый отдельно, а просто указать смещение в пространстве регистров, по которому они будут определены. Поэтому в спецификации компилятора помечаем область памяти в пространстве регистров (space="register") в теге <input> для регистров, через которые происходит передача аргументов в функции, по смещению 0x14000 (0x14000 не несет в себе сакрального смысла, это просто смещение, по которому в *.slaspec далее будут определены регистры aX).

По умолчанию результат вызова функций сохраняется в аккумулятор (acc), что нужно прописать в теге <output>. Для альтернативных вариантов регистров, в которые происходит сохранение возвращаемых функциями значений, можно определить логику при описании инструкций. Отметим в теге <unaffected>, что вызовы функций на регистр, хранящий указатель на стек, не влияют.

Для работы с частью регистров наиболее удобным будет вариант определения их как изменяемых глобально, поэтому в теге <global> определяем диапазон регистров в пространстве register по смещению 0x2000.

LDEFS

Перейдем к определению языка это файл с расширением .ldefs. Он требует немного информации для оформления: порядок байт (у нас le), названия ключевых файлов (*.sla, *.pspec,*.cspec), id и название байткода, которое будет отображаться в списке поддерживаемых процессорных модулей при импорте файла в Ghidra. Если когда-то понадобится добавить процессорный модуль для файла, скомпилированного версией Node.js, существенно отличающейся от текущей, то имеет смысл описать его тут же через создание еще одного тега <language>, как это сделано для описания семейств процессоров в *.ldefs модулей, поставляемых в рамках Ghidra.

Практическое применение информации, не касающейся определения файлов, будет видно при попытке импорта файла.

PSPEC

Сложнее в плане документации дела обстоят со спецификацией процессора (файл с расширением .pspec). В данном случае можно обратиться к готовым решениям в рамках самого фреймворка или к файлу processor_spec.rxg (вариант с полноценным разбором исходных кодов Ghidra мы не рассматривали). Чего-то более подробного на момент написания модуля не было. Вероятно, со временем разработчики опубликуют официальную документацию.

В текущем проекте на данный момент от спецификации процессора может понадобиться только программный счетчик, оставим этот тег из стандартного шаблона свежесозданного проекта (на самом деле можно оставить <processor_spec> пустым).

SLASPEC

Теперь можно приступить к непосредственному описанию инструкций на SLEIGH в файле с расширением .slaspec.

Базовые определения и макросы препроцессора

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

Адресные пространства, которые понадобятся для описания байткода (у нас создаются пространства с именами register и ram), определяются через define space, а регистры через define register. Значение offset в определении регистров не принципиально, главное, чтобы они находились по разным смещениям. Занимаемое регистрами количество байтов определяется параметром size. Стоит помнить, что определенная тут информация должна соответствовать обращениям к аналогичным абстракциям и величинам в рамках *.cspec и анализатора, если вы ссылаетесь на эти регистры.

Описание инструкций

В документации (https://ghidra.re/courses/languages/html/sleigh_constructors.html) можно прочитать, что определение инструкций происходит через таблицы, которые состоят из одного и более конструкторов и имеют имена идентификаторы символов семейства. Таблицы в SLEIGH по сути являются тем, что называется символами семейства, в статье мы не будем углубляться в определения символов, для этих целей проще прочитать Знакомство с символами. Конструкторы состоят из 5частей.

  1. Table Header (заголовок таблицы)

  2. Display Section (секция отображения)

  3. Bit Pattern Sections (секция битового шаблона)

  4. Disassembly Actions Section (секция действий при дизассемблировании инструкций)

  5. Semantics Actions Section (семантическая секция)

Пока что звучит страшно, опишем основной смысл.

  1. В Table Header либо помечается идентификатор, который можно использовать в других конструкторах, либо ничего нет (тогда это непосредственно описание инструкции).

  2. Display Section шаблон, показывающий как выводить инструкцию в листинг Ghidra.

  3. Bit Pattern Section перечень каких-либо идентификаторов, которые забирают реальные биты программы под инструкцию и выводятcя в листинг по шаблону секции отображения (иногда с использованием следующей секции).

  4. Disassembly Actions Section дополняет секцию битового шаблона какими-то вычислениями, если ее в чистом виде недостаточно.

  5. Semantics Actions Section описывает, что делает эта инструкция по смыслу, чтобы показать это в декомпиляторе.

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

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

Несколько документированных особенностей секции отображения, которые понадобятся дальше:

  • ^ разделяет идентификаторы и/или символы в секции, между которыми не должно быть пробелов;

  • используются, чтобы вставлять жестко закодированные строки, которые не будут считаться идентификатором;

  • пробельные символы обрезаются в начале и конце секции, а их последовательности сжимаются в один пробел;

  • некоторые знаки пунктуации и спецсимволы вставляются в шаблон (неиспользуемые для каких-то определенных функций, в отличие от, например#, которые применяются для комментариев).

Токены и их поля

Для описания конструкторов инструкций необходимо определить битовые поля. Через них осуществляется привязка битов программы к определенным абстракциям языка, в который будет происходить трансляция. Такими абстракциями могут быть мнемоники, операнды и т.п. Определение полей происходит в рамках задания токенов, синтаксис их определения выглядит так:

Размер токена tokenMaxSize должен быть кратен8. Это может быть неудобно, если операнды или какие-то нюансы для инструкции кодируются меньшим количеством бит. С другой стороны, это компенсируется возможностью создавать поля разных размеров, кодирующих позиционно любые биты в пределах размеров, задаваемых токеном. Для таких полей должны соблюдаться условия: start- и endBitNumX находятся в диапазоне от 0 до tokenMaxSize-1 включительно и startBitNumX <= endBitNumX.

Для разбираемого байткода v8 не было необходимости создавать поля, отличные по размеру от токена. Но, если бы такие поля были и использовались совместно, они бы объединялись через логические операторы & или |.

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

Теперь опишем простейшую инструкцию байткода, не имеющую операндов. Определим поле, которое будет описывать опкод инструкции. Как видно выше в разделе про v8, код инструкции описывается одним байтом (есть также Wide- и ExtraWide- инструкции, но они здесь не будут рассматриваться, по сути они просто используют операнды больших размеров и дополнительные байты под опкод инструкции). Таким образом получаем:

Теперь, используя поле op для идентификации первого и единственного опкода, определяющего инструкции Illegal и Nop, пишем для них конструкторы:

Байт 0xa7 в листинге Ghidra отобразит как инструкцию Illegal, не имеющую операндов. Для этой инструкции в примере использовалось ключевое слово unimpl. Это неимплементированная команда, дальнейшая декомпиляция будет прервана, что удобно для отслеживания нереализованных семантических описаний. Для Nop оставлена пустая семантическая секция, то есть команда не повлияет на отображение в декомпиляторе, что и должна делать эта инструкция. На самом деле Nop не присутствует как инструкция в Node.js нашей версии, мы ввели ее искусственно для реализации функционала SwitchOnSmiNoFeedback, но об этом будет рассказано в статье Владимира Кононовича.

Описываем операнды и семантику

Усложним концепцию: опишем конструктор для операций LdaSmi, в рамках которой происходит загрузка целого числа в аккумулятор (acc в определении пространства регистров), и AddSmi, которая по сути представляет собой сложение значения в аккумуляторе c целым числом.

Для текущих и будущих нужд определим чуть больше полей на манер операндов в bytecodes.h Node.js, создадим их в новом токене с именем operand, поскольку у этих полей будет другое назначение. Создание нескольких полей с одинаковыми битовыми масками может быть обусловлено как удобством восприятия, так и использованием нескольких полей одного токена в рамках одной инструкции (см. пример с AddSmi).

С точки зрения листинга хочется видеть что-то наподобие LdaSmi [-0х2]. Поэтому определяем в секции отображения мнемонику, а в шаблон прописываем имена полей, которые должны подставляться из секции disassembly action или битового шаблона (квадратные скобки тут не обязательны, это просто оформление).

Для инструкции AddSmi в секции битового шаблона, помимо поля op, устанавливающего ограничение на опкод, через ; появляются поля из токена operand. Они будут подставлены в секцию отображения в качестве операндов. Маппинг на реальные биты происходит в том порядке, в котором поля указаны в секции битового шаблона. В семантической секции, используя документированные операции, реализуем логику инструкций (то, что делал бы интерпретатор, выполняя эти инструкции).

Через ; могут также, например, идти регистры, контекстные переменные (о них поговорим позже), комбинации полей одного токена или полей с контекстными переменными.

Вот так выглядит окно листинга с описанными инструкциями со включенным полем PCode в меню изменение полей листинга Ghidra. Окно декомпилятора пока что не будет показательным из-за оптимизации кода, поэтому на данном этапе стоит ориентироваться только на промежуточный p-code.

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

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

Выводим регистры по битовым маскам

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

В разделе Базовые определения и макросы препроцессора регистры уже были объявлены, но для того, чтобы нужные регистры выбирались в зависимости от представленных в байткоде бит, необходимо привязать их список к соответствующим битовым маскам. Поле kReg имеет размер 8бит. Через конструкцию attach variables последовательно определяем каким битовым маскам от 0b до 11111111b вышеприведенные регистры будут соответствовать в рамках последующего использования полей из заданного списка (в нашем случае только kReg) в конструкторах. Например, в этом описании видно, что операнд, закодированный как 0xfb (11111011b), интерпретируется при описании его через kReg как регистр r0.

Теперь, когда за переменной kReg закреплены регистры, ее можно использовать в конструкторах:

Усложним конструкцию для соответствия конструктора более высокоуровневым описаниям инструкций из interpreter-generator.cc исходников Node.js. Вынесем поле kReg в отдельный конструктор, идентификатор таблицы которого в Table Header назовем src. В его семантической секции появляется новое ключевое слово export. Если не вдаваться в детали построения p-code, то по смыслу export определяет значение, которое должно быть подставлено в семантическую секцию конструктора вместо src. Вывод в Ghidra не изменится.

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

Переходы по адресам с goto

В байткоде встречаются операции условного и безусловного перехода по смещению относительно текущего адреса. Для перехода по адресу или метке в SLEIGH используется ключевое слово goto. Примечательно для определения то, что в секции битового шаблона используется поле kUImm, однако оно не используется в чистом виде. В секцию отображения выводится просчитанное в disassembly action секции значение через идентификатор rel. Величина inst_start предопределена для SLEIGH и содержит адрес текущей инструкции.

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

Воспользуемся рекомендуемым разработчиками способом и вынесем часть определения через создание дополнительного конструктора с идентификатором dest. Конструкция *[ram]:4 rel не обозначает, что мы берем 4байта по адресу rel. По факту экспортируется адрес rel в пространстве ram. Оператор * в SLEIGH обозначает разыменование, но в данном конкретном случае относится к нюансу создания варнод (подробнее в Dynamic References).

Указание пространства [ram] может быть опущено (пример в комментарии), так как при определении мы указали его пространством по умолчанию. Как видно в инструкциях p-code, смещение было помечено как принадлежащее ram.

Чуть сложнее выглядит инструкция JumpIfFalse из-за использования условной конструкции. В SLEIGH она используется вместе с ключевым словом goto. Для большего соответствия концепциям js величина False ранее была определена как регистр, и можно заметить, что в pspec диапазон пространства регистров, к которому она привязана, помечен как глобальный. Благодаря этому в псевдокоде она отображается в соответствии с именованием регистра, а не численным значением.

В рассмотренных примерах переход осуществляется по рассчитываемому относительно inst_start адресу. Рассмотрим инструкцию TestGreaterThan, в которой происходит переход с помощью goto к метке (<true> в примере ниже) и inst_next. Переход к метке в принципе должен быть интуитивно понятным: если условие истинно, то далее должны выполняться инструкции, следующие за местом ее расположения. Метка действительна в только в пределах ее семантической секции.

Конструкция goto inst_next фактически завершает обработку текущей инструкции и передает управление на следующую. Стоит обратить внимание, что для выполнения знакового сравнения используется s>, см. документацию.

Несколько регистровых операндов

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

Описание однотипных операндов через конструкторы, имеющие разные идентификаторы таблиц (см. конструкторы в примере ниже), может иметь практическое применение для использования в рамках одного конструктора корневой таблицы. Такой вариант применим не только с точки зрения соответствия описанию инструкций v8, но и для преодоления возможных ошибок. Например, 4операнда инструкции CallProperty2 являются регистрами, идентично задаваемыми с точки зрения битовой маски. Попытка определить конструктор как :CallProperty2 kReg, kReg, kReg, kReg, [kIdx] вызовет ошибку в компиляторе Sleigh при попытке открыть файл с помощью процессорного модуля. Поэтому в нашем модуле использовались конструкторы для создания чего-то наподобие алиасов:

Стоит отметить, конечно, что решить эту проблему также можно было без определения новых конструкторов. Например, определив и прописав поля callable, receiver, arg1 и arg2 в рамках какого-либо токена с последующей их привязкой через attach к списку регистров:

Каждое из этих полей работало бы аналогично kReg в предыдущих примерах. Какой именно способ использовать вопрос эстетики.

Вызовы функций

В инструкции CallProperty2 также примечательно то, что она в семантической секции использует конструкцию call [callable];, которую мы не использовали до этого. В v8 аргументы функции хранятся в регистрах aX (как мы и пометили в cspec). Однако, с точки зрения байткода, помещения туда аргументов непосредственно перед вызовом функции не происходит (случаи, когда это происходит, можно посмотреть в sinc-файле, например для x86). Интерпретатор делает это самостоятельно, ведь у него есть вся необходимая информация. Но ее нет у Ghidra, поэтому в семантической секции мы пропишем помещение аргументов в регистры вручную. Однако нам необходимо будет восстановить значения задействованных регистров после вызова, так как в вызывающей функции эти регистры тоже могут хранить какие-то значения, необходимые для потока выполнения. Можно сохранить их через локальные переменные:

Можно также применять вариант с сохранением аргументов в памяти (в данном случае на стеке: sp не используется инструкциями, потому не повлияет на отображение в декомпиляторе) при использовании макросов на примере CallUndefinedReceiver1:

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

Определяемые пользователем операции

Чтобы не терять при декомпиляции инструкции, в которых не планируется или нет возможности описывать семантический раздел, можно использовать определяемые пользователем операции. Стоит отметить, что в ситуации с acc не требуется явно указывать размер, поскольку размер регистра определен явно, а использовать его не полностью тут не нужно. Однако при передаче в подобные пользовательские операции, например, числовых констант придется явно указывать размер передаваемого значения (как в примере с CallVariadicCallOther в разделе О диапазонах регистров далее по тексту). Пользовательские операции определяются как define pcodeop OperationName и используются в семантическом разделе конструкторов в формате, напоминающем вызов функции во многих языках программирования.

Эти операции могут использоваться для внедрения p-code-инструкций в анализаторе: вызовы добавляются через тег callotherfixup в cspec-файл и прописывается логика в анализаторе.

Без переопределения в java-коде пользовательские операции в декомпиляторе выглядят так же, как они определены в семантическом разделе:

Тестируем модуль

Уже на этом этапе можно попробовать проверить работу процессорного модуля на практике. Скомпилируем через bytenode jsc-файл из небольшого примера на js:

Попробуем запустить написанный на основании статьи проект и импортировать полученный jsc-файл в Ghidra. Если что-то описано неправильно, Ghidra выдаст ошибку, а в логах eclipse будет локализирован номер строки, вызвавшей ошибку. При исправлении кода не нужно перестраивать проект: sleigh перекомпилируется при следующей попытке открыть файл.

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

Поскольку мы описывали только инструкции, файл не будет проанализирован и разобран по функциям. Код находится по смещению 0х1с0. НажмемD для преобразования байтов в инструкции по этому смещению иF, чтобы создать функцию. Вот так она будет выглядеть:

Более понятным становится вывод при использовании других средств (помимо SLEIGH), доступных разработчикам модулей. Например, при добавлении работы с пулом констант (для обращения к нему в SLEIGH зарезервировано ключевое слово cpool) появится возможность разрезолвить числовой идентификатор в команде LdaGlobal. Вот так в последней версии нашего проекта выглядит функция (для сравнения):

Разумеется, было бы приятнее видеть большее соответствие исходному коду, написанному на JavaScript, однако этого нельзя добиться, описывая инструкции только в файлах .slaspec (и .sinc). Чуть больший простор для воображения откроет статья, в которой будет описан механизм внедрения операций p-code, позволяющий при полном доступе ко всем ресурсам приложения манипулировать инструкциями, из которых собирается дерево p-code. Как раз на основании созданного дерева p-code результат декомпиляции выстраивается и отображается в интерфейсе.

О диапазонах регистров

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

На основании описания понятно, что достаточно простым решением было бы отобразить при разборе инструкций первый регистр и их количество, то есть примерно так: ForInPrepare r9, r10!3. Чуть большим компромиссом в пользу читаемости было бы выводить первый и последний регистры диапазона, но, забегая вперед, можно сказать, что с точки зрения реализации уже это потребовало бы использования таблиц, состоящих из нескольких конструкторов.

Таблицы, содержащие несколько конструкторов

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

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

Как можно предположить, глядя на побайтовое описание инструкции CallProperty выше, для раскрытия диапазона необходимо распечатать регистры, отталкиваясь от первого вхождения, ориентируясь на известный первый регистр диапазона и количество элементов в нем. С точки зрения секции отображения, диапазон создается из двух конструкторов: rangeSrc и rangeDst. rangeSrc своего рода инициализация, где мы сохраняем входные данные, rangeDst будет распечатывать регистры на основании полученной информации. И как раз для rangeDst понадобится создавать таблицы, содержащие несколько конструкторов: как минимум для отображения диапазонов на регистрах aX и rX отдельно.

Для реализации условий необходимо учесть ряд ограничений. Проверять значения в секции битового шаблона рекомендуется только через =, а уточнять тут значение напрямую регистра нельзя, как и присваивать ему значения в секции disassembly action. Это лишает нас возможности использовать какой-то временный регистр. Стартовый регистр и длина диапазона могут быть любыми, а реализоваться, как уже упоминалось, диапазон может как на регистрах aX, так и на rX, а также быть нулевой длины. Уже на этом этапе понятно: если мы не хотим создавать гигантское количество определений на все случаи жизни, было бы неплохо иметь некие счетчики, чтобы выяснить, сколько регистров выводить и с какой позиции.

Контекстные переменные

Для решения задачи подходят контекстные переменные. Их определение похоже на определение полей токенов. Но поля в данном случае используют не реальные биты программы, а биты указанного регистра (contextreg ниже).

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

Важно отметить, что контекстные переменные должны объявляться до конструкторов.

В документации поясняется, что контекстные переменные, как правило, используются в секции битовых шаблонов для проверки наличия какого-то контекста и изменяются в секции disassembly action. Так что в конструкторе с идентификатором таблицы rangeSrc, которую мы будем использовать для отображения диапазонов, в disassembly action секции сохраняем код первого регистра диапазона в контекстную переменную offStart, а их количество в counter. В секции отображения обозначаем начало диапазона открывающейся скобкой {.

Также стоит отметить, что в v8 не используется регистр range_size: он введен искусственно для хранения размера диапазона, чтобы было удобнее работать с этим значением в рамках семантической секции конструктора инструкции. Именно rangeSrc поставляет стартовый регистр и размер диапазона для семантической секции инструкции.

В рамках таблицы с идентификатором rangeDst описано 5конструкторов для следующих случаев.

  • Код стартового регистра диапазона соответствует a0 и счетчик counter равен 0 (пустой диапазон).

  • Код стартового регистра диапазона соответствует r0 и счетчик counter равен 0 (пустой диапазон).

  • Код регистра диапазона в offStart совпадает с a0, в disassembly action секции счетчик counter уменьшается, код регистра в offStart увеличивается, переход к конструктору rangedst1.

  • Код регистра диапазона в offStart совпадает с r0, в disassembly action секции счетчик counter и код регистра в offStart уменьшаются, переход к конструктору rangedst1.

  • Код стартового регистра диапазона пока не найден, переход к конструктору rangedst1(для этого случая в конструкторе нет никаких условий, как упоминалось выше, он будет браться, когда остальные конструкторы не подошли).

В третьем и четвертом случаях происходит вывод регистра в отображение. Последующие конструкторы rangeDstN, где N натуральное число, состоят из тех же вариантов, только для регистров aN/rN.

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

В примере ниже описаны только rangeDst, rangeDst1, rangeDst2, чтобы не загромождать статью. Для получения представления о виде подобных таблиц этого достаточно, полную версию можно посмотреть в исходниках проекта на github. По сути, при работе с rangeDst будет проходиться цепочка конструкторов по возрастанию индекса Х в rangeDstX, пока не встретится стартовый регистр, а затем цепочка конструкторов, соответствующая по длине размеру выводимого диапазона.

Конструкторы с закрывающими фигурными скобками выглядят похожими, можно попробовать их объединить. Вспоминаем про использование логических операторов & и |.

Конструктор для CallProperty в готовом проекте выглядит так:

Вот что получается в листинге:

Возможно, сейчас сбивает с толку, что в семантической секции используется пользовательская операция CallVariadicCallOther. В проекте на github она была переопределена в java-коде инструкциями p-code. Использование инъекции p-code вместо реализации через операцию call было обусловлено желанием видеть список передаваемых аргументов в декомпиляторе (согласно исходникам Node.js, первый регистр диапазона является приемником, а остальные передаваемыми аргументами). Используя только slaspec, добиться этого было бы, мягко говоря, тяжело:

Если есть желание попробовать повторить реализацию диапазонов самостоятельно, можно описать семантику как:

Затем по аналогии можно доопределить конструкторы rangeDstХ (понадобится до r7 включительно) и уже тогда попробовать посмотреть, как выглядит скомпилированный код console.log(1,2,3,4,5,6). Можно собрать его самостоятельно через bytenode или забрать готовый тут. Функция будет находиться по смещению 0x167, а сама инструкция на 0x18b.

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

Стоит отметить, что в нашем проекте мы вынесли все конструкторы rangeDst в отдельный файл, чтобы не загромождать файл с описанием инструкций (как и расширенные инструкции, работающие операндами размером 2 и 4байта):

Итог

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

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

Автор: Наталья Тляпова

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

  1. https://ghidra.re/courses/languages/html/sleigh.html документация на SLEIGH.

  2. https://github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Framework/SoftwareModeling/data/languages полезные файлы с описаниями *.cspec, *.pspec, *.opinion, *.ldefs.

  3. https://spinsel.dev/2020/06/17/ghidra-brainfuck-processor-1.htmlхорошая статья о реализации модуля для brainfuck в Ghidra.

  4. https://github.com/PositiveTechnologies/ghidra_nodejs репозиторий с полной версией процессорного модуля для Ghidra с загрузчиком и анализатором.

Подробнее..

Создание исполняемого файла ELF вручную

22.02.2021 14:21:33 | Автор: admin

Привет, класс, и добро пожаловать в x86 Masochism 101. Здесь вы узнаете, как использовать опкоды непосредственно для создания исполняемого файла, даже не прикасаясь к компилятору, ассемблеру или компоновщику. Мы будем использовать только редактор, способный изменять двоичные файлы (т.е. шестнадцатеричный редактор), и chmod, чтобы сделать файл исполняемым.

Если это вас не заводит, то я не даже не знаю...

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

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

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

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

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

Прежде чем мы начнем практиковаться, убедитесь, что у вас есть настоящий шестнадцатеричный редактор на вашем компьютере, и что вы можете запускать двоичные файлы ELF и вы используете компьютер архитектуры x86. Большинство шестнадцатеричных редакторов должны работать и позволять редактировать и сохранять вашу работу - мне лично нравится Bless. Если вы работаете в Linux, с двоичными файлами ELF все будет в порядке. Некоторые другие Unix-подобные операционные системы тоже могут работать, но разные ОС реализуют вещи немного по-разному, поэтому я не могу быть уверен. Я также широко использую системные вызовы, что еще больше ограничивает совместимость. Если вы используете Windows, вам не повезло. Точно так же, если архитектура вашего процессора отличается от x86 (хотя x86_64 должна работать), поскольку я просто не могу предоставить коды операций для каждой архитектуры.

Создание исполняемого файла ELF состоит из трех этапов. Сначала мы создадим фактическую полезную нагрузку (payload), используя опкоды. Во-вторых, мы создадим заголовки ELF и program header table, чтобы превратить эту полезную нагрузку в рабочую программу. Наконец, мы убедимся, что все смещения и виртуальные адреса верны, и заполним последние пробелы.

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

Создание полезной нагрузки

Давайте постараемся сделать полезную нагрузку простой, но достаточно сложной, чтобы быть интересной. Наша полезная нагрузка должна вывести "Hello World!" на экран, затем выйти с кодом 93. Это сложнее, чем кажется. Нам понадобится как текстовый сегмент (содержащий исполняемые инструкции), так и сегмент данных (содержащий строку Hello World! и некоторые другие второстепенные данные). Давайте посмотрим на ассемблерный код, который нам нужен для этого:

(text segment)mov ebx, 1mov eax, 4mov ecx, HWADDRmov edx, HWLENint 0x80mov eax, 1mov ebx, 0x5Dint 0x80

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

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

0xBB 0x01 0x00 0x00 0x000xB8 0x04 0x00 0x00 0x000xB9 0x** 0x** 0x** 0x**0xBA 0x0D 0x00 0x00 0x000xCD 0x800xB8 0x01 0x00 0x00 0x000xBB 0x5D 0x00 0x00 0x00 0xCD 0x80

(Здесь звёздочки обозначают виртуальные адреса. Мы их еще не знаем, поэтому пока оставим их пустыми)

Вторая часть полезной нагрузки состоит из сегмента данных, который на самом деле представляет собой просто строку Hello World!\n. Используйте таблицу преобразования ASCII ('man ascii'), чтобы преобразовать эти значения в шестнадцатеричный формат, и вы увидите, как мы получим следующие данные:

(data segment)0x48 0x65 0x6C 0x6C 0x6F 0x20 0x57 0x6F 0x72 0x6C 0x64 0x21 0x0A

И вот наша полезная нагрузка готова!

Создание заголовков

Вот где это может очень быстро стать очень сложным. Я объясню некоторые из наиболее важных параметров в процессе построения заголовков, но вы, вероятно, захотите внимательно взглянуть на несколько более подробное руководство, если вы когда-нибудь собираетесь создавать заголовки ELF полностью самостоятельно. Заголовок ELF имеет следующую структуру, размер в байтах в скобках:

e_ident(16), e_type(2), e_machine(2), e_version(4), e_entry(4), e_phoff(4),e_shoff(4), e_flags(4), e_ehsize(2), e_phentsize(2), e_phnum(2), e_shentsize(2)e_shnum(2), e_shstrndx(2)

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

e_ident (16) - этот параметр содержит первые 16 байтов информации, которая идентифицирует файл как файл ELF. Первые четыре байта всегда содержат 0x7F, 'E', L ', F'. Байты с пятого по седьмой содержат 0x01 для 32-битных двоичных файлов на машинах с little-endian. Байты с восьмого по пятнадцатый являются заполнителями, поэтому они могут быть 0x00, а шестнадцатый байт содержит длину этого блока, поэтому он должен быть 16 (= 0x10).

e_type (2) - установите в 0x02 0x00. По сути, это говорит компьютеру, что это исполняемый файл ELF.

e_machine (2) - установите значение 0x03 0x00, что сообщает компьютеру, что файл ELF был создан для работы на процессорах типа i386.

e_version (4) - установите 0x01 0x00 0x00 0x00.

e_entry (4) - передать управление на этот виртуальный адрес при исполнении. Мы еще не определили его, поэтому пока это 0x** 0x** 0x** 0x**.

e_phoff (4) - смещение от файла к program header table. Мы помещаем его сразу после заголовка ELF, так что размер заголовка ELF в байтах: 0x34 0x00 0x00 0x00.

e_shoff (4) - смещение от начала файла к таблице заголовков раздела. Нам это не нужно. 0x00 0x00 0x00 0x00.

e_flags (4) - флаги нам тоже не нужны. 0x00 0x00 0x00 0x00 снова.

e_ehsize (2) - размер заголовка ELF, поэтому содержит 0x34 0x00.

e_phentsize (2) - размер заголовка программы. Технически мы этого еще не знаем, но я уже могу сказать вам, что он должен содержать 0x20 0x00. Прокрутите вниз, чтобы проверить, если хотите.

e_phnum (2) - количество заголовков программы, что напрямую соответствует количеству сегментов в файле. Нам нужен текст и сегмент данных, поэтому это должно быть 0x02 0x00.

e_shentsize (2), e_shnum (2), e_shstrndx (2) - все это на самом деле не актуально, если мы не реализуем заголовки секций (а мы не реализуем), поэтому вы можете просто установить это значение 0x00 0x00 0x00 0x00 0x00 0x00.

И это заголовок ELF! Это первое, что находится в файле, и если вы все сделали правильно, окончательный заголовок в шестнадцатеричном формате должен выглядеть так:

0x7F 0x45 0x4C 0x46 0x01 0x01 0x01 0x00 0x00 0x00 0x00 0x000x00 0x00 0x00 0x10 0x02 0x00 0x03 0x00 0x01 0x00 0x00 0x000x** 0x** 0x** 0x** 0x34 0x00 0x00 0x00 0x00 0x00 0x00 0x000x00 0x00 0x00 0x00 0x34 0x00 0x20 0x00 0x02 0x00 0x00 0x000x00 0x00 0x00 0x00

Однако мы еще не закончили с заголовками. Теперь нам нужно также создать program header table. Он имеет следующие записи:

p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4), p_memsz(4),p_flags(4), p_align(4)

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

p_type (4) - сообщает программе тип сегмента. И текст, и данные используют здесь PT_LOAD (= 0x01 0x00 0x00 0x00).

p_offset (4) - смещение от начала файла. Эти значения зависят от размера заголовков и сегментов, поскольку мы не хотим, чтобы они перекрывались. Пока пусть будет 0x** 0x** 0x** 0x**.

p_vaddr (4) - какой виртуальный адрес назначить сегменту. Пусть будет 0x** 0x** 0x** 0x** 0x**, мы поговорим об этом позже.

p_paddr (4) - физическая адресация не имеет значения, поэтому вы можете указать здесь 0x00 0x00 0x00 0x00.

p_filesz (4) - количество байтов в образе файла сегмента, должно быть больше или равно размеру полезной нагрузки в сегменте. Опять же, установите значение 0x** 0x** 0x** 0x**. Мы изменим это позже.

p_memsz (4) - количество байтов в памяти образа сегмента. Обратите внимание, что это не обязательно равно p_filesz, но может быть и так. Пока оставьте его на 0x** 0x** 0x** 0x**, но помните, что позже мы можем установить его на то же значение, которое мы присваиваем p_filesz.

p_flags (4) - эти флаги могут быть непростыми, если вы не привыкли с ними работать. Что вам нужно запомнить, так это то, что флаг READ - 0x04, флаг WRITE - 0x02, а флаг EXEC - 0x01. Для текстового сегмента мы хотим READ + EXEC, поэтому 0x05 0x00 0x00 0x00, а для сегмента данных мы предпочитаем READ + WRITE + EXEC, поэтому 0x07 0x00 0x00 0x00.

p_align (4) - указывает на выравнивание страниц памяти. Размер страницы обычно составляет 4 КиБ, поэтому значение должно быть 0x1000. Помните, что x86 является little-endian, поэтому окончательное значение равно 0x00 0x10 0x00 0x00.

Уф. Мы, безусловно, уже многое сделали. Мы еще не заполнили многие поля в заголовках программ, и нам также не хватает нескольких байтов в заголовке ELF, но мы приближаемся. Если все пойдет по плану, таблица заголовков вашей программы (которую, кстати, можно вставить непосредственно за заголовком ELF - помните наше смещение в этом заголовке?) Должна выглядеть примерно так:

0x01 0x00 0x00 0x00 0x** 0x** 0x** 0x** 0x** 0x** 0x** 0x**0x00 0x00 0x00 0x00 0x** 0x** 0x** 0x** 0x** 0x** 0x** 0x**0x05 0x00 0x00 0x00 0x00 0x10 0x00 0x00 0x01 0x00 0x00 0x00 0x** 0x** 0x** 0x** 0x** 0x** 0x** 0x**0x00 0x00 0x00 0x00 0x** 0x** 0x** 0x** 0x** 0x** 0x** 0x**0x07 0x00 0x00 0x00 0x00 0x10 0x00 0x00

Заполнение пробелов

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

Во-первых, мы хотим вычислить размер наших заголовков и полезной нагрузки, прежде чем мы сможем определить какие-либо смещения. Просто сложите вместе размеры всех полей в заголовках и получите минимальное смещение для любого из сегментов. В заголовке ELF 116 байт + 2 заголовка программы, и 116 = 0x74, поэтому минимальное смещение равно 0x74. Чтобы сделать это безопасно, давайте установим начальное смещение на 0x80. Заполните от 0x74 до 0x7F 0x00, затем поместите текстовый сегмент в 0x80 в файл.

Размер самого текстового сегмента составляет 34 = 0x22 байта, что означает, что минимальное смещение для сегмента данных составляет 0x80 + 0x22 = 0xA2. Поместим сегмент данных в 0xA4 и заполним 0xA2 и 0xA3 значениями 0x00.

Если вы делали все вышеперечисленное в своем шестнадцатеричном редакторе, теперь у вас будет двоичный файл, содержащий ELF, и заголовки программ от 0x00 до 0x73, от 0x74 до 0x7F будут заполнены нулями, текстовый сегмент размещен от 0x80 до 0xA1, 0xA2 и 0xA3 снова являются нулями, и сегмент данных идет от 0xA4 до 0xB0. Если вы следуете этим инструкциям, и не получаете правильного результата, сейчас самое время посмотреть, что пошло не так.

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

e_entry (4) - 0x80 0x80 0x04 0x08; Мы выберем 0x8048080 в качестве точки входа в виртуальной памяти. Существуют некоторые правила относительно того, что вы можете, а что не можете выбрать в качестве точки входа, но самое важное, что нужно помнить, - это то, что начальный адрес виртуальной памяти по модулю размера страницы должен быть равен смещению в файле по модулю размера страницы. Вы можете проверить это в справочнике по ELF и некоторым другим хорошие книгам для получения дополнительной информации, но если это кажется слишком сложным, просто забудьте об этом и используйте эти значения.

p_offset (4) - 0x80 0x00 0x00 0x00 для текста, 0xA4 0x00 0x00 0x00 для данных. Это из-за очевидной причины, по которой эти сегменты находятся в файле.

p_vaddr (4) - 0x80 0x80 0x04 0x08 для текста, 0xA4 0x80 0x04 0x08 для данных. Мы хотим, чтобы сегмент текста был точкой входа для программы, и мы помещаем сегмент данных в память таким образом, чтобы он прямо соответствовал физическим смещениям.

p_filesz (4) - 0x24 0x00 0x00 0x00 для текста, 0x20 0x00 0x00 0x00 для данных. Это просто байтовые размеры различных сегментов файла и памяти. В этом случае p_memsz = p_filesz, поэтому используйте те же значения там.

Окончательный результат

Если вы выполнили все до буквы, вот что вы получите, если выгрузите все в шестнадцатеричном формате:

7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 10 02 00 03 0001 00 00 00 80 80 04 08 34 00 00 00 00 00 00 00 00 00 00 0034 00 20 00 02 00 00 00 00 00 00 00 01 00 00 00 80 00 00 0080 80 04 08 00 00 00 00 24 00 00 00 24 00 00 00 05 00 00 0000 10 00 00 01 00 00 00 A4 00 00 00 A4 80 04 08 00 00 00 0020 00 00 00 20 00 00 00 07 00 00 00 00 10 00 00 00 00 00 0000 00 00 00 00 00 00 00 BB 01 00 00 00 B8 04 00 00 00 B9 A480 04 08 BA 0D 00 00 00 CD 80 B8 01 00 00 00 BB 2A 00 00 00CD 80 00 00 48 65 6C 6C 6F 20 57 6F 72 6C 64 21 0A

Вот и все. Запустите chmod +x для этого двоичного файла, а затем выполните его. Hello World в 178 байтах. Надеюсь, вам понравилось это писать. :-) Если вы считаете этот HOWTO полезным или интересным, дайте мне знать! Я всегда это ценю. Также всегда приветствуются советы, комментарии и / или конструктивная критика.

Подробнее..

Перевод Реверс-инжиниринг тетриса на Nintendo для добавления Hard Drop

25.03.2021 18:04:40 | Автор: admin

Тетрис на Nintendo одна из моих любимых версий тетриса. Моя единственная жалоба заключается в том, что ему не хватает возможности Hard Drop мгновенного падения текущей фигуры и её фиксации на месте. Давайте её добавим

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

Ускоренное и мгновенное падение

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

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

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

До моих изменений NES Тетрис поддерживал только ускоренное падение.

Артефакт

Я сделал программу на rust, которая считывает файл NES ROM в формате INES. Если на входе был NES Tetris (обычно файл назван что-то вроде Tetris(U)[!].nes), на выходе она создаст новый файл ROM NES, который представляет собой NES Tetris, с добавлением быстрого падения фигуры.

Входной файл должен иметь хэш sha1 a99f922e9da20b2a27e4398348505d2e9d15271b.

$ cargo install nes-tetris-hard-drop-patcher   # install my tool$ nes-tetris-hard-drop-patcher < 'Tetris (U) [!].nes' > tetris-hd.nes   # patch a NES Tetris ROM$ fceux tetris-hd.nes   # run the result in an emulator

Этот инструмент полагается на то, что пользователь получит ROM-файл NES Tetris. В нём нет встроенного тетриса. Полученный файл ROM совместим со всеми эмуляторами NES он неспецифичен для fceux.

Патч

После публикации этого поста некоторые отметили, что существует стандартный формат для патча ROM (IPS), который широко поддерживается эмуляторами. Вы можете скачать мой патч здесь.

Инструменты

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

Чтобы протестировать свой эмулятор, я создал библиотеку для написания ассемблерных программ на языке Rust. Вот пример, в котором значение в регистре аккумулятора умножается на 12:

b.inst(Clc, ());                  // clear carry flagb.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x2)b.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x4)b.inst(Sta(ZeroPage), 0x20);      // store current accumulator value at address 0x0020b.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x8)b.inst(Adc(ZeroPage), 0x20);      // add the accumulator with the value at 0x0020 (x12)

Это позволяет мне использовать rust как язык высокого уровня для ассемблерных программ NES. Гибкость Rust важна при добавлении пользовательского кода к существующей программе, написанной в 1980-х годах.

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

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

Визуализация конечного положения фигуры

В NES есть два разных типа графики:

  • фон представляет собой сетку из плиток 8x8 пикселов;

  • спрайты это плитки, которые можно рисовать в произвольных местах на экране.

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

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

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

Говоря о контурных плитках, я добавил в игру новую плитку, чтобы использовать её для призрачной фигуры:

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

Для рендеринга спрайтов на NES вы заполняете область основной памяти метаданными спрайта (положение, плитка и т. д.), а затем записываете адрес начала этой области памяти в регистр OAMDMA. (Прямой доступ к памяти атрибутов объекта OAM это специальная память для хранения метаданных спрайта, а DMA это общий термин для устройств, непосредственно считывающих и записывающих основную память.) Запись адреса в OAMDMA заставляет графическое оборудование NES копировать метаданные спрайта указанной области основной памяти и в специализированную память атрибутов объекта, которая будет использоваться во время рендеринга для рисования спрайтов.

Регистр OAMDMA отображается в адресное пространство ЦП по адресу 0x4014. Поиск в дизассемблированной программе по этому адресу показывает:

0xAB63  Lda(Immediate) 0x02       # load accumulator with 20xAB65  Sta(Absolute) 0x4014      # write accumulator to 0x4014

При этом значение 2 записывается в OAMDMA, в результате чего память от 0x0200 до 0x02FF копируется в OAM. И одна функция определённо передаёт управление подпрограмме как ответственная за заполнение буфера. Она находится в 0x8A0A и может многое рассказать о том, как работает Тетрис.

Она начинается с чтения значений из адресов 0x0040 и 0x0041, умножения каждого на 8 и добавления их к некоторым смещениям. На NES каждая плитка имеет размер 8x8 пикселей, так что это, по-видимому, переводится из координаты плитки в координату пиксела, где смещения являются компонентами координаты пиксела верхнего левого угла доски. Несколько минут копания в мезене подтверждают это: 0x40 это координата x, а 0x41 координата Y текущего фрагмента.

Затем функция считывает данные из 0x42. Это место всегда содержит значение от 0 до 12, которое, по-видимому, кодирует форму текущей фигуры, а также её вращение. Для фигур с вращательной симметрией (например, фигура S) несколько одинаковых вращений получают одно значение в 0x42. Я буду называть это значение индексом формы.

Каждая фигура в Тетрисе состоит из 4 плиток, и для каждой плитки рендерится один спрайт. Координаты в 0x40 и 0x41 это позиция фигуры, но для рендеринга спрайтов мы должны узнать положение каждой плитки. С этой целью эта функция обращается к таблице в ПЗУ по адресу 0x8A9C, которую я буду называть таблицей форм. Каждая из 13 частей (включая уникальные вращения) имеет 12-байтовую запись в таблице форм. Запись таблицы форм для фрагмента хранит по 3 байта для каждой из 4 плиток:

  • смещение плитки по оси y (относительно 0x41);

  • индекс спрайта, используемый при рендеринге плитки;

  • смещение плитки по оси x (относительно 0x40).

Эта функция вычисляет местоположение и индекс спрайта каждой плитки текущей фигуры и заполняет буфер OAM DMA этой информацией. Чтобы визуализировать призрачную фигуру, мне нужна аналогичная функция, за исключением того, что она отображает каждую плитку с контуром, а не плиткой из таблицы форм, и визуализирует фигуру с вертикальным смещением, так что фигура появляется в том месте, где она должна приземлиться после hard drop. Было бы нетривиально изменить эту функцию на месте, чтобы она была общей для призрачной и обычной фигуры, поэтому вместо этого я скопировал/вставил код и изменил его, чтобы сделать то, что мне нужно.

Сначала я стал использовать программу для просмотра памяти mesen, чтобы найти, казалось бы, неиспользуемую область ПЗУ. Я не знаю, что здесь делают строки с 0x00 и 0xFF! Также я не знаю, как изменить шрифт в mesen на Monospace!

Я выделил 512 байт памяти, начиная с адреса 0xD6D0. Первым кодом, который я добавил в эту область, была функция, которая просто вызывает существующую функцию обновления буфера DMA OAM:

b.label("oam-dma-buffer-update");// Call original functionb.inst(Jsr(Absolute), 0x8A0A);// Returnb.inst(Rts, ());

Мой инструмент для патча заменяет все вызовы исходной функции (0x8A0A) вызовами новой функции.

Затем я взял дизассемблированный код из исходной функции обновления буфера DMA OAM и вручную перевел его на язык rust для сборки NES.

Этот код:

0x8A0A  Lda(ZeroPage) 0x400x8A0C  Asl(Accumulator)0x8A0D  Asl(Accumulator)0x8A0E  Asl(Accumulator)0x8A0F  Adc(Immediate) 0x600x8A11  Sta(ZeroPage) 0xAA...

превратился в:

b.label("render-ghost-piece"); // function label so it can be called by name laterb.inst(Lda(ZeroPage), 0x40);b.inst(Asl(Accumulator), ());b.inst(Asl(Accumulator), ());b.inst(Asl(Accumulator), ());b.inst(Adc(Immediate), 0x60);b.inst(Sta(ZeroPage), 0xAA);...

Я изменил свою копию обновления буфера DMA OAM, чтобы использовать контурную плитку вместо плитки, считанной из буфера формы. Чтобы проверить это изменение, я обновил oam-dma-buffer-update, чтобы вызвать мою функцию вместо оригинала:

b.label("oam-dma-buffer-update");// Call new functionb.inst(Jsr(Absolute), "render-ghost-piece");// Returnb.inst(Rts, ());

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

b.label("oam-dma-buffer-update");  // Call original function first b.inst(Jsr(Absolute), 0x8A0A); // Render the ghost piece, passing the vertical offset argument in address 0x0028. b.inst(Lda(Immediate), 6); b.inst(Sta(ZeroPage), 0x28); b.inst(Jsr(Absolute), "render-ghost-piece"); // Return b.inst(Rts, ());b.label("oam-dma-buffer-update");// Call original function firstb.inst(Jsr(Absolute), 0x8A0A);// Render the ghost piece, passing the vertical offset argument in address 0x0028.b.inst(Lda(Immediate), 6);b.inst(Sta(ZeroPage), 0x28);b.inst(Jsr(Absolute), "render-ghost-piece");// Returnb.inst(Rts, ());

Теперь вычислим истинное вертикальное смещение от текущей фигуры до места, где она приземлится после падения. Наблюдая за памятью с помощью mesen, я заметил, что ничего, похоже, не работает с памятью от 0x0020 до 0x0028. Первые 256 байтов памяти называются нулевой страницей и обеспечивают более быстрый доступ, чем остальная часть памяти. Мне нужно 8 байтов нулевой страницы для хранения координат X, Y каждой плитки текущей фигуры и обнаружения столкновений, а также один дополнительный байт для хранения временных значений во время вычислений.

Начните с инициализации значений от 0x20 до 0x27 координатами X, Y каждой плитки текущей фигуры:

b.label("compute-hard-drop-distance"); // function label so it can be called by name laterconst SHAPE_TABLE: Address = 0x8A9C;const ZP_PIECE_COORD_X: u8 = 0x40;const ZP_PIECE_COORD_Y: u8 = 0x41;const ZP_PIECE_SHAPE: u8 = 0x42;// Multiply the shape by 12 to make an offset into the shape table,// storing the result in IndexRegisterX.b.inst(Lda(ZeroPage), ZP_PIECE_SHAPE);  // read shape index into accumulatorb.inst(Clc, ());               // clear carry flag to prepare for arithmeticb.inst(Rol(Accumulator), ());  // rotate left: index * 2b.inst(Rol(Accumulator), ());  // rotate left: index * 4b.inst(Sta(ZeroPage), 0x20);   // store index * 4 at 0x0020b.inst(Rol(Accumulator), ());  // rotate left: index * 8b.inst(Adc(ZeroPage), 0x20);   // add to 0x0020: index * 12b.inst(Tax, ());               // transfer accumulator to IndexRegisterX// Store absolute X,Y coords of each tile by reading relative coordinates from shape table// and adding the piece offset, storing the result in zero page 0x20..=0x27.for i in 0..4 { // this is a rust loop - the assembly generated inside will be generated 4 times    b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read Y offset from shape table    b.inst(Clc, ());                                  // clear carry flag to prepare for addition    b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);          // add to Y coordinate of piece    b.inst(Sta(ZeroPage), 0x21 + (i  2));            // store the result in zero page    b.inst(Inx, ());                                  // increment IndexRegisterX to sprite index    b.inst(Inx, ());                                  // increment IndexRegisterX to X offset    b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read X offset from shape table    b.inst(Clc, ());                                  // clear carry flag to prepare for addition    b.inst(Adc(ZeroPage), ZP_PIECE_COORD_X);          // add to X coordinate of piece    b.inst(Sta(ZeroPage), 0x20 + (i  2));            // store the result in zero page    b.inst(Inx, ());                                  // increment IndexRegisterX to next tile}

Теперь о фактическом обнаружении столкновений! Неоднократно увеличивайте компонент Y каждой координаты плитки в адресах от 0x20 до 0x27, пока одна из плиток не столкнётся с зафиксированной плиткой или не выйдет за нижнюю часть поля. Изучая память с помощью mesen, я узнал, что состояние поля хранится в виде строкового массива индексов спрайтов, начинающихся с 0x0400, и что 0xEF это индекс плитки пустого пространства. Стратегия будет заключаться в использовании координаты каждой плитки для построения индекса в этом массиве и остановке, если будет найдено что-либо кроме 0xEF.

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

const BOARD_TILES: Address = 0x0400;const EMPTY_TILE: u8 = 0xEF;const BOARD_HEIGHT: u8 = 20;b.inst(Ldx(Immediate), 0);   // Load 0 into IndexRegisterX - this will be our loop counterb.label("start-ghost-depth-loop"); // This is a label - a target for branch instructionsfor i in 0..4 { // the assembly in this rust loop will be emitted 4 times    // Increment the Y component of the coordinate    b.inst(Inc(ZeroPage), 0x21 + (i * 2));    // Break out of the loop if the tile is off the bottom of the board    b.inst(Lda(ZeroPage), 0x21 + (i * 2));    b.inst(Cmp(Immediate), BOARD_HEIGHT);    b.inst(Bpl, LabelRelativeOffset("end-ghost-depth-loop"));    // Multiply the Y component of the coordinate by 10 (the number of columns)    b.inst(Asl(Accumulator), ());    b.inst(Sta(ZeroPage), 0x28); // store Y * 2    b.inst(Asl(Accumulator), ());    b.inst(Asl(Accumulator), ()); // accumulator now contains Y * 8    b.inst(Clc, ());    b.inst(Adc(ZeroPage), 0x28); // accumulator now contains Y * 10    // Now add the X component to get the row-major index of the cell    b.inst(Adc(ZeroPage), 0x20 + (i * 2));    // Load the tile at that coordinate    b.inst(Tay, ());    b.inst(Lda(AbsoluteYIndexed), BOARD_TILES);    // Test whether the tile is empty, breaking out of the loop if it is not    b.inst(Cmp(Immediate), EMPTY_TILE);    b.inst(Bne, LabelRelativeOffset("end-ghost-depth-loop"));}// Increment counter and loopb.inst(Inx, ());b.inst(Jmp(Absolute), "start-ghost-depth-loop");b.label("end-ghost-depth-loop");

Это приводит к тому, что IndexRegisterX содержит количество повторений цикла, которое также является вертикальным расстоянием от текущей фигуры до того места, где она окажется после падения. Для удобства эта функция вернёт результат через регистр аккумулятора:

// Return depth via accumulatorb.inst(Txa, ());  // transfer IndexRegisterX to accumulatorb.inst(Rts, ());  // return

Вот полный код замещающей функции обновления буфера DMA OAM:

b.label("oam-dma-buffer-update");// Call original function firstb.inst(Jsr(Absolute), 0x8A0A);// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Check if the distance is 0, and skip rendering the ghost piece in this caseb.inst(Beq, LabelRelativeOffset("after-render-ghost-piece"));// Render the ghost piece, passing the vertical offset argument in address 0x0028.b.inst(Sta(ZeroPage), 0x28);b.inst(Jsr(Absolute), "render-ghost-piece");b.label("after-render-ghost-piece");// Returnb.inst(Rts, ());

Результат:

Добавление контроллера для Hard Drop

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

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

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

Моя вторая идея заключалась в том, чтобы настроить мой эмулятор для логирования каждой выполненной инструкции. Я загрузил Тетрис и прошёлся по меню, чтобы начать новую игру, а затем сохранил файл состояния. У моего эмулятора есть возможность запускать определённое количество кадров. Я настроил его на работу в 20 кадров, загрузил файл состояния и записал каждую инструкцию, не нажимая никаких элементов управления. Затем я повторил этот процесс, но на этот раз я нажимал левую кнопку на протяжении 20 кадров. Теперь у меня было два лога потока инструкций один без нажатых элементов управления, а второй с их нажатием. Само собой разумеется, что в первую очередь эти потоки различаются, когда программа в первый раз разветвляется по состоянию левой кнопки.

Конечно же:

@@ -116912,9 +116912,175 @@ 0x89B8  Lda(ZeroPage) 0xB5 0x89BA  And(Immediate) 0x03 0x89BC  Bne(Relative) 0x15-0x89BE  Lda(ZeroPage) 0xB6-0x89C0  And(Immediate) 0x03-0x89C2  Beq(Relative) 0x45+0x89D3  Lda(Immediate) 0x00+0x89D5  Sta(ZeroPage) 0x46+0x89D7  Lda(ZeroPage) 0xB6+0x89D9  And(Immediate) 0x01+0x89DB  Beq(Relative) 0x0F...

Перекрёстная ссылка с дизассемблированным ПЗУ, эта функция начинается с:

0x89AE  Lda(ZeroPage) 0x400x89B0  Sta(ZeroPage) 0xAE0x89B2  Lda(ZeroPage) 0xB60x89B4  And(Immediate) 0x040x89B6  Bne(Relative) 0x51 (relative: 0x51, absolute: 0x8A09)0x89B8  Lda(ZeroPage) 0xB50x89BA  And(Immediate) 0x030x89BC  Bne(Relative) 0x15 (relative: 0x15, absolute: 0x89D3)0x89BE  Lda(ZeroPage) 0xB60x89C0  And(Immediate) 0x030x89C2  Beq(Relative) 0x45 (relative: 0x45, absolute: 0x8A09)...

Это ветвление на основе содержимого адресов 0x00B5 и 0x00B6. Во время наблюдения за этими адресами в mesen во время затирания элементов управления у меня создаётся впечатление, что 0xB5 хранит различия между кадрами в состоянии контроллера, а 0xB6 хранит текущее состояние контроллера. Несмотря на то что тетрис не использует её, состояние кнопки вверх отражается в этих значениях.

Я запустил эту функцию так же, как и мою замену для обновления буфера DMA OAM. Всё, что он сделал, это вызвал исходную функцию и вернул:

b.label("handle-controls");// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Returnb.inst(Rts, ());

Теперь добавим проверку, нажата ли кнопка вверх. А пока просто телепортируем текущую фигуры на фиксированную высоту при нажатии кнопки:

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Set the current piece's Y coordinate to 7b.inst(Lda(Immediate), 7);b.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);b.label("controller-end");// Returnb.inst(Rts, ());

Вот код в действии, когда я несколько раз нажимаю вверх:

Затем замените тестовую константу 7 на фактическое положение, в котором деталь окажется после резкого падения. Используйте функцию compute-hard-drop-distance, которую мы написали для рендеринга призрачной части, а затем просто добавьте текущую позицию фигуры, чтобы получить абсолютную координату Y, в которой он окажется после падения:

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);b.label("controller-end");// Returnb.inst(Rts, ());

Выглядит неплохо!

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

Глядя на память с помощью mesen, можно увидеть, что есть счётчик по адресу 0x0045, который ведёт отсчёт до некоторого числа, а затем сбрасывается на следующем тике (когда текущая фигура перемещается вниз сама по себе). Чтобы узнать больше, я заставил свой эмулятор записывать все инструкции и запускать игру в течение 13 тиков. Я выбрал 13, потому что казалось маловероятным, что они генерируются случайно.

Во время этого прогона таймер истёк бы 13 раз. Где-то в логах инструкций есть связанная инструкция, которая была выполнена ровно 13 раз. Давайте найдём!

Логи инструкций находится в файле с именем /tmp/log.txt:

cat /tmp/log.txt | sort | uniq --count | sort --numeric-sort

Мы сортируем инструкции по частоте. Просматривая те, которые были выполнены 13 раз, я заметил:

13 0x8958  Lda(Immediate) 0x0013 0x895A  Sta(ZeroPage) 0x45

Это кажется актуальным, потому что он взаимодействует с таймером по адресу 0x0045!

Обращение к дизассемблированному коду этой инструкции:

0x8980  Lda(ZeroPage) 0x45    # load the timer value0x8982  Cmp(ZeroPage) 0xAF    # compare with the value at 0x00AF0x8984  Bpl(Relative) 0xD2 (relative: D2, absolute: 8958)  # branch if it was higher0x8986  Jmp(Absolute) 0x89720x8972  Rts(Implied)0x8958  Lda(Immediate) 0x00  # load 0 into the accumulator0x895A  Sta(ZeroPage) 0x45   # store the accumulator (0) in the timer

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

Наблюдаем за 0x00AF в mesen, и это, кажется, максимальное значение, которого достигает таймер в 0x0045. Кроме того, когда вы завершаете уровень, значение 0x00AF уменьшается, что ускоряет игру! Поэтому после hard drop просто установите значение таймера на значение 0x00AF:

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;const TIMER: u8 = 0x45;const TIMER_MAX: u8 = 0xAF;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);// Set the timer to its maximum valueb.inst(Lda(ZeroPage), TIMER);b.inst(Sta(ZeroPage), TIMER_MAX);b.label("controller-end");// Returnb.inst(Rts, ());

Выглядит лучше, но всё равно есть большая задержка, если вы очень быстро опустите первую фигуру во время первого тика. Оказывается, первый тик занимает больше времени, чем все остальные тики. Глядя на память в mesen, я заметил, что значение 0x004E увеличивается во время первого тика. Для всех остальных тиков он установлен на 0. Установка его на 0 после появления hard dropa решает проблему с синхронизацией.

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;const TIMER: u8 = 0x45;const TIMER_MAX: u8 = 0xAF;const TIMER_FIRST_TICK: u8 = 0x4E;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);// Set the timer to its maximum valueb.inst(Lda(ZeroPage), TIMER);b.inst(Sta(ZeroPage), TIMER_MAX);// Clear the first tick timerb.inst(Lda(Immediate), 0x00);b.inst(Sta(ZeroPage), TIMER_FIRST_TICK);b.label("controller-end");// Returnb.inst(Rts, ());

Кажется, это работает!
Исходный код инструмента исправления доступен на github. Загрузите патч IPS, который применяет изменения, описанные в этом посте, здесь. Второй патч, который добавляет hard drop, но не визуализирует конечное положение фигуры, доступен здесь.

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

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Играем в DOOM на тесте на беременность. Что? Да

07.09.2020 18:22:46 | Автор: admin
А также на микроволновке, валидаторе билетов и многом другом.



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

Программист-энтузиаст и адепт реверс-инжиниринга Foone сумел запустить полнофункциональную игру DOOM на электронном тесте на беременность, а до этого то же самое проделал с The Elder Scrolls: Skyrim. Первое видео классического шутера на миниатюрном экране устройства было показано им на выходных в личном микроблоге в Twitter. Тогда он признался, что на самом деле просто проигрывал видео, но с тех пор поднял ставки и нашел способ действительно сыграть в DOOM на электронном тесте при помощи беспроводной клавиатуры.


Как такое возможно, спросите вы? Конечно, не обошлось без читерства.

В своем треде Foone ссылался на исследования другого юзера Twitter, Xtoff, в прошлом месяце решившем проверить, что кроется за корпусом теста на беременность компании ClearBlue. Foone использовал другой бренд, Equate, но обнаружил в нем похожую начинку: бумажную полоску, батарею CR1616 3V, восьмибитный микроконтроллер Holtek HT48C06 (64 байта RAM, работает на частоте 4 МГц либо 8 МГц) и оперативная память.

image

По словам Foone, этот наполнение этого теста на беременность быстрее в вычислении базовых математических операций и I/O, чем IBM PC.

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

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

image

Началом истории Will it run DOOM? можно считать 2016 год, когда DOOM был запущен на миниатюрном Game Boy, помещенном в брелке для ключей. Времени с тех пор прошло прилично, и теперь уже кажется, что легендарный шутер можно запустить практически на чем угодно. Чтобы доказать это, ниже приведем список (не полный!), на каких устройствах это удалось.




Принтер Canon Proxima


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



Осциллограф


Сам по себе осциллограф использует операционную систему Windows 95, поэтому, по словам пользователя Zekinn, впервые запустившего DOOM на таковом, дальше дело оставалось за малых отбросить лишние предубеждения во имя улучшения человеческого опыта.



Банкомат


Обычно банкомат используют для снятия или внесения денег, но энтузиасты решили вместо этого запустить на нем DOOM. С предустановленной на таких автоматах операционной системой Windows XP дальнейшие действия по установке игры упирались лишь в вопрос желания.



DOOM в DOOM


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



Пианино


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



Minecraft


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



Валидатор билетов




Микроволновка


image


iPod Mini


image

Больше примеров в треде на Reddit.
Подробнее..

Исполняемый обвес

04.03.2021 20:15:20 | Автор: admin

Привет, хабровчане. Для будущих студентов курса Reverse-Engineering. Basic Александр Колесников подготовил полезную статью.

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


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

Disclamer: Вся информация предоставляется исключительно для обучающих целей.

Навесная защита

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

  • Охранные процедуры, которые детектируют присутствие отладчика в системе;

  • Охранные процедуры для детектирования песочницы;

  • Охранные процедуры для детектирования динамической инструментации;

  • Блокирование возможностей отладки охраняемого процесса;

  • Виртуальная машина с собственным набором команд;

  • Методы обфускации и запутывания кода имитовставки, наномиты.

Список внушителен, все это заставить работать для защищаемого приложения достаточно сложная задача, но тем не менее не невыполнимая. На сегодняшний день такие защиты существуют и довольно успешно применяются. Примерами может служить WinLicense, Themida, Enigma Protector.

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

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

Методы исследования

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

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

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

Обычная архитектура виртуальной машины со своим набором команд имеет следующие отличительные черты:

  • Алгоритм инициализации виртуальной машины;

  • Огромный switch блок, который имплементирует каждую команду виртуальной машины;

  • Большой блок данных, который содержится в файле и не поддается декомпиляции. Обычно это байткод, который используется виртуальной машиной. Themida, к примеру, может использовать 4 разных вида байткода. При упаковке приложения пользователь может выбрать, какой вариант байткода будет использоваться.

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

Картинка позаимствована отсюда.

Как такое анализировать? Общий алгоритм анализа сводится к:

  • Снятие всех антиотладочных приемов;

  • Идентификация места сохранения всех регистров в структуру для начала работы виртуальной машины;

  • Идентификация используемого набора команд;

  • Идентификация используемого байткода;

  • Идентификация типа виртуальной машины (расшифровывается ли код до native в памяти и затем работает как обычно или нет);

  • Создание декодировщика для работы с идентифицированным байткодом.

Проведем небольшую практику.

Начало пути

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

#include <iostream>int main{  std::cout<< "Hello World!" <<std::endl;  system("pause");  return 0;}

Приложение соберем в release варианте и декомпилируем. В итоге Main функция будет выглядеть так:

А после преобразования через VMProtect:

Так как команд вообщем-то не так много, то и на графе мы видим не огромный switch блок, а всего лишь несколько дополнительных блоков.

Пройдемся по каждому из них:

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

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

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

Кстати, байткод выглядит так:

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


Узнать подробнее о курсе Reverse-Engineering. Basic.

Смотреть открытый вебинар на тему Эксплуатация уязвимостей в драйвере.

Подробнее..

Исполняемый обвес. Часть 2

16.03.2021 20:17:50 | Автор: admin

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

Описание работы VMProtect

Рассматриваемая защита имеет ряд функций, которые здорово портят жизнь реверс-инженеру. Например:

  1. Создание полиморфных обработчиков для одной и той же программы;

  2. Однонаправленное кодирование команд процессора;

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

Дизассемблированный граф выглядит так:

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

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

pusha ; сохранить все регистрыpush 0 ; установка начального значенияmov esi, [esp+x+var] ; esi = указатель на VM байт код, адрес может меняться за счет x и varmov ebp, esp ; так как VMProtect использует стековую виртуальную машину, то ebp = VM "stack" указательsub esp, 0C0hmov edi, esp ; edi = область памяти, где находятся регистры общего назначенияСместить обработчик:add esi, [ebp+0]Выбрать следующую команду:mov al, [esi]; читаем байт байткода в EIP виртуальной машиныmovzx eax, alsub esi, -1; смещаем значение EIP виртуальной машиныjmp ds:VMHandlers[eax*4] ; выполняем обработчик команды

Теперь тоже самое, но на графе:

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

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

Отладка VM

Для отладки будем использовать x64dbg, на сегодняшний день это самый активно развивающийся отладчик для ОС Windows (помимо WinDBG). Вообще можно пользоваться любым отладчиком, лишь бы вы могли удобно видеть регистры и память, с которой работает приложение. Загрузим приложение в отладчик и встанем на первую команду протектора:

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

На рисунке пунктирной красной рамкой выделен main loop. Это основной цикл, который работает с байткодом, расположенным по адресу из регистра ESI. EIP для виртуальной машины является AX регистр. В него помещаются идентификаторы обработчиков. В итоге, чтобы получить развернутый листинг приложения, нам нужно собрать номера вызываемых обработчиков. Сделать это можно либо используя приложение, которое будет самостоятельно парсить сегмент упакованного исполняемого файла, либо нужно установить условный breakpoint, который будет регистрировать заданные данные. Будем использовать второй метод и запишем в лог отладчика все номера вызванных хэндлеров.

Для начала определим точку, где индекс хэндлера можно получить уже после вычислений:

Теперь откроем меню "breakpoints" и установим вот такие значения:

Настройка достаточно проста, поле "Log Text" используется для занесения данных в лог отладчика. Формат строки, которая заполняется из регистров или локальных переменных отладчика выглядит так: {формат данных:объект откуда взять данные}. В нашем случае мы просматриваем регистр eax и локальную переменную $breakpointcounter.

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

Посмотрим на результат:

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

Для замены использовалась следующая регулярка: ^(.*?)$\s+?^(?=.*^\1$) Итого у нас 53 уникальных индекса для хэндлеров. Уже лучше, перейдем к следующему этапу.

Сбор команд алгоритма

К сожалению, на этом этапе придется попрощаться на время с отладчиком и заняться программированием. Основная наша задача будет получение общего листинга всех обработчиков приложения, которые вызываются последовательно из main loop. Зачем это нужно? Чтобы собрать алгоритм воедино и попробовать его оптимизировать для разбора.

Скрипт, который нам поможет собрать обработчики в единую последовательность команд:

import pefile# загружаем файлpe = pefile.PE(filePath)# помещаем в памятьimage = pe.get_memory_mapped_image()# смещение до таблицы хэндлеровbaseOffset = 0xB400# Всего 255 хэндлеров в исполняемом файлаhandlers = []for i in range(255):    offset+=4    handlers.append(image[offset])# собираем байткод хэндлеровfor h in handlers:    md = Cs(CS_ARCH_X86, CS_MODE_32)    for i in md.disasm(h, 0x1000):        print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))

Фрагмент получаемого листинга:

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


Автор статьи Александр Колесников.

Статья приурочена к курсу "Reverse-Engineering. Basic".

Приглашаем также посетить открытый вебинар на тему Эксплуатация уязвимостей в драйвере. Часть 2: разберём уязвимость переполнения пула памяти и уязвимость типа type confusion; напишем эксплойт.

Подробнее..

Категории

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

  • Имя: Макс
    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