Trao đổi với tôi

http://www.buidao.com

3/3/10

[Hacking] Lỗi tràn bộ đệm (3)

3. Tràn bộ đệm trong cấu hình Intel 32-bit

Bạn có thể tham khảo thêm về quá trình gọi hàm và tổ chức stack trong các manuals [1] của Intel cho cấu hình IA-32 [2], đặc biệt là chương 6 của Volume 1: Basic Architecture [3]. Ở đây tôi chỉ tóm tắt các chi tiết chính.

Trong cấu hình i686 [4] như tôi (và đa số các bạn xài PC) đang dùng thì stack phình xuống dưới vùng địa chỉ thấp, heap phình lên trên địa chỉ cao.

Hình dưới đây minh họa vùng nhớ của một process trong Linux.

Hình dưới đây minh họa vùng nhớ của một process trong Windows.

Windows Process Memory Map

Trong miền bộ nhớ của một process, đáy stack bắt đầu từ một vị trí nhất định trong bộ nhớ, đỉnh stack thay đổi theo thời gian và được trỏ tới bởi stack pointer (SP). Giá trị của biến SP nằm trong một thanh ghi (ESP) để truy cập nhanh. Stack chứa vài stack frames. Khi một hàm được gọi, stack frame tương ứng sẽ được PUSH(ed) vào stack. Stack frame của hàm được gọi chứa các tham số, biến cục bộ, và dữ liệu dùng để quay về hàm gọi.

Vì nhiều lý do, các bộ vi xử lý của Intel (IA-32 [2], IA-34 [5]), Motorola [6], SPARC [7], và MIPS [8] lưu giữ thêm một biến nữa gọi là frame pointer (FP, còn gọi là frame base pointer) trỏ về đáy của frame hiện tại trong stack. Trong cấu hình Intel, thanh ghi chứa base pointer là EBP. Thông thường thì hàm được gọi sẽ chép nội dung của ESP vào EBP trước khi PUSH các biến cục bộ lên stack. Các biến cục bộ, tham số, … thường được truy cập theo địa chỉ tương đối từ FP.

Để nhìn rõ hơn các khái niệm này, ta dịch ví dụ 1 ra Linux assembly dùng gcc [9]. Trình dịch gcc dùng ngữ pháp AT&T [10] cho assembly file.

hqn@hanoi (~/BO/Examples) % gcc -S -o e1.s e1.c

Xem file “e1.c”, vài dòng đầu tiên của hàm foo là:

foo:
pushl %ebp
movl %esp, %ebp
subl $56, %esp

Biến trong thanh ghi (register) %ebp chính là FP cũ (trước khi gọi hàm foo), thanh ghi %esp chứa SP. Khi gọi một hàm mới, ta

  1. Ghi lại %ebp cũ bằng cách PUSH nó vào stack:
            pushl   %ebp

  2. Chép %esp vào %ebp để có FP mới (cho hàm sắp gọi):
            movl    %esp, %ebp

  3. Rồi chuyển SP “lên” đỉnh stack

(Lưu ý: thông thường thì là thế, nhưng các trình dịch không nhất thiết phải đi theo các bước này, nhất là khi ta chọn cho trình dịch tốt ưu hóa chương trình.)

Như vậy là cái stack frame mới cho foo có kích thước 56 bytes. Tại sao 56 bytes trong khi ta chỉ cần 20 bytes cho biến buffer, 8 bytes cho các biến “i” và “c”, và 8 bytes nữa cho FP cũ và return address (tổng cộng 36 bytes)?

Để hiểu rõ hơn ta phải tham khảo các tài liệu về gcc [11] và các yêu cầu về memory alignment của họ i686. Trình dịch gcc của GNU có một thuật toán allocate memory riêng cho từng cấu hình. Chi tiết này không quan trọng trong thảo luận của chúng ta. (Trong cấu hình i386 và i686, bạn có thể dùng chọn lựa

-mpreferred-stack-boundary [12]

của gcc để ép trình dịch align memory theo số bytes nhất định. Các bộ vi xử lý khác cũng có chọn lựa tương tự.)

/* ---------------------------------------------------------------------
* Vi' du. 2:
* ---------------------------------------------------------------------
*/
#include

void foo(int a, int b) {
unsigned char buffer[20] = "Hello World";
unsigned long int i=5;
unsigned long int c=6;
(*((int *) (buffer+44))) += 13;
}

int main() {
int x=1; foo(2, 3); x=4;
printf("x = %d\n", x);
return 0;
}

Hừm …, x=1 chứ không phải 4 ? Cái dòng lệnh

  (*((int *) (buffer+44))) += 13;

đã làm gì nhỉ? Số là ta đã truy cập đến địa chỉ trả về của hàm foo và tăng nó lên 13, bỏ qua dòng gán x=1. Tại sao ta biết nhảy lên 13 bytes? Hãy thử disassemble chương trình ex2 bằng gdb. Các chú thích sau các dấu “;” là tôi thêm vào cho rõ.

[hqn@hanoi]:~/BO$ gcc -g ex2.c -o ex2
[hqn@hanoi]:~/BO$ gdb ex2
GNU gdb 6.2-2mdk (Mandrakelinux)
Copyright 2004 Free Software Foundation, Inc.
...

(gdb) disas main
Dump of assembler code for function main:
; main's prologue
0x080483ae
: push %ebp
0x080483af
: mov %esp,%ebp
0x080483b1
: sub $0x8,%esp
0x080483b4
: and $0xfffffff0,%esp
0x080483b7
: mov $0x0,%eax
0x080483bc
: add $0xf,%eax
0x080483bf
: add $0xf,%eax
0x080483c2
: shr $0x4,%eax
0x080483c5
: shl $0x4,%eax
0x080483c8
: sub %eax,%esp
; x = 1
0x080483ca
: movl $0x1,0xfffffffc(%ebp)
; preparing to call foo
0x080483d1
: push $0x3
0x080483d3
: push $0x2
; foo is called, EIP pushed onto the stack
0x080483d5
: call 0x804836c
; returning to main (EIP = 0x080483da)
0x080483da
: add $0x8,%esp
; x = 4
0x080483dd
: movl $0x4,0xfffffffc(%ebp)
0x080483e4
: sub $0x8,%esp
; prepare for printf (13 bytes from the above EIP)
0x080483e7
: pushl 0xfffffffc(%ebp)
0x080483ea
: push $0x80484ec
; printf is called
0x080483ef
: call 0x80482b0 <_init>
0x080483f4
: add $0x10,%esp
0x080483f7
: mov $0x0,%eax
; main's epilogue
0x080483fc
: leave
0x080483fd
: ret
End of assembler dump.
- oOo -

Ta thấy sau khi gọi foo thì đáng lẽ ta phải trở về lệnh ở địa chỉ 0×080483da (bằng ). Lệnh này chỉnh %esp lại cho đúng, và lệnh kế tiếp gán 4 vào x. Ta bỏ qua hai lệnh này, tăng địa chỉ trả về lền 13 bytes, vào đúng lệnh ở địa chỉ .

Thế tại sao ta lại biết là buffer+44 sẽ là địa chỉ trả về? Dùng disassembler và xem trực tiếp các bytes, ta biết rất rõ cái stack frame cho hàm foo được cấu tạo như thế nào.

Trước hết, hãy xem cấu trúc của hàm foo.

(gdb) disas foo
Dump of assembler code for function foo:
; foo's prologue
0x0804836c : push %ebp ; save main's EBP
0x0804836d
: mov %esp,%ebp ; new EBP is old ESP
0x0804836f
: sub $0x38,%esp ; size of foo's frame
0x08048372
: mov 0x80484d8,%eax ; buffer
0x08048377
: mov %eax,0xffffffd8(%ebp)
0x0804837a
: mov 0x80484dc,%eax ; buffer = "Hello World"
0x0804837f
: mov %eax,0xffffffdc(%ebp)
0x08048382
: mov 0x80484e0,%eax
0x08048387
: mov %eax,0xffffffe0(%ebp)
0x0804838a
: movl $0x0,0xffffffe4(%ebp)
0x08048391
: movl $0x0,0xffffffe8(%ebp)
0x08048398
: movl $0x5,0xffffffd4(%ebp) ; i = 5
0x0804839f
: movl $0x6,0xffffffd0(%ebp) ; c = 6
0x080483a6
: lea 0x4(%ebp),%eax
0x080483a9
: addl $0xd,(%eax) ; += 13
; foo's epilogue
0x080483ac
: leave
0x080483ad
: ret
End of assembler dump.

Sau đó, ta dùng gdb để xem stack của foo trong lúc đang chạy:

(gdb) run
Starting program: /home/hungngo/BO/ex1

Breakpoint 1, foo (a=2, b=3) at ex1.c:11
11 (*((int *) (buffer+44))) += 13;
(gdb) x/30x $esp
0xbffff508: 0x400156f0 0x00000000 0x00000006 0x00000005
0xbffff518: 0x6c6c6548 0x6f57206f 0x00646c72 0x00000000
0xbffff528: 0x00000000 0x40141940 0x00000000 0x080495e0
0xbffff538: 0xbffff548 0x0804828d 0xbffff568 0x080483da
0xbffff548: 0x00000002 0x00000003 0x40141940 0x00000000
0xbffff558: 0x080494f8 0x40141940 0x00000000 0x00000001
0xbffff568: 0xbffff598 0x4003c323 0x00000001 0xbffff5c4
0xbffff578: 0xbffff5cc 0x080482c0

Từ đó, ta hình dung ra chính xác cấu trúc stack của foo như sau


Article printed from Blog Khoa Học Máy Tính: http://www.procul.org/blog

URL to article: http://www.procul.org/blog/2005/08/17/l%e1%bb%97i-tran-b%e1%bb%99-o%e1%bb%87m-3/

URLs in this post:

[1] các manuals: http://developer.intel.com/design/pentium4/manuals/index_new.htm

[2] IA-32: http://en.wikipedia.org/wiki/IA-32

[3] Volume 1: Basic Architecture: http://www.procul.org/blogftp://download.intel.com/design/Pentium4/manuals/25366516.pdf

[4] i686: http://en.wikipedia.org/wiki/I686

[5] IA-34: http://en.wikipedia.org/wiki/IA-64

[6] Motorola: http://en.wikipedia.org/wiki/Motorola_6800

[7] SPARC: http://en.wikipedia.org/wiki/Sparc

[8] MIPS: http://en.wikipedia.org/wiki/MIPS_architecture

[9] gcc: http://en.wikipedia.org/wiki/Gcc

[10] ngữ pháp AT&T: http://sig9.com/articles/att-syntax

[11] tài liệu về gcc: http://gcc.gnu.org/onlinedocs/gcc-3.3.1/gcc/

[12] -mpreferred-stack-boundary: http://gcc.gnu.org/onlinedocs/gcc-2.95.3/gcc_2.html#SEC31