Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

방망이

[프로젝트]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 로직

  1. DXE Dispatcher는 아직 실행되지 않은 드라이버들의 목록을 가지고 있음.
  2. 목록들을 쭉 돌면서 각 드라이버의 DEPEX가 현재 TRUE인지 평가. (즉, 필요한 프로토콜이 메모리에 다 올라왔는지 확인)
  3. 조건이 충족된 드라이버는 즉시 실행시킴.
  4. 방금 실행된 드라이버가 새로운 프로토콜을 메모리에 설치.
  5. Dispatcher는 다시 목록의 처음으로 돌아가서 조건이 안 맞아서 대기중이던 드라이버들의 DEPEX를 다시 평가하면서 실행.

그렇다면 질문!

  • Q. Dispatcher가 확인하는 '실행 되지 않은 드라이버 리스트'가 순서가 있고, 그 순서에 맞게 차례대로 돌면서 확인하는것인가?
    • A. 리스트에는 뚜렷한 순서가 있다!
    • Dispatcher는 메모리에 올라온 드라이버들을 무작위로 고르는 게 아니라, 펌웨어 볼륨(FV)에서 파싱(Parsing)되어 발견된 물리적 순서대로 구성된 리스트를 위에서부터 아래로 차례대로 검사해 내려간다.
  • Q. 하나의 드라이버가 설치되면 다시 처음으로 돌아가서 다시 확인하는건지, 한바퀴를 돌고 처음으로 돌아가는건지?
    • A. 한바퀴를 돌고 처음으로 돌아간다! 
      1. Dispatcher는 실행되지 않은 드라이버 목록을 훑어 내려가면서 DEPEX 조건이 TRUE인 드라이버를 바로 실행하는 것이 아니라 Scheduled Queue에 넣어둠.
      2. 목록을 모두 훑고 나면 (한바퀴 돌고 나면) Scheduled Queue에 있는 드라이버를 순서대로 실행시킴.
      3. 새로 설치된 프로토콜을 가지고 다시 목록을 처음부터 훑으며 반복.

- 정리

  1. Apriori 파일에 있는 드라이버를 Scheduled Queue에 push(Scheduled 상태). 나머지 DXE 드라이버들을 Discovered 상태로 올린 뒤 DEPEX를 평가해서 Dependent/Unrequested로 분류.
  2. Scheduled Queue에 있는 드라이버를 순서대로 로드 및 실행하며, 해당 드라이버 실행 결과물인 프로토콜이 메모리에 설치. 
  3. 그 설치된 프로토콜을 가지고 Dependent 상태의 드라이버 목록을 Dispatcher가 돌면서 조건(DEPEX)이 충족된 드라이버를 찾아서 Scheduled Queue에 push.
  4. 목록을 한바퀴 돌고 나서 Scheduled Queue에 있는 드라이버들을 순서대로 드레인.
  5. 만약, 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바이트
        → 스택 오버플로우 발생!

 

[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 조작

 

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

기존 드라이버 (origin_file.ffs)

 

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

DEPEX 조작 드라이버 (vuln_file.ffs)

 

여러 드라이버가 들어있는 환경에서도 잘 실행되는지 확인해보았으나, DEPEX를 잘 잡아주지 못한다는 것을 알게 되었다. 

스크립트에서 DEPEX를 가져오는 방식을 수정해야 할 것 같다.

여러 드라이버가 들어있는 firmware volume 파일에서 탐지