UEFI 스터디 4차 - 메모리에 로딩된 전역변수 추적하기

3 minute read

Published:

PE포맷에 대해 공부하면서 “우리에게 PE포맷이 제공해주는 가장 귀한 정보는 뭘까?”에 대해 ‘디스패처에게 메모리에 재 배치 되기 위한 정보들을 제공한다는 것’ 이라고 난 생각했다. 다음 달부터 기드라 등 툴을 쓸것인데, 이를 대비해서 어느정도 메모리에 로딩되었을때 모습은 예측이 되어야할 것 같다.
따라서 직접 만든 매우 단순한 드라이버를 메모리에 올리고 해당 드라이버의 전역변수의 위치를 특정해 봄 으로서 실행 파일 -> 메모리 로딩에 대해 이해해 볼 것이다.

가상환경에서 UEFI 구축하기

UEFI의 DXE 단계에 디스패처가 PE 포맷의 드라이버를 파싱해서 메모리에 로딩한다.
그렇다 보니 PI 과정 중 필수적이지 않은 드라이버는 함께 빌드할 필요 없이 UEFI 쉘로도 실행 가능하다고한다.
이를 위해 일단 UEFI 부팅이 가능한 환경이 필요하다.

이를 위해 OVMF를 사용한다.
OVMF는 EDK2베이스로 UEFI가 버추얼머신을 지원하도록 한 프로젝트이다. 내부에 QEMU 같은 가상머신을 돌리기위한 에뮬레이터를 위한 샘플 펌웨어를 가지고 있다.
https://github.com/tianocore/tianocore.github.io/wiki/OVMF

하단에는 OVMF.fd 파일을 이용해서 가상환경에서 UEFI를 부팅시켜보는 과정이다. 프로젝트와는 조금 멀게 느껴져서 GPT의 도움을 받아 해결했다. 따라할 사람은 따라해 보길 바란다. (윈도우, WSL UBUNTU 환경에서 진행하였다.)


sudo apt install qemu-system-x86

로 QEMU를 인스톨.
현재 폴더로 OVMF.fd를 가져오기. 아래의 QEMU 실행 스크립트를 vim으로 편집, 저장한뒤 실행.


#!/bin/bash

qemu-system-x86_64 \
  -nodefaults \
  -nographic \
  -m 512 \
  -bios ./OVMF.fd \
  -serial mon:stdio

생성한 실행파일에 실행 권한 부여, 쉘을 실행


chmod +x run_qemu.sh
./run_qemu.sh

임시로 사용할 드라이버 생성하기

임시로 사용할 efi 파일도 제작하였다. 전역변수 하나만 있기에, 실행 후 탐색에 매우 편할 것이다.

//파일이름 = my_driver.c
#include <efi.h>
#include <efilib.h>

// volatile: 컴파일러가 해당 변수를 사용하지 않아 지워버리는 것을 방지.
volatile UINT64 MyVal = 0xDEADC0DE; 

EFI_STATUS
EFIAPI
efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
    InitializeLib(ImageHandle, SystemTable);

    return EFI_SUCCESS;
}

아래는 해당 c파일을 efi 파일로 컴파일하기 위한 Makefile(vim으로 작성후 make)


# Makefile for my_driver
ARCH            = x86_64
OBJS            = my_driver.o
TARGET          = my_driver.efi

EFIINC          = /usr/include/efi
EFIINCS         = -I$(EFIINC) -I$(EFIINC)/$(ARCH) -I$(EFIINC)/protocol
LIB             = /usr/lib
EFILIB          = /usr/lib
EFI_CRT_OBJS    = $(EFILIB)/crt0-efi-$(ARCH).o
EFI_LDS         = $(EFILIB)/elf_$(ARCH)_efi.lds

CFLAGS          = $(EFIINCS) -fno-stack-protector -fpic \
                  -fshort-wchar -mno-red-zone -Wall
LDFLAGS         = -nostdlib -znocombreloc -T $(EFI_LDS) -shared \
                  -Bsymbolic -L $(EFILIB) -L $(LIB) $(EFI_CRT_OBJS)

all: $(TARGET)

my_driver.so: $(OBJS)
        ld $(LDFLAGS) $(OBJS) -o $@ -lefi -lgnuefi

%.efi: %.so
        objcopy -j .text -j .sdata -j .data -j .dynamic \
                -j .dynsym  -j .rel -j .rela -j .reloc \
                --target=pei-x86-64 --subsystem=11 \
                $^ $@
clean:
        rm -f *.o *.so *.efi

이제 해당 my_driver.efi를 Peview를 이용하여서 직접 전역변수 MyVal의 메모리 로딩 후 위치를 예측해보려고한다. 그러나, 내가 컴파일한 efi 파일은 PE32+포맷(64비트) 였고, 이 때문에 해당 툴에서 제대로 동작하지 않았다.

그래서 pe-bear라는 새로운 툴을 사용하였다. (어마어마한 star수를 보라)
https://github.com/hasherezade/pe-bear

IMAGE_OPTIONAL_HEADER를 먼저 살펴보자.
3

우리에게 필요한 값인 이미지 베이스가 0이다. 계산의 수고를 덜 수 있겠다.

4 하단에 보면 DataDirectory란 배열이 있다. 실제로 여기 5번째 원소에 재배치 테이블의 주소가 적혀 있는 것을 볼 수 있다. 그러나 이는 가상 주소에서의 offset으로 파일에서 실제 0x9000에 가면 이상한 값이 있다. 우리는 얌전히 네비게이션을 보고 .reloc 섹션으로 가보도록 하자.

5 reloc 섹션에는 이상하게도 값이 하나 밖에 없었다. 그러나 RVA가 .data보다 훨씬 앞으로 상관이 없기에 일단 띄어 넘도록 하자..

전역변수는 PE 포맷의 .data 섹션에 저장된다.

2

IMAGE_SECTION_HEADER를 살펴보면 .data 섹션의 Raw Addr은 0x6600, virtual Addr은 0xA000임을 알 수 있다. 즉 파일 내에서 .data 섹션은 시작부터 0x6600부터 시작이며, 메모리 로딩후엔 섹션 시작부터 0xA000에 있다는 것을 알 수 있다.

본격적으로 .data내부에서 전역변수를 찾아보자.
일단 데이터들이 little-endian으로 정렬되어있기에, 우리는 0x00000000DEADC0DE의 해당 표현인 DE C0 AD DE 00 00 00 00을 찾으면된다.
문자열이 아니라 문자열 검색은 불가능하고 헥스 에디터처럼 찾아야한다. .data 섹션에 우클릭 후 DEC0의 시그니처를 검색했다.

6
찾았다. 파일기준으로 오프셋 0x7620에 있었다.

자 그러면 이제 해당 전역 변수의 가상 메모리 주소를 추정해 보도록 하자..

해당 데이터의 오프셋 - .data 섹션의 raw 오프셋 + .data 섹션의 = 0x7620 - 0x6600 + 0xA000 = 0xB020
이 값에 실제 드라이버가 배정된 가상 메모리 주소를 더해주기만 하면 끝이다.

실제 가상환경에서 해당 드라이버 실행 후 드라이버가 배정된 가상 메모리주소 0x1e7e7000을 더한주소로 접근하니 해당 전역변수값이 잘 저장된 것을 볼 수 있다. 7