Note: Chia sẻ được lấy từ sự kiện DDoS – Tư duy phòng thủ
Part 1,2: DDoS Protection – From Inside View
Tham khảo: DDoS Protection – From Inside View (Day 1,2)
Part 3: DDOS Protection – From Inside View
NGÀY THỨ 3: THAY ĐỔI CHIẾN THUẬT
Mr. Tèo đã quay lại và phá hoại hơn xưa. Một lần nữa tôi phải vào lưới nhặt bóng và chấp nhận tỉ số 3-2 khi website không còn truy cập được nữa. Tuy nhiên, lần này thủ đoạn không giống hai lần trước.
SSH vào server kiểm tra, RAM và CPU cũng không bị quá tải nhiều, cũng không thấy nhiều process httpd chạy. Vậy tại sao website vẫn không truy cập được? Việc không có nhiều process web server chạy chứng tỏ không phải handle nhiều HTTP request. Tôi check access_log thì không ghi nhận truy cập vào website, error_log cũng im lặng như crush của tôi. Điểm đáng chú ý duy nhất là process [ksoftirqd] đang sử dụng 100% CPU.
Hệ điều hành Linux giao tiếp với các thiết bị phần cứng được gắn vào nó thông qua IRQ ( Interrupt Request – Yêu cầu ngắt). Khi các thiết bị có tác vụ cần được xử lý, chúng sẽ tạo ra một IRQ để báo hiệu cho hệ điều hành và ksoftirqd là tiến trình chịu trách nhiệm xử lý chúng. Việc ksoftirqd chiếm 100% cho thấy hệ thống đang gặp tình trạng interrupt storm, nghĩa là có quá nhiều interrupt cần được xử lý và hệ thống đang bị quá tải nặng. Phần lớn các trường hợp, tình trạng này xảy ra khi card mạng nhận được số lượng gói tin rất lớn trong thời gian ngắn.
Dùng lệnh “vnstat” để kiểm tra ngay số liệu network của card mạng, tôi nhận ra server đang phải nhận hàng triệu packet/s. Lúc này cần xác định các gói tin đang gửi đến server là thể loại gì, tôi phóng ngay lệnh tcpdump để capture 10,000 packet và lưu vào file để tiện phân tích:
tcpdump -i any -nn port 80 -c 10000 -w flood.pcap
Phân tích file pcap, gần như 100% gói tin là gói SYN và đến từ rất nhiều nguồn IP khác nhau, vậy là Mr.Tèo đang tấn công SYN Flood nhắm vào port 80 khiến web không thể truy cập được. Để khởi tạo một kết nối, Client và Server sẽ trải qua một quá trình gọi là Three Way Handshake (Bắt tay ba bước). Quá trình này bao gồm:
- Client: gửi SYN packet – Yêu cầu khởi tạo kết nối.
- Server: gửi lại SYN-ACK packet – Chấp nhận yêu cầu khởi tạo kết nối và yêu cầu xác nhận.
- Client: gửi ACK packet – xác nhận khởi tạo kết nối.
Khi hoàn tất 3 bước trên, kết nối sẽ được thiết lập vào Client có thể bắt đầu gửi dữ liệu cho server. Tuy nhiên, trong tấn công SYN Flood, client chỉ thực hiện bước thứ 1 là gửi SYN packet để yêu cầu khởi tạo kết nối nhưng lại không thực hiện bước thứ 3 để hoàn tất quá trình. Do đó, phía server sau khi gửi cờ SYN-ACK đã dành sẵn 1 slot “chờ sẵn” cho client nhưng đợi hoài không thấy hoàn tất kết nối. Việc liên tục nhồi hàng triệu SYN request mỗi giây khiến cho các slot của server bị các kết nối không hợp lệ chiếm giữ, và hết slot cho người dùng hợp lệ. Trường hợp có những slot hợp lệ được mở ra, hàng triệu kết nối tấn công nhanh chóng chiếm giữ lấy chúng, những người dùng hợp lệ ít có cơ hội kết nối thành công.
Cái khó khi chống tấn công SYN Flood là chúng là gói tin đầu tiên trong kết nối và không mang theo dữ liệu, server có rất ít lý do để từ chối chúng hoặc có ít cơ sở để phân biệt giữa gói SYN tấn công và gói SYN của người dùng hợp lệ. Tuy nhiên, phân tích kỹ hơn gói tin pcap đã capture được và thống kê danh sách IP, số lượng packet của chúng, tôi nhận thấy mỗi IP đang gửi SYN packet với tần số rất cao, lên đến hàng trăm packet/s. Điều này là bất thường vì người dùng hợp lệ sẽ không gửi SYN với tần số cao như thế. Tôi tiến hành sử dụng hashlimit để giới hạn tần số SYN packet trên mỗi IP:
iptables -I INPUT -p tcp --dport 80 --syn -m hashlimit --hashlimit-name SYN_FLOOD --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-above 100/s --hashlimit-burst 150 -j DROP
Rule hoạt động chính xác, số lượng SYN packet bị DROP tăng dần nhưng việc truy cập vào server vẫn còn hơi khó khăn vì 2 lý do:
- Không phải botnet nào cũng gửi SYN vượt quá rate đã limit, có rất nhiều botnet tấn công như rate thấp hơn rule quy định, thế nào chúng không bị bắt.
- Bản chất của HTTP application là bursty by default, nghĩa là khi truy cập, client hợp lệ cũng sẽ gửi dồn dập những request để tải nội dung trang. Điểm khác biệt là botnet mỗi giây sẽ đều đặn nhồi request vào server, còn người dùng hợp lệ thì chỉ nhồi request khi tải trang, sau khi tải xong thì không request nữa. Vậy nên, việc limit tần số SYN/s trên mỗi IP ít nhiều ảnh hưởng đến client thật. Có tỉ lệ các gói SYN của client thật bị drop bởi vượt quá limit và phải retry, dẫn đến tình trạng truy cập website không được mượt.
Việc dùng hashlimit phần nào giải quyết được vấn đề nhưng không max ping, tôi chuyển sang dùng module recent để tính toán và giới hạn số lượng SYN packet theo 5 giây gần nhất:
iptables -A INPUT -p tcp --syn --dport 80 -m recent --rcheck --seconds 5 --hitcount 400 --name SYN_FLOOD --mask 255.255.255.255 --rsource -j DROP
iptables -A INPUT -p tcp --syn --dport 80 -m recent --set --name SYN_FLOOD --mask 255.255.255.255 --rsource
Lần này thì bốc thuốc đúng bệnh hơn, server khoẻ khoắn trở lại và user hợp lệ truy cập vào server cũng mượt mà. Tỉ số một lần nữa được cân bằng 3-3, tuy nhiên trận đấu càng về sau càng khó khăn và cam go hơn!
Part 4: DDOS Protection – From Inside View
NGÀY THỨ TƯ: TẤN CÔNG SYN FLOOD FAKE SOURCE IP
Mr. Tèo nghĩ trong đầu: việc tấn công SYN Flood bằng botnet, nếu tần số trên mỗi IP thấp thì sẽ không đủ sức hạ gục server, còn nếu nhồi tần số cao thì sẽ không thoát khỏi rule limit của tôi. Vậy nếu có thể tạo được tấn công với tổng tần số cao nhưng tần số trên mỗi IP thấp thì vẫn sẽ qua mặt được Firewall ư?
Bất hạnh thay cho tôi, các kiểu tấn công ở Layer 4 cho phép Tèo giả mạo địa chỉ source IP (còn gọi là Fake source hoặc Spoofed source) trong gói tin gửi đến server. Với kỹ thuật này, Tèo có thể tạo ra packet với địa chỉ source IP bất kỳ mà Tèo muốn, mặc dù không trực tiếp sở hữu IP đó. Về lý thuyết, Tèo có tạo ra cuộc tấn công với 4 tỉ địa chỉ source IP khác nhau, và chỉ cần mỗi IP gửi một packet thì Firewall không thể nhận diện dựa vào tần số nữa rồi. Chưa kể, việc nhận diện IP botnet để đưa vào blacklist trong trường hợp này là vô nghĩa vì căn bản các IP đấy đều là giả mạo.
Website lại down, tôi lại lọ mọ SSH vào server kiểm tra. Dấu hiệu cũng như lần tấn công SYN Flood trước: [ksoftirqd] chiếm 100% CPU, không có nhiều process web server chạy, không có access_log cũng như error_log. Capture 100,000 packet về xem chi tiết, tôi phát hiện ra khá nhiều điều thú vị:
Có data rồi, bắt đầu đi bắt cướp thôi:
- [1]. Nhìn sơ bộ, gần như 100% gói tin server nhận được là gói SYN. Có thể kết luận đây là tấn công SYN Flood.
- [2],[3]. Phân tích Endpoint:, capture 100,000 packet nhưng có đến 50,000 địa chỉ source IP, mỗi IP chỉ gửi 1-2 packet. Khi xem đầy đủ danh sách, dễ dàng nhận ra số của các địa chỉ IP này tăng dần một cách tuần tự và đều đặn nhau. Không lẽ trùng hợp đến mức các máy có địa chỉ IP liên tục nhau lại cùng request về server? Chưa kể các IP này đến từ các quốc gia mà trước giờ tôi còn chưa nghe tên, không lẽ họ cũng đọc tiếng Việt
- [4]. Các gói tin có source IP từ khắp nơi trên thế giới, nhưng kỳ diệu thay chúng lại có cùng TTL. Đùa bố à con trai? TTL cho biết thông tin một packet từ khi xuất phát đến khi tới đích đã vượt qua bao nhiêu “trạm”. Giá trị của TTL phụ thuộc vào khoảng cách địa lý giữa client và server và sẽ khác nhau giữ các gói tin. Vậy nên việc các truy cập có source IP khắp nơi trên thế giới nhưng lại có cùng giá trị TTL thì đúng là giỡn mặt thiệt. Điều này cho thấy là các gói tin dù có source IP khác nhau nhưng lại được xuất phát từ cùng 1 nơi.
- [5]. Ngoài ra, để ý các TCP/IP options cũng sẽ buồn cười thấy nhiều điểm giống nhau và cố định theo một pattern như: Win=4096, Length=68 bytes. Nhất là việc SYN packet nhưng lại có ACK number là nonzero value. Không ai làm như này cả Tèo à.
Tổng hợp tất cả các thông tin trên có thể chắc chắn để kết luận các packet được “handmade” từ một tool nên mang các đặc điểm giống nhau, không đúng tiêu chuẩn và chạy trên cùng một nguồn tấn công. Tôi tiến hành viết rule cho iptables để nhận diện và DROP bỏ các gói tin thỏa các đặc điểm đã phân tích như sau:
- Sử dụng giao thức TCP
- Có cờ SYN được bật
- Destnation port: 80
- Length: 52 bytes (68 bytes trừ đi 16 bytes của Ethernet Header)
- TTL: 52
- Win=4096
- ACK flag: không bật
- ACK Number: khác 0
Do iptables không hỗ trợ match win value và ack number, tôi sử dụng kết hợp module u32 để thọc vào các offset của tcp header:
iptables -I INPUT -p tcp --dport 80 --tcp-flags SYN,ACK SYN -m ttl --ttl-eq 52 -m length --length 52 -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@8=0x01:0xFFFFFFFF && 0>>22&0x3C@12&0xFFFF=4096" -j DROP
Trong vòng vài giây, rule trên đã DROP hơn 21 triệu packet. Tỉ số một lần nữa được cân bằng 4-4. Hôm nay cũng là ngày cuối cùng dự thi rồi nên tối nay tôi và Mr. Tèo sẽ đá trái quyết định.
Tái bút: Hy vọng trong những ngày vừa qua tôi đã phần nào chia sẻ được những kiến thức có giá trị thực tế cho các bạn. Những tình huống trong bài đều là những trận đánh thực tế tôi đã trải qua và phải mất rất nhiều công sức mới có lời giải. Phần lớn những kỹ thuật tôi chia sẻ đều chưa được public trên Internet và thường được các cao nhân giữ làm bí kíp.
Part 5: DDOS Protection – From Inside View
NGÀY CUỐI CÙNG: VŨ KHÍ CHALLENGE & RESPONSE
Những ngày vừa qua hết sức căng thẳng cho cả đôi bên. Sau 4 ngày đôi công bất phân thắng bại, Mr. Tèo nhận thấy những đợt tấn công của mình bị vô hiệu hóa đều do để lại chữ ký (signature) để tôi nhận diện:
- Ngày 1: các request chứa Proxy Header.
- Ngày 2: Nhiều HTTP được nhét chung vào 1 TCP Packet.
- Ngày 3: SYN Flood bằng botnet với tần số trên mỗi IP cao.
- Ngày 4: SYN Flood fake source để lại nhiều dấu vết như TTL, ACK Number, Windows Size.
Lần này Mr. Tèo quyết định dồn hết công lực vào trận đánh cuối cùng, trận đánh quyết định toàn bộ cuộc chiến giữa tôi và hắn. Mr. Tèo tinh chỉnh lại botnet các request tấn công random tất cả mọi thông số một cách hợp lệ. Mục tiêu của Tèo là để giữa các request tấn công và request của client hợp lệ không chứa bất kỳ “signature” nào để phân biệt. Mr. Tèo đích thân đánh thử vào server của hắn và theo dõi kỹ payload của các request, hắn nghĩ thầm: “Bố mày code ra giờ nhìn vào còn không biệt được đâu là bot đâu là người thì mày làm gì có cửa”. Mr. Tèo hoàn toàn có lý do để tự tin với điều đó!
Về phần tôi, sau nhiều lần bị đánh úp, tôi quyết định chủ động xây dựng hệ thống phòng thủ để chờ địch:
- Giới hạn tần số truy cập trên mỗi IP.
- Giới hạn số lượng kết nối đồng thời (concurrent connection) trên mỗi IP.
- Apply hết toàn bộ chữ ký (known signature) của các cuộc tấn công DDoS mà trước kia tôi đã từng gặp vào Firewall.
- Setup hệ thống monitor & notify khi hệ thống bắt đầu sử dụng nhiều tài nguyên.
- Setup hệ thống monitor tấn công DDoS (DDoS Detection System) và cấu hình Trigger để khi phát hiện tấn công thì kích hoạt “vũ khí bí mật” của tôi lên.
Cốt lõi của việc chống tấn công DDoS là tìm ra cách phân biệt đâu là bot, đâu là người. Nếu dựa vào hành vi hoặc signature mà hệ thống vẫn không tự phân biệt được thì biện pháp cuối cùng là cho client chứng minh họ là người. Đây cũng chính là cách đang được sử dụng phổ biến nhất ở thời điểm hiện tại, chắc các bạn cũng không lạ gì với các thông báo kiểu như:
- Click vào đây để chứng tỏ bạn là người (I’m Human).
- Click vào đây để tiếp tục (click here to continue).
- Nhập captcha để tiếp tục (Enter captcha to continue).
- Hoặc phổ biến hơn là giải reCaptcha của Google (Xếp hình).
Cách này được gọi chung là kỹ thuật Challenge & Response. Server đưa ra một thử thách (Challange) và yêu cầu User đưa kết quả (Response). Nếu kết quả đúng thì cho truy cập, còn sai thì cho giải lại, nếu sai nhiều lần thì Say Goodbye. Và đây cũng là “vũ khí bí mật” tôi setup sẵn để chờ Mr. Tèo, một tính năng tương tự như “I’m Under Attack Mode” của CloudFlare mặc dù cùi hơn nhiều.
Tôi dùng Nginx làm web server nên sử dụng module “testcookie” để thiết lập “Challenge & Response”. Khi client truy cập vào website, testcookie sẽ đưa ra một đoạn Javascript được mã hóa và yêu cầu người dùng “giải toán” để lấy kết quả. Mỗi client khác nhau sẽ giải một bài toán khác nhau và nhận được kết quả khác nhau. Kết quả của phép toán sẽ nhét vào Cookie để gửi lên cho server kiểm tra. Nếu chính xác thì server sẽ cho truy cập vào web mà không Challenge nữa. Ngoài ra, đối với các bot tốt như Google, Facebook hoặc các range IP tin cậy thì có thể cho vào whitelist của testcookie để ko cần thực hiện Challenge.
Với những client hợp lệ sử dụng browser, việc xử lý javascript được diễn ra tự động và người dùng hòan toàn không nhận biết quá trình challenge này. Kết quả sau đó sẽ được Browser nhét vào Cookie trong các request gửi lên server, nghĩa là quá trình Challenge chỉ diễn ra một lần nên không ảnh hưởng gì đến trải nghiệm của người dùng. Ngược lại, với các botnet thường không phải là browser, chúng không được trang bị khả năng xử lý javascript và càng không thể tự động xử lý các javascript phức tạp đã bị mã hóa (encrypted).
Tôi cấu hình testcookie như sau:
testcookie on;
testcookie_name hello_teo;
testcookie_secret may_khong_thoat_duoc_dau_con_trai;
testcookie_session $remote_addr;
testcookie_arg hello_teo;
testcookie_max_attempts 3;
testcookie_get_only on;
testcookie_fallback /cookies.html?backurl=http://$host$request_uri;
testcookie_redirect_via_refresh on;
testcookie_refresh_encrypt_cookie on;
testcookie_refresh_encrypt_cookie_key e1ab7ca7a68790c56611a4a47b8200f0;
testcookie_refresh_encrypt_cookie_iv e1ab7ca7a68790c56611a4a47b8200f0;
testcookie_refresh_template '<html><body>setting cookie...<script type=\"text/javascript\" src=\"/aes.js\" ></script><script>function toNumbers(d){var e=[];d.replace(/(..)/g,function(d){e.push(parseInt(d,16))});return e}function toHex(){for(var d=[],d=1==arguments.length&&arguments[0].constructor==Array?arguments[0]:arguments,e="",f=0;f<d.length;f++)e+=(16>d[f]?"0":"")+d[f].toString(16);return e.toLowerCase()}var a=toNumbers("$testcookie_enc_key"),b=toNumbers("$testcookie_enc_iv"),c=toNumbers("$testcookie_enc_set");document.cookie="hello_teo="+toHex(slowAES.decrypt(c,2,a,b))+"; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/";location.href="$testcookie_nexturl";</script></body></html>';
testcookie_whitelist {
# facebook
31[.]13[.]97[.]0/24;
...
# Google
203[.]208[.]60[.]0/24;
...
}
Lúc này, truy cập vào website sẽ hiện lên đoạn challenge ta đã setup thay vì trả về nội dung website:
Đối với browser thì đoạn Javascript này được xử lý tự động và người dùng sẽ truy cập bình thường, không ảnh hưởng nhiều đến trải nghiệm. Còn đối với bot sẽ luôn bị kẹt ở đây. Cơ chế này mặc định tôi không bật lên và sẽ được trigger tự động khi máy chủ cho rằng đang bị tấn công DDoS. Ngoài ra, cơ chế challenge bằng reCaptcha cũng được cấu hình sẵn sàng lâm trận khi cần.
Hàng phòng thủ đã hoạt động tốt, khi tôi đang đi xem phim với crush thì nhận được notify hệ thống có dấu hiệu bị tấn công và testcookie đã được kích hoạt. Tôi truy cập vào monitor thấy resource có tăng một chút nhưng vẫn ổn định, website vẫn truy cập vẫn nuột như crush. Trong trận đánh quyết định, bên chủ động hơn đã giành chiến thắng!
Sau trận đánh đó, “tuần trăng mật” bên nhau của tôi và Mr. Tèo đã kết thúc. Hắn vừa là đối thủ, vừa là “partner” của tôi. Không thể phủ nhận nhờ va chạm với hắn mà kỹ năng của tôi được nâng lên rất nhiều.
Việc chống tấn công DDoS là một lĩnh vực phức tạp, những tình huống tôi chia sẻ trong seri vừa rồi chỉ là một phần rất nhỏ so với những gì đang diễn ra trong thực tế. Cách phòng chống DDoS tốt nhất vẫn là phải luôn chủ động xây dựng hệ thống phòng thủ, trang bị đầy đủ kiến thức, công cụ và kỹ năng. Để chống lại một cuộc tấn công “phân tán” (distributed) thì cách tốt nhất là hệ thống của chúng ta cũng phải phân tán. Hy vọng trong thời gian tới tôi sẽ có cơ hội chia sẻ với các bạn những kỹ thuật ở mức cao hơn như CDN, Auto Scaling, Anycast, ECMP, Load Balancing…
Trích: SinhVienIT, 7onez, Vietnix, Quantrilinux, Chongluadao