Notice
Recent Posts
Recent Comments
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

topcue

ELF 포맷(The ELF Format) 본문

바이너리 분석

ELF 포맷(The ELF Format)

topcue 2021. 9. 14. 17:12

리눅스 기반 시스템의 기본 바이너리 형식인 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.hElf64_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 파일 헤더의 각 필드
    • e_ident array

      Magig code: 4바이트의 매직 코드(magic code) b'\x7FELF'

      EI_PAD: 현재는 0으로 예약되어 있다.

      EI_CLASS: 바이너리가 비트 아키텍쳐인지 나타낸다. 32비트는 ELFCLASS32(= 1), 64비트는 ELFCLASS64(= 2)이다.

      EI_DATA: 바이너리의 엔디안 정보를 나타낸다. ELFDATA2LSB(= 1)는 리틀 엔디언, ELFDATA2MSB(= 2)는 빅 엔디언이다.

      EI_VERSION: 바이너리가 생성된 시점에서의 ELF 명세서 버전을 나타낸다. 현재는 EV_CURRENT(= 1)만 유효하다.

      EI_OSABI: ABI(Application Binary Interface)정보를 포함한다. 보통은 0이다.

      EI_ABIVERSION: 바이너리가 컴파일된 운영체제 정보를 포함한다. 보통은 0이다.

    • e_type

    바이너리의 타입을 명시한다.

    ET_REL(재배치 가능한 목적 파일), ET_EXEC(실행 가능한 바이너리), ET_DYN(동적 라이브러리 또는 공유 목적 파일) 등이 있다.

    • e_machine

    바이너리가 수행될 아키텍처 환경을 나타낸다.

    EM_X86_64(64비트의 인텔 x86), EM_386(32비트 x86), EM_ARM(ARM) 등이 있다.

    • e_version

    e_ident의 EI_VERSION 바이트와 비슷한 역할이다. EV_CURRENT(=1)만 유효하다.

    • e_entry

    바이너리의 엔트리 포인트를 나타낸다.

    인터프리터(보통 ld-linux.so)가 바이너리를 메모리에 로드한 뒤에 이 주소로 제어권을 넘긴다.

    • e_phoff, e_shoff

    프로그램 헤더와 섹션 헤더의 오프셋.

    ELF 헤더가 아닌 프로그램 헤더 테이블과 섹션 헤더 테이블 등의 위치는 가변적이므로 해당 위치를 찾기 위한 필드들이 필요하다.

    e_phoff와 e_shoff는 실제 주소를 나타내는 e_entry와 달리 offset을 나타낸다. 만약 바이너리에 프로그램 헤더나 섹션 헤더 테이블이 없다면 0으로 설정된다.

    • e_flags

    바이너리가 컴파일된 아키텍쳐 정보를 나타낸다. (인텔 x86 바이너리는 0)

    • e_ehsize

    ELF 헤더의 크기를 바이트 단위로 나타낸다.

    32비트 x86 바이너리는 52바이트, 64비트 x86 바이너리는 64바이트로 고정이다.

    • e_*entsize, e_*num

    헤더 테이블의 크기와 개수.

    링커나 로더가 프로그램 헤더와 섹션 헤더 테이블을 참조하기위해 각 테이블 크기에 대한 정보와 각 테이블 내의 헤더 개수를 알아야 한다.

    프로그램 헤더 테이블의 정보는 e_phentsuze와 e_phnum, 섹션 헤더 테이블의 정보는 e_shentsize와 e_shnum 필드에서 참조한다.

    • e_shstrndx

    섹션 헤더 테이블 중 .shstrtab이라는 문자열 테이블 섹션과 관련된 인덱스 역할을 한다.

Section Headers

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

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

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

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

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

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

  • /usr/include/elf.hElf64_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;
  • 섹션 헤더의 각 필드
    • sh_name 필드

    이름이 저장되어 있는 문자열 테이블상의 인덱스를 의미한다. 인덱스는 ELF 헤더의 e_shstrndx 필드에 대응되는 문자열 테이블을 따른다. 섹션의 이름이 없으면 0이다.

    • sh_type 필드

    모든 섹션은 테이블 가지고 있으며 sh_type 필드에 표기된 숫자로 구분된다.

    • SHT_PROGBITS

      기계어 명령이나 상수값 등의 데이터를 포함하고 있다. 이런 섹션은 링커가 분석해야 할 별도의 구조를 가지고 있지 않다.

    • SHT_SYMTAB, SHT_DYNSYM, SHT_STRTAB

      심볼 테이블을 위한 섹션 타입(정적 심볼 테이블을 위한 SHT_SYMTAB, 동적 링킹 시에 필요한 심볼 테이블을 위한 SHT_DYNSYM)도 있고, 문자열 테이블(SHT_STRTAB)도 있다.

      심볼 테이블에는 파일 오프셋 또는 주소에 위치한 심볼의 명칭과 타입 정보를 명시해 둔 잘 정의된 형식의 심볼 정보가 포함된다. (struct Elf64_Sym 참고)

    • SHT_REL, SHT_RELA

      이 타입의 섹션은 링커에게 아주 중요한데, 링커가 다른 섹션들 간의 필수적인 재배치 관계를 파악할 수 있도록 하고자 잘 정의된 형식(struct Elf64_Rel과 struct Elf64_Sym 참고)에 맞춰 재배치 엔트리 정보를 제공한다.

      각각의 재배치 엔트리 정보는 재배치가 필요한 부분의 주소와, 재배치 시 해결해야 하는 심볼 정보를 포함한다. 이 두 타입의 섹션은 정적 링킹을 위한 목적으로 사용된다.

    • SHT_DYNAMIC

      이 타입의 섹션은 동적 링킹에 필요한 정보를 담고 있다. (struct Elf64_Dyn 참고)

    • sh_flags

      섹션과 관련된 추가 정보를 제공한다.

      • SHF_WRITE

      실행 시점에 해당 섹션이 쓰기 가능한 상태임을 나타낸다. 이 정보를 통해 정적 데이터(상수값 등)에 해당하는 섹션과 변수 값을 저장하는 섹션을 구분할 수 있다.

      • SHF_ALLOC

      바이너리가 실행될 때 해당 섹션의 정보가 가상 메모리에 적재된다는 의미다. (실제로는 섹션이 아닌 세그먼트 단위로 처리)

      • SHF_EXECINSTR

      실행 가능한 명령어들을 담고 있는 섹션임을 의미한다.

    • sh_addr, sh_offset, sh_size

    sh_addr는 가상 메모리의 주소, sh_offset은 파일 오프셋, sh_size는 섹션의 크기를 나타낸다.

    • sh_link

    관련된 섹션 헤더 테이블상 섹션들의 인덱스 정보들을 표기한다.

    • sh_info

    섹션의 추가적인 정보를 제공한다.

    • sh_addralign

    배치 관련 규칙들이 명시된다.

    • sh_entsize

    심볼 테이블이나 재배치 테이블과 같은 일부 섹션들은 잘 설계된 자료 구조(Elf64_Sym 또는 Elf64_Rela) 형태로 테이블을 갖는다. 이런 섹션들에는 해당 테이블의 각 엔트리의 크기가 몇 바이트인지를 명시하는 sh_entsize 필드가 존재한다. 사용하지 않는다면 0이다.

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 섹션의 시작 주소를 나타낸다.

    JUMP_SLO(jump slots): .got.plt 섹션에 오프셋이 있으며 라이브러리 함수의 주소가 연결될 수있는 슬롯을 나타냅니다. 엔트리는 점프 슬롯(jump slot)이라고 부른다.

    이 엔트리의 오프셋은 해당 함수에서 간접 점프하는 점프 슬롯의 주소(rip로부터의 상대 주소로 계산)다.

  • .dynamic 섹션은 바이너리가 로드될 때 운영체제와 동적 링커에게 일종의 road map을 제시하는 역할을 한다. 일명 태그(tag)라고 하는 Elf64_dyn 구조의 테이블을 포함한다. 태그는 번호로 구분한다.

    DT_NEEDED 태그는 바이너리와 의존성 관계를 가진 정보를 동적 링커에게 알려준다.

    DT_VERNEED와 DT_VERNEEDNUM 태그는 버전 의존성 테이블(version dependency table)의 시작 주소와 엔트리 수를 지정한다.

    게다가 동적 링커의 수행에 필요한 중요한 정보들을 가리키는 역할을 하기도 한다. 예를 들어 동적 문자열 테이블(DT_STRTAB), 동적 심벌 테이블(DT_SYMTAB), .got.plt 섹션(DT_PLTGOT), 동적 재배치 섹션(DT_RELA) 등.

  • .shstrtab 섹션은 섹션의 이름을 포함하는 문자열 배열이다. 각 이름들을 숫자로 인덱스가 매겨져 있다.
  • .symtab 섹션에는 Elf64_Sym 구조체의 테이블인 심벌 테이블이 포함되어 있다. 각 심벌 테이블은 심벌명을 함수나 변수와 같이 코드나 데이터와 연관시킨다.
  • .strtab 섹션에는 심벌 이름을 포함한 실제 문자열들이 위치한다. 이 문자열들 Elf64_Sym 테이블과 연결된다. 스트립 된 바이너리에는 .symtab과 .strtab 테이블은 전부 삭제된다.
  • .dynsym 섹션과 .dynstr 섹션은 동적 링킹에 필요한 심벌과 문자열 정보를 담고 있다는 점을 제외하면 .symtab이나 .strtab와 유사하다.

    정적 심벌 테이블은 섹션 타입이 SHT_SYMTAB이고 동적 심벌 테이블은 SHT_DYNSYM 타입이다.

Program Headers

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

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

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

  • /usr/include/elf.hElf64_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


참고 및 인용

[1] https://practicalbinaryanalysis.com/

[2] http://www.acornpub.co.kr/book/binary-analysis

[3] https://hexterisk.github.io/blog/posts/


Comments