Home > 졸업프로젝트 > UEFI DXE 바이너리 취약점 분석기 프로젝트_2

UEFI DXE 바이너리 취약점 분석기 프로젝트_2
UEFI Ghidra

SMM

SMM(System Management Mode)는 x86 및 x86-64 프로세서의 작동 모드로, 해당 모드는 OS가 실행되는 동안 OS의 아래에서 저수준 시스템 관리 작업을 수행하는 데 사용된다.
초기엔 부팅 관련 Phase에서만 사용되었지만 요즘에는 하드웨어 보호 및 제어, 각 제조사별 하드웨어 기능(키보드 백라이트 조절, 배터리 수명 모드 등)과 같은 곳에서도 사용된다.

제목
SMM의 설계도를 보면 SMM은 내부에서만 사용되는 것이 아닌 일반 모드들과도 연결이 되는데, 이 일반 모드와 SMM을 연결해주는 것이 바로 SMI(System Management Interrupt)이다. SMI가 발생하면 SMRAM이라는 SMM이 있는 전용 메모리 공간으로 들어가 SMM을 불러오는 것이다. 이 때 SMM에게 이 정보를 같이 처리해주세요! 라는 정보를 함께 넘기게 되는데 이를 CommBuffer라고 한다.
SMM이 이번 프로젝트에서 왜 중요하다고 생각한지 설명하기 전 CPU의 특권 레벨(Privilege Level)에 대해 설명할 필요가 있다. 특권 레벨이란 어떤 시점에서의 CPU의 권한 상태를 나타내는지, 다시 말해 CPU가 어떤 명령을 실행할 수 있는지, 메모리 어느 범위까지 도달할 수 있는지의 정도를 말한다.
제목
특권 레벨은 Ring 0~3까지 구성되며, 더 낮은 숫자로 내려갈수록 할 수 있는 것들이 많아진다. Ring 3은 우리가 흔히 사용하는 카카오톡과 같은 응용 프로그램이 해당되며, 여기선 하드웨어 조작, 타 프로그렘 메모리 읽기 등이 금지된다. Ring 0은 커널 모드로, OS, 하드웨어 드라이버 등이 이 단계에 해당된다. 여기선 모든 메모리의 접근, 하드웨어 직접 접근, Ring 3 프로그램의 강제 종료 등의 컴퓨터 내에서의 대부분의 것들을 수행할 수 있다.
(카카오톡이나 줌 등에서 웹캠 키고 마이크 킬 수 있잖아요! : 그것은 Ring 3이 Ring 0에게 켜달라고 요청을 하는 것이다.)
근데 여기서 SMM은 Ring 0인 커널보다도 더 낮은 숫자인 Ring -2에 위치한다.
(Ring -1은 OS를 담당하는 가상화 하이퍼바이저를 의미한다고 한다.)
Ring -2에 해당하는 SMM이 실행되면 OS 마저도 멈추고 SMM에 해당하는 작업을 처리하게 된다. 이는 메모리 및 장치 리소스에 대한 제한 없는 접근 권한을 가지므로 이 부분이 악성 코드들의 공격 경로로 자주 사용된다.
(출처)

CommBuffer 공격

다시 SMM으로 돌아와보자. SMM이 존재하는 SMRAM은 물리적으론 RAM에 존재하지만 논리적으로는 격리가 되어 만약 이 공간에 Ring -2보다 높은 숫자를 가진 레벨으로 접근하려 하면 쓰레기 값만 보여주거나 칩셋에서 자체 차단을 하게 된다. 그런데 만약 해커가 악의적 목적으로 CommBuffer에 덮어씌울 주소로 SMRAM 내부 주소를 입력한다면? 그리고 SMM이 해당 주소에 대한 검사를 하지 않고 바로 받아드린다면?
외부 오염된 데이터가 SMRAM으로 들어오면서 해커가 Ring -2의 권한을 탈취할 수도 있는 것이다.

SMM Callouts

SMM Callouts 공격은 위 CommBuffer 공격과 반대로 SMM Code가 SMRAM 경계 밖에 있는 함수를 호출할 때 발생한다. 원래 SMM Code는 SMRAM 내에서만 실행이 되어야 한다. 하지만 SMM Code가 외부에 있는 함수를 호출한다면? 그리고 그 외부 함수 주소를 타고 가보니 해커가 심어둔 악성코드의 주소라면?
악성 코드가 Ring -2라는 최상위 권한으로 실행되게 되는 것이다.

악성코드가 Ring -2로 실행되게 된다면 OS보다 더 높은 권한으로 실행됨으로써 해당 악성코드는 OS를 아무리 포맷해도 하드디스크가 아닌 메인보드 펌웨어 칩에 실리게 되어 치료가 불가능해질 수도 있다. 또한 악성코드를 탐지하는 백신들 역시 Ring 0에서 실행되므로 Ring -2에 실린 악성코드를 탐지할 수도 없게 된다. 그리고 이를 방지하기 위해 우리가 만들 분석기의 역할은 두가지.
1. 들어온 CommBuffer에 대한 검사를 수행하는가?
2. SMM Code가 외부 함수를 호출하려고 하는가?
를 감지할 수 있다면 훌륭한 분석기가 되지 않을까 생각하고 있다.

PE

UEFI의 분석을 위해서는 PE에 대한 공부가 필수적이라 생각한다.
제목
지난번에 뜯어본 .efi 파일의 맨 처음 부분을 보면 MZ라는 것이 적혀있는 것을 볼 수 있는데, MZ가 바로 PE파일임을 나타내는 signature이다. 이를 통해 UEFI의 DXE 드라이버들이나 SMM 모듈들은 PE의 구조를 따르는 것을 알 수 있다.
PE를 통해서 알 수 있는 정보들이 매우 많은데,
1. 섹션의 구분을 알 수 있다.
 몇번지부터 code인지, 몇번지부터 data인지 등을 여기서 알 수 있다.
2. 해당 코드의 진짜 시작점(AddressOfEntryPoint)을 알아낼 수 있다.
3. 주소 재배치 계산 정보가 여기에 담겨있다.
 이 부분이 틀리면 메모리 번지수 계산이 전부 망가지게 된다.
PE 헤더에 어떤 내용들이 담겨있는지를 여기서 전부 다 다루진 않지만, UEFI뿐 아니라 PE 전반적으로 중요한 부분들을 위주로 다뤄보겠다.

IMAGE_DOS_HEADER

이곳은 DOS 파일과의 하위 호환성을 위한 공간이다.
Signature(e_magic) : “MZ”가 아닐 경우 해당 파일을 실행하지 않는다.
Offset to New EXE Header : 실제 PE 헤더의 시작 Offset이 담겨있다.

IMAGE_FILE_HEADER

여기엔 해당 PE 파일의 기본적인 정보들이 담겨있다.
Machine : 컴퓨터 아키텍처의 유형이 적혀있다. x64인지, x86인지, ARM인지 등이 담겨있다.
Number Of Sections : .text, .data, .reloc와 같은 섹션이 몇 개 있는지를 알려준다.
Size of Optional Header : 다음에 나올 IMAGE_OPTIONAL_HEADER의 크기를 나타낸다.
Characterstics : 해당 파일의 속성값이 담겨있다.

IMAGE_OPTIONAL_HEADER

여기엔 해당 PE 파일의 부가적이지만 분석에 필수적인 정보들이 담겨있다.
Magic : 32비트인지(0x10B), 64비트인지(0x20B)가 적혀있다.
Size of Code : 코드의 크기를 나타낸다.
AddressOfEntryPoint : 실제 파일이 메모리에서 시작되는 지점을 나타낸다.
BaseOfCode : 실제 코드가 시작되는 번지수를 나타낸다.
ImageBase : 실제 가상 메모리에 올라가는 번지수를 나타낸다.
Section Alignment : 섹션 및 파일의 정렬을 위한 최소 단위를 나타낸다.
Size of Image : 해당 파일이 메모리에 로딩된 순간의 전체 크기를 나타낸다.
SubSystem : 해당 파일이 GUI인지, 드라이버인지, CLI 등인지를 나타낸다.
Number of Data Directiory : DataDirectory의 개수를 나타낸다.

이 때 BaseOfCode, AddressOfEntryPoint, ImageBase의 차이가 처음에 헷갈렸는데,
ImageBase : 실제 가상 메모리에 올라가는 번지수를,
BaseOfCode : ImageBase에서 코드부분 시작 지점까지 얼마나 떨어져 있는지를,
AddressOfEntryPoint : 실제 프로그램을 실행할 때 제일 처음 시작되는 부분(Main함수)이 얼마나 떨어져 있는지를 나타낸다.

IMAGE_SECTION_HEADER

여기선 각 Section들의 속성들을 나타낸다.
Virtual Size : 메모리 내에서 해당 섹션이 차지하는 크기를 나타낸다.
RVA : 메모리에서 해당 섹션의 시작 주소의 offset을 나타낸다.(RVA : Relative Virtual Address)
Size of Raw Data : 파일에서의 섹션의 크기를 나타낸다.
Pointer to Raw data : 파일에서의 해당 섹션의 offset을 나타낸다.
Characteristics : 해당 섹션의 속성(읽기 전용인지, 읽고 쓰기 전부 가능한지 등)을 나타낸다.

이 때 Section으론

  • .text : 프로그램의 실행 코드가 담겨있음.
  • .data : 읽고 쓰기 모두 가능한 Data Section. 초기화된 전역변수 및 static 변수 위치.
  • .rdata : 읽기 전용 Data Section. const 및 문자열 상수 등이 위치.
  • .bss : 초기화되지 않은 전역변수가 담겨있음.
  • .idata : Import할 DLL 및 API 관련 정보가 담긴 Section. IAT가 여기에 위치한다.
  • .didat : DLL 단위 지연 로딩을 위한 Section.
  • .edata : Export할 API가 담긴 Section.
  • .rsrc : 리소스 관련 Data가 담긴 Section(아이콘, 커서 등).
  • .reloc : 기본 재배치 정보들을 담고 있는 Section.

주소 계산법

PE에 적혀있는 주소들은 가상 주소에 해당된다. 해당 주소는 실제 메모리에 올라가면 주소가 바뀌게 되므로, 적혀있는 주소를 그대로 참조하는 것이 아닌 실제 주소와 가상 주소 사이 Offset을 계산해준 뒤 해당 Offset만큼 더해준 위치에 접근을 해주면 된다. 해당 Offset의 계산은 다음과 같다.
Offset = Load Address - ImageBase
하지만 이 Offset을 무작정 써먹을 순 없고, 약간의 계산을 추가적으로 해줘야 한다. 일반적으로 DataDirectory의 6번째(DataDirectory[5])에 재배치에 관한 테이블이 존재한다.
제목
위 그림이 바로 재배치 테이블이다. 해당 테이블의 data에 있는 값들이 바로 재배치를 해야 할 RVA 주소들로, 이 주소를 파일 위치(RAW)로 바꾼 뒤 해당 주소가 가르키는 값에 Offset을 더하는 과정을 반복해줘야 한다.
이 때 RVA를 RAW로 바꾸는 공식은 아래와 같다.
RAW = RVA - VA + PTRD
이 때 VA는 해당 RVA가 속한 Section 헤더의 RVA를, PTRD는 해당 Section 헤더의 Pointer To Raw Data를 뜻한다.