Безопасность 64-битного кода

Андрей Карпов
Статей: 373



Аннотация

В статье рассматриваются вопросы обеспечения безопасности программного кода при его адаптации для 64-битных систем.

Введение

Не будем говорить об угрозе взлома программного обеспечения и о размере вреда, который эта угроза может причинить. Об этом написаны многие книги и статьи. Перейдем сразу к новому практическому вопросу в сфере повышения надежности программного кода, связанному с освоением 64-битных систем. И скорее всего, вас не удивит, что речь пойдет о языках Си/Си++, для которых вопросы обеспечения безопасности стоят наиболее остро.

Из-за ошибок и недочетов программный код при переносе с 32-битных на 64-битные системы может стать более восприимчив к атакам, основанных на переполнении буферов. Это связанно с изменением размеров базовых типов данных, что может дать возможность воспользоваться ранее недоступными путями атаки на код. Другими словами код, которой в рамках 32-битных систем был безопасен и не давал возможности использовать его для целей вторжения в систему, после перекомпиляции для 64-битных систем может начать представлять потенциальную угрозу.

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

Анализ программного кода

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

Существует достаточно много различных инструментов статического анализа, обеспечивающих диагностику потенциально опасных участков кода, которые могут быть использованы для различных видов атак. В качестве примера можно привести: ITS4, SourceScope, Flawfinder, АК-ВС [1].

Кстати, недавно познакомился с интересным фактом. Я всегда рассматривал инструменты статического анализа кода, как средства поиска ошибок в программах, с целью сделать ее более надежной и устойчивой к входным данным. Но оказывается, хакеры также используют инструменты статического анализа, но с противоположной целью [2]. Они выявляют потенциально ненадежные места в программах, для дальнейшего их подробного изучения. Вручную просматривать код современных приложений практически нереально из-за их размеров, и статический анализ оказывается им хорошим подспорьем. После дизассемблирования кода, хакеры с помощью статического анализа отсеивают наиболее интересные области кода для изучения. Например, они могут искать код, использующий копирование строк и при этом наличие рядом уменьшение/увеличение регистра или ячейки памяти на единицу. Программисты достаточно часто допускают ошибки при работе со строками, когда приходится резервировать дополнительный байт под терминальный символ 0x00 (конец строки). Этот код обычно содержит магические арифметические комбинации, где присутствует -1 или +1. И такой код конечно очень интересен для подробного изучения хакером, поскольку потенциально он может помочь осуществить атаку на основе переполнения буфера.

Но мы отвлеклись. Статические анализаторы помогают программисту выявить потенциально уязвимые участки кода в своих программах и их пользу нельзя недооценивать. Рассмотрим теперь некоторые примера кода, который станет опасным или даже некорректным при переносе на 64-битную систему.

Примеры некорректного и уязвимого кода

С большой коллекцией ошибок, которые возникают в 64-битных программах можно познакомиться в статьях "20 ловушек переноса Си++ - кода на 64-битную платформу" [3] и "Проблемы 64-битного кода на примерах" [4]. Но в данных статьях акцент сделан на ошибки, которые приводят к неработоспособности программы, а не с точки зрения ее уязвимости для атаки.

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

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

void *SpecificMalloc(unsigned int size) {
  return malloc(size);
} 
...
char *buf;
size_t len; 
read(fd, &len, sizeof(len)); 
buf = SpecificMalloc(len);
read(fd, buf, len);

Напомним, что в 64-битных системах (Linux, Windows) размер типа int составляет 32-бита, а size_t - 64-бита. Ошибка заключается в приведении типа size_t к типу unsigned int при вызове функции SpecificMalloc. Если размер файла будет больше 4 гигабайт, то при чтении данных из файла мы выйдем за границы массива, что является недопустимой ситуацией. Конечно, ошибка в данном примере очевидна, но пример показывает опасность явного и неявного приведения типов, которое может возникнуть в 64-битном коде, смешено использующие 32-битные и 64-битные типы для хранения размеров, индексов и так далее.

Другая разновидность угроз кроется в использовании фиксированных размеров буферов и магических констант. Особенно этим грешит старый код, написанный с десяток лет назад программистами, которые не задумывались, что когда-то изменится размер указателя или перемененной типа time_t.

Рассмотрим простой пример переполнения буфера с жестко заданным размером:

char buf[9];
sprintf(buf, "%p", pointer);

Да, такое бывает в программах. Особенно в старых.

Приведем другой пример, где использование магического числа 4 приводит к ошибке выделения необходимого объема памяти:

LPARAM *CopyParamList(LPARAM *source, size_t n)
{
  LPARAM *ptr = (LPARAM *)malloc(n * 4);
  if (ptr)
    memcpy(ptr, source, n * sizeof(LPARAM);
  return ptr;
}

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

int a = -2;
unsigned b = 1;
ptrdiff_t c = a + b;
if (c == -1)
{
  printf("Case: 32-bit\n");
} else {
  printf("Case: 64-bit\n");
}

В этом неаккуратном коде в зависимости от разрядности платформы будут выполняться различные ветки в оператора 'if'. Согласно правилам языка Си++ выражение ptrdiff_t c = a + b; будет вычисляться следующим образом:

  • Значение типа int, равное -2 будет преобразовано в тип unsigned со значением 0xFFFFFFFEu.
  • Произойдет сложение двух 32-битных значений 0x00000001u и 0xFFFFFFFEu, результатом чего станет число 0xFFFFFFFFu размерностью 32-бита.
  • Значение 0xFFFFFFFFu будет помещено в 64-битную переменную знакового типа. Для 32-битной системе это означает, что переменная будет содержать значение -1. В случае 64-битной системы переменная так и будет иметь значение 0xFFFFFFFF.

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

int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B);
*ptr = 10; // Обращение к памяти за пределами массива
           // в случае 64-битной среды.

Подобную ошибку можно использовать, если удастся установить некорректное значение переменных A и B таким образом, чтобы произвести запись в желаемую область памяти.

Ошибки в логике программы легко могут возникнуть и в коде обрабатывающем отдельные биты. Следующий тип ошибки связан с операциями сдвига. Рассмотрим пример:

ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) {
  ptrdiff_t mask = 1 << bitNum;
  return value | mask;
}

Приведенный код работоспособен на 32-битной архитектуре и позволяет выставлять бит с номерами от 0 до 31 в единицу. После переноса программы на 64-битную платформу возникнет необходимость выставлять биты от 0 до 63. Но данный код никогда не выставит биты, с номерами 32-63. Обратите внимание, что "1" имеет тип int и при сдвиге на 32 позиции произойдет переполнение. В результате мы получим значение 0 или 1, что зависит от реализации компилятора. Заметим также, что неисправленный код приведет еще к одной интересной ошибке. При выставлении 31-ого бита на 64-битной системе результатом работы функции будет значение 0xffffffff80000000. Результатом выражения "1 << 31" является отрицательное число -2147483648. Это число представляется в 64-битной целой переменной как 0xffffffff80000000.

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

Если приводимые выше примеры кажутся вам надуманными и нереальными, то предлагаю познакомиться с еще одним кодом (в упрощенном виде), который использовался в реальном приложении в подсистеме UNDO/REDO, хотя выглядит он, пожалуй, наиболее странно:

// Здесь указатели сохранялись в виде строки
int *p1, *p2;
....
char str[128];
sprintf(str, "%X %X", p1, p2);
// А в другой функции данная строка
// обрабатывалась следующим образом:
void foo(char *str)
{
  int *p1, *p2;
  sscanf(str, "%X %X", &p1, &p2);
  // Результат - некорректное значение указателей p1 и p2.
  ...
}

Результатом манипуляций указателями с использованием %X стало некорректное поведение программы на 64-битной системе. Данный пример не столько демонстрирует проблему безопасности 64-битного кода, сколько показывает, как опасны потаенные дебри больших и сложных проектов, которые пишутся многими годами. Если проект достаточно велик и стар, то с большой вероятностью в нем существуют уязвимости и ошибки связанные с предположениями о размерах различных структурах данных, правилах выравнивания данных и много других сюрпризов.

Диагностика уязвимых мест в 64-битном коде

Вначале систематизируем типы целей, которые могут стать доступными для атаки при переносе кода на 64-битную систему:

  • Опасны участки кода с арифметическими выражениями, в которых смешанно используются 32-битные и 64-битные типы данных.
  • Опасны участки кода с адресной арифметикой, где присутствуют операции с 32-битными типами данных.
  • Внимание заслуживают выражения, содержавшие магические константы, которые могут означать размер типов данных, максимально допустимые значения, смещение данных в структурах данных.
  • Целью атаки может стать код содержащий операторы сдвига или иные битовые операции.
  • Потенциальную опасность могут представлять различные операции явного и неявного приведения 32-битных и 64-битных типов между собой.
  • Высокому риску подвержен код, реализующий чтение или ввод данных в которых присутствуют типы, изменившие свой размер на 64-битной системе.

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

На данный момент нет выделенного продукта для контроля безопасности кода при переносе его на 64-битные системы. Однако существует статический анализатор кода PVS-Studio, который полностью поддерживает диагностику всех описанных в этой статье 64-битных проблем уязвимости.

Программный продукт PVS-Studio представляет собой разработку российской компании ООО "Системы программной верификации" и предназначен для верификации современных приложений [5]. PVS-Studio встраивается в среду Microsoft Visual Studio 2005/2008, а также в справочную систему MSDN.

Входящая в состав PVS-Studio подсистема Viva64, помогает специалисту отслеживать в исходном коде Си/Си++-программ потенциально опасные фрагменты, связанные с переходом от 32-битных систем к 64-битным. Анализатор помогает писать безопасный корректный и оптимизированный код для 64-битных систем.

Возможности PVS-Studio покрывают диагностику описанных ранее проблем уязвимостей в 64-битном программном коде. Диагностические возможности данного анализатора избыточны для решения исключительно задач обеспечения безопасности 64-битного кода, поскольку он предназначен не только для выявления потенциальных ошибок, но и например для поиска неоптимальных структур данных. Впрочем, лишние диагностические сообщения легко отключить, используя настройки.

На всякий случай хочется подчеркнуть, что PVS-Studio предназначен для выявления ошибок, возникающих при переносе 32-битных программ в 64-битные системы или при разработке новых 64-битных программ. Но PVS-Studio не диагностирует ошибки, которые могут возникать при использовании небезопасных на любых платформах функций, таких как sprintf, strncpy и так далее. Для диагностики подобных ошибок не следует забывать про уже упомянутые инструменты, такие как ITS4, SourceScope, Flawfinder, АК-ВС. PVS-Studio дополняет ряд этих инструментов, закрывая брешь в диагностики 64-битных проблем, но не заменяет их.

Заключение

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

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

Библиографический список



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;

Андрей Карпов
Статей: 373


Найденные ошибки

Проверено проектов
346
Собрано ошибок
13 188

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;