Изображение: Internet Archive Book Images. Modified by Opensource.com. CC BY-SA 4.0

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

Некоторые из них компилятор включает или отключает по умолчанию. Так в бинарных файлах могут возникать уязвимости, о которых мы не знаем.

Checksec — это простая утилита, позволяющая определить, какие свойства были включены при компиляции. В этой статье я расскажу:

  • как использовать утилиту checksec для поиска уязвимостей;
  • как использовать компилятор gcc для устранения найденных уязвимостей.

Установка checksec


Для Fedora OS и других систем на базе RPM:

$ sudo dnf install checksec

Для систем на базе Debian используйте apt.

Быстрый старт с checksec


Утилита сhecksec состоит из единственного скриптового файла, который, впрочем, довольно большой. Благодаря такой прозрачности вы можете узнать, какие системные команды для поиска уязвимостей в бинарных файлах выполняются под капотом:

$ file /usr/bin/checksec

/usr/bin/checksec: Bourne-Again shell script, ASCII text executable, with very long lines

$ wc -l /usr/bin/checksec

2111 /usr/bin/checksec

Давайте запустим checksec для утилиты просмотра содержимого каталогов (ls):

$ checksec --file=/usr/bin/ls

<strong>RELRO           STACK CANARY      NX            PIE             RPATH     RUNPATH      Symbols       FORTIFY Fortified       Fortifiable    FILE</strong>

Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols        Yes   5       17              /usr/bin/ls

Выполнив команду в терминале, вы получите отчёт о том, какими полезными свойствами обладает этот бинарник, и какими не обладает.  

Первая строка — это шапка таблицы, в которой перечислены различные свойства безопасности — RELRO, STACK CANARY, NX и так далее. Вторая строка показывает значения этих свойств для бинарного файла утилиты ls.

Hello, бинарник!


Я скомпилирую бинарный файл из простейшего кода на языке С:

#include <stdio.h>

int main()

{

        printf(«Hello World\n»);

        return 0;

}

Обратите внимание, что пока я не передал компилятору ни одного флага, за исключением -o (он не относится к делу, а просто говорит, куда выводить результат компиляции):

$ gcc hello.c -o hello

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped

$ ./hello

Hello World

Теперь запущу утилиту checksec для моего бинарника. Некоторые свойства отличаются от свойств

ls (для него я запускал утилиту выше):

$ checksec --file=./hello

<strong>RELRO           STACK CANARY      NX            PIE             RPATH     RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE</strong>

Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   85) Symbols       No    0       0./hello

Checksec позволяет использовать различные форматы вывода, которые вы можете указать с помощью опции --output. Я выберу формат JSON и сделаю вывод более наглядным с помощью утилиты jq:

$ checksec --file=./hello --output=json | jq

{

  «./hello»: {

    «relro»: «partial»,

    «canary»: «no»,

    «nx»: «yes»,

    «pie»: «no»,

    «rpath»: «no»,

    «runpath»: «no»,

    «symbols»: «yes»,

    «fortify_source»: «no»,

    «fortified»: «0»,

    «fortify-able»: «0»

  }

}

Анализ (checksec) и устранение (gcc) уязвимостей


Созданный выше бинарный файл имеет несколько свойств, определяющих, скажем так, степень его уязвимости. Я сравню свойства этого файла со свойствами бинарника ls (тоже указаны выше) и объясню, как это сделать с помощью утилиты checksec. 

Для каждого пункта я дополнительно буду показывать, как устранить найденные уязвимости.

1. Отладочные символы


Начну с простого. Во время компиляции в бинарный файл включаются определённые символы. Эти символы используются при разработке программного обеспечения: они нужны для отладки и исправления ошибок.

Отладочные символы обычно удаляются из той версии бинарника, которую разработчики выпускают для широкого использования. Это никак не влияет на работу программы. Такая очистка (она обозначена словом strip) часто выполняется для экономии места, так как файл становится легче после удаления символов. А в проприетарном программном обеспечении эти символы часто удаляют ещё и потому, что у злоумышленников есть возможность прочитать их в двоичном формате и использовать в своих целях.

Сhecksec показывает, что отладочные символы присутствуют в моём бинарнике, но их нет в файле ls. 

$ checksec --file=/bin/ls --output=json | jq | grep symbols

    «symbols»: «no»,

$ checksec --file=./hello --output=json | jq | grep symbols

    «symbols»: «yes»,


То же самое может показать запуск команды file. Символы не удалены (not stripped).

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, <strong>not stripped</strong>

Как работает checksec


Запустим эту команду с опцией --debug:

$ checksec --debug --file=./hello

Так как утилита checksec — это один длинный скрипт, то для его изучения можно использовать функции Bash. Выведем команды, которые запускает скрипт для моего файла hello:

$ bash -x /usr/bin/checksec --file=./hello

Особое внимание обратите на echo_message — вывод сообщения о том, содержит ли бинарник отладочные символы:

+ readelf -W --symbols ./hello

+ grep -q '\.symtab'

+ echo_message '\033[31m96) Symbols\t\033[m  ' Symbols, ' symbols=«yes»' '«symbols»:«yes»,'

Утилита checksec использует для чтения двоичного файла команду readelf со специальным флагом --symbols. Она выводит все отладочные символы, которые содержатся в бинарнике. 

$ readelf -W --symbols ./hello

Из содержимого раздела .symtab можно узнать количество найденных символов:

$ readelf -W --symbols ./hello | grep -i symtab

Как удалить отладочные символы после компиляции


В этом нам поможет утилита strip.

$ gcc hello.c -o hello

$
 

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>not stripped</strong>

$
 

$ strip hello

$
 

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>stripped</strong>

Как удалить отладочные символы во время компиляции


При компиляции используйте флаг -s:

$ gcc -s hello.c -o hello

$

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=247de82a8ad84e7d8f20751ce79ea9e0cf4bd263, for GNU/Linux 3.2.0, <strong>stripped</strong>

Убедиться, что символы удалены, можно и с помощью утилиты checksec:

$ checksec --file=./hello --output=json | jq | grep symbols

    «symbols»: «no»,

2. Canary


Canary (осведомители) — это «секретные» значения, которые хранятся в стеке между буфером и управляющими данными. Они используются для защиты от атаки переполнения буфера: если эти значения оказываются изменены, то стоит бить тревогу. Когда приложение запускается, для него создаётся свой стек. В данном случае это просто структура данных с операциями push и pop. Злоумышленник может подготовить вредоносные данные и записать их в стек. В этом случае буфер может быть переполнен, а стек повреждён. В дальнейшем это приведёт к сбою работы программы. Анализ значений canary позволяет быстро понять, что произошёл взлом и принять меры.

$ checksec --file=/bin/ls --output=json | jq | grep canary

    «canary»: «yes»,

$

$ checksec --file=./hello --output=json | jq | grep canary

    «canary»: «no»,

$

Чтобы проверить, включен ли механизм canary, скрипт checksec запускает следующую команду:

$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'

Включаем canary


Для этого при компиляции используем флаг -stack-protector-all:

$ gcc -fstack-protector-all hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep canary

    «canary»: «yes»,

Вот теперь сhecksec может с чистой совестью сообщить нам, что механизм canary включён:

$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'

     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)

    83: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2.4

$

3. PIE


Включённое свойство PIE позволяет произвольно размещать в памяти исполняемый код независимо от его абсолютного адреса:

PIE (Position Independent Executable) — исполняемый позиционно-независимый код. Возможность предсказать, где и какие области памяти находятся в адресном пространстве процесса играет на руку взломщикам. Пользовательские программы загружаются и выполняются с предопределённого адреса виртуальной памяти процесса, если они не скомпилированы с опцией PIE. Использование PIE позволяет операционной системе загружать секции исполняемого кода в произвольные участки памяти, что существенно усложняет взлом.

$ checksec --file=/bin/ls --output=json | jq | grep pie

    «pie»: «yes»,

$ checksec --file=./hello --output=json | jq | grep pie

    «pie»: «no»,

Часто свойство PIE включают только при компиляции библиотек. В выводе ниже hello помечен как LSB executable, а файл стандартной библиотеки libc (.so) — как LSB shared object:

$ file hello

hello: ELF 64-bit <strong>LSB executable</strong>, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped

$ file /lib64/libc-2.32.so

/lib64/libc-2.32.so: ELF 64-bit <strong>LSB shared object</strong>, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4a7fb374097fb927fb93d35ef98ba89262d0c4a4, for GNU/Linux 3.2.0, not stripped

Checksec получает эту информацию следующим образом:

$ readelf -W -h ./hello | grep EXEC

  Type:                              EXEC (Executable file)

Если вы запустите эту же команду для библиотеки, то вместо EXEC увидите DYN:

$ readelf -W -h /lib64/libc-2.32.so | grep DYN

  Type:                              DYN (Shared object file)

Включаем PIE


При компиляции программы нужно указать следующие флаги:

$ gcc -pie -fpie hello.c -o hello

Чтобы убедиться, что свойство PIE включено, выполним такую команду:

$ checksec --file=./hello --output=json | jq | grep pie

    «pie»: «yes»,

$

Теперь у нашего бинарного файла (hello) тип сменится с EXEC на DYN:

$ file hello

hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb039adf2530d97e02f534a94f0f668cd540f940, for GNU/Linux 3.2.0, not stripped

$ readelf -W -h ./hello | grep DYN

  Type:                              DYN (Shared object file)

4. NX


Средства операционной системы и процессора позволяют гибко настраивать права доступа к страницам виртуальной памяти. Включив свойство NX (No Execute), мы можем запретить воспринимать данные в качестве инструкций процессора. Часто при атаках переполнения буфера злоумышленники помещают код в стек, а затем пытаются его выполнить. Однако, если запретить выполнение кода в этих сегментах памяти, можно предотвратить такие атаки. При обычной компиляции с использованием gcc это свойство включено по умолчанию:

$ checksec --file=/bin/ls --output=json | jq | grep nx

    «nx»: «yes»,

$ checksec --file=./hello --output=json | jq | grep nx

    «nx»: «yes»,

Чтобы получить информацию о свойстве NX, checksec вновь использует команду readelf. В данном случае RW означает, что стек доступен для чтения и записи. Но так как в этой комбинации отсутствует символ E, на выполнение кода из этого стека стоит запрет:

$ readelf -W -l ./hello | grep GNU_STACK

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10

Отключение NX 


Отключать свойство NX не рекомендуется, но сделать это можно так:

$ gcc -z execstack hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep nx

    «nx»: «no»,

После компиляции мы увидим, что права доступа к стеку изменились на RWE:

$ readelf -W -l ./hello | grep GNU_STACK

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10

5. RELRO


В динамически слинкованных бинарниках для вызова функций из библиотек используется специальная таблица GOT (Global Offset Table). К этой таблице обращаются бинарные файлы формата ELF (Executable Linkable Format). Когда защита RELRO (Relocation Read-Only) включена, таблица GOT становится доступной только для чтения. Это позволяет защититься от некоторых видов атак, изменяющих записи таблицы:

$ checksec --file=/bin/ls --output=json | jq | grep relro

    «relro»: «full»,

$ checksec --file=./hello --output=json | jq | grep relro

    «relro»: «partial»,

В данном случае включено только одно из свойств RELRO, поэтому checksec выводит значение «partial». Для отображения настроек сhecksec использует команду readelf. 

$ readelf -W -l ./hello | grep GNU_RELRO

  GNU_RELRO      0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R   0x1

$ readelf -W -d ./hello | grep BIND_NOW

Включаем полную защиту (FULL RELRO)


Для этого при компиляции нужно использовать соответствующие флаги:

$ gcc -Wl,-z,relro,-z,now hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep relro

    «relro»: «full»,

Всё, теперь наш бинарник получил почётное звание FULL RELRO:

$ readelf -W -l ./hello | grep GNU_RELRO

  GNU_RELRO      0x002dd0 0x0000000000403dd0 0x0000000000403dd0 0x000230 0x000230 R   0x1

$ readelf -W -d ./hello | grep BIND_NOW

 0x0000000000000018 (BIND_NOW)    
      

Другие возможности checksec


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

Проверка нескольких файлов


Нет необходимости для каждого файла запускать отдельную команду. Можно запустить одну команду сразу для нескольких бинарных файлов:

$ checksec --dir=/usr/bin

Проверка процессов


Утилита checksec также позволяет анализировать безопасность процессов. Следующая команда отображает свойства всех запущенных программ в вашей системе (для этого нужно использовать опцию --proc-all): 

$ checksec --proc-all

Вы также можете выбрать для проверки один процесс, указав его имя:

$ checksec --proc=bash

Проверка ядра


Аналогично вы можете анализировать уязвимости в ядре вашей системы.

$ checksec --kernel

Предупреждён — значит вооружён


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



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!