5 điểm bởi GN⁺ 2024-09-27 | 1 bình luận | Chia sẻ qua WhatsApp

Loại bỏ tận gốc các lỗ hổng an toàn bộ nhớ

Kết quả mang tính nghịch lý

  • Khi codebase được viết bằng các ngôn ngữ không an toàn bộ nhớ ngày càng tăng, việc chuyển các tính năng mới sang ngôn ngữ an toàn bộ nhớ sẽ làm giảm mạnh các lỗ hổng an toàn bộ nhớ
  • Điều này là do các lỗ hổng giảm theo hàm mũ theo thời gian

Giải thích bằng toán học

  • Vòng đời của lỗ hổng tuân theo phân phối hàm mũ
  • Lỗ hổng chủ yếu phát sinh trong mã mới, và theo thời gian mã sẽ trở nên an toàn hơn
  • Mật độ lỗ hổng của mã đã 5 năm tuổi thấp hơn từ 3,4 đến 7,4 lần so với mã mới

Trường hợp thực tế trên Android

  • Từ năm 2019, nhóm Android đã bắt đầu chuyển hoạt động phát triển mới sang các ngôn ngữ an toàn bộ nhớ
  • Tính đến năm 2024, tỷ lệ lỗ hổng an toàn bộ nhớ đã giảm từ 76% xuống 24%
  • Khi các lỗ hổng an toàn bộ nhớ giảm, rủi ro bảo mật tổng thể cũng giảm theo

Sự tiến hóa của chiến lược an toàn bộ nhớ

  • Thế hệ 1: vá phản ứng - phát hiện rồi sửa lỗ hổng
  • Thế hệ 2: giảm thiểu chủ động - khiến việc khai thác lỗ hổng trở nên khó hơn
  • Thế hệ 3: phát hiện lỗ hổng chủ động - tìm ra lỗ hổng trước khi chúng bị khai thác
  • Thế hệ 4: phòng ngừa độ tin cậy cao - chuyển sang ngôn ngữ an toàn bộ nhớ để ngăn lỗ hổng phát sinh ngay từ đầu

Ưu điểm của phòng ngừa độ tin cậy cao

  • Chấm dứt cuộc chạy đua bất tận giữa bên phòng thủ và bên tấn công
  • Nâng cao bảo mật và giảm chi phí thông qua các ngôn ngữ an toàn bộ nhớ
  • Cải thiện tính đúng đắn của mã và năng suất của nhà phát triển

Từ bài học đến thực hành

  • Không cần loại bỏ hoặc viết lại toàn bộ mã hiện có không an toàn bộ nhớ
  • Tăng tốc quá trình chuyển sang ngôn ngữ an toàn bộ nhớ bằng cách cải thiện khả năng tương tác
  • Phát triển các công cụ cải thiện khả năng tương tác giữa Rust và C++, cũng như giữa Rust và Kotlin

Vai trò của các thế hệ trước

  • Sử dụng có chọn lọc các biện pháp giảm thiểu và phát hiện chủ động
  • Khi chuyển sang mã an toàn bộ nhớ, nhu cầu về giảm thiểu và phát hiện sẽ giảm xuống

Kết luận

  • Việc dùng ngôn ngữ an toàn bộ nhớ cho mã mới khiến lỗ hổng giảm theo hàm mũ
  • Hơn 6 năm kết quả nhất quán trên Android đã chứng minh hiệu quả của cách tiếp cận này

Tóm tắt của GN⁺

  • Việc chuyển sang các ngôn ngữ an toàn bộ nhớ là rất quan trọng để giảm các lỗ hổng an toàn bộ nhớ
  • Trường hợp của nhóm Android cho thấy các lỗ hổng an toàn bộ nhớ đã giảm mạnh
  • Cải thiện khả năng tương tác thực tế hơn so với việc viết lại hoàn toàn mã hiện có
  • Sử dụng các ngôn ngữ an toàn bộ nhớ như Rust có thể đồng thời nâng cao bảo mật và năng suất

1 bình luận

 
GN⁺ 2024-09-27
Các ý kiến trên Hacker News
  • Bài viết thú vị. Điểm cốt lõi là không cần viết lại toàn bộ thế giới
    Chỉ cần chuyển phần phát triển mới sang các ngôn ngữ an toàn bộ nhớ cũng có thể tạo ra cải thiện đáng kể, và việc này dễ hơn, rẻ hơn rất nhiều so với port toàn bộ để thấy hiệu quả

    • Kết quả này cho thấy lợi ích của việc viết lại toàn diện từ góc độ bảo mật là có giới hạn. Chiến lược giữ lại mã legacy đã trưởng thành và chỉ dùng ngôn ngữ an toàn bộ nhớ cho mã mới trở nên hiệu quả hơn về chi phí
      Đồng thời, giá trị của các ngôn ngữ và công cụ có thể tích hợp vững chắc với mã legacy không an toàn cũng tăng lên
    • Viết lại quá đắt đỏ nên chắc chắn rất hiếm, nhưng cách gặm dần từ rìa vào lại rất hiệu quả
      Tôi cũng tò mò liệu mã rất cũ có hiệu ứng kiểu “đường cong bồn tắm” hay không. Tôi còn nhớ khi có lỗ hổng OpenSSL nghiêm trọng, có lẽ là Heartbleed, nhiều người đã nhìn vào mã và kinh hãi
    • Xét từ góc độ bảo mật thì tôi đồng ý, nhưng nếu muốn loại bỏ garbage collection hoặc giảm mức sử dụng tài nguyên tổng thể thì câu chuyện có thể khác
    • Việc viết toàn bộ mã mới bằng ngôn ngữ an toàn bộ nhớ sẽ giảm lỗ hổng an toàn bộ nhớ tự thân không có gì đáng ngạc nhiên. Vì khả năng mã mới tạo ra vấn đề an toàn bộ nhớ mới cao hơn khả năng phát hiện vấn đề sẵn có trong mã cũ
      Tuy nhiên, điều ấn tượng là từ 2019 đến 2023, không cần viết lại mà chỉ cần thêm mã mới bằng ngôn ngữ an toàn bộ nhớ, có lẽ phần lớn là Rust và một phần Kotlin, đã khiến vấn đề giảm gần 60%. Tôi cũng tò mò vì sao lại có giai đoạn chững từ 2021 đến 2023
      Việc ngoại suy số liệu năm 2024 để dùng cho thấy rõ ý đồ muốn trình bày con số đẹp; nếu thay vào đó phân tích và giải thích vì sao từ 2022 đến 2023 lại tăng thì sẽ hữu ích hơn
      Cũng đáng tiếc là bài thiếu thông tin mã an toàn bộ nhớ mới được viết bằng ngôn ngữ nào và bao nhiêu. Có vẻ họ dùng cả Rust lẫn Kotlin, nhưng tôi muốn biết Rust chiếm 95% hay 50%, và nếu tỷ trọng Kotlin lớn thì nó được dùng thay Rust ở lĩnh vực nào
  • Biểu đồ trong bài này nổi bật ở sự rõ ràng và súc tích. Nó cho thấy rất tốt rằng việc chọn dữ liệu và gắn nhãn cẩn trọng có thể hòa trộn ý tưởng dự định vào phần văn xuôi một cách tự nhiên
    Kết luận từ thực tế rằng lỗ hổng giảm theo hàm mũ là nên tập trung vào mã thuần mới. Đổ tài nguyên vào các dự án “viết lại bằng Rust” khổng lồ, đại trà là không hiệu quả, ngay cả với mục tiêu đạt an toàn bộ nhớ tối đa
    Việc chiến lược dễ nhất, cũng là chiến lược mà các chuyên gia Rust thực dụng khuyến nghị, theo dữ liệu lại cũng là chiến lược tốt nhất để giảm thiểu lỗ hổng bộ nhớ, quả là khá khớp
    Đoạn “Nhóm Android quan sát thấy tỷ lệ rollback của các thay đổi Rust thấp hơn một nửa so với C++” thật đáng ngạc nhiên

  • “Lỗ hổng giảm theo hàm mũ. Chúng có chu kỳ bán rã. [...] Một nghiên cứu quy mô lớn về vòng đời lỗ hổng được công bố tại Usenix Security năm 2022 cũng xác nhận hiện tượng này. Các nhà nghiên cứu phát hiện phần lớn lỗ hổng nằm trong mã mới hoặc mã được sửa đổi gần đây”
    Nếu vậy, với bảo mật, có khi ngừng thêm tính năng mới không thật sự cần thiết lại tốt hơn. Windows LTSC có lẽ là phiên bản an toàn nhất trong các bản Windows

    • Với phần mềm được bảo trì, từng lỗi riêng lẻ phát sinh trong hoạt động thông thường đúng là giảm theo thời gian. Vì nếu lỗi gây vấn đề, ai đó sẽ báo cáo, và một phần trong số đó được sửa. Đây là cơ chế suy giảm
      Nhưng với các lỗ hổng chưa bị khai thác thì không có cơ chế suy giảm như vậy. Chúng không tạo ra phàn nàn của người dùng hay báo cáo lỗi mà cứ nằm đó, cho đến khi một đối thủ có đủ tài nguyên và động cơ tìm ra và khai thác
      So với trước đây, số đối thủ ở mức đó đã nhiều hơn
    • Câu “lỗ hổng giảm theo hàm mũ” có lẽ không chỉ đúng với lỗ hổng mà còn với mọi loại bug. Tôi cũng thấy hiện tượng đó trong kiểm thử của SBCL, một dự án phần mềm tự do mà tôi tham gia. Bug mới có xu hướng xuất hiện ở những phần được thay đổi gần đây
      Tất nhiên điều đó không có nghĩa mọi bug đều chỉ nằm trong mã gần đây. Hẳn ai cũng từng thấy các bug tồn tại nhiều năm mà không bị phát hiện. Với những bug như vậy, cần hỏi vì sao kiểm thử đã bỏ sót chúng
      Vì vậy, kiểm thử nên tập trung vào mã được thay đổi gần đây. Đặc biệt, mutation testing có thể được áp dụng rất tập trung vào mã thay đổi hoặc mã liên kết chặt với nó, nhờ đó giảm đáng kể gánh nặng của loại kiểm thử này
      Google từng có một hệ thống dùng mutation testing theo cách như vậy cùng với code review
    • Ngay cả khi tính năng mới không thật sự cần để bán phần mềm, phần cứng mới, các thuật toán bảo mật tốt hơn và việc loại bỏ hoàn toàn các thuật toán cũ vẫn tiếp tục diễn ra. Rốt cuộc mã mới vẫn sẽ được đưa vào
    • Cũng có thể có cách tiếp cận khác: chỉ biên dịch tập con các tính năng được yêu cầu một cách rõ ràng
      Dĩ nhiên mức độ thực tế của cách này khác nhau rất nhiều tùy nơi, nhưng đây nên là cách phổ biến hơn hiện nay
    • Vì vậy tôi khuyên hầu hết mọi người đừng dùng ngay bản point release mới nhất của ngôn ngữ hay thư viện
      Phiên bản tối tân nhất có rất nhiều lỗ hổng mới. Thông thường, bản phát hành cũ nhất còn được hỗ trợ thường là bản an toàn nhất
      Tất nhiên ngoại lệ là khi tính năng của phiên bản mới mang lại giá trị, nhưng nhìn chung tôi cho rằng nên tránh các phiên bản kết thúc bằng “.0”
  • Có mối tương quan giữa code mới và các lỗ hổng bộ nhớ. Bài blog cũng đưa ra một cách giải thích khả dĩ rằng lỗ hổng có chu kỳ bán rã giảm rất nhanh. Nhưng tôi không hiểu vì sao họ lại thể hiện rằng giữa hai yếu tố này có quan hệ nhân quả
    Có nhiều cách giải thích hợp lý cho mối tương quan này. Code mới thường liên quan đến tính năng mới, và mọi người tập trung tìm lỗ hổng ở các tính năng mới. Ngoài ra, code cũ đã trải qua nhiều sử dụng thực tế hơn, nên có thể đã chạy qua nhiều điều kiện biên nơi lỗ hổng bộ nhớ ẩn náu hơn
    Tôi không thoải mái khi nói rằng code mới gây ra lỗ hổng bộ nhớ và rằng lỗ hổng có chu kỳ bán rã giảm nhanh. Nếu chỉ tính số lượng đơn thuần thì có thể vậy, nhưng nếu nghĩ đến các lỗ hổng nguồn mở có tác động lớn như Heartbleed hay lỗi vô hiệu hóa cache CPU, thì xét về mức độ ảnh hưởng có vẻ không đúng

    • Có thể xem code lâu đời nhất là do những người giỏi nhất viết, dưới áp lực thời gian ít hơn
      Công ty hiện tại của tôi cũng là một ví dụ như vậy. Code ban đầu do các nhà sáng lập viết, còn một phần code mới do các contractor làm trong lịch trình gấp gáp
    • Bài này lấy dữ liệu thú vị từ một dự án và ngôn ngữ rất đặc thù, khác thường, trong một văn hóa rất đặc thù, khác thường, rồi khái quát hóa mạnh thành các con số chắc nịch cho mọi loại code
      Nếu kết luận “ví dụ, dựa trên tuổi thọ trung bình của lỗ hổng, code 5 năm tuổi có mật độ lỗ hổng thấp hơn code mới 3,4 lần; còn nếu dùng tuổi thọ quan sát được ở Android và Chromium thì thấp hơn 7,4 lần” là đúng, thì có nghĩa là cứ viết code C đầy lỗi rồi để offline 5 năm là nó sẽ trở nên an toàn sao?
      Nghiên cứu này có dữ liệu quan trọng và cũng có phần sự thật bên dưới những gì được chia sẻ, nhưng sự tự tin thiếu cơ sở và việc mở rộng quá mức khiến tôi rất khó chịu
  • “Tăng năng suất: Lập trình an toàn đẩy việc phát hiện bug sang trái, trước khi code được check-in, qua đó nâng cao độ chính xác của code và năng suất của lập trình viên. Sự thay đổi này thể hiện qua các chỉ số tỷ lệ rollback, chẳng hạn như phải khẩn cấp hoàn nguyên code do bug bất ngờ”
    “Nhóm Android quan sát thấy tỷ lệ rollback của các thay đổi Rust thấp hơn chưa đến một nửa so với C++”
    Tôi đã viết code production quy mô lớn bằng nhiều ngôn ngữ trong 20 năm, nhưng khi phát hiện ra Rust vào năm 2016, tôi biết đây sẽ là ngôn ngữ khiến mình đắm chìm. Tôi cũng mua ngay cuốn sách của Klabnik và Carol trong ngày hôm đó, và vẫn còn giữ bản giấy
    Thành thật mà nói, nó đã khơi lại tình yêu lập trình của tôi

    • Tôi hiểu điều này, vì lý do lớn nhất khiến các commit C++ của tôi phải rollback là các crash do ngớ ngẩn quên kiểm tra con trỏ null. Nếu Rust ngăn được vấn đề đó và những lỗi code ngớ ngẩn tương tự, thì hẳn là cả một lớp rollback sẽ biến mất
  • Bài viết nói “ngôn ngữ an toàn bộ nhớ (MSL)” ở dạng số nhiều, nhưng ngôn ngữ duy nhất được nêu rõ là mục tiêu chuyển đổi và đang được cải thiện khả năng tương tác là Rust
    Kotlin cũng được nhắc đến trong bối cảnh cải thiện khả năng tương tác Rust<>Kotlin, và ở mức nào đó có các tính năng an toàn bộ nhớ, nhưng tôi không biết liệu nó có cùng cấp độ với Rust hay không. Tôi tò mò liệu Google chỉ dùng hai ngôn ngữ này, hay còn ám chỉ các ngôn ngữ khác nữa

    • Những người quan tâm đến vấn đề này, đặc biệt trong vài năm gần đây, thường thích cách phân chia “ngôn ngữ an toàn bộ nhớ” và “ngôn ngữ không an toàn bộ nhớ”
      Về cơ bản, gốc rễ của vấn đề là có an toàn mặc định hay không. Đây là cách diễn đạt nhằm tập trung vào nguyên nhân gốc, thay vì chỉ đích danh hay khuyến nghị một ngôn ngữ cụ thể
      Với Android, đối tượng của bài viết này, tôi không biết rõ có nỗ lực chuyển sang ngôn ngữ an toàn bộ nhớ nào ngoài hai ngôn ngữ đó hay không. Tôi không theo dõi toàn bộ quá trình phát triển Android, nhưng khá hay đọc các bài kiểu này, và không nhớ có bài nào nói đến ngôn ngữ ngoài Rust hay Kotlin
    • Đây không chỉ là câu chuyện riêng của Rust. Việc viết lại các dịch vụ mạng bằng C sang Java, Python, Go cũng là ví dụ về chuyển sang ngôn ngữ an toàn bộ nhớ
      Điểm cốt lõi là code của bạn không bị phơi nhiễm trước các bug an toàn bộ nhớ. Nếu không thật sự cần, có thể chọn một ngôn ngữ không có quản lý bộ nhớ thủ công kiểu Rust sẽ tốt hơn
    • Android đã nói chi tiết hơn về các ngôn ngữ an toàn bộ nhớ đang dùng trong một bài blog trước đây: https://security.googleblog.com/2022/12/memory-safe-language...
      Google cũng đã trình bày quan điểm của mình về an toàn bộ nhớ tại https://security.googleblog.com/2024/03/secure-by-design-goo..., đồng thời nói về các ngôn ngữ an toàn bộ nhớ đang dùng như Java, Go, Rust
    • Có nhiều ngôn ngữ an toàn bộ nhớ, và Google cũng dùng nhiều ngôn ngữ như Rust, Python, Java, Go. Tuy nhiên, code cấp thấp của Android trong lịch sử là C++, và trong lĩnh vực đó, lựa chọn thay thế an toàn bộ nhớ chủ yếu cho những phần viết mới là Rust
    • Java và Kotlin được dùng cho ứng dụng, còn Rust được dùng cho phần mềm hệ thống mới
      Trên toàn Google, Go được dùng cho một số phần mềm hệ thống, nhưng tôi chưa từng thấy nó được dùng trong Android
  • Nếu tuổi thọ của lỗ hổng tuân theo phân phối mũ, thì việc tập trung vào các mặc định an toàn như an toàn bộ nhớ trong code mới là điều có giá trị bất cân xứng, cả về mặt lý thuyết lẫn theo dữ liệu 6 năm của codebase Android
    Đáng ngạc nhiên. Đây là lần đầu tôi thấy lập luận kiểu này được dùng để ủng hộ các guardrail an toàn nhằm kéo bảo mật sang trái, và nó rất hay. Đặc biệt hữu ích với các codebase legacy lớn, nơi người ta có thể nói “trong 100 triệu dòng code C++ legacy thì không hưởng được lợi ích của an toàn bộ nhớ, vậy làm để làm gì”
    Điều này cũng có vẻ ngụ ý rằng cả việc phát hiện lỗ hổng nhẹ cũng có thể đem lại lợi ích lớn một cách bất cân xứng. Tức là chỉ cần nhìn vào code mới và dependency mới, chứ không phải backlog

  • Kết luận rút ra ở đây hơi khó chịu. Một phản biện hiển nhiên, tức là có thể vì chúng ta xem xét mã cũ kém chăm hơn nên phát hiện ít lỗ hổng hơn, lại không được bàn tới.
    Việc xem log commit gần đây phổ biến hơn nhiều so với việc xem một thư viện không thay đổi suốt 20 năm.

    • Trước đây chúng ta cũng không xem xét mã cũ kỹ đến vậy, và tôi không nghĩ điều đó đã thay đổi.
      Bài viết không có lý thuyết nào về lý do mã cũ ít lỗi hơn, nhưng suy nghĩ của tôi thì đơn giản: vì chúng đã được phát hiện rồi.
      Nếu giả định bất kỳ đoạn mã nào cũng có một số lỗi chưa biết cố định trên mỗi 1000 dòng, thì theo thời gian, số lần nó được chạy trong production với nhiều loại input khác nhau sẽ tích lũy, làm tăng khả năng bị phát hiện. Qua các lần sửa và code review trong quá trình đó, ta có thể kỳ vọng trung bình nó sẽ tốt hơn.
      Vì vậy, theo thời gian, số lỗi trên mỗi 1000 dòng của mã hiện có sẽ giảm. Có thể nói nó đã qua kiểm chứng thực chiến.
      Như bài viết nói, nếu cứ tiếp tục đưa lỗi mới vào với cùng tốc độ thì sẽ không có tiến triển, nhưng nếu ngôn ngữ an toàn bộ nhớ giúp đưa ít lỗi hơn vào các tính năng mới, thì theo thời gian tổng số lỗi sẽ giảm.
    • Tôi không hiểu rõ luận điểm này. Đối tượng được xem xét là Android, và người ta tìm lỗ hổng bằng tay và tự động dựa trên mã nguồn và binary, chứ không phải log commit. Tôi không hiểu vì sao log commit lại liên quan đến việc tìm lỗi.
      Commit chỉ được dùng để quy kết nguồn gốc mà thôi. Nếu một thư viện cũ không thay đổi suốt 20 năm đã vượt qua 20 năm fuzzing và kiểm tra mã thủ công, thì khả năng cao là nó khá vững chắc.
    • Tôi chưa hoàn toàn thỏa mãn với kết luận rằng mã cũ có ít lỗ hổng hơn. Ngoài tuổi đời, có thể còn những yếu tố khác giải thích sự khác biệt.
      Ví dụ, trong vài năm qua các kỹ sư có thể đã tập trung viết lại những phần nguy hiểm nhất bằng ngôn ngữ an toàn bộ nhớ, còn ít thay đổi những đoạn mã cũ có rủi ro thấp.
      Hoặc các thay đổi về quy trình hay nhân sự cũng có thể dẫn đến việc số lỗi tăng lên.
      Dù vậy, lời giải thích rằng mỗi lỗi có một xác suất được phát hiện trên mỗi đơn vị thời gian, và số lỗi còn lại giảm dần theo thời gian, nghe có vẻ hợp lý. Nếu maintainer sửa được nhiều lỗ hổng hơn số lỗ hổng mới mà họ đưa vào, thì mã cũ nhiều khả năng sẽ có ít lỗ hổng hơn, và những lỗ hổng còn lại sẽ khó tìm hơn.
    • Điều đáng quan tâm không phải là các lỗ hổng trên lý thuyết, mà là các lỗ hổng thực sự bị khai thác. Nếu kẻ tấn công không cố tìm lỗ hổng trong một đoạn mã nào đó, thì trên thực tế nó cũng gần như không tồn tại.
  • Tôi tò mò logic này áp dụng thế nào cho Mac và Windows, khi mã Mac mới phần lớn được viết bằng Swift an toàn bộ nhớ, còn Windows vẫn chủ yếu dùng C hoặc C++.

  • Nếu nghĩ về giai đoạn cuối cùng ở đây, khi lỗ hổng càng hiếm thì chúng càng có giá trị. Những lỗ hổng còn lại sẽ được các tác nhân nhà nước tích trữ kỹ lưỡng và để dành dùng cho các mục tiêu giá trị cao.
    Nếu bài blog này mô tả thế hệ thứ 4, thì thế hệ thứ 5 có thể giống Lockdown Mode của iOS. Tức là cho phép những người dùng lo ngại về bảo mật bật một checkbox để tăng bảo mật, chấp nhận đánh đổi hiệu năng.
    Checkbox lý tưởng sẽ phát hiện và bắt giữ cuộc tấn công bằng các cách như ảo hóa, rồi gửi cho đội bảo mật phân tích. Điều này tạo ra sức răn đe đối với kẻ tấn công. Chúng sẽ không muốn đốt một lỗ hổng hiếm hoi khi không biết người dùng có thể đã bật checkbox bảo mật đó hay không, và nhiều mục tiêu giá trị cao sẽ bật checkbox ấy.
    Đó là miễn dịch cộng đồng đối với lỗ hổng phần mềm, không phải tác nhân gây bệnh sinh học.
    Người dùng có ý thức cao về bảo mật nhiều khả năng cũng có ý thức cao về quyền riêng tư. Vì vậy, thay vì âm thầm gửi toàn bộ hoạt động của người dùng, khi phát hiện tấn công hệ thống nên hiển thị thông báo cho người dùng. Chỉ cần hiển thị vài KB hoạt động mạng bất thường cũng có thể đủ để đội bảo mật tái dựng cuộc tấn công, và có thể yêu cầu người dùng phê duyệt trước khi chia sẻ.