https://medium.com/@numencyberlabs/analysis-of-the-first-critical-0-day-vulnerability-of-aptos-move-vm-8c1fd6c2b98e
1. 서문
Move 프로그래밍 언어는 Ethereum의 Solidity 언어에 비해 강력한 장점으로 인해 최근 인기를 얻고 있습니다. Move는 Aptos 및 Sui와 같은 많은 잘 알려진 프로젝트에서 사용됩니다. 최근에,누멘 Web3 보안 취약점 탐지 제품은 Aptos 퍼블릭 체인의 가상 머신(VM)에서 심각한 수준의 보안 취약점을 발견했습니다. 우리가 발견한 것은 언어의 취약점으로 인해 Aptos 노드가 충돌하고 서비스 거부가 발생할 수 있다는 것입니다. 이 기사에서는 이 취약점에 대한 설명을 통해 Move 언어와 보안에 대해 더 잘 이해하기를 바랍니다. Move 언어 보안 연구의 리더로서 우리는 계속해서 생태 보안에 기여할 것입니다.
2. Move 언어의 중요한 개념
모듈 및 스크립트
Move에는 모듈과 스크립트라는 두 가지 유형의 프로그램이 있습니다. 모듈은 구조적 유형과 해당 유형에서 작동하는 기능을 정의하는 라이브러리입니다. 구조 유형은 Move의 글로벌 스토리지 패턴을 정의하고 모듈 기능은 스토리지 업데이트 규칙을 정의합니다. 모듈 자체도 전역 저장소에 저장됩니다. 스크립트는 기존 언어의 기본 기능과 유사한 실행 파일의 진입점입니다. 스크립트는 일반적으로 게시된 모듈의 함수를 호출하여 전역 저장소를 업데이트합니다. 스크립트는 전역 저장소에 게시되지 않는 임시 코드 조각입니다. Move 소스 파일(또는 컴파일 단위)에는 여러 모듈과 스크립트가 포함될 수 있습니다. 그러나 게시 모듈 또는 실행 스크립트는 별도의 가상 머신(VM) 작업을 활용합니다.
운영 체제에 익숙한 사람들에게 Move 모듈은 시스템의 실행 파일이 실행될 때 로드되는 동적 라이브러리 모듈과 유사하고 스크립트는 기본 프로그램과 유사합니다. 사용자는 모듈을 호출하는 코드를 포함하여 전역 저장소에 액세스하기 위해 자체 스크립트를 작성할 수 있습니다.
글로벌 스토리지
Move 프로그램의 목적은 트리 형태로 글로벌 스토리지에 읽고 쓰는 것입니다. 프로그램은 파일 시스템, 네트워크 또는 이 트리 외부의 데이터에 액세스할 수 없습니다.
의사 코드에서 전역 저장소는 다음과 같습니다.
구조적으로 글로벌 스토리지는 계정 주소에 뿌리를 둔 트리로 구성된 포리스트입니다. 각 주소는 리소스 데이터와 모듈 코드를 저장할 수 있습니다. 위의 의사 코드에서 볼 수 있듯이 각 주소는 주어진 유형의 최대 하나의 리소스 값과 주어진 이름의 최대 하나의 모듈을 저장할 수 있습니다.
MOVE 가상 머신 원리
movevm 및 evm 가상 머신은 동일하며 소스 코드를 바이트 코드로 컴파일한 다음 가상 머신에서 실행해야 합니다. 다음 차트는 프로세스를 보여줍니다.
1. 바이트코드는 execute_script 함수를 통해 로드됩니다.
2. load_script 함수를 실행합니다. 이 함수는 주로 바이트코드를 역직렬화하고 바이트코드가 유효한지 확인하는 데 사용됩니다. 확인에 실패하면 실패로 반환됩니다.
3. 성공적인 검증 후 실제 바이트 코드 코드가 실행됩니다.
4. 리소스, 모듈을 포함한 글로벌 스토리지의 바이트코드 실행, 액세스 또는 수정
참고: Move와 관련된 다른 많은 기능이 있지만 여기서는 모두 소개하지 않고 보안 관점에서 Move 언어의 기능을 계속 분석할 것입니다.
3. 취약점 설명
이 취약점은 주로 확인 모듈과 관련이 있습니다. 특정 취약점에 대해 이야기하기 전에 검증 모듈 및 StackUsageVerifier::verify의 기능을 소개합니다.
검증 모듈
우리는 바이트코드 코드의 실제 실행 전에 바이트코드의 검증이 있을 것이며 검증은 각각 여러 하위 프로세스로 세분될 수 있음을 알고 있습니다.
그들은:
경계 검사기 , 주로 모듈과 스크립트의 경계 보안을 확인하는 데 사용됩니다. 여기에는 서명, 상수 등의 경계 확인이 포함됩니다.
중복 검사기 , CompiledModule의 각 벡터에 다른 값이 포함되어 있는지 확인하기 위해 검사기를 구현하는 모듈
서명 검사기 , 함수 매개 변수, 지역 변수 및 구조체 멤버에 서명이 사용될 때 필드 구조가 올바른지 확인합니다.
지침 일관성 , 명령 일관성을 확인합니다.
상수 상수가 원래 유형이고 상수 데이터가 해당 유형으로 올바르게 직렬화되는지 확인하는 데 사용됩니다.
CodeUnitVerifier , 각각 stack_usage_verifier.rs 및 abstract_interpreter.rs를 통해 함수 본문 코드의 정확성을 확인합니다.
script_signature , 스크립트 또는 항목 기능이 유효한 서명인지 확인
취약점은 확인 프로세스 내에서 발생합니다.
CodeUnitVerifier::verify_script(구성, 스크립트)? ;
기능. 여기에 많은 확인 하위 프로세스가 있음을 알 수 있습니다.
이들은 스택 안전 체크섬, 유형 안전 체크섬, 로컬 변수 안전 체크섬 및 참조 안전 체크섬입니다. 스택 보안 검증 과정에서 취약점이 발생합니다.
스택 보안 확인(StackUsageVerifier::verify)
이 모듈은 함수의 바이트코드 명령어 시퀀스의 기본 블록이 균형 잡힌 방식으로 사용되는지 확인하는 데 사용됩니다. Ret(호출자에게 반환) opcode로 끝나는 것을 제외한 각 기본 블록은 처음과 동일한 스택 높이로 블록을 떠나도록 해야 합니다. 또한 기본 블록의 경우 스택 높이가 블록 시작 부분의 스택 높이보다 낮지 않아야 합니다.
위의 조건이 충족되는지 확인하기 위해 모든 블록을 반복합니다.
루프는 모든 기본 블록의 적법성을 확인하기 위해 반복됩니다.
취약점 세부 정보
앞서 소개한 바와 같이 movevm은 스택 가상 머신이기 때문에 명령어의 적법성을 검증할 때 첫째로 명령어 바이트코드가 올바른지 확인해야 하고 둘째로 스택 메모리가 올바른지 확인해야 합니다. 블록 호출 후 합법적입니다. 즉, 스택 작업 후 스택이 균형을 이룹니다.verify_block
기능은 두 번째 목적을 달성하는 데 사용됩니다.
에서 볼 수 있듯이verify_block
블록 코드 블록의 모든 명령을 반복한 다음 더하기 또는 빼기를 통해 스택에 대한 명령 블록의 효과가 합법적인지 여부를 확인합니다.num_pops
,num_pushes
. 첫째, 통해stack_size_increment < num_pops
스택 공간이 합법적인지 확인합니다. 만약에num_pops
보다 크다stack_size_increment
즉, 바이트코드 팝의 수가 스택 자체의 크기보다 크며 오류가 반환되고 바이트코드 체크섬이 실패합니다. 그런 다음stack_size_increment -= num_pops; stack_size_increment += num_pushes;
, 이 두 명령어는 각 명령어가 실행된 후 스택 높이에 미치는 영향을 수정합니다. 그리고 마지막으로 루프가 끝나면stack_size_increment
0과 같아야 합니다. 즉, 이 블록에서 작업을 유지한 후 스택의 균형을 맞춰야 합니다.
여기에는 이상이 없는 것 같지만 16줄의 코드 실행에서 정수 오버플로우 여부를 판단하지 않아 큰 num_pushes, stack_size_increment를 구성하여 간접적으로 제어할 수 있는 정수 오버플로우 취약점이 발생하기 때문입니다. . 그렇다면 어떻게 그렇게 많은 수의 푸시를 구성할 수 있을까요?
문제가 없는 것 같지만 여기에서 16번째 코드가 실행되기 때문에 정수 오버플로우가 있는지 없는지 판단하지 않는다. 그 결과,stack_size_increment
오버사이즈를 구성하여 간접적으로 제어할 수 있습니다.num_pushes
, 정수 오버플로 취약점이 발생합니다.
여기에서 먼저 이동 바이트코드 파일 형식을 도입해야 합니다.
바이트코드 파일 형식 이동
Windows PE 파일 또는 Linux ELF 파일과 마찬가지로 이동 바이트코드 파일은 .mv로 끝나고 파일 자체는 특정 형식을 갖습니다.
첫 번째는 매직, 값은 A11CEB0B, 다음은 버전 정보, 테이블 수, 그 다음은 테이블 헤더이며 많은 테이블이 있을 수 있습니다. Table kind는 테이블의 종류로, 총 0x10 종류(그림 오른쪽에 표시), 자세한 내용은 이동 언어 설명서를 참조하세요. 다음은 테이블의 오프셋, 길이입니다. 탁자. 그 다음은 테이블 내용이고 마지막으로 특정 데이터입니다. 두 가지 종류가 있습니다. 모듈의 경우 모듈 특정 데이터이고 스크립트 유형의 경우 스크립트 특정 데이터입니다.
구성된 악성 파일 형식
여기에서 우리는 스크립트에서 Aptos와 상호 작용하고 있으므로 stack_size_increment 오버플로를 유발하기 위해 아래에 표시된 파일 형식을 구성합니다.
먼저, 이 바이트코드 파일의 형식을 설명하겠습니다:
+0x00–0x03: 매직 워드 0xA11CEB0B
+0x04–0x7: 파일 형식 버전이며 버전은 4입니다.
+0x8–0x8: 테이블 수, 값은 1
+0x9–0x9: 테이블 종류, 유형은 SIGNATURES
+0xa-0xa: 테이블 오프셋, 값은 0
+0xb-0xb: 테이블 길이, 값은 0x10
+0xc-0x18: SIGNATURES 토큰의 데이터입니다.
0x22부터 시작하여 스크립트의 주요 기능 코드의 코드 부분입니다.
이동 디스어셈블러 도구를 통해 명령의 디스어셈블러 코드가 다음과 같은 것을 볼 수 있습니다.
이 중 3개의 명령어 0, 1, 2에 해당하는 코드는 각각 빨간색 상자, 녹색 상자, 노란색 상자의 데이터입니다.
LdU64는 취약점 자체와 관련이 없습니다. 여기서는 자세히 다루지 않겠지만 관심이 있는 경우 코드를 확인할 수 있습니다. 여기서는 VecUnpack 명령을 설명하는 데 중점을 둡니다. VecUnpack의 기능은 코드에서 벡터 개체를 만났을 때 모든 데이터를 스택으로 푸시하는 것입니다.
이 구성된 파일에서 VecUnpack을 두 번 구성합니다. 벡터의 숫자는 각각 3315214543476364830,18394158839224997406입니다.
기능이지시_효과
가 실행되면 아래 코드의 두 번째 줄이 실제로 실행됩니다.
실행 후지시_효과
함수에서 처음으로 (1,3315214543476364830)을 반환합니다. 이때 stack_size_increment는 0, num_pops는 1, num_pushes는 3315214543476364830입니다. 두 번째 반환 값은 (1,18394158839224997406)입니다. 다시 실행할 때stack_size_increment += num_pushes ;
stack_size_increment는 이미 0x2e020210021e161d(3315214543476364829)입니다.
num_pushes는 0xff452e02021e161e(18394158839224997406)이고, 둘을 더하면 u64의 최대값보다 커서 데이터 잘림이 발생하고, stack_size_increment의 값은 0x12d473012043c2c3b가 되어 정수 오버플로가 발생하여 Aptos 노드가 충돌하게 되고, 그러면 노드 실행이 중지됩니다. Rust 언어의 보안 기능으로 인해 C/C++와 같은 추가 코드 보안 영향은 발생하지 않습니다.
4. 취약점 영향
이 취약점은 Move 실행 모듈에서 발생하기 때문에 체인 상의 노드에 대해 바이트코드 코드가 실행되면 DoS 공격을 유발하게 됩니다. 심각한 경우 Aptos 네트워크가 완전히 중지될 수 있으며, 이는 헤아릴 수 없는 피해를 입히고 노드의 안정성에 심각한 영향을 미칩니다.
5. 공식 수정
이 취약점을 발견했을 때 공식 Aptos 팀에 보고했고 그들은 신속하게 취약점을 수정했습니다. 수정 사항의 스크린샷은 아래 그림을 참조하십시오.
관련 코드 링크는 다음과 같습니다.