Bài trước đo băm mật khẩu — làm mỗi lần băm chậm để kẻ tấn công đoán chậm. Bài này lật sang phía kẻ tấn công: dựng một cuộc tấn công thật vào một cơ sở dữ liệu mật khẩu đã băm, và đo bao nhiêu mật khẩu bẻ được, mất bao lâu, trên thuật toán nhanh so với chậm. Kết quả cho thấy một điều mà cả người dùng lẫn lập trình viên thường hiểu sai: hai lớp phòng thủ — mật khẩu mạnh và thuật toán chậm — bảo vệ những thứ khác nhau, và bạn cần cả hai.

Toàn bộ chạy trong container tự dựng, tự dọn. Chỉ tấn công dữ liệu do chính tôi tạo. Không nhắm vào hệ thống của ai.

Do mat khau: tan cong tu dien tren SHA256 vs bcrypt, hai truc phong thu

Cách đo

Tôi tạo cơ sở dữ liệu của mười người dùng. Sáu người đặt mật khẩu yếu — nằm trong danh sách mật khẩu phổ biến (123456, password, qwerty...). Bốn người đặt mật khẩu mạnh — chuỗi ngẫu nhiên 18 ký tự. Băm cả mười theo hai cách: SHA256 (nhanh, sai cách) và bcrypt (chậm, đúng cách). Rồi chạy một cuộc tấn công từ điển: thử lần lượt hơn một trăm mật khẩu phổ biến với từng bản băm.

Số đo

Ba lần chạy, kết quả giống hệt:

SHA256 (nhanh) bcrypt cost 11 (chậm)
Mật khẩu yếu bẻ được 6/10 6/10
Mật khẩu mạnh bẻ được 0/10 0/10
Thời gian chạy từ điển ~0,00 giây ~51 giây
Tốc độ dò ~3.900.000 lần/giây ~12 lần/giây
Dò vét cạn mật khẩu 8 ký tự ~0,011 năm (~4 ngày) ~3.800 năm

Điều đáng nhớ

Thuật toán chậm không cứu được mật khẩu yếu. Đây là điểm dễ hiểu sai nhất. Cả sáu mật khẩu yếu bị bẻ kể cả trên bcrypt — vì tấn công từ điển chỉ thử đúng những mật khẩu phổ biến, và 123456 nằm trong danh sách đó. Bcrypt làm cuộc tấn công mất 51 giây thay vì 0 giây, nhưng 51 giây vẫn là "bẻ được ngay". Nếu mật khẩu của bạn nằm trong bất kỳ danh sách rò rỉ nào, thuật toán băm mạnh đến đâu cũng không cứu được — kẻ tấn công không dò vét cạn, nó chỉ thử đúng mật khẩu của bạn.

Thuật toán chậm bảo vệ mật khẩu mạnh — và bảo vệ khổng lồ. Bốn mật khẩu ngẫu nhiên không bẻ được bằng từ điển, nên kẻ tấn công phải dò vét cạn. Ở đó thuật toán quyết định tất cả: với SHA256, một mật khẩu 8 ký tự chữ-và-số dò vét cạn hết trong ~4 ngày; với bcrypt, cùng mật khẩu đó cần ~3.800 năm. Chênh lệch ~325.000 lần đến thẳng từ chênh lệch tốc độ dò (3,9 triệu so với 12 lần mỗi giây). Bcrypt biến một mật khẩu mạnh từ "bẻ được trong một tuần" thành "không bao giờ bẻ được".

Hai lớp nhân nhau, không cộng. Sức mạnh mật khẩu quyết định kích thước không gian cần dò; tốc độ băm quyết định bao lâu mỗi lần thử. Thời gian bẻ là tích của hai. Một mật khẩu mạnh trên SHA256 vẫn chỉ tốn 4 ngày (thuật toán yếu kéo xuống); một mật khẩu yếu trên bcrypt vẫn bẻ trong 51 giây (mật khẩu yếu kéo xuống). Chỉ mật khẩu mạnh cộng thuật toán chậm mới cho 3.800 năm. Thiếu một lớp là toàn bộ sụp về mức của lớp yếu hơn.

Vì sao tôi tấn công từng người dùng một, không cả bảng cùng lúc

Một chi tiết trong cách đo phản ánh một tính chất bảo mật thật. Bộ đo của tôi thử từ điển cho từng bản băm riêng — nó không thể tính một lần rồi so với cả bảng. Lý do là salt: mỗi mật khẩu được băm với một chuỗi ngẫu nhiên khác nhau (bài trước), nên 123456 của người dùng A và 123456 của người dùng B ra hai bản băm khác hẳn. Kẻ tấn công không thể băm 123456 một lần rồi tra cả mười người; nó phải băm lại 123456 với salt của từng người.

Đây là lý do salt nhân chi phí tấn công lên theo số người dùng: bẻ một bảng nghìn người là làm lại toàn bộ công việc nghìn lần. Không có salt, kẻ tấn công băm mỗi mật khẩu từ điển đúng một lần và tra cả bảng — nhanh gấp số-người-dùng lần. Trong phép đo, 51 giây của bcrypt là cho mười người; không có salt nó sẽ gần như không đổi dù bảng lớn tới đâu.

Vì sao

Hai kiểu tấn công có kinh tế học khác nhau, và hai lớp phòng thủ nhắm vào hai kiểu:

  • Tấn công từ điển thử một danh sách hữu hạn các mật khẩu có khả năng. Nó không quan tâm không gian mật khẩu lớn cỡ nào — chỉ quan tâm mật khẩu của bạn có trong danh sách không. Phòng thủ duy nhất: đừng dùng mật khẩu có thể đoán. Thuật toán chậm chỉ làm nó tốn thời gian hơn một chút, không chặn được.
  • Tấn công vét cạn thử mọi tổ hợp. Nó luôn thành công nếu có đủ thời gian, nên phòng thủ là làm thời gian đó dài vô lý. Ở đây cả hai lớp đều có tác dụng: mật khẩu dài hơn tăng số tổ hợp theo hàm mũ, thuật toán chậm tăng thời gian mỗi lần thử.

Điểm mấu chốt: kẻ tấn công luôn thử từ điển trước, vì nó rẻ. Chỉ khi từ điển thất bại nó mới vét cạn. Nên mật khẩu yếu chưa bao giờ chạm tới lớp phòng thủ thuật toán — nó thua ở bước đầu tiên.

Nghĩa là gì trong thực tế

  • Với người dùng: mật khẩu ngẫu nhiên, dài, là lớp phòng thủ bạn kiểm soát. Một mật khẩu 16 ký tự ngẫu nhiên không nằm trong từ điển nào, nên nó buộc kẻ tấn công vét cạn — nơi thuật toán chậm phát huy. Dùng trình quản lý mật khẩu; đừng tự nghĩ mật khẩu "khó đoán" (con người sinh mật khẩu tệ hơn mình tưởng).
  • Với lập trình viên: dùng thuật toán chậm là điều kiện cần, không đủ. Bcrypt/argon2 chỉ bảo vệ những người dùng đã có mật khẩu mạnh. Với người dùng đặt 123456, bạn cần thêm lớp khác: kiểm mật khẩu so với danh sách rò rỉ khi họ đăng ký (dịch vụ như Have I Been Pwned có API cho việc này), và giới hạn số lần đăng nhập sai để chặn dò trực tuyến.
  • Cấm mật khẩu phổ biến khi đặt. Chặn 123456 ngay lúc đăng ký rẻ hơn nhiều so với chịu hậu quả khi cơ sở dữ liệu rò.

Chỗ tôi không kết luận được

Từ điển của tôi chỉ hơn trăm mật khẩu; từ điển thật (như rockyou) có hàng chục triệu, và các danh sách rò rỉ hiện đại có hàng tỷ. Tỷ lệ bẻ được mật khẩu yếu trong thực tế còn cao hơn 6/10 nhiều — tôi chỉ đo cơ chế, không đo quy mô thật.

Và tôi dò trên CPU đơn. Kẻ tấn công thật dùng cụm GPU: tốc độ SHA256 của họ không phải 3,9 triệu mà hàng chục tỷ mỗi giây, kéo "~4 ngày" xuống còn phút. Với bcrypt/argon2 (memory-hard, bài trước), lợi thế GPU đó co lại nhiều — đó chính là lý do con số 3.800 năm của bcrypt bền hơn con số của SHA256 khi lên phần cứng thật.

Thử ba mươi giây

Nghĩ tới mật khẩu bạn dùng cho một tài khoản quan trọng. Nó có nằm trong đầu bạn như một từ, một cụm, một ngày tháng, một tên không? Nếu có, nó gần như chắc chắn nằm trong một từ điển tấn công nào đó — và không thuật toán băm nào ở phía máy chủ cứu được nó.

Cách kiểm thật: vào trang Have I Been Pwned, gõ mật khẩu đó (trang dùng k-anonymity, không gửi nguyên mật khẩu đi). Nếu nó xuất hiện trong dữ liệu rò rỉ, đổi ngay — không phải vì trang đó biết bạn là ai, mà vì kẻ tấn công cũng có đúng danh sách đó.