Главная / Блог / Скрипты и функции в x64dbg: автоматизация реверса и примеры
Реверс чужих приложений отнимает много времени, поэтому сегодня все инструменты анализа приложений поддерживают скрипты (сценарии), которые позволяют автоматизировать процесс. В данном материале мы научимся создавать скрипты для наиболее распространённых из отладчиков под Windows – это пользовательский x64dbg, а так-же дебагер режима ядра WinDBG. Начнём, пожалуй, с первого и попробуем ответить на вопрос, зачем в принципе нужны скрипты, и какую выгоду мы сможем от них поиметь. Забегая вперёд скажу, что автор x64dbg «Дункан Огилви» со-своей командой постарались здесь на славу, и грех нам не использовать их труды в своей практике.
В процессе реверса софта мы не можем предугадать, какие именно техники использовал разработчик в своём софте, а потому в любом случае требуется поверхностный анализ кода приложения. Например, о многом может сказать список импортируемых API-функций, после чего уже прорисовывается общая картина. Ну или проверка значения «Энтропии» секций в инструменте типа «PE-Anatomist», что позволит обнаружить сжатые данные.
Кстати, даже секция ресурсов .rsrc способна скрывать вредоносный код — мы подробно разбирали это в материале шелл-код в секции ресурсов PE-файла.
А вот дальше уже потребуется сбор сведений по определённой логике, который имеет смысл автоматизировать. В большинстве случаях для этого в отладчиках есть поддержка точек-останова «BreakPoint», но и их возможности в дефолте сильно ограничены. Вот здесь и приходят на помощь скрипты (на русском сценарии), импульс от сделки с которыми просто впечатляет. Скриптом мы можем запускать отлаживаемые процессы сразу на исполнение, и останавливать их по какому-либо условию, или налету распаковывать/расшифровывать информацию, и т.д. В общем делать всё, что душа пожелает, и именно в этом реальная сила современных инструментов отладки, с которыми многие из начинающих реверсеров просто не знакомы. Цель данной статьи – заполнить этот пробел.
Формат скриптов отладчика x64dbg
Скрипты здесь делятся на 2 группы – непосредственно «Сценарии» и более мощные «Функции». Не смотря на то, что грань между ними тонка, они всё-же отличаются как по содержимому, так и по возможностям. По формату это обычные текстовые файлы с любым расширением, хотя предпочтительно использовать рекомендуемое автором расширение *.txt. Функции считаются более производительными, т.к. представляют собой просто набор команд управления дебагером, где каждая команда указывается в скрипте с новой строки. Но у каждого из них есть своя зона ответственности, а обзор мы начнём именно со «Сценариев», как более простых для понимания.
Пример сценария
Эта крутая фишка позволяет просматривать блоки памяти в чётко структурированной форме. Например многие функции Win32-API возвращают массивы данных, сбрасывая их в заранее подготовленные нами структуры. Эти структуры имеют именованные поля, которые мы потом читаем. Но в реале API-функция просто записывает данные в память по указанному в аргументе адресу, где нет никаких именованных полей. Посмотрите на скрин, где по Ctrl+G я запросил у отладчика дамп системной «РЕВ» (Process Environment Block). Кстати у x64dbg в запазухе есть зарезервированные слова для запроса адресов, например peb(), teb(), и tid() (идентификатор потока).
Как видим, отладчик послушно вернул нам содержимое блока по адресу 0x000007ff`fffd8000, только что означают эти данные – сам чёрт не разберёт. Если мы хотим получить профит от вкладки «Структура», сначала нужно создать текстовый файл с подробным описанием полей (прототипы структур можно найти в сишных хидерах). Вот фрагмент такого файла для структуры РЕВ, который можно будет сохранить в папку \script отладчика:
//-------- Заголовок с дефайнами ----------------- //-------- заполняется 1 раз для каждого файла --- //-------- и описывает размеры полей ------------- ClearTypes AddType uint64_t, QWORD AddType uint32_t, DWORD AddType uint16_t, WORD AddType uint8_t, BYTE AddType wchar_t, UNICODE AddType char, ASCII //-------- Конец описателя размеров полей -------- AddStruct PEB64 // create an РЕВ structure AddMember PEB64,BYTE,InheritedAddressSpace AddMember PEB64,BYTE,ReadImageFileExecOptions AddMember PEB64,BYTE,BeingDebugged AddMember PEB64,BYTE,BitField AddMember PEB64,DWORD,Reserved,0x01 AddMember PEB64,DWORD64,Mutant AddMember PEB64,DWORD64,ImageBaseAddress AddMember PEB64,DWORD64,Ldr AddMember PEB64,DWORD64,ProcessParameters AddMember PEB64,DWORD64,SubSystemData AddMember PEB64,DWORD64,ProcessHeap //......... продолжение следует
Теперь (1)переходим на вкладку «Сценарии» в верхнем окне отладчика, (2)правым кликом мыши выбираем в ней наш новоиспечённый файл скриптов выше, и (3)как он появится в окне, обязательно запустим его клавишей «Space» пробел. Если в ответ получим окно «ОК», значит всё сделали правильно, и теперь переходим на вкладку «Структура» в окне дампов памяти. Здесь аналогично правой клавишей выбираем «Тип посещения» и вводим «РЕВ64», где «РЕВ64» задаёт название структуры в нашем текстовом файле сценариев. По нажатию ОК появится ещё бокс, на этот раз с запросом адреса дампа в памяти (на который планируем наложить структуру) – отвечам зарезервированным словом peb(). Результатом этих телодвижений будет аккуратно оформленный дамп, где каждая строка в столбце «Имя» соотносится к столбцу «Значение». Теперь сравните, что мы получили на предыдущем скрине с сырым дампом, и на этом. Обратите внимание на стартовый адрес структуры в памяти 000007ff`fffd8000 – в обоих случаях он совпадает:
Поле «BeingDebugger» в структуре РЕВ является флагом, что процесс находится под отладкой. Этот байт принимает логическое значение True/False (соответственно да/нет), и как видим в данном случае он взведён в 1, а значит всё идёт по плану. Можно в меню активировать плагин «ScyllaHide», тогда этот байт сразу сбросится в нуль, ведь плаг и работает над тем, чтобы скрыть отладчик от исследуемых процессов.
А сами техники антиотладки и трюки с проверками вроде CMP/JE я подробно показывал в статье антиотладка и обход CMP/JE на практике.
Ясно, что это пример лишь с одной структурой РЕВ, а ведь в системе этих структур как звёзд на небе. Создав один общий файл с такими сценариями, позже можно добавлять в него прототипы всё новых и новых структур, с которыми вы будете сталкиваться в процессе реверса софта. Поверьте, что потраченное на это дело время полностью оправдает себя в будущем.
Пример функций x64dbg
Функции поддерживаются встроенным в отладчик мощным скриптовым языком, который чем-то похож на плюсы С++, но по сути не является таковым, хотя-бы на уровне синтаксиса. Результат работы функций отображается в окне «Журнал», а сами функции – это лишь последовательность вынесенных в отдельный файл команд. Все команды скриптов можно вводить прямо в окно ком.строки (в подвале основного окна), а выхлоп проверять в окне журнала. Самих команд огромное кол-во, а потому перечислить их все в одной статье никак не получится. Кому интересно, можно посмотреть описание в прилагаемом к x64dbg файле-справки *.pdf (правда оформление его на редкость отвратительное).
Начнём, пожалуй, со сбора информации о загруженных в исследуемое приложение модулей DLL – вот синтаксис, который имеет смысл сначала отшлифовать через ком.строку, и если в окне «Журнал» получим желаемый результат, то сохранять уже во-внешний файл скрипта. Здесь пример для либы Kernel32.dll, но можно указать и любую, причём обязательно без квадратных скобок:
Аналогично можно приручить функцию скрипта и к основным командам отладчика, и работой с точками-останова «BreakPoint»:
А вот для чтения/записи памяти адрес непременно должен быть прописан внутри квадратных скобок, причём вместо «byte» можно подставлять зарезервированные слова «word/dword/qword» (2,4,8 соответственно):
Числа и строки можно выводить только в системах счисления HEX и DEC, причём большинство аргументов совпадают со-спецификатороми API printf() из msvcrt.dll:
При необходимости можно задавать переменные командой «var», однако есть и зарезервированная для общего случая и команда «$result», что очень удобно использовать на практике:
Ну команды в глобальном масштабе..
Функции поддерживают большинство инструкций ассемблера, например: пересылку данных mov, вызовы процедур call/ret, проверку с условными переходами cmp/jne, прыжки goto/run, все лог.инструкции and/or/xor, и многое другое. В документации всё описано, и остаётся просто проштудировать её несколько раз.
Но самым отличным на мой взгляд решением во-первых стала возможность устанавливать обратные вызовы «Callback» на точках останова, которая реализуется командой «SetBreakpointCommand», а во-вторых расширять регистры общего назначения с 32 до 64 бит просто префиксом(С), например CAX=EAX=RAX, CIP=EIP=RIP, и т.д. Как результат, один скрипт можно будет использовать как для 32, так и для 64-битных приложений. В общем вместо 1000 слов посмотрим на такой скрипт, по результат которого в лог сбрасывается основная инфа при срабатывании API выделения памяти VirtualAlloc(). Само приложение может выглядеть так (выделяет память и копирует туда строку):
А вот скрипт для его обработки:
cls bpc bphc bp VirtualAlloc SetBreakpointCommand VirtualAlloc, "scriptcmd call cb_virtual_alloc" $base = mod.main() $entry = mod.entry($base) log log " Image Base: {p:$base}" log " Base of Code: {p:entry} {a:cip}" log " Size in memory: {d:mod.size($base)} byte" log " PEB address: {p:peb()}" log " TEB address: {p:teb()}" log " Thread ID: {x:tid()}" log " Process ID: {x:$pid}" log " Process Handle: {x:$hp}" log " First bytes: {mem;.16@cip}" log " First instruct: {i:cip} (len={dis.len(cip)} byte)" log refstr i = 0 loop: addr = ref.addr(i) log " String refer: {d:i} = {p:addr} --> {i:addr}" i++ cmp i, ref.count() jne loop log goto main cb_virtual_alloc: rtr log " Memory alloc..: {p:cax}" log " Memory size...: {d:arg.get(1)} byte" log " Memory protect: {d:arg.get(3)}. (R=2, RW=4, RE=20, RWE=40)" log savedata "f:\dump.bin", 00402000, .128 main: run ret
Немного комментов для этого скрипта..
Первые три строчки на входе очищают все софт/хард бряки, если таковые имелись. Далее устанавлиется точка на VirtualAlloc(), а «SetBreakpointCommand» определяет колбек, который сработает при останове. На сл.этапе печатается лог о текущем модуле, который предваряют 2 переменные «$base/entry». Выражение типа «ref.count()» считает количество ссылок, а «ref.addr(index)» получает адрес ссылки по индексу. Под занавес в хвосте прописана функция обратного вызова «Callback», внутри которого читаются аргументы API VirtualAlloc() именно на входе, посколько сам колбек начинается с команды «rtr», что подразумевает RunTrace. Если убрать эту «rtr», то получим аргументы уже на выходе из VirtualAlloc(), что собственно безсмысленно. И наконец команда с тремя аргументами «savedata» позволяет сохранить заданный участок памяти на диск, т.е. сдампить его.
Теперь загружаем приложение в x64Dbg, и на вкладке "Скрипты/Сценарии" жмём [ПКМ --> Загрузить скрипт] или просто Ctrl+O. Если всё ок, то текст скрипта увидим в окне, а запустить его можно клавишей "Пробел". Текстовый оригинал скрипта можно править снаружи, и не выгружая обновлять его в окне отладчика через Ctrl+R (или пкм --> перезагрузить скрипт). Результат будет отображён как в окне "Сценарии", так и в консольном окне логов "Журнал". В репозитории разраба: https://github.com/x64dbg/Scripts имеются сдесяток готовых скриптов. Заглянув в их содержимое можно ознакомиться с основными командами, и общей философией создания функций – очень выручает. Результат работы нашего тестового скрипта выглядит как на скрине ниже, хотя в соседнем окне «Сценарии» можно получить этот-же лог в более информативном виде:
Заключение
Понять структуру стриптов отладчика x64dbg можно только пощупав их руками, а потому эта статья была написана только с целью заинтересовать читателя. Если объяснять всё досканаль, то в тетради не хватит клеток, да и не нужно это, т.к. на сайте разработчика по указанному выше линку имеется масса примеров, например для поиска в памяти приложения сигнатур протекторов/упаковщиков, и многое другое. Просто откройте их, и всё станет намного прозрачней. Удачи в изучении этого интересного направляений!
Все права защищены. © 2016 - 2025