BTC
ETH
HTX
SOL
BNB
시장 동향 보기
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt

etherscan의 제로데이(zero-day) 취약점 리뷰

星球君的朋友们
Odaily资深作者
이 기사는 약 4717자로, 전체를 읽는 데 약 7분이 소요됩니다
etherscan에서 0day(제로데이) 취약점을 발견하면 어떻게 하시겠습니까?
AI 요약
펼치기
etherscan에서 0day(제로데이) 취약점을 발견하면 어떻게 하시겠습니까?

"이더스캔에서 제로데이를 찾으면 어떻게 하시겠습니까? '핀볼 머신'을 만들기로 했습니다."——samczsun, 패러다임 연구 파트너

참고: 원저자는 samczsun입니다.

나는 가정에 도전하는 것을 좋아합니다.

나는 불가능한 것을 시도하고, 다른 사람들이 놓친 것을 찾고, 그들이 본 적이 없는 것으로 그들을 놀라게 하는 것을 좋아합니다. 작년에 저는 매우 복잡한 Solidity 취약점을 기반으로 Paradigm CTF 2021에 대한 챌린지를 작성했습니다. 변종이 공개적으로 공개되었지만 제가 발견한 익스플로잇은 실제로 논의된 적이 없습니다. 그 결과, 그것에 도전하려고 시도한 거의 모든 사람들은 겉보기에 불가능해 보이는 그 특성에 당황했습니다.

우리는 몇 주 전에 Paradigm CTF 2022에 대한 계획을 논의하고 있었는데 Georgios가 퍼즐에 대해 트윗을 했을 때 킥오프 콜과 같은 날에 챌린지를 게시하는 것이 정말 멋질 것이라고 생각했습니다. 그러나 이것은 진부한 도전이 될 수 없습니다. 나는 이 세상에서 다른 누구도 볼 수 없는 것, 사람들이 상상할 수 있는 한계를 넘어서는 것을 원합니다. 제로데이(zero-day) 취약점을 악용하는 첫 번째 이더리움 CTF 챌린지를 작성하고 싶습니다.

1

0day(제로데이) 취약점 생성 방법

보안 연구원으로서 우리는 시간을 최적화하기 위해 몇 가지 기본적인 가정을 합니다. 하나는 우리가 읽고 있는 소스 코드가 우리가 분석하고 있는 계약을 생성한다는 것입니다. 물론 이 가정은 Etherscan과 같은 신뢰할 수 있는 소스에서 소스 코드를 읽는 경우에만 적용됩니다. 따라서 Etherscan이 무언가를 잘못 확인하도록 할 수 있는 방법을 찾을 수 있다면 이를 둘러싼 정말 교활한 퍼즐을 고안할 수 있을 것입니다.

Etherscan의 계약 검증 시스템을 활용하는 방법을 알아보기 위해 일부 계약을 검증해야 했습니다. 나는 Ropsten 테스트넷에 몇 가지 계약을 배포하여 이를 검증하려고 했습니다. 곧 아래의 인터페이스를 보았습니다.

올바른 설정을 선택하고 다음 페이지로 진행했습니다. 여기에서 계약 소스 코드를 제공하라는 요청을 받았습니다.

소스 코드를 입력하고 확인 버튼을 눌렀더니 이제 제 소스 코드가 계약서에 첨부되었습니다.

이제 작동 방식을 알았으므로 확인 프로세스를 시작할 수 있습니다. 가장 먼저 시도한 것은 신규 컨트랙트를 배포하고, foo를 bar로 변경하고, 원본 소스코드로 컨트랙트를 검증하는 것이었습니다. 당연히 Etherscan은 내 계약 확인을 거부했습니다.

그러나 두 바이트 코드 출력을 수동으로 비교할 때 이상한 점을 발견했습니다. 계약 바이트코드는 16진수여야 하지만 일부는 그렇지 않은 것 같습니다.

Solidity가 배포된 바이트코드에 계약 메타데이터를 추가한다는 것을 알고 있지만 이것이 계약 검증에 어떤 영향을 미치는지 실제로 고려한 적이 없습니다. 분명히 Etherscan은 메타 데이터에 대한 바이트 코드를 스캔하고 "이 영역의 모든 것은 다를 수 있으며 여전히 동일한 바이트 코드라고 생각할 것입니다."라는 마커로 대체합니다.

이것은 잠재적인 0day(제로데이) 익스플로잇에 대한 유망한 단서처럼 보입니다. Etherscan을 속여 메타데이터가 아닌 데이터를 메타데이터로 해석할 수 있다면 {ipfs}로 표시된 영역에 배포하는 바이트코드를 조정할 수 있으며 여전히 합법적인 바이트코드로 확인됩니다.

생성 트랜잭션에 임의의 바이트코드를 포함하는 가장 쉬운 방법은 생성자 매개변수로 인코딩하는 것입니다. Solidity는 ABI로 인코딩된 양식을 생성 트랜잭션 데이터에 직접 첨부하여 생성자 매개변수를 인코딩합니다.

그러나 Etherscan은 매우 영리해서 모든 종류의 메타데이터 스니핑에서 생성자 매개변수를 제외합니다. 생성자 매개변수가 코드 자체와 분리되어 있음을 나타내기 위해 기울임꼴로 표시되어 있음을 알 수 있습니다.

즉, 포함된 메타데이터와 유사하도록 내가 제어하는 ​​일련의 바이트를 내보내도록 Solidity 컴파일러를 어떻게든 속여야 합니다. 그러나 Solidity가 심각한 컴파일러 논쟁 없이 사용하기로 선택한 opcode 또는 바이트에 대한 제어 권한이 거의 없고 나중에 소스 코드가 매우 의심스러워 보이기 때문에 이것은 파악하기 어려운 문제인 것 같습니다.

나는 이것에 대해 잠시 생각하다가 깨달았습니다. 실제로 Solidity가 (거의) 임의의 바이트를 내보내도록 하는 것은 꽤 쉽습니다. 다음 코드는 Solidity가 32바이트의 0xAA를 내보내도록 합니다.

bytes32 value = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;

용기를 얻어 저는 Solidity가 포함된 메타데이터와 똑같이 보이는 바이트코드를 내보낼 수 있도록 일련의 상수를 푸시하는 작은 계약을 신속하게 작성했습니다.

기쁘게도 Etherscan은 계약 중간에 IPFS 해시의 존재를 표시했습니다.

예상되는 바이트코드를 빠르게 복사하고 IPFS 해시를 임의의 바이트로 교체한 다음 결과 계약을 배포했습니다. 아니나 다를까, Etherscan은 평소와 같이 다른 바이트 비즈니스를 고려하고 내 계약을 검증할 수 있도록 허용했습니다.

이 계약이 적용되면 소스 코드는 example() 호출이 간단한 바이트열 개체를 반환해야 한다고 제안합니다. 그러나 실제로 호출하려고 하면 이렇게 됩니다.

$ 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)

Etherscan에서 0day(제로데이) 취약점을 성공적으로 발견했으며 이제 소스 코드가 제안하는 것과 완전히 다르게 작동하는 계약을 확인할 수 있습니다. 이제 퍼즐 게임을 디자인하기만 하면 됩니다.

2

잘못된 출발

분명히 이 암호 해독 게임은 Etherscan에서 볼 수 있는 소스 코드가 계약이 실제로 어떻게 작동하는지 암시하지 않는다는 아이디어를 중심으로 진행됩니다. 또한 플레이어가 트랜잭션을 직접 재생할 수 없도록 하여 솔루션이 주소별로 고유해야 합니다. 가장 좋은 방법은 분명히 서명을 요청하는 것입니다.

하지만 어떤 상황에서 플레이어가 일부 데이터에 서명하도록 요청받게 될까요? 나의 첫 번째 디자인은 하나의 노출된 기능이 있는 간단한 퍼즐이었습니다. 플레이어는 일부 입력으로 함수를 호출하고 데이터에 서명하여 솔루션을 찾았음을 증명하고 입력이 다양한 검사를 모두 통과하면 해결사로 표시됩니다. 그러나 다음 몇 시간 동안 디자인에 살을 붙였을 때 결과에 ​​대해 금세 불만이 생겼고, 매우 투박하고 세련되지 않은 느낌이 들기 시작했고, 이렇게 형편없는 디자인으로 퍼즐을 푸는 것을 참을 수 없었습니다. 정말 훌륭한 제로데이(제로데이) 취약점 아이디어입니다.

인정해야 합니다, 금요일까지 이 일을 끝낼 수 없었기 때문에 잠을 좀 자기로 했습니다.

3

핀볼 퍼즐

주말 동안 초기 디자인을 계속 반복했지만 더 이상 진전이 없었습니다. 마치 내 현재 접근 방식이 벽에 부딪힌 것 같고, 인정하고 싶지는 않지만 만족스러운 것을 원한다면 처음부터 다시 시작해야 할 수도 있다는 것을 알고 있습니다.

결국 나는 첫 번째 원칙에서 문제를 재검토하고 있음을 발견했습니다. 내가 원하는 것은 플레이어가 다양한 지식 검사를 완료해야 하는 퍼즐 게임입니다. 그러나 나는 지식 검사를 완료하는 것 자체가 승리를 위한 조건이라고 주장하는 것이 아니라 플레이어가 선택할 수 있는 많은 경로 중 하나일 수 있습니다. 아마도 플레이어는 퍼즐 전체에서 포인트를 축적하여 일종의 보상을 얻기 위해 이 버그를 악용할 수 있습니다. 승리 조건은 단순히 최고 점수이기 때문에 간접적으로 익스플로잇 사용을 장려합니다.

저는 작년에 제가 디자인한 과제인 Lockbox를 기억합니다. Lockbox는 플레이어가 6가지 다른 계약의 요구 사항을 충족하기 위해 단일 데이터 블록을 구성하도록 했습니다. 계약은 동일한 바이트에 서로 다른 제약 조건을 적용하여 플레이어가 페이로드를 구성하는 방법에 대해 영리하게 만듭니다. 나는 여기서 플레이어에게 단일 데이터 블롭을 제출하도록 요청하고 특정 요구 사항을 충족하는 데이터의 특정 부분을 기준으로 점수를 부여하는 비슷한 일을 하고 싶다는 것을 깨달았습니다.

이 시점에서 나는 기본적으로 내가 DEFCON CTF 2020 결승전에서 직면한 도전인 pinboooll을 설명하고 있다는 것을 깨달았습니다. pinboooll의 특수 효과는 바이너리를 실행할 때 핀볼 머신에서 공이 튀는 것처럼 제어 흐름 그래프 주위에서 실행이 튀는 것입니다. 입력 내용을 적절하게 구조화하면 특정 코드 섹션을 찾고 포인트를 얻을 수 있습니다. 물론 버그가 있긴 하지만 솔직히 그게 뭔지 잊어버렸고 찾아보지도 않겠다. 그 외에도 이미 악용하고 싶은 취약점이 있었습니다.

작동하는 제로데이 익스플로잇을 다루고 있었기 때문에 이 퍼즐을 가능한 한 빨리 해결하기로 결정했습니다. 결국 나는 pinboooll이 어떻게 작동하는지 다시 익히는 데 몇 시간을 보냈고, 그것을 다시 구현하는 데 며칠을 보냈습니다. 이로써 퍼즐의 브래킷 문제가 해결되었습니다. 이제 익스플로잇을 통합하기만 하면 됩니다.

4

제로데이 익스플로잇 무장

Solidity가 올바른 바이트를 출력하도록 하는 방법은 몇 가지 상수를 로드하고 Solidity가 해당 PUSH 명령을 실행하도록 하는 것입니다. 그러나 이와 같은 임의의 상수는 큰 위험 신호가 될 수 있으며 더 잘 혼합되는 것을 원합니다. 또한 모든 상수를 한 줄에 로드해야 하는데 실제 코드로는 설명하기 어렵습니다.

두 개의 매직 바이트 시퀀스(0xa264...1220 및 0x6473...0033)만 하드코딩하면 되었기 때문에 세 번째 상수 대신 코드를 사이에 끼울 수 있는지 확인하기로 결정했습니다. 배포된 계약에서 샌드위치 코드를 다른 지침으로 교체하기만 하면 됩니다.

address a = 0xa264...1220;
uint x = 1 + 1 + 1 + ... + 1;
address b = 0x6473...0033;

약간의 실험 후에 나는 이것이 가능하다는 것을 알았지만 옵티마이저가 활성화된 경우에만 가능했습니다. 그렇지 않으면 Solidity가 과도한 가치 삭제 코드를 내보냅니다. 이는 허용 가능하므로 코드 자체를 개선하는 단계로 넘어갑니다.

2개의 주소 내에서만 코드를 수정할 수 있는데 끝에 매달려 있는 주소가 보이면 이상할 것 같아서 조건문에 사용하기로 했습니다. 저도 두 번째 조건의 필요성을 증명해야 했기 때문에 결국 약간의 가산점에 투표했습니다. 나는 이 코드 경로를 더 추구할 필요가 없다는 초기 인상을 주기 위해 tx.origin이 하드코딩된 값과 일치하는지 첫 번째 조건부 검사를 수행했습니다.

if (tx.origin != 0x13378bd7CacfCAb2909Fa2646970667358221220) return true;
state.rand = 0x40;
state.location = 0x60;
if (msg.sender != 0x64736F6c6343A0FB380033c82951b4126BD95042) return true;
state.baseScore += 1500;

이제 소스 코드가 모두 준비되었으므로 실제 백도어를 작성해야 합니다. 내 백도어는 플레이어가 익스플로잇을 올바르게 트리거하는지 확인하고, 그렇지 않은 경우 메시지를 표시하지 않고 실패하고, 성공적으로 트리거되면 보상해야 합니다. 익스플로잇이 쉽게 재생되지 않도록 하고 싶었기 때문에 플레이어가 자신의 주소에 서명하고 트랜잭션에 서명을 제출하도록 요구하기로 결정했습니다. 재미를 더하기 위해 서명이 일반적으로 볼이 시작되는 트랜잭션 데이터의 오프셋 0x44에 있도록 하기로 결정했습니다. 이를 위해서는 플레이어가 ABI 인코딩 작동 방식을 이해하고 공 데이터를 다른 위치에 수동으로 재배치해야 합니다.

그러나 여기서 큰 문제에 봉착합니다. 이 모든 논리를 31바이트의 손으로 작성한 어셈블리에 맞추는 것은 불가능합니다. 다행스럽게도 몇 가지 생각 끝에 작업할 31바이트가 더 있다는 것을 깨달았습니다. 결국, 실제 임베디드 메타데이터에는 Etherscan도 무시하는 또 다른 IPFS 해시가 포함되어 있습니다.

코드 골프를 치고 나서 작동하는 백도어에 도착했습니다. 첫 번째 IPFS 해시에서 방금 푸시한 주소를 즉시 팝한 다음 두 번째 IPFS 해시로 점프합니다. 거기에서 호출자를 해시하고 메모리/스택을 부분적으로 설정하여 ecrecover를 호출합니다. 그런 다음 첫 번째 IPFS 해시로 돌아가서 스택 설정을 마치고 호출을 실행합니다. 마지막으로 점수 승수를 (msg.sender == ecrecover()) * 0x40 + 1로 설정했습니다. 이는 추가 분기가 필요하지 않음을 의미합니다.

백도어를 특정 크기로 코딩한 후, 수도꼭지에서 테스트넷 ETH를 얻기 위해 내 Rinkeby 주소를 트윗한 다음 트윗을 보는 사람에게 무슨 일이 일어날 수 있다고 암시했습니다. 다음으로 계약을 배포하고 확인했습니다.

이제 남은 일은 인연이 있는 누군가가 훤히 들여다보이는 뒷문을 발견하기를 기다리는 일뿐이다.

참고: Rocket Pool의 소프트웨어 개발자인 Kane Wallmann은 보도 시간 현재 이 퍼즐을 풀었습니다. 프로세스는 다음과 같습니다.

이 기사는 허가를 받아 복제한 Yuan Universe의 Tao에서 발췌한 것입니다.

이 기사는 허가를 받아 복제한 Yuan Universe의 Tao에서 발췌한 것입니다.

ETH
Odaily 공식 커뮤니티에 가입하세요