Quá trình tạo ra lỗ hổng tràn bộ nhớ heap trong curl
(daniel.haxx.se)- CVE-2023-38545 được công bố cùng với bản phát hành curl 8.4.0 là một lỗ hổng tràn bộ đệm heap xảy ra trong quá trình xử lý proxy SOCKS5, và là một lỗ hổng mức độ nghiêm trọng HIGH khá hiếm trong các vấn đề bảo mật của curl
- Vấn đề được đưa vào từ năm 2020 trong quá trình chuyển mã kết nối SOCKS5 từ lời gọi blocking sang máy trạng thái non-blocking, và ảnh hưởng từ curl 7.69.0
- Logic cũ vốn chuyển chế độ phân giải tên từ xa sang phân giải cục bộ khi gặp hostname dài quá 255 byte đã kết hợp với việc máy trạng thái được gọi lại, khiến hostname quá dài có thể bị sao chép vào một bộ đệm nhỏ
- Để khai thác thành công, ứng dụng khách libcurl phải dùng SOCKS5 proxy-resolver-mode và tự động chuyển hướng, đồng thời máy chủ HTTPS do kẻ tấn công kiểm soát phải trả về HTTP 30x
Location:chứa hostname dài hơn 16KB và không quá 64KB - Từ curl 8.4.0, với hostname quá dài, curl không còn chuyển từ phân giải từ xa sang cục bộ mà trả về lỗi, đồng thời đã bổ sung bài kiểm thử để chặn cùng kịch bản này
CVE-2023-38545 và curl 8.4.0
- Cùng với bản phát hành curl 8.4.0, advisory bảo mật và chi tiết về CVE-2023-38545 đã được công bố
- Đây được đánh giá là vấn đề bảo mật nghiêm trọng nhất xuất hiện ở curl sau một thời gian dài, với mức độ nghiêm trọng được xếp là HIGH
- Lỗi cốt lõi là một tràn bộ đệm heap xảy ra trong một số điều kiện nhất định khi xử lý kết nối proxy SOCKS5
SOCKS5 và cách phân giải tên
- curl hỗ trợ SOCKS5 từ tháng 8 năm 2002
- SOCKS5 là một giao thức proxy dùng để thiết lập giao tiếp mạng thông qua một máy chủ trung gian
- Có thể được dùng để thiết lập giao tiếp qua Tor
- Cũng có thể được dùng khi truy cập Internet từ bên trong tổ chức hoặc công ty
- SOCKS5 có hai cách phân giải hostname
- Ứng dụng khách phân giải cục bộ hostname rồi chuyển địa chỉ đã phân giải cho proxy
- Ứng dụng khách chuyển toàn bộ hostname cho proxy và proxy sẽ phân giải từ xa
Thay đổi năm 2020 đã đưa lỗ hổng vào
- Đầu năm 2020, hàm kết nối tới proxy SOCKS5 được chuyển từ lời gọi blocking sang máy trạng thái non-blocking
- Thay đổi này được đưa vào master ngày 14/2/2020 và có trong curl 7.69.0
- curl 7.69.0 là bản phát hành đầu tiên có cải tiến này, và cũng là bản phát hành đầu tiên trở nên dễ tổn thương bởi CVE-2023-38545
- Mục tiêu của thay đổi là mang lại cải thiện rõ rệt hơn khi nhiều phiên truyền song song đều đi qua SOCKS5
Chế độ phân giải bị phá vỡ trong máy trạng thái
- Hàm máy trạng thái sẽ được gọi lặp lại mỗi khi có thêm dữ liệu mạng đi vào cho đến khi kết nối được thiết lập
- Biến cục bộ
socks5_resolve_localở đầu hàm biểu thị curl sẽ tự phân giải hostname hay chuyển tên cho proxy - Biến này được đặt lại ở đầu mỗi lần gọi hàm theo chế độ proxy mỗi khi máy trạng thái chạy
- Điều kiện ở trạng thái INIT đã tạo ra vấn đề
- Trường hostname của SOCKS5 chỉ cho phép tối đa 255 byte
- Nếu hostname vượt quá 255 byte, proxy SOCKS5 không thể phân giải nó
- Mã curl trước đó khi gặp hostname quá dài trong chế độ phân giải từ xa sẽ đổi
socks5_resolve_localsangTRUEđể chuyển sang chế độ phân giải cục bộ
- Nếu người dùng đã yêu cầu phân giải từ xa, curl lẽ ra không nên đổi chế độ mà phải thất bại, nhưng hành vi chuyển đổi được thêm từ rất lâu trước đó vẫn tiếp tục tồn tại
Luồng dẫn đến tràn heap
- Nếu máy chủ SOCKS5 không đủ nhanh nên máy trạng thái không nhận được thêm dữ liệu mạng để tiếp tục, hàm sẽ trả về
- Sau đó khi có dữ liệu, cùng hàm máy trạng thái sẽ được gọi lại
- Khi được gọi lại,
socks5_resolve_locallại được đặt theo chế độ proxy ở đầu hàm- Giá trị đã bị đổi sang
TRUEở lần gọi trước do hostname quá dài sẽ không được giữ lại - Giá trị lại trở về trạng thái proxy phải phân giải tên từ xa
- Giá trị đã bị đổi sang
- curl tạo khung giao thức để gửi cho proxy trong một bộ đệm bộ nhớ và sao chép thông tin đích vào đó
- Do giá trị trạng thái sai, nếu nó cố chuyển nguyên hostname quá dài, dữ liệu có thể ghi đè sang bộ nhớ heap lân cận vượt ra ngoài bộ đệm đích đã cấp phát
Kích thước bộ đệm và giới hạn hostname
- Khi tạo khung giao thức để gửi cho proxy, curl tái sử dụng bộ đệm tải xuống thông thường
- Các điều kiện về kích thước bộ đệm tải xuống như sau
- Giá trị mặc định là 16KB
- Công cụ curl đặt kích thước bộ đệm là 100KB
- Có thể dùng kích thước khác tùy theo yêu cầu của ứng dụng
- Kích thước nhỏ nhất được cho phép là 1024 byte
- Nếu kích thước bộ đệm nhỏ hơn 65541 byte thì có thể xảy ra tràn này
- Bộ đệm càng nhỏ thì kích thước tràn có thể càng lớn
- Hostname trong URL về thực tế không có giới hạn kích thước cứng, nhưng trình phân tích URL của libcurl từ chối tên dài quá 65535 byte
- DNS chỉ cho phép hostname tối đa 253 byte
- Tên hợp lệ dài hơn 253 byte là hiếm, và tên thực tế dài hơn 1024 byte hầu như không gặp, nên cuộc tấn công đòi hỏi một hostname rất dài được tạo ra có chủ ý
- Trường hostname của URL chỉ có thể chứa một số octet nhất định, và các giá trị byte không hợp lệ sẽ bị trình phân tích URL từ chối
- Nếu libcurl được build để dùng thư viện IDN, thư viện đó cũng có thể từ chối hostname không hợp lệ
Điều kiện để khai thác thành công
- Kẻ tấn công phải kiểm soát một máy chủ HTTPS mà ứng dụng khách dùng libcurl truy cập qua proxy SOCKS5 ở proxy-resolver-mode
- Máy chủ tấn công phải có thể trả về chuyển hướng HTTP 30x đã bị thao túng
- Header
Location:của chuyển hướng có thể chứa hostname rất dài theo dạng sauLocation: https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/- Độ dài hostname là lớn hơn 16KB và không quá 64KB
- Nếu ứng dụng khách libcurl bật tự động theo dõi chuyển hướng, và proxy SOCKS5 “đủ chậm” để kích hoạt vấn đề với biến cục bộ này, hostname bị thao túng sẽ bị sao chép vào một bộ đệm quá nhỏ
- Khi các điều kiện này khớp, thao tác ghi sang bộ nhớ heap lân cận sẽ xảy ra và hình thành tràn bộ đệm heap
Bản sửa lỗi và bug bounty
- Từ curl 8.4.0, khi gặp hostname quá dài, curl không còn đổi từ phân giải từ xa sang cục bộ mà trả về lỗi
- Một ca kiểm thử chuyên biệt cho cùng kịch bản này cũng đã được thêm vào
- Vấn đề này được Jay Satiro báo cáo, phân tích và vá lỗi
- Lỗ hổng này nhận được khoản bug bounty lớn nhất từng được chi trả trong lịch sử curl cho đến nay
- 4.660 USD cho người báo cáo
- 1.165 USD cho dự án curl theo IBB policy
C và ngôn ngữ an toàn bộ nhớ
- Nếu curl được viết bằng ngôn ngữ an toàn bộ nhớ thay vì C, loại lỗi này đã không xảy ra
- Tuy vậy, việc port curl sang ngôn ngữ khác hiện không nằm trong kế hoạch
- Cách tiếp cận thực tế hiện thu hẹp còn hai hướng
- Cho phép, sử dụng và hỗ trợ nhiều phụ thuộc được viết bằng ngôn ngữ an toàn bộ nhớ hơn
- Thay thế dần từng phần của curl như cách đưa hyper vào
- Những nỗ lực này hiện tiến triển rất chậm và các khó khăn liên quan cũng đã bộc lộ rõ
- curl trong tương lai gần vẫn sẽ được viết bằng C
- Nếu tính cả 2 CVE mới nhất được báo cáo trong curl 8.4.0, thì 41% các lỗ hổng bảo mật từng được phát hiện trong curl có thể đã không xảy ra nếu dùng ngôn ngữ an toàn bộ nhớ
- Tuy vậy, khoảng 80% các vấn đề liên quan đến C được đưa vào ở giai đoạn đầu là lúc Rust chưa phải một lựa chọn thực tế cho mục đích này
Lỗ hổng được phát hiện sau 1315 ngày
- Lỗ hổng này đã tồn tại trong mã suốt 1315 ngày
- Nếu có bộ kiểm thử tốt hơn, nó có thể đã được phát hiện sớm hơn
- curl chạy lặp lại nhiều công cụ phân tích mã tĩnh, nhưng không công cụ nào tìm ra được vấn đề trong hàm này
- Việc phát hành mã có lỗi tràn heap vào hơn 20 tỷ môi trường cài đặt rõ ràng không phải là trải nghiệm đáng khuyến nghị
- Có thể xem quá trình báo cáo và xử lý trước khi công bố tại HackerOne report
1 bình luận
Ý kiến trên Hacker News
Vẫn thật đáng kinh ngạc khi nhiều thiết bị đến vậy về cơ bản lại phụ thuộc vào một thư viện phần lớn do một người viết. Hẳn áp lực là rất lớn, và câu dưới đây cũng cho thấy điều đó
“Giờ đọc lại mã thì không thể không thấy lỗi. Thật đau khi phải chấp nhận rằng tôi đã không nhận ra sai lầm này, và khiếm khuyết đó đã nằm trong mã suốt 1315 ngày mà không bị phát hiện. Tôi xin lỗi. Tôi cũng chỉ là con người.”
Nếu Daniel đọc được điều này, tôi nghĩ rằng cảm ơn anh vì đã làm việc chăm chỉ, và anh hoàn toàn không cần phải xin lỗi. Rốt cuộc mã nguồn đã được công khai để bất kỳ ai cũng có thể đọc và rà soát
https://www.buzzfeed.com/chrisstokelwalker/the-internet-is-b...
“Báo cáo này trông hoàn toàn đúng, và nó khiến tôi đau lòng sâu sắc.”
Cũng như câu trong blog: “Tôi không khuyến khích trải nghiệm phát hành một heap overflow trong mã được đưa vào hơn 2 tỷ môi trường cài đặt”; thật khắc nghiệt khi phải gánh trách nhiệm lớn đến vậy với phần đền đáp ít ỏi như thế
Cá nhân tôi thường nghĩ về việc dù cố tập trung đến đâu, ta vẫn bỏ lỡ rất nhiều thứ. Những hoạt động như code review có vẻ là cách tốt để rèn luyện phần này, tức rèn sự chú ý hoặc phát triển nhận thức toàn cục, theo ngữ cảnh. Ví dụ, ở một nơi tôi thường đi ngang qua suốt nhiều năm có một luống hoa được ai đó chăm sóc rất đẹp; sau khi lần đầu nhận ra, trong khoảng một năm tôi thỉnh thoảng dừng lại ngắm, nhưng vài tháng trước tôi mới lần đầu biết rằng chỉ cách đó vài feet có một luống hoa khác như một cặp với nó. Rất có thể luống hoa đó đã ở đó từ đầu, nhưng vì tôi thậm chí không biết nó tồn tại nên không thể biết chắc
Nếu mở rộng cách nghĩ này ra tri thức của toàn nhân loại, đôi khi dường như chỉ một người nhận ra điều gì đó, rồi quan sát ấy làm thay đổi hành động và bắt đầu lan ra cho tất cả chúng ta. Đôi khi cần nhiều lần thử và rất nhiều thời gian. Tôi cũng tự hỏi còn bao nhiêu trái thấp dễ hái mà chưa ai nhận ra, và cho rằng chỉ riêng việc tích hợp những thực hành mẫu mực đơn giản, nền tảng do những người đã chú ý xây dựng cũng có thể nâng mức sàn chung của tất cả mọi người
Bài của Julia Evans https://jvns.ca/blog/2023/10/06/new-talk--making-hard-things... và của Dan Luu https://danluu.com/p95-skill/ cũng liên quan đến điều này. Tóm lại, tôi rất biết ơn Stenberg và curl, cũng như những người đã tạo ra nhiều phần của hạ tầng Internet mà chúng ta dùng hằng ngày như điều hiển nhiên
Đôi khi nguyên tắc “nếu chưa hỏng thì đừng sửa” bị diễn giải thành “đừng hỗ trợ cho đến khi nó hỏng”, dẫn đến những bất ngờ khá khó chịu
Có thể có vấn đề về tính bền vững, nhưng tôi nghĩ những dự án như vậy sẽ còn tiếp tục tồn tại, và xin bày tỏ sự kính trọng với Daniel cũng như tất cả những người làm việc không chỉ cho bản thân mà còn cho cộng đồng
Kết luận về an toàn bộ nhớ và ngôn ngữ an toàn bộ nhớ rất hợp lý. Ý chính là như sau
Nếu curl được viết bằng một ngôn ngữ an toàn bộ nhớ thay vì C, lớp khiếm khuyết này hẳn đã không thể xảy ra
Cách tiếp cận được xem là khả thi và hợp lý theo hướng đó là cho phép, sử dụng và hỗ trợ nhiều hơn các dependency được viết bằng ngôn ngữ an toàn bộ nhớ, đồng thời thay thế dần từng mảnh một của curl, như việc đưa hyper vào
Tuy nhiên kiểu phát triển này hiện đang diễn ra chậm gần như băng hà, và cho thấy các bài toán khó liên quan một cách đau đớn rõ ràng. curl sẽ vẫn là C trong tương lai có thể dự đoán được. Ai không thích thì có thể tự xắn tay áo lên mà làm
Tính cả hai CVE mới nhất được báo cáo trong curl 8.4.0, cho đến nay 41% tổng số lũy kế các lỗ hổng bảo mật được phát hiện trong curl có lẽ đã không xảy ra nếu dùng ngôn ngữ an toàn bộ nhớ. Nhưng Rust không phải là một lựa chọn thực dụng cho mục đích này vào thời điểm khoảng 80% đầu tiên của các vấn đề liên quan đến C được đưa vào
Đã chạy đi chạy lại nhiều trình phân tích mã tĩnh, nhưng không trình nào phát hiện được vấn đề nào trong hàm này
Bài phân tích rất xuất sắc. Tuy nhiên, ngay cả sau khi đọc đến CVE, vẫn chưa rõ trong tình huống nào thì bị ảnh hưởng. Theo tôi hiểu, sẽ bị ảnh hưởng khi thỏa các điều kiện sau
Sử dụng proxy SOCKS5, phân giải tên máy chủ thông qua proxy đó, kích thước bộ đệm đã được đổi từ giá trị mặc định 100KB xuống dưới 65541 byte, và proxy SOCKS5 quá chậm để xử lý yêu cầu ngay lập tức
Sửa: phần mô tả liên quan đến bộ đệm không chính xác. libcurl tái sử dụng bộ đệm tải xuống và mặc định là 16KB, nhưng bản thân curl được cho là đặt thủ công thành 100KB trừ khi dùng
--limit-rate. Điều kiện độ trễ cũng sai. Theo CVE, chỉ độ trễ máy chủ thông thường cũng nhiều khả năng đủ “chậm” để kích hoạt lỗi này, và kẻ tấn công không cần tác động bằng cách gây từ chối dịch vụ hay kiểm soát máy chủ SOCKSVì vậy đường tấn công trông rất hẹp, tôi tự hỏi liệu mình có bỏ sót điều gì không
root@1aac5e228e16:/build/curl-7.74.0# curl -vvv -x socks5h://host.docker.internal:9050 $(python3 -c "print(('A'10000), end='')")Trying 192.168.65.254:9050...* SOCKS5: server resolving disabled for hostnames of length > 255 [actual len=10000]* SOCKS5 connect to AAAAA...* Send failure: Bad file descriptor* Failed to send SOCKS5 connect request.Segmentation faulthttps://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a8...
docker-proxy, dùng để tạo đường hầm SOCKS5 tới một container Docker chạy openconnect VPN. Nó hữu ích khi phải xử lý nhiều VPN khác nhau của nhiều khách hàng, trong đó mỗi VPN đều yêu cầu gửi toàn bộ lưu lượng của máy về phía họhttps://github.com/carlosonunez/docker-proxy
Vì nó được thiết kế để phân giải DNS ở phía từ xa, nên CVE này áp dụng trực tiếp ở đây
Có thể nói đây là bài phân tích CVE hay nhất tôi từng đọc đến nay. Dù là người tạo ra một trong những phần mềm cốt lõi của thời đại, tác giả vẫn thể hiện thái độ khiêm tốn xuyên suốt bài viết, điều này thật đáng khen. Thực sự rất đáng kính
if(!socks5_resolve_local && hostname_len > 255) {socks5_resolve_local = TRUE;}Đây thật sự là một ý tưởng tệ. Với những người dựa vào công cụ vượt kiểm duyệt để bảo vệ quyền riêng tư, điều này có thể làm rò rỉ danh tính qua DNS
Tác giả cũng nghĩ như vậy
https://hackerone.com/reports/2187833
Người ta nói “nếu curl được viết bằng một ngôn ngữ an toàn bộ nhớ thay vì C thì loại lỗi này đã không thể xảy ra”, nhưng cùng lắm là có thể đã không xảy ra. Ta không bao giờ biết chắc liệu có cách nào vượt qua rào chắn máy ảo của ngôn ngữ hay không. Tuy nhiên, tác giả rõ ràng đã bỏ qua giới hạn tên máy chủ DNS được nêu trong RFC1123, và giới hạn này cũng được hardcode trong các thư viện Java
https://www.rfc-editor.org/rfc/rfc1123
“Tên máy chủ trong URL không có giới hạn kích thước thực tế, nhưng bộ phân tích URL của libcurl từ chối các tên dài hơn 65535 byte. DNS chỉ cho phép tên máy chủ tối đa 253 byte. Vì vậy, tên hợp lệ dài hơn 253 byte là hiếm. Trên thực tế gần như chưa từng nghe đến tên thực dài hơn 1024 byte.”
DNS ngày nay được dùng trong 99,9% trường hợp, nhưng không phải là cơ chế duy nhất để phân giải tên máy chủ thành địa chỉ
Tôi không tìm thấy chỗ RFC đặt giới hạn trên cho kích thước tên máy chủ
Nó cũng có thể có nghĩa là trình biên dịch cố gắng chứng minh rằng bộ nhớ không bị truy cập vượt biên, không bị truy cập khi không có quyền sở hữu rõ ràng, và chỉ được truy cập trong vòng đời của đối tượng nền. Ví dụ Rust làm như vậy
Tất nhiên trình biên dịch cũng có thể thêm các kiểm tra lỗi nội bộ và cơ chế bảo vệ tại những điểm quan trọng. Rust không làm vậy, nhưng điều đó có thể hữu ích trong các hệ thống phải lo đến những thứ như bit flip bên trong thanh ghi. Ví dụ như môi trường bức xạ cao kiểu máy quét X-ray, nơi ECC memory không bắt được
Để khai thác được thì cần những điều kiện rất cụ thể, nhưng vẫn có khá nhiều thổi phồng và kịch tính
Bản cập nhật bảo mật Windows 10 phát hành hôm qua không bao gồm phiên bản curl mới. curl đi kèm trong Windows vẫn là 8.0.1
Đây là một bài dài và thú vị, nhưng có thể rút gọn thành: “vì vậy các thư viện hệ thống nên được viết bằng ngôn ngữ an toàn hoặc được chứng minh tính đúng đắn”
Nếu một trong những lập trình viên C giỏi nhất, tử tế nhất và minh bạch nhất thời đại chúng ta đang viết những điều như thế này, thì chúng ta nên chú ý