제어 흐름 무결성

2016년에는 Android에서 발생하는 모든 취약성의 약 86%가 메모리 안전과 관련이 있었습니다. 대부분의 취약성은 애플리케이션의 정상적인 제어 흐름을 변경하여 악용된 애플리케이션의 모든 권한으로 임의의 악의적인 활동을 실행하는 공격자에 의해 악용됩니다. 제어 흐름 무결성(CFI)은 컴파일된 바이너리의 원래 제어 흐름 그래프에 관한 변경을 차단하는 보안 메커니즘으로, 이러한 공격을 실행하기 훨씬 어렵게 만듭니다.

Android 8.1에서는 미디어 스택에 LLVM의 CFI 구현을 사용 설정했으며, Android 9에서는 추가적인 구성요소는 물론 커널에도 CFI를 사용 설정했습니다. 시스템 CFI는 기본으로 사용되지만, 커널 CFI는 사용 설정해야 합니다.

LLVM의 CFI에는 링크 시점 최적화(LTO)를 통한 컴파일이 필요합니다. LTO는 링크 시점까지 객체 파일의 LLVM 비트코드 표현을 보존하여 컴파일러가 어떤 최적화를 실행할 수 있는지에 관해 더 나은 추론을 내릴 수 있게 해줍니다. LTO를 사용 설정하면 최종 바이너리의 크기가 축소되고 성능이 개선되지만 컴파일 시간은 증가합니다. Android에서 테스트할 때는 LTO 및 CFI 조합이 코드 크기 및 성능에 있어 미미한 수준의 오버헤드로 이어지며, 경우에 따라서는 둘 다 개선되기도 합니다.

CFI에 대한 기술적 세부정보, 그리고 정방향 제어 검사가 처리되는 방식은 LLVM 설계 문서를 참조하세요.

예 및 소스

CFI는 컴파일러에 의해 제공되며 컴파일 시간 동안 바이너리에 계측을 추가합니다. Google은 AOSP의 Clang 도구 모음과 Android 빌드 시스템에서 CFI를 지원합니다.

CFI는 /platform/build/target/product/cfi-common.mk 내 구성요소 집합의 Arm64 기기를 위해 기본으로 사용 설정됩니다. 또한, /platform/frameworks/av/media/libmedia/Android.bp/platform/frameworks/av/cmds/stagefright/Android.mk와 같은 미디어 구성요소의 makefile/청사진 파일 집합에도 직접적으로 사용 설정됩니다.

시스템 CFI 구현

CFI는 Clang 및 Android 빌드 시스템을 사용하는 경우 기본으로 사용 설정됩니다. CFI는 Android 사용자를 안전하게 지키는 데 도움이 되므로 사용 중지하면 안 되며,

추가 구성요소에도 CFI를 사용 설정하는 것이 좋습니다. 이상적인 후보는 권한 있는 네이티브 코드 또는 신뢰할 수 없는 사용자 입력을 처리하는 네이티브 코드입니다. Clang 및 Android 빌드 시스템을 사용 중인 경우 makefile 또는 청사진 파일에 몇 개의 행을 추가하여 새 구성요소에 CFI를 사용 설정할 수 있습니다.

makefile에 CFI 지원

/platform/frameworks/av/cmds/stagefright/Android.mk와 같은 makefile에 CFI를 사용 설정하려면 다음을 추가합니다.

LOCAL_SANITIZE := cfi
# Optional features
LOCAL_SANITIZE_DIAG := cfi
LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
  • LOCAL_SANITIZE는 빌드 도중 CFI를 새니타이저로 지정합니다.
  • LOCAL_SANITIZE_DIAG는 CFI의 진단 모드를 켭니다. 진단 모드는 충돌이 진행되는 동안 logcat에 추가적인 디버그 정보를 출력하며, 이는 빌드를 개발 및 테스트할 때 유용합니다. 하지만 프로덕션 빌드에서는 진단 모드를 삭제해야 합니다.
  • LOCAL_SANITIZE_BLACKLIST는 구성요소가 개별 함수 또는 소스 파일의 CFI 계측을 선택적으로 사용 중지할 수 있게 해줍니다. 해결되지 않은 채 남아있을 경우 사용자에게 영향을 미칠 수 있는 모든 문제를 해결하기 위해 블랙리스트를 최후의 수단으로 활용할 수 있습니다. 자세한 내용은 CFI 사용 중지를 참조하세요.

청사진 파일에 CFI 지원

/platform/frameworks/av/media/libmedia/Android.bp와 같은 청사진 파일에 CFI를 사용 설정하려면 다음을 추가합니다.

   sanitize: {
        cfi: true,
        diag: {
            cfi: true,
        },
        blacklist: "cfi_blacklist.txt",
    },

문제 해결

새 구성요소에 CFI를 사용 설정하는 경우 함수 유형 불일치 오류어셈블리 코드 유형 불일치 오류를 비롯한 몇 가지 문제에 직면할 수 있습니다.

함수 유형 불일치 오류가 발생하는 이유는 CFI가 호출에 사용된 정적 유형과 동일한 동적 유형을 포함하는 함수로만 점프하기 위해 간접 호출을 제한하기 때문입니다. CFI는 호출을 전송하기 위해 사용된 객체의 정적 유형에서 파생된 클래스인 객체로만 점프하기 위해 가상 및 비 가상 멤버 함수 호출을 제한합니다. 즉, 이러한 가정을 위반하는 코드가 있으면 CFI에서 추가하는 계측이 취소됩니다. 예를 들어 스택 트레이스에는 SIGABRT가 표시되고 logcat에는 불일치를 찾는 제어 흐름 무결성에 관한 행이 포함됩니다.

이를 해결하려면 호출된 함수에 정적으로 선언된 동일한 유형이 있는지 확인해야 합니다. CL의 예는 다음과 같이 두 가지입니다.

또 다른 잠재적인 문제는 어셈블리의 간접적인 호출을 포함하는 코드에 CFI를 사용 설정하려고 시도하는 상황입니다. 어셈블리 코드는 유형이 지정되지 않으므로 이는 유형 불일치로 이어집니다.

이를 해결하려면 각 어셈블리 호출의 네이티브 코드 래퍼를 생성하고 래퍼에 포인터 호출과 동일한 함수 서명을 제공하세요. 그러면 래퍼가 어셈블리 코드를 직접 호출할 수 있습니다. 직접 브랜치는 CFI에 의해 계측화되지 않으므로(런타임 시에 다시 가리킬 수 없으므로 보안 위험을 야기하지 않음) 이를 통해 문제를 해결할 수 있습니다.

어셈블리 함수가 너무 많고 전부 수정할 수 없는 경우에는 어셈블리에 대한 간접 호출을 포함하는 모든 함수를 차단할 수도 있습니다. 하지만 이는 이러한 함수의 CFI 검사를 사용 중지하여 공격 영역이 열리는 결과로 이어지므로 권장하지는 않습니다.

CFI 사용 중지

관찰된 성능 오버헤드는 없으므로 CFI를 사용 중지할 필요가 없습니다. 하지만 사용자에 미치는 영향이 있는 경우 컴파일 시간에 새니타이저 블랙리스트 파일을 제공하여 개별 함수 또는 소스 파일의 CFI를 선택적으로 사용 중지할 수 있습니다. 블랙리스트는 지정된 위치의 CFI 계측을 사용 중지하도록 컴파일러에 지시합니다.

Android 빌드 시스템은 Make 및 Soong 둘 다에 구성요소별 블랙리스트를 지원하여 CFI 계측을 수신하지 않는 소스 파일 또는 개별 함수를 선택할 수 있게 해줍니다. 블랙리스트 파일 형식에 관한 자세한 내용은 업스트림 Clang 문서를 참조하세요.

유효성 검사

현재로서는 CFI 전용 CTS 테스트가 없습니다. 대신, CFI 사용 설정 여부와 상관없이 CTS 테스트가 통과되도록 하여 CFI가 기기에 영향을 미치지 않는지 확인하세요.