ARM Exploit Development Part 1: Stack Overflow

ARM là một CPU dựa trên kiến trúc RISC được phát triển bởi Advanced RISC Machines. ARM sản xuất bộ vi xử lý đa nhân RISC 32 bit và 64 bit. Các bộ xử lý RISC được thiết kế để thực hiện một số lượng nhỏ hơn các loại lệnh máy tính để chúng có thể hoạt động ở tốc độ cao hơn, thực hiện nhiều lệnh mỗi giây (MIPS). Bộ xử lý RISC cung cấp hiệu suất vượt trội nhưng mức tiêu thụ điện năng ở mức thấp nên nó là một trong những kiến trúc bộ xử lý phổ biến nhất trên thế giới hiện nay. Bộ xử lý ARM được sử dụng rộng rãi trong các thiết bị điện tử như IoT, Healthcare, Smartphone...

Process Memory

Mỗi chương trình chạy, một vùng bộ nhớ cho sẽ được tạo riêng cho nó. Sau đó được chia thành nhiều khu vực nhưng đặc biệt là các phân vùng cần chú ý:

  • Program Image

  • Heap

  • Stack

Program Image về cơ bản chứ tệp thực thi của chương trình đã được tải vào bộ nhớ. Vùng nhớ này có thể được chia thành nhiều phân đoạn khác nữa: .plt, .text, .got, .data, .bss, v.v. Trong đó:

  • .text: chứa phần thực thi của chương trình với tất cả lệnh Assembly.

  • .data và .bss: chứa các biến hoặc contror tới các biến được sử dụng trong ứng dụng

  • .plt và .got lưu trữ các con trỏ cụ thể tới các hàm được import ví dụ như từ các shared libraries.

Nếu có thể rewrite phần .text, thì có thể xảy ra execute arbitrary code, tương tự trong phần Procedure Linkage Table (.plt) và Global Offsets Table (.got) tùy từng trường hợp cụ thể cũng dẫn đến execute arbitrary code.

StackHeap được ứng dụng sử dụng để lưu trữ và vần hành dư liệu tạm thời (các biến) được sử dụng trong quá trình thực thi chương trình. Những vùng này thường bị khai thác vì dữ liệu trong Stack và Heap có thể bị sửa đổi bởi input của người dùng, nếu không được xử lý đúng cách.

Ngoài mapping memory, cần lưu ý các thuộc tính liên kết với các vùng nhớ. Một số thuộc tính liên kết với vùng nhớ như: Read, Write, Execute.

  • Read: Cho phép đọc dữ liệu từ một vùng cụ thể.

  • Write: Cho phép chương trình ghi dữ liệu vào một vùng bộ nhớ cụ thể.

  • Execute: Thực thi lệnh trong vùng nhớ đó.

Trong GDB có hai cách để xem các vùng của process memory như sau:

Với GDB lệnh info proc mappings được sử dụng để hiển thị các vùng mapping của process memory. Ngoài ra với GEF - GDB còn hỗ trợ một lệnh khác là vmmap cũng tương tự.

Phần Heap trong vmmap chỉ xuất hiện khi chương trình sử dụng các function liên quan đến Heap ví dụ hàm malloc của C sẽ tạo một buffer trong vùng Heap.

Ngoài GDB, cũng có thể xem các vùng bộ nhớ của một process thông qua file dành riêng cho các process.

Introduction into Memory Corruptions

Memory Corruption là một bug cho phép sửa đổi bộ nhớ, các bug liên quan đến memory thường dẫn đến execute arbitrary code, disable security mechanisms… Có một số loại bug liên quan đến memory như sau:

  • Buffer Overflows

    • Stack Overflow

    • Heap Overflow

  • Format String

  • Dangling Pointer (Use-after-free)

Buffer Overflows

Buffer overflow là một trong những loại bug về memory phổ biến nhất và thường do các nhà phát triển cho phép user input nhiều dữ liệu hơn mức bình thường. Ví dụ một số function trong C như gets, strcpy, memcpy được call cùng với input của người dùng. Các function không kiểm tra độ dài dữ liệu mà người dùng nhập vào, nên điều này có thể dẫn đến việc ghi tràn các buffer đã được allocated.

Stack Overflow

Việc ghi tràn các Stack rất có thể gây ra lỗi chương trình, nhưng khi được tạo cẩn thận Stack buffer overflow có thể dẫn đến arbitrary code execution.

Stack là một vùng nhớ của program/process, nó được allocate khi một process được tạo. StackLIFO (Last In Fisrt Out), khi PUSH một giá trị vào Stack, nó sẽ đi vào Stack Pointer và khi nó được POP ra khỏi Stack và nó sẽ được POP value vào một thanh ghi do mình chọn.

Stack có các phần như: User data, Frame Pointer, Link Register… Trong trường hợp khi người dùng input quá nhiều dữ liệu vào một biến mà người dùng có thể kiểm soát, Frame Pointer và Link Register có thể bị ghi đè. Nó có thể làm crash chương trình vì người dùng làm hỏng các địa chỉ của những nơi mà ứng dụng sẽ return/jump.

Ví dụ về buffer_demo:

#include <stdio.h>
#include <string.h>

void demo(char *s)
{
    char buffer[12];
    strcpy(buffer, s);
}

void execCommand(){
    system("echo pwned");
}

int main(int argc, char *argv[])
{
    if(argc > 1) {
        demo(argv[1]);
        printf("End.\n");
    }
}

Compile chương trình trên môi trường test:

gcc -o buffer_demo buffer_demo.c

Với chương trình demo sử dụng biến “buffer”, có độ dài 12 ký tự được lấy từ arg input của người dùng. Hàm dược sử dụng ở đây là strcpy đơn giản là copy bất kỳ input nào của người dùng vào biến buffer.

gdb --args buffer_demo AAAAAAAAAAA

Assembly code:

Ở đây giả sử rằng sẽ có lỗi buffer overflow ngay sau khi kết thúc hàm strcpy. Đặt một break point ngay sau hàm strcpy có địa chỉ là 0x00010488. Khi chạy chương trình với input đã cung cấp vào arg của chương trình là 11A.

Sau khi input 11A vào chương trình, Stack ở vẫn đảm bảo chạy đúng chương trình. Do người dùng đã nhập vào 11 bytes buffer đúng với mong đợi của nhà phát triển. Vậy chạy với chương trình với input lớn hơn số buffer mà nhà phát triển mong đợi chương trình sẽ xảy ra như nào?

Exploitation

Khi một chương trình con đang được gọi, địa chỉ trả về sẽ được lưu giữ trong thanh ghi LR (Link Register). Điều này được thực hiện với lệnh Branch with Link (BL) hoặc Branch with Link and Exchange (BLX). Nhưng nếu chương trình con này gọi một chức năng khác thì sao? Link regsiter sẽ bị ghi đè và chương trình sẽ không tìm được đường quay lại chức năng trước đó. Cách xử lý việc này là bảo toàn địa chỉ trả về trên ngăn xếp bằng lệnh PUSH. Lệnh PUSH lưu thanh ghi mà nó được cung cấp (trong trường hợp này là LR: PUSH {LR}) lên đầu Stack trước khi ghi đè thanh ghi LR bằng địa chỉ trả về mới.

Đầu tiên function demo lưu “return address” trong LR vào Stack, nhưng đến cuối funtion, nó lại được lưu trở lại thanh ghi PC. Mà thanh ghi PC là thanh ghi chứa địa chỉ của lệnh tiếp theo được thực thi.

Với Stack (Last In First Out) thì khi giá trị đầu tiên nó PUSH vào Stack là giá trị cuối cùng nó POP ra khỏi Stack.

Quay lại ví dụ trên khi chương trình chạy với input là arg với sẽ được lưu vào local variable, nếu không được xử lý input sẽ dẫn đến việc người dùng iuput vào chương trình quá nhiều bytes dẫn đến tràn Stack.

Với GDB có thể debug để xác định số bytes buffer và gây tràn Stack, như vậy với chương trình này có thể thấy đến byte thứ 17 input đã bắt đầu ghi vào thanh ghi PC. Khi kết thúc function lệnh POP {PC} sẽ được call, theo như chương trình trên nó sẽ chuyển đến địa chỉ 0x00004240 (byte cuối cùng được tự động chuyển đổi thành 0x40 do chuyển sang Thumb mode) nhưng không thể truy cập nên chương trình sẽ bị lỗi.

Đoạn code ban đầu trong chương trình có một hàm là execCommand nhưng không được call tại bất kỳ chỗ nào.

Khi đã xác định được địa chỉ của hàm trên có thể hướng thanh ghi PC về địa chỉ của hàm trên. Chạy command run chương trình với input là 16 bytes buffer và sau đó bắt đầu bằng địa chỉ của hàm execCommnad.

./buffer_demo $(python2 -c "print 'A'*16 + '\x94\x04\x01\x00'")

Như vậy với mã exploit trên hàm execCommand đã được thực thi mặc dù trong đoạn code của chương trình function không được call ở chương trình con nào.

Reference

https://azeria-labs.com/