Home > 졸업프로젝트 > UEFI DXE 바이너리 취약점 분석기 프로젝트_10

UEFI DXE 바이너리 취약점 분석기 프로젝트_10
UEFI Ghidra

추가 수정 로직

 회의 이후 방식을 다시 바꿨다. 기존 방식의 문제점으로는 XREF를 타고 올라가는 과정에서 주소 + 0xd0 이런 식으로 타고 가는 것들이 존재하기 때문에 Ghidra는 이게 그냥 숫자 덧셈인지, 포인터인지 전혀 알 수 없다. 그러므로 Ghidra는 이를 단순 상수라고 생각하므로 XREF가 끊기게 되고, 찾지 못하게 되는 것이였다. 그리고 그렇다고 모든 함수를 다 타겟으로 넣었던 기존 방식으로 돌아가기엔 gBS를 호출 가능한 entry 영역까지 같이 찾아버리게 되는 문제가 있다.
 그래서 생각한 방식은 옛날 방식과 이번 방식을 섞어서 만들어냈다. 먼저 Ghidra의 모든 함수들을 타겟으로 하여 무작정 전부 다 가져온다. 여기까지는 옛날 방식과 동일하다. 그리고 이 함수들을 가지고 한번, 두번 더 걸러내는데, 해당 함수 내에서 간접 호출을 하는 경우, 즉 CALLIND 라는 P-Code를 호출하는 경우를 찾는다. 또 이 함수들을 가지고 한번 더 걸러내는데, 만약 0xE0을 꺼내온 지점이 있다면 Root Handler로, 그렇지 않은 것들 중 EFI_SMM_SW_DISPATCH2_PROTOCOL의 GUID를 호출하는 지점이 있다면 Child Handler로 간주하여 해당 함수들을 타고 들어가 핸들러 주소만 가지고 나와서 타겟으로 등록한다.
 핸들러를 호출하는 함수를 잡는 것이 아닌 핸들러 자체를 잡았을 때 장점이 존재하는데, 바로 이 과정에서 “더 이상 부팅 시점에서 gBS를 맘대로 사용해도 된다.”란 조건을 생각하지 않아도 된다. 해당 핸들러를 호출하는 지점이 어디인지 판단할 필요 없이 해당 핸들러 안에 gBS가 있는지 없는지만 확인하면 되는 것이다.

SmmCalloutHunter_v4_0_1 코드

(3.18일 수정. JSON 저장 위치 수정)
SmmCalloutHunter_v4_0_2 코드

지난 번 탐지하지 못했던 드라이버 분석 결과

제목
지난 주에는 탐지하지 못했던 팀원이 생성한 드라이버에 대해 잘 잡는 것을 확인할 수 있었다!

추가 변경사항

 아래는 지난 주 회의에서 나왔던 변경사항 및 회의 결과 리스트다.

  1. timestamp는 스크립트를 돌린 날짜 시분초가 아닌 스크립트를 돌리는 데 걸린 시간으로 한다.
  2. 파일명은 (잡고 있는 취약점)Hunter_v(버전)으로 하며, 이름은 파스칼 케이스로, 버전 숫자는 0_0_0의 형태로 표기한다.
  3. 팀원간 깃허브 레포지토리를 하나 개설하였다.

테스트 데이터 생성

 일단 잠시 코드 로직은 미뤄두고, 지금까지 만든 내 분석 스크립트를 평가해보고자 싶었다. 레드팀 역할을 너무나도 잘 해주는 형의 일손을 약간(?)은 덜어주고자… 생성형 AI를 사용해 테스트 데이터 총 20개를 뽑았다. 10개는 정상, 10개는 취약 코드로 하여 만들었다.

파일명 타겟 시나리오
test_1 gSmst->SmmLocateProtocol 호출
test_1_bad gBS->LocateProtocol 호출
test_2 gSmst->SmmAllocatePool 호출
test_2_bad gBS->AllocatePool 호출
test_3 gSmst->SmmFreePool 호출
test_3_bad gBS->FreePool 호출
test_4 gSmst->SmmLocateHandle 호출
test_4_bad gBS->LocateHandle 호출
test_5 gSmst->LocateHandleBuffer 호출
test_5_bad gBS->LocateHandleBuffer 호출
test_6 gSmst->SmmHandleProtocol 호출
test_6_bad gBS->HandleProtocol 호출
test_7 gSmst I/O 연산 수행
test_7_bad gBS->LocateProtocol(SATA) 레거시 서비스 찾기
test_8 자식 함수 생성 및 안전한 SMM 서비스 호출
test_8_bad 자식 함수 생성 이후 자식 함수에서 gBS 호출
test_9 정상적 gSmst 호출
test_9_bad 전역변수에 gBS 포인터 호출
test_10 SMM 영역 포인터 사용
test_10_bad entry에서 gBS->AllocatePool로 받아온 포인터 전역변수에 저장. 받아온 포인터 런타임에 호출

각 데이터에 대한 설명은 위 표와 같고, 데이터는 여기에 만들어 올려두었다.

빌드를 수행하는 방법은 다음과 같다.

제목
먼저 만들고 싶은 드라이버의 수만큼 폴더를 생성한다. 본인은 edk2/Ovmfpkg/ 아래에 저장해두었다.

제목
각 폴더 하위에는 .inf 파일과 취약점 드라이버 소스인 .c가 하나씩 들어간다.

## @file
#  Test Driver: test_1
##

[Defines]
  INF_VERSION                    = 0x00010005
  BASE_NAME                      = test_1
  FILE_GUID                      = 22334455-6677-4889-9AAB-BCCDDEEFF011
  MODULE_TYPE                    = DXE_SMM_DRIVER
  PI_SPECIFICATION_VERSION       = 0x0001000A
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = test_1EntryPoint

[Sources]
  test_1.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiDriverEntryPoint
  SmmServicesTableLib
  # UefiBootServicesTableLib  
  DebugLib

[Protocols]
  gEfiSmmSwDispatch2ProtocolGuid

[Depex]
  gEfiSmmSwDispatch2ProtocolGuid

.inf 파일은 다음과 비슷한 형태로 구성되며, 본인은 드라이버명, BASE_NAME, ENTRY_POINT, SOURCE, 그리고 FILE_GUID를 수정해주었다. 특히 GUID는 다른 파일과 겹치면 안된다.

이후 드라이버 C 코드를 짜준다.

#include <PiSmm.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/SmmServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Protocol/SmmSwDispatch2.h>


EFI_STATUS EFIAPI test1Handler (
  IN EFI_HANDLE  DispatchHandle,
  IN CONST VOID  *Context,         // 선택사항 (SMI 번호 등의 정보)
  IN OUT VOID    *CommBuffer,      // 벙커 밖(OS)과 통신하는 메모리 버퍼
  IN OUT UINTN   *CommBufferSize   // 통신 버퍼의 크기
  )
{
  VOID *Protocol;
  gSmst->SmmLocateProtocol (&gEfiSmmSwDispatch2ProtocolGuid, NULL, &Protocol);
  return EFI_SUCCESS;
}
EFI_STATUS EFIAPI test_1EntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS                     Status;
  EFI_SMM_SW_DISPATCH2_PROTOCOL  *SwDispatch;
  EFI_SMM_SW_REGISTER_CONTEXT    SwContext;
  EFI_HANDLE                     DispatchHandle;

  Status = gSmst->SmmLocateProtocol (&gEfiSmmSwDispatch2ProtocolGuid, NULL, (VOID **)&SwDispatch);
  if (EFI_ERROR (Status)) return Status;

  SwContext.SwSmiInputValue = 0x01; 
  Status = SwDispatch->Register (SwDispatch, test1Handler, &SwContext, &DispatchHandle); 

  return Status;
}

(여기서 SwSmiInputValue는 전부 다 다른 값을 넣어주라고 했는데 이유는 잘 모르겠다…)

그렇게 다 코드를 짜줬다면 다음으로 OvmfPkg 내 OvmfPkgX64.dsc, OvmfPkgX64.fdf 파일 두 개를 수정해준다.

제목
OvmfPkgX64.dsc 파일 맨 아래에 위 이미지처럼 만든 드라이버의 inf 파일 경로를 입력해주고,
제목
OvmfPkgX64.fdf 파일의 [FV.DXEFV] 아래에 위 이미지처럼 INF + 만든 드라이버의 inf 파일 경로를 입력해준다.

이후 항상 빌드하듯이

source edksetup.sh
build -p OvmfPkg/OvmfPkgX64.dsc -a X64 -t XCODE5 -D SMM_REQUIRE=TRUE -D SECURE_BOOT_ENABLE=TRUE

를 입력해주면 빌드가 진행이 되고, 만들어진 드라이버는 Build/OvmfX64/DEBUG_XCODE5/X64 아래에 만든 드라이버들이 .efi 파일로 예쁘게 잘 만들어진 것을 볼 수 있을 것이다!
(이 때 경로와 빌드 명령어는 사용 OS 및 컴파일러에 따라 달라진다. 본인은 M4 pro Mac + XCODE5를 통해 빌드하였다.)

테스트 결과

만든 20개 데이터를 Ghidra에 올린 뒤 각각 전부 테스트 해보았고, 결과는 20개중 19개를 잘 맞췄다. 틀린 하나는 test_10_bad였다.
test_10_bad는 전역변수를 하나 만든 뒤 entry에서 전역 변수에 gBS의 주소를 담고, 런타임 핸들러에서 해당 전역변수를 불러와 사용한다. 이때 우리가 만든 스크립트는 전역변수를 만났을 때 전역변수의 주소와 gBS 주소를 비교하고, 둘은 당연히 다르므로 안전하다고 판단하고 넘어가게 되는 것이다.