haehet

[Linux kernel] 링 권한 구조와 SMM(System Management Mode) 본문

Linux kernel

[Linux kernel] 링 권한 구조와 SMM(System Management Mode)

haehet 2026. 1. 6. 20:21

이번 글에서는 x86의 보호 링(Protection Ring) 구조를 정리하고 Ring -2에 있는 SMM(System Management Mode)에 대해 정리해보겠다.

 

1. 링 권한 구조

 x86 아키텍처는 코드가 실행될 수 있는 권한 수준을 Protection Ring(Ring 0~3) 으로 나눠 관리한다. 숫자가 낮을수록 권한이 높고, 리눅스에서는 보통 다음처럼 사용한다.

● Ring 3 (User mode): 일반 사용자 프로세스

● Ring 0 (Kernel mode): 리눅스 커널

유저 프로세스는 privileged instruction을 실행할 수 없고 커널 메모리에도 직접 접근할 수 없다. 커널 기능이 필요하면 시스템 콜을 통해 Ring 0 경계로 진입한다. 

 

Protection ring을 나타낸 그림 https://en.wikipedia.org/wiki/Protection_ring)

보통 Ring 0까지만 다루지만 더 아래의 권한도 존재한다. 가상화가 도입되면서 하이퍼바이저 계층(Ring -1)이 등장했고 그보다 더 아래에는 펌웨어 영역에서 동작하는 SMM(System Management Mode, Ring -2) 이 존재한다. (사실 ring -3도 있는 것 같긴한데 뇌절이라 zz 설명 안하겠다.)

2. Ring -1, Ring -2

2.1 Ring -1: Hypervisor

Intel VT-x / AMD-V 같은 하드웨어 가상화 확장에서는 하이퍼바이저가 커널보다 더 낮은 계층에서 실행된다.

 

2.2 Ring -2: SMM(System Management Mode)

시스템 관리 모드(SMM)는 전력 관리, 하드웨어 제어 등 시스템 전체 기능을 처리하기 위한 특수 목적 운영 모드이다. SMM은 펌웨어(BIOS 또는 UEFI)에 의해 사용되며 시스템 관리 인터럽트(SMI)를 통해서 진입할 수 있다.

 

3. SMM(System Management Mode) 동작

SMM에 진입하면 CPU는 현재 실행 중인 OS 문맥을 모두 저장한 뒤 SMRAM(System Management RAM) 이라는 별도의 메모리 영역으로 진입한다. SMM 내에서 코드는 시스템 관리 인터럽트(SMI)에 대한 핸들러로 동작하며 작업이 끝나면 RSM(Return from SMI) 명령을 통해 다시 원래 실행 상태로 복귀한다.

 

부팅 로그를 보면 다음과 같이 RAM에 SMRAM 영역이 생기는 것을 확인 가능하다.

Q35SmramAtDefaultSmbaseInitialization: SMRAM at default SMBASE found
DetectSmbiosVersion: SMBIOS version from QEMU: 0x0208
NegotiateSmiFeatures: using SMI broadcast
NegotiateSmiFeatures: CPU hotplug with SMI negotiated
NegotiateSmiFeatures: CPU hot-unplug with SMI negotiated
SmbiosCreateTable() re-allocate SMBIOS 32-bit table
SMM IPL opened SMRAM window
SMM IPL found SMRAM window 7F001000 - 7FFFFFFF
SMRAM attributes: 0000000000000008
SMM IPL loading SMM Core at SMRAM address 7FFEE000
SMM IPL calling SMM Core at SMRAM address 7FFF1F9F
Loading SMM driver at 0x0007FFE2000 EntryPoint=0x0007FFE4274 CpuIo2Smm.efi
Loading SMM driver at 0x0007FFD8000 EntryPoint=0x0007FFDB706 SmmLockBox.efi
Loading SMM driver at 0x0007FFBE000 EntryPoint=0x0007FFC5CF8 PiSmmCpuDxeSmm.efi
SMRR Base: 0x7F000000, SMRR Size: 0x1000000
SMRAM TileSize = 0x00002000 (0x00001000, 0x00001000)
SMRAM SaveState Buffer (0x7FFB6000, 0x00008000)
CPU[000]  APIC ID=0000  SMBASE=7FFAE000  SaveState=7FFBDC00  Size=00000400
Initialize IDT IST field for SMM Stack Guard
SMM IPL registered SMM Entry Point address 7FFF5DD7
SMM CPU Module exit from SMRAM with EFI_SUCCESS
SMM IPL closed SMRAM window
Loading SMM driver at 0x0007FF92000 EntryPoint=0x0007FF95864 FvbServicesSmm.efi
Installing QEMU flash SMM FVB
Loading SMM driver at 0x0007FF7B000 EntryPoint=0x0007FF82BE0 VariableSmm.efi
Loading SMM driver at 0x0007FEDE000 EntryPoint=0x0007FEE16A6 CpuHotplugSmm.efi
Error: SMM image at 0007FEDE000 start failed: Unsupported
Loading SMM driver at 0x0007FEDB000 EntryPoint=0x0007FEE0A6C SmmFaultTolerantWriteDxe.efi
[Variable]SMM_END_OF_DXE is signaled
SMM IPL locked SMRAM window

4. SMM과 통신하기 

SMM과 소통하기 위해서는 Communication buffer에 값을 써두면 된다. (guid는 드라이버 마다 다르다.)

typedef struct {
  EFI_GUID  HeaderGuid;
  UINTN     MessageLength;
  UINT8     Data[1];
} EFI_MM_COMMUNICATE_HEADER;

보통 소통을 도와주기 위한 래퍼함수 (communicate)가 존재한다. 하지만 특정 CTF문제에서는 그런 기능을 제공하지 않을 때가 많다. 따라서 직접 root 권한에서 소통을 하는 방법을 알아보자.

 

위에서 말했듯이 SMM과 소통을 하기 위해서는 communication buffer에 값을 써둬야 한다. 만약 래퍼가 없으면 우리가 직접 communication buffer로 쓸 장소를 지정해야 한다.

#define SMM_CORE_PRIVATE_DATA_SIGNATURE SIGNATURE_32 ('s', 'm', 'm', 'c')
typedef struct {
  UINTN                  Signature;
  EFI_HANDLE             SmmIplImageHandle;
  UINTN                  SmramRangeCount;
  EFI_SMRAM_DESCRIPTOR   *SmramRanges;
  EFI_SMM_ENTRY_POINT    SmmEntryPoint;
  BOOLEAN                SmmEntryPointRegistered;
  BOOLEAN                InSmm;
  EFI_SMM_SYSTEM_TABLE2  *Smst;
  VOID                   *CommunicationBuffer;
  UINTN                  BufferSize;
  EFI_STATUS             ReturnStatus;
  EFI_PHYSICAL_ADDRESS   PiSmmCoreImageBase;
  UINT64                 PiSmmCoreImageSize;
  EFI_PHYSICAL_ADDRESS   PiSmmCoreEntryPoint;
} SMM_CORE_PRIVATE_DATA;

Communication buffer를 지정하는 방법은 먼저 메모리에서 다음 구조체를 찾아 준다. (CTF 관점에서 설명할 거기 때문에 qemu를 기준으로 설명하겠다, 만약 qemu 환경이 아니라면 /dev/mem을 open 후 메모리를 매핑 받아가면서 smmc signature를 찾아도 될듯하다.)

 

먼저 주로 SMM이 할당되는 영역의 물리 주소는 고정이기 때문에 부팅 로그 등을 확인해서 주소를 찾아준다. 그리고 shell에서 cat /proc/iomem을 열어서 할당을 확인해본다.

# cat /proc/iomem
00000000-00000fff : Reserved
00001000-0002ffff : System RAM
00030000-0004ffff : Reserved
00050000-0009ffff : System RAM
000f0000-000fffff : System ROM
00100000-7e8eefff : System RAM
  0ea00000-0f3fffff : Kernel code
  0f400000-0f6a8fff : Kernel rodata
  0f800000-0f8ea67f : Kernel data
  0fadb000-0fbfffff : Kernel bss
7e8ef000-7eaeefff : Reserved
7eaef000-7eb74fff : System RAM
7eb75000-7eb7efff : ACPI Tables
7eb7f000-7ebfefff : ACPI Non-volatile Storage
7ebff000-7effffff : System RAM
7f000000-7fffffff : Reserved
b0000000-bfffffff : Reserved
c0040000-c005ffff : 0000:00:01.0
c0060000-c0060fff : 0000:00:1f.2
fffc0000-ffffffff : 0000:00:01.0
100000000-17fffffff : System RAM

그 다음에는 SMM과 관련된 영역 (Reserved가 적혀 있으면서 디버깅 로그에서 확인)을 qemu-monitor에서 dump해준다. 

haehet@haehet:~/wargame$ nc localhost 4444
QEMU 8.0.2 monitor - type 'help' for more information
(qemu) pmemsave 0x7e000000 0x1000000 dump.bin
pmemsave 0x7e000000 0x1000000 dump.bin
(qemu) q
haehet@haehet:~/wargame$ grep -oba "smmc" ./dump.bin | awk -F: '{printf "0x%x: %s\n", $1, $2}'
0x5b9400: smmc
0xace380: smmc

 

그러면 다음과 같이 smmc signature를 찾을 수 있다. 그 후 다음과 같이 해당 영역에 할당 후 내부 필드를 채워주면된다. 

(iopl 명령어는 유저모드에서 outb 같은 저수준 I/O 명령어를 사용하기 위함이다.)

if (iopl(3) != 0) {perror("iopl(3)");} 
int fd = open("/dev/mem", O_RDWR | O_SYNC);
uint64_t smmc_page_phys = SMMC_PHYS_ADDR & ~0xFFFULL;
uint8_t *smmc_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,MAP_SHARED, fd, smmc_page_phys);
uint8_t *smmc = smmc_page + (SMMC_PHYS_ADDR & 0xFFFULL);
*(uint64_t *)(smmc + 0x38) = COMM_PHYS_ADDR;  // CommunicationBuffer
*(uint64_t *)(smmc + 0x40) = total_sz;        // CommunicationBufferSize

커뮤니케이션 버퍼는 다음과 같이 SMM이 접근 가능한 메모리에 mmap을 해주면된다.

#define COMM_PHYS_ADDR 0x7e8ef000ULL
uint8_t *comm = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, COMM_PHYS_ADDR);

 

그 후 outb를 통해 예약된 포트에 값을 써서 trigger하면 된다.

#define SW_SMI_PORT   0xB2
#define SW_SMI_DATA   0xB3
static inline void smi(void) {
    outb(0, SW_SMI_DATA);
    outb(0, SW_SMI_PORT);
}

 

이번 글에서는 리눅스의 링 권한 구조와 SMM에 대해 알아보았다. 조만간 UFEI랑 리눅스, 컴퓨터 부팅과정과 관련된 공부를해야겠다. 

 

 

reference: https://en.wikipedia.org/wiki/Protection_ring

https://web.archive.org/web/20160304033404/http://www.stewin.org/slides/pstewin-SPRING6-EvaluatingRing-3Rootkits.pdf

https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

https://www.willsroot.io/2023/08/smm-diary-writeup.html

https://towerofhanoi.it/writeups/2022-08-15-uiuctf-2022-smm-cowsay/

https://raw.githubusercontent.com/tianocore/edk2/master/MdeModulePkg/Core/PiSmmCore/PiSmmCorePrivateData.h

https://raw.githubusercontent.com/tianocore/edk2/master/MdePkg/Include/Protocol/SmmCommunication.h