Урок 5. Карта памяти процесса: где лежат код, строки и переменные

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

Виртуальное адресное пространство

Когда вы запускаете ELF, операционная система создаёт для процесса его собственное адресное пространство — как будто вся память принадлежит только ему. В это пространство раскладываются: части самого ELF-файла (код и данные), стек, куча и подключённые библиотеки. Каждая часть попадает в свою область с определёнными правами доступа.

Карта памяти процесса: стек, куча, .bss, .data, .rodata, .text и что где живёт

Сверху вниз (от старших адресов к младшим):

  • Стек — вверху, растёт вниз. Здесь локальные переменные, кадры функций и адреса возврата (весь урок 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 — список всех областей памяти с их правами:

Экран pwndbg: vmmap для map_reader — области с адресами, правами и файлами; отмечены 1 — 0x401000 r-xp (.text), 2 — 0x402000 r--p (.rodata), 3 — 0x404000 rw-p (.data и .bss), 4 — [heap], 5 — код libc r-xp, 6 — [stack]

Пример разобран на бинарнике map_reader.

  1. 0x401000, r-xp — .text: код программы, его можно читать и исполнять.
  2. 0x402000, r--p — .rodata: строки и константы, только чтение.
  3. 0x404000, rw-p — .data и .bss: глобальные переменные, чтение и запись.
  4. [heap] — куча. Она уже есть: стандартная библиотека выделила в ней буфер для ввода.
  5. libc.so.6, r-xp — код библиотеки, адреса вида 0x7ffff7….
  6. [stack] — стек, у самой верхней границы пространства.

Первая строка, 0x400000, — заголовок ELF, а 0x403000 — служебные таблицы загрузчика: после загрузки их сделали доступными только для чтения. Буква p в конце прав значит private: изменения этой памяти видит только сам процесс. Цвета pwndbg расставляет по типу области, легенда — в первой строке.

В какую секцию попадёт строковый литерал "vsosh{...}" из программы на C? · 4 б.

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.

Что это меняет для нас:

  1. Смещения внутри программы не меняются никогда. Меняется только база. Если функция лежит на 0x1136 от начала файла, то при базе 0x400000 она будет по 0x401136, а при случайной базе 0x5555_5555_4000 — по 0x5555_5555_5136. Поэтому в реверсе оперируют смещениями, а не абсолютными адресами.
  2. В дизассемблере и в отладчике адреса будут разные. Ghidra показывает адреса относительно своей условной базы, gdb — реальные, уже случайные. Пугаться не нужно: сопоставляются они по смещению.
  3. Для pwn это принципиально (адрес не угадать заранее), и там есть отдельные приёмы. Мы к ним вернёмся в своё время.

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

Глобальный массив int table[1000]; объявлен без инициализации. Где он окажется? · 4 б.

Чем читать секции статически

Всё это видно, не запуская программу:

  • 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).
Скачать map_reader
R12

Где начинается .rodata

10 б.

Возьми уже знакомый файл 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, из которой собирается флаг.

Пароль не виден в выводе strings. Где логичнее всего искать его дальше? · 4 б.
R5

Читатель карты

14 б.

Скачай крекми map_reader. Он спрашивает пароль и говорит «Access granted» только при правильном.

strings map_reader флаг не покажет — он не хранится строкой. Открой дизассемблер:

objdump -d -M intel map_reader

Найди в функции main цепочку сравнений cmp al, 0x.., прочитай константы по порядку и переведи их из hex в ASCII. Это и есть флаг. Сдай его целиком.

Скачать magic
R6

Магическое число

10 б.

Скачай программу 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.