15: strcpy(dyn, "12345");
16: printf ("1: %sn", dyn);
17: free(dyn);
18:
19: /* Теперь перезаписать буфер изрядно */
20: dyn = malloc(5);
21: strcpy(dyn, "12345678");
22: printf("2: %sn", dyn);
23:
24: /* Пройти перед началом выделенного с помощью malloc локального буфера */
25: * (dyn-1) =' ';
26: printf ("3: %sn", dyn);
27: /* обратите внимание, что указатель не освобожден! */
28:
29: /* Теперь двинуться после переменной local */
30: strcpy(local, "12345");
31: printf ("4: %sn", local);
32: local[-1] = ' ';
33: printf("5: %sn", local);
34:
35: /* Наконец, атаковать пространство данных global */
36: strcpy(global, "12345");
37: printf ("6: %sn", global);
38:
39: /* И записать поверх пространства перед буфером global */
40: global[-1] = ' ';
41: printf("7: %sn", global);
42:
43: return 0;
44: }
45:
46: int main (void) {
47: return broken();
48: }
В этой главе мы рассмотрим проблемы в показанном выше сегменте кода. Этот код разрушает три типа областей памяти: память, выделенную из динамического пула памяти (кучи) с помощью malloc(), локальные переменные размещенные в стеке программы и глобальные переменные, хранящиеся в отдельной области памяти, которая была статически распределена при запуске программы[9]. Для каждого класса памяти эта тестовая программа выполняет запись за пределами зарезервированной области памяти (по одному байту) и также сохраняет байт непосредственно перед зарезервированной областью. К тому же в коде имеется утечка памяти, что позволит продемонстрировать, как с помощью различных средств отследить эти утечки.
Несмотря на то что в представленном коде кроется много проблем, в действительности, он работает нормально. Не означает ли это, что проблемы подобного рода не важны? Ни в коем случае! Переполнение буфера часто приводит к неправильному поведению программы задолго до фактического его переполнения, а утечки памяти в программах, работающих длительное время, приводят к пустой растрате ресурсов компьютера. Более того, переполнение буфера является классическим источником уязвимостей безопасности, как описано в главе 22.
Ниже показан пример выполнения программы.
$ gcc -Wall -о broken broken.с
$ ./broken
1: 12345
2: 12345678
3: 12345678
4: 12345
5: 12345
6: 12345
7: 12345
7.2. Средства проверки памяти, входящие в состав glibc
Библиотека GNU С (glibc) предлагает три простых средства проверки памяти. Первые два — mcheck() и MALLOC_CHECK_ — вызывают проверку на непротиворечивость структуры данных кучи, а третье средство — mtrace() — выдает трассировку распределения и освобождения памяти для дальнейшей обработки.
7.2.1. Поиск повреждений кучи
Когда память распределяется в куче, функциям управления памятью необходимо место для хранения информации о распределениях. Таким местом является сама куча; это значит, что куча состоит из чередующихся областей памяти, которые используются программами и самим функциями управления памятью. Это означает, что переполнения или недополнение буфера может фактически повредить структуру данных, которую отслеживают функции управления памятью. В такой ситуации есть много шансов, что сами функции управления памятью, в конце концов, приведут к сбою программы.
Если вы установили переменную окружения MALLOC_CHECK_, выбирается другой, несколько более медленный набор функций управления памятью. Этот набор более устойчив к ошибкам и может обнаруживать ситуации, когда free() вызывается более одного раза для одного и того же указателя, а также когда происходят однобайтные переполнения буфера. Если MALLOC_CHECK_ установлена в 0, функции управления памятью просто более устойчивы к ошибкам, но не выдают никаких предупреждений. Если MALLOC_CHECK_ установлена в 1, функции управления памятью выводят предупреждения о стандартных ошибках при замеченной проблеме. Если MALLOC_CHECK_ установлена в 2, функции управления памятью вызывают abort(), когда замечают проблемы.
Установка MALLOC_CHECK_ в 0 может оказаться полезной, если вам мешает найти ошибку в памяти другая ошибка, которую в этот момент исправить нет возможности; эта установка позволяет работать с другими средствами отслеживания ошибок памяти.
Установка MALLOC_CHECK_ в 1 полезна в случае, когда никаких проблем не видно, поэтому определенные уведомления могут помочь.
Установка MALLOC_CHECK_ в 2 наиболее полезна при работе в отладчике, поскольку при возникновении ошибки он позволяет выполнить обратную трассировку вплоть до функций управления памятью. В результате вы максимально приблизитесь к месту возникновения ошибки.
$ MALLOC_CHECK_=1 ./broken
malloc: using debugging hooks
malloc: используются отладочные функции
1: 12345
free(): invalid pointer 0x80ac008!
free(): недопустимый указатель 0x80ac008!
2: 12345678
3: 12345678
4: 12345
5: 12345
6: 12345
7: 12345
$ MALLOC_CHECK_=2 gdb ./broken
...
(gdb) run
Starting program: /usr/src/lad/code/broken
Запуск программы: /usr/src/lad/code/broken
1: 12345
Program received signal SIGABRT, Aborted.
Программа получила сигнал SIGABRT, прервана.
0x00 с 64 с 32 in _dl_sysinfo_int80() from/lib/ld-linux.so.2
(gdb) where
#0 0x00c64c32 in _dl_sysinfo_int80() from /lib/ld-linux.so.2
#1 0x00322969 in raise() from /lib/tls/libc.so.6
#2 0x00324322 in abort() from /lib/tls/libc.so.6
#3 0x0036d9af in free_check() from /lib/tls/libc.so.6
#4 0x0036afa5 in free() from /lib/tls/libc.so.6
#5 0x0804842b in broken() at broken.c:17
#6 0x08048520 in main() at broken.с:47
Другой способ заставить glibc проверить кучу на непротиворечивость — воспользоваться функцией mcheck():
typedef void(*mcheck Callback)(enummcheck_status status);
void mcheck(mcheck Callback cb) ;
В случае вызова функции mcheck(), функция malloc() размещает известные последовательности байтов перед и после возвращенной области памяти, чтобы можно было обнаружить переполнение или недогрузку буфера, free() ищет эти сигнатуры и, если они были повреждены, вызывает функцию, указанную аргументом cb. Если cb равен NULL, выполняется выход. Запуск программы, связанной с mcheck(), в gdb может показать, какие именно области памяти были повреждены, если только они явно освобождаются с помощью free(). Однако метод mcheck() не может точно определить место ошибки; лишь программист может вычислить это, основываясь на понимании логики работы программы.
Компоновка нашей тестовой программы с mcheck дает следующие результаты:
$ gcc -ggdb -о broken broken.с -lmcheck
$ ./broken
1: 12345
memory clobbered past end of allocated block
память разрушена после конца распределенного блока
Вследствие того, что mcheck всего лишь выдает сообщения об ошибках и завершает работу, найти ошибку невозможно. Для точного обнаружения ошибки потребуется запустить программу внутри gdb и заставить mcheck вызывать abort() при обнаружении ошибки. Можно просто вызвать mcheck() внутри gdb или поместить mcheck(1) в первой строке вашей программы (веред вызовом malloc()). (Следует отметить, что mcheck() можно вызвать в gdb без необходимости компоновки программы с библиотекой mcheck.)
$ rm -f broken; make broken
$ gdb broken
...
(gdb) break main
Breakpoint 1 at 0x80483f4: file broken.c, line 14.
Точка прерывания 1 по адресу 0x80483f4: файл broken, с, строка 14.
(gdb) command 1
Type commands for when Breakpoint 1 is hit, one per line.
End with a line saying just "end".
Наберите команды, которые выполнятся при достижении точки прерывания 1, по одной в строке.
Завершите строкой, содержащей только "end".
> call mcheck(&abort)
> continue
> end (gdb) run
Starting program: /usr/src/lad/code/broken
Запуск программы: /usr/src/lad/code/broken
Breakpoint 1, main () at broken.с: 14
47 return broken();
$1 = 0
1: 12345
Program received signal SIGABRT, Aborted.
Программа получила сигнал SIGABRT, прервана.
0x00e12c32 in _dl_sysinfo_int80() from /lib/ld-linux.so.2
(gdb) where
#00x00el2c32 in _dl_sysinfo_int80() from /lib/ld-linux.so.2