Типичные ошибки программистов

Олег Зайцев

Введение

Переполнение буфера

Атаки через GUI

   Настройка доступности элементов управления в зависимости от анализа привилегий пользователя

   Утечка паролей из полей ввода

Отсутствие контроля типов параметров и их содержимого

Утечка информации через средства регистрации ошибок

Хранение паролей в теле программы

Собственная реализация идентификации пользователя и ограничения его прав

Применение криптографических алгоритмов собственной разработки

Передача конфиденциальной информации по открытому каналу

Выводы

Введение

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

В начало В начало

Переполнение буфера

Переполнение буфера является очень распространенным видом уязвимости приложений и сводится к записи в некий буфер программы информации, объем которой превышает размер буфера. Подобная ситуация возникает исключительно из-за ошибок программистов, чаще всего в результате использования в программе буферов статического размера для хранения данных динамической длины. Например, выделяя буфер для хранения введенного пароля, программист рассуждает: «Длина пароля — не более 16 символов, поэтому я выделю 30 символов, и этого точно хватит».

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

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

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

 

int _tmain(int argc, _TCHAR* argv[])

{

bool AuthComplete = false;

char Passwd[10];

char Login[10];

int Cnt = 0;

do {

// Запрос имени пользователя

printf(“Enter login:”);

gets(&Login[0]);

// Запрос пароля

printf(“Enter password:”);

gets(&Passwd[0]);

// Увеличение счетчика попыток

Cnt++;

// Проверка имени и пароля

if (strcmp(&Login[0], “Admin”) == 0 &&

strcmp(&Passwd[0], “TopSecret”) == 0)

AuthComplete = true;

} while ((Cnt < 3) && !AuthComplete);

if (AuthComplete)

printf(“OK !”);

else

printf(“Invalid UserName/Password”);

}

 

В данном примере производится ввод имени и пароля пользователя с последующей проверкой имени (должно быть «Admin») и пароля («TopSecret»). В случае несовпадения имени или пароля запрос повторяется. Условие завершения цикла — ввод правильного имени пользователя и пароля или три безуспешных попытки их ввода. Результат проверки сохраняется в переменной AuthComplete. В примере допущены две грубые ошибки: буферы для имени пользователя и пароля имеют фиксированную длину, поэтому ввод имени или пароля длиной более девяти символов приведет к переполнению отведенного для их хранения буфера. Эти переменные хранятся в стеке (на рис. 1 показан дамп памяти, содержащей стек данного примера после ввода имени «Admin» и пароля «TopSecretPwd»).

Поскольку длина строки «TopSecretPwd» составляет 13 байт (12 символов плюс завершающий символ с кодом 0), переполнение буфера уже произошло, но пока без последствий для работы программы. Прямоугольником зеленого цвета помечен байт памяти, отведенный для хранения данных AuthComplete с результатом проверки имени и пароля. Нетрудно заметить, что ввод пароля длиной более 24 символов приведет к тому, что символ номер 24 будет записан в отведенный переменной AuthComplete байт памяти (рис. 2), что позволит злоумышленнику изменить значение этой переменной.

 

Рис. 1. Стек примера после ввода первого образца данных

Рис. 2. Стек примера после взлома

Рис. 2. Стек примера после взлома

Однако после успешного взлома будет выдано сообщение о возможном повреждении стека в районе переменной Passwd. Это сообщение является результатом работы кода, который компилятор разместил в программе для поиска переполнения буферов. Принцип действия этой защиты очень прост: на рис. 1 можно заметить, что пространство между переменными заполнено байтами с кодом 0xCC. Это и есть элемент защиты — в начале программы компилятор размещает машинный код, выделяющий в стеке место для хранения переменных и заполняющий выделенный буфер байтами 0xCC. Соответственно после выполнения примера производится вызов функции _RTC_CheckStackVars, которая в качестве параметра получает указатель на структуру, описывающую размещенные в стеке переменные с указанием их имени и размера (эта структура подготавливается и сохраняется в программе компилятором). Легко заметить, что между переменными в стеке имеется незадействованное пространство размером минимум в 4 байта. В случае нормальной работы программы в этих незадействованных зонах памяти должны остаться байты с кодом 0xCC, а при переполнении буфера — некоторые данные, записанные туда в результате переполнения. На этом и основана работа функции _RTC_CheckStackVars: она проверяет 4 байта после каждой из переменных и при обнаружении в них посторонних данных формирует исключение при помощи _RTC_StackFailure. Ясно, что подобные проверки удобны для программистов, поскольку позволяют выявить очевидные переполнения буфера в ходе отладки программы. Однако программный код нашего примера выполнился с данными, которые были сфабрикованы за счет переполнения буфера.

В данном примере можно достаточно легко вызвать срыв стека — для этого нужно ввести пароль большой длины, например в 40 символов.

Методика защиты:

  • в ходе разработки проекта не применять буферы статического размера; в противном случае следует проводить жесткий контроль объема информации при ее записи в буфер;
  • отказаться от применения в программе «магических» размеров различных массивов и буферов. При задании размера некоего буфера имеет смысл создать именованную константу и применять ее в коде программы — это повысит читаемость кода и во многих случаях упростит его анализ;
  • максимально использовать возможности поиска ошибок, предлагаемые компилятором, а это особенно важно на стадии отладки приложения. При этом необходимо помнить, что подобные проверки снижают быстродействие программы и увеличивают ее объем. В частности, в Microsoft Visual C++ такая настройка может быть произведена в разделе «Code Generation» или с помощью ключей /RTCu, /RTCs, /GS. В компиляторе Borland Delphi предусмотрена опция «Range checking». Важно отметить, что генерируемый компилятором код позволяет выявить небольшое количество характерных ошибок, подобных продемонстрированному выше переполнению размещенного в стеке буфера.
В начало В начало

Атаки через GUI

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

Настройка доступности элементов управления в зависимости от анализа привилегий пользователя

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

 

begin

btnInsert.Enabled := UserIsAdmin;

btnEdit.Enabled := UserIsAdmin;

btnDelete.Enabled := UserIsAdmin;

end;

 

С одной стороны, ошибки в данном коде нет: если пользователь обладает правами администратора, то кнопки btnInsert, btnEdit и btnDelete будут доступна, в противном случае окажутся заблокированными. С другой стороны, разблокировать неактивную кнопку очень легко: достаточно отправить ей соответствующие сообщения. Аналогично можно эмулировать нажатие неактивной кнопки, изменить текст в недоступной для редактирования строке ввода и т.п.

Методика защиты:

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

Утечка паролей из полей ввода

Большинство программ для ввода пароля применяют обычные тестовые поля, в которых символы пароля при отображении заменяются на заданный программистом символ. Утечка пароля может произойти в результате отключения замены символов при отображении или за счет запроса текста, введенного в подобное поле. Задача злоумышленника упрощается тем, что в Интернете существует масса утилит, предназначенных для просмотра содержимого полей ввода пароля (рис. 3).

 

Рис. 3. Поле ввода пароля и утилиты программы WinID

Рис. 3. Поле ввода пароля и утилиты программы WinID

Методика защиты:

  • отказ от применения стандартных GUI-компонентов для ввода паролей, что особенно важно в случае со стандартным компонентом TEdit в Delphi;
  • контроль и блокировка сообщений, присылаемых элементам ввода пароля.
В начало В начало

Отсутствие контроля типов параметров и их содержимого

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

Рассмотрим несколько вариантов характерных ошибок.

Первый вариант очень прост — это небольшая и визуально безобидная вставка на языке PHP:

 

<?php echo “{$HTTP_GET_VARS[‘text_var1’]}”; ?>

 

Данный код вставляет в генерируемую HTML-страницу текст, переданный ему в параметре запроса с именем text_var1. Однако никакого контроля содержимого text_var1 не предусмотрено, а потому злоумышленник вместо текста получает возможность передать произвольный HTML-код, который может содержать вредоносный скрипт.

Методика защиты:

  • контроль типа параметров, в результате которого должно выявляться соответствие переданных данных ожидаемому типу данных;
  • создание единых алгоритмов проверки содержимого параметров, содержащих текстовые данные.

Другой тип ошибки состоит в том, что приложение подготавливает текст запроса к базе данных динамически, вставляя в него некоторые параметры. Отсутствие контроля параметров может привести к тому, что злоумышленник получит возможность модифицировать выполняемый запрос или вообще выполнить в базе произвольные SQL-команды. Предположим, что приложение выполняет запрос к базе Oracle, для чего в разработанном на Delphi приложении имеется код вида:

 

OracleQuery1.SQL.Clear;

OracleQuery1.SQL.Add(‘SELECT EMPNO, ENAME, JOB, SAL’);

OracleQuery1.SQL.Add(‘FROM SCOTT.EMP’);

OracleQuery1.SQL.Add(‘Where DEPTNO = 20’);

if GetParam(‘EMPNO’) <> ‘’ then

OracleQuery1.SQL.Add(‘ AND EMPNO=’+ GetParam(‘EMPNO’);

OracleQuery1.Execute;

 

На первый взгляд код не содержит грубых ошибок: подготавливается текст запроса, выборка всегда ограничена отделом номер 20, при отсутствии параметра с табельным номером сотрудника выводятся данные по всему отделу, а при его наличии добавляется условие фильтрации по табельному номеру. Данный код будет прекрасно работать, но сделает приложение крайне уязвимым для атаки. Например, передача параметра EMPNO, равного «1 or 1=1», приведет к выводу всей таблицы SCOTT.EMP (причина этого очевидна: описанный выше программный код соберет запрос с условием «Where DEPTNO = 20 and EMPNO=1 or 1=1»). Немного усложнив передаваемую строку, можно получить доступ к другим таблицам базы данных, например передача параметра «-1 union all select sid, username||’ - ‘||osuser||’ - ‘||machine, null, null from v$session» приведет к выводу данных о текущих сессиях базы данных вместо данных о сотрудниках 20-го отдела. Подобные ошибки часто встречаются как в web-приложениях, так и в клиентских приложениях в архитектуре «клиент-сервер». Иногда случаются более опасные уязвимости подобного типа, позволяющие злоумышленнику выполнить произвольные SQL-операторы.

Методика защиты:

  • обязательно контролировать типы, размерность и содержимое всех параметров, применяемых при конструировании запросов. Например, в описанном выше примере для устранения данной уязвимости достаточно было применить конструкцию вида «OracleQuery1.SQL.Add(‘ AND EMPNO=’+ IntToStr(StrToIntDef(GetParam(‘EMPNO’), -1));»;
  • по возможности следует избегать динамической сборки и модификации запросов. Помимо повышения уязвимости приложения и затруднения его отладки, динамическая модификация запросов отрицательно сказывается на быстродействии из-за затрат времени и ресурсов на повторный парсинг запросов, построения для них планов выполнения и т.п.;
  • работа приложения с базой данных должна производиться из-под учетной записи пользователя, обладающего минимально необходимыми для работы задачи привилегиями;
  • при разработке клиентских приложений необходимо учитывать возможность модификации приложения злоумышленником — текст запросов достаточно легко найти и изменить в откомпилированной программе. Поэтому желательно снабдить клиентское приложение защитой от модификации;
  • для ответственных задач следует применить дополнительные средства ограничения доступа (например, доступ к таблицам через представления, обработку и модификацию данных при помощи хранимого программного кода, разработка триггеров для расширенного аудита и т.п.).
В начало В начало

Утечка информации через средства регистрации ошибок

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

Методика защиты:

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

Хранение паролей в теле программы

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

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

Методика защиты:

  • изучение применяемых компонентов и правил их использования. Например, в VCL-компонентах DOA для доступа к серверу Oracle имеется свойство DesignConnection, блокирующее сохранение параметров соединения с базой данных в ресурсах создаваемого EXE-файла (рис. 4);
  • анализ приложения на предмет наличия в нем конфиденциальной информации.

 

Рис. 4. Фрагмент ресурсов EXE-файла, использующего компонент DOA

Рис. 4. Фрагмент ресурсов EXE-файла, использующего компонент DOA

В начало В начало

Собственная реализация идентификации пользователя и ограничения его прав

Данная ошибка широко распространена и состоит в том, что идентификация пользователя средствами базы данных подменяется его идентификацией при помощи обрабатывающей запросы пользователя программы, которая, в свою очередь, поддерживает соединение или пул соединений с базой данных. Применяемая в таком приложении методика идентификации пользователей зачастую не выдерживает никакой критики и сводится к наличию в базе данных таблицы с полями «Имя пользователя», «Пароль», «Уровень доступа». В клиент-серверных приложениях данная ошибка нередко усугубляется тем, что пароль пользователя хранится в базе в виде открытого текста (или зашифрован примитивным алгоритмом типа XOR) и запрашивается для проверки на сторону клиента.

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

Утечка данных из таблицы пользователей и их подмена позволяют взломать подобную систему. В случае с приложением типа «клиент-сервер» возможен взлом клиентской программы, сводящийся к отключению проверки идентификационных данных пользователя или нейтрализации программного ограничения его прав. Третьим путем атаки, часто применяемым в случае с клиент-серверным приложением, является соединение с базой данных при помощи неких посторонних программ — это возможно, поскольку выяснить параметры соединения с базой, являющиеся общими для всех пользователей, нетрудно.

Методика защиты:

  • в случае применения подобных способов необходимо предпринять дополнительные меры защиты базы данных — в частности создать учетную запись с минимальными правами доступа;
  • по возможности использовать механизмы идентификации, защиты и аудита базы данных. Многие разработчики игнорируют подобную возможность (в частности, добиваясь совместимости своего приложения с несколькими базами данных различных производителей), что, естественно, снижает защищенность системы. Например, для проверки имени или пароля пользователя можно применить хранимый программный код, доступный приложению только на выполнение. Хранимый программный код, в свою очередь, будет оперировать таблицами, содержащими данные о пользователях, но эти таблицы не будут доступны приложению напрямую;
  • применение особых механизмов защиты базы данных, основанных на блокировке доступа к объектам по определенным условиям, на динамическом назначении привилегий и на установке поведенческих ловушек, различающих почерк работы легитимного приложения и посторонних программ. Описание этих механизмов выходит за рамки данной статьи, однако можно отметить, что у современных серверов баз данных (например, Oracle последних версий) предусмотрена масса средств, позволяющих реализовать подобные методы защиты.
В начало В начало

Применение криптографических алгоритмов собственной разработки

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

Методика защиты:

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

Передача конфиденциальной информации по открытому каналу

Подобные случаи можно часто наблюдать в различных Web-приложениях, немного реже — в приложениях в архитектуре «клиент-сервер». Передаваемые открытым текстом конфиденциальные данные могут быть перехвачены злоумышленником при помощи сниффера и использованы для последующего доступа к системе. В качестве простейшего примера можно рассмотреть стандартную идентификацию пользователя в HTTP-запросе:

 

GET / HTTP/1.1

Accept: */*

Accept-Language: ru

Accept-Encoding: gzip, deflate

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)

Connection: Keep-Alive

Authorization: Basic QWRtaW46VG9wU2VjcmV0

 

В строке Authorization содержатся имя и пароль (Admin:TopSecret), закодированные по алгоритму Base64.

Идентификация при помощи Web-форм еще более уязвима, например:

 

POST / HTTP/1.1

Accept-Language: ru

Content-Type: application/x-www-form-urlencoded

Accept-Encoding: gzip, deflate

Content-Length: 51

Connection: Keep-Alive

Cache-Control: no-cache

id=loginmake&ns=no&UserName=User&Password=TopSecret

 

Методика защиты:

  • применение защищенных протоколов (например, HTTPS) для передачи конфиденциальных данных. В качестве альтернативного варианта возможно применение собственных реализаций защиты на основе несимметричного шифрования;
  • передача вместо имени и пароля некоторой вычисленной по ним хеш-функции.
В начало В начало

Выводы

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

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

 

Представленные в статье примеры вы найдете на нашем CD-ROM.

КомпьютерПресс 3'2006


Наш канал на Youtube

1999 1 2 3 4 5 6 7 8 9 10 11 12
2000 1 2 3 4 5 6 7 8 9 10 11 12
2001 1 2 3 4 5 6 7 8 9 10 11 12
2002 1 2 3 4 5 6 7 8 9 10 11 12
2003 1 2 3 4 5 6 7 8 9 10 11 12
2004 1 2 3 4 5 6 7 8 9 10 11 12
2005 1 2 3 4 5 6 7 8 9 10 11 12
2006 1 2 3 4 5 6 7 8 9 10 11 12
2007 1 2 3 4 5 6 7 8 9 10 11 12
2008 1 2 3 4 5 6 7 8 9 10 11 12
2009 1 2 3 4 5 6 7 8 9 10 11 12
2010 1 2 3 4 5 6 7 8 9 10 11 12
2011 1 2 3 4 5 6 7 8 9 10 11 12
2012 1 2 3 4 5 6 7 8 9 10 11 12
2013 1 2 3 4 5 6 7 8 9 10 11 12
Популярные статьи
КомпьютерПресс использует