Đồ án thực hiện tìm hiểu, nghiên cứu, thực hành các kĩ thuật khai thác lỗ hổng bảo mật trong Linux Kernel. Đồ án gồm chia thành các phần tường ứng với các giai đoạn làm đồ án: Phần 1 tìm hiểu các thành phần trong Linux Kernel như quản lý bộ nhớ, quản lý tiến trình, tương tác với bộ vi xử lý; Phần 2 tìm hiểu các lỗ hổng bảo mật phổ biến trong Linux Kernel, nắm rõ nguyên nhân gốc và các trường hợp dễ dẫn tới lỗ hổng; Phần 3 nghiên cứu, tổng hợp các kĩ thuật khai thác đã biết trên thế giới; Phần 4 thực hành các kĩ thuật nghiên cứu được để viết mã khai thác lỗ hổng, thực hành tấn công trên hệ thống lab; Phần 5 đưa ra các biện pháp phòng chống mà lâu nay các quản trị viên hệ thống chưa hiểu rõ và lắm vững.
Trang 1TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI VIỆN CÔNG NGHỆ THÔNG TIN VÀ TRUYỀN THÔNG
mật trong nhân hệ điều hành Linux
Sinh viên thực hiện : Phạm Văn Khánh
Lớp KSTN - CNTT – K55
Giáo viên hướng dẫn: ThS Lương Ánh Hoàng
HÀ NỘI 12-2015
Trang 2PHIẾU GIAO NHIỆM VỤ ĐỒ ÁN TỐT NGHIỆP
1 Thông tin về sinh viên
Họ và tên sinh viên: Phạm Văn Khánh
Điện thoại liên lạc: 01642642065 Email: phamvankhanhbka@gmail.com
Lớp: KSTN-CNTT-K55 Hệ đào tạo: Kĩ sư
Đồ án tốt nghiệp được thực hiện tại: Hà Nội
Thời gian làm ĐATN: Từ ngày 15/08/2015 đến 18/12/2015
2 Mục đích nội dung của ĐATN
Tìm hiểu, nghiên cứu và thực hành các kĩ thuật khai thác lỗ hổng bảo mật trong Linux Kernel
3 Các nhiệm vụ cụ thể của ĐATN
Tìm hiểu Linux Kernel
Tìm hiểu các lỗ hổng bảo mật trong Linux Kernel
Nghiên cứu các kĩ thuật khai thác lỗ hổng trong Linux Kernel
Thực hành viết mã khai thác một lỗ hổng điển hình minh họa các kĩ thuật tìm hiểu
và nghiên cứu được
4 Lời cam đoan của sinh viên:
Tôi – Phạm Văn Khánh - cam kết ĐATN là công trình nghiên cứu của bản thân tôi dưới sự hướng dẫn của Ths Lương Ánh Hoàng
Các kết quả nêu trong ĐATN là trung thực, không phải là sao chép toàn văn của bất kỳ công trình nào khác
Hà Nội, ngày 18 tháng 12 năm 2015
Tác giả ĐATN Phạm Văn Khánh
5 Xác nhận của giáo viên hướng dẫn về mức độ hoàn thành của ĐATN và cho phép bảo vệ:
………
………
………
………
………
Hà Nội, ngày 18 tháng 12 năm 2015
Giáo viên hướng dẫn Ths Lương Ánh Hoàng
Trang 3TÓM TẮT NỘI DUNG ĐỒ ÁN TỐT NGHIỆP
Đồ án thực hiện tìm hiểu, nghiên cứu, thực hành các kĩ thuật khai thác lỗ hổng bảo mật trong Linux Kernel Đồ án gồm chia thành các phần tường ứng với các giai đoạn làm đồ án: Phần 1 tìm hiểu các thành phần trong Linux Kernel như quản lý bộ nhớ, quản lý tiến trình, tương tác với bộ vi xử lý; Phần 2 tìm hiểu các lỗ hổng bảo mật phổ biến trong Linux Kernel, nắm rõ nguyên nhân gốc và các trường hợp dễ dẫn tới lỗ hổng; Phần 3 nghiên cứu, tổng hợp các kĩ thuật khai thác đã biết trên thế giới; Phần 4 thực hành các kĩ thuật nghiên cứu được để viết mã khai thác lỗ hổng, thực hành tấn công trên hệ thống lab; Phần 5 đưa racác biện pháp phòng chống mà lâu nay các quản trị viên hệ thống chưa hiểu rõ và lắm vững
Trang 4MỤC LỤC
1 Đặt vấn đề và định hướng giải pháp 10
2 Giới thiệu Linux Kernel 12
2.1 Tổng quan về Linux và Linux Kernel 12
2.2 Nhắc lại kiến thức nền tảng về vi xử lý 12
2.3 Quản lý bộ nhớ trong Linux Kernel 16
2.4 Quản lý tiến trình trong Linux Kernel 18
3 Giới thiệu lỗ hổng trong Linux Kernel 21
3.1 Giới thiệu chung về lỗ hổng 21
3.2 Lỗ hổng NULL Pointer Dereference 23
3.3 Lỗ hổng Stack Buffer Overflow 25
3.4 Lỗ hổng Kernel Heap Overflow 28
3.5 Lỗ hổng liên quan đến số nguyên 30
4 Kĩ thuật khai thác lỗ hổng cho Linux Kernel 34
4.1 Bước 1 thu thập thông tin 34
4.1.1 Mục đích thu thập thông tin 34
4.1.2 Các thông tin cần thu thập 35
4.2 Bước 2 chuẩn bị shellcode 41
4.2.1 Tìm vị trí bộ nhớ đặt shellcode 42
4.2.2 Kĩ thuật nâng quyền 45
4.2.3 Khôi phục trạng thái Kernel sau khi nâng quyền 46
4.3 Bước 3 thực thi mã khai thác 46
4.3.1 NULL Pointer Dereference 46
4.3.2 Stack Overflow 48
4.3.3 Heap Overflow 49
4.3.4 Ghi đè giá trị tại địa chỉ bất kì 53
5 Khai thác lỗ hổng thực tế CVE-2013-2094 55
5.1 Khai thác lỗ hổng GHOST để chiếm quyền người dùng từ xa 55
5.2 Khai thác lỗ hổng kernel chiếm quyền toàn bộ hệ thống 56
6 Biện pháp phòng chống 59
6.1 Nâng cấp bản vá Kernel thường xuyên 59
6.2 Cấu hình bảo mật cho hệ thống 61
Trang 5Kết luận 62
Trang 6DANH MỤC HÌNH ẢNH
Hình 1.1 Kiến trúc tổng quan hệ điều hành Linux 11
Hình 1.2 Kiến trúc bộ nhớ little-endian 14
Hình 1.3 Bảng phân trang bộ nhớ trong Linux 16
Hình 1.4 Không gian địa chỉ bộ nhớ trong Linux 17
Hình 1.5 Giao tiếp giữa user mode và kernel mode 18
Hình 1.6 Xử lý ngắt trong Linux Kernel 19
Hình 2.1 Lỗ hổng trong Microsoft Word cho phép chạy chương trình calc.exe 20
Hình 2.2 Khai thác lỗ hổng trong Linux Kernel để chiếm quyền root 21
Hình 2.3 Không gian địa chỉ bộ nhớ userspace và kernelspace trong Linux 22
Hình 2.4 Địa chỉ được cấp phát (mmap) ở địa chỉ 0x00000000 23
Hình 2.5 Mã khai thác (exploit payload) được đặt ở địa chỉ 0x00000000 23
Hình 2.6 Hàm ptr_un_initialized không khởi tạo con trỏ p 24
Hình 2.7 Khung stack của hàm khi được gọi, địa chỉ trả về được đẩy lên stack 25
Hình 2.8 Stack Buffer Overflow xảy ra khi localbuf ghi đè địa chỉ trả về thành giá trị khác .26
Hình 2.9 Mô hình cấu trúc dữ liệu quản lý slab object 27
Hình 2.10 Hai kiểu cấu trúc slab 28
Hình 2.11 Đối tượng target bị ghi đè khi đối tượng Victim tràn 28
Hình 2.12 Đối tượng free nằm ngay sau bị ghi đè thông tin metadata khi đối tượng trước đó tràn 29
Hình 2.13 Khoảng giá trị cho các kiểu số nguyên 30
Hình 2.14 Tràn số nguyên trên biến total 30
Hình 2.15 Tràn số nguyên khi tính kích thước bộ đệm 31
Hình 2.16 Biến len không được kiểm tra dấu, khi nhập giá trị âm sẽ dẫn tới lỗ hổng tràn buf 31
Hình 2.17 Biến pos không được kiểm tra dấu, truyền pos âm sẽ dẫn tới truy cập bộ nhớ trước table 32
Hình 3.1 Lệnh kiểm tra phiên bản Linux Kernel đang sử dụng 34
Hình 3.2 Lệnh kiểm tra kernel module đang được load 35
Hình 3.3 Lệnh dmesg để kiểm tra kernel log từ khi hệ thống khởi động 36
Hình 3.4 File kallsyms chưa thông tin tất cả symbol trong Linux Kernel 36
Hình 3.5 Cấu trúc một vector ngắt 38
Hình 3.6 Một số vector ngắt thông dụng 38
Hình 3.7 Khai báo IDT và cách gọi lệnh sidt để lấy địa chỉ bảng ngắt trong C 38
Hình 3.8 Kĩ thuật NOPSled trong trường hợp chỉ ghi đè được một phần địa chỉ trả về 42
Hình 3.9 Mã khai thác tại địa chỉ 0x00000000 46
Hình 3.10 Cấp phát bộ nhớ tại địa chỉ 0x0 và khởi tạo payload tại đây 46
Hình 3.11 Con trỏ hàm sendpage được gọi nhưng chưa được khởi tạo 47
Hình 3.12 Khung stack khi gọi hàm con, địa chỉ trả về được đẩy lên stack 47
Hình 3.13 Địa chỉ trả về được ghi đè bởi địa chỉ 0x80C03508 48
Hình 3.14 Ghi đè đối tượng lân cận đã được cấp phát 50
Hình 3.15 Ghi đè con trỏ next-free-object để trỏ tới vùng nhớ userland 52
Hình 3.16 Kĩ thuật ghi đè vector ngắt 53
Trang 7Hình 4.1 Khai thác lỗ hổng GHOST để chiếm quyền người dùng Debian-exim 55
Hình 4.2 Định nghĩa và khai báo sử dụng hàm hệ thống perf_event_open 56
Hình 4.3 Cấu trúc perf_event_attr với trường config có kiểu unsigned 64 bit 56
Hình 4.4 Hàm perf_swevent_init ép kiểu config về số nguyên 32 bit có dấu 56
Hình 4.5 Mã khai thác vượt quyền từ người dùng Debian-exim lên root 57
Hình 5.1 Cập nhật bản vá bằng cách sử dụng công cụ yum trên Centos 58
Hình 5.2 Trang tin tức cập nhật bản vá lỗ hổng của Redhat 59
Trang 8DANH MỤC THUẬT NGỮ VÀ TỪ VIẾT TẮT
Linux: hệ điều hành mã nguồn được cung cấp miễn phí dưới dạng các bản phân
phối khác nhau Thành phần chính của Linux là Linux Kernel
Linux Kernel: Thành phần chính của hệ điều hành tương tác với phần cứng, thực
hiện các chức năng quản lý bộ nhớ, quản lý tiến trình, quản lý vào ra thiết bị ngoạivi
Debug: Quá trình chạy từng lệnh trong một chương trình, kiểm tra bộ nhớ, thanh
ghi, trạng thái của chương trình
Debugger: Công cụ cho phép thực hiện debug một chương trình Trình debugger
phổ nhất trên linux là gdb
Remote debug: Thực hiện debug từ xa Kĩ thuật này được sử dụng khi cần debug
kernel máy khác hoặc không được truy cập trực tiếp vào máy khác
Little-endian: một định dạng lưu trữ dữ liệu trong đó các byte ít quan trọng nhất
được lưu ở địa chỉ cao nhất Kiến trúc Intel sử dụng kiểu lưu trữ này
Page frame: Không gian địa chỉ vật lí được chia thành các khối kích thước cố định
để quản lý Mỗi khối này được gọi là một page frame Kích thước mỗi page framethường là 4KB
Page: Không gian địa chỉ ảo được chia thành các khối kích thước cố định để quản
lý Mỗi khối này được gọi là một page Kích thước page thường là 4KB
Page table: Bảng ánh xạ page vào page frame Bảng ánh xạ cho phép tìm địa chỉ
vật lí trên RAM khi biết địa chỉ ảo của một vùng nhớ nào đó
Userland: Vùng nhớ dành cho các tiến trình thông thường, thường 3GB từ địa chỉ
0x00000000 đến 0xC0000000
Kernelland: Vùng nhớ dành cho kernel, thường 1GB từ địa chỉ 0xC0000000 đến
0xFFFFFFFF đối với Linux cho Intel x86
User Mode: Trạng thái của CPU, các lệnh sẽ bị giới hạn không được phép truy cập
không gian địa chỉ dành cho kernel Thỉnh thoảng user mode cũng chỉ trạng tháitiến trình đang chạy trong user path
Kernel Mode: Trạng thái của CPU, các lệnh không bị giới hạn Thỉnh thoảng
kernel mode cũng chỉ trạng thái tiến trình đang chạy trong kernel path (xử lý ngắthoặc gọi hàm hệ thống)
User Path: Mã lệnh đang thực thi nằm ở userland khi đó CPU đang chạy mã lệnh
của tiến trình bên phía user
Kernel Path: Mã lệnh đang thực thi nằm ở kernelland khi đó CPU đang chạy mã
lệnh của kernel (gọi hàm hệ thống, xử lý ngắt, lập lịch)
System Call: Lời gọi hệ thống cho phép tiến trình tương tác với kernel Bản chất
system call chỉ là một hàm bình thường, khi gọi CPU sẽ được chuyển sang kernelmode
Interrupt: Tín hiệu được gửi đến CPU khi có một sự kiện nào đó xảy ra, ví dụ
người dùng gõ phím, di chuột, gói tin mạng đến card mạng
Interrupt Handler: Hàm xử lý ngắt được gọi khi một ngắt nào đó gửi đến CPU và
CPU quyết định xử lý ngắt đó
Vector ngắt (Interrupt vector): Vùng nhớ chứa thông tin về một ngắt cụ thể: địa chỉ
hàm xử lý ngắt, quyền để gọi ngắt,
Trang 9 IDT ( interrupt descriptor table): Bảng lưu trữ thông tin tất cả vector ngắt được sử
dụng trong hệ thống Bản chất IDT sẽ là một mảng lưu trữ trong kernelland, mỗiphần từ mảng lưu trữ một vector ngắt
Lỗ hổng: Là một loại lỗi phần mềm cho phép kẻ tấn công có thể thay đổi được
luồng thực thi của phần mềm theo ý của kẻ tấn công Lỗi thông thường (bug)thường chỉ làm phần mềm hoạt động sai
Shellcode: một đoạn mã lệnh nhỏ được đặt trên vùng nhớ nào đó Mục tiêu khai
thác lỗ hổng thường là để chuyển hướng thực thi đến shellcode
Payload: một đoạn dữ liệu được gửi tới các phần mềm để khai thác lỗ hổng nào đó,
trong payload thường chứa shellcode
Local Privilege Escalation: Lỗ hổng phần mềm trong đó kẻ tấn công đã chiếm
được tài khoản thông thường và muốn chiếm được quyền của tài khoản cao hơn,nhiều quyền hơn Mục tiêu thường là tài khoản root đối với Linux hoặc tài khoảnAdministrator đối với Windows
Dereference: Một con trỏ trong ngôn ngữ C lưu trữ địa chỉ của vùng nhớ.
Dereference là quá trình lấy địa chỉ vùng nhớ và truy cập tới giá trị vùng nhớ đó
NULL Pointer Dereference: Lỗ hổng phần mềm trong đó một con trỏ NULL
được giải tham chiếu, tức là truy cập tới vùng nhớ địa chỉ 0x0
Buffer: một vùng nhớ được cấp phát dùng để lưu trữ dữ liệu, buffer được xác định
bằng địa chỉ byte đầu tiên và kích thước
Buffer Overflow: Lỗ hổng phần mềm gây ra khi sao chép dữ liệu vào buffer Kích
thước dữ liệu lớn hơn kích thước buffer dẫn tới tràn ra khỏi buffer, khi đè luôn cácvùng nhớ nằm sau buffer
Stack Frame: Một vùng nhớ trên stack được cấp phát khi gọi một hàm con Vùng
nhớ này bao gồm tham số truyền vào hàm, địa chỉ trả về khi hàm kết thúc, địa chỉstack frame của hàm cha, biến cục bộ dùng trong hàm
Return Address: Địa chỉ trả về của một hàm lưu trên stack Khi hàm kết thúc gọi
lệnh ret, nó sẽ pop địa chỉ trả về trên stack và chuyển hướng EIP vào địa chỉ này.Hàm cha sẽ tiếp tục chạy từ vị trí gọi hàm con
Saved Frame Pointer: Địa chỉ stack frame của hàm cha được lưu trên stack frame
của hàm con Địa chỉ hiện tại của hàm con lưu trong thanh ghi EBP Địa chỉ nàydùng để khôi phục lại EBP khi hàm con thực hiện xong
Stack Overflow: Thuộc loại lỗ hổng buffer overflow khi mà buffer được cấp phát
trên stack
Heap Overflow: Thuộc loại lỗ hổng buffer overflow khi mà buffer được cấp phát
động trên heap thông qua các hàm malloc (userland) hoặc kalloc (kernelland)
Infoleak: Một loại lỗ hổng trong đó dữ liệu quan trọng bị đọc mất Chẳng hạn một
vùng nhớ kernelland bị đọc bởi tiến trình userland mặc dù tiến trình này không cóquyền truy cập vùng địa chỉ kernel
Executable: Trạng thái để đánh dấu một vùng nhớ có quyền thực thi.
MỞ ĐẦU
Trang 10Ngày nay an toàn thông tin là một vấn đề hết sức quan trọng Các hệ thốngcông nghệ thông tin luôn tiềm ẩn nhiều nguy cơ mất an toàn đặc biệt các lỗ hổngbảo mật Nghiên cứu về lỗ hổng bảo mật giúp ta hiểu rõ cách thức hacker lợi dụngcác lỗ hổng để tấn công hệ thống từ đó xây dựng được các biện pháp phòng chống,nâng cao mức độ an toàn cho hệ thống Từ trước đến nay, các lỗ hổng bảo mật ở lớpứng dụng đã từng được biết đến và nghiên cứu khá nhiều ở Việt Nam chẳng hạn lỗhổng SQL Injection tấn công website, lỗ hổng trên Microsoft Office cho phép càiđặt virus lên máy tính người dùng,… Tuy nhiên một lớp lỗ hổng ở sâu trong nhâncủa hệ điều hành lại chưa thực sự được quan tâm Nguyên nhân chính do nhân hệđiều hành là một thành phần phức tạp của hệ thống, khó để tìm hiều và đào sâunghiên cứu, đòi hỏi nắm vững nhiều kiến thức căn bản về hệ điều hành, vi xử lýmáy tính.
Sau thời gian học tập tại trường, được sự chỉ bảo hướng dẫn nhiệt tình củathầy cô giáo trong viện Công nghệ thông tin và truyền thông, trường đại học BáchKhoa Hà Nội, em đã kết thúc khoá học và đã tích luỹ được vốn kiến thức nhất định.Được sự đồng ý của nhà trường và thầy cô giáo trong viện em được giao đề tài tốt
nghiệp: “Nghiên cứu các kĩ thuật khai thác lỗ hổng bảo mật trong nhân hệ điều
hành Linux”
Đồ án tốt nghiệp của em bố cục gồm sáu chương:
Chương 1: Đặt vấn đề và định hướng giải pháp
Chương 2: Giới thiệu Linux Kernel
Chương 3: Giới thiệu các lỗ hổng trong Linux Kernel
Chương 4: Kĩ thuật khai thác lỗ hổng trong Linux Kernel
Chương 5: Thực hiện khai thác một lỗ hổng điển hình trong Linux Kernel
Hà nội, ngày 18/12/2015 Phạm Văn Khánh
Trang 111 Đặt vấn đề và định hướng giải pháp
Đặt vấn đề:
Linux Kernel được sử dụng rất phổ biến trên các máy chủ, thiết bị di động, thiết bịnhúng Thống kê năm 2015 khoảng 1 tỷ 400 triệu điện thoại Android chạy Linux,35.9% số lượng máy chủ trong số 1 triệu máy chủ khảo sát chạy hệ điều hànhLinux, 98.8% siêu máy tính trong số 500 siêu máy tính chạy Linux (nguồnWikipedia) Tuy nhiên nhận thức về các lỗ hổng bảo mật cho chúng lại chưa thực
sự được quan tâm Sở dĩ như vậy vì kiến thức về lỗ hổng bảo mật và cách khai tháccác lỗ hổng này còn rất kém đặc biệt đối với các quản trị viên hệ thống, chuyên viênbảo mật ở Việt Nam Qua kinh nghiệm thực tế, em phát hiện ra rằng các máy chủLinux ở Việt Nam sử dụng các phiên bản Kernel rất thấp, tồn tại nhiều lỗ hổng bảomật dễ dàng bị hacker khai thác tấn công leo thang lên tài khoản root; các thiết bịAndroid của người dùng hầu hết không được cập nhật các phiên bản mới thườngxuyên Từ đó em nhận thấy để nâng cao nhận thức về bảo mật đối với Linux Kernelđồng thời nắm rõ các biện pháp phòng chống thì trước tiên cần phải hiểu rõ cơ chếhoạt động của các lỗ hổng, chứng minh được sự nguy hiểm của các lỗ hổng nàythông qua việc tìm hiểu kĩ thuật khai thác và viết mã tấn công Để làm được điều
này, đề tài em thực hiện sẽ tập trung giải quyết cho bài toán gốc sau: “Làm thế nào
chiếm được quyền root khi đã có quyền người dùng bình thường trên hệ thống Linux”.
Nhiệm vụ của đề tài:
Nghiên cứu các kĩ thuật khai thác lỗ hổng bảo mật trên Linux Kernel: tổnghợp các kĩ thuật được phát triển trên thế giới
Thực hành viết mã khai thác một lỗ hổng điển hình: chọn một lỗ hổng hay,điển hình đã được công bố trên Linux Kernel và viết mã khai thác bằng ngônngữ C cho lỗ hổng đó Dựng môi trường lab để thực hiện tấn công thửnghiệm
Đưa ra các biện pháp phòng chống lỗ hổng bảo mật trong Linux Kernel
Hướng giải quyết:
Đề tài này thiên hướng nghiên cứu, lý thuyết Do đó, hướng giải quyết của em bắtđầu từ tìm hiểu, nghiên cứu kĩ kiến thức lý thuyết Ba phần kiến thức lý thuyếtchính cần đào sâu tìm hiểu trong đề tài là kiến thức về Linux Kernel, kiến thức về lỗhổng phần mềm và kiến thức về các kĩ thuật khai thác lỗ hổng phần mềm sử dụngcho Linux Kernel Các kiến thức lý thuyết này sẽ được áp dụng trong phần thựchành viết mã khai thác thông qua các công cụ lập trình và các công cụ debug trênmôi trường lab mô phỏng hệ thống máy chủ Linux trong thực tế
Các công cụ được lựa chọn:
Trang 12 Phần mềm tạo máy ảo Vmware 10 có hỗ trợ module cho phép debug kernelmáy ảo
Bản phân phối Ubuntu 12.04.0 chạy Linux Kernel phiên bản
Trang 132 Giới thiệu Linux Kernel
2.1 Tổng quan về Linux và Linux Kernel
Linux là hệ điều hành mã nguồn mở, được phát triển dựa trên hệ điều hành UNIX.Linux gồm có một nhân kernel (mã cốt lõi quản lý các tài nguyên phần cứng vàphần mềm) và một bộ sưu tập các ứng dụng của người dùng (chẳng hạn như các thưviện, các trình quản lý cửa sổ và các ứng dụng) (hình 1.1)
Nhân Linux (Linux Kernel) là trung tâm điều khiển của hệ điều hành Linuxchứa các mã nguồn điều khiển hoạt động toàn bộ của hệ thống, tương tác với vi xử
lý, bộ nhớ, quản lý các tiến trình ứng dụng, quản lý vào ra Nhân Linux được lậptrình bằng ngôn ngữ C và được Linus Torvalds phát triển từ năm 1991
Hình 1.1 Kiến trúc tổng quan hệ điều hành Linux
Phạm vi đồ án sẽ tập trung vào Linux Kernel trên nền tảng Intel x86 vàx86_64 vì đây là bộ vi xử lý được dùng phổ biến nhất trong các hệ thống server,hơn nữa cũng là bộ vi xử lý phổ biến nhất trên các dòng máy tính PC, laptop
2.2 Nhắc lại kiến thức nền tảng về vi xử lý
- Bộ vi xử lý (CPU)
Bộ vi xử lý đọc lệnh từ bộ nhớ và thực hiện các lệnh này một cách liên tục,không nghỉ Lệnh sắp được thực thi được quyết định bởi con trỏ lệnh (instructionpointer) Con trỏ lệnh là một thanh ghi của CPU, có nhiệm vụ lưu trữ địa chỉ của
Trang 14lệnh kế tiếp trên bộ nhớ Sau khi CPU thực hiện xong lệnh hiện tại, CPU sẽ thựchiện tiếp lệnh tại vị trí do con trỏ lệnh chỉ tới
- Thanh ghi
Thanh ghi là một dạng bộ nhớ tốc độ cao, nằm ngay bên trong CPU Thôngthường, thanh ghi sẽ có độ dài bằng với độ dài của cấu trúc CPU Các nhóm thanhghi chính:
Thanh ghi chung là những thanh ghi được CPU sử dụng như bộ nhớ
siêu tốc trong các công việc tính toán, đặt biến tạm, hay giữ giá trị tham
số Các thanh ghi này thường có vai trò như nhau Chúng ta hay gặp bốn
thanh ghi chính là EAX, EBX, ECX, và EDX.
Thanh ghi xử lý chuỗi là các thanh ghi chuyên dùng trong việc xử lý
chuỗi ví dụ như sao chép chuỗi, tính độ dài chuỗi Hai thanh ghi thường
gặp gồm có EDI, và ESI.
Thanh ghi ngăn xếp là các thanh ghi được sử dụng trong việc quản lý
cấu trúc bộ nhớ ngăn xếp Hai thanh ghi chính là EBP và ESP.
Thanh ghi đặc biệt là những thanh ghi có nhiệm vụ đặc biệt, thường
không thể được gán giá trị một cách trực tiếp Chúng ta thường gặp các
thanh ghi như EIP và EFLAGS EIP chính là con trỏ lệnh chúng ta đã
biết EFLAGS là thanh ghi chứa các cờ (mỗi cờ một bit) như cờ dấu (signflag), cờ nhớ (carry flag), cờ không (zero flag) Các cờ này được thay đổinhư là một hiệu ứng phụ của các lệnh chính Ví dụ như khi thực hiện lệnhlấy hiệu của 0 và 1 thì cờ nhớ và cờ dấu sẽ được bật Chúng ta dùng giátrị của các cờ này để thực hiện các lệnh nhảy có điều kiện ví dụ như nhảynếu cờ không được bật, nhảy nếu cờ nhớ không bật
Thanh ghi phân vùng là các thanh ghi góp phần vào việc đánh địa chỉ
bộ nhớ Chúng ta hay gặp những thanh ghi DS, ES, CS Trong những thế
hệ 16 bit, các thanh ghi chỉ có thể định địa chỉ trong phạm vi từ 0 đến 216
− 1 Để vượt qua giới hạn này, các thanh ghi phân vùng được sử dụng để
hỗ trợ việc đánh địa chỉ bộ nhớ, mở rộng nó lên 220 địa chỉ ô nhớ Chođến thế hệ 32 bit thì hệ điều hành hiện đại đã không cần dùng đến cácthanh ghi phân vùng này trong việc định vị bộ nhớ nữa vì một thanh ghithông thường đã có thể định vị được tới 232
- Địa chỉ bộ nhớ
Thanh ghi là bộ nhớ siêu tốc nhưng đáng tiếc dung lượng của chúng quá ít nênchúng không phải là bộ nhớ chính Bộ nhớ chính mà chúng ta nói đến là RAM vớidung lượng thường thấy đến 1 hoặc 2 GB RAM là viết tắt của Random AccessMemory (bộ nhớ truy cập ngẫu nhiên) Đặt tên như vậy vì để truy xuất vào bộ nhớthì ta cần truyền địa chỉ ô nhớ trước khi truy cập nó, và tốc độ truy xuất vào địa chỉnào cũng là như nhau Vì thế việc xác định địa chỉ ô nhớ là quan trọng
Trang 15- Định địa chỉ ô nhớ
Đến thế hệ 32 bit, các hệ điều hành đã chuyển sang dùng địa chỉ tuyến tính(linear addressing) thay cho địa chỉ phân vùng Cách đánh địa chỉ tuyến tính làmđơn giản hóa việc truy xuất bộ nhớ Khi ta nói đến địa chỉ bộ nhớ, chúng ta đang nóiđến địa chỉ tuyến tính của RAM Địa chỉ tuyến tính này không nhất thiết là địa chỉthật của ô nhớ trong RAM mà sẽ phải được hệ điều hành ánh xạ lại Công việc ánh
xạ địa chỉ bộ nhớ được thực hiện qua phần quản lý bộ nhớ ảo (virtual memorymanagement) của hệ điều hành Kiểu đánh địa chỉ tuyến tính ảo như vậy cho phép
hệ điều hành mở rộng bộ nhớ thật có bằng cách sử dụng thêm phân vùng trao đổi(swap partition) Chúng ta thường thấy máy tính chỉ có 1 GB RAM nhưng địa chỉ
bộ nhớ có thể có giá trị 0xBFFFF6E4 tức là khoảng hơn 3 GB Trong 3 GB này,ngoài dữ liệu còn có các mã lệnh của chương trình
- Truy xuất bộ nhớ và tính kết thúc nhỏ (little-endian)
Như đã nói sơ qua, bộ vi xử lý cần xác định địa chỉ ô nhớ, và sẵn sàng nhận
dữ liệu từ hoặc truyền dữ liệu vào bộ nhớ Do đó để kết nối CPU với bộ nhớ chúng
ta có hai đường truyền là đường truyền dữ liệu (data bus) và đường truyền địa chỉ(address bus) Khi cần đọc dữ liệu từ bộ nhớ, CPU sẽ thông báo rằng địa chỉ ô nhớ
đã sẵn sàng trên đường truyền địa chỉ, và yêu cầu bộ nhớ truyền dữ liệu qua đườngtruyền dữ liệu Khi ghi vào thì CPU sẽ yêu cầu bộ nhớ lấy dữ liệu từ đường truyền
dữ liệu và ghi vào các ô nhớ Các đường truyền dữ liệu và địa chỉ đều có độ rộng 32bit cho nên mỗi lần truy cập vào bộ nhớ thì CPU sẽ truyền hoặc nhận cả 32 bit đểtối ưu việc sử dụng đường truyền Điều này dẫn đến câu hỏi về kích thước các kiểu
dữ liệu nhỏ hơn 32 bit Câu hỏi đầu tiên là làm sao để CPU nhận được 1 byte thay
vì 4 byte (32 bit) nếu mọi dữ liệu từ bộ nhớ truyền về CPU đều là 32 bit? Câu trả lời
là CPU nhận tất cả 4 byte từ bộ nhớ, nhưng sẽ chỉ xử lý 1 byte theo như yêu cầu củachương trình Việc này cũng giống như ta có một thùng hàng to nhưng bêntrong chỉ để một vật nhỏ Câu hỏi thứ hai liên quan tới vị trí của 8 bit dữ liệu sẽđược xử lý trong số 32 bit dữ liệu nhận được Làm sao CPU biết lấy 8 bit nào? Cácnhà thiết kế vi xử lý Intel x86 32 bit đã quyết định tuân theo tính kết thúc nhỏ (littleendian) Kết thúc nhỏ là quy ước về trật tự và ý nghĩa các byte trong một kiểu trìnhbày dữ liệu mà byte ở vị trí cuối (vị trí thấp nhất) có ý nghĩa nhỏ hơn byte ở vị trí kế(Hình 1.2)
Trang 16Hình 1.2 Kiến trúc bộ nhớ little-endian
- Tập lệnh và hợp ngữ
Tập lệnh là tất cả những lệnh mà CPU có thể thực hiện Đây có thể được coinhư kho từ vựng của một máy tính Các chương trình là những tác phẩm văn học;chúng chọn lọc, kết nối các từ vựng riêng rẽ lại với nhau thành một thể thống nhấtdiễn đạt một ý nghĩa riêng
- Các nhóm lệnh
Hợp ngữ có nhiều nhóm lệnh khác nhau Chúng ta sẽ chỉ điểm qua các nhóm
và những lệnh sau:
Nhóm lệnh gán là những lệnh dùng để gán giá trị vào ô nhớ, hoặc thanh ghi
ví dụ như LEA, MOV, SETZ
Nhóm lệnh số học là những lệnh dùng để tính toán biểu thức số học ví dụ
như INC, DEC, ADD, SUB, MUL, DIV
Nhóm lệnh luận lý là những lệnh dùng để tính toán biểu thức luận lý ví dụ
như AND, OR, XOR, NEG
Nhóm lệnh so sánh là những lệnh dùng để so sánh giá trị của hai đối số và
thay đổi thanh ghi EFLAGS ví dụ như TEST, CMP
Nhóm lệnh nhảy là những lệnh dùng để thay đổi luồng thực thi của CPU
bao gồm lệnh nhảy không điều kiện JMP, và các lệnh nhảy có điều kiện nhưJNZ, JZ, JA, JB
Nhóm lệnh ngăn xếp là những lệnh dùng để đẩy giá trị vào ngăn xếp, và lấy
giá trị từ ngăn xếp ra ví dụ như PUSH, POP, PUSHA, POPA
Nhóm lệnh hàm là những lệnh dùng trong việc gọi hàm và trả kết quả từ
một hàm ví dụ như CALL và RET
- Cú pháp
Mỗi lệnh hợp ngữ có thể nhận 0, 1, 2, hoặc nhiều nhất là 3 đối số Đa số các
trường hợp chúng ta sẽ gặp lệnh có hai đối số theo dạng tương tự như ADD dst, src Với dạng này, lệnh số học ADD sẽ được thực hiện với hai đối số dst và src, rồi kết
Trang 17quả cuối cùng sẽ được lại trong dst, thể hiện công thức dst = dst + src Tùy vào mỗi lệnh riêng biệt mà dst và src có thể có các dạng khác nhau Nhìn chung, chúng ta có các dạng sau đây cho dst và src:
Giá trị trực tiếp là một giá trị cụ thể như 0x6789ABCD Ví dụ MOV EAX,
0x6789ABCD sẽ gán giá trị 0x6789ABCD vào thanh ghi EAX Giá trị trực
tiếp không thể đóng vai trò của dst.
Thanh ghi là các thanh ghi như EAX, EBX, ECX, EDX.
Bộ nhớ là giá trị tại ô nhớ có địa chỉ được chỉ định Để tránh nhầm lẫn với
giá trị trực tiếp, địa chỉ này được đặt trong hai ngoặc vuông Ví dụ MOVEAX, [0x6789ABCD] sẽ gán giá trị 32 bit bắt đầu từ ô nhớ 0x6789ABCDvào thanh ghi EAX
- Ngăn xếp
Ngăn xếp là một vùng bộ nhớ được hệ điều hành cấp phát sẵn cho chươngtrình khi nạp Chương trình sẽ sử dụng vùng nhớ này để chứa các biến cục bộ (localvariable), và lưu lại quá trình gọi hàm, thực thi của chương trình Ngăn xếp hoạtđộng theo nguyên tắc vào sau ra trước (Last In, First Out) Thanh ghi ESP lưu giữ
vị trí đỉnh ngăn xếp, tức địa chỉ ô nhớ của đối tượng được đưa vào ngăn xếp saucùng, nên còn được gọi là con trỏ ngăn xếp (stack pointer) Thao tác đưa một đốitượng vào ngăn xếp là lệnh PUSH Thao tác lấy từ ngăn xếp ra là lệnh POP
2.3 Quản lý bộ nhớ trong Linux Kernel
Linux quản lý bộ nhớ theo cơ chế bộ nhớ ảo, cụ thể là bộ nhớ ảo phân trang(paging) theo 3 cấp, tùy vào CPU được dùng mà kích thước mỗi trang sẽ khác nhau.Đối với intel x86, kích thước trang nhớ thường là 4KB Như chúng ta đã biết, máytính có kích thước bộ nhớ vật lí cố định (bộ nhớ RAM) được sử dụng để lưu trữ dữliệu tạm thời khi hệ thống chạy Không gian địa chỉ của bộ nhớ vật lí chạy từ 0 đến
RAM_SIZE – 1 Tuy nhiên, mỗi tiến trình khi chạy không truy xuất trực tiếp đến
không gian địa chỉ vật lí này mà sử dụng một không gian địa chỉ ảo, riêng độc lậpvới nhau Không gian địa chỉ ảo này thường lớn hơn không gian địa chỉ vật lí, phụthuộc vào kiến trúc Đối với linux x86, không gian này chạy từ 0 đến 232-1 Thànhphần quản lý bộ nhớ ảo trong linux kernel sẽ chịu trách nhiệm ánh xạ không gianđịa chỉ ảo sang không gian địa chỉ vật lí Để thực hiện điều này, Linux chia khônggian địa chỉ vật lí thành các khối kích thước cố định được gọi là khung trang (pageframe), và cũng chia không gian địa chỉ ảo thành các khối kích thước bằng nhau gọi
là các trang nhớ (page) Mỗi khi tiến trình cần sử dụng một trang nhớ trong khônggian ảo, hệ thống sẽ cấp phát khung trang nhớ trong bộ nhớ vật lí và ánh xạ tươngứng cho nó Việc ánh xạ này được thực hiện thông qua bảng phân trang (page table)(hình 1.3)
Trang 18Hình 1.3 Bảng phân trang bộ nhớ trong Linux
Khi tất cả khung trang nhớ được sử dụng hết, và một khung trang nhớ mớiđược yêu cầu, hệ điều hành cụ thể là Linux Kernel sẽ lấy một khung trang hiệnđang không được sử dụng và copy nó lên trên đĩa tại phân vùng swap Khung trangnhớ vừa được giải phóng sẽ được cấp phát và sử dụng Nếu khung trang nhớ banđầu được sử dụng lại, (giả sử tiến trình cần truy cập lại các biến lưu trên đó sau mộtthời gian) hệ thống tương tự sẽ chọn một khung trang nhớ khác, và hoản đổi nó vàovùng nhớ swap trêm ổ đĩa, sau đó copy dữ liệu cũ từ vùng swap ngược lại để tiếntrình sử dụng lại trang nhớ đó Quá trình swap cho phép hệ thống tận dụng đượcnhiều hơn các khung trang nhớ đặc biệt đối với các hệ thống có ít RAM
Không gian địa chỉ ảo cho mỗi tiến trình là giống nhau Linux sử dụng khônggian chung cho cả phần kernel và phần user Không gian địa chỉ kernel bắt đầu từđịa chỉ 0xC0000000 đến 0xFFFFFFFF, độ lớn là 1GB Linux sử dụng chung mộtkhông gian địa chỉ ảo cho cả kernel và user Đặc điểm này đem lại nhiều lợi ích tớiquá trình khai thác lỗ hổng sau này (Phần 3 đồ án sẽ trình bày rõ hơn vấn đề này).Hình vẽ dưới đây minh họa, không gian địa chỉ cho một tiến trình:
Trang 19
Hình 1.4 Không gian địa chỉ bộ nhớ trong Linux
2.4 Quản lý tiến trình trong Linux Kernel
Tiến trình là đối tượng quan trọng được quản lý bởi kernel Tiến trình sử dụng bộnhớ trong để lưu trữ mã chạy và dữ liệu, sử dụng CPU để đọc lệnh từ bộ nhớ vàthực thi lệnh đó Mỗi CPU có nhiều trạng thái thực thi khác nhau, mỗi trạng thái xácđịnh quyền thực thi trong trạng thái đó Ví dụ CPU Intel x86 có 4 trạng thái khácnhau, xác định bởi 2 bit trong thanh ghi cr0 Tuy nhiên Linux Kernel chỉ sử dụnghai trạng thái trong số đó để xác định trạng thái thực thi tiến trình: User Mode vàKernel Mode (hình 1.5) Khi một tiến trình đang chạy trong User Mode, nó không
Trang 20thể trực tiếp truy cập và sử dụng các cấu trúc dữ liệu kernel cụ thể là phần bộ nhớ1GB phía trên dành cho kernel hoặc các dịch vụ kernel cung cấp Khi tiến trìnhchuyển sang Kernel Mode, những giới hạn đó không còn Mỗi CPU đều cung cấpmột số lệnh đặc biệt cho phép chuyển trạng thái từ User Mode sang Kernel Mode vàngược lại Một tiến trình thường chạy nhiều hơn trong User Mode, khi nó cần sửdụng dịch vụ hoặc dữ liệu kernel cung cấp nó sẽ chạy lệnh đặc biệt chuyển sangKernel Mode Khi phần kernel chạy xong, nó đẩy tiến trình trở lại User Mode Việcchuyển đổi giữa User Mode và Kernel Mode chính là cách giao tiếp giữa tiến trình
và kernel, được gọi là system call System call là tập hợp các hàm để tương tác vớikernel Mỗi system call bao gồm một số tham số để truyền yêu cầu cũng như dữliệu cho kernel xử lý
Ngoài các tiến trình user thông thường, bản thân Linux Kernel luôn luôn chạymột số tiến trình đặc biệt gọi là Kernel Threads, những tiến trình chạy các tác vụnhư lập lịch, xử lý ngắt, Những tiến trình này có đặc điểm sau:
Chúng chỉ chạy trong Kernel Mode trong không gian địa chỉ của kernel
Chúng không tương tác với người dùng, do đó không cần thiết bị cuối
(terminals)
Chúng được tạo khi hệ thống bắt đầu và tồn tại cho đến khi hệ thống tắt
Hình 1.5 Giao tiếp giữa user mode và kernel mode
Ngoài system call, Linux còn sử dụng nhiều cách khác cho phép tiến trình user
có thể tương tác với Kernel:
CPU đang chạy tiến trình sinh ra một ngoại lệ (exception) chẳng hạn CPUđọc được một lệnh không hợp lệ, hoặc thực hiện phép chia cho 0, khi đó tiếntrình sẽ tạm dừng lại, kernel sẽ xử lý ngoại lệ đó thay cho tiến trình (Hình1.6)
Thiết bị ngoại vi (chuột, bàn phím, ) sinh ra một tín hiệu ngắt (interrupt)cho CPU để thông báo một sự kiện nào đó xảy ra Mỗi tín hiệu ngắt đó sẽ
Trang 21được chuyển đến một tiến trình kernel gọi là interrupt handler xử lý Tiếntrình đang chạy sẽ buộc phải tạm dừng lại để chuyển sang xử lý ngắt (Hình1.6)
Timer Interrupt: đây là loại ngắt đặc biệt, cứ sau một khoảng thời gian, timerinterrupt sẽ được kích hoạt, các tiến trình đang chạy dừng lại, chuyển thựcthi sang một tiến trình kernel đặc biệt là Scheduler, tiến chình này có nhiệm
vụ lập lịch cho các tiến trình user, giúp ta có một hệ thống đa nhiệm (hình1.6)
Hình 1.6 Xử lý ngắt trong Linux Kernel
Mỗi tiến trình khi chạy được kernel quản lý bởi một cấu trúc dữ liệu gọi làprocess descriptor chứa các thông tin trạng thái cho tiến trình đó Khi tiến trình tạmthời dừng lại (do lập lịch hoặc gọi system call hoặc xử lý ngắt, … ), nó sẽ lưu giá trịcác thanh ghi của CPU lên process descriptor để sau này có thể khôi phục lại Cácthanh ghi bao gồm:
Bộ đếm chương trình (PC), con trỏ stack (SP)
Thanh ghi thông thường (EAX, EBX, ECX, EDX, …)
Thanh ghi xử lý dấu phẩy động
Thanh ghi trạng thái CPU (cr0, cr3, … )
Thanh ghi quản lý bộ nhớ
Khi tiến trình được khôi phục, kernel sẽ sử dụng các thông tin đã lưu trongprocess descriptor để đặt lại giá trị các thanh ghi đặc biệt là bộ đếm chương trìnhhay con trỏ lệnh, từ đó tiến trình có thể chạy lại lệnh cuối cùng từ thời điểm dừng
Trang 223 Giới thiệu lỗ hổng trong Linux Kernel
3.1 Giới thiệu chung về lỗ hổng
Lỗ hổng phần mềm là khái niệm chỉ các lỗi xảy ra trong phần mềm có thể giúp kẻtấn công thay đổi được luồng thực thi của phần mềm đó, từ đó thực hiện được cáchành động mà kẻ tấn công mong muốn Ví dụ như người dùng sử dụng Microsoftoffice để đọc file doc, nhưng Microsoft office có lỗ hổng, khiến cho khi người dùng
mở file doc do kẻ tấn công gửi đến, sẽ chạy một đoạn mã hoặc chương trình nào đócủa kẻ tấn công (hình 2.1) Lỗ hổng phần mềm bản chất cũng là một lỗi của phầnmềm (bug), nhưng khác với lỗi thông thường, thường khiến chương trình khônghoạt động, chạy sai chức năng, lỗ hổng phần mềm thường hướng đến việc điềukhiển, chiếm quyền thực thi chương trình đó, khiến chương trình đó chạy theo ýmuốn của kẻ tấn công
Hình 2.7 Lỗ hổng trong Microsoft Word cho phép chạy chương trình calc.exe
Lỗ hổng phần mềm xảy ra do lập trình viên lập trình không đúng, thường doviệc xử lý dữ liệu đầu vào không chính xác Ví dụ phần mềm Microsoft office, dữliệu đầu vào là file doc, phần mềm sẽ đọc file doc, đọc dữ liệu trong đó, xử lý dữliệu và hiển thị cho người dùng Trình duyệt Chrome đọc mã html từ trang web,phân tích mã đó và hiển thị trang web cho người dùng Lỗ hổng phần mềm thườngxuất hiện trong các phần mềm lập trình bởi ngôn ngữ cấp thấp như C, Assembly khi
Trang 23mà lập trình viên thường phải tự quản lý bộ nhớ, cấp phát, giải phóng, sử dụng contrỏ để truy cập bộ nhớ.
Lỗ hổng phần mềm được chia thành nhiều loại: memory corruption như stackoverflow, heap overflow, lỗi format string, sử dụng con trỏ không đúng như use-after-free, double-free, không khởi tạo con trỏ, các lỗi logic như race condition.Lịch sử lĩnh vực nghiên cứu lỗ hổng phần mềm bắt đầu từ những năm 1982, khi màcon sâu máy tính đầu tiên Morris được viết khai thác lỗ hổng của phần mềm finger.Các kĩ thuật khai thác lỗ hổng phần mềm ngày càng phức tạp, các cơ chế bảo vệđược xây dựng đồng thời các cơ chế bypass cũng được tìm ra
Khi nhắc đến lỗ hổng phần mềm, chúng ta thường nghĩ đến lỗ hổng trên cácphần mềm người dùng phổ biến như Microsoft Office, Adobe Reader, Chrome,Firefox Tuy nhiên không chỉ có vậy, Linux Kernel bản chất cũng là một phần mềm,
nó là một phần mềm phức tạp được code bằng ngôn ngữ C và nhiều phần code bằngassembly Do đó, Linux Kernel tiềm ẩn nhiều lỗ hổng khác nhau Thực tế quá khứ
đã cho thấy nhiều lỗ hổng đã từng được tìm ra và khai thác trên Linux Kernel Khinhắc đến việc khai thác lỗ hổng trên kernel chúng ta thường sử dụng thuật ngữ localprivilege escalation – vượt quyền Giả sử ta đã chiếm được quyền tài khoản thôngthường trên hệ thống (đơn giản như là tấn công ứng dụng web của hệ thống), nhưngtài khoản thường có rất ít quyền, không thể đọc được file shadow, không thể tắtđược hệ thống, không thể tạo thêm được tài khoản mới, … Làm nào ta có thể vượtđược lên quyền cao hơn, cụ thể quyền root Rõ ràng hệ thống hoạt động bìnhthường không cho phép ta làm như vậy Một cách phổ biến để chiếm được quyềnroot là thực hiện chiềm quyền kernel, bởi vì mã lệnh trong kernel có thể làm tùy ý(hình 2.2) Nếu trong kernel có một lỗ hổng nào đó, tài khoản bình thường thực hiệnkhai thác lỗ hổng đó, và chạy được mã lệnh trong kernel thì đồng nghĩa cũng chiếmđược quyền cao nhất (Tham khảo phần quản lý tiến trình – chương 1)
Hình 2.8 Khai thác lỗ hổng trong Linux Kernel để chiếm quyền root
Trang 24Cách thức hoạt động và mã nguồn của kernel tương đồng với các phần mềmuser thông thường do đó các lớp lỗ hổng thường gặp cũng tương tự nhau Phần tiếptheo sẽ trình bày các dạng lỗ hổng thường gặp trong Linux Kernel.
3.2 Lỗ hổng NULL Pointer Dereference
Đây là lớp lỗ hổng đã từng rất phổ biến cho Linux Kernel Lỗ hổng này liên quanđến việc sử dụng con trỏ không đúng Như chúng ta đã biết, con trỏ được sử dụng
để lưu trữ địa chỉ của một biến khác trong bộ nhớ, mỗi lần con trỏ được giải thamchiếu, giá trị được lưu tại địa chỉ mà con trỏ giữ sẽ được tham chiếu Trong ngônngữ lập trình C theo chuẩn ISO, một con trỏ tĩnh, không được khởi tạo giá trị banđầu sẽ có giá trị là NULL (0x0), nói cách khác địa chỉ mà nó chứa là địa chỉ 0x0.NULL cũng là giá trị thường được trả về trong các hàm cấp phát bộ nhớ nếu có lỗixảy ra Giả sử phần kernel giải tham chiếu một con trỏ NULL, nó sẽ thử sử dụng bộnhớ tại địa chỉ 0x0, điều này thường làm chết kernel, vì địa chỉ 0x0 thường khôngđược ánh xạ để sử dụng
Hình 2.9 Không gian địa chỉ bộ nhớ userspace và kernelspace trong Linux
Trang 25Hình 2.10 Địa chỉ được cấp phát (mmap) ở địa chỉ 0x00000000
Hình 2.11 Mã khai thác (exploit payload) được đặt ở địa chỉ 0x00000000
Số lượng lỗ hổng NULL Pointer Dereference được tìm ra khá nhiều Nhìn mộtcách tổng quát, lỗi này nằm trong một lớp lỗi rộng hơn được biết đến với thuật ngữ
tiếng anh “uninitialized/nonvalidated/corrupted pointer dereference” Lớp lỗi này
bao phủ tất cả các tình huống khi mà con trỏ được sử dụng nhưng nội dung của contrỏ (địa chỉ nó chứa) đã bị sửa đổi, hoặc không được khởi tạo, hoặc không đượckiểm tra chính xác Một con trỏ tĩnh không khởi tạo thì sẽ có giá trị NULL, tuynhiên một con trỏ cục bộ (biến cục bộ trong hàm) nếu không được khởi tạo thì nó sẽ
có giá trị ngẫu nhiên nào đó phụ thuộc vào khung stack của hàm tại thời điểm đó, vì
Trang 26biến cục bộ được lưu trên bộ nhớ stack Bằng cách điều khiển stack ta có thể điềukhiển được giá trị con trỏ đó.
Ví dụ minh họa:
Hình 2.12 Hàm ptr_un_initialized không khởi tạo con trỏ p
Trong phần code trên, hàm main gọi hàm big_stack_usage() trước, hàm
big_stack_usage khởi tạo một chuỗi lớn trên stack và lấp đầy chuỗi này bằng các kí
tự A Khi kết thúc hàm big_stack_usage, phía trên đỉnh stack sẽ chứa các kí tự A
này Đến khi hàm ptr_un_initialized được gọi, hàm này sẽ sử dụng lại phần stackcủa hàm trước, do đó con trỏ p mặc dù không được khởi tạo nhưng sẽ nhận giá trịcủa phần stack được cấp cho nó, nghĩa là giá trị của nó hoàn toàn tiên đoán được.Nếu chuỗi string “A” là do người dùng truyền vào, thì giá trị của con trỏ sẽ điềukhiển được Tức là ta có thể điều khiển được p trỏ tới bất kì vị trí nào Nếu p được
sử dụng như con trỏ hàm, thì luồng thực thi của chương trình hoàn toàn có thể điềukhiển được bằng cách cho p trỏ đến đoạn shellcode của ta
3.3 Lỗ hổng Stack Buffer Overflow
Kernel stack tồn tại cho mỗi luồng/tiến trình khi chạy ở mức kernel Mỗi một tiếntrình user-land chạy trên hệ thống có ít nhất hai stack: user-land stack và kernel-land stack Kernel stack được sử dụng bất kì khi nào tiến trình chuyển sang kernelland (Thông qua việc xử lý ngắt hoặc gọi một system call)
Chức năng chung của kernel stack không khác nhiều so với chức năng củauser-land stack, nó có chung một số đặc điểm với user-land stack: hướng phát triểncủa stack (theo hướng giảm dần, từ địa chỉ cao tới địa chỉ thấp), thanh ghi esp (hoặcrsp cho x86_64) lưu địa chỉ đỉnh stack và cách một hàm sử dụng stack (lưu trữ biếncục bộ, tham số được truyền vào hàm, địa chỉ trả về của một hàm,…) (hình 2.7)
Trang 27Mặc dù kernel stack và userland stack giống nhau về nhiều mặt, nhưng nó cómột điểm khác cần lưu ý: Thứ nhất là kích thước kernel stack nhỏ (kiến trúc x86 sửdụng 4KB hoặc 8KB), do tư tưởng lập trình kernel là sử dụng biến cục bộ ít nhất cóthể Ngoài ra tất cả kernel stack của các tiến trình đều thuộc cùng một không gianđịa chỉ ảo do không gian địa chỉ kernel được sử dụng chung cho tất cả các tiến trình.
Hệ điều hành Linux có một trường hợp riêng: interrupt stack, chúng là stackcho mỗi CPU được sử dụng mỗi lần CPU xử lý một vài loại ngắt nào đó (thườngcác ngắt phần cứng) Stack đặc biệt này được sử dụng để tránh tình huống kernelstack có kích thước quá nhỏ, không đủ cho xử lý các ngắt phức tạp
Stack Buffer Overflow xảy ra khi một vùng nhớ buffer được cấp phát trênstack với kích thước cố định, kernel copy một lượng dữ liệu kích thước lớn hơn vàovùng nhớ này (do không kiểm tra kích thước nguồn dữ liệu sao chép) dẫn tời một sốvùng nhớ lân cận bị ghi đè Vùng nhớ lân cận này có thể là biến cục bộ khác tronghàm, địa chỉ trả về hoặc địa chỉ frame được lưu (saved frame pointer) Quan sathình 2.7, vùng nhớ cục bộ chỉ có kích thước 8, nhưng dữ liệu sao chép vào (thườngđến từ attacker và không được kiểm tra chặt chẽ) lớn hơn dẫn tời ghi đè địa chỉ trả
về, do đó khi hàm thực thi xong, sẽ trả về ở một địa chỉ khác Điều nguy hiểm là địachỉ này được điều khiển bởi dữ liệu do attacker truyền vào (hình 2.8)
Hình 2.13 Khung stack của hàm khi được gọi, địa chỉ trả về được đẩy lên stack
Trang 28Hình 2.14 Stack Buffer Overflow xảy ra khi localbuf ghi đè địa chỉ trả về thành giá trị
khác
Lỗ hổng Stack Buffer Overflow được coi là lỗ hổng kinh điển nhất trong lịch
sử khai thác lỗ hổng bảo mật Nó thường xảy ra khi lập trình bằng ngôn ngữ cấpthấp như C Nguyên nhân chính dẫn tới lỗ hổng này là do:
Sử dụng những hàm không an toàn của ngôn ngữ C như strcpy() hoặcsprint() Những hàm này tiếp tục ghi vào buffer đích mà không quan tâm đếnkích thước của nó, nó chỉ dừng ghi khi nào gặp kí tự NULL (\x00) trongbuffer nguồn
Tính toán điều kiện kết thúc vòng lặp không đúng khi xử lý mảng Ví dụ:
Xử lý mảng không đúng dẫn tới ghi vượt ra khỏi biên buffer Vì phần tửmảng chạy từ 0 đến ARRAY_SIZE, khi chúng ta sao chép giá trịsome_value vào phần từ mảng array[j] với j == 10 thì chúng ta đã ghi quá vịtrí cho phép một đơn vị Vì thực chất phần tử cuối của mảng chỉ đến chỉ sốARRAY_SIZE -1 = 9 mà thôi Phần tử array[10] nằm sau mảng đã là mộtvùng nhớ quan trọng khác (có thể là một biến con trỏ hàm chẳng hạn)
Trang 29 Sử dụng một hàm an toàn của C chẳng hạn strncpy() Memncpy() hoặcsnprintf(), nhưng tính toán kích thước buffer đích không đúng Điều này cóthể do lỗi xử lý các kiểu dữ liệu không chính xác.
3.4 Lỗ hổng Kernel Heap Overflow
Giống như khái niệm vùng nhớ Heap được sử dụng trong các tiến trình user, kernelheap là vùng nhớ được sử dụng để cấp phát động trong kernel Linux đã từng sửdụng nhiều cơ chế cấp phát bộ nhớ khác nhau: slab allocator, slob allocator, sluballocator, slqb allocator Các cơ chế cấp phát bộ nhớ heap này khác nhau ở cách tổchức các vùng nhớ đã được cấp phát, vùng nhớ còn trống, và thuật toán cấp phát,giải phóng, tuy nhiên chúng cung cấp chung một interface cho phép kerneldeveloper cấp phát một vùng nhớ mới: kmalloc()/kfree() hoặc kzalloc()/kzfree()
nếu muốn khởi tạo 0 cho toàn bộ buffer Phạm vi đồ án tập trung vào cơ chế slaballocator (hình 2.9), đây là cơ chế được sử dụng mặc định trong các phiên bản
kernel 2.4 trở xuống Từ phiên bản 2.6.22 trở nên, linux kernel cung cấp cấu hình
cho phép tùy chọn sử dụng cơ chế cấp phát bộ nhớ Hình dưới đây minh họa slaballocator
Hình 2.15 Mô hình cấu trúc dữ liệu quản lý slab object
Heap sẽ bao gồm tập hợp các cache Mỗi cache sẽ được sử dụng để quản lí vàcấp phát bộ nhớ cho một loại đối tượng nào đó Mỗi cache sẽ bao gồm một tập hợpcác slab quản lý cùng một loại đối tượng Mỗi slab Mỗi slab tương ứng với mộtpage frame của hệ thống, thường có kích thước là 4KB (4192 byte) Cấu trúc củamột slab sẽ bao gồm phần đầu chứa thông tin metadata (slab descriptor), phần sau làcác object thực sự được cấp phát Thông tin metadata sẽ cho biết object nào đã đượccấp phát, object nào vẫn đang free Kích thước của page frame là cố định, do đó sốlượng object có thể được quản lý trong một slab phụ thuộc vào loại object đó
Trang 30Hình 2.16 Hai kiểu cấu trúc slab
Chúng ta đã hiểu cơ bản về khái niệm heap được sử dụng trong kernel Vậyheap overflow là như thế nào Lỗ hổng heap overflow về bản chất cũng như nguyênnhân hoàn toàn giống với lỗ hổng stack overflow Điểm khác biệt là ngữ cảnh xảy
ra lỗi và những vùng nhớ có thể bị ghi đè Trong lỗ hổng heap overflow, một vùngnhớ với kích thước cố đinh n được kernel cấp phát trên heap Sau đó kernel saochép dữ liệu nguồn vào vùng nhớ đó, nếu kích thước dữ liệu nguồn lớn hơn kíchthước buffer trên heap thì overflow sẽ sảy ra Nhưng điều quan trọng là đối với lỗhổng này, thì chúng ta có thể ghi đè được những loại dữ liệu nào?
Các buffer khác trên heap nằm ngay sau đó, các buffer đó có thể chứa nhữngcấu trúc dữ liệu quan trọng mà nếu ta có thể thay đổi được chúng, sẽ có thể thay đổiđược luồng thực thi của kernel (chẳng hạn con trỏ hàm) Trong hình 2.11, đối tượngtrên heap Victim object bị ghi đè tràn sang đối tượng Target ở ngay bên cạnh
Hình 2.17 Đối tượng target bị ghi đè khi đối tượng Victim tràn
Trang 31Thông tin metadata nằm ở đầu các free object trong tình huống ngay saubuffer bị ghi đè là một đối tượng đang free Thông tin metadata này bao gồm contrỏ trước và con trỏ sau đến các đối tượng free khác Vì các đối tượng free đượcquản lý bằng một danh sách liên kết Trong hình 2.13 đối tượng Free object nằmngay sau bị ghi đè mất thông tin metadata, ban đầu trong thông tin metadata nàychứa con trỏ trỏ đến địa chỉ của một đối tượng free khác trong danh sách, tuy nhiênsau khi bị ghi đè, địa chỉ này bị thay đổi, cụ thể được điều khiển bởi dữ liệu ghi đè
Hình 2.18 Đối tượng free nằm ngay sau bị ghi đè thông tin metadata khi đối tượng trước đó tràn
Một điểm đáng lưu ý là phần dữ liệu control structure, chính là slab descriptor
ở đầu sẽ không bao giờ bị ghi đè, lí do rất đơn giản nó nằm ở đầu ứng với địa chỉthấp
3.5 Lỗ hổng liên quan đến số nguyên
Mỗi kiểu số nguyên có kích thước cụ thể xác định khoảng giá trị mà kiểu đó có thểbiểu diễn được Số nguyên có thể có dấu (biểu diễn cả số âm hoặc số dương) hoặckhông có dấu (biểu diễn chỉ số dương) Với kích thước n bit có thể biểu diễn tối đa2^n giá trị Một số nguyên không dấu sẽ biểu diễn các giá trị từ 0 tới 2n-1 và sốnguyên có dấu biểu diễn giá trị từ -(2n-1) đến (2n-1 - 1) Điểm đặc biệt, số nguyên códấu được lưu trữ bằng số bù hai Khi xử lý số nguyên không cẩn thận, có thể dẫn tớilỗi tràn số hoặc lỗi chuyển đổi dấu Các lỗi này không ngay lập tức có thể khai thácđược mà thường dẫn tới một lớp lỗi khác, phổ biến nhất là dẫn tới các lỗi tràn bộđệm
Trang 32Hình 2.19 Khoảng giá trị cho các kiểu số nguyên
- Tràn số nguyên
Tràn số nguyên xảy ra khi đặt vào biến số nguyên một giá trị lớn hơn giá trị
mà kiểu biến có thể lưu trữ được Ví dụ biến a kiểu char có giá trị lớn nhất là 127,gán giá trị 1000 vào a sẽ dẫn tới tràn số, cộng hai giá trị a = 125+125 cũng sẽ dẫntới tràn số Tràn số thường xảy ra do công hoặc nhân hai số nguyên cùng kiểunhưng không lường đến trường hợp kết quả ra quá lớn Xem những ví dụ minh họasau:
Hình 2.20 Tràn số nguyên trên biến total