ELF 포맷(The ELF Format) (tistory.com)

 

ELF 포맷(The ELF Format)

Executable HeaderSection HeadersSectionsProgram HeadersLazy Binding참고 및 인용리눅스 기반 시스템의 기본 바이너리 형식인 ELF(Executable and Linkable Format)을 살펴보자.ELF는 실행 가능한 바이너리 파일, 목적 파일,

rond-o.tistory.com

 

리눅스 기반 시스템의 기본 바이너리 형식인 ELF(Executable and Linkable Format)을 살펴보자.

ELF는 실행 가능한 바이너리 파일, 목적 파일, 공유 라이브러리, 코어 덤프 등에서 사용되는 형식이다. 여기서는 64비트의 실행 가능한 바이너리 파일을 중심으로 살펴보겠다.

  • A 64-bit ELF binary at a glance

ELF 바이너리는 ELF 파일 헤더, 프로그램 헤더, 그리고 섹션들과 섹션 헤더들로 이루어져 있다.

Executable Header

모든 ELF 파일은 executable header로 시작하며, /usr/include/elf.h에서 확인할 수 있다.

  • /usr/include/elf.h의 Elf64_Ehdr 정의
typedef struct {
  unsigned char	e_ident[EI_NIDENT];  /* Magic number and other info */
  Elf64_Half	e_type;         /* Object file type */
  Elf64_Half	e_machine;      /* Architecture */
  Elf64_Word	e_version;      /* Object file version */
  Elf64_Addr	e_entry;        /* Entry point virtual address */
  Elf64_Off	  e_phoff;        /* Program header table file offset */
  Elf64_Off	  e_shoff;        /* Section header table file offset */
  Elf64_Word	e_flags;        /* Processor-specific flags */
  Elf64_Half	e_ehsize;       /* ELF header size in bytes */
  Elf64_Half	e_phentsize;    /* Program header table entry size */
  Elf64_Half	e_phnum;        /* Program header table entry count */
  Elf64_Half	e_shentsize;    /* Section header table entry size */
  Elf64_Half	e_shnum;        /* Section header table entry count */
  Elf64_Half	e_shstrndx;     /* Section header string table index */
} Elf64_Ehdr;

Elf64_Ehdr 구조체의 각 원소들의 자료형인 Elf64_Half, Elf64_Word 등은 uint16_t와 uint32_t다.

  • ELF 파일 헤더의 각 필드




    •  
    •  
    •  
    •  

    •  

    •  

    •  
    •  
    •  
    •  
    •  
    •  

Section Headers

ELF 바이너리의 코드와 데이터는 섹션(section)으로 나누어져 있다.

섹션의 구조는 각 섹션의 내용이 구성된 방식에 따라 다른데, 각 섹션 헤더(section header)에서 그 속성을 찾을 수 있다. 바이너리 내부의 모든 섹션에 대한 헤더 정보는 섹션 헤더 테이블(section header table)에서 찾을 수 있다.

섹션은 링커가 바이너리를 해석할 때 편리한 단위로 나눈 것이다.

링킹이 수반되지 않은 경우에는 섹션 헤더 테이블이 필요하지 않다. 만약 섹션 헤더 테이블 정보가 없다면 e_shoff 필드는 0이다.

바이너리를 실행할 때 바이너리 내부의 코드와 데이터를 세그먼트(segment)라는 논리적인 영역으로 구분한다.

세그먼트는 링크 시 사용되는 섹션과는 달리 실행 시점에 사용된다.

  • /usr/include/elf.h의 Elf64_Shdr 구조체 정의
typedef struct {
  Elf64_Word	sh_name;        /* Section name (string tbl index) */
  Elf64_Word	sh_type;        /* Section type */
  Elf64_Xword	sh_flags;       /* Section flags */
  Elf64_Addr	sh_addr;        /* Section virtual addr at execution */
  Elf64_Off	sh_offset;        /* Section file offset */
  Elf64_Xword	sh_size;        /* Section size in bytes */
  Elf64_Word	sh_link;        /* Link to another section */
  Elf64_Word	sh_info;        /* Additional section information */
  Elf64_Xword	sh_addralign;   /* Section alignment */
  Elf64_Xword	sh_entsize;     /* Entry size if section holds table */
} Elf64_Shdr;
  • 섹션 헤더의 각 필드
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  

Sections

GNU/Linux 시스템의 ELF 파일들은 대부분 표준적인 섹션 구성으로 이루어져 있다.

$ readelf --sections --wide a.out

There are 29 section headers, starting at offset 0x1168:

Section Headers:
  [Nr] Name           Type      Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                NULL      0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp        PROGBITS  0000000000400238 000238 00001c 00   A  0   0  1
  [ 2] .note.ABI-tag  NOTE      0000000000400254 000254 000020 00   A  0   0  4
  [ 3] .note.gnu.build-id NOTE     0000000000400274 000274 000024 00   A  0   0  4
  [ 4] .gnu.hash      GNU_HASH  0000000000400298 000298 00001c 00   A  5   0  8
  [ 5] .dynsym        DYNSYM    00000000004002b8 0002b8 000060 18   A  6   1  8
  [ 6] .dynstr        STRTAB    0000000000400318 000318 00003d 00   A  0   0  1
  [ 7] .gnu.version   VERSYM    0000000000400356 000356 000008 02   A  5   0  2
  [ 8] .gnu.version_r VERNEED   0000000000400360 000360 000020 00   A  6   1  8
  [ 9] .rela.dyn      RELA      0000000000400380 000380 000018 18   A  5   0  8
  [10] .rela.plt      RELA      0000000000400398 000398 000030 18  AI  5  24  8
  [11] .init          PROGBITS  00000000004003c8 0003c8 00001a 00  AX  0   0  4
  [12] .plt           PROGBITS  00000000004003f0 0003f0 000030 10  AX  0   0 16
  [13] .plt.got       PROGBITS  0000000000400420 000420 000008 00  AX  0   0  8
  [14] .text          PROGBITS  0000000000400430 000430 000192 00  AX  0   0 16
  [15] .fini          PROGBITS  00000000004005c4 0005c4 000009 00  AX  0   0  4
  [16] .rodata        PROGBITS  00000000004005d0 0005d0 000012 00   A  0   0  4
  [17] .eh_frame_hdr  PROGBITS  00000000004005e4 0005e4 000034 00   A  0   0  4
  [18] .eh_frame      PROGBITS  0000000000400618 000618 0000f4 00   A  0   0  8
  [19] .init_array    INIT_ARRAY0000000000600e10 000e10 000008 00  WA  0   0  8
  [20] .fini_array    FINI_ARRAY0000000000600e18 000e18 000008 00  WA  0   0  8
  [21] .jcr           PROGBITS  0000000000600e20 000e20 000008 00  WA  0   0  8
  [22] .dynamic       DYNAMIC   0000000000600e28 000e28 0001d0 10  WA  6   0  8
  [23] .got           PROGBITS  0000000000600ff8 000ff8 000008 08  WA  0   0  8
  [24] .got.plt       PROGBITS  0000000000601000 001000 000028 08  WA  0   0  8
  [25] .data          PROGBITS  0000000000601028 001028 000010 00  WA  0   0  8
  [26] .bss           NOBITS    0000000000601038 001038 000008 00  WA  0   0  1
  [27] .comment       PROGBITS  0000000000000000 001038 000034 01  MS  0   0  1
  [28] .shstrtab      STRTAB    0000000000000000 00106c 0000fc 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
  • .init 섹션에는 초기화에 필요한 실행 코드가 포함된다. 운영체제의 제어권이 바이너리의 메인 엔트리로 넘어가면 이 섹션의 코드부터 실행된다.
  • .fini 섹션은 메인 프로그램이 완료된 후에 실행된다. .init과 반대로 소멸자 역할을 한다.
  • .init_array 섹션은 일종의 생성자로 사용할 함수에 대한 포인터 배열이 포함된다. 각 함수는 메인 함수가 호출되기 전에 초기화되며 차례로 호출된다. .init_array는 데이터 섹션으로 사용자 정의 생성자에 대한 포인터를 포함해 원하는 만큼 함수 포인터를 포함할 수 있다.
  • .fini_array 섹션은 소멸자에 대한 포인터 배열이 포함된다. .init_array와 유사하다. 이전 버전의 gcc로 생성한 바이너리는 .ctors와 .dtors라고 부른다.
  • .text 섹션에는 메인 함수 코드가 존재한다. 사용자 정의 코드를 포함하기 때문에 SHT_PROGBITS라는 타입으로 설정되어 있다. 또한 실행 가능하지만 쓰기는 불가능해 섹션 플래그는 AX다. _start, register_tm_clones, frame_dummy와 같은 여러 표준 함수가 포함된다.
  • .rodata 섹션에는 상숫값과 같은 읽기 전용 데이터가 저장된다.
  • .data 섹션은 초기화된 변수의 기본값이 저장된다. 이 값은 변경되어야 하므로 쓰기 가능한 영역이다.
  • .bss 섹션은 초기화되지 않은 변수들을 위해 예약된 공간이다. BSS는 심벌에 의해 시작되는 블록 영역(Block Strarted by Symbol)이라는 의미로, (심벌) 변수들이 저장될 메모리 블록으로 사용한다.
  • .rel.*와 .rela.* 형식의 섹션들은 재배치 과정에서 링커가 활용할 정보를 담고 있다. 모두 SHT_RELA 타입이며 재배치 항목들을 기재한 테이블이다. 테이블의 각 항목은 재배치가 적용돼야 하는 주소와 해당 주소에 연결해야 하는 정보를 저장한다.동적 링킹 단계에서 수행할 동적 재배치만 남아 있다. 다음은 동적 링킹의 가장 일반적인 두 타입이다.
  • GLOB_DAT(global data): 이 재배치는 재배치는 데이터 심벌의 주소를 계산하고 .got의 오프셋에 연결하는 데 사용된다. 오프셋이 .got 섹션의 시작 주소를 나타낸다.이 엔트리의 오프셋은 해당 함수에서 간접 점프하는 점프 슬롯의 주소(rip로부터의 상대 주소로 계산)다.
  • JUMP_SLO(jump slots): .got.plt 섹션에 오프셋이 있으며 라이브러리 함수의 주소가 연결될 수있는 슬롯을 나타냅니다. 엔트리는 점프 슬롯(jump slot)이라고 부른다.
  • .dynamic 섹션은 바이너리가 로드될 때 운영체제와 동적 링커에게 일종의 road map을 제시하는 역할을 한다. 일명 태그(tag)라고 하는 Elf64_dyn 구조의 테이블을 포함한다. 태그는 번호로 구분한다.DT_NEEDED 태그는 바이너리와 의존성 관계를 가진 정보를 동적 링커에게 알려준다.게다가 동적 링커의 수행에 필요한 중요한 정보들을 가리키는 역할을 하기도 한다. 예를 들어 동적 문자열 테이블(DT_STRTAB), 동적 심벌 테이블(DT_SYMTAB), .got.plt 섹션(DT_PLTGOT), 동적 재배치 섹션(DT_RELA) 등.
  • DT_VERNEED와 DT_VERNEEDNUM 태그는 버전 의존성 테이블(version dependency table)의 시작 주소와 엔트리 수를 지정한다.
  • .shstrtab 섹션은 섹션의 이름을 포함하는 문자열 배열이다. 각 이름들을 숫자로 인덱스가 매겨져 있다.
  • .symtab 섹션에는 Elf64_Sym 구조체의 테이블인 심벌 테이블이 포함되어 있다. 각 심벌 테이블은 심벌명을 함수나 변수와 같이 코드나 데이터와 연관시킨다.
  • .strtab 섹션에는 심벌 이름을 포함한 실제 문자열들이 위치한다. 이 문자열들 Elf64_Sym 테이블과 연결된다. 스트립 된 바이너리에는 .symtab과 .strtab 테이블은 전부 삭제된다.
  • .dynsym 섹션과 .dynstr 섹션은 동적 링킹에 필요한 심벌과 문자열 정보를 담고 있다는 점을 제외하면 .symtab이나 .strtab와 유사하다.정적 심벌 테이블은 섹션 타입이 SHT_SYMTAB이고 동적 심벌 테이블은 SHT_DYNSYM 타입이다.

Program Headers

프로그램 헤더 테이블은 바이너리를 세그먼트의 관점에서 볼 수 있게 해준다.

바이너리를 섹션 관점에서 보는 것은 정적 링킹의 목적만으로 한정하는 것이다.

세그먼트의 관점으로 본다는 것은 운영체제와 동적 링킹 과정을 통해 바이너리가 프로세스의 형태가 되고, 그와 관련된 코드와 데이터들을 어떻게 처리할 것인지를 다룬다는 의미다.

  • /usr/include/elf.h의 Elf64_Phdr 정의
typedef struct elf64_phdr {
  Elf64_Word p_type;      /* Segment type */
  Elf64_Word p_flags;     /* Segment flags */
  Elf64_Off p_offset;     /* Segment file offset */
  Elf64_Addr p_vaddr;     /* Segment virtual address */
  Elf64_Addr p_paddr;     /* Segment physical address */
  Elf64_Xword p_filesz;   /* Segment size in file */
  Elf64_Xword p_memsz;    /* Segment size in memory */
  Elf64_Xword p_align;    /* Segment alignment, file & memory */
} Elf64_Phdr;
  • readelf로 프로그램 헤더 테이블 확인
$ readelf --wide --segments a.out

Elf file type is EXEC (Executable file)
Entry point 0x400430
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R E 0x8
  INTERP         0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x00070c 0x00070c R E 0x200000
  LOAD           0x000e10 0x0000000000600e10 0x0000000000600e10 0x000228 0x000230 RW  0x200000
  DYNAMIC        0x000e28 0x0000000000600e28 0x0000000000600e28 0x0001d0 0x0001d0 RW  0x8
  NOTE           0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R   0x4
  GNU_EH_FRAME   0x0005e4 0x00000000004005e4 0x00000000004005e4 0x000034 0x000034 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x000e10 0x0000000000600e10 0x0000000000600e10 0x0001f0 0x0001f0 R   0x1

➊ Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07
   08     .init_array .fini_array .jcr .dynamic .got

➊에서 섹션과 세그먼트의 매핑을 확인할 수 있다. 이를 통해 세그먼트는 여러 개(0개 이상)의 섹션들을 묶은 형태라는 것을 알 수 있다.


Lazy Binding

바이너리가 프로세스의 형태로 메모리에 로드될 때 동적 링커에 의해 최종적인 재배치가 이루어진다.

예를 들어, 공유 라이브러리의 함수를 호출할 때 컴파일 시에는 해당 주솟값을 미확정인 상태로 두고, 실행 시점에 참조해 로드 한다. 특히 대부분의 재배치 작업은 첫 번째 참조가 이루어지는 시점에 수행되는 일명 지연 바인딩(lazy binding) 기법을 사용한다.

지연 바인딩은 바이너리가 실행되는 시점에서 실제로 호출하는 함수만 재배치하도록 해준다.

리눅스 ELF 바이너리의 지연 바인딩은 PLT(Procedure Linkage Table)와 GOT(Global Offset Table) 섹션을 통해 구현된다.

ELF 바이너리는 .got.plt라는 또 하나의 GOT 섹션을 이용하며, 이는 .plt 섹션과의 연동을 위한 목적이다. 결국 .got.plt 섹션은 보통 .got 섹션과 유사하며 편의상 둘이 같다고 가정하자.

아래 그림은 PLT와 GOT를 이용한 지연 바인딩 과정을 그림으로 나타낸 것이다.

  • Calling a shared library function via the PLT

.plt 영역은 실행 가능한 코드가 담겨 있는 코드 섹션이며, .got.plt는 데이터 섹션이다.

.plt 섹션을 디스어셈블해 살펴보자.

$ objdump -M intel --section .plt -d a.out

a.out:     file format elf64-x86-64

Disassembly of section .plt:

➊ 00000000004003f0 <puts@plt-0x10>:
  4003f0:	ff 35 12 0c 20 00    	push   QWORD PTR [rip+0x200c12]        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4003f6:	ff 25 14 0c 20 00    	jmp    QWORD PTR [rip+0x200c14]        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4003fc:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]

➋ 0000000000400400 <puts@plt>:
  400400:	ff 25 12 0c 20 00    	jmp    QWORD PTR [rip+0x200c12]        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400406:	68 00 00 00 00       	push   0x0 ➌
  40040b:	e9 e0 ff ff ff       	jmp    4003f0 <_init+0x28>

➍ 0000000000400410 <__libc_start_main@plt>:
  400410:	ff 25 0a 0c 20 00    	jmp    QWORD PTR [rip+0x200c0a]        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400416:	68 01 00 00 00       	push   0x1 ➎
  40041b:	e9 d0 ff ff ff       	jmp    4003f0 <_init+0x28>

PLT는 <puts@plt-0x10>:와 같이 구성되어 있다. 다음으로 일련의 함수들이 나타나며 모두 라이브러리 함수 각각에 대한 것이고 스택에 값이 1씩 증가되며 push 한다.

  • PLT를 사용해 동적으로 라이브러리 함수 참조

라이브러리 함수 중 하나인 puts 함수 호출할 때 PLT 구조를 사용해 puts@plt와 같이 호출할 수 있다➊.

PLT 구문은 간접 점프 명령으로 시작하며, .got.plt 섹션에 저장된 주소로 점프한다➋. 지연 바인딩 전에는 이 주소가 puts@plt의 다음 명령어 주소를 가리키고 있다.

스택에 push 하는 값은 PLT 구문에서 함수들을 식별하기 위한 번호다➌.

다음 명령을 통해 default stub으로 이동한다➍.

default stub은 GOT에서 가져온 또 다른 식별자를 스택에 push하고, 동적 링커 부분으로 점프(GOT로 다시 간접 점프)한다➎.

PLT 구문에 의해 스택에 push된 이 식별 정보를 통해 동적 링커는 puts 함수 주소를 찾을 수 있음을 확인하고, puts 함수를 대신 호출한다. 동적 링커는 puts 함수 주소를 찾고 puts@plt와 관련된 GOT 테이블의 항목에 해당 함수의 주소를 연결(기록)한다.

이후 puts@plt를 호출하면 GOT 테이블 내부에는 패치를 통해 실제 puts 함수의 주소가 저장되어 있으므로 PLT 구문으로 점프될 때 (앞서 다룬) 동적 링커 작업을 반복하지 않고 즉시 puts 함수로 점프한다➏.

  • GOT를 사용하는 이유

PLT 코드에 라이브러리 주소를 바로 쓰지 않고 GOT와 함께 사용하는 이유는 보안상의 문제 때문이다. 바이너리에 취약점이 존재하는 경우 공격자가 PLT overwrite를 이용해 exploit 할 수 있기 때문이다. 물론 GOT 내부의 주소를 변경해 공격하는 GOT overwrite 기법도 발표되긴 하였으나 난이도가 더 높다.

또 다른 이유는 공유 라이브러리의 코드 공유성과 관련이 있다.

  • PLT GOT 동작 과정 분석

https://rond-o.tistory.com/216?category=844537

블로그 이미지

wtdsoul

,