Các kỹ thuật xác minh chương trình có thể ngăn chặn các lỗi thuộc thể loại Heartbleed xảy ra không?


9

Về vấn đề lỗi Heartbleed, Bruce Schneier đã viết trong Crypto-Gram ngày 15 tháng 4: '"Thảm họa" là từ đúng. Trên thang điểm từ 1 đến 10, đây là 11. ' Tôi đã đọc cách đây vài năm rằng một hạt nhân của một hệ điều hành nhất định đã được xác minh nghiêm ngặt với một hệ thống xác minh chương trình hiện đại. Do đó, các lỗi thuộc thể loại Heartbleed có thể được ngăn chặn xảy ra thông qua việc áp dụng các kỹ thuật xác minh chương trình ngày nay hay điều này chưa thực tế hoặc thậm chí là không thể?


2
Dưới đây là một phân tích thú vị về câu hỏi này của J. Regehr.
Martin Berger

Câu trả lời:


6

Để trả lời câu hỏi của bạn theo cách ngắn gọn nhất - có, lỗi này có thể đã bị các công cụ xác minh chính thức bắt gặp. Thật vậy, thuộc tính "không bao giờ gửi một khối lớn hơn kích thước của vòng nghe được gửi" khá đơn giản để chính thức hóa trong hầu hết các ngôn ngữ đặc tả (ví dụ: LTL).

Vấn đề (một lời chỉ trích phổ biến chống lại các phương pháp chính thức) là các thông số kỹ thuật bạn sử dụng được viết bởi con người. Thật vậy, các phương thức chính thức chỉ chuyển thách thức săn lỗi từ tìm lỗi sang xác định lỗi là gì. Đây là một nhiệm vụ khó khăn.

Ngoài ra, phần mềm xác minh chính thức là rất khó khăn do vấn đề nổ nhà nước. Trong trường hợp này, nó đặc biệt có liên quan, vì nhiều lần để tránh sự bùng nổ của nhà nước, chúng tôi trừu tượng hóa giới hạn. Ví dụ: khi chúng tôi muốn nói "mọi yêu cầu được theo sau bởi một khoản trợ cấp, trong vòng 100000 bước", chúng tôi cần một công thức rất dài, vì vậy chúng tôi trừu tượng hóa nó thành công thức "mọi yêu cầu cuối cùng được thực hiện theo một khoản trợ cấp".

Do đó, trong trường hợp đau lòng, ngay cả khi cố gắng chính thức hóa các yêu cầu, ràng buộc trong câu hỏi có thể đã bị trừu tượng hóa, dẫn đến hành vi tương tự.

Tóm lại, có khả năng lỗi này có thể tránh được bằng cách sử dụng các phương thức chính thức, nhưng sẽ phải có một người xác định trước tài sản này.


5

Những người kiểm tra chương trình thương mại như Klocwork hoặc Coverity có thể đã tìm thấy Heartbleed vì đây là một lỗi tương đối đơn giản "quên làm lỗi kiểm tra giới hạn", đây là một trong những vấn đề chính mà họ được thiết kế để kiểm tra. Nhưng có một cách đơn giản hơn nhiều: sử dụng các kiểu dữ liệu trừu tượng mờ đục được kiểm tra tốt để không bị tràn bộ đệm.

Có một số loại dữ liệu trừu tượng "chuỗi an toàn" có sẵn cho lập trình C. Người tôi quen thuộc nhất là Vstr . Các tác giả, James Antill, có một cuộc thảo luận lớn về việc tại sao bạn cần một chuỗi kiểu dữ liệu trừu tượng với nhà thầu riêng / phương pháp nhà máy của nó và cũng là một danh sách các chuỗi các kiểu dữ liệu trừu tượng khác cho C .


2
Coverity không tìm thấy Heartbleed, xem phân tích này của John Regehr.
Martin Berger

Liên kết tốt đẹp! Nó cho thấy đạo đức thực sự của câu chuyện: xác minh chương trình không thể bù đắp cho những trừu tượng được thiết kế kém (hoặc không tồn tại).
Logic lang thang

2
Nó phụ thuộc vào những gì bạn có nghĩa là bằng cách xác minh chương trình. Nếu bạn có nghĩa là phân tích tĩnh, thì có, đó luôn là một xấp xỉ, là hệ quả trực tiếp của định lý Rice. Nếu bạn xác minh hành vi đầy đủ trong một trình hỗ trợ định lý tương tác, thì bạn sẽ nhận được một đảm bảo chắc chắn rằng chương trình đáp ứng các thông số kỹ thuật của nó, nhưng điều đó cực kỳ tốn công. Và bạn vẫn phải đối mặt với vấn đề rằng thông số kỹ thuật của bạn có thể sai (xem ví dụ vụ nổ Ariane 5).
Martin Berger

1
@MartinBerger: Coverity tìm thấy nó ngay bây giờ .
Phục hồi Monica - M. Schröder

4

Nếu bạn tính là "  kỹ thuật xác minh chương trình  ", sự kết hợp giữa kiểm tra ràng buộc thời gian chạy và làm mờ, vâng , lỗi cụ thể này có thể đã bị bắt .

Làm mờ đúng cách sẽ khiến cho bây giờ khét tiếng memcpy(bp, pl, payload);đọc qua giới hạn của khối bộ nhớ plthuộc về. Về nguyên tắc, kiểm tra ràng buộc thời gian chạy có thể bắt được bất kỳ quyền truy cập nào như vậy và trong thực tế, trong trường hợp cụ thể này, ngay cả một phiên bản gỡ lỗi mallocquan tâm để kiểm tra ràng buộc các tham số memcpysẽ thực hiện công việc (không cần phải lộn xộn với MMU ở đây) . Vấn đề là, thực hiện các thử nghiệm mờ trên từng loại gói mạng cần nỗ lực.


1
Mặc dù nói chung, IIRC, trong trường hợp của OpenSSL, các tác giả đã thực hiện quản lý bộ nhớ trong của riêng họ sao cho ít có khả năng memcpyđạt được ranh giới thực sự của khu vực (lớn) ban đầu được yêu cầu từ hệ thống malloc.
William Giá

Có, trong trường hợp OpenSSL như tại thời điểm xảy ra lỗi, memcpy(bp, pl, payload)sẽ phải kiểm tra các giới hạn được sử dụng mallocthay thế của OpenSSL chứ không phải hệ thống malloc. Điều đó loại trừ việc kiểm tra ràng buộc tự động ở cấp nhị phân (ít nhất là không có kiến ​​thức sâu về sự mallocthay thế). Phải có sự biên dịch lại với thuật sĩ cấp nguồn bằng cách sử dụng macro C thay thế mã thông báo mallochoặc bất kỳ thay thế nào OpenSSL được sử dụng; và có vẻ như chúng ta cần điều tương tự memcpyngoại trừ với các thủ thuật MMU rất thông minh.
fgrieu

4

Sử dụng ngôn ngữ chặt chẽ hơn không chỉ chuyển mục tiêu bài viết xung quanh từ việc triển khai chính xác sang làm đúng thông số kỹ thuật. Thật khó để làm một cái gì đó rất sai nhưng vẫn nhất quán về mặt logic; đó là lý do tại sao trình biên dịch bắt được rất nhiều lỗi.

Số học con trỏ như thường được xây dựng là không có cơ sở vì hệ thống loại không thực sự có nghĩa là gì. Bạn có thể tránh vấn đề này hoàn toàn bằng cách làm việc trong một ngôn ngữ được thu gom rác (cách tiếp cận thông thường khiến bạn cũng phải trả tiền cho sự trừu tượng). Hoặc bạn có thể cụ thể hơn nhiều về loại con trỏ bạn đang sử dụng, để trình biên dịch có thể từ chối bất cứ thứ gì không nhất quán hoặc không thể được chứng minh là chính xác như được viết. Đây là cách tiếp cận của một số ngôn ngữ như Rust.

Các kiểu xây dựng tương đương với bằng chứng, vì vậy nếu bạn viết một hệ thống loại mà quên điều này, thì tất cả các loại đều sai. Giả sử trong một thời gian rằng khi chúng ta khai báo một loại, chúng ta thực sự có nghĩa là chúng ta đang khẳng định sự thật về những gì trong biến.

  • int * x; // Khẳng định sai. x tồn tại và không trỏ đến một int
  • int * y = z; // Chỉ đúng nếu z được chứng minh là trỏ đến một int
  • * (x + 3) = 5; // Chỉ đúng nếu (x + 3) trỏ đến một int trong cùng một mảng với x
  • int c = a / b; // Chỉ đúng nếu b là khác không, như: "nonzero int b = ...;"
  • null null int * z = NULL; // nullable int * không giống như int *
  • int d = * z; // Khẳng định sai, vì z là null
  • if (z! = NULL) {int * e = z; } // Ok vì z không null
  • miễn phí (y); int w = * y; // Khẳng định sai, vì y không còn tồn tại ở w

Trong thế giới này, con trỏ không thể là null. Các hội nghị NullPulum không tồn tại và con trỏ không phải được kiểm tra tính vô hiệu ở bất cứ đâu. Thay vào đó, "nullable int *" là một loại khác có thể được trích xuất giá trị của nó thành null hoặc thành một con trỏ. Điều này có nghĩa là tại thời điểm mà giả định không null bắt đầu, bạn hãy ghi lại ngoại lệ của mình hoặc đi xuống một nhánh null.

Trong thế giới này, lỗi ngoài giới hạn cũng không tồn tại. Nếu trình biên dịch không thể chứng minh rằng nó nằm trong giới hạn, thì hãy thử viết lại để trình biên dịch có thể chứng minh điều đó. Nếu không thể, thì bạn sẽ phải đưa vào Giả định theo cách thủ công tại điểm đó; trình biên dịch có thể tìm thấy một mâu thuẫn với nó sau này.

Ngoài ra, nếu bạn không thể có một con trỏ không được khởi tạo, thì bạn sẽ không có con trỏ tới bộ nhớ chưa được khởi tạo. Nếu bạn có một con trỏ để giải phóng bộ nhớ, thì nó sẽ bị trình biên dịch từ chối. Trong Rust, có các loại con trỏ khác nhau để làm cho các loại bằng chứng này hợp lý để mong đợi. Có con trỏ thuộc sở hữu độc quyền (nghĩa là: không có bí danh), con trỏ đến các cấu trúc bất biến sâu sắc. Loại lưu trữ mặc định là bất biến, v.v.

Ngoài ra còn có vấn đề thực thi một ngữ pháp được xác định rõ trên các giao thức (bao gồm các thành viên giao diện), để giới hạn diện tích bề mặt đầu vào chính xác với những gì được dự đoán. Điều về "tính đúng đắn" là: 1) Loại bỏ tất cả các trạng thái không xác định 2) Đảm bảo tính nhất quán logic . Khó khăn để đạt được điều đó có liên quan nhiều đến việc sử dụng công cụ cực kỳ tồi tệ (từ quan điểm chính xác).

Đây chính xác là lý do tại sao hai thực tiễn tồi tệ nhất là các biến toàn cầu và gotos. Những điều này ngăn chặn việc đặt các điều kiện trước / sau / bất biến xung quanh bất cứ điều gì. Đó cũng là lý do tại sao các loại rất hiệu quả. Khi các loại trở nên mạnh hơn (cuối cùng sử dụng Các loại phụ thuộc để tính giá trị thực tế), chúng tiếp cận bằng chứng là tính chính xác mang tính xây dựng; làm cho các chương trình không nhất quán thất bại biên dịch.

Hãy nhớ rằng đó không chỉ là những sai lầm ngớ ngẩn. Đó cũng là về việc bảo vệ cơ sở mã từ những kẻ xâm nhập thông minh. Sẽ có trường hợp bạn phải từ chối một bài nộp mà không có bằng chứng thuyết phục được tạo ra bởi các thuộc tính quan trọng như "tuân theo giao thức được chỉ định chính thức".



1

xác minh phần mềm tự động / chính thức là hữu ích và có thể giúp đỡ trong một số trường hợp nhưng như những người khác đã chỉ ra, nó không phải là viên đạn bạc. người ta có thể chỉ ra rằng OpenSSL dễ bị tổn thương ở chỗ nguồn mở của nó và được sử dụng rộng rãi về mặt thương mại và công nghiệp, được sử dụng rộng rãi và không được xem xét kỹ lưỡng trước khi phát hành (người ta tự hỏi liệu có bất kỳ nhà phát triển trả tiền nào trong dự án không). lỗi được phát hiện cơ bản thông qua đánh giá mã sau phát hành và mã rõ ràng đã được xem xét trước khi phát hành (lưu ý mặc dù có lẽ không có cách nào để theo dõi ai đã thực hiện đánh giá mã nội bộ). "khoảnh khắc có thể dạy được" với heartbleed (trong số nhiều người khác) về cơ bản là xem xét mã tốt hơn trước khi phát hành mã có độ nhạy cao, có thể được theo dõi tốt hơn. có lẽ OpenSSL bây giờ sẽ chịu sự giám sát kỹ lưỡng hơn.

thêm bkg từ phương tiện truyền thông chi tiết nguồn gốc của nó:

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.