haehet

ELF 바이너리 포맷 본문

CS

ELF 바이너리 포맷

haehet 2025. 5. 22. 21:04

이번 글에서는 ELF파일의 포맷에 대해서 중요한 부분만 간략히 알아보도록 하겠다. 

ELF파일을 간략히 나타낸 것

ELF파일은 위와 같이 크게 ELF header, program header, section, section header로 이루어져 있다. 차례대로 알아보자

*program header, section header는 선택적으로 안넣을 수도 있다.

 

1. ELF header

ELF파일 헤더는 ELF 파일임을 나타내는 바이트 배치와 어떤 종류의 ELF파일인 명시되어 있다. 또한 기타 다른 요소들을 파일의 어느 위치에서 찾을 수 있는지 적혀있다.

typedef struct
{
  unsigned char e_ident[16];   // 매직코드 & 다른 정보
  uint16_t      e_type;        // 목적 파일 형식
  uint16_t      e_machine;     // 아키텍쳐
  uint32_t      e_version;     // 목적 파일 버전
  uint64_t      e_entry;       // 엔트리 포인트
  uint64_t      e_phoff;       // 프로그램 헤더 테이블 오프셋
  uint64_t      e_shoff;       // 섹션 헤더 테이블 오프셋
  uint32_t      e_flags;       // 프로세서에 따른 전용 플래그
  uint16_t      e_ehsize;      // ELF 헤더 크기
  uint16_t      e_phentsize;   // 프로그램 헤더 엔트리 크기
  uint16_t      e_phnum;       // 프로그램 헤더 엔트리 수
  uint16_t      e_shentsize;   // 섹션 헤더 엔트리 크기
  uint16_t      e_shnum;       // 섹션 헤더 엔트리 수
  uint16_t      e_shstrndx;    // 섹션 스트링 테이블 인덱스
} Elf64_Ehdr;

 

2. program header 

프로그램 헤더는 바이너리를 세그먼트의 관점에서 해설할 수 있도록 한다. 다음 구조체는 프로그램 속 각각의 세그먼트 마다 하나씩 할당된다. 

typedef struct {
    uint32_t   p_type;    // 세그먼트 타입
    uint32_t   p_flags;   // 세그먼트 플래그(권한 등)
    uint64_t   p_offset;  // 파일 내 세그먼트 시작 위치(바이트 오프셋)
    uint64_t   p_vaddr;   // 메모리 내 세그먼트 시작 주소 (가상 주소)
    uint64_t   p_paddr;   // 물리 주소 (일반적으로 무시됨)
    uint64_t   p_filesz;  // 파일에 저장된 세그먼트 크기
    uint64_t   p_memsz;   // 메모리에 적재될 세그먼트 크기
    uint64_t   p_align;   // 세그먼트 정렬(Alignment)
} Elf64_Phdr;

 

*p_filesz와 p_memsz가 구분되어 있는 이유가 뭘까?

 그건 둘이 다른 경우가 진짜로 있기 때문이다. 예를들어 .bss 섹션은 0으로 초기화된 데이터 부분을 포함한다. 하지만 파일 내에서 굳이 0으로 .bss 섹션을 모두 0으로 채우는 것은 불필요하다. 따라서 이 경우에서는 p_memsz > p_filesz이다. 

 

3. section header

섹션은 ELF 바이너리를 논리적으로 분리 한 것을 말한다. 섹션헤더는 바이너리 내부의 모든 섹션헤더에 대한 정보를 담고 있다.

*위 정보는 프로세스 형태로 수행되는 시점에서는 그다지 필요하지 않다.

typedef struct {
    uint32_t sh_name;       // 섹션 이름(문자열 테이블 인덱스 번호)
    uint32_t sh_type;       // 섹션 타입 
    uint64_t sh_flags;      // 섹션 속성 플래그 
    uint64_t sh_addr;       // 실행시점의 섹션 가상 주소
    uint64_t sh_offset;     // 파일 내 섹션 데이터의 오프셋
    uint64_t sh_size;       // 섹션 크기
    uint32_t sh_link;       // 다른 참조 섹션
    uint32_t sh_info;       // 추가 정보 
    uint64_t sh_addralign;  // 섹션 배치 규칙
    uint64_t sh_entsize;    // 섹션 테이블이 존재하는 경우 엔트리 크기
} Elf64_Shdr;

 

4. section

다음은 hello world를 출력하는 간단한 C 프로그램의 섹션 목록이다. 이 중 중요한 것을 살펴보자.

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        0000000000000318 000318 00001c 00   A  0   0  1
  [ 2] .note.gnu.property NOTE            0000000000000338 000338 000030 00   A  0   0  8
  [ 3] .note.gnu.build-id NOTE            0000000000000368 000368 000024 00   A  0   0  4
  [ 4] .note.ABI-tag     NOTE            000000000000038c 00038c 000020 00   A  0   0  4
  [ 5] .gnu.hash         GNU_HASH        00000000000003b0 0003b0 000024 00   A  6   0  8
  [ 6] .dynsym           DYNSYM          00000000000003d8 0003d8 0000a8 18   A  7   1  8
  [ 7] .dynstr           STRTAB          0000000000000480 000480 00008d 00   A  0   0  1
  [ 8] .gnu.version      VERSYM          000000000000050e 00050e 00000e 02   A  6   0  2
  [ 9] .gnu.version_r    VERNEED         0000000000000520 000520 000030 00   A  7   1  8
  [10] .rela.dyn         RELA            0000000000000550 000550 0000c0 18   A  6   0  8
  [11] .rela.plt         RELA            0000000000000610 000610 000018 18  AI  6  24  8
  [12] .init             PROGBITS        0000000000001000 001000 00001b 00  AX  0   0  4
  [13] .plt              PROGBITS        0000000000001020 001020 000020 10  AX  0   0 16
  [14] .plt.got          PROGBITS        0000000000001040 001040 000010 10  AX  0   0 16
  [15] .plt.sec          PROGBITS        0000000000001050 001050 000010 10  AX  0   0 16
  [16] .text             PROGBITS        0000000000001060 001060 000112 00  AX  0   0 16
  [17] .fini             PROGBITS        0000000000001174 001174 00000d 00  AX  0   0  4
  [18] .rodata           PROGBITS        0000000000002000 002000 000011 00   A  0   0  4
  [19] .eh_frame_hdr     PROGBITS        0000000000002014 002014 000034 00   A  0   0  4
  [20] .eh_frame         PROGBITS        0000000000002048 002048 0000ac 00   A  0   0  8
  [21] .init_array       INIT_ARRAY      0000000000003db8 002db8 000008 08  WA  0   0  8
  [22] .fini_array       FINI_ARRAY      0000000000003dc0 002dc0 000008 08  WA  0   0  8
  [23] .dynamic          DYNAMIC         0000000000003dc8 002dc8 0001f0 10  WA  7   0  8
  [24] .got              PROGBITS        0000000000003fb8 002fb8 000048 08  WA  0   0  8
  [25] .data             PROGBITS        0000000000004000 003000 000010 00  WA  0   0  8
  [26] .bss              NOBITS          0000000000004010 003010 000008 00  WA  0   0  1
  [27] .comment          PROGBITS        0000000000000000 003010 00002b 01  MS  0   0  1
  [28] .shstrtab         STRTAB          0000000000000000 00303b 00010a 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)

 

4.1 .init과 .fini 섹션

.init 센션에는 초기화 작업을 수행하고 바이너리의 다른 코드를 실행가지 전에 선행돼야 하는 실행 코드가 포함된다. 프로그램이 실행 될 때 가장 먼저 .intit 섹션 속 코드가 실행된다. .fini섹션 속 코드는 메인 프로그램의 실행이 완전히 종료된 다음에 실행 되며 프로그램을 정리한다.

 

4.2 .text 섹션

이곳에는  프로그램 속 코드가 저장된다. 다양한 함수의 코드가 포함되어 있는데 main함수부터 프로그래머가 작성한 함수, 초기화 및 종료 후속처리를 수행하는 _start, register_tm_clones, frame_dummy 함수가 있다. 

*이곳에는 AX 권한이 있다. w가 없는 이유는 이곳의 코드를 수행하는 익스플로잇을 방지하기 위해서이다.

 

4.3 .bss, .data, .rodata 섹션

.text 섹션은 수정이 불가능 하기 때문에 변수를 처리하려면 기록이 가능한 섹션이 필요하다. 이 섹션들이 이러한 데이터를 기록하기 위해 존재한다. .rodata섹션은 읽기 전용 데이터를 관리하는 공간으로 상수 값을 저장한다. .data섹션은 초기화된 변수의 기본값이 저장되며 쓰기가능하다. .bss섹션은 초기화 되지 않은 변수들을 위해 예약된 공간이다. 이곳에 존재하는 변수들은 0으로 초기화 되며 해당 섹션에는 쓰기가 가능하다.

4.4 .plt., .got., .got.plt 섹션

이 섹션은 동적 링킹 과정에서 지연바인딩을 지원하기 위해 존재한다. 지연바인딩과 동적링킹은 내용이 길기 때문에 다른 글에서 정리하도록 하겠따.

4.5 .rel.*과 .rela.* 섹션

위 바이너리의 섹션 헤더를 보면 rela.* 형식으로 명명된 몇 개의 섹션들이 보인다. 이 섹션들은 재배치 과정에서 링커가 활용할 정보들을 담고 있다.

 

4.6 .dynamic 섹션

ELF 바이너리가 실행을 위해 준비되고 로드될 때 운영체제와 동적 링커에게 로드맵을 제시하는 역할을 한다.

 

4.7 .init_array와 .fini_array 섹션 

.init_array 섹션은 생성자 역할을 하는 함수들로 연결하는 포인터들의 배열이 담겨있다.  .fini_array 섹션은 소멸자와 관련된 포인터 정보를 담고 있다.

 

4.8 .shstrtab, .symtab, .strtab, .dynsym, .dynstr 섹션

.shstrtab 섹션은 바이너리에서 사용된 모든 섹션의 이름 정보를 포함하고 있는 문자열 배열이다. .symtab 섹션은 심벌 테이블 정보를 포함하고 있고 .strtab는 심벌들의 실제 이름을 담고 있다. .dynsym과 .dynstr 섹션은 앞의 .symtab, .strtab과 유사하지만 정적 링킹 단계가 아닌 동적 링킹에서 필요한 심벌과 스트링 정보를 담고 있다. 

 

이상으로 ELF 바이너리의 포맷을 알아보았다. 기본적인 내용만 담았으니 실제 헤더 속 내용을 더 알고 싶다면 공식 문서를 읽어보도록 하자

 

 

 

 

 

'CS' 카테고리의 다른 글

보안기법 정리  (1) 2025.06.07
컴파일 과정  (0) 2025.05.21