
안녕하세요, 저와 함께 스마트 계약을 계속 해체하는 것을 환영합니다. 이 기사는 시리즈의 세 번째 부분이므로 이전 기사를 읽지 않았다면 다음 기사를 살펴보십시오.
간단한 Solidity 스마트 계약의 EVM 바이트코드를 분해하고 있습니다.
앞선 글에서 스마트 컨트랙트의 바이트코드가 생성과 작동의 두 부분으로 나누어진다고 판단했고 왜 이렇게 하는지 알게 되었습니다. 런타임 부분. 분해 다이어그램을 보면 먼저 BasicToken.evm(런타임)이라는 두 번째 큰 분할 블록을 볼 수 있습니다.
실행할 코드가 생성할 코드 크기의 최소 4배이기 때문에 이것은 약간 무섭게 보일 수 있습니다! 그러나 걱정하지 마십시오. 이전 기사에서 EVM 코드를 이해하기 위해 개발한 기술과 견고한 "분할 및 정복" 전략의 사용이 결합되어 이 문제를 보다 체계적이고 쉽게 만들 수 있습니다. 이것은 시작에 불과하며, 계속해서 독립적인 구조를 식별하고 해결할 수 있는 문제로 분해될 때까지 계속 분해할 것입니다.
먼저 Remix 온라인 편집기로 돌아가서 런타임 바이트코드로 디버깅 세션을 시작하겠습니다. 우리는 그것을 어떻게 지난 시간에 우리는 스마트 컨트랙트를 배포하고 배포 트랜잭션을 디버깅했습니다. 이번에는 기능 중 하나를 사용하여 배포된 스마트 계약과 인터페이스하고 트랜잭션을 디버그합니다.
먼저 스마트 계약을 상기해 보겠습니다.
최적화 컴파일러, v0.4.24 버전 및 초기 공급으로 10000을 사용하여 Javascript VM을 활성화했습니다. 스마트 계약을 배포한 후 Remix의 "실행" 패널에 있는 "배포된 계약" 섹션에 스마트 계약이 나열되어 있어야 합니다. 확장하여 스마트 계약의 인터페이스를 보려면 클릭하십시오.
이 인터페이스는 무엇입니까? 스마트 계약의 모든 공개 또는 외부 방법 목록입니다.즉, 모든 이더리움 계정 또는 스마트 계약이 상호 작용할 수 있습니다. 프라이빗 및 내부 메서드는 여기에 표시되지 않으며, 스마트 계약 런타임 코드의 특정 부분과 상호 작용하는 방법은 이 기사의 분해의 초점이 될 것입니다.
Remix의 "실행" 패널에서 totalSupply 버튼을 클릭하여 사용해 볼 수 있습니다. 버튼 바로 아래에 응답이 표시되어야 합니다. 이는 초기 토큰 공급으로 스마트 계약을 배포한 이후 예상한 것입니다. 이제 콘솔 패널에서 디버그 버튼을 클릭하여 이 특정 트랜잭션으로 디버그 세션을 시작합니다. 콘솔 패널에는 여러 디버그 버튼이 있으므로 최신 버전을 사용하고 있는지 확인하십시오.
이 경우 이전 게시물에서 보았듯이 이 0x0 주소로 트랜잭션을 디버깅하지 않고 스마트 계약이 생성되었습니다. 대신 스마트 계약 자체, 즉 런타임 코드에 대한 트랜잭션을 디버깅합니다.
지침 패널이 나타나면 Remix가 분해 그래프의 BasicToken.evm(런타임) 섹션과 동일한 지침을 나열하는지 확인할 수 있어야 합니다. 일치하지 않으면 문제가 발생한 것입니다. 처음부터 다시 시도하고 위와 같이 올바른 설정을 사용하고 있는지 확인하십시오.
가장 먼저 알아차릴 수 있는 것은 디버거가 명령 246에 위치시키고 트랜잭션 슬라이더가 바이트코드의 약 60%에 있다는 것입니다. 왜? Remix는 매우 관대 한 프로그램이기 때문에 totalSupply 기능의 본문을 실행하려는 EVM 부분으로 직접 이동합니다. 그러나 그 이전에 많은 일이 있었고 우리가 여기서 주목해야 할 것입니다. 사실 이 기사에서는 함수 본문의 실행에 대해서도 살펴보지 않을 것입니다.우리의 유일한 관심사는 Solidity에서 생성된 EVM 코드가 들어오는 트랜잭션을 라우팅하는 방법이며, 이는 계약의 "기능 선택기"로 이해하게 될 작업입니다.
따라서 슬라이더를 잡고 왼쪽 끝까지 드래그하여 명령 0에서 시작합니다. 이전에 본 것처럼 EVM은 예외 없이 항상 명령 0에서 코드를 실행한 다음 나머지 코드를 통해 흐릅니다. opcode를 실행하여 opcode를 실행해 보겠습니다.
나타나는 첫 번째 구조는 이전에 본 것입니다(실제로는 많이 보게 될 것입니다).
그림 1. 여유 메모리 포인터
이것은 Solidity에서 생성된 EVM 코드가 호출하기 전에 항상 수행하는 작업입니다. 나중에 사용할 수 있도록 메모리에 포인트를 저장합니다.
다음에 무슨 일이 일어나는지 봅시다:
그림 2. 호출 데이터 길이 확인.
디버그 탭에서 Remix의 스택 패널을 열고 지침 5~7을 건너뛰면 이제 스택에 숫자가 두 번 포함되어 있음을 알 수 있습니다. 이렇게 긴 숫자를 읽는 데 문제가 있는 경우 숫자가 한 줄에 잘 맞도록 Remix 디버그 패널의 너비를 조정해야 합니다. 첫 번째는 일반 푸시에서 발생하지만 두 번째는 노란 종이에서 말했듯이 인수를 받지 않고 "현재 환경의 입력 데이터" 크기를 반환하는 opcode를 실행한 결과입니다. 콜 데이터 호출: 4CALLDATASIZE
콜데이터란? Solidity 문서 ABI 사양에 설명된 대로 calldata는 호출하려는 스마트 계약 기능과 해당 매개변수 또는 데이터에 대한 정보를 포함하는 인코딩된 16진수 블록입니다. 간단히 말해서 함수의 서명을 해싱(처음 4바이트로 잘림)한 다음 매개변수 데이터를 압축하여 생성되는 "함수 ID"로 구성됩니다. 원하는 경우 문서 링크를 자세히 연구할 수 있지만 이 래퍼가 가장 세부적으로 작동하는 방식에 대해 걱정하지 마십시오. 문서에 설명되어 있지만 가끔 이해하기가 약간 어렵습니다. 실제 사례를 통해 이해하기가 훨씬 쉽습니다.
이 calldata가 무엇인지 봅시다. Remix의 디버거에서 통화 데이터 패널을 열고 0x18160ddd를 확인합니다. 이것은 keccak256 함수 서명에 문자열로 알고리즘을 적용하여 생성된 정확히 4바이트입니다."totalSupply()"상기 절단을 수행한다. 이 특정 함수는 매개변수를 사용하지 않기 때문에 4바이트 함수 ID일 뿐입니다. CALLDATASIZE가 호출되면 두 번째 4를 스택으로 푸시합니다.
그런 다음 명령 8 LT를 사용하여 호출 데이터 크기가 4보다 작은지 확인합니다. 그렇다면 다음 두 명령어는 JUMPI 명령어 86(0x0056)을 실행합니다. 4바이트 미만이므로 이 경우 점프가 없으며 실행 흐름은 명령 13으로 계속됩니다. 하지만 그렇게 하기 전에 빈 calldata로 스마트 계약을 호출한다고 가정해 보겠습니다. 즉, 0x0입니다. 0x18160ddd 대신. Remix btw로는 그렇게 할 수 없지만 트랜잭션을 수동으로 빌드할 수 있습니다.
이 경우 기본적으로 몇 개의 0을 스택에 푸시하고 REVERT opcode에 공급하는 명령 86에서 끝납니다. 왜? 이 스마트 계약에는 대체 기능이 없기 때문입니다. 바이트 코드가 들어오는 데이터를 인식하지 못하면 흐름을 폴백 함수로 전환하고 해당 구조가 호출을 "잡지" 못하면 이 복구 구조가 실행을 종료하고 롤백은 전혀 발생하지 않습니다.반환할 항목이 없으면 수행할 작업이 없으며 통화가 완전히 복원됩니다.
이제 좀 더 재미있는 일을 해봅시다. Remix의 실행 탭으로 돌아가 계정 주소를 복사하고 트랜잭션을 디버그하기 위해 totalSupply 대신 balanceOf를 호출하는 인수로 사용합니다. 이것은 완전히 새로운 디버깅 세션입니다. 잠시 동안 totalSupply는 잊어버리십시오. 명령 8로 이동하면 CALLDATASIZE가 이제 36(0x24)을 스택에 푸시합니다. calldata를 보면 이제 0x70a08231000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c입니다.
이 새로운 호출 데이터는 실제로 분해하기가 매우 쉽습니다. 처음 4바이트 70a08231은 서명의 해시이고 그 뒤에"balanceOf(address)"32바이트에는 인수로 전달하는 주소가 포함됩니다. Ethereum 주소의 길이가 20바이트인 경우 왜 32바이트입니까? 호기심 많은 독자가 질문할 수 있습니다. ABI는 항상 32바이트 "단어" 또는 "슬롯"을 사용하여 함수 호출에 사용되는 인수를 보관합니다.
balanceOf 호출 환경을 계속 진행하면서 이 시점에서 스택에 아무 것도 없는 명령 13에서 중단한 위치를 선택해 보겠습니다. 명령어 13은 0xffffffff를 스택에 푸시하고 다음 명령어는 29바이트 길이의 0x000000001000...000 숫자를 스택에 푸시합니다. 잠시 후에 그 이유를 살펴보겠습니다. 지금은 하나는 4바이트를 포함하고 다른 하나는 0'의 4바이트를 포함한다는 점에 유의하십시오.
다음 CALLDATALOAD는 인수(명령 48에서 스택으로 푸시된 인수)를 취하고 해당 위치의 calldata에서 32바이트 청크를 읽습니다. 이 경우 Yul은 다음과 같습니다.
calldataload(0)
기본적으로 내 전체 호출 데이터를 스택에 푸시합니다. 이제 재미있는 부분이 있습니다. DIV는 스택에서 두 개의 매개변수를 사용하고 calldata를 가져와 이상한 숫자 0x000000001000...000으로 나누어 calldata의 함수 서명을 제외한 모든 것을 효과적으로 필터링하고 스택에 남겨 둡니다: 0x000...000070a08231. 다음 명령어는 AND를 사용하는데 스택에서 두 개의 요소인 함수 ID와 4바이트의 숫자 f를 사용합니다. 서명 해시의 길이가 정확히 8바이트인지 확인하고 다른 것이 있는 경우 이를 가리기 위한 것입니다. Solidity가 사용하는 보안 조치라고 생각합니다.
요컨대, calldata가 너무 짧은지 확인하고, 그렇다면 되돌린 다음 조금 개선하여 스택에 함수가 있도록 합니다.
게다가 거의 끝났습니다. 다음 부분은 이해하기 쉬울 것입니다.
그림 3. 기능 선택기
명령 53에서 코드는 18160ddd(함수 id totalSuppy)를 스택에 푸시한 다음 DUP2를 사용하여 현재 스택의 두 번째 위치에 있는 수신 호출 데이터 값 70a08231을 복제합니다. 왜 복사합니까? EQ 명령 59의 opcode가 스택에서 두 개의 값을 소비할 것이기 때문에 calldata에서 추출하는 수고를 겪었기 때문에 70a08231 값을 유지하려고 합니다.
이제 코드는 calldata의 함수 id를 알려진 함수 id 중 하나와 일치시키려고 시도합니다. 70a08231이 들어오기 때문에 18160ddd와 일치하지 않고 63번 명령에서 JUMPI를 건너뜁니다. 그러나 다음 검사에서 일치하고 74번 명령에서 JUMPI로 점프합니다.
스마트 계약의 각 공용 또는 외부 기능에서 이러한 동등성 검사가 어떻게 수행되는지 잠시 살펴보겠습니다.이것은 함수 선택기의 핵심입니다. 코드의 올바른 부분으로 실행을 라우팅하는 일종의 switch 문 역할을 합니다. 이것이 우리의 "허브"입니다.
따라서 마지막 경우가 일치하므로 실행 흐름은 위치 130에 있는 JUMPDEST로 이동합니다. 이 시리즈의 다음 부분에서 볼 수 있듯이 이것은 balanceOf 함수에 대한 ABI "래퍼"입니다. 앞으로 보게 되겠지만 이 래퍼는 함수 본문에서 사용할 트랜잭션 데이터를 언래핑하는 역할을 합니다.
계속해서 이 디버깅 기능을 전송해 보십시오. 기능 선택기에는 미스터리가 없습니다. 모든 컨트랙트(최소한 Solidity에서 컴파일된 모든 컨트랙트)의 입구에 위치하며 실행을 코드의 적절한 위치로 리디렉션하는 간단하지만 효과적인 구조입니다. 이것은 Solidity가 스마트 계약의 바이트코드에 여러 진입점을 시뮬레이션하여 인터페이스를 제공하는 방법입니다.
분해 다이어그램을 보면 방금 분해한 것이 다음과 같습니다.
그림 4. 스마트 계약의 런타임 코드에 대한 기능 선택기 및 기본 진입점.
대체로 여러분, 여러분이 알기도 전에 여러분은 대부분의 사람들보다 견고함의 기본 코드에 더 익숙하고 그것을 고수하면 완전히 열 수 있을 것입니다.
*이 기사는 Alejandro Santander가 매체에 처음 게시했으며 Cheetah Blockchain에서 번역 및 구성했습니다.*
Cheetah 블록체인 보안은 인공 지능, nlp 및 기타 기술과 결합된 Kingsoft Internet Security의 기술을 기반으로 블록체인 사용자에게 계약 감사 및 감정 분석과 같은 생태 보안 서비스를 제공합니다.
레이팅토큰 공식 홈페이지https://www.ratingtoken.net/?from=z