Находим и устраняем уязвимости бинарных файлов в Linux — с утилитой checksec и компилятором gcc

Kate

Administrator
Команда форума
rkuacjyaf0qpkwe-m91avgkwscm.png


Изображение: 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 вам в помощь!


Источник статьи: https://habr.com/ru/company/macloud/blog/562420/
 
Сверху