2. 공격 벡터
UEFI는 여러가지 phase로 나뉘어서 부팅되는만큼, phase마다 존재하는 취약점 유형에 조금씩 차이가 난다. 이러한 취약점들은 링 권한을 상승 시킬 수 있을 뿐만 아니라 상승 시킨 권한을 바탕으로 임의 코드 실행도 가능하다. 이와 같은 방식으로 설치된 악성코드는 운영 체제 부팅 프로세스 및 런타임에서 살아남거나 SPI 플래시 스토리지의 NVRAM 영역 수정을 통하여 지속적인 노출이 가능하다. 또한 코드상에 결함이 있는 경우 OS 보안 메커니즘을 우회하고 OS 부팅 프로세스에 영향을 미칠 수 있다.
다음 설명될 취약점들은 UEFI의 전반적인 취약점을 다루지만 모든 취약점은 다루지 않는다는 것을 언급하고 시작하겠다.
Stack Overflow
모든 프로그램에서 기본적으로 발생할 수 있는 취약점이 UEFI에서도 동일하게 발생한다. 버퍼의 크기에 맞지 않게 입력 값을 설정하여 오버플로우가 발생하는 경우도 존재하지만 UEFI에서는 Getvariable 함수의 중복 사용 시 데이터 사이즈를 명시하지 않아서 생기는 취약점이 더 대중적이다.
해당 취약점은 PEI, DXE 및 SMI Handler에서 발견된다.
Getvariable
이전에 Getvariable 함수를 설명하면서 “EFI_BUFFER_TOO_SMALL 오류를 발생 시키면 우리가 할당해야 할 배열의 크기로 Size 변수를 초기화 시키고 올바른 실행을 위해 함수에 전달한다” 라는 이야기를 했었다. (Getvariable함수의 자세한 기능은 이전에 설명했기 때문에 SP-15에서 확인하길 바란다.)
이는 해당 함수의 좋은 기능이지만 개발자들의 실수로 취약점으로 이어질 수 있다. 다음의 코드는 위에서 설명한 데이터 사이즈보다 값을 크게 넣었을 경우(EFI_BUFFER_TOO_SMALL) Getvariable 내부에서 처리하는 방식이다.
보다시피 오류 발생 시 입력한 값을 데이터 사이즈로 변경하는 것을 볼 수 있다.
단편적으로 보면 문제가 없어 보이지만 다음의 코드를 보고 생각을 다시 해보자.
해당 코드는 첫번째 Getvariable 함수 사용 시 EFI_BUFFER_TOO_SMALL 에러가 발생하면 DataSize가 공격자가 원하는 크기로 변경 될 것이다. 그렇게 되면 두번째 Getvariable함수를 호출 시 Stack BufferOverflow가 발생한다.
이를 해결하는 방법은 몇 가지가 존재한다. 완벽하게 조치하기 위해서는 Status 값으로 EFI_BUFFER_TOO_SMALL이 반환이 된 경우 에러를 반환하면 된다. 혹은 EFI_BUFFER_TOO_SMALL인 경우에도 프로그램의 중단을 원하지 않으면 DataSize에 대한 명시를 하거나 사이즈에 대한 변수를 동일 시 하지 않으면 해결이 가능하다.
PEI Phase
S3BootScript Modify:
S3는 RAM의 일시 중단 상태이다. CPU가 꺼지고 마더보드의 일부 칩도 꺼질 수 있다는 점을 제외하면 S1 절전 상태와 유사하지만 메인 시스템 메모리에 대한 전원은 유지되며 해당 상태에서 원래 상태로 돌아가기 위해서는 S3 부팅 스크립트를 사용한다. S3 부팅 스크립트는 S3 절전 상태에서 올바르게 복구하기 위해 펌웨어가 수행해야 하는 작업을 나열하는 데이터 구조이다.
S3 부팅 스크립트가 잘못 구현된 경우 많은 공격의 대상이 될 수 있다. 부팅 스크립트가 실행될 때까지 시스템이 완전히 구성되지 않았기 때문에 해당 시점에서 제어 흐름을 Hijacking하면 플랫폼에서 제공하는 특정 보호기법을 비활성화하거나 우회할 수 있다. 뿐만 아니라 이후에 나올 SMRAM 영역이라는 곳을 읽거나 쓰는 것도 가능하다.
자세한 내용은 다음의 첨부된 링크를 확인하면 된다.https://www.sentinelone.com/labs/adventures-from-uefi-land-the-hunt-for-the-s3-boot-script/
SMM
SMM(System Management Mode)는 x86 및 x86-64 프로세서의 작동 모드로, 펌웨어/BIOS OS가 실행되는 동안 저수준 시스템 관리 작업을 수행하는데 사용된다. 처음 SMM이 만들어진 것은 부팅 관련 Phase들에 대해서 실행하기 위해서 생겼지만 현재는 이외에도 다른 곳들에서 많이 사용된다.
위의 설계도를 보면 SMM은 SMM 내부에서만 사용되는게 아니라 Normal 모드들에 대해서도 연결된 것을 볼 수 있다. 그렇기 때문에 Normal과 SMM을 연결해주는 장치가 필요한데 이를 SMI(System Management Interrupt)라고 부른다.
Normal에서 SMM으로 넘어오게 되면 여러가지 CPU들이 가지고 있는 상태들(Register)이 존재하는데 이는 SaveState라는 곳에 저장되게 된다.
SaveState는 SMRAM이라는 곳에 저장이 되는데 이곳은 Normal하게 접근하는 것이 불가하도록 SMRR을 통하여 통제가 된다. (SMRR은 일반 Access로부터 보호되는 물리적인 메모리 범위를 정의할 수 있으며 추가적으로 SMM의 초기화 과정이 끝나면 Firmware측에서 설정해야 된다.)
+SMBASE는 SMM으로 전환할 당시의 상태들과 명령포인터들이 저장되는 위치를 나타내기 위해서 사용한다.(코어당 SMBASE가 하나씩 따로 존재한다) +DXE단계가 끝나면 SMM관련 설정을 해준 후 SMRAM은 LOCK 상태가 된다.
SMM Callouts(Calling out of SMRAM)
SMM Code가 SMRAM 경계 밖에 위치한 함수를 호출시 발생한다. 이를 통한 시나리오는 SMI Handler를 이용해서 UEFI boot service 또는 runtime service를 이용하는 방법이 존재한다. 또한 Os-Level의 권한을 가지고 있는 공격자들은 SMI를 트리거 하기 이전의 위치한 물리적 주소들에 대해서 수정이 가능하므로 이와 같은 영향받는 서비스가 호출되면 Priviledged된 실행들을 Hijacking이 가능하다.
위와 같이 SMI Handler가 정의되어지는 부분에서 EFI_RUNTIME_SERVICES 또는 EFI_BOOT_SERIVCES 테이블과 같은 곳에 존재하는 LocateProtocol, Setvariable 등의 함수를 사용할 시 취약하다.
Mitigation
SMM_Code_Chk_En SMM_CODE_CHK_EN은 MSR MSR_SMM_FEATURE_CONTROL(향상된 SMM 기능 제어, 0x4E0)의 단일 비트이며 이 비트의 공식 설명은 다음과 같다.
이 제어 비트는 MSR_SMM_MCA_CAP[58] == 1인 경우에만 사용할 수 있습니다. '0'(기본값)으로 설정하면 논리 프로세서 중 어느 것도 SMRR에 의해 정의된 범위를 벗어나 SMM 코드를 실행하는 것이 금지되지 않습니다. '1'로 설정하면 SMRR에 의해 정의된 범위 내에 있지 않은 SMM 코드를 실행하려고 시도하는 패키지의 논리 프로세서는 복구 불가능한 MCE를 표명합니다.
모든 프로세서에서 사용할 수 있는 것은 아니지만 실제로 최신 기능은 아니며 대부분의 컴퓨터에는 이 기능이 있다. SMRR은 두 개의 MSR(코어당 IA32_SMRR_PHYSBASE 및 IA32_SMRR_PHYSMASK)로 SMM, SMRAM에 제공되는 물리적 RAM의 범위를 나타내며 논리 프로세서가 SMM에 없는 경우 이에 대한 액세스를 금지한다. SMM_CODE_CHK_EN이 활성화되면 SMM에서 실행되는 코드가 SMRAM 외부에 있는 것을 금지한다.
How to Bypass?
SMRAM State Save Area라는 곳이 존재하는데 해당 부분은 공격자가 Control 할 수 있는 값들이 많이 존재한다.
실제로 위와 같은 register들을 OS-Level에서 가져올 수 있다. SMI 종류의 일부인 SWSMI(SoftWare SMI)의 IOPort(0xB2)를 통해 이용이 가능하며. 최근의 SMM 관련 취약점은 해당 부분에서 나오고 있다.
하지만 레지스터에 Shellcode를 삽입하더라도 해당 주소로 이동하기 위해서는 SMBASE값을 구해야한다. Local 환경에서의 공격자는 Brute Force와 같은 방법으로 구할 수 있으며, Remote 환경의 경우에는 아래와 같은 방식을 사용해 SMBASE를 계산할 수 있다. 추가적으로 SMBASE는 0x2000마다 새로 할당이 된다. (TileSize=0x2000)
추가정보
단순히 EFI_RUNTIME_SERVICE에서 사용되는 함수들을 사용한다고 취약한 것이 아니다. EFI_RUNTIME_SERVICE에 존재하는 함수들을 Mapping하여 EFI_SMM_RUNTIME_SERVICES_TABLE로 옮겨주는 작업을 하는 함수가 있으면 해당 부분은 취약한 것이 아니다.
아래는 Remapping관련 함수 예시이며 OEM별로 다르게 존재한다.
SMM Corruption(Using Comm buffer)
SMM Corruption은 기본적으로 SMI Handler에서 값을 입력받는 CommBuffer(Communication Buffer)를 통해서 SMRAM 내부에 값을 작성하는 것이다. (※ CommBuffer는 SMI 핸들러에서 Normal과 SMM의 연결 시 필요한 데이터들을 담은 버퍼이다.)
하지만 보호기법들이 다수 존재하기 때문에 SMRAM 부분에 Comm Buffer를 할당 받지 못한다. 이 중 하나는 SmmIsBufferOutsideSmmValid라는 함수를 통해 할당받는 영역이 SMRAM을 침범하는지 확인하는 과정이 존재한다.
하지만 약간의 Trick을 이용하면 Mitigation에 대한 우회가 가능하다. SMRAM 바로 아래에 CommBuffer를 위치하게 만든다. (SMRAM - 1) 이후 해당 SMI를 실행하면 SmmEP(Smm EntryPoint)가 CommBuffer 크기를 확인해서 해당 부분이 SMRAM을 덮는지의 유무를 파악한다.
SMRAM 바로 아래에 위치하기 때문에 정상적으로 Logic이 동작한다. 이후 SMI handler를 실행하게 되고 CommBuffer가 정상적으로 SMRAM 내부의 값들을 조작할 수 있게 된다.
그렇기 때문에 입력 값에 대한 정확한 확인이 필요하다.
❌
위와 같이 CommBuffer & ComBufferSize에 대해서만 검증하게 되는 경우에는실제로 공격자가 입력하고자 하는 Size를 파악하지 않기 때문에 SMRAM에 대한 값들을 조작이 가능하다.
✅
예시 1
SmiHandlerRegister를 통해서 SmiHandler를 불러온다.
Smi Handler는 아래와 같은 코드이고 SmmAllocateWirte부분에서 문제가 발생한다.
SMMAllocateWrite를 할당 받은 부분에서 CommBuffer로부터 옮겨온 데이터에 대한 사이즈 Check를 하지 않기 때문에 이전 글에서 설명했던 SMRAM 범위를 검사하는 루틴을 Bypass 가능하다.
이렇게 조작을 하게 되면 이어지는 코드에서 nested_ptr, ptr_plus40_ptr, ptr_plus18_pt…등 여러가지 포인터들이 SMRAM 내부를 가리키게 된다.
이후 dest에 대해서 정의를 하게 되어 주고(값이 들어갈 부분) sub_800018A8
함수에 들어가게 되면서 dest부분에 값을 적는 것이 가능하다.
결론적으로 해당 취약점을 제보한 binarly측에서도 Handler에 대해서 UnRegister과정을 거치고 있기 때문에 OS상에서는 exploited하지 않다고 설명한다. 그렇지만 해당 취약점을 통해서 OS단계가 아닌 DXE단계에서 코드 실행이 가능한 공격자가 Ring-2권한으로 상승 시킬 수 있다고 한다.
예시2
핸들러 내부에는 SmfbFunc1()으로 들어갈 수 있는 Routine이 존재한다. 해당 루틴으로 들어가는 인자들은 모두 CommBuffer로 통제가 가능한 값들이며 이 값들은 각각 addr, size, dest를 나타내기 때문에 SmfbFunc1내부에서 동작하는 값들을 Controllable하게 가능하다.
SmfbFunc1을 보면 아래와 같이 memcpy를 통해서 src값을 읽은 후 src에 넣는 것을 볼 수 있는데 이는 Leak까지 이어진다.
Nested Pointer in SMM
CommBuffer를 통해서 값을 받고 해당 CommBuffer에 값을 저장할 때 이중 포인터를 사용 시 생길 수 있는 문제들을 말한다.
Microsoft사에서 WSMT라는 것을 사용해서 WSMT의 COMM_BUFFER_NESTED_PTR_PROTECTION flag를 통해서 보호를 한다고 하지만 OEM에서 제공하는 코드들에서 이중 포인터를 이용해서 해당 문제가 야기되기도 한다.
Sentinel One의 연구원인 Assaf Carlsbad도 아래와 같이 WSMT가 존재해도 취약점이 존재한다고 개인적인 견해를 밝혔다.
아래의 예시를 통해서 정확히 어떻게 사용되는지 알아보겠다.
예시
코드를 보면 CommBuffer를 통해서 swtich문으로 들어가게 되며 default: 상태가 되면 ComBuffer + 1을 이중 포인터로 사용하여 v_status값을 넣게 된다.
해당 switch문이 default로 들어가기 위해서 CommBuffer에 0, 2, 3이 아닌 값 넣어준다.
이후 CommBuffer+1부분에 SMRAM의 주솟값을 적어주면 이중 포인터로 SMRAM을 가리키게 되는 부분에 v_status의 값이 들어가게 된다. 이러한 방식으로 SMRAM의 값을 덮을 수 있게 된다.
TOCTOU(Double-Fetches From the CommBuffer)
TOCTOU는 프로그램 상에서 시간차를 이용해 공격자가 원하는 방식으로 흐름을 제어하는 것이다. 자세한 것은 다음의 예시를 통해서 설명하겠다.
예시
ComBuffer→field_18의 값을 smm_field_18에 저장하고 이후 smm_field_18이 SMRAM을 Overlap하는지 확인한다. SMRAM 내부 영역에 쓰지 않는다면 SmmIsBufferOutsideSmmVlid 조건 문을 통과하게 되고 CopyMem을 통해서 값을 ComBuffer→filed_18에 저장한다.
실제로는 위와 같이 정상적으로 동작이 되는게 맞지만 CopyMem을 할 때SmmIsBufferOutsideSmmValid로 정의한 smm_filed_18을 사용하지 않고 원래 있던CommBuffer→field_18을 사용한게 문제가 된다.
왜냐하면 해당 Mitigation을 통과한 이후에 DMA를 통해서 Commbuffer→field_18의 값을 바꾸게 되면 Commbuffer→field_18에 값을 쓸 수 있게되고 이곳을 SMRAM으로 설정하면 이중 포인터로 인하여 SMRAM을 덮을 수 있게 된다.
Last updated