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

PLT GOT 동작과정 분석 본문

바이너리 분석

PLT GOT 동작과정 분석

topcue 2021. 2. 25. 16:15

Overview

PLT와 GOT의 개념을 정리하고 동적으로 링크된 라이브러리의 함수가 어떻게 호출되는지 상세히 분석한다.


Linkage

알다시피 바이너리에 라이브러리를 링크하는 방식은 동적과 정적 두 가지가 있다.

아래 코드로 간단히 살펴보자.

static link

  • 정적으로 링크 한 경우
$ gcc ex.c -o ex -static

$ file ex
ex: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=3496c197ec7a3f0d08898d9dd5565b51c0d229dc, not stripped

gdb로 disassemble 해보면 함수를 호출할 때 정적인 주소를 이용해 호출하는 것을 알 수 있다.

  • call statically
    gdb-peda$ pd main
    Dump of assembler code for function main:
       0x0000000000400b8d <+0>:	push   rbp
       0x0000000000400b8e <+1>:	mov    rbp,rsp
       0x0000000000400b91 <+4>:	sub    rsp,0x30
       0x0000000000400b95 <+8>:	mov    rax,QWORD PTR fs:0x28
       0x0000000000400b9e <+17>:	mov    QWORD PTR [rbp-0x8],rax
       0x0000000000400ba2 <+21>:	xor    eax,eax
       0x0000000000400ba4 <+23>:	mov    QWORD PTR [rbp-0x30],0x0
       0x0000000000400bac <+31>:	mov    QWORD PTR [rbp-0x28],0x0
       0x0000000000400bb4 <+39>:	mov    QWORD PTR [rbp-0x20],0x0
       0x0000000000400bbc <+47>:	mov    QWORD PTR [rbp-0x18],0x0
       0x0000000000400bc4 <+55>:	lea    rdi,[rip+0xab059]        # 0x4abc24
       0x0000000000400bcb <+62>:	call   0x410480 <puts>
       0x0000000000400bd0 <+67>:	lea    rdi,[rip+0xab05b]        # 0x4abc32
       0x0000000000400bd7 <+74>:	call   0x410480 <puts>
       0x0000000000400bdc <+79>:	lea    rax,[rbp-0x30]
       0x0000000000400be0 <+83>:	mov    rsi,rax
       0x0000000000400be3 <+86>:	lea    rdi,[rip+0xab052]        # 0x4abc3c
       0x0000000000400bea <+93>:	mov    eax,0x0
       0x0000000000400bef <+98>:	call   0x40f7a0 <__isoc99_scanf>
       0x0000000000400bf4 <+103>:	mov    eax,0x0
       0x0000000000400bf9 <+108>:	mov    rdx,QWORD PTR [rbp-0x8]
       0x0000000000400bfd <+112>:	xor    rdx,QWORD PTR fs:0x28
       0x0000000000400c06 <+121>:	je     0x400c0d <main+128>
       0x0000000000400c08 <+123>:	call   0x44b580 <__stack_chk_fail_local>
       0x0000000000400c0d <+128>:	leave
       0x0000000000400c0e <+129>:	ret
    End of assembler dump.

예) call 0x410480 <puts>

정적으로 링크한 경우 바이너리 내부에 함수들의 주소가 있기 때문에 외부 라이브러리에 접근할 필요가 없다.

dynamic link

아무런 옵션도 주지 않고 컴파일하면 동적으로 링크된다.

  • 동적으로 링크 한 경우
$ gcc ex.c -o ex

$ file ex
ex: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e9ea033e071140ffca5ca77208b6697bc79e94e9, not stripped

동적으로 링크하면 함수를 호출할 때 plt를 이용한다.

  • call dynamically use plt
    gdb-peda$ pd main
    Dump of assembler code for function main:
       0x000055555555471a <+0>:	push   rbp
       0x000055555555471b <+1>:	mov    rbp,rsp
       0x000055555555471e <+4>:	sub    rsp,0x30
       0x0000555555554722 <+8>:	mov    rax,QWORD PTR fs:0x28
       0x000055555555472b <+17>:	mov    QWORD PTR [rbp-0x8],rax
       0x000055555555472f <+21>:	xor    eax,eax
       0x0000555555554731 <+23>:	mov    QWORD PTR [rbp-0x30],0x0
       0x0000555555554739 <+31>:	mov    QWORD PTR [rbp-0x28],0x0
       0x0000555555554741 <+39>:	mov    QWORD PTR [rbp-0x20],0x0
       0x0000555555554749 <+47>:	mov    QWORD PTR [rbp-0x18],0x0
       0x0000555555554751 <+55>:	lea    rdi,[rip+0xcc]        # 0x555555554824
       0x0000555555554758 <+62>:	call   0x5555555545d0 
       0x000055555555475d <+67>:	lea    rdi,[rip+0xce]        # 0x555555554832
       0x0000555555554764 <+74>:	call   0x5555555545d0 
       0x0000555555554769 <+79>:	lea    rax,[rbp-0x30]
       0x000055555555476d <+83>:	mov    rsi,rax
       0x0000555555554770 <+86>:	lea    rdi,[rip+0xc5]        # 0x55555555483c
       0x0000555555554777 <+93>:	mov    eax,0x0
       0x000055555555477c <+98>:	call   0x5555555545f0 <__isoc99_scanf@plt>
       0x0000555555554781 <+103>:	mov    eax,0x0
       0x0000555555554786 <+108>:	mov    rdx,QWORD PTR [rbp-0x8]
       0x000055555555478a <+112>:	xor    rdx,QWORD PTR fs:0x28
       0x0000555555554793 <+121>:	je     0x55555555479a 
       0x0000555555554795 <+123>:	call   0x5555555545e0 <__stack_chk_fail@plt>
       0x000055555555479a <+128>:	leave
       0x000055555555479b <+129>:	ret
    End of assembler dump.

예) call 0x5555555545d0 <puts@plt>

동적으로 링크한 경우 라이브러리 함수를 호출하기 위해 외부 라이브러리에 접근해야 한다.


PLT GOT

PLT와 GOT는 흔히 다음과 같이 정의한다.

  • PLT(Procedure Linkage Table): 외부 라이브러리 함수를 사용할 수 있도록 주소를 연결 해주는 테이블.
  • GOT(Global Offset Table): PLT에서 호출하는 resolve() 함수를 통해 구한 라이브러리 함수의 절대 주소가 저장되어 있는 테이블.

PLT와 GOT의 동작 과정을 간단히 요약하면 다음과 같다.

(동적으로 링크 된 바이너리에서 라이브러리 함수인 foo()를 호출한다고 가정하자.)

  1. call foo@plt 형태로 라이브러리 함수인 foo() 호출
  1. PLT에 접근하여 GOT로 점프
  1. 처음 호출한 경우 링커가 dl_resovle() 함수를 통해 foo()의 실제 주소를 알아내어 GOT에 쓴다.
  1. GOT에 있는 foo()의 실제 주소를 이용해 foo() 호출

이후 호출에는 3번 과정에서 바로 실제 함수의 주소로 접근할 수 있다.


x86에서의 PLT와 GOT의 동작 과정은 Hackerz on the Ship에서 자세히 분석하였다.

하지만 x64에서의 동작 과정을 위와 같이 자세히 분석한 글을 찾기 힘들었고, 특히 모든 과정을 하나의 그림으로 표현하면 좋을 것 같아서 직접 그려보았다.

PLT GOT Flow Diagram

원본 파일을 고화질로 확인할 수 있다.

아래는 삽질 과정과 약간의 설명이다.

PLT GOT 동작 과정 분석

아래 plt_got.c를 기준으로 분석(삽질) 했다.

  • plt_got.c
#include <stdio.h>
#include <unistd.h>

int main()
{
    char buf[32];

    puts("call puts()");
    puts("call puts()2");

    read(0, buf, 30);
    read(0, buf, 30);

    putchar(buf);
    putchar(buf);

    return 0;
}

// EOF
  • compile flag
gcc plt_got.c -o plt_got -no-pie

두 번째 이후 호출이나 다른 함수의 호출 등 분석을 위해 여러 함수를 선언했지만, 맨 처음 puts() 함수를 호출하는 과정만 글로 정리한다.

PLT

먼저 main() 함수에서 puts()를 호출하기 직전 상황이다.

0x400605 <main+30>:	call   0x4004d0 <puts@plt>

puts@plt 이후 instruction은 아래와 같다.

0x4004d0 <puts@plt>:    jmp    QWORD PTR [rip+0x200b4a]  # 0x601020
0x4004d6 <puts@plt+6>:  push   0x1
0x4004db <puts@plt+11>: jmp    0x4004b0

점프는 $rip+0x200b4a0x601020가 가리키는 값이다.

0x601020에는 아래와 같은 주소가 있다.

0x601020:	0x00000000004004d6

0x4004d6puts@plt+6의 주소이며, 이후 puts()를 호출할 때는 이 자리에 puts()의 실제 주소가 위치할 것이다.

그렇다면 이 주소가 어떻게 puts()의 실제 주소로 바뀌는 것일까? 이어서 실행해보면 알 수 있다.

reloc_offset

이어서 puts@plt+6에서는 0x1을 stack에 push한다.

이 0x1은 reloc_offset으로, plt_got가 호출하는 라이브러리 함수들 중 puts() 함수가 0x1번째 index 임을 나타낸다.

알다시피 plt_got 바이너리는 puts(), read(), putchar() 함수를 호출하며 mitigation 중 하나인 SSP로 인해 __stack_chk_fail() 함수 또한 호출하여 총 4개의 함수를 호출한다.

이때 가장 먼저 호출한 puts()reloc_offset가 0x0이 아닌 0x1 임을 생각하면 이 reloc_offset은 바이너리가 호출한 순서대로 index를 부여하지 않는다는 것을 알 수 있다.

즉 라이브러리 함수인 libc의 모든 함수들은 정해진 순서가 있으며, 외부 바이너리에서 함수를 호출하면 공유 라이브러리 내부의 순서를 우선시하여 index(reloc_offset)를 부여한다. 특히 정황상 알파벳 사전 순으로 부여하는 것 같다.

(가령 예를 들어 puts()putchar()를 모두 호출하는 경우 putchar의 reloc_offset이 0x0, puts의 reloc_offset이 0x1인데, puts()만 호출하는 경우 puts()의 reloc_offset이 0x0이다.)


이어서 아래 instruction을 실행하자.

0x4004db <puts@plt+11>: jmp    0x4004b0

그리고 jmp 하는 0x4004b0는 다음과 같다.

0x4004b0: push   QWORD PTR [rip+0x200b52]        # 0x601008
0x4004b6: jmp    QWORD PTR [rip+0x200b54]        # 0x601010
0x4004bc: nop    DWORD PTR [rax+0x0]

link_map

먼저 push 하는 0x601008.got.plt section을 가리킨다.

0x00601008:	0x00007ffff7ffe170

이는 link_map 구조체의 주소다. link_map 구조체는 아래와 같이 정의되어 있다.

  • struct link_map
struct link_map
{
    ElfW(Addr) l_addr;         /* Difference between the address in the ELF
                                   file and the addresses in memory.        */
    char *l_name;              /* Absolute file name object was found in.   */
    ElfW(Dyn) *l_ld;           /* Dynamic section of the shared object.     */
    struct link_map *l_next;   /* Chain of loaded objects.                  */
    struct link_map *l_prev;    
};

더 자세한 정보는 여기에서 확인할 수 있다.

실제 메모리는 아래와 같이 해석할 수 있다.

0x7ffff7ffe170: 0x0000000000000000  // l_addr
0x7ffff7ffe178: 0x00007ffff7ffe700  // l_name
0x7ffff7ffe180: 0x0000000000600e20  // l_ld
0x7ffff7ffe188: 0x00007ffff7ffe710  // l_next
0x7ffff7ffe190: 0x0000000000000000  // l_prev

.dynamic section

여기서 l_ld0x00600e20.dynamic section의 주소다.

.dynamic section으로 따라가보자.

gdb-peda$ x/24x 0x0000000000600e20

0x600e20:	0x0000000000000001	0x0000000000000001
0x600e30:	0x000000000000000c	0x0000000000400498
0x600e40:	0x000000000000000d	0x00000000004006f4
0x600e50:	0x0000000000000019	0x0000000000600e10
0x600e60:	0x000000000000001b	0x0000000000000008
0x600e70:	0x000000000000001a	0x0000000000600e18
0x600e80:	0x000000000000001c	0x0000000000000008
0x600e90:	0x000000006ffffef5	0x0000000000400298
0x600ea0:	0x0000000000000005	0x0000000000400360
0x600eb0:	0x0000000000000006	0x00000000004002b8
0x600ec0:	0x000000000000000a	0x0000000000000065
0x600ed0:	0x000000000000000b	0x0000000000000018

이는 Elf64_Dyn 구조체들인데, 정의는 아래와 같다.

typedef struct {
        Elf64_Xword d_tag;
        union {
                Elf64_Xword     d_val;
                Elf64_Addr      d_ptr;
        } d_un;
} Elf64_Dyn;

d_tag와 그에 대응하는 d_val 또는 d_ptr이 어떤 의미를 갖는지는 이 링크에 자세히 나와있다.

STRTAB (.dynstr section)

0x600ea0 주소로 예를 들어보면 0x600ea0에 있는 0x5d_tag에 해당하고, 0x600ea8에 있는 0x00400360는 주소이므로 d_ptr 일 것이다. 아래 표를 참고하자.

d_val이 5이므로 이 구조체는 DT_STRTAB이며, d_und_ptr에 해당한다.

d_ptr로 가보면 다음과 같은 문자열들이 있다.

0x400360: ""
0x400361: "libc.so.6"
0x40036b: "puts"
0x400370: "__stack_chk_fail"
0x400381: "putchar"
0x400389: "read"
0x40038e: "__libc_start_main"
0x4003a0: "GLIBC_2.4"
0x4003aa: "GLIBC_2.2.5"
0x4003b6: "__gmon_start__"
...

이 주소는 .dynstr section으로, 이 바이너리가 사용하는 각종 심볼들의 문자열이 있는 곳이다.


다시 함수 흐름으로 돌아가 보자.

0x4004b0에서 link_map의 주소를 push 한 뒤에 0x601010jmp한다.

0x4004b0: push   QWORD PTR [rip+0x200b52]        # 0x601008
0x4004b6: jmp    QWORD PTR [rip+0x200b54]        # 0x601010
0x4004bc: nop    DWORD PTR [rax+0x0]

이 0x601010은 got와 관련된 영역이다. 여기서 잠시 .got 관련 영역을 정리하고 가자.

.got.plt section

plt_got 바이너리의 got 관련 section을 readelf로 읽어보면 아래와 같다.

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align

$ readelf -S plt_got | grep -A2 got

  [21] .got              PROGBITS         0000000000600ff0  00000ff0
       0000000000000010  0000000000000008  WA       0     0     8
  [22] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000038  0000000000000008  WA       0     0     8

0x00600ff0.got section의 시작 주소이고, 0x00601000.got.plt section의 시작 주소다.

.got.plt section에 대한 설명인 이 링크에서 확인할 수 있다.

설명을 요약하면 .got.plt의 첫 번째 주소부터 GOT[0]과 같이 배열처럼 부른다.

GOT[0]에는 .dynamic segment의 주소가 저장된다.

GOT[1]에는 dynamic linker가 관리하는 data structure의 포인터가 저장된다. 이 data structure는 각 공유 라이브러리의 심볼 테이블에 해당하는 노드를 가리키는 연결 리스트다.

GOT[2]에는 symbol resolution 함수인 _dl_runtime_resolve()의 주소를 저장한다.

gdb로 해당 영역을 확인해보면 아래와 같다.

// 이하 0x10은 .got section
0x600ff0: 0x00007ffff7a03b10  // & __libc_start_main()
0x600ff8: 0x0000000000000000
// 이하 0x38은 .got.plt section
0x601000: 0x0000000000600e20  // & .dynamic section
0x601008: 0x00007ffff7ffe170  // & link_map
0x601010: 0x00007ffff7dea8f0  // & _dl_runtime_resolve_xsavec()
0x601018: ??????????????????
0x601020: ??????????????????
0x601028: ??????????????????
0x601030: ??????????????????

현재 함수 흐름에서 jmp 하려는 주소인 0x601010_dl_runtime_resolve_xsavec()의 주소를 저장하고 있다.

그 아래에 ????로 표시한 곳이 흔히 GOT라고 부르는 테이블이다.

'함수 호출 전에는 PLT+6의 주소가 저장되었다가, 이후 호출 시에는 함수의 실제 주소가 저장된다'고 익히 알려진 그 테이블이다.

어떤 라이브러리 함수도 호출하지 않은 지금은 아래와 같이 plt+6의 주소들이 저장되어 있다.

0x601018: 0x00000000004004c6  # <putchar@plt+6>:
0x601020: 0x00000000004004d6  # <puts@plt+6>:
0x601028:	0x00000000004004e6  # <__stack_chk_fail@plt+6>:
0x601030: 0x00000000004004f6  # <read@plt+6>:

그리고 이 순서는 위에서 언급한 reloc_offset을 부여받는 순서와 일치한다. (라이브러리 내부에서 부여하는 특정 순서. 사전 순으로 추정.)


_dl_runtime_resolve_xsavec()

다시 함수 흐름으로 돌아오면 아래 instruction을 수행하여 0x601010으로 jmp 한다. 즉 _dl_runtime_resolve_xsavec() 함수를 호출한다.

0x4004b6: jmp    QWORD PTR [rip+0x200b54]        # 0x601010
---------------------------------------------------------------
0x601010: 0x00007ffff7dea8f0  // & _dl_runtime_resolve_xsavec()

이 함수는 레지스터와 스택을 설정하고 _dl_fixup() 함수를 호출하는 assembly stub이다.

그리고 이 _dl_fixup() 함수가 실제로 심볼을 resolution 한다. _dl_fixup() 함수를 호출하기 전 인자를 확인해보자.

0x00007ffff7dea95d <+109>:	mov    rsi,QWORD PTR [rbx+0x10]
0x00007ffff7dea961 <+113>:	mov    rdi,QWORD PTR [rbx+0x8]
0x00007ffff7dea965 <+117>:	call   0x7ffff7de2f80 <_dl_fixup>

첫 번째 인자인 rdi0x7ffff7ffe170를 mov 하는데, 이 주소는 앞서 다룬 link_map 구조체의 주소다.

두 번째 인자인 rsi0x1을 mov 하는데, 이 값은 앞서 다룬 reloc_offset이다.

_dl_fixup(&link_map, reloc_offset) 형태로 함수를 호출한다.

_dl_fixup()

여기서부터 함수가 아주 길어진다. 중요한 부분만 살펴보자

먼저 함수 초반에 눈에 띄는 주소들이 보인다.

0x7ffff7de2f8e <_dl_fixup+14>:	mov    rax, QWORD PTR [rdi+0x68]
0x7ffff7de2f92 <_dl_fixup+18>:	mov    rdi, QWORD PTR [rax+0x8]

<_dl_fixup+14>에서 rax에 mov 하는 주소는 .dynamic section의 d_tag(DT_STRTAB)다.

<_dl_fixup+18>에서 DT_STRTAB의 주소 +0x8을 rdi로 mov 한다. 즉 DT_STRTAB의 d_ptr인 STRTAB을 rdi에 저장하는 것이다.

이어서 진행해보자.

0x7ffff7de2f9d <_dl_fixup+29>:	mov    rax,QWORD PTR [rax+0x8]

<_dl_fixup+29>에서 rax에 저장하는 주소는 0x400438이다.

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align

$ readelf -S plt_got | grep -A1 rela.plt
  [10] .rela.plt         RELA             0000000000400438  00000438
       0000000000000060  0000000000000018  AI       5    22     8

이는 .rela.plt section의 주소다.

32비트 환경에서는 JMPREL이라는 구조체로 불린다.

64비트 환경에서는 Elf64_Rela라는 구조체가 있다.

// typedef
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Xword;
typedef int64_t  Elf64_Sxword;

// Relocation entry with explicit addend.
struct Elf64_Rela {
  Elf64_Addr    r_offset;  // Location (file byte offset, or program virtual addr).
  Elf64_Xword   r_info;    // Symbol table index and type of relocation to apply.
  Elf64_Sxword  r_addend;  // Compute value for relocatable field by adding this.
}

실제로 확인해보자.

gdb-peda$ x/3g 0x400438
0x400438: 0x0000000000601018   // r_offset
0x400440: 0x0000000100000007   // r_info
0x400448:	0x0000000000000000   // r_addend

처음에 r_offsetreloc_offset인 줄 알고 ELF 관련 소스코드를 한참이나 찾아다녔다..

놀랍게도 r_offset은 주소가 맞다! (왜 이름이 offset 인가)

이어서 함수 흐름대로 진행해보면 다음과 같다.

0x7ffff7de2f9d <_dl_fixup+29>:	mov    rax,QWORD PTR [rax+0x8]
0x7ffff7de2fa1 <_dl_fixup+33>:	lea    r8,[rax+rdx*8]
0x7ffff7de2fa5 <_dl_fixup+37>:	mov    rax,QWORD PTR [r10+0x70]
0x7ffff7de2fa9 <_dl_fixup+41>:	mov    rcx,QWORD PTR [r8+0x8]
0x7ffff7de2fad <_dl_fixup+45>:	mov    rbx,QWORD PTR [r8]
0x7ffff7de2fb0 <_dl_fixup+48>:	mov    rax,QWORD PTR [rax+0x8]
0x7ffff7de2fb4 <_dl_fixup+52>:	mov    rdx,rcx
0x7ffff7de2fb7 <_dl_fixup+55>:	shr    rdx,0x20

<_dl_fixup+33>에서는 Elf64_Rela 구조체의 주소에서 rdx*8을 더한 주소를 r8에 저장한다.

이때 rdx에는 reloc_offset이었던 0x1에 3을 곱한 0x3이 있다. 그러면 r8에는 puts() 함수에 해당하는 Elf64_Rela 구조체의 주소가 저장된다.

이렇게 reloc_offset을 통해 Elf64_Rela 구조체에 접근하는 과정을 살펴보았다.

<_dl_fixup+41>에서는 putsElf64_Rela 구조체 멤버 중 하나인 r_info를 rcx로 mov 한다.

puts의 r_info0x0000000200000007인데, symbol table index와 relocation type 정보를 포함하고 있다.

특히 상위 바이트인 0x2를 이용해 0x120000000b라는 값을 저장하고 있는 주소 0x4002e8를 구한다. 이 주소는 나중에 dl_lookup_symbol_x()의 인자로 전달한다.

이어서 진행해보면 <_dl_fixup+206>에서 _dl_lookup_symbol_x() 함수를 호출한다.

0x7ffff7de304e <_dl_fixup+206>:	call   0x7ffff7dde260 <_dl_lookup_symbol_x>

_dl_lookup_symbol_x()

_dl_lookup_symbol_x()의 c prototype은 아래와 같다.

lookup_t _dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
                     const ElfW(Sym) **ref,
                     struct r_scope_elem *symbol_scope[],
                     const struct r_found_version *version,
                     int type_class, int flags, struct link_map *skip_map)

인자의 개수가 무려 8개지만 gdb가 상위 3개만 확인하자.

arg[0]: 0x40036b --> 0x735f5f0073747570 ('puts')
arg[1]: 0x7ffff7ffe170 --> 0x0
arg[2]: 0x7fffffffdfe8 --> 0x4002e8 --> 0x120000000b

첫 번째 인자인 undef_name은 찾으려는 함수명인 "puts"의 주소다.

두 번째 인자는 link_map 구조체의 주소다.

세 번째 인자는 symbol_scope로 앞서 구한 0x120000000b를 저장하고 있는 주소다.

_dl_lookup_symbol_x() 함수는 더 길다..

함수 call stack만 살펴보면 다음과 같다.

_dl_lookup_symbol_x() -> do_lookup_x() -> _dl_name_match_p()

이 과정에서 수행하는 중요한 작업은 라이브러리의의 실제 주소와 puts() 함수의 실제 offset을 가져온다는 것이다.

라이브러리의 주소는 rax를 이용해 return 하고 puts 함수의 offset은 stack을 이용해 가져온다.


Patch GOT

다시 _dl_fixup()로 돌아가 보자.

0x7ffff7de307c <_dl_fixup+252>:	add    rax,QWORD PTR [rsi+0x8]

<_dl_fixup+252>에서 rax에 $rsi+0x8의 값을 더한다.

여기서 rax는 0x7ffff79e2000로 실제 라이브러리 함수를 가리키고 있다.

RAX: 0x7ffff79e2000 --> 0x3010102464c457f

gdb-peda$ x/s 0x7ffff79e2000
0x7ffff79e2000:	"\177ELF\002\001\001\003"   // <- ELF 파일 시그니처

그리고 rsi+8에는 어떤 offset이 있다.

x/g $rsi+8
0x7ffff79e8680:	0x0000000000080aa0

이 값은 <_dl_fixup+228>를 수행하면서 stack에서 가져온 값이다.

이어서 라이브러리에 이 offset을 더해보자.

RAX: 0x7ffff7a62aa0 (<_IO_puts>:	push   r13)

드디어 라이브러리에 존재하는 puts() 함수의 진짜 주소를 구했다.

이어서 진행하면 아래 instruction을 수행한다.

0x7ffff7de3092 <_dl_fixup+274>:	mov    QWORD PTR [rbx],rax

이때 rax에는 puts() 함수의 실제 주소가, rbx에는 GOT의 주소가 있다.

RAX: 0x7ffff7a62aa0 (<_IO_puts>:	push   r13)
RBX: 0x601020 --> 0x4004d6 (<puts@plt+6>:	push   0x1)

즉 <_dl_fixup+274> instruction을 수행하면 <puts@plt+6>의 주소를 가지고 있던 기존 GOT를 실제 puts() 함수의 주소로 덮어쓰게 된다.


call puts()

_dl_fixup() 함수의 에필로그가 끝나면 caller였던 _dl_runtime_resolve_xsavec()로 돌아간다.

그리고 곧 아래 instruction을 수행한다.

0x7ffff7dea9a6 <_dl_runtime_resolve_xsavec+182>:	bnd jmp r11

이때 r11에는 puts() 함수의 실제 주소가 들어있다.

R11: 0x7ffff7a62aa0 (<_IO_puts>:	push   r13)

이렇게 puts()까지 수행한다.


Conclusion

동적으로 링크된 라이브러리에서 함수를 호출하는 과정을 살펴보았다.

예전에 PLT GOT를 정리한 블로그들을 보면서 공부했었는데 하나의 그림으로 정리한 블로그가 없어서 아쉬웠다.

그래서 직접 해보았다.. 덕분에 리버싱도 많이 하고 커널 소스코드도 많이 읽어볼 수 있어서 좋았다.


'바이너리 분석' 카테고리의 다른 글

ELF 포맷(The ELF Format)  (0) 2021.09.14
Anatomy of a Binary  (0) 2021.09.14
Fuzzing on OS X m1 with AFL++  (0) 2021.03.10
Fuzz with AFL & Exploit dact (2) Exploit  (0) 2021.02.25
Fuzz with AFL & Exploit dact (1) Fuzzing  (1) 2021.02.25
Comments