Все материалы этой работы являются учебными. Исследовать, отлаживать и модифицировать можно только программы, написанные для учебных целей, либо ваше собственное ПО. Модификация чужих программ с целью обхода технических средств защиты (в том числе лицензионных проверок коммерческого ПО) является нарушением лицензионных соглашений и может повлечь ответственность, в том числе по российским нормам о неправомерном доступе к компьютерной информации и о вредоносных программах (ст. 272, 273 УК РФ).
Разработка системного ПО принципиально отличается от прикладной разработки. Ошибки часто проявляются не в коде, а на границе между программой и ядром. Поэтому инструментарий системного программиста строится вокруг двух вопросов: что программа просит у ядра (системные вызовы) и что происходит внутри процесса в конкретный момент времени (состояние регистров, стека, памяти). В этой работе используются три взаимодополняющих инструмента:
strace — показывает диалог программы с ядром (динамический анализ на уровне системных вызовов)gdb — позволяет остановить процесс в любой точке и изучить его состояние (динамический анализ на уровне инструкций)ghidra — восстанавливает структуру программы по одному лишь бинарнику (статический анализ)Типичный порядок исследования неизвестного бинарника такой: сначала наблюдаем поведение (strace), затем проверяем гипотезы в отладчике (gdb), и только потом подтверждаем полную логику по дизассемблированному коду (Ghidra).
Для полноценной работы отладчика программа должна быть собрана с отладочными символами:
gcc -g -O0 -fno-pie -no-pie -Wall -o mindvault mindvault.c
-g — записать в бинарник отладочную информацию: имена функций, переменных, типов и соответствие машинных инструкций строкам исходного кода (секции .debug_info, .debug_line и другие). Проверить их наличие можно командой readelf -S mindvault | grep debug-O0 — отключить оптимизации. С оптимизацией (-O2) компилятор переупорядочивает код и удаляет переменные — отлаживать такое гораздо сложнее-fno-pie -no-pie — собрать непозиционно-независимый исполняемый файл (тип ET_EXEC). Такой бинарник всегда загружается по одним и тем же адресам, а таблица секций содержит фиксированные виртуальные адреса вида 0x4016a4. Это существенно упрощает и анализ в Ghidra, и бинарный патчинг. Проверка: readelf -h mindvault | grep Type должен быть EXECВ реальных дистрибутивах почти все бинарники собираются PIE (
ET_DYN, загружаются по случайному адресу благодаря ASLR) и без отладочных символов.
Установка: sudo apt install gdb strace.
GDB работает с процессом через системный вызов ptrace, с помощью которого отладчики и трассировщики получают контроль над другой программой. Ключевая идея: отладчик ставит в коде точку останова (breakpoint), заменяя первый байт инструкции на специальную однобайтовую инструкцию 0xCC. Когда процесс доходит до неё, ядро передаёт управление отладчику. Для программы это выглядит как обычное прерывание.
Основные команды:
| Команда | Сокращение | Действие |
|---|---|---|
break <функция> |
b |
поставить точку останова на входе функции |
break *0x4016a4 |
точка останова по адресу | |
run [аргументы] |
r |
запустить программу под отладчиком |
next / step |
n / s |
шаг по строке без входа / с входом в функцию |
finish |
выполнить функцию до конца и показать возвращаемое значение | |
continue |
c |
продолжить выполнение до следующей точки |
print <выражение> |
p |
вывести значение переменной или выражения |
print/x <выражение> |
то же, в шестнадцатеричном виде | |
x/16xb <адрес> |
дамп 16 байт памяти с адреса | |
x/s <адрес> |
интерпретировать память как строку | |
info registers |
i r |
показать регистры (rax, rbx, rip, rflags...) |
backtrace |
bt |
стек вызовов |
disassemble |
disas |
дизассемблировать текущую функцию |
call <функция>(...) |
вызвать функцию прямо из отладчика | |
quit |
q |
выход |
Пример сессии:
(gdb) break check_license
(gdb) run
Breakpoint 1, check_license (lic=0x7fffffffddb0) at mindvault.c:128
(gdb) print *lic
$1 = {user = "pmifi", expires = "20991231", key = 'A' <repeats 11 times>}
(gdb) finish
Value returned is $2 = -3
GDB знает имена функций, полей структуры и номера строк — это работает благодаря отладочной информации из флага
-g. Более того, GDB позволяет вызвать функцию бинарника с нужными аргументами прямо во время отладки.
Любая работа программы с внешним миром сводится к системным вызовам (syscalls), то есть запросам к ядру ОС. Библиотечные функции fopen/fread/fclose — обёртка над системными вызовами openat/read/close. Утилита strace перехватывает все системные вызовы процесса и печатает их с аргументами и результатами:
strace -o trace.log ./mindvault
strace -e trace=openat,read ./mindvault
strace -c ./mindvault
Пример из запуска учебной программы без лицензии:
openat(AT_FDCWD, "notes.dat", O_RDONLY) = 3
openat(AT_FDCWD, "license.key", O_RDONLY) = -1 ENOENT (No such file or directory)
Программа сначала читает notes.dat, затем пытается открыть license.key и получает ошибку -1 с кодом ENOENT. Возвращаемое значение -1 и код ошибки errno — стандартный механизм сообщений ядра о неудаче системного вызова (подробнее — man 3 errno).
strace и gdb отвечают на вопрос "что делает программа?", но не "по каким правилам?". Для ответа на второй вопрос нужен статический анализ. Ghidra — открытый фреймворк реверс-инжиниринга, основу которого составляет декокомпилятор. Он преобразует машинный код обратно в псевдо-С.
Сценарий работы:
File — Import File — загрузить ELF-бинарник.L в окне декомпилятора переименовывает безымянные функции и переменные.Search — For Strings — поиск всех строковых литералов в бинарнике.В декомпиляторе Ghidra вы будете искать функции вычисления ключа лицензии
Декомпилятор показывает псевдо-С, поэтому сверять следует логику, а не текст.
Машинный код x86-64 выполняет проверки в два шага. Шаг первый: инструкция сравнения (cmp, test) вычисляет результат и включает флаги в регистре rflags. Например, флаг ZF (zero flag) включается, если результат был нулевым. У strcmp нулевой результат означает, что строки совпали. Шаг второй: инструкция условного перехода (jcc) смотрит на флаги и решает, прыгать ли на другой адрес. Ключевые инструкции и их коды:
| Инструкция | Код | Переход, если |
|---|---|---|
test eax,eax |
85 c0 |
не переходит, только ставит флаги |
je rel8 |
74 cb |
равно |
jne rel8 |
75 cb |
не равно |
jmp rel8 |
eb cb |
безусловно |
nop |
90 |
ничего не делать |
Классический приём против проверки лицензии — превратить условный переход в безусловный.
Чтобы изменить байт в файле, нужно преобразовать виртуальный адрес инструкции в смещение в файле. Секции ELF проецируются в память со сдвигом, который берётся из таблицы секций:
readelf -S mindvault | grep -A1 .text
# [13] .text PROGBITS 0000000000401600 00001600 ...
Формула: offset = vaddr − vaddr_секции + offset_секции. В этом примере .text лежит в памяти с 0x401600 при смещении в файле 0x1600, то есть offset = vaddr − 0x400000, и инструкции по адресу 0x401829 соответствует смещение 0x1829 в файле. Само изменение байта удобно делать следующим образом:
cp mindvault mindvault_patched
printf '\xeb' | dd of=mindvault_patched bs=1 seek=$((0x1829)) conv=notrunc
При патче критично, чтобы размер файла не изменился, так как все инструкции рассчитаны относительно своих адресов, и сдвиг хотя бы одного байта сломает весь код. Поэтому патч всегда заменяет байты байтами той же длины (1-байтовый код
74на 1-байтовыйeb).
Освоить инструментарий динамического и статического анализа бинарных файлов в Linux: трассировку системных вызовов (strace), отладку (GDB) и реверс-инжиниринг с декомпиляцией (Ghidra). На учебном приложении восстановить алгоритм вычисления лицензионного ключа, написать на языке C кейген, генерирующий корректные файлы лицензий, а также выполнить бинарный патч.
В папке есть mindvault и файл данных notes.dat. Приложение показывает зашифрованную заметку только при наличии корректного файла лицензии license.key (его не даю). В заметке спрятано кодовое слово. Его необходимо прочитать двумя способами: с помощью кейгена и с помощью бинарного патча.
license.key. Зафиксируйте вывод и коды возврата.errno.check_license, изучите структуру лицензии print *lic, выполните функцию целиком (finish) и зафиксируйте возвращаемое значение для верной и неверной лицензии.mindvault, найдите через Symbol Tree функции проверки лицензии и вычисления ключа, получите псевдо-С в декомпиляторе. Восстановите алгоритм вычисления ключа.keygen <владелец> <YYYYMMDD>, генерирующую файл license.key с корректным ключом. Сгенерируйте лицензию для владельца дневника (имя и срок действия программа выводит даже в заблокированном режиме) и получите кодовое слово из заметки.check_license, вычислите файловое смещение, выполните патч так, чтобы проверка ключа всегда завершалась успешно. Подготовьте лицензию с неверным ключом и покажите, что патченый бинарник принимает её и печатает кодовое слово.keygen <владелец> <YYYYMMDD> создаёт файл лицензии, который исходный бинарник принимает, печатается кодовое слово, код возврата 0je, jne, jmp и nop и может предсказать эффект замены je на jne