Giới thiệu

Static binary injection là một kỹ thuật dùng để chèn những đoạn code từ ngoài vào trong một file thực thi để theo dõi hoặc thay đổi hành vi của chương trình trong quá trình chạy. Nếu là một kẻ tấn công, hắn có thể sử dụng kỹ thuật này để thực hiện việc lây nhiễm lâu dài, còn đối với người nghiên cứu bảo mật, đây sẽ là sự hỗ trợ tuyệt vời cho việc instrument các file thực thi.

Hiện tại các tool thực hiện việc này chủ yếu đều là inject những đoạn mã assembly vào file thực thi và cũng chỉ trên một số hệ điều hành hay kiến trúc máy tính nhất định. Việc inject mã assembly vào file sẽ mất rất nhiều thời gian nếu đoạn mã của ta phức tạp.

Bài viết này chỉ ra một kỹ thuật mới giúp ta có thể inject được một đoạn mã được viết bằng C/C++ vào 1 file thực thi trên 2 nền tảng MacOS và Linux.

Sơ lược về file thực thi trên hệ điều hành MacOS

File thực thi trên hệ điều hành MacOS, hay còn gọi là MachO, thường sẽ có các phần như sau:

macho_format
  • Header bao gồm:
    • Magic number: là chuỗi byte để định danh file thực thi
    • Cpu type, sub-cpu type: thông tin về kiểu kiến trúc máy tính
    • Số lượng và tổng kích thước các load-commands
  • Flags:
    • MH_PIE: khác với Unix ( thông tin về ASLR có được bật hay không nằm ở file hệ thống ), thì MacOS tìm kiếm thông tin đó dựa vào cờ này trên từng file mà nó chuẩn bị thực thi
    • MH_NO_HEAP_EXECUTION: nếu cờ này được bật thì bộ nhớ heap sẽ có quyền thực thi
  • Các Load-commands
  • Phần padding
  • Dữ liệu chính của file

Load commands

Load-command là một đoạn dữ liệu trên file dùng để cung cấp thông tin cho hệ điều hành khi nó load file thực thi lên. Từng load-commands sẽ có từng vai trò, nhiệm vụ khác nhau. Ví dụ:

  • __TEXT: chứa thông tin về vùng code chính của file
  • __DATA: chứa thông tin của một số con trỏ hàm
  • __LINKEDIT: cung cấp thông tin cho dynamic linker
  • DYLD_ÌNO_ONLY: cung cấp thông tin cho dynamic linker
  • LOAD__DYLIB: chứa thông tin về các thư viện được file thực thi load lên
  • DY_SYM_TAB: bảng con trỏ của các symbol được file thực thi load động
macho_load_commands

LC_SEGMENT là một kiểu load-command mà nhiệm vụ của nó là giúp OS load trực tiếp các nội dung từ file lên RAM, dựa vào các thông tin như là:

  • file_size: kích thước của nội dung trên file mà sẽ được load lên RAM
  • file_off: vị trí của nội dung trên file mà sẽ được load
  • vm_size: kích thước của vùng nhớ trên RAM
  • vm_addr: vị trí của vùng nhớ trên RAM
  • init_prot: quyền mặc định của vùng nhớ trên RAM
  • max_prot: quyền tối đa cho vùng nhớ trên RAM
macho_lc_segments

Inject code vào file thực thi

Bài toán được đặt ra là:

input:
- target file: ta sẽ inject code vào file này
- inject file: ta tạo ra file này bằng cách code một đoạn mã C/C++ sau đó compile ra thư viện động
output: file chứa cả code cũ và code mới

Với target file, ta sẽ code một đoạn code đơn giản in ra màn hình, còn với inject file, ta sẽ code một đoạn code phức tạp vào gọi nhiều hàm từ hệ thống mà target file không gọi

macho_example

Như các bạn đã biết thì code trong thư viện động đã là code có thể thực thi được, tức là tất cả các vị trí đều được đặt đúng chỗ, chỉ cần link với file thực thi và load lên RAM là xong, đây là công việc của một static linker. Vậy ta sẽ làm việc đó như sau:

  • Tạo ra 2 segment mới, đặt tên ví dụ như là __NEW__TEXT và __NEW__DATA
    • Đặt header của 2 segment này vào phần padding, như vậy ta sẽ không làm dịch chuyển các dữ liệu cũ
    • Thay đổi thông tin về số lượng cũng như kích thước của các load-commands tại header của file
  • Sao chép toàn bộ các section có trong thư viện đã được biên dịch:
    • Thêm các header của section vào segment tương ứng
    • Đặt nội dung đã sao chép vào cuối file
macho_new_format

Tiếp theo, ta sẽ cần xử lý với những con trỏ toàn cục và những hàm được gọi từ hệ thống, tức địa chỉ của nó không có trong file thực thi, mà sẽ được tìm và lấy khi chương trình chạy. Một số lưu ý khi làm việc này đó là đôi khi thứ tự của libSystem.dylib ( thư viện chính mà mọi file thực thi đều load lên ) trong thư viện mà ta biên dịch khác với trong file cần lây nhiễm. Ngoài ra một số opcode cũng như các đoạn jump code cần phải sửa đề phù hợp với context mới.

Như vậy điều ta cần làm là tận dụng dynamic linker xử lý những vấn đề đó.

Đôi điều về Dynamic Linker trong MacOS

  • Đường dẫn tới dynamic linker được đề cập ở phần LOAD_DYLINKER của file thực thi
  • Nhiệm vụ chính đó là load và link các thư viện mà file thực thi cần để chạy
  • Có 2 kiểu dynamic symbol đó là:
    • Lazy symbol: những symbol được dyld xử lý chỉ khi nó được gọi tới
    • Non-lazy symbol: những symbol được dyld xử lý trước khi file chạy
  • DY_SYMTAB là load-command cón nhiệm vụ lưu trữ một số thông tin về 2 kiểu symbol này

Ta sẽ đi vào một ví dụ để thấy rõ cách hoạt động của dyld

#include <stdio.h>

int main()
{
	puts("Hello world!");
	return 0;
}

Ban đầu, địa chỉ hàm puts không tồn tại trong file thực thi, và nó sẽ chỉ có tại lúc lần đầu tiên được gọi tới. Và thay vì địa chỉ thực đấy, puts sẽ lưu một con trỏ hàm trỏ vào một vùng jump code. Vùng jump code này sẽ setup môi trường và gọi dynamic linker sau đó:

macho_illu

Một số load-command khác sẽ liên quan đến quá trình này, đó là:

  • __LINKEDIT: nắm giữ opcode và một số thông tin của các symbol
  • __DYLD_INFO_ONLY:
    • Rebase: vị trị của đoạn opcode cho quá trình rebase
    • Bind_off: vị trí của đoạn opcode cho quá trình binding non-lazy symbol
    • Lazy_bind_off: vị trí của đoạn opcode cho quá trình binding lazy symbol

Vậy opcode là gì?

Opcode là một byte có chức năng điều khiển hoạt động của dynamic linker. Một opcode được cấu thành từ 8 bit với 4 bit đầu là định danh chức năng của hoạt động và 4 bit sau chứa giá trị truyền vào. Một số hoạt động cần có gía trị truyền vào lớn hơn 4 bit, khi đó chuỗi opcode sẽ được mã hoá bằng phương thức Uleb128.

Có 2 loại opcode:

  • BIND_OPCODE: dùng để tìm các hàm gọi từ thư viện ngoài
  • REBASE_OPCODE: dùng để điều chỉnh địa chỉ khi chạy trong với ASLR

Với ví dụ bên trên thì có 2 quá trình sẽ xảy ra:

Rebase

Đầu tiên, khi file thực thi được chạy với ASLR, hệ điều hành sẽ trích xuất thông tin có được từ __LINKEDIT load-command, sau đó sẽ lấy được ra chuỗi opcode mà nó cần truyền cho dyld, ta sẽ có được chuỗi 11 22 10 51 00 00 00 00. Bạn đọc có thể tìm trên mạng bảng quy đổi opcode sang công việc mà nó cần làm. Ở đây, với byte đầu tiên (11) dyld sẽ đặt kiểu dữ liệu là 1, tức là kiểu con trỏ. Sau đó (22), dyld đi đến segment thứ 2, chính là phần segment của __DATA, rồi cộng thêm 10 là trỏ đúng vào entry của puts trên vùng __nl_symbol_pointer. Cuối cùng (55) sẽ thực hiện việc chỉnh lại địa chỉ.

Binding operation

Tương tự với rebase, hệ điều hành trích xuất thông tin có được cùng với gía trị mà ta đẩy vào stack trước khi gọi dyld, sau đó lấy được chuỗi opcode tương ứng là 72 10 11 40 5F 70 75 74 73 00 90 00 00 00 00 00. Đầu tiên (72 10), dyld đi tới ô địa chỉ của puts trên __nl_symbol_pointer. Sau đấy (11) nó sẽ lấy ra thư viện cần để tìm hàm đó ( libSystem.dylib sẽ có thứ tự là 1 trong file thực thi ). Tiếp theo (40 5F 70 75 74 73) là đọc tên hàm mà cần tìm , chính là puts. Cuối cùng là lấy địa chỉ thực vừa tìm được điền vào vị trí của puts trên __nl_symbol_pointer.

Quay trở lại bài toán của chúng ta:

Con trỏ toàn cục: ta sẽ sử dụng chính những thông tin và rebase opcode lấy được từ thư viện, sau đó chạy những công việc đó bằng tay với địa chỉ cơ sở bằng đúng địa chỉ của segment __RB__DATA. Di chuyển rebase opcode của file cần inject vào cuối vùng __LINKEDIT và nối vào đó rebase opcode lấy được từ thư viện. Như vậy các opcode cũ của file sẽ ko bị dịch chuyển, và khi file chạy với ASLR thì nó sẽ chạy tiếp với các con trỏ mới. Nhớ rằng phải sửa các opcode liên quan đến segment ( vì __RB__DATA ko còn là segment đầu tiên như trong thư viện nữa mà sẽ là segment cuối cùng ở file ouput ) cũng như các thông tin ở DYLD_INFO load-command

Hàm đươc gọi từ bên ngoài: tương tự với việc rebase, tuy nhiên ở đây ta cần phải sửa lại chỉ số tương ứng với libSystem.dylib khi chạy opcode, cũng như fix lại các thông tin về segment. vị trí của opcode khi đẩy vào stack và các thông tin trên __DYLD_INFO

Bạn đọc có thể nhìn mô tả dưới đây:

macho_new_opcode

Sau khi hoàn thành xong các bước trên, ta sẽ có được file ta cần:

Tool sẽ được tác giả bài viết công bố trong thời gian sớm nhất.

Ở phần tiếp theo, chúng ta sẽ thực hiện việc inject trên file ELF của hệ điều hành Linux, mời các bạn đón đọc.