Эта статья была первоначально размещена на blog.isis.poly.edu 2 июня 2011 г..

В этой статье описываются разделы перемещения ELF, как использовать их для выполнения произвольного кода и как защитить их во время выполнения.

Разделы переезда в формате ELF

Динамически связанный двоичный файл ELF использует поисковую таблицу, называемую глобальной таблицей смещения (GOT), для динамического разрешения функций, которые находятся в разделяемых библиотеках.

Когда вы вызываете функцию, которая находится в общей библиотеке, это выглядит следующим образом. Это общее представление о том, что происходит, есть много вещей, которые делает компоновщик, мы не будем вдаваться в подробности.

Во-первых, вызов фактически указывает на Таблицу Связи Процедур (PLT), которая существует в разделе .plt двоичного файла.

objdump -M intel -d YOUR_BINARY

80484da:       e8 95 fe ff ff        call   8048374 <printf@plt>

Раздел .plt содержит инструкции x86, которые указывают непосредственно на GOT, который находится в разделе .got.plt.

objdump -M intel -d YOUR_BINARY

08048374 <printf@plt>:
 8048374:       ff 25 54 97 04 08      jmp    DWORD PTR ds:0x8049754
 804837a:       68 20 00 00 00         push   0x20
 804837f:       e9 a0 ff ff ff         jmp    8048324 <_init+0x30>

Раздел .got.plt содержит двоичные данные. GOT содержат указатели на PLT или на расположение динамически связанной функции.

objdump -s YOUR_BINARY

Contents of section .got.plt:
 8049738 6c960408 00000000 00000000 3a830408  l...........:...
 8049748 4a830408 5a830408 6a830408 7a830408  J...Z...j...z...
 8049758 8a830408 9a830408                    ........

По умолчанию GOT заполняется динамически во время работы программы. При первом вызове функции GOT содержит указатель обратно на PLT, где вызывается компоновщик, чтобы найти фактическое местоположение рассматриваемой функции (это та часть, о которой мы не будем вдаваться в подробности). Найденное местоположение затем записывается в GOT. При втором вызове функции GOT содержит известное местоположение функции. Это называется «ленивое связывание».

ленивый
При создании исполняемой или разделяемой библиотеки отметьте его
, чтобы динамический компоновщик откладывал разрешение вызова функции до
точки, когда функция вызывается (ленивый привязка), а
чем во время загрузки. По умолчанию используется ленивая привязка. [1]

Есть несколько конструктивных ограничений для GOT и PLT.

  • Поскольку PLT содержит код, который вызывается программой напрямую, он должен быть размещен с известным смещением от сегмента .text.
  • Поскольку GOT содержит данные, которые используются различными частями программы напрямую, его необходимо разместить по известному статическому адресу в памяти.
  • Поскольку GOT имеет «ленивую привязку», он должен быть доступен для записи.

GOT Overwrite

Поскольку мы знаем, что GOT находится в заранее определенном месте и доступен для записи, все, что нужно, - это ошибка, позволяющая злоумышленнику записывать четыре байта в любом месте. Мы воспользуемся следующей уязвимой программой, чтобы смоделировать это. Обратите внимание, что мы работаем с тем же двоичным файлом, который мы рассмотрели выше.

Здесь у нас есть программа, уязвимая к преднамеренности, которая, будем надеяться, достаточно правдоподобна, чтобы сделать эту демонстрацию реалистичной. Понимание этой программы не является необходимым для понимания демонстрируемых методов эксплуатации и предотвращения, она включена только для полноты.

// Include standard I/O declarations
#include <stdio.h>
// Include string declarations
#include <string.h>
// Program entry point
int main(int argc, char** argv) {
    // Terminate if program is not run with three parameters.
    if (argc != 4) {
        // Print out the proper use of the program
        puts("./a.out <size> <offset> <string>");
        // Return failure
        return -1;
    }
    // Convert size to an integer
    int size = atoi(argv[1]);
    // Convert offset to an integer
    int offset = atoi(argv[2]);
    // Place string into its own string on the stack
    char* str = argv[3];
    // Declare a 256 byte buffer on the stack
    char buffer[256];
    // Print the location of the buffer for calculating the offset.
    printf("Buffer:\t\t%8x\n", &buffer);
    // Fill the buffer with the letter 'A'.
    memset(buffer, 65, 256–1);
    // Null-terminate the buffer.
    buffer[255] = 0;
    // Attempt to copy the string into the specified location.
    strncpy(buffer + offset, str, size);
    // Print out the buffer.
    printf('%s', buffer);
    // Return success
    return 0;
}

gcc -g -O0 -Wl,-z,norelro -fno-stack-protector -o YOUR_BINARY YOUR_SOURCE_CODE

Сначала немного разведки. Из проведенного выше исследования мы знаем, что наша запись GOT для printf находится по адресу 0x08049754. Из тестирования программы мы знаем, что наш буфер будет находиться в стеке по адресу 0xbffff284.

(gdb) x 0x08049754
0x8049754 <_GLOBAL_OFFSET_TABLE_+28>:	0x0804837a
(gdb) p -(0xbffff284 - 0x08049754) % 0x80000000
$1 = 1208263888
(gdb) r 4 1208263888 \$\$\$\$
Starting program: /home/hake/code/relro/c 4 1208263888 \$\$\$\$
Buffer:		bffff284
Program received signal SIGSEGV, Segmentation fault.
0x08048374 in printf@plt ()
(gdb) x 0x08049754
0x8049754 <_GLOBAL_OFFSET_TABLE_+28>:	0x24242424

Здесь мы видим, что можем перезаписать запись GOT для printf нашей строкой. Программа аварийно завершает работу, потому что пытается перейти в не отображенную память.

RELRO: RELocation только для чтения

Чтобы предотвратить описанную выше технику эксплуатации, мы можем указать компоновщику разрешить все динамически связанные функции в начале выполнения и сделать GOT доступным только для чтения. Обратите внимание, что мы работаем с другим двоичным файлом ниже, скомпилированным из того же исходного кода.

сейчас
При создании исполняемой или разделяемой библиотеки отметьте ее
, чтобы динамический компоновщик разрешал все символы при запуске программы
или когда разделяемая библиотека связан с использованием
dlopen вместо откладывания разрешения вызова функции до точки
при первом вызове функции. [1]

Этот метод предотвращения эксплуатации известен как RELRO, что означает RELocation Read-Only. Идея проста: сделать разделы перемещения, которые используются для разрешения динамически загружаемых функций, доступными только для чтения. Таким образом, они не могут их перезаписать, и мы не сможем контролировать выполнение, как это было сделано выше.

Вы можете включить Full RELRO с помощью параметра компилятора gcc: -Wl,-z,relro,-z,now. Это передается компоновщику как -z relro -z now. В большинстве современных дистрибутивов Linux по умолчанию используется вариант RELRO, известный как Partial RELRO. При частичном RELRO используется параметр -z relro, но не параметр -z now.

gcc -g -O0 -Wl,-z,relro,-z,now -fno-stack-protector -o YOUR_BINARY YOUR_SOURCE_CODE

80484fa:       e8 95 fe ff ff         call   8048394 <printf@plt>
08048394 <printf@plt>:
 8048394:       ff 25 f0 9f 04 08     jmp    DWORD PTR ds:0x8049ff0
 804839a:       68 20 00 00 00        push   0x20
 804839f:       e9 a0 ff ff ff        jmp    8048344 <_init+0x30>
Contents of section .got:
 8049fd4 fc9e0408 00000000 00000000 5a830408  ............Z...
 8049fe4 6a830408 7a830408 8a830408 9a830408  j...z...........
 8049ff4 aa830408 ba830408 00000000           ............

Запись GOT для printf находится по адресу 0x08049ff0. Буфер будет жить в стеке по адресу 0xbffff284.

(gdb) x 0x08049ff0
0x8049ff0 <_GLOBAL_OFFSET_TABLE_+28>:	0x0804839a
(gdb) p -(0xbffff284 - 0x08049ff0) % 0x80000000
$1 = 1208266092
(gdb) r 4 1208266092 \$\$\$\$
Starting program: /home/hake/code/relro/r 4 1208266092 \$\$\$\$
Buffer:		bffff284
Program received signal SIGSEGV, Segmentation fault.
strncpy (s1=0x8049ff0 "ph\027", s2=0xbffff5fc "$$$", n=4) at strncpy.c:43
43	strncpy.c: No such file or directory.
	in strncpy.c
(gdb) x 0x08049ff0
0x8049ff0 <_GLOBAL_OFFSET_TABLE_+28>:	0x00176870

Здесь мы видим, что мы не можем перезаписать запись GOT для printf нашей строкой. Программа аварийно завершает работу из-за попытки записи в сегмент памяти, доступный только для чтения.

Техническое примечание. Для этой демонстрации были отключены все другие методы защиты от повреждения памяти.

Техническое примечание: RELRO автоматически применяет все указанные средства защиты к следующим сегментам: .ctors, .dtors, .jcr, .dynamic и .got.

Некоторые удобные команды

Отображение записей динамического перемещения objdump -R YOUR_BINARY
Отображение таблицы заголовков программ readelf -l YOUR_BINARY
Отображение таблицы заголовков разделов readelf -S YOUR_BINARY
Отображение перемещений readelf -r YOUR_BINARY

Спасибо

Джон Оберхайде

Цитаты

[1] Справочная страница ld (1)

Связанные ресурсы

RELRO - (не очень известный) метод предотвращения повреждения памяти
Как взломать таблицу глобального смещения с помощью указателей
Глава 9. Динамическое связывание: таблицы глобального смещения
Формат ELF - Как программы выглядят изнутри
Решение проблемы смещения имени / символов в формате ELF