Vì sao ứng dụng do những lập trình viên giỏi viết vẫn có lỗi bảo mật

08:39 | 07/01/2016

Rất nhiều người tin rằng lỗi bảo mật là do những lập trình viên kém cỏi và thiếu trách nhiệm tạo ra, nhưng thực tế diễn ra không phải chỉ như vậy.

Có thể nói rằng đa số lập trình viên đều muốn làm ra những phần mềm tốt và dĩ nhiên họ không cố ý tạo các lỗi bảo mật trong chương trình của mình. Ngoài những lý do như sức ép tiến độ, tâm lý,… thì sự phức tạp của các hệ thống kỹ thuật cũng góp phần tạo ra những lỗ hổng bảo mật. 

Các ứng dụng tuân thủ những đặc tả rất chi tiết, nghiêm ngặt vẫn có thể có lỗ hổng bảo mật và rất nhiều lỗi trong số đó đã được tạo ra bởi những lập trình viên giỏi, những người đã làm hết sức mình để hoàn thành công việc. Vì sao lập trình an toàn, không có lỗi bảo mật lại khó đến thế? Cuốn sách Secure Coding: Principles & Practices của các tác giả Mark G. Graff và Kenneth R. van Wyk cho chúng ta một ví dụ thực tế, rất sinh động về điều đó.



Vào năm 1993, khi Mark (một trong hai tác giả) còn làm việc ở hãng Sun, ông nhận được một cuộc gọi lúc nửa đêm từ CERT – điều khiến ông cực kỳ lo lắng. Jim Ellis báo rằng CERT nhận được và đã kiểm chứng một báo cáo về việc tất cả các tệp archive do tiện ích tar (Tape Archive) tạo ra trên hệ điều hành Solaris 2.0 đều chứa một phần nào đó tệp /etc/passwd file. 

Nếu điều đó là đúng thì Sun và khách hàng của công ty đang ở trong tình thế cực kỳ nguy hiểm: tệp mật khẩu là phần cực kỳ quan trọng đối với an toàn của mọi hệ thống, nó là mục tiêu giá trị mà tin tặc luôn mơ ước. Có phải Sun đã để lộ nó? Phần mềm của Sun đã đưa tệp mật khẩu ra mọi tệp lưu trữ, các trang web và vô số đĩa CD-ROM?

Jim đã cung cấp bằng chứng và Mark nhanh chóng xác nhận lỗ hổng. Ông nhanh chóng = thành lập một đội ngũ kỹ thuật để xem xét, xử lý vấn đề. Mark sợ rằng đã có gián điệp nằm vùng trong công ty đã đưa lỗi bảo mật vào mã nguồn của Sun từ nhiều năm trước, với mục đích thu thập các tệp mật khẩu của khách hàng để tung lên Internet.

Nhưng rồi mọi việc không tệ đến thế và lỗi được xử lý êm thấm. Mark có thể yên tâm coi rằng lỗi chỉ xuất hiện một cách tình cờ. Từ đó về sau, tệp passwd không còn quá quan trọng với an toàn hệ thống: Sun giới thiệu tệp shadow để tệp /etc/passwd không chứa mật khẩu của người dùng nữa. Họ khắc phục lỗi, đưa ra bản vá và công bố một khuyến cáo an toàn (Sun Security Bulletin 122, phát hành ngày 21/10/1993).

Và chi tiết về lỗ hổng bảo mật như sau: Dữ liệu từ các khối 512 byte trên đĩa được ghi vào tệp archive. Vòng lặp đọc một khối/ghi một khối được lặp lại cho đến khi toàn bộ tệp cần lưu trữ được ghi vào tệp tar. Tuy nhiên, bộ đệm để chứa dữ liệu đọc từ đĩa không được làm sạch trước khi đọc. Vì thế phần của khối vượt quá đoạn cuối tệp trong lần đọc cuối không được lấy từ tệp cần lưu mà từ “rác” trong bộ nhớ trước khi đọc đĩa. Thông thường điều này sẽ tạo ra một đám rác ngẫu nhiên ở cuối tệp archive. Nhưng tại sao những đoạn của tệp mật khẩu lại được ghi vào đó? Hóa ra là vùng bộ nhớ nơi các khối dữ liệu trên đĩa được đọc vào luôn chưa một phần của tệp mật khẩu, trăm lần đúng cả trăm. Điều gì đã xảy ra ở đây?

Bộ đệm luôn chứa thông tin của tệp mật khẩu vì trong vòng lặp đọc/ghi, tiện ích tar tìm kiếm thông tin về người dùng đang gọi nó. Lệnh của hệ thống dùng để tìm kiếm thông tin người dùng làm điều này bằng cách đọc các đoạn của tệp /etc/passwd vào bộ nhớ. Tiện ích tar lấy bộ nhớ để phục vụ hoạt động đó từ vùng "heap" của hệ thống và giải phóng khi kiểm tra xong. Vì trình quản lý heap cũng không xóa các vùng nhớ được trả lại trước khi cấp phát nên mọi tiến trình cần đến bộ nhớ ngay sau lệnh tìm kiếm thông tin người dùng đều nhận được một bộ đệm có chứa các đoạn của tệp /etc/passwd. Và tar gọi lệnh kiểm tra thông tin người dùng ngay trước khi xin một bộ đệm để đọc tệp từ đĩa.

Vậy tại sao Sun không phát hiện điều này trong những năm trước đó? Trong những phiên bản trước, lệnh kiểm tra thông tin người dùng được thực hiện sớm hơn. Tiếp theo đó là một loạt các thao tác cấp phát và giải phóng bộ nhớ khác. Nhưng khi một lập trình viên xóa bớt những đoạn đoạn mã nằm giữa đó để khắc phục một lỗi khác thì lỗ hổng bảo mật bắt đầu xuất hiện. Việc chỉnh sửa đó tình cờ đẩy lệnh kiểm tra thông tin người dùng với thao tác đọc đĩa lại gần nhau hơn.

Sau khi xác định được nguyên nhân, việc khắc phục khá đơn giản. Họ sửa câu lệnh:

char *buf = (char *) malloc(BUFSIZ);

thành:

char *buf = (char *) calloc(BUFSIZ, 1);

Chỉ sửa vài ký tự (buộc ứng dụng xóa bộ đệm sau khi cấp phát) đã đủ khắc phục lỗi bảo mật.

Câu chuyện có thật này cho chúng ta thấy những lỗ hổng bảo mật nghiêm trọng không chỉ do lỗi thiết kế hay lập trình, mà đôi khi xuất hiện một cách tình cờ, do không lường được tương tác giữa các phần tử an toàn, được thiết kế “ổn” của hệ thống.

Câu chuyện không chỉ dừng ở đó. Các tác giả của cuốn sách còn phân tích kỹ hơn cách chỉnh sửa lỗi của Sun. Chúng ta đã thấy rằng cách làm của Sun rất kinh tế (câu lệnh mới không thay đổi cấu trúc chung và luồng xử lý của ứng dụng) nhưng vẫn có một số thứ cần xem xét.

Trước hết, hàm calloc được thiết kế để phân bổ vùng nhớ cho các mảng. Nó có thể dùng thay cho malloc trong tình huống này vì số phần tử của mảng được đặt bằng số byte của bộ đệm. Và các phần tử của mảng được chỉ định là một byte. Nhưng việc dùng hàm calloc có thể khiến những người bảo trì ứng dụng cảm thấy lúng túng, khó hiểu. Một cách chuẩn tắc hơn để xóa nội dung của một bộ đệm là dùng hàm memset. Ví dụ như dòng lệnh sau:

memset( buf, 0, BUFSIZ);

sẽ thực hiện việc xóa bộ đệm mà không gây hiểu nhầm (tất nhiên chúng ta phải đổi việc khai báo biến buf ngay trong dòng lệnh thành một khai báo tường minh). Và chúng ta sẽ có đoạn lệnh sau:

char* buf; 

buf = malloc(BUFSIZ); 

memset( buf, 0, BUFSIZ);

Nhưng vẫn chưa xong, chúng ta còn chưa kiểm tra kết quả do hàm malloc trả về! Nếu heap đã bị dùng hết thì không thể cấp phát bộ đệm mới, khi đó memset sẽ cố xóa tham chiếu một con trỏ null. Và điều tốt nhất có thể xảy ra là tiện ích tar bị đổ vỡ. Để tránh điều đó, chúng ta có thể làm như sau (hoặc dùng các thủ tục xử lý lỗi - error-handling routine- như mọi người vẫn thường làm trong các chương trình khác):

char* buf; 

buf = malloc(BUFSIZ); 

if (buf == ) { 

perror(argv[0]); 

exit(0); 



memset(buf, 0, BUFSIZ);