Привет, когда-то давно я писал свой первый таск на переполнение стека, и вот решил обернуть всё это в пост про введение в категорию pwn.

TL;DR

Исходники доступны ***на гитхабе.*** Таск поднимается несколькими командами, нужен лишь Docker. Решение также лежит в репе.

Если вы уже обладаете навыками пывна, то предлагаю решить таск.

Остальным соболезную — придется читать. Начну с небольшого введения в pwn, далее покажу пошаговое создание простого задания и его пошаговое решение. Очень сильно я углубляться в реверс я не буду, тут даже не будет одного дизассемблирования, но минимальные знания работы исполняемых файлов и ассемблера всё-таки понадобятся.

Приятного чтения.

0x00 Введение

[Небольшая историческая справка]

На начало 2025 года язык C (не путать с C++) является практически самым низкоуровневым языком из придуманных. Он позволяет гибко и напрямую работать с памятью процесса, исключая 100500 слоёв абстракций. Собственно, на нём и написаны более высокоуровневые языки, такие, как Perl, Python, PHP и Go.

Так вот, в стандарте языка C, помимо всего прочего, указан набор интерфейсов для взаимодействия с ОС, который называется C Standart Library (или libc). Согласно стандарту, каждый указанный там интерфейс должен быть реализован, если мы хотим программировать на C. Сама реализация идет уже на усмотрение разработчика.

Собственно, одной из популярных реализаций libc является опенсурсная реализация проекта GNU, и называется она glibc.

[Конец небольшой исторической справки]

Про glibc я упомянул не просто так. Наше внимание привлекает структура некоторых интерфейсов libc и их реализация в glibc. Со временем оказалось, что некоторые функции, о ужас, являются небезопасными, и могут привести к нарушению целостности доступа к памяти по время работы программы с использованием этих функций.

Например, есть функция gets, копирующая стандартный ввод в переменную, например:

#include <stdio.h>

int main(){
	printf("Type your name.. ");
	char name[20];
	gets(name);
	printf("Hello, %s", name);
}

С виду всё хорошо и просто. Стандартный пример с вопросом “как тебя зовут?”. Вот только функция gets не видит размер переменной name (равный 20 байтам), и будет копировать данные в её память (в стеке) независимо от того, ввели мы с клавиатуры 20 символов или 2000. Таким образом, при переполнении размера переменной мы перезапишем важные части памяти стека, что приведет к его нарушению целостности. Такая проблема называется Stack Corruption. А если сойдутся все звезды, то эту проблему можно превратить в уязвимость выполнения произвольного кода.

На этом тривиальном примере показана несовершенность стандарта. В реальности же, компилятор gcc просто не скомпилирует этот пример :)

$ gcc main.c
test.c: In function ‘main’:
test.c:6:9: error: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
    6 |         gets(name);
      |         ^~~~
      |         fgets

На этапе прекомпиляции нам напрямую запрещают использовать небезопасную эту функцию и предлагают заменить её на fgets (В которой необходимо указывать максимальный размер буфера для избежания переполнений).