Oob 담당 팀원의 스크립트를 검증하기 위해, 테스트 데이터를 만들어보고 테스트를 진행했다.
나는 보편적인 취약점을 가진 드라이버를 만들고, 그걸 탐지해 보면서 스크립트의 부족한 부분을 찾아봐야 한다는 생각을 했었다. 그런데 “보편적 데이터가 무엇일까?”, “편향의 가능성은 없을까?” 등등의 생각부터 시작해서, 팀원들의 스크립트의 성격이 다른 것도 더해져 답을 내리지 못했다.
팀원들은 스크립트를 짜면서 경로 폭발을 마주했지만, 난 스스로 생각하다가 경로 폭발이 일어났다. 아무튼 그래서, 그냥 기존의 스크립트가 탐지할 수 있는 취약점을 가진 드라이버 기준으로 이것저것 방해물만 넣어보는 식으로 테스트를 진행했다.
탐지에 사용한 데이터

기존에 동건이가 만들어 놓은 취약점 드라이버를 기준으로 변형을 시도했다.
사용한 드라이버는 블로그 참고
https://eatrawlife.github.io/posts/2025/03/22/01
기드라 흐름 추적 5가지(간이 난독화..)
1. 포인터 덧셈/뺄셈(test_ptrsub.efi)
#define PROPAGATE_PTRSUB(TaintIn, TaintOut) { \
UINT8* _tb = (UINT8*)(TaintIn); \
TaintOut = (VOID*)(_tb + 8); \
TaintOut = (VOID*)((UINT8*)TaintOut - 8); \
}
...
PROPAGATE_PTRSUB(Buffer, MutatedBuffer);
return MutatedBuffer;
입력값을 1바이트 단위 포인터로 캐스팅한 뒤, 주소값에 8을 더했다가 다시 8을 뺀다다. 결과적으로 원래 주소값이 그대로 들어감
2. 의미 없는 사칙 연산 (PROPAGATE_INT_ADD.efi)
#define PROPAGATE_INT_ADD(TaintIn, TaintOut) { \
UINTN _idx = (UINTN)(TaintIn); \
_idx = _idx + 5 - 5; \
TaintOut = (VOID*)_idx; \
}
...
VOID* TaintSize = NULL;
PROPAGATE_INT_ADD((VOID*)(UINTN)AllocationSize, TaintSize);
Buffer = AllocatePoolWrapper((UINT32)(UINTN)TaintSize);
입력값을 시스템 워드 크기로 변환한 뒤, 5를 더하고 5를 뺀다.
3. 비트 확장 및 잘라내기 (test_subpiece.efi)
#define PROPAGATE_SUBPIECE(TaintIn, TaintOut) { \
UINT64 _lv = (UINT64)(UINTN)(TaintIn); \
UINT32 _tv = (UINT32)(_lv & 0xFFFFFFFF); \
TaintOut = (VOID*)(UINTN)_tv; \
}
...
VOID* TaintX = NULL;
PROPAGATE_SUBPIECE((VOID*)(UINTN)X, TaintX);
UINT32 MutatedX = (UINT32)(UINTN)TaintX;
UINT32 AllocationSize = MutatedX * Y * 4;
값을 64비트(UINT64)로 확장한 뒤, 비트 마스킹(& 0xFFFFFFFF)을 통해 하위 32비트만 남기고 상위 32비트를 날린다. 그리고 다시 포인터 형태로 캐스팅
4. 단순 타입 캐스팅(test_cast.efi)
#define PROPAGATE_CAST(TaintIn, TaintOut) { \
VOID* _vp = (VOID*)(TaintIn); \
TaintOut = _vp; \
}
...
VOID* TaintX = NULL;
PROPAGATE_CAST((VOID*)(UINTN)ParamX, TaintX);
return VulnerableProtocolFunction((UINT32)(UINTN)TaintX, ParamY);
단순히 입력값을 VOID* 로 형변환하여 넘겨준다.
5. 가짜 조건부 분기 (test_multiequal.efi)
int globalFive = 5;
#define PROPAGATE_MULTIEQUAL(TaintIn, TaintOut) { \
VOID* _bm; \
if(globalFive==5) _bm = (VOID*)(TaintIn); \
else _bm = NULL; \
TaintOut = _bm; \
}
...
VOID* TaintX = NULL;
PROPAGATE_MULTIEQUAL((VOID*)(UINTN)ParamX, TaintX);
return VulnerableProtocolFunction((UINT32)(UINTN)TaintX, ParamY);
전역 변수 globalFive가 5인지 검사하여, 참이면 원래 값을, 거짓이면 NULL을 반환
탐지 결과
| 정탐 여부 | |
| 포인터 덧셈/뺄셈 | O |
| 의미 없는 사칙 연산 | O |
| 비트 확장 및 잘라내기 | O |
| 단순 타입 캐스팅 | O |
| 가짜 조건부 분기 | O |
1. 시프트 연산 우회(test_shift_op.efi)
// 기존
UINT32 AllocationSize = X * Y * 4;
// 수정
UINT32 AllocationSize = (X * Y) << 2;
곱셈 대신 비트 시프트 사용
2. 연산 쪼개기(test_split_op.efi)
// 기존
UINT32 AllocationSize = X * Y * 4;
// 수정
UINT32 BaseArea = X * Y;
UINT32 AllocationSize = BaseArea * 4;
X, Y, 4가 하나의 식에 뭉쳐있지 않고 여러 줄에 걸쳐 쪼개져 있을 때, 스크립트가 이전 변수를 따라가며 하나의 수식으로 합쳐서 판단할 수 있는지 테스트
3. 덧셈 우회 (test_add_op.efi)
// 기존
UINT32 AllocationSize = X * Y * 4;
// 수정
UINT32 XY = X * Y;
UINT32 AllocationSize = XY + XY + XY + XY;
곱하기 4 대신 4번 더하기해서 P-Code 상의 덧셈(INT_ADD) 반복을 곱셈 오버플로우와 동일한 위험으로 인지하는지 테스트
탐지 결과
| 정탐 여부 | |
| 시프트 연산 우회 | O |
| 연산 쪼개기 | O |
| 덧셈 우회 | O |
1. 구조체 포인터 (test_struct_memory.efi)
//기존 코드
UINT32 AllocationSize = X * Y * 4;
// 수정 코드
typedef struct {
UINT32 Dummy;
UINT32 HiddenTaint;
} TAINT_BOX;
TAINT_BOX Box = {0, 0, 0};
Box.HiddenTaint = X;
TAINT_BOX* pBox = &Box;
UINT32 AllocationSize = pBox->HiddenTaint * Y * 4;
스크립트가 X라는 값이 Box.HiddenTaint 메모리 공간에 저장되었다가, 포인터 pBox를 통해 다시 불려 나오는 과정을 놓치지 않고 끝까지 오염 상태로 유지하는지 확인
2. 함수 포인터 동적 호출 (test_func_ptr.efi)
// 기존 코드
Buffer = AllocatePoolWrapper(AllocationSize);
//수정 코드
typedef VOID* (EFIAPI *ALLOC_FUNC)(UINT32);
ALLOC_FUNC pfnAlloc = AllocatePoolWrapper;
Buffer = pfnAlloc(AllocationSize);
AllocatePoolWrapper를 직접 호출하지 말고, 함수 포인터로 한 번 더 감싸서 호출
변수에 담긴 함수 포인터의 실제 타겟(AllocatePoolWrapper)을 정적으로 해석해서 추적을 이어갈 수 있는지 테스트
3. 오탐 방어 (test_sanitization.efi)
// 기존 코드
UINT32 AllocationSize = X * Y * 4;
VOID* Buffer = NULL;
if (AllocationSize > 0) { ... }
// 수정 코드
if (X > 1000 || Y > 1000) {
return NULL;
}
UINT32 AllocationSize = X * Y * 4;
VOID* Buffer = NULL;
if (AllocationSize > 0xFFFFF) {
return NULL;
}
if (AllocationSize > 0) { ... }
안전한 범위 검증이 존재할 때, 오탐을 유발하지 않는지 테스트
입력값 X가 sink로 들어가는 흐름 자체는 존재함!
탐지 결과
| 정탐 여부 | |
| 메모리 경유 및 구조체 포인터 | O |
| 함수 포인터 동적 호출 | O |
| 오탐 방어 | O (취약점 0건으로 잘 판단함) |
앞으로의 방향성: 검증 자동화를 해보자
테스트 케이스의 입력에 대해 시스템이 올바른 결과를 산출했는지 판단하는 기준을 테스트 오라클(Test Oracle)이라고 한다고 들었다. 따라서 이를 토대로 OOB탐지 스크립트의 성능을 검증하기 위한 자동화 환경을 구상해 보았다.
먼저 탐지 스크립트가 취약점을 정확히 찾아내기 위한 네 가지 조건을 아래와 같이 정의했다.
[테스트 오라클 확립]
조건 1: 연산 패턴
기준: 두 개의 변수가 곱해진 후, 그 결괏값에 4를 곱하거나 2번 좌측 시프트(<<) 해야 함
조건 2: sink
기준: 연산된 최종 결괏값이 단일 인자를 갖는 함수 호출에 직접 사용되어야 함
조건 3: 오탐 방어
기준: X * Y 연산이 일어나기 이전(주소 오프셋 기준)에, 두 변수 X, Y 중 최소 하나 이상이 작다(<) 혹은 작거나 같다(<=)로 검증되지 않아야 함
조건 4: Source
기준: 연산에 사용된 두 변수 중 최소 하나 이상이 외부에서 유입된 파라미터여야 함
이 조건들을 만족하는 다양한 케이스를 대입했을 때, 탐지 스크립트가 실제 취약점을 얼마나 정확하게 짚어내는지 확인하는 것이 목표다.
템플릿 구성 및 렌덤 데이터 생성


기존 취약점 드라이버 구조를 기반으로, 테스트 오라클의 세부 로직을 동적으로 치환할 수 있는 파이썬 스크립트를 작성했다. 보안검사 슬롯(15개)과 산술 연산 슬롯(18개)의 조합에 대해 카테시안 곱을 적용해 총 270개의 드라이버 소스를 만들었다.
빌드 후 실행


생성된 270개의 드라이버를 edk2 환경에서 빌드해서 .efi 파일을 확보했다. 이후 기드라 헤드리스 모드를 활용해 탐지 스크립트를 실행했고, 각 드라이버에 대한 분석 결과물로 270개의 JSON 파일을 얻을 수 있었다.
결과 분석

json 파일의 info를 수집하는 bat 파일을 만들어 결과를 확인했는데, 지금 결과는 270개가 전부 탐지되지 않았다. 아마 템플릿 자체에 문제가 있어서 그런 게 아닐까 싶다.
개선점 및 향후과제..
- 정확히 동작 할 수 있게 템플릿을 수정 해야 한다.
- 각 슬롯에 넣을 수 있는 데이터의 기준을 잡아야한다.
- 이게 명확해지면 수백, 수천개를 만들 수 있을 듯
- 테스트 관련 개념을 더 찾아봐야...
- (1,2번 성공 후..) 데이터 생성, 빌드, 드라이버 추출, 기드라 실행, 결과 분석 등 각 단계를 통합한다.
- 단계별로 문제가 없다면 딸깍 한번으로 수백개 드라이버 때려넣으면서 검증 할 수 있을 듯
'CS > Graduation project' 카테고리의 다른 글
| [UEFI] DEPEX Cycle Vulnerability 가능성 판단하기 (1) | 2026.03.25 |
|---|---|
| [UEFI] SMI 발생 원리 및 처리 방법 (0) | 2026.03.10 |
| Taint Analysis 개념 정리 (0) | 2026.03.01 |
| [UEFI] Ghidra에서 edk2 함수 P-code로 확인해 보기 (0) | 2026.02.26 |
| [UEFI] gRT(Runtime Services)란? (0) | 2026.02.24 |
