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

Ngô Quang Hưng | 17 tháng 08, 2005 | Bản để in Bản để in

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 của Intel cho cấu hình IA-32, đặc biệt là chương 6 của Volume 1: Basic Architecture. Ở đây tôi chỉ tóm tắt các chi tiết chính.

Trong cấu hình i686 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, IA-34), Motorola, SPARC, và MIPS 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. Trình dịch gcc dùng ngữ pháp AT&T 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 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

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 <stdio .h>

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 +56> 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 <main+44>). 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ỉ <main+57>.

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

Chủ đề: Bảo mật và mật mã học |

Ghi lời bình của bạn: