Самата дума „препълване“ е доста описателна за уязвимостта, която ще обсъдим в тази публикация. Помислете за чаша, в която се налива вода. Ако общият обем на налятата вода е по-малък или равен на обема на чашата, всичко е розово. Когато обемът на водата надвиши обема на чашата, тя „прелива“ и ще имате мокър под за миене.

Спецификатори и модификатори на тип променлива

В C (и много други езици) стойностите се съхраняват в променливи, които имат определен тип. (Разбира се, има езици като Python, където програмистът не трябва да указва типове променливи.) Има четири основни типа променливи:

  1. въглен
  2. вътр
  3. плавам
  4. двойно

В допълнение към горните типове (наричани също спецификатори на типове), има четири модификатори на типове:

  1. подписан
  2. неподписан
  3. къс
  4. дълго

Типът променлива заедно с (незадължителния) модификатор решава максималните и минималните стойности, които променливата може да съхранява. Например на 32-битови машини тип променлива signed int може да съхранява стойности от -2,147,483,648 до 2,147,483,647, или с други думи signed int е с размер 4 байта. За повече информация относно размерите на типовете данни вижте „C типове данни“

Забележка

Системата, която използвам за всички примери:

kr@k3n@ubuntu:~$ uname -a
Linux ubuntu 4.15.0–36-generic #39~16.04.1-Ubuntu SMP вторник, 25 септември, 09:00:45 UTC 2018 i686 i686 i686 GNU /Linux

Катастрофа на ракета Ariane 5

Ако имаме signed int променлива с име, кажете x и се опитваме да поставим стойност, която е по-голяма от максималната или минималната позволена стойност за signed int , ще прелее. В свободно типизирани езици като C ситуациите на препълване обикновено не се маркират като грешка по време на компилация. gcc вероятно ще даде предупреждение в такива случаи, но те могат да бъдат игнорирани (което е проблем). Помислете за следната програма:

int main(void) {
int x = 0x180000000;
printf (“%d\n”, x);
return 0;
}

В C модификаторът на типа по подразбиране за int е signed. Имаме тип променлива signed int, в който поставяме стойност (по някаква причина) от 0x180000000, което е шестнадесетичен за 6,442,450,944, което очевидно е повече от максимално допустимата стойност от 2 147 483 647. Нека да видим какво се случва, когато компилираме с помощта на gcc и стартираме:

kr@k3n@ubuntu:~$ gcc -o препълване overflow.c
overflow.c: Във функция 'main':
overflow.c:5:13: предупреждение: препълване в неявна константа преобразуване [-Woverflow]
int x = 0x180000000;
^
kr@k3n@ubuntu:~$ ./overflow
-2147483648

C просто погледна 32 бита, отхвърли стойността на 33-ия бит и по-горе. Това е случай на съкращаване, причинено от препълване. Представете си ситуация, при която стойността на x се изисква на по-късен етап от програмата и се очаква, че x ще съдържа 0x180000000. В най-лошия случай това би причинило катастрофа. Помислете за случая с ракетата Ariane 5, която се „разби“ поради „малка компютърна програма, която се опитва да напълни 64-битово число в 16-битово пространство“.

Препълване на цели числа — Примери

Като цяло не всички уязвимости могат да се използват от нападателите. Въпреки това, уязвимостите при препълване са особено опасни, защото могат да доведат до нестабилност на програмата дори при нормална работа и в много случаи също могат да се използват. Преди да разгледаме още примери, ето нещо, което трябва да имате предвид:

Кръг от цели числа

Когато учех за препълването на цели числа, намерих кръга от цели числа (напълно измислено име) полезен за развиване на интуитивно усещане за тази тема.

За signed int, най-значимият бит (MSB) също маркира знака за стойността — битът за знак. Например: 0x7fffffff е равно на 2147483647, но 0x80000000 е равно на -2147483648, което е същото като 2³¹, но с отрицателен знак, тъй като MSB е зададен на 1. За unsigned int не съществува такъв знаков бит. Следователно 0x80000000 е равно на 2147483648.

Препълване възниква, когато две числа с:

  1. се добавят същия знак и резултатът е отрицателен,
  2. различен знак се изваждат и резултатът е положителен.

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

Препълване на цели числа — добавяне

Помислете за следната програма:

int main(void) {
int num1 = 0x7fffffff;
int num2 = 0x1;
int add_result = num1 + num2;
printf (“%d\n” , add_result);

unsigned int num3 = 0xffffffff;
unsigned int num4 = 0x1;
unsigned int uaddition_result = num3 + num4;
printf (“%u\n”, uaddition_result);

връща 0;
}

За нетренирано око стойността в addition_result се очаква да бъде 0x80000000 (или 2 147 483 648), а uaddition_resultсе очаква да бъде 0x100000000 (или 4,294,967,296). Да видим действителните стойности:

kr@k3n@ubuntu:~$ gcc -o overflow overflow.c
kr@k3n@ubuntu:~$ ./overflow
-2147483648
0

Какво …? Ето какво се случи:

  1. Когато 0x1 беше добавен към 0x7fffffff, резултатът стана 0x80000000. И така, защо това причини преливане? Отговорът все още може да бъде представен в 32 бита. Всъщност не. За типа signed int броят битове, налични за стойността, е 31 бита, тъй като 32-ият бит е битът за знак. В този случай 32-ият бит също беше необходим за представяне на сумата и това предизвика препълване.
  2. При добавяне на unsigned int 33-ият бит беше необходим за представяне на сумата и по този начин предизвика препълване.

Отново, представете си, ако стойностите в тези променливи се изискват в по-късните етапи на програмата. Какво бедствие би било, когато операция, очакваща 0x100000000получава вместо това 0x0.

Препълване на цели числа — Умножение

Имам идеален пример за това. Следният код е фрагмент от едно от предизвикателствата в picoctf 2018:

if(number_flags › 0){
int total_cost = 0;
total_cost = 1000*number_flags;
printf(“\nОбщата ви цена е: %d\n”, total_cost) ;
if(total_cost ‹= account_balance){
account_balance = account_balance — total_cost;
printf(“\nВашето ново салдо: %d\n\n”, account_balance);
}
else{
printf(“Няма достатъчно средства\n”);
}

Ето как може да се използва този код:

nikel@pico-2018-shell-1:~$ nc 2018shell1.picoctf.com 5795

Добре дошли в приложението Store V1.0

Най-сигурното приложение за пазаруване в света

[1] Проверка на баланса на сметката

[2] Купете неща

[3] Изход

Въведете избор от меню

1

Баланс: 1100

Добре дошли в приложението Store V1.0

Най-сигурното приложение за пазаруване в света

[1] Проверка на баланса на сметката

[2] Купете неща

[3] Изход

Въведете избор от меню

2

Текущи търгове

[1] Не мога да повярвам, че не е флаг!

[2] Истински флаг

1

Имитационни знамена струват 1000 всеки, колко искате?

1000000000

Общата ви цена е: -727379968

Вашият нов баланс: 727381068

И това е целочислено препълване поради умножение! Обърнете внимание на това твърдение в изходния код:

total_cost = 1000*number_flags

Какво ще стане, ако стойността на продукта надхвърли диапазона от положителни стойности, възможни за signed int променлива, total_cost? Щеше да стане отрицателно и ето какво се случи! Представете си, ако истински производствен софтуер имаше код като този!

Свършен!

Това е всичко! В тази статия специално разгледахме целочислените препълвания, но препълването може да възникне в променлива от всеки тип данни.

Уязвимостите на Overflow лесно се промъкват в кода и е доста трудно да бъдат открити. Най-лесният начин да ги смекчите е да не ги въвеждате на първо място. Фузингът обаче е добра техника за откриване на преливания. Прегледите на кода също могат да помогнат, но се нуждаят от обучено око. Уязвимостите при препълване са опасни, както вече видяхте в случая с Ariane 5 и примера с picoctf.

Благодаря, че прочетохте и ако имате въпроси, моля, уведомете ме в секцията за коментари по-долу и ще се свържа с вас възможно най-скоро.