1. SMM/MM Communication Protocol이란?
일반 UEFI 환경(DXE)이 SMRAM(또는 MMRAM) 내부에 있는 특정 핸들러와 통신하려면 양쪽 모두 접근할 수 있는 영역이 필요하다. SMM은 시스템과 격리된 고유 메모리인 SMRAM을 사용하기 때문에, DXE 측에서 일반 메모리(SMRAM 외부)에 공유 메모리(CommBuffer)를 할당하는 방식으로 데이터를 주고받는다. 이렇게 할당된 버퍼와 소프트웨어 인터럽트(SMI)를 이용해 데이터를 전달하고 결과를 돌려받는 표준 통신 인터페이스를 SMM/MM Communication Protocol이라고 부른다.
최신 EDK2와 Platform Initialization Specification에서는 x86에 종속적인 SMM이라는 용어 대신, 아키텍처 독립적인 MM(Management Mode)이라는 용어와 프로토콜(EFI_MM_COMMUNICATION_PROTOCOL) 사용을 권장한다. Standalone MM 환경이나 ARM의 TrustZone 등 다양한 격리 실행 환경을 통합하기 위함이다.
통신을 시작하려면 먼저 일반 메모리에 버퍼를 할당한 뒤, 통신 헤더(EFI_SMM_COMMUNICATE_HEADER)와 실제 전달할 페이로드를 채워 넣어야 한다. 이때 헤더에는 호출할 대상을 식별하는 HeaderGuid와 데이터 크기인 MessageLength가 들어간다. 만약 OS 런타임 단계에서 OS 드라이버가 이 통신을 활용해야 한다면, 버퍼는 OS에 의해 보호되거나 EfiRuntimeServicesData 타입으로 사전에 예약된 영역이어야 한다.
그 후 프로토콜의 Communicate() 함수를 호출하면, 하위의 SMM_CONTROL2_PROTOCOL 등을 통해 플랫폼별로 정의된 특정 메커니즘(x86의 경우 APM I/O 포트)을 사용하여 동기식 SMI를 유발한다. CPU가 SMM으로 진입하면 SMM 코어 디스패처가 전달받은 버퍼 헤더의 GUID를 확인해서 미리 등록된 핸들러를 찾고 버퍼 포인터를 넘겨주어 처리를 위임한다.
여기서 CommBuffer 자체는 SMRAM 외부에 있어 외부 코드에 의해 조작될 수 있으므로, SMM 핸들러는 전달받은 버퍼의 주소와 크기가 SMRAM 영역을 침범하지 않는지 SmmIsBufferOutsideSmmValid와 같은 함수로 확인해야 한다. 페이로드 안에 다른 메모리를 가리키는 포인터가 들어있는 경우에도 해당 주소의 유효성 검사를 거쳐야만 SMM 권한 상승 취약점을 막을 수 있다.
2. 프로토타입 및 구조체
EDK2에서 통신을 위해 사용되는 프로토콜 구조체와 통신 메시지 헤더 포맷이다.
2.1. 프로토콜 구조체 (MmCommunication.h)
// MdePkg/Include/Protocol/MmCommunication.h
// 이 프로토콜은 DXE 환경에 설치됨 (gBS를 통해 접근 가능)
typedef struct _EFI_MM_COMMUNICATION_PROTOCOL {
EFI_MM_COMMUNICATE Communicate;
} EFI_MM_COMMUNICATION_PROTOCOL;
// Communicate 함수 시그니처
typedef
EFI_STATUS
(EFIAPI *EFI_MM_COMMUNICATE)(
IN CONST EFI_MM_COMMUNICATION_PROTOCOL *This,
IN OUT VOID *CommBuffer, // 공유 통신 버퍼
IN OUT UINTN *CommSize OPTIONAL // 버퍼 전체 크기
);
이 프로토콜은 DXE 단계에서 설치되며, Boot Services(gBS)를 통해 접근할 수 있다.
2.2. 통신 메시지 헤더 포맷 (MmCommunication.h)
// MdePkg/Include/Protocol/MmCommunication.h
// CommBuffer의 맨 앞부분은 반드시 이 구조체 형태를 띄어야 함
typedef struct {
EFI_GUID HeaderGuid; // 수신할 SMM/MM 핸들러를 식별하는 고유 GUID
UINTN MessageLength; // 헤더 뒤에 따라오는 실제 Payload 데이터의 크기
UINT8 Data[1]; // 실제 Payload가 시작되는 가변 길이 배열 시작점
} EFI_MM_COMMUNICATE_HEADER;
할당된 CommBuffer의 맨 앞부분은 반드시 위의 구조체 형태를 띄어야 한다.
3. 자주 쓰이는 함수/패턴
통신은 송신자(DXE 환경)와 수신자(SMM 환경)의 상호작용으로 이루어진다. 자주 등장하는 3단계 패턴이다.
3.1. [DXE/OS 측] 통신 버퍼(CommBuffer) 구성 및 페이로드 준비
#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/MmCommunication.h>
// 전송할 커스텀 데이터 페이로드
typedef struct {
UINT32 Command;
UINT32 Value;
EFI_STATUS ReturnStatus;
} MY_SMM_PAYLOAD;
EFI_STATUS PrepareAndSendToSmm() {
EFI_STATUS Status;
EFI_MM_COMMUNICATE_HEADER *CommHeader;
MY_SMM_PAYLOAD *Payload;
UINTN BufferSize;
// 헤더 크기 + 페이로드 크기만큼 버퍼 계산
// OFFSET_OF 매크로를 사용하여 구조체 패딩 문제를 안전하게 처리
BufferSize = OFFSET_OF(EFI_MM_COMMUNICATE_HEADER, Data) + sizeof(MY_SMM_PAYLOAD);
// 반드시 SMM 외부(일반 메모리)에 할당
gBS->AllocatePool(EfiRuntimeServicesData, BufferSize, (VOID **)&CommHeader);
// 헤더 세팅: 누구에게 보낼 것인가?
CopyGuid(&CommHeader->HeaderGuid, &gMyCustomSmiHandlerGuid);
CommHeader->MessageLength = sizeof(MY_SMM_PAYLOAD);
// 페이로드 세팅
Payload = (MY_SMM_PAYLOAD *)CommHeader->Data;
Payload->Command = 1; // 예: 1=비밀번호 검증 등
Payload->Value = 1234;
송신자는 SMM 핸들러가 식별할 수 있도록 EFI_SMM_COMMUNICATE_HEADER 규칙에 맞게 메모리를 할당하고 데이터를 쓴다. 이때 새로 할당된 메모리에 기존 쓰레기값이 남아 오작동을 유발할 수 있기 때문에 ZeroMem을 통해 버퍼를 초기화하는 과정이 추가로 필요하다.
3.2. [DXE/OS 측] Communicate() 호출로 SMI 유발
EFI_MM_COMMUNICATION_PROTOCOL *SmmComm;
// SMM Communication 프로토콜 찾기
Status = gBS->LocateProtocol(&gEfiSmmCommunicationProtocolGuid, NULL, (VOID **)&SmmComm);
if (!EFI_ERROR(Status)) {
// SMI 유발! (CPU가 SMM으로 점프하여 핸들러 실행)
Status = SmmComm->Communicate(SmmComm, CommHeader, &BufferSize);
// SMM 처리가 끝나고 복귀함. 처리 결과 확인
if (Payload->ReturnStatus == EFI_SUCCESS) {
// SMM이 작업을 성공적으로 수행함
}
}
gBS->FreePool(CommHeader);
return Status;
}
준비된 버퍼를 SMM 통신 프로토콜에 실어 보낸다. 함수가 반환될 때까지 시스템은 SMM 환경에서 동기적으로 블로킹된다.
3.3. [SMM 측] 수신 데이터 검증
#include <Library/SmmMemLib.h> // SmmIsBufferOutsideSmmValid 사용
EFI_STATUS
EFIAPI
MySmiHandler (
IN EFI_HANDLE DispatchHandle,
IN CONST VOID *Context OPTIONAL,
IN OUT VOID *CommBuffer OPTIONAL,
IN OUT UINTN *CommBufferSize OPTIONAL
)
{
MY_SMM_PAYLOAD *Payload;
// 1. 버퍼가 유효한지 검사
if (CommBuffer == NULL || CommBufferSize == NULL) {
return EFI_SUCCESS; // SMM은 보통 SUCCESS를 반환하고, 페이로드 내부에 에러를 씁니다.
}
// 2. CommBuffer 전체가 SMRAM 외부 영역인지 완벽히 검증 (EDK2 필수 규약)
if (!SmmIsBufferOutsideSmmValid((UINTN)CommBuffer, *CommBufferSize)) {
// 해커의 공격 시도! 즉시 반환
return EFI_SUCCESS;
}
// 3. 안전이 확인된 후 페이로드 파싱 및 처리
Payload = (MY_SMM_PAYLOAD *)((EFI_MM_COMMUNICATE_HEADER *)CommBuffer)->Data;
if (Payload->Command == 1) {
// SMM 권한으로 하드웨어 제어 등 보안 로직 수행
Payload->ReturnStatus = EFI_SUCCESS; // 결과를 버퍼에 다시 씀
}
return EFI_SUCCESS;
}
악의적인 해커가 CommBuffer 포인터를 SMRAM 내부 주소로 조작해 던지면 SMM이 자기 자신의 보안 메모리를 덮어쓰는 Confused Deputy 공격이 발생할 수 있기 때문에, SMM 내부의 핸들러는 외부에서 넘어온 CommBuffer 포인터를 절대 그대로 신뢰해서는 안 된다. 따라서 버퍼 포인터 사용 전 철저한 검증을 해야한다.
'CS > Graduation project' 카테고리의 다른 글
| [UEFI] gRT(Runtime Services)란? (0) | 2026.02.24 |
|---|---|
| [UEFI] gBS(Boot Services)란? (0) | 2026.02.24 |
| [UEFI] CommBuffer 검증 우회 (Ring -2) (0) | 2026.02.24 |
| [UEFI] BRLY-2022-016 분석 및 재현 (CommBuffer) (0) | 2026.02.23 |
| CVE-2021-42059 분석 및 실습 (0) | 2026.02.19 |