Loại bỏ các lỗ hổng an toàn bộ nhớ từ gốc trong mã nguồn
(security.googleblog.com)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
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ả
Đồ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
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
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
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
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
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
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ô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
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
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
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
Đ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
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
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.
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.
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.
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.
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++.
Tôi không tìm được số liệu về mức sử dụng ngôn ngữ trong các phiên bản Windows gần đây, nhưng Microsoft đang dùng Rust cho cả phát triển mới lẫn viết lại các tính năng hiện có [1] [2]
[0] Xem phần “Evolution of the programming languages” https://blog.timac.org/2023/1128-state-of-appkit-catalyst-sw...
[1] https://www.theregister.com/2023/04/27/microsoft_windows_rus...
[2] https://www.theregister.com/2024/01/31/microsoft_seeks_rust_...
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ẻ.