Nếu bạn đã theo dõi thị trường tiền mã hóa đủ lâu, bạn có thể đã nghe nói về một hoặc hai cuộc tấn công hợp đồng thông minh. Các cuộc tấn công đã gây tổn thất hàng chục triệu USD. Cuộc tấn công đáng chú ý nhất vẫn là DAO, một trong những dự án được mong chờ nhất và là một minh chứng về khả năng cách mạng của các hợp đồng thông minh. Trong khi hầu hết mọi người đã nghe nói về các cuộc tấn công này, nhưng rất ít người thực sự hiểu những gì đã xảy ra và làm thế nào để tránh lặp lại sai lầm.
Hợp đồng thông minh năng động, phức tạp và cực kỳ mạnh mẽ. Mặc dù tiềm năng của chúng là không thể tưởng tượng được, nhưng dường như chúng sẽ không thể chống lại các cuộc tấn công chỉ trong một đêm. Tuy nhiên, vì tương lai của tiền mã hóa mà tất cả chúng ta cần phải học hỏi từ những sai lầm trước đây và cùng nhau phát triển. Mặc dù DAO đã là chuyện của quá khứ, nó vẫn là một ví dụ tuyệt vời của các cuộc tấn công hợp đồng thông minh mà các nhà phát triển, nhà đầu tư và các thành viên cộng đồng nên làm quen với chúng.
Trong Phần 1 của loạt bài viết về các cuộc tấn công hợp đồng thông minh, tôi sẽ chỉ ra cho bạn chi tiết (bao gồm cả bộ mã) về 3 cuộc tấn công thường thấy mà chúng ta có thể học từ DAO. Cho dù bạn là nhà phát triển, nhà đầu tư hay người hâm mộ tiền mật mã thì việc hiểu biết về những cuộc tấn công này sẽ trang bị cho bạn sự kiến thức và đánh giá sâu sắc hơn về công nghệ đầy hứa hẹn này.
Cuộc tấn công thứ nhất: Reentrancy
Một cuộc tấn công reentrancy xảy ra khi kẻ tấn công rút tiền từ mục tiêu bằng cách tạo ra vòng lặp đệ quy hàm rút tiền (withdraw) của mục tiêu, như trường hợp với DAO. Khi hợp đồng không cập nhật trạng thái của nó (số dư của người dùng) trước khi gửi tiền, kẻ tấn công có thể liên tục triển khai hàm withdraw để rút cạn tiền của hợp đồng. Bất cứ khi nào kẻ tấn công nhận được Ether, hợp đồng của kẻ tấn công sẽ tự động thực hiện hàm fallback được viết ra để thực hiện hàm withdraw một lần nữa. Tại thời điểm này cuộc tấn công đã bước vào một vòng lặp đệ quy và các khoản tiền của hợp đồng bắt đầu bị rút ra bởi kẻ tấn công. Hợp đồng mục tiêu không bao giờ có thể cập nhật số dư của kẻ tấn công. Hợp đồng mục tiêu bị lừa khi nghĩ rằng không có điều gì bất thường… Để rõ ràng, hàm fallback là hàm của hợp đồng được thực hiện tự động bất cứ khi nào hợp đồng nhận được Ether và dữ liệu trống.
Cuộc tấn công
- Kẻ tấn công gửi Ether cho hợp đồng mục tiêu.
- Hợp đồng mục tiêu cập nhật số dư của kẻ tấn công.
- Kẻ tấn công yêu cầu lấy tiền lại.
- Tiền được gửi lại.
- Hàm Fallback của kẻ tấn công kích hoạt và yêu cầu lệnh rút tiền tiếp theo.
- Hợp đồng thông minh chưa cập nhật số dư của kẻ tấn công, do đó lệnh rút tiền được gọi lại thành công.
- Tiền được gửi đến kẻ tấn công.
- Lặp lại các bước 5–7.
- Khi cuộc tấn công kết thúc, kẻ tấn công sẽ gửi tiền từ hợp đồng của họ đến địa chỉ cá nhân của họ.
Thật không may là không có cách nào để ngăn chặn cuộc tấn công khi nó đã bắt đầu. Hàm withdraw của kẻ tấn công sẽ được gọi lặp đi lặp lại cho đến khi hợp đồng hoặc số dư Ether của nạn nhân đã cạn kiệt.
Bộ mã
Dưới đây là một phiên bản đơn giản của hợp đồng DAO nhạy cảm, bao gồm các nhận xét để hiểu rõ hơn về hợp đồng cho những người không quen thuộc với lập trình/Solidity.
Nếu chúng ta xem xét hàm withdaw() chúng ta có thể thấy rằng hợp đồng của DAO sử dụng hàm address.call.value() để gửi tiền đến msg.sender. Không chỉ vậy, hợp đồng không cập nhật trạng thái tín dụng [msg.sender] sau khi tiền đã được gửi. Nhận biết các lỗ hổng này trong bộ mã hợp đồng, kẻ tấn công có thể sử dụng một hợp đồng như contractThisIsAHodlUp{} bên dưới để thanh lý tất cả các khoản tiền của contract babyDAO{}.
Lưu ý rằng hàm fallback, function(), gọi hàm withdraw của DAO hoặc contract babyDAO{}, để lấy cắp tiền từ hợp đồng. Mặt khác, hàm drainFunds () sẽ được gọi vào cuối cuộc tấn công khi kẻ tấn công muốn gửi tất cả Ether bị đánh cắp đến địa chỉ của chúng.
Giải pháp
Rõ ràng rằng các cuộc tấn công reentrancy tận dụng hai lỗ hổng cụ thể của hợp đồng thông minh. Thứ nhất là khi trạng thái của hợp đồng được cập nhật SAU KHI tiền đã được gửi và không cập nhật TRƯỚC ĐÓ. Bằng cách không cập nhật trạng thái hợp đồng trước khi gửi tiền, hàm có thể bị gián đoạn trong khí tính toán và hợp đồng sẽ bị lừa và nghĩ rằng số tiền chưa thực sự được gửi. Lỗ hổng thứ hai là khi hợp đồng sai khi sử dụng address.call.value() để gửi tiền, thay vì address.transfer() or address.send(). Cả hai hàm được giới hạn trong 2.300 gas, nên khi kẻ tấn công chuyển ETH bằng vòng lặp đệ quy sẽ bị cạn kiệt gas.
- Cập nhật số dư hợp đồng TRƯỚC KHI gửi tiền
- Sử dụng address.transfer () hoặc address.send () khi gửi tiền.
Cuộc tấn công số 2: Underflow
Mặc dù hợp đồng DAO không phải là nạn nhân của một cuộc tấn công underflow nhưng chúng ta có thể tận dụng babyDAO contract{} hiện tại để hiểu rõ hơn về cách mà tất cả các cuộc tấn công quá phổ biến này có thể xảy ra.
Máy ảo Ethereum được thiết kế để sử dụng 256 bit hoặc số bit được xử lý bởi CPU của máy tính trong một lần chạy. Vì EVM bị giới hạn ở kích thước 256 bit, phạm vi số được gán là 0 đến 4,294,967,295 (2²⁵⁶). Nếu chúng ta vượt quá phạm vi này, con số được đặt lại ở dưới cùng của phạm vi (2²⁵⁶ + 1 = 0). Nếu chúng ta xuống dưới phạm vi này, con số được thiết lập lại đến đầu cuối của phạm vi (0–1 = 2²⁵⁶).
Underflow xảy ra khi chúng ta trừ số 0 cho một số lớn hơn 0, dẫn đến việc một số nguyên mới được gán định là 2²⁵⁶. Bây giờ, nếu số dư của kẻ tấn công trải qua underflow, số dư sẽ được cập nhật, tất cả số tiền có thể bị đánh cắp.
Cuộc tấn công
- Kẻ tấn công bắt đầu tấn công bằng cách gửi 1 Wei cho hợp đồng đích
- Hợp đồng ghi nhận người gửi đã gửi tiền
- Một lệnh rút cùng 1 Wei đó được đưa ra
- Hợp đồng trừ 1 Wei từ tín dụng của người gửi, bây giờ số dư lại bằng 0
- Vì hợp đồng đích gửi Ether cho kẻ tấn công nên hàm Fallback của kẻ tấn công cũng kích hoạt và lệnh rút tiền được gọi lại
- Việc rút 1 Wei được ghi lại
- Số dư của hợp đồng của kẻ tấn công đã được cập nhật hai lần, lần đầu tiên là 0 và lần thứ hai là -1
- Số dư của kẻ tấn công được đặt lại thành 2²⁵⁶
- Kẻ tấn công hoàn thành cuộc tấn công bằng cách rút tất cả số tiền của hợp đồng được nhắm mục tiêu.
Bộ mã
Giải pháp
Để tránh trở thành nạn nhân của cuộc tấn công underflow, cách tốt nhất là kiểm tra xem số nguyên được cập nhật có nằm trong phạm vi byte của nó hay không. Chúng ta có thể thêm một kiểm tra tham số trong bộ mã của chúng ta để hoạt động như một tuyến phòng thủ cuối cùng. Dòng đầu tiên của function withdraw() kiểm tra các số tiền thích hợp, dòng thứ 2 kiểm tra overflow, và dòng thứ 3 kiểm tra underflow.
Lưu ý rằng bộ mã của chúng ta ở trên cũng cập nhật số dư của người dùng TRƯỚC KHI gửi tiền, như đã thảo luận trước đó.
Cuộc tấn công thứ 3: Cross-Function Race Condition
Cuối cùng là tấn công Cross-Function Race Condition. Như đã thảo luận trong cuộc tấn công Reentrancy, hợp đồng DAO không thể cập nhật chính xác trạng thái hợp đồng và cho phép tiền bị đánh cắp. Một phần của vấn đề với DAO và các tác động ngoại lai nói chung là nguy cơ cho một cuộc tấn công Cross-Function Race Condition xảy ra.
Mặc dù tất cả các giao dịch trong Ethereum chạy theo thứ tự (lần lượt từng giao dịch một) nhưng các tác động bên ngoài (một tác động đến một hợp đồng hoặc địa chỉ) có thể tạo ra thảm họa nếu không được quản lý đúng cách. Trong một thế giới hoàn hảo, chúng hoàn toàn tránh được. Một cuộc tấn công Cross-Function Race Condition xảy ra khi hai hàm được gọi và chia sẻ cùng một trạng thái. Hợp đồng bị lừa khi nghĩ rằng hai trạng thái hợp đồng tồn tại, trong thực tế chỉ có một trạng thái hợp đồng thực sự có thể tồn tại. Chúng ta không thể có X=3 và X=4 cùng một lúc.
Hãy làm rõ khái niệm này với một ví dụ.
Cuộc tấn công và bộ mã
Hợp đồng trên có hai hàm – một để chuyển tiền và một để rút tiền. Giả sử kẻ tấn công yêu cầu function transfer() trong khi đồng thời thực hiện yêu cầu function withdrawalBalance() bên ngoài . Trạng thái của userBalance[msg.sender] đang được kéo theo hai hướng khác nhau. Số dư của người dùng chưa được đặt là 0, nhưng kẻ tấn công cũng sẽ có thể chuyển các khoản tiền mặc dù thực tế chúng đã bị rút. Trong trường hợp này hợp đồng đã cho phép kẻ tấn công tấn công chi tiêu gấp đôi, một trong những vấn đề công nghệ blockchain được thiết kế để giải quyết.
Lưu ý: Cross-Function Race Condition có thể xảy ra trên nhiều hợp đồng nếu các hợp đồng đó chia sẻ trạng thái.
- Hoàn thành tất cả công việc nội bộ trước
- Tránh thực hiện các yêu cầu bên ngoài
- Đánh dấu các hàm yêu cầu bên ngoài là “không tin cậy” khi không thể tránh khỏi
- Sử dụng mutex khi không thể tránh khỏi yêu cầu bên ngoài
Với hợp đồng dưới đây, chúng ta có thể thấy ví dụ về hợp đồng (1) tiến hành công việc nội bộ trước khi thực hiện cuộc yêu cầu bên ngoài và (2) đánh dấu tất cả các hàm thực thi bên ngoài là “không đáng tin cậy”. Hợp đồng của chúng ta cho phép tiền được gửi đến địa chỉ và cho phép người dùng nhận phần thưởng một lần khi lần đầu nạp tiền vào hợp đồng.
Có thể thấy, hàm đầu tiên của hợp đồng thực hiện yêu cầu bên ngoài khi gửi tiền đến hợp đồng/địa chỉ của người dùng. Tương tự như vậy, hàm thưởng cũng sử dụng hàm rút tiền để gửi phần thưởng một lần và do đó không đáng tin cậy. Hợp đồng thực hiện tất cả các công việc nội bộ trước. Giống như ví dụ tấn công reentrancy của chúng ta, function untrustedGetReward() công nhận phần thưởng một lần của người dùng trước khi cho phép rút tiền để ngăn chặn cross-function race condition xảy ra.
Trong một thế giới hoàn hảo, các hợp đồng thông minh không cần phải phụ thuộc vào thực hiện các yêu cầu bên ngoài. Vì lý do đó, hãy sử dụng một mutex để “khóa” một số trạng thái và chỉ cấp cho chủ sở hữu khả năng thay đổi trạng thái có thể giúp tránh được một sự cố. Mặc dù các mutex là cực kỳ hiệu quả nhưng họ có thể trở nên phức tạp khi được sử dụng cho nhiều hợp đồng.
Ở trên chúng ta có thể thấy contract mutexExample() có trạng thái khóa riêng để thực thi function deposit() và function withdraw(). Khóa sẽ ngăn người dùng yêu cầu thành công withdraw() trước khi yêu cầu đầu tiên kết thúc, đồng thời ngăn chặn bất kỳ loại cross-function race condition nào xảy ra.
Lời kết
Sức mạnh càng lớn trách nhiệm càng cao. Mặc dù công nghệ blockchain và hợp đồng thông minh tiếp tục phát triển từng ngày nhưng vẫn còn rất nhiều sự rủi ro. Những kẻ tấn công sẽ không từ bỏ việc tìm kiếm cơ hội để tung ra các hợp đồng được thiết kế kém và bỏ trốn với tài sản của bạn. Chúng ta cần đảm bảo rằng chúng ta học hỏi được từ những thất bại của các đồng nghiệp của chúng ta, cũng như chính chúng ta, nếu chúng ta muốn phát triển và đẩy xa hơn ranh giới. Hy vọng rằng thông qua bài đăng này, và phần còn lại trong loạt bài viết của tôi, bạn sẽ cảm giác tự tin hơn với sự hiểu biết của bạn về các cuộc tấn công hợp đồng thông minh và các hợp đồng thông minh nói chung.
Theo: TapchiBitcoin.vn/hackernoon
Ethereum vẫn chưa tìm ra lỗi đã gây ra sự sụp đổ của DAO