https://medium.com/numen-cyber-labs/the-story-of-a-high-vulnerability-in-move-reference-safety-verify-module-2340f3d8c642
0x0 서문
얼마 전에 우리는 결정적인 것을 발견했습니다.취약점 앱토스 무브엠에서 심도 있는 연구 끝에 정수 오버플로인 또 다른 치명적인 취약점을 발견했지만 이번에는 매우 흥미로웠습니다.
우리는 Move 언어가 바이트코드를 실행하기 전에 코드 단위를 확인한다는 것을 알고 있습니다. 코드 단위에서 4단계로 분할되어 있는지 확인합니다. 이 버그는 reference_safety에서 발생합니다. 아래에서 자세히 다루도록 하겠습니다.
이 모듈은 절차 본문의 참조 안전성을 확인하기 위한 전달 함수를 정의합니다. 검사에는 매달려 있는 참조가 없는지, 변경 가능한 참조에 대한 액세스가 안전한지, 전역 저장소 참조에 대한 액세스가 안전한지 확인하는 것이 포함되지만 이에 국한되지 않습니다.
다음은 확인 진입점이며, analyze_function을 호출합니다.
analyze_function에서 각 기본 블록으로 함수를 검증할 것입니다. 기본 블록은 무엇입니까?
에서컴파일러 구성 , 기본 블록은 입구를 제외하고 분기가 없고 출구를 제외하고 분기가 없는 직선 코드 시퀀스입니다.
Move 언어에서 기본 블록을 어떻게 식별합니까?
Move 언어에서 기본 블록은 바이트코드를 순회하고 모든 분기 명령과 루프 명령을 찾아 결정됩니다. 아래에서 핵심 코드를 볼 수 있습니다.
다음은 Move 언어의 코드 블록의 예입니다. 3개의 기본 블록이 있음을 알 수 있습니다. 분기는 명령에 의해 결정됩니다: BrTrue, Branch, Ret.
이동 중 0x1 참조 안전
Move는 두 가지 유형의 참조를 지원합니다.불변 — & (예: &T) 및변하기 쉬운 — &mut(예: &mut T). 불변(&) 참조를 사용하여 구조체에서 데이터를 읽고 가변(&mut)을 사용하여 데이터를 수정할 수 있습니다. 적절한 유형의 참조를 사용하면 보안을 유지하는 데 도움이 되며 이 메서드가 값을 변경하는지 아니면 읽기만 하는지 알아야 합니다.
다음은 공식 Move 튜토리얼의 예입니다.
위의 예에서 mut_ref_t가 값 t의 변경 가능한 참조임을 알 수 있습니다.
따라서 Move 참조 안전 모듈은 함수를 단위로 삼아 함수의 기본 블록을 스캔하고 바이트 코드 명령어 검증을 통해 모든 참조 작업이 합법적인지 여부를 판단하여 참조가 유효한지 여부를 확인하려고 합니다.
아래 그림은 참조의 안전성을 확인하는 루틴을 보여줍니다.
여기서 상태는 참조를 보호하는 데 사용되는 차용 그래프와 로컬을 포함하는 AbstractState입니다.
빌린_그래프 로컬 참조 간의 관계를 나타내는 그래프입니다.
우리는 위의 그림에서 볼 수 있습니다.사전 상태 로컬과 차용 그래프(L ,BG)를 포함하고 기본 블록을 실행하면사후 상태 (L',BG')와 함께 블록 상태를 업데이트하고 이 블록의 사후 조건을 후속 블록으로 전파하기 위해 이전과 이후의 상태를 병합합니다. 이것은 마치노드의 바다 V8 터보팬 최적화.
아래 코드는 위 그림에 해당하는 메인 루프입니다. 먼저 블록 코드를 실행하고(명령 실행에 실패하면 AnalysisError가 반환됨) 병합을 시도합니다.사전 상태 그리고사후 상태 여부를 결정함으로써조인 결과 변경 여부. 변경되고 현재 블록에 에지 포인트가 포함되어 있으면(즉, 루프가 있음을 의미) 루프의 시작 부분으로 다시 점프하고 다음 라운드에서 다음 라운드까지 이 블록을 계속 실행합니다.사후 상태 동일하다사전 상태 또는 일부 오류로 인해 중단됩니다.
joinResult가 변경되었는지 변경되지 않았는지 어떻게 판단합니까??
위의 코드를 통해 로컬과 빌린 관계의 관계가 변경되었는지 여부를 판단하여 조인 결과가 변경되었는지 여부를 알 수 있습니다. 여기에서 join_ 함수는 로컬 및 차용 그래프 상태를 업데이트하는 데 사용됩니다.
아래의 join_ 코드에서 6행은 새 로컬 맵을 초기화하는 것이고, 9행은 모든 값이 None인 경우 블록 실행 전후에 로컬의 모든 인덱스를 반복하는 데 사용되므로 새 맵에 삽입하지 않습니다. 현지인 지도. 상태가 값을 갖기 전, 이후 상태가 None이면 brow_graph id를 해제해야 합니다. 이는 값의 차용 관계를 제거한다는 의미입니다. 그 반대도 마찬가지입니다. 특히 두 값이 모두 존재하고 같으면 30~33행과 같이 새 맵에 삽입한 다음 38행에서 borrow_graph를 병합합니다.
위에서 우리는 self.iter_locals()가 지역의 숫자임을 알 수 있습니다. 그리고 이 로컬에는 함수의 실제 로컬뿐만 아니라 매개변수도 포함됩니다.
0x2 취약점
여기서 우리는 취약점과 관련된 모든 코드를 교차했습니다. 찾았습니까?
취약점을 찾을 수 없다면 문제가 되지 않습니다. 취약점 유발 과정은 아래에서 자세히 다루도록 하겠습니다.
매개 변수 길이가 256보다 큰 지역 길이를 추가하는 경우 blew 코드에서 먼저. 문제가 없는 것 같습니까?
하지만 이 함수는 항목 유형이 u8인 Iterator를 반환합니다.
따라서 함수 join_()에서 function_view.parameters().len()과 function_view.locals().len()을 합친 값은 256보다 큽니다.
코드에서,self.iter_locals()의 로컬용 , 지역 변수는 u8 유형입니다. 256번 반복하면 오버플로가 발생합니다. 오버플로 후 local의 값은 8입니다.
실제로 Move에는 로컬 번호를 확인하는 루틴이 있지만 불행히도 매개 변수의 길이를 포함하지 않고 검사 범위 모듈의 로컬만 확인합니다.
여기에서 개발자는 매개변수 + 지역 값을 확인해야 할 필요성을 알고 있는 것 같습니다. 그러나 이 코드는 검사 범위 모듈에서 지역 수를 확인하며 매개 변수의 길이는 포함하지 않습니다.
0x3 오버플로를 DoS로 이동
우리는 코드 블록을 스캔하고 execute_block 함수를 호출하고 상태에 합류하는 메인 루프가 있다는 것을 알고 있습니다. 이동 코드가 존재하면 루프는 다시 실행을 시작하는 블록으로 점프합니다.
그래서 우리가 루프 코드 블록을 만들고 오버플로를 이용하여 블록의 상태를 변경하면 이전과 다르게 AbstractState 객체에 새로운 로컬 매핑을 만든 다음 execute_block 함수를 사용하여 블록을 다시 실행하면 이 함수를 알 수 있습니다. 코드 바이트 코드를 분석하고 로컬에 대한 액세스 권한을 부여하므로 참조 값 오프셋이 새 AbstractState 로컬 맵에 존재하지 않으면 DoS로 이어집니다.
코드를 감사한 후 MoveLoc/CopyLoc/FreeRef opcode를 사용하면 이 목표를 달성할 수 있다는 것을 알게 되었습니다.
파일 경로에서 execute_block 함수에 의해 호출되는 코드인 copy_loc 함수를 살펴보겠습니다.
이동/언어/이동-바이트코드-검증기/src/reference_safety/abstract_state.rs
287행에서 코드는 LocalIndex를 매개변수로 사용하여 로컬 값을 가져오려고 시도하고 LocalIndex가 존재하지 않으면 패닉이 발생합니다. Imagation은 노드가 이 사악한 코드를 실행할 때 전체 노드를 충돌시킵니다.
0x4PoC
다음은 git commit:add615b64390ea36e377e2a575f8cb91c9466844에서 재현할 수 있는 PoC입니다.
다음은 충돌 로그입니다.
'regression_tests::reference_analysis::PoC' 스레드가 '옵션::unwrap()'이라는 패닉에 빠졌습니다. `None` 값', language/move-bytecode-verifier/src/reference_safety/abstract_state.rs:287:39
참고: `RUST_BACKTRACE=1` 역추적을 표시하는 환경 변수
DoS 트리거 단계:
우리는 코드 블록이 무조건 분기임을 알 수 있으며 마지막 명령어 분기(0)를 실행할 때마다 첫 번째 명령어로 다시 점프하여 execute_block 및 join 함수를 여러 번 호출합니다.
1. 여기에서 처음으로 파라미터를 SignatureIndex(0)로 설정하고, SignatureIndex(0)에 대한 로컬은 num_locals 132*2=264를 이끌 것입니다. 그래서 전화를 한 후
264–256=8이 될 새로운 현지인 길이 선도
2. 두 번째로 execute_block 함수를 실행하고 첫 번째 명령어 copy_local(57)을 실행할 때 57은 스택에 푸시해야 하는 로컬의 오프셋이지만 이번에는 길이가 8인 로컬만 오프셋 57이 존재하지 않습니다. , 그래서 이로 인해 get(57).unwrap() 함수가 아무것도 반환하지 않고 패닉 상태가 됩니다.
0x5 요약
이것이 이 취약점에 대한 전체 이야기입니다. 그것으로부터 우리는 다음을 배웁니다.
첫째, 이 취약점은 절대적으로 안전한 코드가 없음을 보여줍니다. Move 언어는 코드가 실행되기 전에 좋은 정적 검증을 수행하지만 이 취약점과 마찬가지로 오버플로 취약점을 통해 이전 경계 검사를 완전히 우회할 수 있습니다.
둘째, 프로그래머가 때때로 태만할 수 있기 때문에 코드 감사가 매우 중요합니다. Move 언어 보안의 리더로서 Move의 보안 문제에 대해 계속해서 깊이 파고들겠습니다.
세 번째 요점은 Move 언어의 경우 예기치 않은 상황이 발생하지 않도록 언어 디자이너가 더 많은 확인 코드를 추가하는 것이 좋습니다.
현재 Move 언어는 검증단계에서 일련의 보안점검을 주로 하고 있는데, 이것만으로는 부족할 것 같습니다. 검증을 우회하면 런타임 단계에서 너무 많은 보안 강화가 이루어지지 않아 더 많은 위험이 심화되어 더 심각한 문제가 발생합니다. 마지막으로 Move 언어에서 또 다른 취약점을 발견했으며 곧 공개하겠습니다.