Trao đổi với tôi

http://www.buidao.com

3/3/10

[Hacking] Lỗi tràn bộ đệm (6): căn bản về shellcode

Ta có thể phối hợp lệnh jmp và lệnh call để truy cập đến địa chỉ của một vùng dữ liệu nào đó mà không biết trước địa chỉ vùng dữ liệu đó. Cấu trúc của phép phối hợp này như sau:

  jmp short string
code:
pop eax ; now eax has the address of the string 'nhan sinh ...'
... ; having the address, we are ready to poke around
string:
call code
db 'Nhan sinh tu co thuy vo tu'

Cái mẹo này rất thông minh và đơn giản. Ta bỏ chuỗi cần dùng ‘nhân sinh tự cổ thùy vô tử’ vào ngay sau lệnh call. Cả call lẫn jmp short đều dùng địa chỉ tương tối để jump hoặc gọi hàm, nên không sợ vấn đề “địa chỉ cứng”. Khi call được gọi, hệ điều hành sẽ pushed địa chỉ lệnh nằm ngay sau call lên stack. Trong trường hợp này, stack sẽ chứa địa chỉ đến chuỗi ‘nhân sinh tự cổ thùy vô tử’. Ta chỉ việc pop nó vào thanh ghi nào đó tùy ý (eax chẳng hạn) và cứ thế mà dùng.

Với ý tưởng này, ta viết lại chương trình “Hello, World!” như sau:

global _start   ; default entry point for ELF linking

_start:
jmp short string
code:
pop ecx ; ecx points to "Hello, World!"
mov ebx, 1 ; output file descriptor for write
mov edx, 14 ; length of output string
mov eax, 4 ; 4 is the system call number of write()
int 0x80 ; finally, invoke the system call
; prepare for exit(0)
mov ebx, 0 ;
mov eax, 1
int 0x80
string:
call code
db 'Hello, World!', 0x0a

Bạn có thể tự kiểm tra là chương trình chạy như ý muốn. Ý tưởng đã sẵn, ta có thể viết đoạn chương trình assembly để dịch ra bytecode như sau:

; file name: hw.asm
USE32 ; tell nasm we're using a 32-bit system
jmp short string
code:
pop ecx ; ecx has the address of "Hello, World!\n"
mov ebx, 1 ; output file descriptor for write
mov edx, 14 ; length of output string
mov eax, 4 ; 4 is the system call number of write
int 0x80 ; finally, invoke the system call
mov ebx, 0 ; and exit cleanly
mov eax, 1
int 0x80
string:
call code
db 'Hello, World!', 0x0a

USE32 (hoặc BITS 32) để báo cho nasm dịch ra mã 32-bit. Giả sử file hw.asm chứa chương trình trên, ta dịch nó ra mã máy bằng
nasm hw.asm
Bây giờ ta đã có file hw chứa mã của đoạn bytecode cần tìm. Làm thế nào để đọc file này ở dạng hex? Có rất nhiều hex editors trên Internet, bạn có thể dùng thử hexedit [1]. Chạy hexedit hw và ta có output như sau:

00000000   EB 1E 59 BB  01 00 00 00  BA 0E 00 00  00 B8 04 00  ..Y.............
00000010 00 00 CD 80 BB 00 00 00 00 B8 01 00 00 00 CD 80 ................
00000020 E8 DD FF FF FF 48 65 6C 6C 6F 2C 20 57 6F 72 6C .....Hello, Worl
00000030 64 21 0A d!.

Đây chính là đoạn bytecode trong ví dụ 3. Nhưng chẳng lẽ cứ mỗi lần muốn thử bytecode mới thì lại phải dùng hexedit, copy từng byte từng byte một vào chuỗi bytecode[] trong ví dụ 3, sau đó dịch và chạy ví dụ 3? Mất thì giờ quá. Tôi viết một chương trình nho nhỏ đặt tên là bct (bytecode tester) với chức năng như sau: gọi “bct hw” và nó sẽ chạy code chứa trong hw cho ta, còn gọi “bct -p hw” thì nó sẽ in ra chuỗi hex trong hw để dùng được trong các chương trình C. Cụ thể, bct làm việc như sau:

[NQH]:~/BO$ cat hw.asm

USE32 ; tell nasm we're using 32-bit system
jmp short string
code:
pop ecx ; write(1, "Hello, World!", 13);
mov ebx, 1 ; output file descriptor for write
mov edx, 14 ; length of output string
mov eax, 4 ; 4 is the system call number of write
int 0x80 ; finally, invoke the system call
mov ebx, 0 ;
mov eax, 1
int 0x80
string:
call code
db 'Hello, World!', 0x0a
[NQH]:~/BO$ nasm hw.asm
[NQH]:~/BO$ bct hw
----------------------
Calling your code ...
----------------------
Hello, World!
[NQH]:~/BO$ bct -p hw
----------------------
Printing your code ...
----------------------

char bytecode[] =
"\xeb\x1e\x59\xbb\x01\x00\x00\x00\xba\x0e\x00\x00\x00\xb8\x04\x00"
"\x00\x00\xcd\x80\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80"
"\xe8\xdd\xff\xff\xff\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c"
"\x64\x21\x0a"

Bài tập 1: Viết chương trình bct có chức năng mô tả như trên. Gợi ý: dùng ý tưởng của đoạn chương trình C sau đây

char bytecode[] =
"\xeb\x1e\x59\xbb\x01\x00\x00\x00\xba\x0e\x00\x00\x00\xb8\x04\x00"
"\x00\x00\xcd\x80\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80"
"\xe8\xdd\xff\xff\xff\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c"
"\x64\x21\x0a";

int main() {
void (* func_ptr)(void);
func_ptr = (void (*)(void)) bytecode;
(* func_ptr)();
}

Lần tới ta sẽ viết một đoạn bytecode có chức năng gọi một shell (như /bin/sh hay /bin/bash). Các đoạn bytecode loại này được gọi là shellcode. Nếu chương trình nào đó chạy với suid bằng root mà bị lỗi tràn stack thì cái shellcode sẽ gọi một shell có đặc quyền root. Có thể nói shellcode là một trong những bytecode phổ biến nhất, vì có shell rồi thì làm gì chẳng được.


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/10/30/l%e1%bb%97i-tran-b%e1%bb%99-o%e1%bb%87m-6-cnn-b%e1%ba%a3n-v%e1%bb%81-shellcode/

URLs in this post:

[1] hexedit: http://www.chez.com/prigaux/hexedit.html