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

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

edk2 맥북 빌드 정리 결과 Linux와 동일하되 컴파일 옵션을 gcc가 아닌 XCODE5 정도로 수정해주면 되어 이전 게시글을 참조하면 좋을 것 같다.

Ghidra

 UEFI DXE 드라이버를 바이너리 단에서 P-Code로 올려서 취약점 분석을 하겠다는 계획에 Ghidra 공부는 필수적이였다. Ghidra는 미국 국가안보국(NSA)이 개발한 역어셈블러 프레임워크로, 위키리크스에 의해 정체가 공개되어 2019년에 오픈소스로 공개되었다. 비슷한 역할로 IDA Pro가 있고 일반적으로 IDA Pro가 업계 표준으로 꼽히지만, IDA Pro는 매우 비싼 가격을 가진 반면 Ghidra는 오픈 소스이면서 디스어셈블 및 디컴파일러 기능, 또한 java, python 등을 통한 스크립트 기능을 지원하여 분석 기능을 자동화할 수 있다.

P-Code

제목
 Ghidra에서 P-Code 설정을 켠 뒤 OVMF의 아무 DXE 드라이버를 열어본 화면이다. 다른 디스어셈블러와 달리 어셈블리 아래에 파란 글씨로 뭔가 잔뜩 있는 것을 볼 수 있다. 저 파란 코드들이 해당 어셈블리에 대한 P-Code다.
 Ghidra 공식 사이트에선 P-Code를 리버싱을 위해 디자인된 레지스터 전이 언어(출처)라고 설명하고 있다. 쉽게 설명하면 LLVM IR과 같이 고수준 언어와 기계어 사이 다리 역할을 하는 언어라고 볼 수 있다. 다만 차이점으로 P-Code는 다른 IR과 달리 기계어를 고수준 언어로 “올리는” 과정에서의 중간 언어로, 기계어가 가지는 복잡함을 걷어내고 추상화된 논리로 복원하는 것에 의의를 가진다. 사실 위 이미지만 보면 그냥 어셈블리어보다 복잡해 보이지만, 이는 해당 어셈블리어가 어떤 행동을 하는지 등을 풀어서 설명하는 것이다. 예를 들어 Call 명령어는

  1. 스택 주소를 8만큼 줄여라(INT_SUB)
  2. 돌아올 주소를 스택 메모리에 저장해라(STORE)
  3. 해당 함수로 가라(CALL)

이 세 가지 과정이 이뤄지는 것이다. 그리고 P-Code는 이를 풀어서 보여주는 것이다. 아마 우리는 여기서 실제로 오염된 데이터들을 전파시킬 수 있는 명령어들인 CALL, COPY, LOAD, STORE 등을 중점적으로 봐야하지 않을까…싶다.
 P-Code를 보면 (register, 0x20, 8), (const, 0x0, 1), (unique, 0x2c180, 1)과 같은 방식으로 값들이 표현이 되는 것을 볼 수 있다. 이는 (저장 방식, 오프셋, 크기)로 해석할 수 있으며, 각각

  • (register, 0x20, 8) : 레지스터 0x20번째 칸부터 8바이트 크기
  • (const, 0x0, 1) : 숫자 0(상수의 offset은 곧 값이다.)
  • (unique, 0x2c180, 1) : Ghidra가 만든 임시변수 0x2c180번에 있는 1바이트짜리 임시 값

으로 해석할 수 있다. 그러므로 지금까지 정리한 내용 및 P-Code가 3-Address 기반의 IR을 따르고 있다는 점을 합쳐서 P-Code 몇 개를 해석해보면

  • (register, 0x20b, 1) = COPY (const, 0x0, 1)
    =>0x20b번째 레지스터에 숫자 0을 1바이트만큼 복사해라
  • (unique, 0x2c180, 8) = INT_AND (unique, 0x70280, 8), (const, 0xff, 8)
    => 0x70280번 임시변수와 0xff를 AND 연산한 결과를 0x2c180에 담아라

정도로 해석할 수 있을 것이다.

그래서 뭘 하지…

 개인적으로 공부하며 5인 팀을 어떻게 나눠야할지 고민을 했다. 개인적으로 했던 생각은

  1. 취약점 분석 스크립트 개발(Using Ghidra Script)(2인)
  2. 탐지 검증을 위한 취약한 코드 생성
  3. 탐지 오판 여부 검증
  4. 결과 문서화 담당

의 역할 분담이 어떨까… 조심스레 생각해 보고 있고, 만약 이러한 형태라면 취약점 분석 로직을 스크립트화 하는 1번 역할을 담당해보고 싶다는 생각을 하고 있다.

분석 방법 고민

 취약점 분석에 어떤 방법을 도입할지 고민하고 있다. 당장 생각나는 것들은 Pattern Matching, Taint Analysis, Symbolic Execution 정도가 생각나고, 해커가 공격의 진입점이 될 수 있는 부분(GetVariable() 함수를 통한 NVRAM 버퍼 오버플로우, CommBuffer 등)에 오염을 걸고 P-Code의 CALL, COPY, LOAD, STORE 등의 전파가 될 수 있는 부분들을 추적해나가며 찾아나가는 형식의 Taint Analysis를 통한 분석을 해보는 것으로 시작하는 것이 좋지 않을까 생각하고 있다.
 하지만 CommBuffer 구조체가 드라이버 별로 다르게 생긴 것으로 알고 있어서 이 부분에 대한 파싱 부분에 고민을 하고 있다. 부가적으로는 WSMT가 있다 하더라도 SmmIsBufferOutsideSmmValid() 함수를 호출하는지 여부를 확인하기 위한 Pattern Matching 기능 정도를 부가적으로 가져갈 수 있지 않을까 라는 생각을 하고 있다.

GetVariable() 함수 찾기

 Ghidra와 친해질 겸과 동시에 실제로 Buffer Overflow를 일으킬 가능성이 높은 함수인 GetVariable() 함수의 빌드 후 올라간 주소를 직접 하나씩 들어가보며 찾아보았다. 해당 부분은 취약점 분석기 스크립트 제작에 있어 중요한 부분이 되지 않을까 싶다..
 GetVariable() 함수는 NVRAM(비휘발성 메모리) 내의 원하는 데이터를 읽어 인자로 넣어준 메모리 버퍼에 값을 채워주는 함수다. 이는 gRT(Global Runtime Services Table) 내에 구현되어있는 함수로, DXE 드라이버들이 메모리 내 상태를 읽을 때 해당 함수를 호출해 사용하는 중요한 함수지만, 함수 구현 코드를 보면

EFI_STATUS
EFIAPI
VariableServiceGetVariable (
  IN      CHAR16            *VariableName,
  IN      EFI_GUID          *VendorGuid,
  OUT     UINT32            *Attributes OPTIONAL,
  IN OUT  UINTN             *DataSize,
  OUT     VOID              *Data OPTIONAL
  )
{
    ...
    if (*DataSize >= VarDataSize) {
        if (Data == NULL) {
            Status = EFI_INVALID_PARAMETER;
            goto Done;
        }

    CopyMem (Data, GetVariableDataPtr (Variable.CurrPtr, mVariableModuleGlobal->VariableGlobal.AuthFormat), VarDataSize);

    ...
  }
  ...
}

이 부분에서 CopyMem 함수가 문제가 된다. 해당 함수의 Data는 값을 써넣을 목적지 역할을 수행하지만 해당 목적지가 안전한 영역인지에 대한 처리를 수행하는 로직은 없는데, 예를 들어 해커가 특정 변수 크기를 키우고 그 곳에 Payload를 집어넣은 뒤 검증 절차를 밟지 않은 SMM 핸들러를 실행시켜 GetVariable() 함수를 통해 해당 Payload를 읽어오면 Payload가 리턴 주소 등을 덮어씌워버리면서 Ring -2 권한으로 실행되는 것이다. 그러므로 해당 함수를 호출하는 SMM 핸들러에 이를 처리하는 SmmIsBufferOutsideSmmValid() 함수가 없다면 문제가 발생할 수 있을 것이다.
 한번 Ghidra에서 VariableSmm이란 파일에서 GetVariable() 함수를 찾아보았다. 먼저 해당 파일을 UEFITool을 통해 추출하고, 이 파일을 Ghidra를 통해 열어보았다.
제목
잘 모르겠지만 entry라고 적혀있는 것으로 보았을 떄 시작 지점임을 알 수 있었다. 시작 지점은 edk2/MdePkg/Library/UefiApplicationEntryPoint/ApplicationEntryPoint.c 파일에 적혀 있었으며, 구조는

EFI_STATUS
EFIAPI
_ModuleEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS                 Status;

  if (_gUefiDriverRevision != 0) {
    if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
      return EFI_INCOMPATIBLE_VERSION;
    }
  }
  ProcessLibraryConstructorList (ImageHandle, SystemTable);
  Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);
  ProcessLibraryDestructorList (ImageHandle, SystemTable);
  return Status;
}

다음과 같았다. 그러므로 ProcessModuleEntryPointList() 함수에 들어가보기로 했다. 위 함수에 해당하는 FUN_00008f6f 함수로 가보자.
제목
해당 함수 역시 굉장히 단순한 구조로 이뤄져있었고, 그 외 특이점으로 “AutoGen.c” 라는 인자를 넣은 에러 처리 함수를 호출하는 것을 볼 수 있는데, 해당 링크에 보면 빌드 시스템이 자동으로 Wrapper로 감싼 것을 볼 수 있었다. 다시 한번 FUN_0000565d 함수로 이동해보자.
제목
FUN_0000565d 함수 역시 껍질만 있는 함수였고, 해당 함수가 FUN_000065d8을 호출하는 것을 보고 들어갔더니 드디어 무언가 코드가 잔뜩 나온 것을 볼 수 있었다. 여기부턴 Gemini의 도움을 받아서 해결해보았다. DAT_00090650이 SMM 모드 전용 시스템 테이블인 gMmst(Global SMM System Table)라 하여 Ghidra에 ghidra-firmware-utils라는 플러그인을 설치한 뒤 DAT_00090650의 이름을 gMmst, 타입을 EFI_SMM_SYSTEM_TABLE2 *로 변경한 결과
제목
VariableSmm.c 파일의 MmVariableServiceInitialize() 함수와 동일함을 알 수 있었다. 해당 C 코드를 보면 gMmst의 SmmInstallProtocolInterface 함수의 네번째 인자가 gSmmVariable을 받는 것을 볼 수 있고, 해당 gSmmVariable의 첫 번째 인자가 GetVariable()임을 알 수 있었고, 두 번째 인자가 GUID임을 알 수 있다.
제목
제목
해당 논리를 따라 타고 들어가 확인해본 결과 해당 함수의 위치 및 GUID를 알아낼 수 있었다!

다음으로는 Gemini가 위 분석을 Ghidra 스크립트화도 가능하다는 이야기를 해서 한번 시도해보지 않을까…싶다