Timing attack là gì?
Vài tháng trước tui từng viết về Billion Laugh Attack, một loại hình tấn công lợi dụng entity reference expansion trong XML để làm tràn bộ nhớ hệ thống. Trong bài viết này, tui sẽ giới thiệu Timing Attack, một kiểu tấn công cũng có thể ảnh hưởng đến miếng cơm manh áo của một kĩ sư phần mềm như bạn.
Photo by Bru-nO on Pixabay.
Timing attack là một loại side-channel attack (tấn công kênh kề) trong ngành mật mã học, kiểu tấn công dựa vào những thông tin thu thập được từ cách thực thi của hệ thống. Cụ thể ở đây timing attack phân tích thời gian thực thi của ứng dụng, để từ đó tiến hành các mưu đồ xấu độc. Xấu độc :thinking_face:, nghe y hệt cách chính phủ mô tả mạng xã hội ha .
Bằng cách đo đạc thời gian thực thi của một số thao tác nhất định, hacker có thể sử dụng những thông tin này để truy dần ra dữ liệu đầu vào.
Ô kê phai! Nhưng thân là kĩ sư phần mềm, timing attack thì có ảnh hưởng gì đến chén cơm của tui?
Nhưng đâm ra nó có. Kiểu tấn công này thường dễ bị bỏ hớ, lại hay đến từ cách ta viết code. Những kẻ tấn công lợi dụng bản năng của chúng ta, những kĩ sư luôn muốn code của mình chạy càng nhanh càng tốt, để thực hiện ý đồ tấn công. Đúng nghĩa mất dạy là mẹ thành công mà .
Ta cùng lướt qua hai ví dụ sau để hiểu rõ thêm.
Password timing attack
API của một website không nổi tiếng lắm sử dụng chữ kí HMAC để verify payload gửi lên server. Đoạn code verify của họ dùng ngôn ngữ Ruby được viết như vầy:
def verify(payload, key, signature)
hmac(payload) == signature
end
private
def hmac(data)
# actual implementation
end
Đoạn code nhìn sơ qua không có gì vấn đề gì quá đặc biệt :thinking_face:. Nó chỉ tạo lại chữ ký HMAC cho payload và so trùng với chữ ký được người dùng đưa lên.
Để biết vấn đề là gì, ta hãy nghiên cứu cách Ruby hiện thực hàm so sánh chuỗi của nó.
str_eql(const VALUE str1, const VALUE str2)
{
const long len = RSTRING_LEN(str1);
const char *ptr1, *ptr2;
if (len != RSTRING_LEN(str2)) return Qfalse;
if (!rb_str_comparable(str1, str2)) return Qfalse;
if ((ptr1 = RSTRING_PTR(str1)) == (ptr2 = RSTRING_PTR(str2)))
return Qtrue;
if (memcmp(ptr1, ptr2, len) == 0)
return Qtrue;
return Qfalse;
}
Cách hiện thực của Ruby khá đơn giản: đầu tiên nó kiểm tra độ dài của hai chuỗi là bằng nhau (1), sau đó thử so sánh hai con trỏ (2), rồi dùng
memcmp(3)
để kiểm tra hai string (3). Vấn đề nằm ở (3). memcmp(3)
, như khuyến cáo trên Linux man page, không nên được dùng để so sánh cryptographic data. Lý do memcmp(3)
là short-circuit, thời gian chạy của nó phụ thuộc vào hai con trỏ đầu vào, chính xác hơn là số byte đầu giống nhau giữa chúng. Như đoạn code so sánh aaa
và bbb
sẽ trả về nhanh hơn đoạn so sánh aaa
và aab
.
Đây là cách hiện thực hợp lý và thông minh. Nếu các byte đầu đã là không khớp, việc gì phải kiểm tra thêm các byte sau. Tuy nhiên khi dùng để so sánh hai chuỗi mật mã, cách hiện thực này có thể vô tình làm tiết lộ thông tin của chuỗi thông qua thời gian thực thi của nó.
Giả sử bạn có hai chuỗi bytes
1A 2B 3C 4D 5E 6F
và 1A 00 00 00 00 00
, nếu memcmp(3)
tốn 1ms để trả về, thì 1A 2B 3C 4D 5E 6F
và 1A 2B 3C 4D 5E 6F
sẽ tốn 6ms. Đương nhiên 11 11 11 11
và 00 00 00 00
sẽ trả về ngay lập tức.
Hacker có thể lợi dụng những thông số này để tiến hành mưu đồ của chúng. Đầu tiên chúng tạo ra và thử lần lượt những HMAC signature sau:
0000000000000000000000000000000000000000
0100000000000000000000000000000000000000
0200000000000000000000000000000000000000
... lược bỏ 250 cái ....
FD00000000000000000000000000000000000000
FE00000000000000000000000000000000000000
FF00000000000000000000000000000000000000
Sau đó chúng tìm được
A100000000000000000000000000000000000000
, signature này tốn lâu hơn một vài mi-li-giây để trả về so với những chuỗi khác. Suy ra chuỗi chính xác bắt đầu bằng A1
. Áp dụng thủ thuật tương tự cho 19 đoạn 00
còn lại, chúng có được signature mong muốn .
Để cho xã hội vững mạnh tiến lên 4.0, chúng ta phải làm cho hàm này chạy chậm 4.0 lần cần loại bỏ yếu tố short-circuit và làm cho thuật toán verify có constant-time. Tức là bắt nó chạy với thời gian cố định mà không phụ thuộc vào input.
def verify(message, signature)
secure_compare(hmac(payload), signature)
end
# https://github.com/plataformatec/devise/blob/94adec3ceef6a3e9eb6bdc4d3fa7ed4a3fc6b348/lib/devise.rb#L500-L508
def self.secure_compare(a, b)
return false if a.blank? || b.blank? || a.bytesize != b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
Username timing attack
Chắc bạn từng viết login form. Thông thường, khi user nhập sai thông tin login, chúng ta hay trả về những thông báo chung chung như "email or password is invalid", thay vì thông báo chính xác cái nào bị sai. Làm vậy để tránh để lộ dữ liệu nhạy cảm như email của người dùng :hugging_face:.
Tuy nhiên, với timing attack, hacker vẫn có thể đạt được ý đồ xấu xa đó. Ví dụ như đoạn code login sau đây:
def login(email, password)
user = User.find_by(email: email)
return false if !user
return encrypt_password(password) == user.encrypted_password
end
Bạn có để ý là: đoạn code trên cũng có short-circuit :face_with_head_bandage:. Cùng là password sai, nhưng login với một email tồn tại sẽ tốn thời gian nhiều hơn đáng kể (để mã hóa password) so với một email không tồn tại.
Những kẻ chống phá có thể lợi dụng lỗ hổng này để dò email. Nếu login bằng
a@quan-cam.com
tốn 135.34ms nhiều hơn so với b@quan-cam.com
và c@quan-cam.com
, chúng sẽ đoán ra a@quan-cam.com
là một email hợp lệ, còn thời gian dôi ra là hệ thống hash password. Cứ thử liên tục như thế, hắn sẽ có được tập email mong muốn từ hệ thống của chúng ta.
Người ta có nghĩ ra một vài cách để chống kiểu tấn công này. Ý tưởng không khác, đó là làm cho thuật toán chạy constant time. Bạn luôn luôn hash password dù cho username có hợp lệ hay không, hoặc cho CPU
sleep
một khoảng thời gian bằng với thuật toán hash. Nhờ đó, code của bạn sẽ chạy chậm một cách ổn định.def login(email, password)
user = User.find_by(email: email)
if user
return encrypt_password(password) == user.encrypted_password
else
encrypt_password("just-to-waste-some-time")
return false
end
end
Thật là cực chẳng đã đúng không bà con?
Cơ mà ... ông có dụ tui không?
Đọc tới đây có thể bạn sẽ nghĩ tui đang dụ bạn, vì cách tấn công này nghe có vẻ hơi bất khả thi. Có quá nhiều nhân tố có thể ảnh hưởng đến thời gian thực thi và trả về của một web: như network, sức mạnh của máy, cá mập, cách mạng, 4.0, 2.02.0 v.v. Với lại, sao mà hacker biết được code bạn viết thế nào?
Theo nghiên cứu Opportunities and Limits of Remote Timing Attacks, họ có thể nhận diện được độ khác biệt của remote timing trong mức dao động 20µs, và mạng LAN ở mức 100ns (thậm chí có thể nhỏ hơn) chỉ trên một tập vài trăm hoặc vài ngàn request.
Với kết quả từ nghiên cứu này, đoạn tấn công HMAC ở trên hoàn toàn có thể thực hiện được với khoảng 5,000,000 request (20 x 256 x 1000). Đó là con số có thể thực hiện chỉ trong vòng 3 ngày với tỉ lệ là 20 request/s.
Còn code bạn viết thế nào thì ... tui e rằng nó có đầy trên mạng ấy. Bạn có dùng thư viện mã nguồn mở mà đúng không ?
Bài viết này sẽ giúp tui tăng lương thế nào?
Bài viết này không những không giúp bạn tăng lương, mà còn khuyên bạn làm cho code của bạn chậm đi .
Túm cái váy là, tui mong bài viết này đã giới thiệu cho bạn thấy:
- khái niệm về timing attack: tấn công bằng cách phân tích thời gian thực thi của hệ thống.
- cách phòng chống là thiết kế thuật toán của bạn chạy constant time mà không phụ thuộc vào input.
- code C nhìn xấu như thế nào .