X86-64 환경의 edk2 펌웨어에서 주요 테이블과 함수들을 추출하고, Ghidra의 P-CODE 상에서 어떻게 표현되는지 정리해봤다.
동일한 X86-64 아키텍처라 하더라도 바이너리에 따라 P-code에서 사용되는 레지스터나 임시 변수명은 달라질 수 있다. 하지만 구조체의 오프셋과 핵심 명령어는 변하지 않는다는 점을 인지!!! 해야한다. (코드에서 변하지 않는 요소들은 볼드 처리해 뒀으니 참고)
분석 대상으로 삼은 것은 DXE 및 SMM 디스패처 취약점, 메모리 커럽션, NVRAM 변수 취약점, SMM Callout 취약점 등을 다룰 때 자주 등장하거나 필수적으로 사용되는 gBS와 같은 시스템 서비스와 함수들이다. 그 외 필요한 게 있을 때마다 추가하려고 한다..
0. 기드라 설정
ghidra-firmware-utils 스크립트를 쓰면 기본적인 함수 이름과 GUID는 잡히지만, 데이터 타입이 지정되지 않거나 포인터가 누락되는 경우가 있다. 따라서 추가적인 설정을 통해 기드라가 코드를 구조체 단위로 해석하게 만들면 편하다.
0.1. __ModuleEntryPoint 수정
// 변경 전
undefined8 _ModuleEntryPoint(longlong ImageHandle,longlong SystemTable)
// 변경 후
EFI_STATUS _ModuleEntryPoint(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
모든 UEFI 드라이버의 시작 함수는 인자가 딱 두 개로 고정되어 있다. 따라서 엔트리 포인트 인자의 타입과 이름을 먼저 입혀주면, 함수 내부의 모든 변수 타입이 자동으로 정리된다.
0.2. 글로벌 포인터 이름 지정
// 변경 전
DAT_00001a58 = SystemTable->BootServices;
_DAT_00001a48 = SystemTable->RuntimeServices;
_DAT_00001a38 = SystemTable;
// 변경 후
gBS = SystemTable->BootServices;
gRT = SystemTable->RuntimeServices;
gST = SystemTable;
- SystemTable + 0x60 위치의 값을 저장하는 변수: gBS (타입: EFI_BOOT_SERVICES *)
- SystemTable + 0x58 위치의 값을 저장하는 변수: gRT (타입: EFI_RUNTIME_SERVICES *)
- SystemTable 자체를 저장하는 변수: gST (타입: EFI_SYSTEM_TABLE *)
1. 테이블
1.1. gBS(EFI_BOOT_SERVICES)
DXE 단계의 모든 것을 담당하는 테이블으로, 메모리 할당, 프로토콜 검색 등 부팅 중 필요한 도구를 모아놓음
$U8f00:8 = INT_ADD RBX, 0x60:8
$U23e00:8 = LOAD ram($U8f00:8)
RAX = COPY $U23e00:8
UEFI 스펙상 SystemTable + 0x60 위치에 gBS 포인터가 존재한다. 따라서 P-CODE에서 INT_ADD 연산에 0x60 상수가 쓰이고, 그 결과값을 즉시 LOAD하는 지점이 gBS 주소를 얻는 지점이다.
1.2. gRT(EFI_RUNTIME_SERVICES)
OS가 부팅된 이후에도 살아남는 서비스로, 주로 NVRAM 변수 조작, 시간 설정 등을 담당
$U8f00:8 = INT_ADD RBX, 0x58:8
$U23e00:8 = LOAD ram($U8f00:8)
RAX = COPY $U23e00:8
UEFI 스펙상 SystemTable + 0x58 위치에 gRT 포인터가 존재한다.
2. gBS 함수
인자 전달 과정을 제외한 함수 호출 로직이 동일하다. 기드라 P-CODE에서 gBS 포인터에 특정 오프셋을 더하는 INT_ADD 연산을 찾고, 해당 주소에서 실제 주소를 읽어오는 LOAD, 그리고 최종 실행인 CALLIND가 발생하는 지점을 추적하여 각 함수를 식별하면 된다.
2.1. gBS -> AllocatePool
특정 타입의 메모리 풀을 원하는 크기만큼 할당받기 위한 함수이다.
$U8f00:8 = INT_ADD RBX, 64:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0xff9:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x40 (64바이트) 떨어진 곳에 AllocatePool 함수 주소가 저장되어 있다.
2.2. gBS -> FreePool
AllocatePool 등을 통해 할당받았던 메모리 풀을 시스템에 반환하기 위한 함수이다.
$U8f00:8 = INT_ADD RBX, 0x48:8
$U23e00:8 = **LOAD** ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1006:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x48 (72바이트) 떨어진 곳에 FreePool 함수 주소가 저장되어 있다.
2.3. gBS -> LocateProtocol
시스템 내에 설치된 프로토콜 중 특정 GUID와 일치하는 첫 번째 인터페이스를 찾기 위한 함수이다.
$U9100:8 = INT_ADD RBX, 0x140:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1024:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x140 (320바이트) 떨어진 곳에 LocateProtocol 함수 주소가 저장되어 있다
2.4. gBS -> HandleProtocol
특정 핸들이 지원하는 특정 프로토콜 인터페이스를 얻기 위한 함수이다.
$U9100:8 = INT_ADD RBX, 0x98:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x103b:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x98 (152바이트) 떨어진 곳에 HandleProtocol 함수 주소가 저장되어 있다.
2.5. gBS -> InstallProtocolInterface
새로운 프로토콜 인터페이스를 특정 핸들에 설치하기 위한 함수이다.
$U9100:8 = INT_ADD RBX, 0x80:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1053:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x80 (128바이트) 떨어진 곳에 InstallProtocolInterface 함수 주소가 저장되어 있다.
2.6. gBS -> CreateEvent
타이머나 대기 작업에 사용될 새로운 이벤트 객체를 생성하기 위한 함수이다.
$U8f00:8 = INT_ADD RBX, 0x50:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x106b:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x50 (80바이트) 떨어진 곳에 CreateEvent함수 주소가 저장되어 있다.
2.7. gBS -> SetTimer
생성된 이벤트에 대해 시간 간격이나 특정 주기에 맞춰 신호를 주도록 타이머를 설정하는 함수이다.
$U8f00:8 = INT_ADD RBX, 0x58:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x107e:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x58 (88바이트) 떨어진 곳에 SetTimer 함수 주소가 저장되어 있다.
2.8. gBS -> WaitForEvent
하나 이상의 이벤트가 신호 상태(Signal State)가 되어 작업이 완료될 때까지 실행을 멈추고 대기하는 함수이다.
$U8f00:8 = INT_ADD RBX, 0x60:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x108e:8
CALLIND $U79600:8
gBS 포인터 주소를 기준으로 0x60 (96바이트) 떨어진 곳에 WaitForEvent 함수 주소가 저장되어 있다.
3. gRT 함수
3.1. gRT -> GetVariable
NVRAM에 저장된 특정 UEFI 환경 변수의 값을 읽어오기 위한 함수이다
$U8f00:8 = INT_ADD RBX, 0x48:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0xed4:8
CALLIND $U79600:8
gRT 포인터 주소를 기준으로 0x48 (72바이트) 떨어진 곳에 GetVariable 함수 주소가 저장되어 있다.
3.2. gRT -> SetVariable
NVRAM에 새로운 UEFI 환경 변수를 생성하거나 기존 변수의 값을 수정(or 삭제)하기 위한 함수이다.
$U8f00:8 = INT_ADD RBX, 0x58:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0xeb6:8
CALLIND $U79600:8
gRT 포인터 주소를 기준으로 0x58 (88바이트) 떨어진 곳에 SetVariable 함수 주소가 저장되어 있다.
3.3. gRT -> GetTime
현재 시스템의 하드웨어 시간과 날짜(RTC)를 가져오기 위한 함수이다.
$U8f00:8 = INT_ADD RBX, 24:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0xe95:8
CALLIND $U79600:8
gRT 포인터 주소를 기준으로 24바이트 떨어진 곳에 GetTime 함수 주소가 저장되어 있다.
3.4. gRT -> ResetSystem
시스템을 재부팅하거나 종료(Cold Reset, Warm Reset, Shutdown 등)하기 위한 함수이다.
$U8f00:8 = INT_ADD RBX, 0x68:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0xee1:8
CALLIND $U79600:8
gRT 포인터 주소를 기준으로 0x68 (104바이트) 떨어진 곳에 ResetSystem 함수 주소가 저장되어 있다
4. gSmst 찾기
SMM 환경은 Ring -2로 격리된 환경인 만큼, 별도로 사용하는 시스템 관리 테이블이 있고 이를 gSmst(SMM System Management Table)라고 한다. 먼저 SMM 드라이버는 자신이 SMM 환경인지 확인하고, gSmst를 얻기 위해 다음과 같은 순서로 진행한다.
4.1. SmmBase2 프로토콜 위치 찾기
SmmBase2는 현재 실행 중인 코드가 SMM 내부인지 외부인지 판별하고, SMM 전용 서비스 테이블인 gSmst의 주소를 제공하는 게이트웨이의 역할
// 디컴파일된 C 코드
EVar1 = (*SystemTable->BootServices->LocateProtocol) (&EfiSmmBase2ProtocolGuid,(void *)0x0,local_20);
// P-CODE
// gBS 주소 값 불러오기
LAB_00001e28
$U9d00:8 = INT_ADD 0x60:8, R12
$U23e00:8 = LOAD ram($U9d00:8)
RAX = COPY $U23e00:8
// 인자 설정
$U9d00:8 = INT_ADD 40:8, RSP
R8 = COPY $U9d00:8
...
// LocateProtocol 함수 호출
$U9100:8 = INT_ADD RAX, 0x140:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1e4a:8
CALLIND $U79600:8
- gBS 주소 값 불러오기
- SystemTable(R12)에 0x60을 더한다.
- SystemTable + 0x60은 gBS(BootServices)가 위치하고 이를 RAX에 저장
- 인자 설정
- 스택 포인터에서 40바이트 만큼 떨어진 위치의 주소를 R8에 저장
- x64 호출 규약에서 R8은 세번 째 인자로, 즉 local_20 변수의 주소를 전달하는 과정
- LocateProtocol 함수 호출
- gBS(RAX)에 0x140을 더한다.
- gBS + 0x140은 LocateProtocol 함수가 위치한다.
이렇게 호출되면 EfiSmmBase2ProtocolGuid에 해당하는 SMM Base2 함수 테이블의 주소를 찾아서 local_20 변수에 반환한다. (함수의 성공 여부는 EVar1에 저장)
4.2. GetSmstLocation 함수 호출
SMMBase2 주소를 찾았으니 gSmst 테이블 주소를 찾으면 된다.
// 디컴파일된 C 코드
(**(code **)((longlong)local_20[0] + 8))(local_20[0],&DAT_00003140);
// P-CODE
RDX = COPY 0x3140:8
RCX = COPY RAX
// 코드 시작 부분
$U8f00:8 = INT_ADD RAX, 8:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1ead:8
CALLIND $U79600:8
SMMBase2 + 0x8 위치에는 gSmst의 주소를 가져오는 함수인 GetSmstLocation가 존재한다. 인자로 프로토콜 본인의 주소와 gSmst 주소를 받아올 메모리 위치(&DAT_00003140)를 전달한다. CALLIND를 통해 GetSmstLocation가 실행되면 gSmst의 실제 주소값이 DAT_00003140에 저장
5. gSmst
5.1. gSmst -> SmiHandlerRegister
특정 SMI(System Management Interrupt)가 발생했을 때 실행될 SMI 핸들러 함수를 시스템에 등록하는 함수이다. SMM 드라이버가 실제로 하는 일(공격이나 방어, 특정 하드웨어 제어 등)은 보통 이 핸들러 안에 담겨 있다.
$U9100:8 = INT_ADD RAX, 0xe0:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1d41:8
CALLIND $U79600:8
gSmst 주소를 기준으로 0xe0 (224바이트) 만큼 떨어진 위치에 SmiHandlerRegister 함수 주소가 존재한다.
5.2. gSmst -> SmmAllocatePool
SMM 전용 메모리 영역인 SMRAM 내에 특정 크기의 메모리 풀을 할당하는 함수이다. 일반적인 malloc과 비슷하지만, SMM 외부(Ring 0 이하)에서는 접근할 수 없는 안전한 공간을 확보할 때 사용한다.
$U8f00:8 = INT_ADD RAX, 0x50:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1cd2:8
CALLIND $U79600:8
gSmst 주소를 기준으로 0x50(80 바이트)만큼 떨어진 위치에 SmmAllocatePool 함수 주소가 존재한다.
5.3. gSmst -> SmmFreePool
SmmAllocatePool을 통해 할당받았던 SMM 전용 메모리를 시스템에 다시 반환하는 함수이다. 더 이상 필요 없는 메모리를 해제하여 리소스 낭비를 막을 때 사용한다.
$U8f00:8 = INT_ADD RAX, 0x58:8
$U23e00:8 = LOAD ram($U8f00:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1da0:8
CALLIND $U79600:8
gSmst 주소를 기준으로 0x58(88 바이트)만큼 떨어진 위치에 SmmFreePool 함수 주소가 존재한다.
5.4. gSmst -> SmmInstallProtocolInterface
SMM 환경 내에 새로운 프로토콜 인터페이스를 등록하여, 다른 SMM 드라이버들이 해당 기능을 사용할 수 있도록 공개하는 함수이다.
$U9100:8 = INT_ADD RAX, 0xa8:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1d21:8
CALLIND $U79600:8
gSmst 주소(RAX)를 기준으로 0xa8(168 바이트)만큼 떨어진 위치에 SmmInstallProtocolInterface 함수 주소가 존재한다.
5.5. gSmst -> SmmLocateProtocol
SMM 환경 내에서 특정 GUID에 해당하는 프로토콜 인터페이스를 찾아 그 주소를 반환해 주는 함수이다.
$U9100:8 = INT_ADD RAX, 0xd0:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x1d02:8
CALLIND $U79600:8
gSmst 주소(RAX)를 기준으로 0xd0 떨어진 위치에 SmmLocateProtocol 함수가 존재한다.
SMI 핸들러 타입 정의
// 변경 전
undefined8 FUN_00001ec4(undefined8 param_1,undefined8 param_2,ulonglong param_3)
// 변경 후
EFI_STATUS FUN_00001ec4(EFI_HANDLE DispatchHandle,void *Context,void *CommBuffer)
// 원래는 4개 정의
EFI_STATUS FUN_00001ec4(EFI_HANDLE DispatchHandle, void *Context, void *CommBuffer, UINTN *CommBufferSize)
SMI 핸들러에 대해 각 타입을 적용해준다.
SmmVariableProtocol
SMM 환경 내부에서 안전하게 NVRAM 변수를 읽고, 쓰고, 쿼리할 수 있도록 지원하는 프로토콜 역할을 하는 구조체이다.
(**(code **)(gSmst + 0xd0))(&EfiSmmVariableProtocolGuid,0,&DAT_000050a0);
$U9100:8 = INT_ADD RAX, 0xd0:8
$U23e00:8 = LOAD ram($U9100:8)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x26bb:8
CALLIND $U79600:8
gSmst + 0xd0은 앞서 본 SmmLocateProtocol가 위치한다. SmmLocateProtocol에 EfiSmmVariableProtocolGuid를 넘겨주고 반환 받는 주소가 DAT_000050a0에 저장되고, 따라서 DAT_000050a0가 SmmVariableProtocol을 가리키는 포인터 변수가 된다.
SmmGetVariable
SMM 환경 내에서 NVRAM(비휘발성 메모리)에 저장된 UEFI 변수를 읽어오는 함수이다. 일반 모드가 아닌 SMM 권한에서 실행되므로 보안이 필요한 설정값에 접근할 때 사용된다.
$U23e00:8 = LOAD ram(RAX)
$U79600:8 = COPY $U23e00:8
RSP = INT_SUB RSP, 8:8
STORE ram(RSP), 0x213d:8
CALLIND $U79600:8
SmmVariableProtocol의 0번 오프셋이 SmmGetVariable 함수의 주소이다.
SmmIsBufferOutsideSmmValid
OS에서 전달된 CommBuffer가 SMM 보호 영역(SMRAM) 밖에 안전하게 위치하는지 검사하는 함수이다. SMM 내부의 민감한 데이터를 외부로 유출하거나, 외부의 악의적인 코드가 SMM 영역을 덮어쓰는 SMM Callout 공격을 방어함
SmmIsBufferOutsideSmmValid의 P-CODE를 확인해보려다가 컴파일러 최적화로 인해 SMI 핸들러 내부에 인라이닝된 걸 발견했다. 이러한 경우 단순한 함수 호출 추적 방식으로는 보안 검사 여부를 판단할 수 없을 것 같음. (일단 P-CODE가 너무 길어..) Reaching Definition을 사용한다고 얼핏.. 들었던 것 같은데, 내가 뭘 할 수 있을지는 더 공부해 봐야겠다 ~,~
'CS > Graduation project' 카테고리의 다른 글
| [UEFI] SMI 발생 원리 및 처리 방법 (0) | 2026.03.10 |
|---|---|
| Taint Analysis 개념 정리 (0) | 2026.03.01 |
| [UEFI] gRT(Runtime Services)란? (0) | 2026.02.24 |
| [UEFI] gBS(Boot Services)란? (0) | 2026.02.24 |
| [UEFI] SMM/MM Communication Protocol이란? (0) | 2026.02.24 |