Lỗ hổng trong vấn đề sinh số ngẫu nhiên, một ví dụ từ dự án MechMaster
Tags: random, exploit, bsc, smartcontract, audit, security audit
Credit: lifebow
Verichains đã liên hệ với dự án MechMaster trước khi bài viết được công bố. Đội ngũ phát triển MechMaster đã biết và có kế hoạch giải quyết các vấn đề được đề cập trong bài viết.
Bài viết nằm trong chuỗi bài viết khảo sát độ an toàn của các dự án Blockchain của nhóm nghiên cứu bảo mật tại Verichains Labs. Nếu bạn có nhu cầu trao đổi thêm hoặc audit dự án, xin email vào địa chỉ info@verichains.io.
Tóm tắt bài viết
Contract thực hiện chức năng
draw MechMaster sử dụng một đoạn logic random với các thông số không đủ mạnh, nên mình đã có thể khai thác và kiểm soát được kết quả tạo ra các item trong game có độ hiếm cao.
Phạm vi bài viết
Mã nguồn của proxy contract mà website trỏ tới đã được verified trên bscscan: https://bscscan.com/address/0xe35f67aec4f633c01130fdc9f18286a4215c3e5f#code
Contract chứa logic vận hành của proxy contract (cho tới thời điểm bài này được viết):
https://bscscan.com/address/0x37281cf9d0eda5059f41a62e969757f55c62bc1f
Website market của Mech master:
https://market.mechmaster.io/#/marketplace
Một số thông tin liên quan về game và contract khai thác
ERC1155 là contract chứa logic stake $MECH token và mint token ERC1155 trong game MechMaster. Người chơi sẽ phải stake một lượng $MECH token để có thể tham gia quay “xổ số” và nhận được các mảnh item dưới dạng token ERC1155.
Mỗi lượt quay có kết quả là ngẫu nhiên và có tỉ lệ rarity phân bố như hình trên.
Quá trình phân tích và khai thác
Phân tích
Nhìn sơ qua website của game mình thấy hàm draw khá là hay ho nên mình tìm tòi xung quanh frontend của web. Webpack enabled nên mình có thể đọc được phần xử lý khi
draw như hình bên dưới.
Phương thức draw chỉ nhận vào 2 giá trị của người dùng là now (thời điểm hiện tại) và count (số lần draw). Có vẻ như user có thể điều khiển được 2 tham số này vì hàm không có truyền signature từ server.
Debug một chút để lấy được abi và địa chỉ contract tương tác.
Địa chỉ contract thu được:
https://bscscan.com/address/0xe35f67aec4f633c01130fdc9f18286a4215c3e5f
Contract mà game tương tác là một proxy contract. Proxy contract là một contract không chứa trực tiếp logic vận hành mà nó sẽ gọi qua một contract khác có địa chỉ được lưu bên trong _IMPLEMENTATION_SLOT của proxy contract để lấy logic.
Để tìm ra địa chỉ đứng phía sau proxy contract có 2 cách:
Đọc giá trị storage của contract tại địa chỉ mà _IMPLEMENTATION_SLOT đang trỏ tới.
Quan sát các transaction của owner contract và tìm transaction mới nhất update address implementation của proxy contract.
Tx update địa chỉ implementation của proxy contract.
https://bscscan.com/tx/0x3bbf4583a54ed46fb41db58fc97fecfcf4d1193b9a50b7ccac5c5369d5bb0000
Bằng cả hai cách ở trên, kết quả mình nhận được đều là địa chỉ contract: 0x37281cf9d0eda5059f41a62e969757f55c62bc1f
https://bscscan.com/address/0x37281cf9d0eda5059f41a62e969757f55c62bc1f
Tuy nhiên contract này chưa được verify nên chúng ta không có một source code tường minh để đào sâu vào hàm draw.
Với Bytecode public, mình thử sử dụng chức năng decompile bytecode có sẵn ngay trên bscscan.
Hầu hết các function trong contract đều được decompile và dễ dàng đọc được. Nhưng mục tiêu chính là hàm draw
thì gặp phải một lỗi.
Chức năng này trên bscscan cũng không hoạt động hiệu quả 100%
Bằng một số kỹ năng dịch ngược, mình đã decompile bytecode này trên máy cá nhân và thu được kết quả như bên dưới.
Một phần mã nguồn hàm draw sau khi decompile.
Một phần đáng chú ý của hàm draw khi được decompile có sử dụng hash của block.difficulty và các input đầu vào để tính toán. Phân bổ các nhánh tương ứng với tỉ lệ cách mảnh item trong document nên ắt hẳn đây là logic của phần random.
Phân tích kỹ hơn về các đối số sử dụng với hàm hash sha3:
block.difficulty: độ khó của block mặc định trên Binance Smart Chain là 2.
_blockNumber: đối số thứ nhất của hàm draw là tương ứng với now - là giá trị user có thể kiểm soát được.
caller: người gửi transaction là một địa chỉ ví mà ta sở hữu.
stor256: giá trị storage của contract tại slot 256, có thể đọc một cách dễ dàng.
Các đối số sử dụng đối với hàm sha3 chúng ta đều có thể kiểm soát hoặc đọc được nên việc điều chỉnh tham số _blockNumber để nhảy vào đúng nhánh if-else là hoàn toàn khả thi.
Khai thác
Mình sử dụng ganache
để fork mainnet ở block.number
mới nhất của bsc mainnet.
Trong chain dưới local này mình sẽ gọi transaction với một giá trị _blockNumber
tới contract, kết quả trả về dưới local sẽ tương ứng nếu dùng giá trị đó trên mainnet. Bằng việc thử đi thử lại nhiều lần với các _blockNumber
khác nhau, ta sẽ có được giá trị _blockNumber
cho ra kết quả của hàm draw
mà ta muốn.
Dưới đây là 3 transacsions liên tục có kết quả draw
được item có rarity Legendary
- TOKEN ID[1]
mà mình đã thực hiện trên mainnet:
https://bscscan.com/tx/0x7dc44ff24ac46250547be458261699a8bb2854e2e189c557985fef4394ecf7f2
https://bscscan.com/tx/0xac972b895e72331a4ba79ba63140f666ea18338383ae15d2d3c3832d1f4019a6
https://bscscan.com/tx/0xf26bc9f07e9e2052da502fda20b55ce1019bb069a9695da3272bbc8cf042535f
Kết quả thu được sau quá trình khai thác
Đôi lời nhận xét về lỗ hổng
Như đã đề cập ở phần phân tích phía trên, hàm draw sử dụng block.difficulty và một số tham số khác làm đầu vào của sha3. Có lẽ developer nhầm lẫn block.difficulty là một thông số thay đổi liên tục như trên Ethereum
, tiếc thay giá trị này là một giá trị cố định trên Binanace smart chain
. Dẫu vậy cho dù giá trị này có thay đổi liên tục thì chúng ta vẫn có một cách thức khác để khai thác (nếu có cơ hội mình sẽ chia sẻ trong các blog khác).
Việc nhầm lẫn sử dụng block.difficulty trong logic random trên BSC là một lỗi cũ, có lẽ do kích thước hàm draw quá lớn dẫn tới việc khó khăn trong việc dịch ngược nên contract chưa bị tấn công ồ ạt.
Kết luận
Những lỗi trên sẽ gây ảnh hưởng rất lớn đến dự án và mình mong các dự án sẽ quan tâm và đầu tư hơn về các vấn đề bảo mật của cả ở phần offchain hay trên blockchain. Rất nhiều dự án đã phải chết yểu hay bị tấn công gây ra rất nhiều thiệt hại nghiêm trọng cho cả nhà đầu tư và chủ dự án. Audit mã nguồn sẽ là một phương pháp để phát hiện sớm các lỗi, tuy nhiên việc audit cũng chưa chắc đã phát hiện hết lỗi trong hệ thống. Để đảm bảo hơn dự án nên được audit bởi các công ty uy tín và sẽ càng tốt nếu được audit nhiều lần bởi nhiều đội ngũ hoạt động độc lập với nhau.