Đánh giá về lỗ hổng 0day (zero-day) của etherscan
"Bạn sẽ làm gì nếu tìm thấy 0 ngày trong etherscan? Tôi quyết định chế tạo một 'máy bắn bi'."——samczsun, Đối tác nghiên cứu mô hình
Lưu ý: Tác giả gốc là samczsun.
Tôi thích thách thức các giả định.
Tôi thích cố gắng làm những điều không thể, tìm thấy những gì người khác bỏ lỡ, làm họ ngạc nhiên với những điều họ chưa từng thấy. Năm ngoái, tôi đã viết một thử thách cho Paradigm CTF 2021 dựa trên một lỗ hổng rất phức tạp của Solidity. Mặc dù một biến thể đã được tiết lộ công khai, nhưng cách khai thác mà tôi tìm thấy chưa bao giờ thực sự được thảo luận. Kết quả là hầu như tất cả những ai cố gắng thách thức nó đều bị bối rối bởi bản chất dường như không thể của nó.
Chúng tôi đang thảo luận về các kế hoạch cho Mô hình CTF 2022 cách đây vài tuần thì Georgios đã tweet về một câu đố và tôi nghĩ sẽ rất tuyệt nếu đăng một thử thách vào cùng ngày với cuộc gọi khởi động. Tuy nhiên, đây không thể chỉ là một thử thách ngô nghê. Tôi muốn một thứ gì đó ngoài thế giới này, một thứ gì đó mà không ai khác có thể nhìn thấy, một thứ gì đó vượt ra ngoài giới hạn của những gì mọi người có thể tưởng tượng. Tôi muốn viết thử thách Ethereum CTF đầu tiên để khai thác lỗ hổng 0day (zero-day).
1
Cách tạo lỗ hổng 0day (zero-day)
Là nhà nghiên cứu bảo mật, để tối ưu hóa thời gian của mình, chúng tôi đưa ra một số giả định cơ bản. Một là mã nguồn mà chúng tôi đang đọc tạo ra hợp đồng mà chúng tôi đang phân tích. Tất nhiên, giả định này chỉ đúng nếu chúng ta đọc mã nguồn từ một nguồn đáng tin cậy, chẳng hạn như Etherscan. Vì vậy, nếu tôi có thể tìm ra cách để Etherscan xác minh sai thứ gì đó, tôi sẽ có thể nghĩ ra một câu đố thực sự quanh co về nó.
Để tìm hiểu cách sử dụng hệ thống xác minh hợp đồng của Etherscan, tôi đã phải xác minh một số hợp đồng. Tôi đã triển khai một số hợp đồng trên mạng thử nghiệm Ropsten để quăng và cố gắng xác thực chúng. Ngay sau đó, tôi thấy giao diện bên dưới.
Tôi đã chọn đúng cài đặt và chuyển sang trang tiếp theo. Ở đây tôi được yêu cầu cung cấp mã nguồn hợp đồng của mình.
Tôi đã nhập mã nguồn và nhấp vào nút xác minh, và chắc chắn rằng mã nguồn của tôi hiện đã được đính kèm với hợp đồng của tôi.
Bây giờ tôi đã biết mọi thứ hoạt động như thế nào, tôi có thể bắt đầu tìm hiểu về quy trình xác minh. Điều đầu tiên tôi thử là triển khai một hợp đồng mới, thay đổi foo thành bar và xác minh hợp đồng với mã nguồn ban đầu. Không có gì ngạc nhiên khi Etherscan từ chối xác minh hợp đồng của tôi.
Tuy nhiên, khi tôi so sánh hai đầu ra mã byte theo cách thủ công, tôi nhận thấy có điều gì đó lạ. Mã byte hợp đồng phải ở dạng hex, nhưng rõ ràng một số thì không.
Tôi biết rằng Solidity thêm siêu dữ liệu hợp đồng vào mã byte đã triển khai, nhưng tôi chưa bao giờ thực sự xem xét điều đó ảnh hưởng đến việc xác minh hợp đồng như thế nào. Rõ ràng, Etherscan đang quét mã byte để tìm siêu dữ liệu và thay thế nó bằng một điểm đánh dấu có nội dung: "Mọi thứ trong khu vực này đều có thể khác và chúng tôi vẫn nghĩ đó là mã byte giống nhau."
Đây có vẻ như là một đầu mối đầy hứa hẹn cho các khai thác 0day (zero-day) tiềm năng. Nếu tôi có thể đánh lừa Etherscan diễn giải phi siêu dữ liệu thành siêu dữ liệu, thì tôi sẽ có thể điều chỉnh mã byte mà tôi triển khai trong các vùng được đánh dấu {ipfs} trong khi vẫn xác minh mã đó là mã byte hợp pháp.
Cách dễ nhất mà tôi có thể nghĩ đến để bao gồm một số mã byte tùy ý trong giao dịch tạo là mã hóa chúng dưới dạng tham số hàm tạo. Solidity mã hóa các tham số của hàm tạo bằng cách đính kèm trực tiếp biểu mẫu được mã hóa ABI vào dữ liệu giao dịch tạo.
Tuy nhiên, Etherscan thông minh đến mức nó loại trừ các tham số hàm tạo khỏi bất kỳ loại đánh hơi siêu dữ liệu nào. Bạn có thể thấy rằng các tham số hàm tạo được in nghiêng để chỉ ra rằng chúng tách biệt với chính mã.
Điều này có nghĩa là tôi cần bằng cách nào đó lừa trình biên dịch Solidity phát ra một chuỗi byte mà tôi kiểm soát sao cho nó giống với siêu dữ liệu được nhúng. Tuy nhiên, đây có vẻ là một vấn đề khó giải quyết, vì tôi có rất ít quyền kiểm soát đối với opcode hoặc byte mà Solidity chọn sử dụng mà không gặp phải sự xáo trộn nghiêm trọng nào của trình biên dịch và mã nguồn sau đó trông rất đáng ngờ.
Tôi đã nghĩ về điều này một lúc, cho đến khi tôi nhận ra: thực sự khá dễ dàng để khiến Solidity phát ra (gần như) byte tùy ý. Đoạn mã sau sẽ khiến Solidity phát ra 32 byte 0xAA.
bytes32 value = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
Được khuyến khích, tôi nhanh chóng viết một hợp đồng nhỏ sẽ đẩy một loạt hằng số để Solidity phát ra mã byte trông giống hệt siêu dữ liệu được nhúng.
Tôi rất vui, Etherscan đã đánh dấu sự tồn tại của hàm băm IPFS ở giữa hợp đồng của tôi.
Tôi đã nhanh chóng sao chép mã byte dự kiến và thay thế hàm băm IPFS bằng một số byte ngẫu nhiên, sau đó triển khai hợp đồng kết quả. Chắc chắn rồi, Etherscan coi hoạt động kinh doanh byte khác như bình thường và cho phép xác minh hợp đồng của tôi.
Với hợp đồng này tại chỗ, mã nguồn gợi ý rằng việc gọi hàm example() sẽ trả về một đối tượng byte đơn giản. Tuy nhiên, đây là những gì xảy ra nếu bạn thực sự cố gắng gọi nó.
$ seth call 0x3cd2138cabfb03c8ed9687561a0ef5c9a153923f 'example()'
seth-rpc: {"id":1,"jsonrpc":"2.0","method":"eth_call","params":[{"data":"0x54353f2f","to":"0x3CD2138CAbfB03c8eD9687561a0ef5C9a153923f"},"latest"]}
seth-rpc: error: code -32000
seth-rpc: error: message stack underflow (5 <=> 16)
Tôi đã tìm thấy thành công lỗ hổng 0day (zero-day) trong Etherscan và giờ đây tôi có thể xác minh các hợp đồng hoạt động hoàn toàn khác so với mã nguồn gợi ý. Và bây giờ, tôi chỉ cần thiết kế một trò chơi giải đố xung quanh nó.
2
một khởi đầu sai lầm
Rõ ràng, trò chơi giải mã này sẽ xoay quanh ý tưởng rằng mã nguồn được thấy trên Etherscan không ám chỉ cách hợp đồng thực sự hoạt động. Tôi cũng muốn đảm bảo rằng người chơi không thể phát lại trực tiếp các giao dịch, vì vậy giải pháp phải là duy nhất cho mỗi địa chỉ. Cách tốt nhất rõ ràng là xin chữ ký.
Nhưng trong hoàn cảnh nào thì người chơi sẽ được yêu cầu ký một số dữ liệu? Thiết kế đầu tiên của tôi là một câu đố đơn giản với một chức năng được hiển thị duy nhất. Người chơi sẽ gọi chức năng với một số đầu vào, ký vào dữ liệu để chứng minh rằng họ đã đưa ra giải pháp và nếu đầu vào vượt qua tất cả các kiểm tra khác nhau, thì họ sẽ được đánh dấu là người giải quyết được. Tuy nhiên, khi tôi dành vài giờ tiếp theo để hoàn thiện thiết kế, tôi nhanh chóng cảm thấy không hài lòng với cách mọi thứ diễn ra, nó bắt đầu có cảm giác rất cồng kềnh và không trang nhã, và tôi không thể chịu được khi giải câu đố với thiết kế kém như vậy. một ý tưởng lỗ hổng 0day (zero-day) tuyệt vời như vậy.
Phải thừa nhận rằng tôi không thể hoàn thành việc này trước thứ Sáu, vì vậy tôi quyết định đi ngủ một giấc.
3
câu đố pinball
Tôi tiếp tục cố gắng lặp lại thiết kế ban đầu của mình vào cuối tuần, nhưng không tiến triển gì thêm. Cứ như thể cách tiếp cận hiện tại của tôi đã va vào tường, và dù tôi ghét phải thừa nhận điều đó, nhưng tôi biết mình có thể phải bắt đầu lại nếu muốn điều gì đó khiến tôi hài lòng.
Cuối cùng, tôi thấy mình đang xem xét lại vấn đề từ những nguyên tắc đầu tiên. Điều tôi muốn là một trò chơi giải đố trong đó người chơi phải hoàn thành nhiều bài kiểm tra kiến thức khác nhau. Tuy nhiên, tôi không khẳng định rằng bản thân việc hoàn thành bài kiểm tra kiến thức là điều kiện để chiến thắng, mà đúng hơn, đó có thể là một trong nhiều con đường mà người chơi được phép chọn. Có lẽ người chơi có thể tích lũy điểm trong suốt câu đố, khai thác lỗi này để nhận một số phần thưởng. Điều kiện để giành chiến thắng chỉ đơn giản là điểm số cao nhất, do đó gián tiếp khuyến khích việc sử dụng khai thác.
Tôi nhớ lại một thử thách mà tôi đã thiết kế vào năm ngoái, Lockbox, buộc người chơi phải xây dựng một khối dữ liệu duy nhất để đáp ứng các yêu cầu của sáu hợp đồng khác nhau. Các hợp đồng áp dụng các ràng buộc khác nhau cho cùng một byte, buộc người chơi phải khéo léo trong cách xây dựng tải trọng của mình. Tôi nhận ra rằng tôi muốn làm điều gì đó tương tự ở đây, nơi tôi yêu cầu người chơi gửi một đốm dữ liệu và tôi sẽ thưởng điểm dựa trên một số phần dữ liệu đáp ứng các yêu cầu nhất định.
Tại thời điểm này, tôi nhận ra rằng về cơ bản, tôi đang mô tả pinboooll, một thử thách mà tôi gặp phải trong vòng chung kết DEFCON CTF 2020. Mánh lới quảng cáo của pinboooll là khi bạn thực thi mã nhị phân, quá trình thực thi sẽ nảy xung quanh biểu đồ luồng điều khiển, giống như một quả bóng nảy trong máy bắn bi. Bằng cách sắp xếp đúng cấu trúc đầu vào của mình, bạn sẽ có thể tìm thấy các phần mã cụ thể và kiếm được điểm. Tất nhiên, có một lỗi, nhưng thành thật mà nói, tôi đã quên nó là gì và tôi sẽ không tìm kiếm nó. Ngoài ra, tôi đã có một lỗ hổng mà tôi muốn khai thác.
Vì tôi đang xử lý một khai thác zero-day đang hoạt động, nên tôi quyết định giải câu đố này càng nhanh càng tốt. Cuối cùng, tôi đã dành vài giờ để làm quen lại với cách thức hoạt động của pinboooll và vài ngày để triển khai lại nó. Điều này giải quyết vấn đề khung của câu đố, bây giờ tôi chỉ cần tích hợp khai thác.
4
Trang bị khai thác zero-day
Cách mà tôi đã giúp Solidity xuất ra các byte chính xác là chỉ cần tải một vài hằng số và để Solidity đưa ra các hướng dẫn PUSH tương ứng. Tuy nhiên, các hằng số tùy ý như thế này có thể là một lá cờ đỏ lớn và tôi muốn thứ gì đó kết hợp tốt hơn. Tôi cũng phải tải tất cả các hằng số trong một dòng, điều này khó giải thích bằng mã thực.
Vì tôi thực sự chỉ phải mã hóa cứng hai chuỗi byte ma thuật (0xa264...1220 và 0x6473...0033), nên tôi quyết định xem liệu tôi có thể kẹp mã giữa chúng thay vì hằng số thứ ba hay không. Trong hợp đồng đã triển khai, tôi chỉ cần thay thế mã kẹp bằng một số hướng dẫn khác.
address a = 0xa264...1220;
uint x = 1 + 1 + 1 + ... + 1;
address b = 0x6473...0033;
Sau một số thử nghiệm, tôi phát hiện ra rằng điều này là có thể, nhưng chỉ khi bật trình tối ưu hóa. Mặt khác, Solidity phát ra mã khử trùng giá trị cao. Điều này có thể chấp nhận được, vì vậy tôi chuyển sang cải thiện bản thân mã.
Tôi chỉ có thể sửa đổi mã trong hai địa chỉ, nhưng sẽ rất lạ nếu nhìn thấy một địa chỉ lơ lửng ở cuối, vì vậy tôi quyết định sử dụng chúng trong các câu điều kiện. Tôi cũng phải chứng minh sự cần thiết của điều kiện thứ hai, vì vậy cuối cùng tôi đã bỏ phiếu cho một điểm thưởng nhỏ. Tôi đã thực hiện kiểm tra có điều kiện đầu tiên để đảm bảo rằng tx.origin khớp với giá trị được mã hóa cứng để tạo ấn tượng ban đầu rằng không cần phải tiếp tục theo đuổi con đường mã này nữa.
if (tx.origin != 0x13378bd7CacfCAb2909Fa2646970667358221220) return true;
state.rand = 0x40;
state.location = 0x60;
if (msg.sender != 0x64736F6c6343A0FB380033c82951b4126BD95042) return true;
state.baseScore += 1500;
Bây giờ mã nguồn đã sẵn sàng, tôi phải viết backdoor thực sự. Cửa hậu của tôi cần xác minh rằng người chơi kích hoạt khai thác chính xác, thất bại mà không đưa ra bất kỳ lời nhắc nào nếu họ không và thưởng cho họ nếu họ kích hoạt thành công. Tôi muốn đảm bảo rằng việc khai thác không thể dễ dàng lặp lại, vì vậy tôi quyết định chỉ yêu cầu người chơi ký địa chỉ của chính họ và gửi chữ ký trong giao dịch. Để cải thiện mọi thứ, tôi quyết định yêu cầu chữ ký phải ở độ lệch 0x44 trong dữ liệu giao dịch, nơi quả bóng thường bắt đầu. Điều này sẽ yêu cầu người chơi hiểu cách mã hóa ABI hoạt động và di chuyển dữ liệu bóng theo cách thủ công sang nơi khác.
Nhưng ở đây tôi gặp phải một vấn đề lớn: Đơn giản là không thể khớp tất cả logic này vào 31 byte của hợp ngữ viết tay. May mắn thay, sau một hồi suy nghĩ, tôi nhận ra mình còn 31 byte khác để làm việc. Xét cho cùng, siêu dữ liệu được nhúng thực chứa hàm băm IPFS khác mà Etherscan cũng bỏ qua.
Sau khi chơi gôn mật mã, tôi đến một cửa hậu đang hoạt động. Ở lần băm IPFS đầu tiên, tôi ngay lập tức bật địa chỉ mà tôi vừa đẩy, sau đó chuyển sang lần băm IPFS thứ hai. Ở đó, tôi sẽ băm trình gọi và thiết lập một phần bộ nhớ/ngăn xếp để gọi ecrecover. Sau đó, tôi quay lại hàm băm IPFS đầu tiên, nơi tôi hoàn tất việc thiết lập ngăn xếp và thực hiện lệnh gọi. Cuối cùng, tôi đặt hệ số điểm bằng (msg.sender == ecrecover()) * 0x40 + 1, có nghĩa là không cần nhánh phụ.
Sau khi mã hóa cửa hậu đến một kích thước nhất định, tôi đã tweet địa chỉ Rinkeby của mình để lấy một số ETH testnet từ vòi, sau đó gợi ý cho bất kỳ ai xem tweet rằng có thể có điều gì đó đang xảy ra. Tiếp theo, tôi triển khai hợp đồng và xác minh nó.
Việc cần làm bây giờ chỉ là đợi người có duyên phát hiện ra cửa sau khuất trong tầm mắt.
Lưu ý: Kane Wallmann, một nhà phát triển phần mềm từ Rocket Pool, đã giải câu đố này vào thời điểm viết bài, đây là quy trình: https://medium.com/@kanewallmann_71759/an-untrustworthy-pinball-machine-d9dcd07882c
Bài viết này đến từ Tao of Yuan Universe, được sao chép với sự cho phép.
Bài viết này đến từ Tao of Yuan Universe, được sao chép với sự cho phép.


