[프로젝트]UEFI DXE 바이너리 취약점 분석기 5주차 - DEPEX 확인법/Ghidra Script 기본 틀
저번주에 간단하게 기드라 스크립트를 가지고 사용하는 방법을 실습해봤는데, 이번 주에는 관련 함수와 DEPEX 관련 공부 등을 해보려고 한다.
(실습 파일은 EDK2 21.02버전을 빌드했을 때 나온 OVMF.fd파일)
[DEPEX 확인]
DEPEX는 생각보다 간단하고 쉽게 확인할 수 있었다.
- UEFITool을 설치하고 실행한 뒤 분석할 펌웨어 롬 파일(.CAP, .fd, .bin 등)을 연다.
- 트리 구조를 확장하며 Subtype이 DXE Driver인 것을 찾는다.
- 이 내부에 DXE dependency section이라는 Name을 가진 DXE dependency를 누르고 화면 우측에 있는 Information 패널에서 Parsed expression 부분을 확인하며, 해당 드라이버의 DEPEX를 볼 수 있다.

현재 관찰중인 드라이버는 D93CE3D8-A7EB-4730-8C8E-CC466A9ECC3C이고, 이는 ReportStatusCodeRouterRuntimeDxe이다.
이름에 적힌 ReportStatusCodeRouter는 말 그대로 상태 코드(Status Code)를 전달(Router)한다는 뜻.
- 주요 역할: 컴퓨터가 켜지는 과정이나 운영체제(Windows/Linux)가 돌아가는 중에 발생하는 각종 상태 메시지(에러, 경고, 디버깅 로그 등)를 수집하여 적절한 곳으로 배달해 주는 역할.
- RuntimeDxe의 의미: 부팅이 끝나고 윈도우가 켜진 뒤에도 메모리에 끝까지 살아남아(Runtime), 윈도우가 하드웨어 에러를 발생시킬 때 메시지 배달.
이 드라이버의 DEPEX를 확인해보면 아래와 같이 작성되어 있다.
"""
PUSH 13A3F0F6-264A-3EF0-F2E0-DEC512342F34
PUSH 0379BE4E-D706-437D-B037-EDB82FB772A4
AND
END
"""
GUID 사전에서 찾아보니 아래와 같은 프로토콜로 해석된다.
13A3F0F6-264A-3EF0-F2E0-DEC512342F34 → EfiPcdProtocolGuid
0379BE4E-D706-437D-B037-EDB82FB772A4 → EfiDevicePathUtilitiesProtocolGuid
1. EFI_PCD_PROTOCOL (플랫폼 설정 데이터베이스)
이 프로토콜은 한마디로 "펌웨어계의 윈도우 레지스트리(Registry)" 또는 "전역 환경 변수(Environment Variable)" 역할.
- PCD의 의미: Platform Configuration Database의 약자.
- 주요 역할: 펌웨어가 작동하는 데 필요한 수많은 설정값(예: 기본 화면 해상도, 특정 보안 기능의 ON/OFF 상태, 하드웨어 타이머 속도 등)을 중앙에서 관리.
- 이 드라이버에서 필요한 이유 : "이 메시지를 화면에 글씨로 띄울지, 숨겨진 디버그 포트로 몰래 보낼지, 아니면 삐- 소리(비프음)로 알려줄지 등"을 결정해야 함. 이 처리 방식에 대한 설정값(PCD)을 읽어오기 위해 이 프로토콜이 필요.
2. EFI_DEVICE_PATH_UTILITIES_PROTOCOL (장치 경로 유틸리티)
이 프로토콜은 "복잡한 하드웨어 주소(경로)를 쉽게 조작하게 해주는 만능 십자드라이버".
- 디바이스 패스(Device Path)란?: UEFI 환경에서는 하드디스크나 USB를 C:\ 같은 단순한 이름으로 부르지 않음. 대신 PCI(0x1F,0x2) / SATA(0x0,0x0) / HD(Part1) 처럼 하드웨어가 물리적으로 연결된 경로를 연결 리스트(Linked List) 형태의 이진 데이터로 길게 늘어뜨려 표현함.
- 주요 역할: 복잡한 이진 데이터 구조를 개발자가 손으로 직접 수정하는 것은 어려움. 이 유틸리티 프로토콜은 경로를 쉽게 자르고, 이어 붙이고, 눈으로 읽을 수 있는 텍스트로 변환해 주는 도구 모음.
- 제공하는 기능(함수): AppendDeviceNode() (경로 끝에 새 장치 추가), IsDevicePathValid() (경로가 올바른지 검사) 등의 API를 제공.
- 이 드라이버에서 필요한 이유 : "메인보드의 PCI 몇 번 슬롯에 꽂힌 어떤 장치에서 에러가 났는지" 같은 복잡한 하드웨어 주소(경로)를 정확하고 쉽게 알려주기 위한 장치 경로를 다루는 유틸리티로써 필요.
결론적으로 ReportStatusCodeRouterRuntimeDxe 드라이버가 실행되기 위해서는 두 가지 선행 조건이 충족(AND)되어야 함.
첫째, 상태 메시지의 처리 방식 등 펌웨어 설정값을 제공하는 PCD 프로토콜(gEfiPcdProtocolGuid)이 필요하며,
둘째, 에러가 발생한 하드웨어의 복잡한 위치 정보를 정확하게 계산하고 다룰 수 있는 장치 경로 유틸리티 프로토콜(gEfiDevicePathUtilitiesProtocolGuid)이 메모리에 준비되어 있어야 함.
위에서 사용한 GUID 사전은 아래 링크에서 확인할 수 있다.
https://raw.githubusercontent.com/al3xtjames/ghidra-firmware-utils/master/data/guids.csv
[Ghidra Script]
기드라는 자바로 만들어졌기 때문에, 자바로 스크립트를 작성하게 되면, 기드라의 심층 API와 메모리 분석 기능을 최대로 사용할 수 있다하여 자바를 사용하려 한다.
지난주에는 디컴파일된 C코드를 읽어서 취약점을 찾게 만들어 봤는데, 실행해보니 로직도 엉망이고 제대로 된 탐지가 불가능할 것 같아서 어셈블리어 혹은 P-code (기드라 중간언어)를 분석하도록 코드를 짜는 것이 좋을 것 같다.
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionManager;
public class UEFIVulnScanner extends GhidraScript {
@Override
protected void run() throws Exception {
// 1. 현재 화면에 띄워진 프로그램(바이너리) 정보 가져오기
Program program = currentProgram;
// 분석할 파일이 열려있지 않다면 종료
if (program == null) {
println("분석할 파일을 띄우시오.");
return;
}
// 2. 스크립트 가동 알림
println("=========================================");
println("[*] 취약점 탐지 스캐너 가동 시작...");
println("[*] 분석 대상 파일: " + program.getName());
println("=========================================");
// 3. (예시) 프로그램 내의 모든 함수를 가져올 준비
FunctionManager functionManager = program.getFunctionManager();
// TODO: 취약점 탐지 로직
// 예: 특정 위험 함수(strcpy, memcpy 등) 검색, UEFI 프로토콜 취약점 분석 등
// 4. 분석 완료 알림
println("=========================================");
println("[+] 분석이 모두 완료됨.");
}
}
- Import
- import ghidra.app.script.GhidraScript;
- 기드라 스크립트의 뼈대.
- 역할: 스캐너(UEFIVulnScanner)가 기드라 안에서 정상적으로 작동하도록 해줌. 화면에 글씨를 띄우는 println()이나, 현재 열려있는 파일을 가리키는 currentProgram 같은 기본적인 함수들을 바로 쓸 수 있게 해주는 도구.
- import ghidra.program.model.listing.Program;
- '분석 대상 파일(예: .efi 파일)' 그 자체를 의미.
- 역할: 기드라에 띄워진 펌웨어 파일의 이름, 메모리 구조, 기계어 바이트 등 프로그램 전체에 대한 방대한 정보를 가지고 있는 데이터 상자.
- import ghidra.program.model.listing.FunctionManager;
- 프로그램 안의 모든 함수를 관리.
- 역할: 펌웨어 안에는 수백, 수천 개의 함수가 숨어 있고, 이 관리자를 호출하면 "이 프로그램에 함수가 총 몇 개인지", 또는 "첫 번째 함수부터 마지막 함수까지 차례대로 줄 세워 가져와" 같은 명령을 내릴 수 있습니다. 취약점을 전수 조사할 때 필요.
- import ghidra.program.model.listing.Function;
- 파일 안에 존재하는 개별 함수 하나하나를 다루기 위함.
- 역할: FunctionManager를 통해 함수를 하나씩 꺼내왔을 때, 그 함수의 진짜 이름(예: entry, LocateProtocol), 함수가 메모리의 어느 주소에서 시작하는지, 매개변수는 무엇인지 등을 정밀하게 뜯어볼 수 있게 해줌.
2. 함수
- run()
- 역할: C언어나 자바 프로그램의 main() 함수와 완전히 동일한 역할.
- 특징: 기드라에서 이 스크립트의 재생(Run) 버튼을 누르는 순간, 시스템은 무조건 이 run() 함수 내부로 뛰어들어와 코드를 위에서부터 아래로 실행.
- println(String message)
- 역할: 기드라 화면 하단의 콘솔(Console) 창에 결과를 출력.
- 특징: 원래 자바에서는 System.out.println()이라고 길게 타이핑해야 하지만, GhidraScript를 상속받았기 때문에 짧고 간결하게 println()만 호출해도 됨.
- getName()
- 역할: 현재 기드라에 로드된 분석 대상 파일의 '이름'을 문자열(String)로 반환하는 함수.
- 사용처: program.getName() 형태로 쓰였으며, 펌웨어 파일(예: ReportStatusCodeRouter.efi)의 이름을 동적으로 알아내어 사용자가 알기 쉽게 함.
- getFunctionManager()
- 역할: 펌웨어 내부에 존재하는 모든 함수들의 목록과 주소 정보를 가지고 있는 '함수 관리자(인사 명부)' 객체를 가져오는 함수.
- 사용처: program.getFunctionManager() 형태로 호출됨. 이 함수를 통해 반환받은 관리자 객체로, "프로그램 내의 함수 500개를 순서대로 전부 가져오기" 혹은 "특정 주소에 있는 함수 찾아오기" 같은 취약점 전수 조사 명령을 내릴 수 있음.
- currentProgram
- 역할: 현재 기드라(CodeBrowser) 화면에 띄워져 활성화된 분석 대상 파일(바이너리)을 가리키는 내장 변수.
- 특징: 별도로 선언하거나 생성할 필요 없이 GhidraScript를 상속받았기 때문에 기본적으로 제공됨. 기드라에 분석할 파일이 열려있지 않은 상태에서 스크립트를 실행하면 null 값을 가지므로, 본격적인 분석 전에 반드시 if (program == null)과 같은 예외 처리를 해주어야 스크립트가 안전하게 동작함.