KỸ THUẬT KHAI THÁC LỖI TRÀN BỘ ĐỆM
trang này đã được đọc lần
Tóm tắt :
Loạt bài viết này trình bày về tràn bộ đệm (buffer overflow) xảy ra trên stack và kỹ thuật khai
thác lỗi bảo mật phổ biến nhất này. Kỹ thuật khai thác lỗi tràn bộ đệm (buffer overflow exploit)
được xem là một trong những kỹ thuật hacking kinh điển nhất. Bài viết được chia làm 2 phần:
Phần 1: Tổ chức bộ nhớ, stack, gọi hàm, shellcode. Giới thiệu tổ chức bộ nhớ của một tiến trình
(process), các thao tác trên bộ nhớ stack khi gọi hàm và kỹ thuật cơ bản để tạo shellcode - đoạn
mã thực thi một giao tiếp dòng lệnh (shell).
Phần 2: Kỹ thuật khai thác lỗi tràn bộ đệm. Giới thiệu kỹ thuật tràn bộ đệm cơ bản, tổ chức
shellcode, xác định địa chỉ trả về, địa chỉ shellcode, cách truyền shellcode cho chương trình bị lỗi.
Các chi tiết kỹ thuật minh hoạ ở đây được thực hiện trên môi trường Linux x86 (kernel 2.2.20,
glibc-2.1.3), tuy nhiên về mặt lý thuyết có thể áp dụng cho bất kỳ môi trường nào khác. Người
đọc cần có kiến thức cơ bản về lập trình C, hợp ngữ (assembly), trình biên dịch gcc và công cụ
gỡ rối gdb (GNU Debugger).
Nếu bạn đã biết kỹ thuật khai thác lỗi tràn bộ đệm qua các tài liệu khác, bài viết này cũng có thể
giúp bạn củng cố lại kiến thức một cách chắc chắn hơn.
Phần 1: Tổ chức bộ nhớ, stack, gọi hàm, shellcode
Mục lục :
• Giới thiệu
• 1. Tổ chức bộ nhớ
o 1.1 Tổ chức bộ nhớ của một tiến trình (process)
o 1.2 Stack
• 2. Gọi hàm
o 2.1 Giới thiệu
o 2.2 Khởi đầu
o 2.3 Gọi hàm
o 2.3 Kết thúc
• 3. Shellcode
o 3.1 Viết shellcode trong ngôn ngữ C
o 3.2 Giải mã hợp ngữ các hàm
#0 0x41414141 in ?? ()
(gdb) info register eip
eip 0x41414141 1094795585
(gdb)
Thanh ghi eip - con trỏ lệnh hiện hành - có giá trị 0x41414141, tương đương 'AAAA' (ký tự A có
giá trị 0x41 hexa). Ta thấy, có thể thay đổi giá trị của thanh ghi con trỏ lệnh eip bằng cách làm
tràn bộ đệm buf. Khi lỗi tràn bộ đệm đã xảy ra, ta có thể khiến chương trình thực thi mã lệnh tuỳ
ý bằng cách thay đổi con trỏ lệnh eip đến địa chỉ bắt đầu của đoạn mã lệnh đó.
Để hiểu rõ quá trình tràn bộ đệm xảy ra như thế nào, chúng ta sẽ xem xét chi tiết tổ chức bộ
nhớ, stack và cơ chế gọi hàm của một chương trình.
1. Tổ chức bộ nhớ
1.1 Tổ chức bộ nhớ của một tiến trình (process)
Mỗi tiến trình thực thi đều được hệ điều hành cấp cho một không gian bộ nhớ ảo (logic) giống
nhau. Không gian nhớ này gồm 3 vùng: text, data và stack. Ý nghĩa của 3 vùng này như sau:
Vùng text là vùng cố định, chứa các mã lệnh thực thi (instruction) và dữ liệu chỉ đọc (read-only).
Vùng này được chia sẻ giữa các tiến trình thực thi cùng một file chương trình và tương ứng với
phân đoạn text của file thực thi. Dữ liệu ở vùng này là chỉ đọc, mọi thao tác nhằm ghi lên vùng
nhớ này đều gây lỗi
segmentation violation
.
Vùng data chứa các dữ liệu đã được khởi tạo hoặc chưa khởi tạo giá trị. Các biến toàn cục và
biến tĩnh được chứa trong vùng này. Vùng data tương ứng với phân đoạn data-bss của file thực
thi.
Vùng stack là vùng nhớ được dành riêng khi thực thi chương trình dùng để chứa giá trị các biến
cục bộ của hàm, tham số gọi hàm cũng như giá trị trả về. Thao tác trên bộ nhớ stack được thao
tác theo cơ chế
"vào sau ra trước"
- LIFO (Last In, First Out) với hai lệnh quan trọng nhất là
PUSH và POP. Trong phạm vi bài viết này, chúng ta chỉ tập trung tìm hiểu về vùng stack.
trị đầu tiên của stack frame, các biến cục bộ và tham số được truy xuất qua độ dời so với FP và
do đó không bị thay đổi bởi các thao tác thêm/bớt tiếp theo trên stack.
Đơn vị lưu trữ cơ bản trên stack là word, có giá trị bằng 32 bit (4 byte) trên các CPU Intel x86.
(Trên các CPU Alpha hay Sparc giá trị này là 64 bit). Mọi giá trị biến được cấp phát trên stack
đều có kích thước theo bội số của word.
Thao tác trên stack được thực hiện bởi 2 lệnh máy:
• push value: đưa giá trị ‘value’ vào đỉnh của stack. Giảm giá trị của %esp đi 1 word và đặt
giá trị ‘value’ vào word đó.
• pop dest: lấy giá trị từ đỉnh stack đưa vào ‘dest’. Đặt giá trị trỏ bởi %esp vào ‘dest’ và
tăng giá trị của %esp lên 1 word.
2. Hàm và gọi hàm
2.1 Giới thiệu
Để giải thích hoạt động của chương trình khi gọi hàm, chúng ta sẽ sử dụng đoạn chương trình ví
dụ sau:
/* fct.c */
void toto(int i, int j)
{
char str[5] = "abcde";
int k = 3;
j = 0;
return;
}
int main(int argc, char **argv)
{
int i = 1;
toto(1, 2);
i = 0;
printf("i=%d\n",i);
}
%es
p trỏ
đến
một
địa
chỉ Y
thấp
hơn
bên
dưới.
Trướ
c khi
chuy
ển
vào
một
hàm,
cần
phải
lưu
lại
môi
trườn
g của
stack
fram
e
hiện
tại,
do
%es
p sẽ
giảm
đi 1
word
. Giá
trị
%eb
p
được
push
vào
stack
này
được
gọi là
"con
trỏ
nền
bảo
lưu"
(SFP
-
save
d
fram
e
point
er).
Lệnh
này
%eb
p và
%es
p sẽ
trỏ
cùng
đến
một
vị trí
có
địa
chỉ là
(Y-1
word
).
Lệnh
máy
thứ
ba
cấp
phát
vùng
nhớ
dành
cho
biến
cục
bộ.
Mản
sao
cho
lớn
hơn
hoặc
bằng
kích
thướ
c của
mảng
. Dễ
thấy
giá
trị đó
là 8
byte
(2
word
).
Biến
k
kiểu
nguy
ên có
kích
thướ
c 4
byte,
vì
vậy
g 12
trong
hệ cơ
số
16).
Một điều cần lưu ý ở đây là biến cục bộ luôn có độ dời âm so với con trỏ nền %ebp. Lệnh máy
thực hiện phép gán i=0 trong hàm main() có thể minh hoạ điều này. Mã hợp ngữ dùng định vị
gián tiếp để xác định vị trí của i:
movl $0x0,0xfffffffc(%ebp)
0xfffffffc tương đương giá trị số nguyên bằng –4. Lệnh trên có nghĩa: đặt giá trị 0 vào biến ở địa
chỉ có độ dời “-4” byte so với thanh ghi %ebp. i là biến đầu tiên trong hàm main() và có địa chỉ
cách 4 byte ngay dưới %ebp.
2.3 Gọi hàm
Cũng giống như bước khởi đầu, bước này cũng chuẩn bị môi trường cho phép nơi gọi hàm truyền
các tham số cho hàm được gọi và trở về lại nơi gọi hàm khi kết thúc.
Hình 2 : Gọi hàm
Trướ
c khi
gọi
hàm
các
tham
số sẽ
được
đặt
vào
stack
, theo
thứ
h ghi
%ei
p giữ
giá
trị
địa
chỉ
của
lệnh
kế
tiếp,
trong
trườn
g
hợp
này
là chỉ
thị
gọi
hàm.
Khi
thực
hiện
lệnh
call,
%ei
p sẽ
lấy
giá
trị