Tác giả: Tiểu Bạch
Chỉnh sửa: Liz
Tổng quan bối cảnh
Trong hệ sinh thái Ethereum, cơ chế tạo địa chỉ hợp đồng có tính xác định đã mang lại sự thuận tiện cho các nhà phát triển, nhưng đồng thời cũng giới thiệu một mặt tấn công mới. Trong số này, chúng tôi sẽ phân tích các phương pháp tấn công và chiến lược phòng ngừa bằng cách sử dụng các mã thao tác CREATE và CREATE2 để triển khai các hợp đồng khác nhau vào cùng một địa chỉ tại các thời điểm khác nhau. Các bài viết trước về cơ bản kiểm toán an toàn hợp đồng thông minh có thể xem trong bộ sưu tập.
Kiến thức nền tảng
Chúng ta hãy hiểu hai quy tắc về tạo địa chỉ Ethereum:
1. CREATE
CREATE là mã thao tác gốc được sử dụng để triển khai động hợp đồng thông minh trong máy ảo Ethereum (EVM). Kể từ khối genesis của Ethereum, tất cả các hợp đồng triển khai đều phụ thuộc vào cơ chế này. Đặc điểm cốt lõi của nó là địa chỉ được tạo ra phụ thuộc vào nonce của tài khoản người triển khai, do đó địa chỉ là không xác định (không thể dự đoán chính xác trước khi triển khai).
Địa chỉ hợp đồng được tạo ra bởi CREATE phụ thuộc vào địa chỉ của người triển khai và nonce của địa chỉ đó:

2. CREATE2
CREATE2 là mã thao tác tạo hợp đồng mới được Ethereum giới thiệu trong đợt hard fork Constantinople (tháng 2 năm 2019). Khác với CREATE truyền thống, trước khi triển khai hợp đồng, các bên tham gia ngoại tuyến có thể tính toán trước địa chỉ hợp đồng, điều này làm cho tương tác ngoại tuyến (chẳng hạn như kênh trạng thái) và kiến trúc hợp đồng phức tạp trở nên khả thi.
Địa chỉ hợp đồng được tạo ra bởi CREATE2 phụ thuộc vào bốn tham số sau:

Đến đây, tôi tin rằng mọi người đã hiểu hai cách tạo địa chỉ hợp đồng trong Ethereum, độc giả cẩn thận có thể nghĩ rằng nếu địa chỉ hợp đồng đã được tính toán mà đã tồn tại thì sẽ thế nào? Điều này không cần lo lắng, trong Ethereum, bất kể thông qua CREATE hay CREATE2 để tạo ra địa chỉ, miễn là đã tồn tại trên chuỗi (dù là tài khoản bên ngoài EOA hay tài khoản hợp đồng), EVM sẽ từ chối yêu cầu tạo hợp đồng. Dưới đây là hai tình huống xung đột địa chỉ:
1. Địa chỉ mục tiêu là tài khoản bên ngoài (EOA)
Quy tắc: Nếu địa chỉ mục tiêu là một EOA đã tồn tại (ví dụ, địa chỉ ví của người dùng), EVM sẽ từ chối yêu cầu triển khai hợp đồng.
Kết quả: Giao dịch thất bại, Gas bị tiêu tốn, hợp đồng sẽ không được tạo ra và không ghi đè dữ liệu nào của địa chỉ đó.
2. Địa chỉ mục tiêu là tài khoản hợp đồng
Quy tắc: Nếu địa chỉ mục tiêu là một tài khoản hợp đồng đã triển khai, EVM cũng sẽ từ chối yêu cầu triển khai.
Kết quả: Giao dịch thất bại, Gas bị tiêu tốn, mã và dữ liệu lưu trữ của hợp đồng gốc không thay đổi.
Tất nhiên, mọi thứ đều có ngoại lệ, nếu địa chỉ mục tiêu là một hợp đồng và đã tự hủy bằng selfdestruct, lúc này có thể triển khai hợp đồng mới tại địa chỉ đó.
Đến đây, các đặc điểm khác nhau của hai mã thao tác CREATE và CREATE2 đã được giới thiệu xong, tiếp theo chúng ta hãy xem cách tận dụng các đặc điểm của hai mã thao tác này để thực hiện một loạt các đòn tấn công hợp đồng.
Ví dụ về lỗ hổng

Phân tích lỗ hổng
Hợp đồng DAO này thực hiện một cơ chế quản lý cơ bản: Chủ sở hữu thông qua hàm approve để xem xét và ghi lại địa chỉ hợp đồng đề xuất vào mảng Proposals, bất kỳ người dùng nào sau đó có thể thực hiện hàm execute để thực hiện đề xuất đã được phê duyệt. Quyền kiểm soát dường như nghiêm ngặt (chỉ Chủ sở hữu có thể xem xét đề xuất) kết hợp với kiểm tra thực hiện (đề xuất cần được phê duyệt và chưa thực hiện), nhưng thực sự có một lỗ hổng logic tiềm ẩn: địa chỉ đề xuất đã phê duyệt có thể trỏ đến mã hợp đồng hoàn toàn khác khi thực hiện. Kẻ tấn công có thể thực hiện tấn công theo ba bước như sau:
1. Triển khai hợp đồng bình thường, lấy quyền hạn
Kẻ tấn công đầu tiên triển khai một hợp đồng A chứa hàm executeProposal() vô hại, thông qua Chủ sở hữu phê duyệt để thêm địa chỉ vào danh sách đề xuất.
2. Tự hủy hợp đồng gốc, chiếm dụng địa chỉ
Hợp đồng A thực hiện thao tác tự hủy (selfdestruct) xóa sạch mã, sau đó kẻ tấn công sử dụng mã thao tác CREATE2 để triển khai hợp đồng độc hại B (chứa logic nguy hiểm của executeProposal()) tại cùng một địa chỉ.
3. Kích hoạt thực hiện, chiếm quyền kiểm soát
Khi người dùng gọi execute, hợp đồng sẽ thực thi mã của hợp đồng độc hại B mới được triển khai thông qua delegatecall. Do delegatecall sẽ giữ nguyên ngữ cảnh của hợp đồng hiện tại, kẻ tấn công có thể thông qua thao tác này thay đổi trạng thái hợp đồng DAO (ví dụ như thay đổi Owner) hoặc chuyển nhượng tài sản.
Tiếp theo, chúng ta sẽ xem quy trình tấn công cụ thể kết hợp với hợp đồng tấn công.
Hợp đồng tấn công

Quy trình tấn công như sau:
1. Alice triển khai hợp đồng DAO.
2. Evil triển khai hợp đồng DeployerDeployer, địa chỉ là DD.
3. Evil gọi DD.deploy(), sử dụng CREATE2 để triển khai Deployer đến địa chỉ D (salt cố định).
4. Gọi D.deployProposal(), tạo địa chỉ hợp đồng Proposal P, lúc này nonce của D là 0.
5. Alice phê duyệt địa chỉ P.
6. Kẻ tấn công gọi D.kill(), hủy bỏ D, lúc này tài khoản của địa chỉ D đã bị xóa, bao gồm cả nonce cũng được đặt lại thành 0.
7. Kẻ tấn công lại gọi DD.deploy(), triển khai lại Deployer đến địa chỉ D (các tham số CREATE2 tương tự).
8. Gọi D.deployAttack(), vì lúc này nonce của D đã được đặt lại thành 0 và địa chỉ của D không thay đổi, nên địa chỉ hợp đồng Attack được tạo ra giống với địa chỉ hợp đồng Proposal trước đó P.
9. Lúc này, trong mảng proposals của DAO có một đề xuất trỏ đến địa chỉ P, nhưng hiện tại địa chỉ P đã được triển khai lại thành hợp đồng Attack. Do đó, khi gọi execute(), sẽ thực thi executeProposal() của Attack, cuối cùng thông qua delegatecall thay đổi Owner của DAO thành msg.sender, tức là hợp đồng Attack.
Nguyên lý tấn công có thể được tóm tắt là kẻ tấn công tận dụng đặc điểm của CREATE2 có thể triển khai lại hợp đồng vào cùng một địa chỉ, trước tiên triển khai một hợp đồng Proposal hợp pháp, để DAO phê duyệt. Sau đó, hủy hợp đồng Deployer, triển khai lại Deployer ở cùng một địa chỉ, đặt lại nonce của địa chỉ đó, cuối cùng thành công triển khai hợp đồng độc hại vào cùng một địa chỉ với hợp đồng Proposal trước đó. Do địa chỉ đề xuất được DAO lưu trữ hiện trỏ đến hợp đồng độc hại, khi thực hiện đề xuất, mã độc sẽ được thực thi trong ngữ cảnh của DAO, thành công thay đổi Owner thành kẻ tấn công.
Đề xuất sửa chữa
Với tư cách là nhà phát triển:
Khi phê duyệt đề xuất, không chỉ ghi lại địa chỉ mà còn ghi lại mã băm của địa chỉ đó và xác minh tính nhất quán của mã băm khi thực hiện.
Tránh sử dụng delegatecall để gọi hợp đồng bên ngoài, trừ khi có các biện pháp an toàn đầy đủ.
Cân nhắc khả năng hợp đồng tự hủy sau đó triển khai lại vào cùng một địa chỉ, khi thực hiện gọi bên ngoài, cần kiểm tra xem hợp đồng bên ngoài có đáng tin cậy hay không, nếu cần gọi hợp đồng bên ngoài lạ, khuyến nghị kiểm tra xem trong hợp đồng có cơ chế chống tự hủy hay không.
Với tư cách là người kiểm toán:
Xác định xem có tồn tại chức năng tự hủy selfdestruct trong hợp đồng được gọi từ bên ngoài hay không, nếu có, cần cảnh giác với nguy cơ tấn công khi hợp đồng bên ngoài tự hủy và triển khai mã độc vào cùng một địa chỉ.
Kiểm tra xem phương thức triển khai hợp đồng có sử dụng CREATE2 không, đảm bảo rằng giá trị salt được sử dụng để tạo địa chỉ hợp đồng đủ ngẫu nhiên, ngăn chặn việc địa chỉ hợp đồng bị dự đoán trước dẫn đến việc kẻ tấn công chiếm dụng địa chỉ hợp đồng.
Kiểm tra xem DAO có xác minh tính nhất quán mã của địa chỉ mục tiêu trong quá trình thực hiện đề xuất hay không, chẳng hạn như so sánh mã băm hiện tại với mã băm khi được phê duyệt.
Phân tích cẩn thận các trường hợp sử dụng delegatecall, xác nhận xem địa chỉ mục tiêu có đáng tin cậy hay không, có tồn tại nguy cơ gọi từ bất kỳ địa chỉ nào hay không.
Kiểm tra quản lý vòng đời của các đề xuất, chẳng hạn như có cơ chế nào ngăn chặn đề xuất bị sửa đổi hoặc thay thế hay không, ví dụ như khóa trạng thái địa chỉ mục tiêu sau khi phê duyệt.
