Информационная безопасность
[RU] switch to English


Digital Scream

январь, 2003

[email protected]

Целочисленное переполнение:атака

Здравствуйте! В последнее время значительно выросло количество людей занимающихся безопасностью ИТ. Следовательно, и произошел прорыв в матодах реализации некоторых атак... И именно поэтому эта статья о новом виде атак Integer Overflow.

Что такое Integer?

В каждом языке программирования существуют типы данных. Основными являются char, string, pointer, float и integer. Не будем останавливаться на каждом типе отдельно, а поговорим о последнем - Integer. Integer это тип целого числа, например 2, 1024, 31337, и т.д. Согласно стандарту RFC1832, в 32 битных системах значения переменных данного типа могут лежать в пределах [-2147483648,2147483647]. Его структура выглядит следующим образом:


Как видно целое число занимает в памяти 32 бита, что ровно 4 байтам. Для нас Integer представляется в виде целого числа, но система работает с ним как с двоичным кодом. Где бит MSB устанавливается в единицу если число отрицательное. Хочу сразу уточнить, что если Integer используется как unsigned(только положительные значения), то значения попадут в пределы [0,4294967295].

Что такое Integer Overflow?

Как было изложено выше для переменной типа Integer, отводится в памяти 4 байта. Переполнение происходит при попытке записать в переменную значение превышающее максимально возможное число. Поведение программы в таких ситуациях полностью зависят от используемого компилятора. Потому, как согласно стандарту ISO C99 каждый компилятор при таком переполнении может делать все что угодно, от игнорирования до аварийного завершения программы. В большинства компиляторах какраз ничего и не делается :) Этот вид атаки опасен еще и тем, что приложение не может определить произошло переполнение или нет (вообще-то есть способ определить произошло переполнение или нет, но это уже другая тема...). Переполнения целых чисел можно использовать для влияния на значения некоторых критических данных, например размера буфера, индекса элемента массива, и т.д. Но на практике можно столкнутся с трудностью использования этой уязвимости. Все дело в том это переполнение в большинстве случяев не может непосредственно переписать область памяти(в отличии от Buffer Overflow и Heap overflow). Но применение этой уязвимости может легко вызвать и ошибки другого класса. В большинстве случаев возникает возможность переполнения буфера (Buffer Overflow). По словам ISO C99 уязвимость исчезает при использовании в вычислениях Unsigned Integer. Но давайте проверим действительно ли это так. Обьявим две переменные A и B, типа Unsigned Integer. Далее занесем в А максимальное значение Unsigned Integer - 4294967295, а в В -1:
unsigned int A=0xFFFFFFFF;
unsigned int B=0x1;
В результате выполнения операции (А+В) полученное значение, согласно стандарту ISO, не вмещается в 32 бита. В таком случае результатом будет (А+В) mod 0x100000000. mod - остаток от деления, например (8 mod 2 =0), а (8 mod 3=2). Следовательно, наш результат будет равен выражению:
result=(A+B) % 0x100000000;
подставив наши значения получим следующее:
result=(0xffffffff + 0x1) % 0x100000000;
//result=(0x100000000) % 0x100000000;
//result=0;
Как видно результат вышел равен нулю. Не совсем то, что должно было выйти :) Этот эффект называют "wrap around", тоесть вращение вокруг нуля.

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

Как применить Integer Overflow?

Для начала рассмотрим самый простой вариант. Есть некая программа для роботы с ней нужно ввести логи пароль и длину пароля. Такой вариант вы нигде не встретите в реальной ситуации, хотя если речь идет о Web приложениях такая авторизация возможна.
int main(int argc,char **argv) {

//создаем переменные для хранения данных
 char chLogin[100];
 char chPassword[100];
 int   intPasswordLength;

//заносим в них пользовательские данные
 strcpy(chLogin,argv[1]);
 strcpy(chPassword,argv[2]);
 intPasswordLength=atoi(argv[3]);

//флаг дающий пользователю права администратора
 int admin=0;
 char chOriginalPassword[100]="administrator";
//простая проверка
 if(intPasswordLength<1)intPasswordLength=0;
 intPasswordLength++;
 if(chLogin="admin"){
	admin=1;
	for(i=0;i<=intPasswordLength;i++)
		if((chPassword[i])!=chOriginalPassword[i])
			admin=0;
 }
 setUserStastusAdmin(admin);
}
Хочу сразу сказать что явную возможность Buffer Overflow я упускаю, так как нас интересует другое. Давайте внимательно посмотрим на исходник, есть некая переменная admin, которая используется как флаг. Если ее значение равно нулю, значит пользователь не имеет прав администратора и наоборот. По умолчанию значение флага устанавливается в ноль (то есть гость) , если введенный логин "admin", то флаг устанавливается в один. Дальше происходит посимвольная проверка пароля введенного пользователем и настоящим паролем. В случае нахождения различий флаг снова устанавливается в ноль. Хочу обратить ваше внимание на цикл:
for(i=0;i<=intPasswordLength;i++)
Все дело в том что если intPasswordLength меньше за начальное значение(в данном случае ноль), то цикл выполнятся не будет. Но это в программе предусмотрено:
if(intPasswordLength<1)intPasswordLength=0;
intPasswordLength++;
Но тут как раз вспомнить о Integer Overflow! Поскольку используется не Unsigned Integer, установив значение в 2147483647(0xFFFFFFFF) оно пройдет проверку и увеличится на один, но из выше сказанного получается что результатом будет не 2147483648, а -2147483648. При такой ситуации цикл исполняться не будет и достаточно просто угадать логин.

В принципе механизм взлома зависит от структуры программы. Для прошлого примера упрощенный вариант выглядел бы следующим образом:
int main(int argc, char **argv) {
 int intNum=10;
 printf(" intNum=%d\n", intNum);

 intNum=2147483647;
 printf(" intNum=%d\n", intNum);

//а вот и переполнение:
 fd++;
 printf(" intNum=%d\n", intNum);
}
Как видите все просто. Но как было сказано в начале статьи эта ошибка дает доступ использованию других видов атак. Рассмотрим следующий пример:
main(int argc, char **argv) {
 char chData[100];
 int intOffset=atoi(argv[1]);

//опять проверка
 if(intOffset<0) intOffset=-intOffset;
 intOffset++;

 printf("intOffset=%d/n",intOffset);
 snprintf(chData,sizeof(chData)-intOffset,"%s",argv[2]);
 printf("chData='%s'\n",chData);
}
А теперь давайте поиграем с входными параметрами. Скомпилим наш пример:
gcc -o demo demo.c
Проведем три эксперимента:
[[email protected]]# ./demo 100 AAAAAAAAAA
intOffset=100
chData='AAAAAAAAAA'
[[email protected]]# ./demo 2147483647 AAAAAAAAAA
intOffset=-2147483648
chData='AAAAAAAAAA'
[[email protected]]# ./demo 2147483647 `perl -e 'print "A"x700'`
intOffset=-2147483648
chData='A...[700 символов]...A'
Segmentation fault (core dumped)
[[email protected]]# gdb demo core
GNU gdb 5.0rh-5 Red Hat Linux 7.2
...
#0  0x41414141 in ?? ()
(gdb)
Все, это уже переполнение буфера! Оно возникает, потому что chData размером в 100 байт, не может принять значение длинной (sizeof(chData)-intOffset). Потому как после инкремента intOffset будет отрицательным и результат выражения (sizeof(chData)-intOffset) намного превысит размер буфера chData.

Вот еще один интересный пример:
 int main(int argc, char *argv[]){
  unsigned short shLength;
  int intLength;
  char chBuffer[100];

  intLength=atoi(argv[1]);
  shLength=intLength;

//проверка:
  if(intLength>=100){
  	printf("Error!\n");
  	return -1;
  }

  printf("shLength=%d\n",shLength);
  memcpy(chBuffer, argv[2], intLength);
  intBuffer[intLength] = '\0';
  printf("%s\n",intBuffer);

  return 0;
}
Проведем снова три эксперимента:
[[email protected]]# ./demo 5 AAAAA
    shLength=5
    AAAAA
[[email protected]]# ./demo 100 AAAAA
   Error!
Кажется ничего нельзя сделать , но тут мы ввели тип short его длинна 16 бит. Указав в качестве первого параметра 65536, intLength будет равно 65536, так как для Integer максимальное значение 2147483647. Но в проверке длинны используется тип Unsigned Short, максимальное значение которого 65535. А поскольку мы ввели 65536, то при переводе с Integer в Unsigned Short, значение shLength станет 0, вспомните "wrap around". Следовательно проверка пройдет успешно, а переполнение буфера все равно произойдет:
[[email protected]]# ./demo 65536 AAAAA
    shLength=0
    Segmentation fault (core dumped)
Следующий пример прост до невозможного:
int main(int argc, char *argv[]){
  char *chBufferOne;
  char *chBufferTwo;
  unsigned int intBufferOneLength;
  unsigned int  intBufferTwoLength){
  char chBuffer[256];
  
 .............................

//несложная проверка:
  if((intBufferOneLength+intBufferTwoLength)>256){
            return -1;
        }

  memcpy(chBuffer,chBufferOne,intBufferOneLength);
  memcpy(chBuffer+intBufferOneLength,chBufferTwo,intBufferTwoLength);

 .............................

  return 0;
}
Весь трюк заключается в том, чтобы обойти проверку:
(intBufferOneLength+intBufferTwoLength)>256)
Учитывая особенности типа Unsigned Integer, легко увидеть, что нужно подобрать два числа которые в суме перевалили бы за максимальное значение типа, например:
 intBufferOneLength=0x104;
 intBufferTwoLength=0xFFFFFFFC;
Примеров можно приводить сколько угодно и в каждом из них будет своя изюминка. Невозможно объять необъятное, поэтому если вы решили попробовать себя в этом деле, советую почитать следующие материалы:

1. Название: Modern kinds of system attacks
Автор: Limpid Byte
Адрес: http://www.void.ru/content/1042
Описание: Достаточно хороший документ на тему актуальных в наше время уязвимостей.

2. Название: Basic Integer Overflows
Автор: blexim
Адрес: http://www.phrack-dont-give-a-shit-about-dmca.org/show.php?p=60&a=10
Описание: Лучшее описание уязвимостей класса Integer Overflow.

P.S. некоторые примеры взяты с phrack #60
О сайте | Условия использования
© SecurityVulns, 3APA3A, Владимир Дубровин
Нижний Новгород