Buffer overflow შეტევები
ყველამ რაღაც იცის!
ზოგადად, როდესაც buffer overflow-ს ვახსენებ, ყველა პასუხობს კონცეფციას და თეორიულად ხსნიან თუ როგორ მუშაობს ეს შეტევა. მაგრამ პრობლემა ისაა, რომ სიღრმისეულად ძალიან ცოტას თუ ესმის ამ საოცარი შეტევის არსი.
შეიძლება ითქვას, რომ buffer overflow არის ყველაფრის საწყისი, როდესაც საქმე ჰაკინგს ეხება, რადგან უმეტესწილად სწორედ memory corruption-ით ხდება სისტემაზე წვდომის მოპოვება.
ეს თემა საკმაოდ რთულია და ბევრ ცოდნას მოითხოვს, რათა ადამიანმა აზრი სრულად გამოიტანოს, მაგრამ ვეცდები რაც შეიძლება მარტივად ავხსნა.
კომუნიკაციის მთელი მუღამი
უფრო კომფორტულია
იდეაში, ელექტრო მოწყობილობები იმიტომ შევქმენით, რომ ერთმანეთს უფრო მარტივად ვესაუბროთ. მაგალითად, SMTP და მსგავსი პროტოკოლები იმ პრობლემას ჭრის, რომ აღარ გვჭირდება მტრედზე მობმული ფურცელი, სადაც სათქმელი ეწერა. ახლა შესაძლოა დაჯდე კომპიუტერთან და შენს ნაცნობს მეილი გაუგზავნო.
მაგრამ რის ფასად?
ავიღოთ მეილის კლიენტი. ანუ პროგრამა, რომლიდანაც მეილი უნდა გააგზავნოს ადამიანმა, როგორიცაა Outlook, მაგალითად.
როდესაც წერილი იწერება, ტექსტი ჯდება წინასწარ შექმნილ ადგილას, ანუ buffer-ში. პრობლემა ისაა, რომ კომპიუტერების რესურსი ამოწურვადია, ასე, რომ buffer წინასწარ უნდა განისაზღვროს:
ეს ლინუქსზე
1
unsigned char* buffer = (unsigned char*) malloc(sizeof(unsigned char) * 1024);
ეს ვინდოუსზე:
1
LPVOID buffer = VirtualAlloc(NULL, sizeof(unsigned char*) * 1024, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ორივეგან გამოიყოფა buffer, სადაც 1024 სიმბოლო ჩაეტევა. პრობლემა აქ მაშინ ჩნდება, როდესაც შიგნით ინფორმაციის ჩაწერას ვცდილობთ.
1
2
char buffer[1024];
getchar(buffer);
ლინუქსის manual-ს(სახელმძღვანელოს) მიხედვით getchar(); ფუნქციის გამოყენება არ შეიძლება, რადგან მოწყვლადია buffer overflow შეტევების მიმართ. ეს იმიტომ, რომ getchar(); არ უკვირდება თუ რა რაოდენობის ინფორმაცია ჩადის buffer-ში.
ასე, რომ შესაძლებელია buffer-დან გადასვლა და სხვა, ბევრად უფრო მნიშვნელოვანი ინფორმაციის შეცვლა.
რა ინფორმაციის?
მაგალითად Stack-ის. რა შუაშია Stack? როდესაც ერთი ფუნქციიდან მეორე ფუნქციის გამოძახება ხდება call ინსტრუქციით, ეს ინსტრუქცია Stack-ზე ე.წ. return address-ს შეაგდებს(ანუ იმ მისამართს, სადაც უნდა დაბრუნდეს CPU, როდესაც მეორე ფუნქცია მორჩება მუშაობას). პრობლემა ისაა, რომ შესაძლებელია მავნე კოდის memory-ში მოთავსება და შემდეგ მისი გაშვება, თუ buffer-დან ინფორმაცია გადავაგდეთ Stack-ის იმ ნაწილას, სადაც ეს მისამართია შენახული.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Overflow Happening Here
│
│
Buffer │ Stack
│ │ ┌─────────────────────────────┐
│ │ │ │
│ │ │ │
▼ │ ▼ ▼
┌─────────────────────▼───────────────────────────────────────────────┐
│XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0x12345678│
└▲────────────────────┴────────────────────────────────────▲▲────────▲┘
│ ││ │
└─────────────────────────────Random Data─────────────────┘│ │
│ │
Return Address───────────────┴────────┘
ისიც შესაძლებელია, რომ თვითონ Random Data იყოს მავნე კოდი და buffer-ის დასაწყისში გადაისროლოს IP(Instruction Pointer). IP არის რეგისტრატორი, რომელიც ინახავს შემდეგი ინსტრუქციის მეხსიერების მისამართს.
რა იქნება ბოროტ კოდში?
რადგან CPU-ს პირდაპირ ეკონტაქტება პროგრამა, რა თქმა უნდა, Python და C++ კოდი ვერ დაიწერება. მაგრამ შესაძლებელია ე.წ. shellcode-ის გაცემა, რაც ენკოდირებული Assembly ინსტრუქციებია.
ნებისმიერი რამ შეიძლება იყოს გაწერილი. შესაძლოა C2 სერვერს reverse shell წაუღოს, იმპლანტი გადმოქაჩოს და Registry Run Key-ები შეცვალოს, რომელიმე პროცესის Memory Dump წაიღოს და ა.შ.
საიდან ხდება return address-ზე დაბრუნება?
ყველა ფუნქციას ბოლოში ორი ინსტრუქცია აქვს:
1
2
leave
ret
ეს ინსტრუქციები სტაკიდან return address-ს წამოიღებენ და IP რეგისტრატორში მოათავსებენ. ანუ გამოდის, რომ CPU პირველ ფუნქციაში დაბრუნდა.
პრაქტიკული მაგალითი
1
2
3
4
5
6
7
8
9
10
11
12
#include <Windows.h>
void CheckProcess(HANDLE hProcess){
// ლოგიკა
}
int main(void){
// ლოგიკა
CheckProcess(proc_handle);
return 0;
}
წარმოვიდგინოთ, რომ ეს პროგრამა ჩაიტვირთა RAM-ში. როდესაც // ლოგიკა მორჩება და CheckProcess(proc_handle); გაეშვება, Stack-ზე შევა ის მეხსიერების მისამართი(Memory address), სადაც უნდა დაბრუნდეს CPU. შემდეგ, IP გადახტება CheckProces()-ის თავში და როდესაც CheckProcess ინსტრუქცია მორჩება, IP ისევ main()-ში დაბრუნდება და return address მოშორდება Stack-დან.
საფრთხე სწორედ აქედან მოდის. შესაძლოა გამოყენებული იყოს scanf ფუნქცია, რომელიც ასევე არ ამოწმებს ზომას. მას გადაეცემა ის მეხსიერების მისამართი, საიდანაც უნდა დაიწყოს ინფორმაციის ჩაწერა. ამის მოსაგვარებლად არსებობს scanf_s ფუნქცია, რომელიც შედარებით უსაფრთხოა.
mseal syscall
ლინუქსმა საკმაოდ საინტერესო რამ გააკეთა ამ პრობლემის მოსაგვარებლად. დაამატეს mseal syscall, რომლის დროსაც memory page იკეტება და მისი ნებართვების შეცვლა შეუძლებელი ხდება.
ანუ, როდესაც ჰაკერებმა buffer overflow აღმოაჩინეს, ისინი Stack-ის ნაწილში წერდნენ მავნე კოდს, რადგან ნაგულისხმევად, Stack-ს execution ნებართვა ჩართული ჰქონდა.
შემდეგ, როდესაც Stack-ზე execution ნებართვა მოშორდა, ჰაკერებმა უკვე არსებული memory page-ების მანიპულაცია დაიწყეს. mprotect syscall უკვე არსებული memory page-ის ნებართვებს ცვლიდა.
ახლა კი, არსებობს mseal system call, რომლის გამოყენების დროსაც, შეუძლებელი ხდება ნებართვებით მანიპულაცია, რადგან კერნელი აღარ აძლევს არავის უკვე არსებულ page-ზე ნებართვების შეცვლის უფლებას.
რა თქმა უნდა, ეს ყველაფერი ქმნის პარადოქსს, რადგან კერნელიც შეიძლება, რომ დაიჰაკოს, მაგრამ ამ ეტაპზე ეს კონცეფცია საკმაოდ საინტერესოა და შესაძლოა სამომავლოდ ოპერაციული სისტემების სტანდარტიც გახდეს.