Урок 5. Карта памяти процесса: где лежат код, строки и переменные
Мы разобрали процессор, регистры, стек и научились читать ассемблер. Осталась последняя деталь фундамента — общая карта: где именно в памяти лежит программа, когда её запустили. Это тот план местности, по которому вы будете ориентироваться в каждой задаче: «флаг — вот здесь, проверка пароля — вот тут, глобальная таблица — вон там». А в конце урока вы решите свой первый настоящий крекми, просто читая файл.
Виртуальное адресное пространство
Когда вы запускаете ELF, операционная система создаёт для процесса его собственное адресное пространство — как будто вся память принадлежит только ему. В это пространство раскладываются: части самого ELF-файла (код и данные), стек, куча и подключённые библиотеки. Каждая часть попадает в свою область с определёнными правами доступа.
Сверху вниз (от старших адресов к младшим):
- Стек — вверху, растёт вниз. Здесь локальные переменные, кадры функций и адреса возврата (весь урок 3). Права:
rw-(чтение/запись, но не исполнение). - Библиотеки (libc) и mmap — где-то посередине; адрес libc обычно случаен от запуска к запуску.
- Куча — растёт вверх. Сюда
malloc()выдаёт динамическую память (в реверсе она нужна редко, а в pwn атакам на кучу посвящена отдельная большая тема). Права:rw-. - .bss — глобальные переменные без начального значения; при старте заполнены нулями. В файле места не занимают — хранится только их размер. Права:
rw-. - .data — глобальные переменные с начальным значением, которые программа может менять. Права:
rw-. - .rodata — константы и строковые литералы, только для чтения. Именно сюда «смотрит»
strings. Права:r--. - .text — код программы: сами инструкции. Исполняемый, только для чтения. Это то, что разбирает
objdump -d. Права:r-x.
Где живёт каждая вещь
Это карта, которую держат в голове при реверсе. Спросите себя: «что я ищу и в какой области оно должно быть?»
| Что в коде | Где в памяти |
|---|---|
char buf[64] — локальная переменная |
стек |
malloc(n) |
куча |
"vsosh{...}" — строковый литерал |
.rodata |
int g = 7; — глобальная с значением |
.data |
int g; — глобальная = 0 |
.bss |
сами инструкции mov, cmp, call |
.text |
И обратный, самый полезный вывод: по адресу часто видно область. Адрес вида 0x7fffffff… — это стек. Низкие адреса 0x40xxxx (в программе без PIE) — код и данные самого файла. Высокие «случайные» — библиотеки.
Так эта карта выглядит у живого процесса. Запустим map_reader из задания урока в отладчике pwndbg, остановимся сразу после ввода пароля и попросим команду vmmap — список всех областей памяти с их правами:
Пример разобран на бинарнике map_reader.
0x401000,r-xp—.text: код программы, его можно читать и исполнять.0x402000,r--p—.rodata: строки и константы, только чтение.0x404000,rw-p—.dataи.bss: глобальные переменные, чтение и запись.[heap]— куча. Она уже есть: стандартная библиотека выделила в ней буфер для ввода.libc.so.6,r-xp— код библиотеки, адреса вида0x7ffff7….[stack]— стек, у самой верхней границы пространства.
Первая строка, 0x400000, — заголовок ELF, а 0x403000 — служебные таблицы загрузчика: после загрузки их сделали доступными только для чтения. Буква p в конце прав значит private: изменения этой памяти видит только сам процесс. Цвета pwndbg расставляет по типу области, легенда — в первой строке.
PIE: почему адреса бывают каждый раз разные
Вы наверняка заметили нестыковку. В этом уроке код лежит по 0x401136, а в задании прошлого урока адрес возврата выглядел как 0x000055555555518a. Оба варианта настоящие — разница в режиме сборки.
- Без PIE программа всегда загружается по одному и тому же фиксированному адресу — в Linux это
0x400000. Поэтому.textоказывается около0x401xxx, а данные — около0x404xxx. Адреса предсказуемы: посмотрели в файле — увидели то же самое при запуске. - С PIE (position independent executable) операционная система каждый запуск кладёт программу по случайной базе. Отсюда адреса вида
0x5555_5555_xxxxили0x5654_xxxx_xxxx: они меняются от запуска к запуску. Это защитная мера — часть механизма ASLR (рандомизация адресного пространства), чтобы атакующий не мог заранее знать, куда прыгать.
Как понять, что перед вами? Проще всего по file: у PIE-программы будет написано pie executable (или shared object), у обычной — просто executable.
Что это меняет для нас:
- Смещения внутри программы не меняются никогда. Меняется только база. Если функция лежит на
0x1136от начала файла, то при базе0x400000она будет по0x401136, а при случайной базе0x5555_5555_4000— по0x5555_5555_5136. Поэтому в реверсе оперируют смещениями, а не абсолютными адресами. - В дизассемблере и в отладчике адреса будут разные. Ghidra показывает адреса относительно своей условной базы,
gdb— реальные, уже случайные. Пугаться не нужно: сопоставляются они по смещению. - Для pwn это принципиально (адрес не угадать заранее), и там есть отдельные приёмы. Мы к ним вернёмся в своё время.
Учебные файлы этого модуля собраны без PIE специально — чтобы адреса в ваших экспериментах совпадали с теми, что показаны в уроке.
Чем читать секции статически
Всё это видно, не запуская программу:
readelf -S fileилиobjdump -h file— список секций с адресами и размерами.objdump -d -M intel file— дизассемблировать.text. Ключ-M intelобязателен: без него objdump печатает в синтаксисе AT&T, где операнды идут в обратном порядке, и строку легко прочитать наоборот (урок 4).objdump -s -j .rodata file— hex-дамп секции.rodata(увидеть строки и константы).strings file— быстро вытащить читаемые строки (в основном из.rodata).
Где начинается .rodata
Возьми уже знакомый файл map_reader и посмотри его карту секций — список того, что где лежит:
readelf -S map_reader
(то же самое покажет objdump -h map_reader)
Найди секцию .rodata и сдай адрес, с которого она начинается.
Ответ — в виде 0x..., без ведущих нулей: например, 0x401000.
Крекми: когда flag прячется в коде
В самом первом уроке флаг лежал строкой в .rodata, и strings его выдал. Но авторы задач так не всегда делают. Частый приём: флаг или пароль нигде не хранится целиком — программа сравнивает ваш ввод с ним посимвольно, а каждый ожидаемый символ «зашит» прямо в инструкцию cmp как константа. Тогда strings бесполезен (строки-то нет!), но в .text лежит цепочка сравнений, которую видно в objdump -d.
Как это выглядит (учебный пример на три символа):
cmp al, 0x41 ; ждём символ 'A' (0x41)
jne fail
cmp al, 0x42 ; ждём 'B' (0x42)
jne fail
cmp al, 0x43 ; ждём 'C' (0x43)
jne fail
; сюда дойдём только при вводе "ABC"
Читаем константы по порядку: 0x41 0x42 0x43 → переводим hex в ASCII → ABC. Вот и весь пароль, и программу мы даже не запускали. Ровно это вы сделаете в задании: strings промолчит, а objdump -d покажет цепочку cmp, из которой собирается флаг.
Читатель карты
Скачай крекми map_reader. Он спрашивает пароль и говорит «Access granted» только при правильном.
strings map_reader флаг не покажет — он не хранится строкой. Открой дизассемблер:
objdump -d -M intel map_reader
Найди в функции main цепочку сравнений cmp al, 0x.., прочитай константы по порядку и переведи их из hex в ASCII. Это и есть флаг. Сдай его целиком.
Магическое число
Скачай программу magic. Она просит ввести число и отвечает «Correct!» только для одного правильного.
Не подбирай — найди это число в дизассемблере: там есть сравнение cmp eax, 0x...
objdump -d -M intel magic | grep cmp
Учти: сравнений в выводе будет несколько, и часть из них — служебные. Например, программа сначала проверяет, что scanf успешно прочитал одно значение, — это даёт сравнение с единицей. Такое есть почти в любом бинаре, не принимай его за ответ. Нужное сравнение — то, где уже прочитанное число сверяют с «неслучайной» большой константой.
Какое число нужно ввести? Ответ — десятичное.
Что запомнить
- Запущенная программа живёт в адресном пространстве: сверху стек (вниз), снизу секции файла — .text (код,
r-x), .rodata (константы/строки,r--), .data/.bss (глобальные,rw-), плюс куча (malloc) и библиотеки. - Локальные переменные — на стеке; строки-литералы — в
.rodata; глобальные — в.data/.bss; код — в.text. - По адресу узнаёшь область:
0x7fff…— стек,0x40xxxx— файл (без PIE). - Если
stringsмолчит — флаг может быть «зашит» в.textкак константы вcmp; читайobjdump -d.