|
|
||
Большинство людей, которые говорят, что "хотят выучить низкий уровень", проходят один и тот же цикл. Открывают книгу по ассемблеру x86, читают первые тридцать страниц, доходят до регистров и флагов. Закрывают книгу. Через полгода открывают снова, читают те же тридцать страниц, снова закрывают. Через два года обнаруживают, что помнят ровно одно - что "EAX - это аккумулятор". Дальше книга отправляется в дальний угол, а мечта о "понимании того, как работает компьютер" переходит в разряд вежливых обещаний самому себе.
Причина не в слабости воли и не в сложности темы. Причина в том, что низкий уровень не учитывается через чтение. Он учится через разлом. Когда ты прочитал про стек и флаги, ты знаешь слова. Когда ты один раз подделал возвратный адрес и заставил функцию вернуться в начало другой функции - ты понял стек. Это разные виды знания, и они не конвертируются друг в друга. Тысяча страниц теории не заменяет одного удачного эксплойта, а один удачный эксплойт без теории не объясняет, почему сработало именно это и что ещё возможно.
В этой статье я излагаю метод, который работает не как учебный план, а как инженерный процесс. Это не список тем для изучения и не подборка курсов. Это способ ставить себе задачи, переводить их в практику, измерять собственный прогресс и строить траекторию, которая не разваливается на третьем месяце. Метод проверен на двух категориях людей: студентах, которым нужно за семестр дойти от нуля до рабочего понимания уязвимостей, и взрослых инженерах, которые десять лет писали на Python и решили, что пора понять, что реально делает их код.
Учебник по ассемблеру устроен как телефонный справочник: список инструкций, список регистров, список режимов адресации. На трёхсотой странице ты знаешь, что инструкция MOV EAX, [EBX+4] существует. На пятисотой - что такое флаг переноса. Ни на одной странице тебе не объясняют, зачем всё это нужно и что ломается, если убрать одну инструкцию из реального бинарника. Учебник отвечает на вопросы, которые ты ещё не научился задавать, и не отвечает на те, которые мешают двигаться дальше.
Это не недостаток конкретной книги - это структурное свойство теоретического изложения. В 2015 году Том Чотья и Крис Новакович из Университета Бирмингема опубликовали работу "An Offline Capture The Flag-Style Virtual Machine and an Assessment of Its Value for Cybersecurity Education" (EduSubmit). Они построили курс, в котором одна группа студентов учила теорию из лекций, а другая работала с виртуальной машиной, где каждая уязвимость была встроена в реальный сервис. Через семестр те, кто работал с практическими задачами, решили 70% заданий на финальном экзамене по бинарной безопасности. Те, кто ограничился лекциями, - 31%. Разрыв почти в два раза, и он воспроизводится в десятках других исследований по cybersecurity education.
Причина глубже, чем кажется. Низкий уровень - это не набор фактов, а набор интуиций о том, как реальные устройства реагируют на конкретные операции. Интуиция не строится из утверждений "стек растёт вниз". Она строится из наблюдения за тем, что push 0x41414141 пять раз подряд действительно переписывает сохранённый адрес возврата, и что между ним и твоими данными лежит сохранённое значение EBP. Это знание, которое проходит через руки, а не через глаза.
Вторая проблема теоретического подхода - он создаёт ложное чувство понимания. Ты прочитал главу про calling conventions, прошёл тест из десяти вопросов, ответил правильно на восемь. Год спустя сталкиваешься с реальным дампом стека и не можешь определить, какой calling convention использовался. Тестовая правильность не равна рабочему знанию. В 2018 году Дэниел Вотипка и соавторы из Университета Мэриленда в работе "Hackers vs. Testers: A Comparison of Software Vulnerability Discovery Processes" (IEEE S&P 2018) сравнили, как ищут уязвимости профессиональные хакеры и профессиональные тестировщики. Тестировщики шли от спецификации и проверяли, что код соответствует требованиям. Хакеры шли от аномалий и спрашивали "что произойдёт, если я подам вот это". Первый подход порождал отчёты, второй - реальные найденные уязвимости. Низкий уровень - территория второго подхода.
Низкий уровень не монолит. Это семь слоёв, и попытка выучить их все одновременно - гарантированный способ не выучить ни один. Слои расположены не от простого к сложному, а от прямого контакта с железом к абстракциям:
1. Бит и байт. Как число представляется в памяти, что такое little-endian, как ASCII и UTF-8 кодируются, как шестнадцатеричная запись связана с двоичной. Это база, без которой любой дамп выглядит как шум. Практика: научиться читать дампы памяти в hex-редакторе и узнавать в них строки, числа, указатели.
2. Архитектура набора инструкций (ISA). Регистры общего назначения, флаги, базовые инструкции (mov, add, jmp, call, ret, push, pop), режимы адресации. Практика: дизассемблировать простую C-функцию и вручную объяснить, что делает каждая инструкция.
3. Стек и calling conventions. Как функции передают аргументы, где сохраняется адрес возврата, что такое кадр стека, как работает leave и ret. Практика: написать две функции на C, скомпилировать их с разными соглашениями (-m32 -mpreferred-stack-boundary=2, fastcall, stdcall) и сравнить, что происходит со стеком.
4. Форматы бинарников. ELF и PE: заголовки, секции, символы, таблицы перемещений, GOT/PLT. Практика: разобрать простой ELF-файл утилитой readelf -a и объяснить назначение каждой секции.
5. Системные вызовы и интерфейс с ОС. Как программа выходит на ядро, что такое файловые дескрипторы, что делает fork, exec, mmap. Практика: написать минимальную программу, которая открывает файл и печатает его содержимое, используя только syscall на ассемблере, без libc.
6. Уязвимости и защиты. Переполнения буфера, format string, use-after-free, ROP, обход ASLR и NX. Практика: пройти одну задачу на pwnable.kr или pwn.college, где требуется получить shell через переполнение буфера без защит, потом с NX, потом с ASLR.
7. Ядро и драйверы. Системный вызов как точка входа, ring 0 против ring 3, контекст переключения, прерывания. Практика: написать минимальный модуль ядра Linux, который регистрирует устройство и печатает в dmesg при обращении.
Эта карта - не расписание. Это география. Ты можешь идти в любом порядке, но должен знать, на каком слое находишься. Главная ошибка начинающих - прыгать с первого слоя сразу на шестой ("хочу научиться взламывать"), не пройдя через третий. Эксплойт без понимания стека - это заклинание, а не знание.
Метод, который я предлагаю, называется двойным проникновением. Каждая тема изучается одновременно двумя инструментами: короткой теоретической заметкой и практической задачей. Не последовательно (сначала теория, потом практика), а параллельно. Ты открываешь главу учебника и задачу одновременно. Теория отвечает на вопросы, которые возникают при попытке решить задачу. Задача порождает эти вопросы.
Вот как это выглядит на конкретном примере. Тема - calling conventions на x86-64 Linux. Теоретическая часть: 20 минут чтения про System V AMD64 ABI, первых шесть аргументов передаются в RDI, RSI, RDX, RCX, R8, R9, возвращаемое значение в RAX. Практическая часть: пишешь минимальную программу на C:
int add(int a, int b) { return a + b; } int main() { return add(3, 5); }
Компилируешь без оптимизации: gcc -O0 -fno-stack-protector -o add add.c. Дизассемблируешь: objdump -d add | grep -A 20 '<add>'. Видишь:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov eax, DWORD PTR [rbp-4]
add eax, DWORD PTR [rbp-8]
pop rbp
ret
И вот теперь теория оживает. Ты видишь, что edi и esi действительно содержат аргументы, как написано в ABI. Видишь, что компилятор сохранил их в стек - это кадр функции. Видишь, что результат оказался в eax, как обещано. Это знание закрепилось через руки, а не через чтение.
Теперь вопрос для рефлексии: что изменится, если включить оптимизацию (-O2)? Перекомпилируй, снова дизассемблируй. Большинство людей обнаруживают, что функция add исчезает - компилятор инлайнит её в main. Это открытие, которое не сделаешь из учебника. Это наблюдение, и оно становится частью интуиции.
Двойное проникновение работает на любом слое карты. Тема "форматы ELF" - читай спецификацию ELF Header, одновременно открывай readelf -a ./add и сверяй каждое поле со своим бинарником. Тема "системные вызовы" - читай man-страницу write(2), одновременно пиши программу на ассемблере, которая вызывает write(1, msg, 13) через инструкцию syscall с номером вызова в RAX. Теория без задачи - словарь. Задача без теории - гадание по дампу. Только вместе они становятся пониманием.
Большинство учебных задач разваливаются не потому, что они сложные, а потому, что они плохо поставлены. "Изучить операционные системы" - не задача. Это тема. Задача имеет конкретную форму. Вот шаблон, который работает:
Объект. На каком конкретном артефакте я работаю (файл, программа, дамп, бинарник)?
Действие. Что я с ним делаю (дизассемблирую, отлаживаю, эксплуатирую, модифицирую)?
Условие успеха. Как я узнаю, что задача решена? Это не "я пойму", это конкретное наблюдаемое событие.
Ограничение. Что я НЕ использую (no gdb, no IDA, no writeups)?
Время. Сколько я готов потратить до того, как открою подсказку.
Пример плохой задачи: "разобраться, как работает функция printf в libc". Пример хорошей задачи: "скомпилировать printf('hello'), дизассемблировать результат, найти в .plt адрес, по которому происходит вызов, объяснить, зачем нужен переход через GOT. Условие успеха - я могу нарисовать на бумаге последовательность из четырёх инструкций, которая происходит между call printf и реальным входом в libc-функцию". Ограничение - без gdb, только objdump. Время - 90 минут, потом подсказка."
Эта форма решает две проблемы. Во-первых, она делает успех проверяемым. Ты либо можешь нарисовать последовательность, либо нет - третьего не дано. Во-вторых, она ограничивает время. Главный враг самообучения - задача, на которую можно потратить неделю, не зная, застрял ты или продвинулся. Ограничение по времени заставляет либо признать поражение и открыть подсказку, либо подтвердить победу и двигаться дальше.
Уровень подсказок - отдельный навык. Я использую три уровня. Первый: перечитать теорию по теме. Часто этого достаточно - оказывается, ответ был в прочитанном, но ты не связал его с задачей. Второй: посмотреть на чужой разбор похожей, но не идентичной задачи. Третье: посмотреть разбор именно этой задачи. Пропускать уровни - значит лишать себя обучения. Останавливаться на первом при затянувшемся тупике - значит обманывать себя, что ты "ещё размышляешь". Правило простое: если за отведённое время нет движения - переходишь на следующий уровень. Время - твой контракт с собой, и его нельзя нарушать без последствий для самооценки.
Самый опасный момент в самообучении - когда ты чувствуешь, что "продвигаешься", но не можешь это проверить. Чувство прогресса без метрик - самообман. Через три месяца ты обнаруживаешь, что решил двадцать задач, но из них пятнадцать были на уровне, который ты уже знал, а пять - на новом, но так и не доведены до конца. Метрики нужны, чтобы различать движение и топтание.
Вот пять метрик, которые я рекомендую вести в простом текстовом файле или таблице:
1. Покрытие карты слоёв. Сколько из семи слоёв ты затронул за месяц. Не глубина - именно касание. Если ты месяц сидишь в слое ISA, не трогая стек и форматы, ты сужаешься. Раз в неделю отмечай, какие слои были в работе.
2. Время до решения задачи. От момента открытия задачи до момента подтверждения успеха. Сравнение не между разными задачами (они неравной сложности), а между задачами одного типа. Если твои первые три задачи на ROP заняли 4 часа, 2 часа, 1.5 часа - ты ускоряешься. Если четыре задачи на переполнение буфера заняли 1, 1, 1, 3 часа - ты не учишься, ты топчешься.
3. Уровень подсказки, на котором ты сдался. Если по одной теме ты трижды подряд открываешь разбор - это не случайность, это системная проблема. Записывай. Если по теме "use-after-free" ты на пяти задачах подряд открывал разбор - тема требует более раннего возврата, а не новых задач.
4. Доля задач, решённых без подсказок. Целевой ориентир - 60% на новом уровне, 80% на знакомом. Ниже 40% - ты слишком высокого о себе мнения, бери задачи проще. Выше 90% - ты слишком легко берёшь, бери сложнее.
5. Время между задачами. От решения одной до открытия следующей. Если это время больше двух дней - ты теряешь контекст. Низкий уровень не прощает длинных пауз. Через неделю без практики ты забываешь 30-40% того, что усвоил. Это не твоё слабость, это работа памяти.
Эти метрики не нужно автоматизировать. Текстовый файл с пятью колонками, обновляемый раз в неделю, занимает пять минут. За три месяца в нём видна вся твоя траектория. Ты видишь, где ты застрял, где ты ускорился, где ты обманывал себя красивыми словами вместо реальных задач.
Глобальная цель формулируется не как "выучить низкий уровень". Это не цель, это настроение. Цель имеет форму конкретного достижимого артефакта, который можно показать другим. Вот три примера целей, которые работают:
Цель типа "взломать". К концу года решить 50 задач на pwnable.kr или pwn.college, из них 10 на уровне, где включены ASLR и NX. Артефакт - личный репозиторий с решениями и комментариями "почему это работает".
Цель типа "построить". К концу года написать свой минимальный интерпретатор командной строки, который умеет fork/exec, перенаправление ввода-вывода, пайпы и сигналы. Без использования system() - только прямые syscall. Артефакт - рабочий бинарник и исходный код.
Цель типа "объяснить". К концу года написать серию из 10 технических разборов: разобрать по одному реальному эксплойту CVE прошлого года, объяснить механизм, защиту, которую он обходит, и почему она не сработала. Артефакт - публикации.
Каждая цель раскладывается на кварталы. Первый квартал - слои 1-3 (бит, ISA, стек). Второй - слои 4-5 (форматы, syscall). Третий - слой 6 (уязвимости и защиты). Четвёртый - финальный артефакт. На каждый квартал - своя метрика: количество решённых задач, количество строк кода на ассемблере, количество разобранных CVE. Без этой привязки к календарю цель превращается в мечту, а мечта, как показывает опыт большинства "хочу выучить низкий уровень", заканчивается в дальнем углу.
Один важный нюанс. Траектория не должна быть линейной. Я рекомендую структуру 70/20/10. 70% времени идёт на основную траекторию. 20% - на смежные темы, которые расширяют понимание (например, разбираешь x86, но читаешь про RISC-V и ARM). 10% - на чистое любопытство без плана: открываешь случайный CVE, читаешь обсуждение на форуме, ковыряешь дамп незнакомого формата. Эти 10% часто оказываются самыми ценными - они не запланированы, и они приносят неожиданные связи, которые линейный план никогда бы не дал.
Глобальная цель без квартальных метрик - это лозунг. Квартальные метрики без глобальной цели - это суета. Они работают только вместе. Один раз в месяц открывай свой файл метрик, смотри на траекторию и задавай один вопрос: если я продолжу в том же темпе, дойду ли я до артефакта за оставшееся время? Если нет - меняешь темп, а не цель. Цель остаётся, потому что без неё теряется смысл идти куда-то.
Самый дорогой антипаттерн - бесконечная подготовка. Человек читает статьи о том, какие книги лучше, какой курс пройти, какие инструменты установить. Устанавливает три дистрибутива Linux, четыре отладчика, два декомпилятора. Готовится год. За год не решает ни одной задачи. Подготовка становится заместителем действия, и это зависимость. Лекарство простое: первое, что ты делаешь в первый день - решаешь одну задачу на OverTheWire Bandit. Это занимает 20 минут. После этого ты уже не "готовишься", ты "учишься".
Второй антипаттерн - прыжки через ступени. Начинающий хочет эксплойты в первый месяц. Не пройдя ISA и стек. Это кончается одинаково: он копирует чужой payload, не понимая ни одного байта, получает shell, и объявляет, что "научился". Через неделю другая задача - и он снова копирует. Через год он решил 200 задач, но не может объяснить, что такое ret2libc и чем отличается от ROP. Знание не накапливается, потому что фундамент не залит. Карта слоёв существует именно для этого: если ты на слое 6, но не прошёл слой 3 - вернись. Это не проигрыш, это правильная инженерная дисциплина.
Третий - имитация работы. Человек открывает GDB, ставит брейкпоинты, шагает по инструкциям, смотрит на регистры. Не задаёт ни одного вопроса о том, что видит. "Я сегодня два часа разбирал бинарник" - говорит он. Но он не разбирал, он смотрел. Разбор - это серия гипотез и проверок. "Думаю, это цикл" - проверяешь, переходишь по адресу трижды. "Думаю, это сравнение строк" - смотришь на функцию strcmp в call. Без гипотез время в отладчике - это медитация, а не обучение.
Четвёртый - игнорирование рефлексии. Решил задачу - перешёл к следующей. Не сделал главное: не записал, что именно узнал. Не записал, на чём застрял. Не записал, какая интуиция появилась. Без этой записи знания растворяются. Через месяц ты помнишь, что "что-то такое было", но не помнишь, что именно. Решение: после каждой задачи - пять строк в журнале. Что я сделал. Что было непонятно. Что стало понятным. Какая интуиция осталась. Какой следующий вопрос эта задача породила. Пять строк. Больше не нужно. Эффект через три месяца - колоссальный.
Пятый - сиротство без сообщества. Самообучение не значит одиночество. Контакт с другими, кто идёт по той же дороге, ускоряет в три-пять раз. Не потому что они объяснят тебе то, что ты не понял. А потому что они зададут вопросы, которые ты не догадался задать. CTF-команды, каналы Discord по pwn.college, форумы root-me. Любой формат, где можно показать свой разбор и услышать чужой - работает. Без этого ты ограничен собственными вопросами, а собственных вопросов на раннем этапе всегда мало.
Шестой, и самый тонкий - переоценка теории как награды. Когда задача решена, возникает соблазн "теперь надо прочитать всё про это, чтобы закрепить". Ты садишься за чтение на три часа. Через три часа ты знаешь больше слов, но не сделал ни одной новой задачи. Теория - это инструмент для задачи, не награда за задачу. Если задача решена - открывай следующую. Чтение по теме включается, когда задача не решается, а не когда она решена.
Все шесть антипаттернов объединяет одна черта: они создают ощущение движения без реального движения. Чувство учебы - самый опасный наркотик в этой сфере. Оно обманывает тебя, как складской счётчик обманывает грузчика, считающего шаги без переноски коробок. Метрики из предыдущего раздела нужны в первую очередь для того, чтобы различать движение и его имитацию.
Низкий уровень - не привилегия избранных и не предмет для академических курсов. Это инженерная дисциплина, в которой продвижение измеряется решёнными задачами, а не прочитанными страницами. Метод, который я описал, не обещает быстрого пути. Он обещает честный. Через двенадцать месяцев траектории с метриками и рефлексией ты окажешься в точке, которую не предсказал бы ни один учебник: ты будешь не просто "знать" устройство стека или формат ELF, ты будешь их узнавать в дампах с первого взгляда, как язык родного города. Это и есть настоящий низкий уровень - не словарь, а интуиция, прошедшая через руки.
Главное, что нужно унести из этой статьи: теория без задачи - это чтение, задача без теории - это гадание, и только их пара, повторённая достаточно много раз, превращается в понимание. Не трать годы на подготовку. Открой OverTheWire Bandit сегодня. Не завтра, не после установки ещё одного дистрибутива, не после прочтения ещё одной книги. Сегодня. У тебя двадцать минут, чтобы решить первую задачу. После неё ты уже будешь не тем, кто "хочет выучить низкий уровень", а тем, кто его учит. Это разные люди, и расстояние между ними измеряется одной решённой задачей.
|