Bản thiết kế · 2.030 từ · 9 phút đọc

Bắt tay ba bước, từng gói một

Tại sao TCP cần chính xác ba gói để mở một kết nối, các bit SYN và ACK thực sự làm gì, và tại sao số thứ tự ban đầu có thể dự đoán được trở thành thảm họa bảo mật.

#TL;DR

Mọi kết nối TCP đều bắt đầu bằng ba gói. Client gửi SYN, server gửi SYN-ACK, client gửi ACK. Hai gói sẽ đủ để đồng bộ trạng thái, nhưng không đủ để phòng thủ chống lại các gói bị trùng lặp hoặc bị trễ từ các kết nối trước đó. Ba là tối thiểu làm cho cuộc trò chuyện vừa đồng bộ vừa không mơ hồ. Các số thứ tự được trao đổi trong ba gói đó trở thành hệ tọa độ cho mọi thứ khác TCP làm — và khi Kevin Mitnick chứng minh năm 1994 rằng chúng có thể dự đoán được, cả một loại tấn công chiếm đoạt kết nối đã có tên.

#Vấn đề TCP giải quyết lúc khởi động

Trước khi hai máy có thể trao đổi dữ liệu qua TCP, chúng cần đồng ý về ba điều:

  1. Một kết nối tồn tại. Cả hai bên biết chúng đang nói chuyện với nhau và đã được cấp phát tài nguyên.
  2. Điểm khởi đầu cho việc đánh số. Mỗi hướng của cuộc trò chuyện có không gian số thứ tự riêng. Mỗi byte được gửi đều được đánh số, đó là cách TCP xử lý phát lại, sắp xếp, và phát hiện trùng lặp.
  3. Bên kia thực sự có thể đến được ngay bây giờ. Không chỉ một gói đã đến — mà bên kia cũng có thể nói lại với bạn, với địa chỉ cụ thể này, tại thời điểm cụ thể này.

Bắt tay ba bước là trao đổi gói tối thiểu đạt được cả ba và bền vững chống lại các gói cũ từ các kết nối trước trôi từ mạng.

#SYN, SYN-ACK, ACK

Đây là những gì mỗi trong ba gói mang. Sơ đồ quen thuộc, nhưng các chi tiết quan trọng.

Client                                         Server
  │                                              │
  │── SYN, seq=x ─────────────────────────────►  │
  │                                              │  cấp phát trạng thái kết nối,
  │                                              │  chọn ISN y
  │                                              │
  │  ◄────────────── SYN, ACK, seq=y, ack=x+1 ── │
  │                                              │
  cấp phát trạng thái kết nối,                   │
  đẩy cửa sổ client tiến lên                     │
  │                                              │
  │── ACK, seq=x+1, ack=y+1 ──────────────────►  │
  │                                              │
  │══════════════  dữ liệu chảy  ═════════════════│
  • SYN (đồng bộ) — một gói với cờ SYN được đặt trong header TCP. Client chọn một Số Thứ Tự Ban Đầu x, và gói này nói: “Tôi muốn mở một kết nối. Số byte của tôi bắt đầu tại x.”
  • SYN-ACK — phản hồi của server. Nó chọn ISN y riêng cho hướng kia của kết nối, và xác nhận SYN của client bằng cách đặt ack = x + 1. Hai cờ được đặt cùng lúc: SYN (“Tôi cũng đang đồng bộ từ y”) và ACK (“Tôi đã nhận byte x của bạn”).
  • ACK — client xác nhận ISN của server: ack = y + 1. Không có cờ SYN lần này. Bắt tay hoàn thành.

Sau gói thứ ba, cả hai bên đã đồng ý về: các điểm cuối của kết nối, hai không gian số thứ tự độc lập, và sự thật rằng mỗi bên có thể gửi thành công đến bên kia.

#Tại sao hai gói không đủ

Câu hỏi hiển nhiên: tại sao ba? SYN + SYN-ACK sẽ không đủ — client nói “hãy nói chuyện,” server nói “ok từ y,” và họ xong?

Vấn đề là các gói cũ. Tưởng tượng client đã mở một kết nối hôm qua, gửi một SYN với ISN x, không nhận được câu trả lời, và từ bỏ. Hôm nay, cùng client mở một kết nối mới và gửi một SYN với ISN mới x'. Trong khi đó, SYN gốc của hôm qua — đã bị mất trong hàng đợi của một router nào đó — cuối cùng đến server.

Nếu giao thức là hai-chiều, server sẽ thấy một SYN với ISN x, cấp phát trạng thái, phản hồi với SYN-ACK, và bắt đầu chờ byte dữ liệu bắt đầu tại x+1. Client, không biết gì về điều này, gửi dữ liệu trên kết nối mới của mình bắt đầu tại x'+1. Server nhận các byte với số thứ tự nó không mong đợi và loại bỏ chúng — hoặc tệ hơn, chấp nhận chúng vào cuộc trò chuyện sai.

Gói thứ ba sửa điều này. Server không mở đầy đủ kết nối cho đến khi client xác nhận ISN của server. Một SYN cũ vẫn có thể đến server và kích hoạt phản hồi SYN-ACK, nhưng client không bao giờ gửi ACK cuối — vì client không biết về kết nối ma này — nên server hết thời gian chờ và tháo trạng thái nửa-mở.

Phiên bản hình thức của lập luận này đôi khi được gọi là “vấn đề hai vị tướng” áp dụng cho thiết lập kết nối. Bạn không bao giờ có thể đảm bảo cả hai bên có kiến thức chung hoàn hảo với một trao đổi tin nhắn hữu hạn, nhưng ba gói đưa bạn đủ gần cho mục đích thực tế.

#Số thứ tự ban đầu và cuộc tấn công Mitnick

Trong một thời gian dài, ISN được tạo ra một cách ngây thơ. Các triển khai BSD sớm tăng một bộ đếm toàn cục một lượng cố định mỗi giây, và một lượng cố định khác cho mỗi kết nối mới. Nếu bạn biết khi nào một máy khởi động lần cuối và nó đã phục vụ bao nhiêu kết nối, bạn có thể dự đoán ISN tiếp theo của nó.

Điều này nghe có vẻ học thuật cho đến khi bạn xem xét điều TCP ngầm giả định: chỉ ai đã thấy SYN-ACK của server mới biết ISN của server. Đó là thứ chứng minh bạn có thể nhận lưu lượng tại địa chỉ bạn tuyên bố. Nếu ISN của server có thể dự đoán được, một kẻ tấn công có thể giả mạo gói thứ ba của bắt tay mà không cần nhận gói thứ hai.

Năm 1994, Kevin Mitnick sử dụng chính xác kỹ thuật này để đột nhập vào máy trạm của Tsutomu Shimomura. Cuộc tấn công:

  1. Hệ thống của Mitnick gửi một SYN đến máy của Shimomura, tuyên bố đến từ địa chỉ IP của một máy đáng tin cậy.
  2. Máy của Shimomura gửi một SYN-ACK trở lại máy đáng tin cậy (không phải Mitnick).
  3. Mitnick đã ngập máy đáng tin cậy bằng các SYN để làm nó phớt lờ các gói đến.
  4. Mitnick đoán ISN của Shimomura dựa trên thời gian và gửi ACK cuối cùng anh ta, giả mạo địa chỉ nguồn của máy đáng tin cậy.
  5. Máy của Shimomura giờ tin rằng nó có một kết nối TCP mở với máy đáng tin cậy, và bắt đầu chấp nhận các lệnh.

Sửa là ISN ngẫu nhiên. RFC 6528 (2012) hình thức hóa yêu cầu rằng ISN phải được dẫn xuất từ một hash mật mã của bộ kết nối cộng với một bí mật, làm cho chúng thực sự không thể đoán được bởi một kẻ tấn công ngoài-đường. Điều này trở thành yêu cầu cứng; bất kỳ OS hiện đại nào không triển khai nó sẽ không tương tác an toàn trên internet công cộng.

#SYN Flood và các kết nối nửa-mở

Có một cuộc tấn công thứ hai mà bắt tay đã mở cửa cho. Khi server nhận một SYN, nó cấp phát trạng thái kết nối — một slot trong cấu trúc dữ liệu kernel — và chờ ACK cuối. Nếu ACK không bao giờ đến, slot vẫn được dành cho đến khi hết thời gian chờ.

Năm 1996, các kẻ tấn công bắt đầu ngập các server bằng các gói SYN từ các địa chỉ IP giả mạo. Server sẽ gửi các SYN-ACK không đi đâu, và bảng kết nối nửa-mở của nó sẽ đầy. Các client hợp pháp không thể có slot; server thực sự ngừng chấp nhận kết nối.

Phòng thủ là SYN cookies, được Daniel J. Bernstein và Eric Schenk phát minh. Thay vì cấp phát trạng thái trên SYN đầu tiên, server mã hóa các tham số kết nối vào ISN của chính nó bằng một hash mật mã. Nếu client ACK với một cookie hợp lệ, server có thể tái tạo trạng thái; nếu không, không có gì được cấp phát ngay từ đầu. Điều này biến bắt tay từ một hoạt động có trạng thái thành không trạng thái trong quá tải, và SYN flood trở thành một cuộc tấn công hiệu quả hơn nhiều.

#Đóng một kết nối

Bắt tay có một đối tác đối xứng lúc tháo dỡ kết nối, đôi khi được gọi là bắt tay bốn bước:

  Client                                   Server
    │── FIN ────────────────────────────►    │
    │  ◄─────────────────────── ACK ──────    │

    │   (server vẫn có dữ liệu để gửi)

    │  ◄─────────────────────── FIN ──────    │
    │── ACK ────────────────────────────►    │

Các kết nối TCP là song công đầy đủ và có thể được đóng độc lập ở mỗi hướng. Client FIN hướng của mình, server ACK nó — nhưng server có thể tiếp tục gửi miễn là nó muốn, cho đến khi nó gửi FIN riêng của nó. Bốn bước thực sự là hai đóng một-chiều, mỗi cái với ACK riêng.

Sau ACK cuối, bên đóng vào trạng thái nổi tiếng TIME_WAIT — thường 60 đến 120 giây — nơi nó từ chối tái sử dụng số cổng cục bộ. Điều này tồn tại chính xác để bắt các gói đi lạc từ kết nối vừa đóng, để chúng không vô tình rơi vào một kết nối mới hoàn toàn tái sử dụng cùng bộ tứ. TIME_WAIT là lý do các server lưu lượng cao định kỳ hết cổng phù du dưới tải; đó là một hệ quả trực tiếp của các đảm bảo đúng đắn của bắt tay.

#Làm cho bắt tay biến đi

Bắt tay ba bước tốn một vòng quay trễ trước khi bất kỳ dữ liệu nào có thể chảy. Trên mạng cục bộ đó là vô hình; trên một liên kết vệ tinh đó là 500+ ms thời gian chết mỗi khi một kết nối mở.

Một số thứ đã cố gắng để tránh nó:

  • TCP Fast Open (RFC 7413) cho phép một client đã nói chuyện với một server trước đây gửi dữ liệu trong chính gói SYN, sử dụng một cookie mật mã từ kết nối trước làm bằng chứng nó không phải kẻ tấn công. Nó hoạt động nhưng được hỗ trợ không nhất quán bởi các middlebox.
  • TLS 1.3 0-RTT cho phép một client tiếp tục một phiên gửi dữ liệu ứng dụng được mã hóa trong tin nhắn TLS đầu tiên — nhưng với chi phí bảo vệ chống replay yếu hơn cho tin nhắn đầu tiên đó.
  • QUIC gấp các bắt tay truyền và mật mã lại với nhau. Cho một kết nối đã tiếp tục, QUIC có thể thực sự 0-RTT: gói đầu tiên chứa dữ liệu.

Tất cả những điều này chia sẻ một chủ đề: chi phí của bắt tay ba bước có thể chấp nhận được khi vòng quay là 10 ms. Không phải khi dịch vụ của bạn toàn cầu và RTT p99 của bạn là 200 ms. Bắt tay chưa được thay thế — nó đã được phân bổ.

#Năm mươi năm của ba gói

Bài viết TCP/IP hiển thị sơ đồ bắt tay trong bốn dòng. Bốn dòng đó đã là điểm xuất phát của mọi kết nối TCP từng được thực hiện — trên ARPANET năm 1974, trên một điện thoại di động năm 2026, và trên thứ gì đó đang chạy trong một rack ở đâu đó ở giữa. Các chi tiết đã được tinh chỉnh: số thứ tự giờ là mật mã, cookie xử lý quá tải, và các phương tiện truyền hiện đại gấp bắt tay vào một cuộc đàm phán dài hơn với mã hóa được tích hợp. Nhưng cấu trúc là như nhau:

  • Ba gói, vì hai không đủ để loại trừ trạng thái cũ.
  • Hai không gian số thứ tự độc lập, vì cuộc trò chuyện là song công đầy đủ.
  • Trạng thái được cấp phát chỉ sau khi cả hai bên xác nhận tính sống, vì cấp phát trên gói đầu tiên có thể bị khai thác.

Mọi tính chất của TCP tiếp theo — độ tin cậy, sắp xếp, điều khiển luồng, điều khiển tắc nghẽn — đều phụ thuộc vào việc bắt tay thiết lập hệ tọa độ chia sẻ đó trước. Đó là nền tảng bạn không thể thấy một khi kết nối được mở. Nó cũng là điều đầu tiên mà bất kỳ phương tiện truyền mới nào cũng phải quyết định có giữ hay không.