방망이
[프로젝트]UEFI DXE 바이너리 취약점 분석기 6주차 - DXE Dispatcher 취약점 및 Ghidra Script 작성 본문
2026_졸업프로젝트
[프로젝트]UEFI DXE 바이너리 취약점 분석기 6주차 - DXE Dispatcher 취약점 및 Ghidra Script 작성
망구입니다 2026. 2. 26. 20:44[DXE Dispatcher & DEPEX Remind]
- Apriori 파일
- DEPEX 조건과 무관하게 최우선으로 실행되어야 하는 드라이버들이 모여있는 파일.
- Dispatcher는 Apriori 파일에 있는 드라이버부터 실행.
- DXE Dispatcher 로직
- DXE Dispatcher는 아직 실행되지 않은 드라이버들의 목록을 가지고 있음.
- 목록들을 쭉 돌면서 각 드라이버의 DEPEX가 현재 TRUE인지 평가. (즉, 필요한 프로토콜이 메모리에 다 올라왔는지 확인)
- 조건이 충족된 드라이버는 즉시 실행시킴.
- 방금 실행된 드라이버가 새로운 프로토콜을 메모리에 설치.
- Dispatcher는 다시 목록의 처음으로 돌아가서 조건이 안 맞아서 대기중이던 드라이버들의 DEPEX를 다시 평가하면서 실행.
그렇다면 질문!
- Q. Dispatcher가 확인하는 '실행 되지 않은 드라이버 리스트'가 순서가 있고, 그 순서에 맞게 차례대로 돌면서 확인하는것인가?
- A. 리스트에는 뚜렷한 순서가 있다!
- Dispatcher는 메모리에 올라온 드라이버들을 무작위로 고르는 게 아니라, 펌웨어 볼륨(FV)에서 파싱(Parsing)되어 발견된 물리적 순서대로 구성된 리스트를 위에서부터 아래로 차례대로 검사해 내려간다.
- Q. 하나의 드라이버가 설치되면 다시 처음으로 돌아가서 다시 확인하는건지, 한바퀴를 돌고 처음으로 돌아가는건지?
- A. 한바퀴를 돌고 처음으로 돌아간다!
- Dispatcher는 실행되지 않은 드라이버 목록을 훑어 내려가면서 DEPEX 조건이 TRUE인 드라이버를 바로 실행하는 것이 아니라 Scheduled Queue에 넣어둠.
- 목록을 모두 훑고 나면 (한바퀴 돌고 나면) Scheduled Queue에 있는 드라이버를 순서대로 실행시킴.
- 새로 설치된 프로토콜을 가지고 다시 목록을 처음부터 훑으며 반복.
- A. 한바퀴를 돌고 처음으로 돌아간다!
- 정리
- Apriori 파일에 있는 드라이버를 Scheduled Queue에 push(Scheduled 상태). 나머지 DXE 드라이버들을 Discovered 상태로 올린 뒤 DEPEX를 평가해서 Dependent/Unrequested로 분류.
- Scheduled Queue에 있는 드라이버를 순서대로 로드 및 실행하며, 해당 드라이버 실행 결과물인 프로토콜이 메모리에 설치.
- 그 설치된 프로토콜을 가지고 Dependent 상태의 드라이버 목록을 Dispatcher가 돌면서 조건(DEPEX)이 충족된 드라이버를 찾아서 Scheduled Queue에 push.
- 목록을 한바퀴 돌고 나서 Scheduled Queue에 있는 드라이버들을 순서대로 드레인.
- 만약, Scheduled Queue에 들어간 드라이버가 하나라도 있다면 2 - 4 반복, 없다면 종료.
//Dispatcher 시작 전 : 각 FV 처리 (루프 밖)
foreach (FV in FirmwareVolumes) { // 각 FV에 대해
if (AprioriFile = FindAprioriInFV(FV)) { // Apriori 파일이 존재하면
foreach (Driver in AprioriFile) { // Apriori 파일에 있는 모든 드라이버에 대해
Driver->State = SCHEDULED; // 상태를 Scheduled로 승격
Scheduled Queue에 Driver를 push
}
}
나머지 드라이버는 Discovered 상태로 변경
DEPEX를 평가하여 Dependent/Unrequested 분류
}
// Dispatcher 메인 루프
BOOLEAN ReadyToRun = TRUE;
do {
while(!IsScheduledQueueEmpty()) { // Scheduled Queue에 있는 모든 드라이버가 드레인 될 때까지
현재 Scheduled Queue에 있는 모든 드라이버를 순서대로 드레인(로드+실행(Initialized 상태로 변경)).
}
ReadyToRun = FALSE; // 모든 드라이버가 실행된 경우 정지하기 위해
foreach (Dependent list (Dependent 상태의 드라이버 리스트)에 있는 각 드라이버에 대해) {
if (DEPEX 조건이 만족되면) {
그 드라이버를 찾아 Scheduled Queue에 push.
ReadyToRun = TRUE;
}
}
} while (ReadyToRun);
[관련 취약점]
- 무한 루프
- 드라이버 A는 B를 기다리고, 드라이버 B는 A를 기다리게 조작하여 Dispatcher가 영원히 끝나지 않는 대기 상태에 빠지게 함.
- 이 경우 우리가 타겟으로 하는 EDK2 v21.02 에서는 내부 감지 로직이 있음. → 취약점 탐지 스크립트로 만들 필요 X
- Apriori 파일 무결성 검증
- Apriori 파일은 Secure Boot가 파일의 무결성을 보장하나, 그 무결성이란 디지털 서명을 검증하는 절차로 보장되는 것이고, 만약 그 디지털 서명이 도용되는 등의 방식으로 공격자가 Apriori 파일에 악성파일을 심어놓게 되면 문제가 생김.
- EDK2의 최신 버전에서도 EDK2 v21.02에 비해 보안 강화는 되었느나, Apriori 파일에 대한 직접적인 보안 강화는 X
- DEPEX 길이 체크
- DEPEX를 계산할 때 사용하는 스택에는 최대 256바이트가 들어갈 수 있음. DEPEX 바이트열의 최대 길이를 제한하여 스택 오버플로우를 방지.
- Opcode
Opcdoe 이름 바이트 수 0x00 BEFORE <File Name GUID> 17바이트 0x01 AFTER <File Name GUID> 17바이트 0x02 PUSH <Protocol GUID> 17바이트 0x03 AND 1바이트 0x04 OR 1바이트 0x05 NOT 1바이트 0x06 TRUE 1바이트 0x07 FASLE 1바이트 0x08 END 1바이트 0x09 SOR 1바이트 - 예시
- push GUID1
push GUID2
...
push GUID15
AND
AND
...
AND
END
→ (17바이트 x 15) + (1바이트 x 14) + (17바이트 x 1) = 270바이트
→ 스택 오버플로우 발생!
- push GUID1
[Ghidra Script 작성]
먼저 DEPEX 길이를 체크하는 스크립트를 작성하였다.
//@category FirmwareAnalysis
//@keybinding
//@menupath Tools.Firmware.DEPEX Length Check
//@description DEPEX length overflow vulnerability detector (EDK2 MAX=256)
import ghidra.app.script.GhidraScript;
import ghidra.program.model.mem.*;
import ghidra.program.model.address.*;
import java.util.List;
import java.util.ArrayList;
public class DEPEXLength_test extends GhidraScript {
private static final int MAX_DEPEX_SIZE = 256;
private int vulnerableCount = 0;
private int totalDepexCount = 0;
private static final int EFI_SECTION_DXE_DEPEX = 0x13;
private static final int EFI_SECTION_PEI_DEPEX = 0x1B;
private static final int EFI_SECTION_MM_DEPEX = 0x1C;
@Override
protected void run() throws Exception {
// Verify if a program is loaded in the current Ghidra workspace
if (currentProgram == null) {
println("[ERROR] No program loaded. Please load a firmware file first.");
return;
}
println("=== DEPEX Length Verification (EDK2/UEFI PI Spec) ===");
println("[INFO] MAX_DEPEX_SIZE: " + MAX_DEPEX_SIZE + " bytes");
println("[INFO] DEPEX Types: DXE=0x13, PEI=0x1B, MM=0x1C");
// Iterate through all initialized memory blocks
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
if (block.isInitialized() && !block.isVolatile()) {
scanMemoryBlock(block);
}
}
// Output final scan results
println("\n=== Scan Results ===");
println("Total DEPEX sections found: " + totalDepexCount);
println("Vulnerable DEPEX sections: " + vulnerableCount);
if (vulnerableCount > 0) {
popup("[ALERT] " + vulnerableCount + " DEPEX overflow vulnerabilities found!");
} else {
println("[INFO] All discovered DEPEX sections comply with length constraints.");
}
}
// Read the entire memory block safely
private byte[] readBlock(MemoryBlock block) {
byte[] data = new byte[(int)block.getSize()];
try {
block.getBytes(block.getStart(), data);
} catch (Exception e) {
println("[ERROR] Memory read failed for block " + block.getName() + ": " + e.getMessage());
}
return data;
}
// Scan the block to locate and verify DEPEX sections
private void scanMemoryBlock(MemoryBlock block) {
try {
byte[] data = readBlock(block);
List<Integer> depexOffsets = findRealDepexSections(data);
for (int offset : depexOffsets) {
totalDepexCount++;
int realLength = getDepexLength(data, offset);
if (realLength > MAX_DEPEX_SIZE) {
vulnerableCount++;
reportVulnerability(block.getStart(), offset, realLength);
} else {
printf("[OK] DEPEX safe: 0x%s+0x%x (%d bytes)\n",
block.getStart().toString(), offset, realLength);
}
}
} catch (Exception e) {
println("[ERROR] Exception during scanning block " + block.getName() + ": " + e.getMessage());
}
}
// Parse EFI_COMMON_SECTION_HEADER to extract authentic DEPEX offsets
private List<Integer> findRealDepexSections(byte[] data) {
List<Integer> realDepexOffsets = new ArrayList<>();
// UEFI Specification dictates 4-byte alignment for sections
for (int offset = 0; offset < data.length - 4; offset += 4) {
// 1. Extract Section Type (Byte 3)
int type = data[offset + 3] & 0xFF;
// Validate against correct UEFI PI Spec DEPEX section types
if (type == EFI_SECTION_DXE_DEPEX ||
type == EFI_SECTION_PEI_DEPEX ||
type == EFI_SECTION_MM_DEPEX) {
// 2. Calculate Section Size (Bytes 0-2, Little Endian, 24-bit)
int size = (data[offset] & 0xFF) |
((data[offset + 1] & 0xFF) << 8) |
((data[offset + 2] & 0xFF) << 16);
// 3. Size Integrity Check (avoid 0 and huge sizes)
if (size >= 8 && size <= 0xFFFFFF && (offset + size) <= data.length) {
// 4. Validate the first opcode to prevent false positives
if (offset + 8 < data.length) { // Header=4 + min DEPEX
int firstOpcode = data[offset + 4] & 0xFF;
// Valid starting opcodes: BEFORE(0x00), AFTER(0x01), PUSH(0x02), END(0x08)
if (firstOpcode == 0x00 || firstOpcode == 0x01 ||
firstOpcode == 0x02 || firstOpcode == 0x08) {
realDepexOffsets.add(offset + 4);
}
}
}
}
}
return realDepexOffsets;
}
// Calculate the actual length of the DEPEX up to the EFI_DEP_END opcode (0x08)
private int getDepexLength(byte[] data, int offset) {
for (int i = offset; i < data.length; i++) {
if (data[i] == 0x08) { // EFI_DEP_END opcode
return i - offset + 1;
}
}
return data.length - offset; // Fallback if no END found
}
// Log vulnerabilities and create bookmarks in the Ghidra UI
private void reportVulnerability(Address baseAddr, int offset, int length) {
Address vulnAddr = baseAddr.add(offset);
printf("[VULNERABILITY] DEPEX overflow at: 0x%s (Length: %d > %d bytes)\n",
vulnAddr.toString(), length, MAX_DEPEX_SIZE);
try {
createBookmark(vulnAddr, "DEPEX_Overflow",
String.format("Length exceeded EDK2 limit: %d/%d bytes", length, MAX_DEPEX_SIZE));
} catch (Exception e) {
println("[ERROR] Failed to create bookmark at " + vulnAddr.toString() + ": " + e.getMessage());
}
}
}
스크립트 성능 확인을 위해 임의의 드라이버의 DEPEX를 아래와 같이 조작하였다. PUSH <GUID> 형태가 총 17개, AND Opcode가 총 16개, END Opcode 1개로 이루어져 있으므로 총 306 byte이다. 따라서 스택 오버플로우가 발생하는 DEPEX이다.

DEPEX를 조작하지 않은 드라이버를 추출한 origin_file.ffs 파일에 스크립트를 실행해본 결과 아래와 같은 결과를 볼 수 있다. 총 DEPEX가 1개이고, 탐지된 DEPEX가 0개로 잘 나타난다.

이번에는 DEPEX를 조작한 드라이버를 추출한 vuln_file.ffs 파일에 스크립트를 실행해본 결과 아래와 같은 결과를 볼 수 있다. 총 DEPEX가 1개이고, 탐지된 DEPEX가 1개로 잘 나타나고, DEPEX 길이가 306 bytes인 것도 잘 계산되어 나타난다.

여러 드라이버가 들어있는 환경에서도 잘 실행되는지 확인해보았으나, DEPEX를 잘 잡아주지 못한다는 것을 알게 되었다.
스크립트에서 DEPEX를 가져오는 방식을 수정해야 할 것 같다.

'2026_졸업프로젝트' 카테고리의 다른 글
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 8주차 - Ghidra Script 로직 보강 (0) | 2026.03.16 |
|---|---|
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 7주차 - Ghidra Script 작성 (0) | 2026.02.28 |
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 5주차 - DEPEX 확인법/Ghidra Script 기본 틀 (0) | 2026.02.26 |
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 4주차 - Ghidra Script 실습 (0) | 2026.02.26 |
| [프로젝트]UEFI DXE 바이너리 취약점 분석기 3주차 - 방향성 탐구 (0) | 2026.02.26 |
