공부중/시스템 해킹

[Dreamhack] Library-Static Link vs. Dynamic Link & Quiz

silver surfer 2024. 9. 18.

많은 정보가 저장되고, 처리되는 컴퓨터 시스템에도 현실 세계의 도서관과 비슷한 라이브러리(Library; 도서관)라는 개념이 있다. 실제 도서관과의 유사점과 차이점에 주목하며 컴퓨터 시스템의 라이브러리가 무엇인지, 어떻게 사용되고, 시스템 해킹의 관점에서 라이브러리가 갖는 중요성은 무엇인지 살펴본다.

 

** 라이브러리

라이브러리

라이브러리는 컴퓨터 시스템에서, 프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 한다. 대개의 프로그램은 서로 공통으로 사용하는 함수들이 많다. 예를 들어 printf, scanf, strlen, memcpy, malloc 등은 많은 C 프로그래머들이 코드를 작성하면서 사용하는 함수이다.

C언어를 비롯하여 많은 컴파일 언어들은 자주 사용되는 함수들의 정의를 묶어서 하나의 라이브러리 파일로 만들고, 이를 여러 프로그램이 공유해서 사용할 수 있도록 지원하고 있다. 라이브러리를 사용하면 같은 함수를 반복적으로 정의해야하는 수고를 덜 수 있어서 코드 개발의 효율이 높아진다.

또한 각 언어에서 범용적으로 많이 사용되는 함수들은 표준 라이브러리가 제작되어있어서 개발자들은 쉽게 해당 함수들을 사용할 수 있다. 대표적으로, C의 표준 라이브러리인 libc는 우분투에 기본으로 탑재된 라이브러리이며, 실습 환경에서는 /lib/x86_64-linux-gnu/libc.so.6에 있다. 처음 코딩을 공부할 때, printf를 정의한 적이 없지만 printf("Hello world") 라는 예제를 컴파일해서, 실행할 수 있는 것은 libc에 이 함수가 이미 정의되어있기 때문이다.

__printf:
   0x0000000000060770 <+0>:	endbr64
   0x0000000000060774 <+4>:	sub    rsp,0xd8
   0x000000000006077b <+11>:	mov    r10,rdi
   0x000000000006077e <+14>:	mov    QWORD PTR [rsp+0x28],rsi
   0x0000000000060783 <+19>:	mov    QWORD PTR [rsp+0x30],rdx
   0x0000000000060788 <+24>:	mov    QWORD PTR [rsp+0x38],rcx
   0x000000000006078d <+29>:	mov    QWORD PTR [rsp+0x40],r8
   0x0000000000060792 <+34>:	mov    QWORD PTR [rsp+0x48],r9
   0x0000000000060797 <+39>:	test   al,al
   0x0000000000060799 <+41>:	je     0x607d2 <__printf+98>
   0x000000000006079b <+43>:	movaps XMMWORD PTR [rsp+0x50],xmm0
   0x00000000000607a0 <+48>:	movaps XMMWORD PTR [rsp+0x60],xmm1
   0x00000000000607a5 <+53>:	movaps XMMWORD PTR [rsp+0x70],xmm2
   0x00000000000607aa <+58>:	movaps XMMWORD PTR [rsp+0x80],xmm3
   0x00000000000607b2 <+66>:	movaps XMMWORD PTR [rsp+0x90],xmm4
   0x00000000000607ba <+74>:	movaps XMMWORD PTR [rsp+0xa0],xmm5
   0x00000000000607c2 <+82>:	movaps XMMWORD PTR [rsp+0xb0],xmm6
   0x00000000000607ca <+90>:	movaps XMMWORD PTR [rsp+0xc0],xmm7
   0x00000000000607d2 <+98>:	mov    rax,QWORD PTR fs:0x28
   0x00000000000607db <+107>:	mov    QWORD PTR [rsp+0x18],rax
   ...

 

** 링크

링크(Link) 전

링크는 많은 프로그래밍 언어에서 컴파일의 마지막 단계로 알려져있다. 프로그램에서 어떤 라이브러리의 함수를 사용한다면, 호출된 함수와 실제 라이브러리의 함수가 링크 과정에서 연결된다.

아래의 코드를 예시로 삼아 자세히 알아보자.

// Name: hello-world.c
// Compile: gcc -o hello-world hello-world.c

#include <stdio.h>

int main() {
  puts("Hello, world!");
  return 0;
}
// Path: /usr/include/stdio.h

...
/* Write a string, followed by a newline, to stdout.

   This function is a possible cancellation point and therefore not
   marked with __THROW.  */
extern int puts (const char *__s);
...

 

리눅스에서 C 소스 코드는 전처리, 컴파일, 어셈블 과정을 거쳐 ELF 형식을 갖춘 오브젝트 파일(Object file)로 번역된다. 다음 명령어로 hello-world.c를 어셈블 할 수 있다.

$ gcc -c hello-world.c -o hello-world.o

 

오브젝트 파일은 실행 가능한 형식을 갖추고 있지만, 라이브러리 함수들의 정의가 어디있는지 알지 못하므로 실행은 불가능하다. 다음 명령어를 실행해보면 puts의 선언이 stdio.h에 있어서 심볼(Symbol; 여기서는 자세히 설명하진 않겠음)로는 기록되어있지만, 심볼에 대한 자세한 내용은 하나도 기록되어있지 않다. 심볼과 관련된 정보들을 찾아서 최종 실행 파일에 기록하는 것이 링크 과정에서 하는 일 중 하나이다.

$ readelf -s hello-world.o | grep puts
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

 

링크 후

예제를 완전히 컴파일하고 다음 명령어를 통해 링크되기 전과 비교해보자. libc에서 puts의 정의를 찾아 연결한 것을 확인할 수 있다.

$ gcc -o hello-world hello-world.c
$ readelf -s hello-world | grep puts
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
    46: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.5
$ ldd hello-world
        linux-vdso.so.1 (0x00007ffec3995000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fee37831000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fee37e24000)

 

여기서 libc를 같이 컴파일하지 않았음에도 libc에서 해당 심볼을 탐색한 것은, libc가 있는 /lib/x86_64-linux-gnu/ 가 표준 라이브러리 경로에 포함되어있기 때문이다. gcc는 소스코드를 컴파일할 때 표준 라이브러리의 라이브러리 파일들을 모두 탐색한다. 하단에 정리된 명령어로 표준 라이브러리의 경로를 확인할 수 있다.

$ ld --verbose | grep SEARCH_DIR | tr -s ' ;' '\n'
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu")
SEARCH_DIR("=/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64")
SEARCH_DIR("=/usr/local/lib64")
SEARCH_DIR("=/lib64")
SEARCH_DIR("=/usr/lib64")
SEARCH_DIR("=/usr/local/lib")
SEARCH_DIR("=/lib")
SEARCH_DIR("=/usr/lib")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib")

 

링크를 거치고 나면 프로그램에서 puts를 호출할 때, puts의 정의가 있는 libc에서 puts의 코드를 찾고, 해당 코드를 실행하게 된다.

 

** 라이브러리와 링크의 종류

라이브러리는 크게 동적 라이브러리와 정적 라이브러리로 구분되며, 동적 라이브러리를 링크하는 것을 동적 링크(Dynamic Link), 정적 라이브러리를 링크하는 것을 정적 링크(Static Link)라고 부른다.

 

동적 링크

프로그램을 사람으로, 라이브러리를 도서관으로 비유했을 때, 동적 링크는 가장 자연스러운 도서관 사용 방법이다. 동적 링크된 바이너리를 실행하면 동적 라이브러리가 프로세스의 메모리에 매핑된다. 그리고 실행 중에 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행한다. 이 과정은 사람이 도서관에 방문해서 원하는 책의 위치를 찾고, 그 책에서 정보를 습득하는 과정과 유사하다.

 

정적 링크

마찬가지의 비유를 했을 때, 정적 링크는 도서관에서 필요한 모든 책을 암기하는 것과 같다. 정적 링크를 하면 바이너리에 정적 라이브러리의 필요한 모든 함수가 포함된다. 따라서 해당 함수를 호출할 때, 라이브러리를 참조하는 것이 아니라, 자신의 함수를 호출하는 것처럼 호출할 수 있다. 라이브러리에서 원하는 함수를 찾지 않아도 되니 탐색의 비용이 절감되는듯 하지만, 여러 바이너리에서 라이브러리를 사용하면 그 라이브러리의 복제가 여러 번 이루어지게 되므로 용량을 낭비하게 된다.

🚩 정적 링크 시 컴파일 옵션에 따라 include 한 헤더의 함수가 모두 포함될 수도 있고 그렇지 않을 수도 있습니다.

이 둘의 차이점에 대해 hello-world.c를 이용하여 간단히 알아보자

 

** 동적 링크 vs. 정적 링크

먼저 앞의 hello-world.c를 정적 컴파일하여 static을, 동적 컴파일하여 dynamic을 생성한다.

$ gcc -o static hello-world.c -static
$ gcc -o dynamic hello-world.c -no-pie

 

용량

각각의 용량을 ls로 비교해보면 static이 dynamic보다 50배 가까이 더 많은 용량을 차지하는 것을 확인할 수 있다.

$ ls -lh ./static ./dynamic
-rwxrwxr-x 1 dreamhack dreamhack  16K May 22 02:01 ./dynamic
-rwxrwxr-x 1 dreamhack dreamhack 880K May 22 02:01 ./static

 

호출 방법

static에서는 puts가 있는 0x40c140을 직접 호출한다. 반면, dynamic에서는 puts의 plt 주소인 0x401040을 호출한다. 이러한 차이가 발생하는 이유는 앞서 이야기했듯, 동적 링크된 바이너리는 함수의 주소를 라이브러리에서 "찾아야"하기 때문이다. plt는 이 과정에서 사용되는 테이블이다.

static

 main:
  push   rbp
  mov    rbp,rsp
  lea    rax,[rip+0x96880] # 0x498004
  mov    rdi,rax
  call   0x40c140 <puts>
  mov    eax,0x0
  pop    rbp
  ret

 

dynamic

main: 
 push   rbp
 mov    rbp,rsp
 lea    rdi,[rip+0xebf] # 0x402004
 mov    rdi,rax
 call   0x401040 <puts@plt>
 mov    eax,0x0
 pop    rbp
 ret

 

 

** PLT & GOT

PLT와 GOT

PLT(Procedure Linkage Table)와 GOT(Global Offset Table)는 라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블이다.

바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑된다. 이 상태에서 라이브러리 함수를 호출하면, 함수의 이름을 바탕으로 라이브러리에서 심볼들을 탐색하고, 해당 함수의 정의를 발견하면 그 주소로 실행 흐름을 옮기게 된다. 이 전 과정을 통틀어 runtime resolve라고 하는데, 이에 대해서는 나중에 자세히 다루도록 하자.

그런데 만약 반복적으로 호출되는 함수의 정의를 매번 탐색해야한다면 비효율적일 것이다. 그래서 ELF는 GOT라는 테이블을 두고, resolve된 함수의 주소를 해당 테이블에 저장한다. 그리고 나중에 다시 해당 함수를 호출하면 저장된 주소를 꺼내서 사용한다.

하단의 예제 코드를 사용하여 실제 바이너리에서 어떻게 이런 동작이 일어나는지 살펴보자.

// Name: got.c
// Compile: gcc -o got got.c -no-pie

#include <stdio.h>

int main() {
  puts("Resolving address of 'puts'.");
  puts("Get address from GOT");
}

 

resolve 되기 전

먼저 got.c를 컴파일하고 실행한 직후에, GOT의 상태를 보여주는 명령어인 got을 사용해본다. puts의 GOT 엔트리인 0x401018에는 아직 puts의 주소를 찾기 전이므로, 함수 주소 대신 .plt 섹션 어딘가의 주소인 0x401030이 적혀있다.

$ gdb ./got
pwndbg> entry
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 1
[0x404018] puts@GLIBC_2.2.5 -> 0x401030 ◂— endbr64

pwndbg> plt
Section .plt 0x401020-0x401040:
No symbols found in section .plt
pwndbg>

 

 

이제 main()에서 puts@plt를 호출하는 지점에 중단점을 설정하고, 내부로 따라가보자.

PLT에서는 먼저 puts의 GOT 엔트리에 쓰인 값인 0x401030으로 실행 흐름을 옮긴다. pwndbg 컨텍스트에서 DISASM 부분은 프로그램에서 명령어가 호출되는 순서인 제어 흐름(Control flow)을 보여주는데, 실행 흐름을 따라가면 _dl_runtime_resolve_fxsave 가 호출될 것임을 알 수 있다.

pwndbg> b *main+18
pwndbg> c
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x40113e <main+8>     lea    rax, [rip + 0xebf]
   0x401145 <main+15>    mov    rdi, rax
 ► 0x401148 <main+18>    call   puts@plt                      <puts@plt>
        s: 0x402004 ◂— "Resolving address of 'puts'."
...
pwndbg> si
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x401040       <puts@plt>                        endbr64
   0x401044       <puts@plt+4>                      bnd jmp qword ptr [rip + 0x2fcd]     <0x401030>
    ↓
   0x401030                                         endbr64
   0x401034                                         push   0
   0x401039                                         bnd jmp 0x401020                     <0x401020>
    ↓
   0x401020                                         push   qword ptr [rip + 0x2fe2]      <_GLOBAL_OFFSET_TABLE_+8>
   0x401026                                         bnd jmp qword ptr [rip + 0x2fe3]     <_dl_runtime_resolve_fxsave>
    ↓
   0x7ffff7fd8be0 <_dl_runtime_resolve_fxsave>      endbr64
   0x7ffff7fd8be4 <_dl_runtime_resolve_fxsave+4>    push   rbx
   0x7ffff7fd8be5 <_dl_runtime_resolve_fxsave+5>    mov    rbx, rsp
   0x7ffff7fd8be8 <_dl_runtime_resolve_fxsave+8>    and    rsp, 0xfffffffffffffff0
...

 

 

여기서 코드를 좀 더 실행시키면 _dl_runtime_resolve_fxsave 라는 함수가 실행되는데, 이 함수에서 puts의 주소가 구해지고, GOT 엔트리에 주소를 쓴다.

실제로 ni 명령어를 반복적으로 수행해서 _dl_runtime_resolve_fxsave 안으로 진입한 후, finish 명령어로 함수를 빠져나오면 puts 의 GOT 엔트리에 libc 영역 내 실제 puts 주소인 0x7ffff7c80e50 가 쓰여있는 모습을 확인할 수 있다.

pwndbg> ni
...
pwndbg> ni
_dl_runtime_resolve_fxsave () at ../sysdeps/x86_64/dl-trampoline.h:67
67  ../sysdeps/x86_64/dl-trampoline.h: No such file or directory.
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x401030                                          endbr64
   0x401034                                          push   0
   0x401039                                          bnd jmp 0x401020                     <0x401020>
    ↓
   0x401020                                          push   qword ptr [rip + 0x2fe2]      <_GLOBAL_OFFSET_TABLE_+8>
   0x401026                                          bnd jmp qword ptr [rip + 0x2fe3]     <_dl_runtime_resolve_fxsave>
    ↓
 ► 0x7ffff7fd8be0 <_dl_runtime_resolve_fxsave>       endbr64
   0x7ffff7fd8be4 <_dl_runtime_resolve_fxsave+4>     push   rbx
   0x7ffff7fd8be5 <_dl_runtime_resolve_fxsave+5>     mov    rbx, rsp
   0x7ffff7fd8be8 <_dl_runtime_resolve_fxsave+8>     and    rsp, 0xfffffffffffffff0
   0x7ffff7fd8bec <_dl_runtime_resolve_fxsave+12>    sub    rsp, 0x240
   0x7ffff7fd8bf3 <_dl_runtime_resolve_fxsave+19>    mov    qword ptr [rsp], rax
...
pwndbg> finish
Run till exit from #0  _dl_runtime_resolve_fxsave () at ../sysdeps/x86_64/dl-trampoline.h:67
Resolving address of 'puts'.
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x401148 <main+18>    call   puts@plt                      <puts@plt>

 ► 0x40114d <main+23>    lea    rax, [rip + 0xecd]
   0x401154 <main+30>    mov    rdi, rax
   0x401157 <main+33>    call   puts@plt                      <puts@plt>

   0x40115c <main+38>    mov    eax, 0
   0x401161 <main+43>    pop    rbp
   0x401162 <main+44>    ret

   0x401163              add    bl, dh
...
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 1
[0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e02ed0 (puts) ◂— endbr64
pwndbg> vmmap 0x7ffff7e02ed0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x7ffff7daa000     0x7ffff7f3f000 r-xp   195000  28000 /usr/lib/x86_64-linux-gnu/libc.so.6 +0x58ed0

 

ni

 

finish
got
vmmap 0x7ffff7c80e50

 

resolve 된 후

puts@plt를 두 번째로 호출할 때는 puts의 GOT 엔트리에 실제 puts의 주소인 0x7ffff7c80e50 가 쓰여있어서 바로 puts가 실행된다.

pwndbg> b *main+33
pwndbg> c
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x401148 <main+18>    call   puts@plt                      <puts@plt>

   0x40114d <main+23>    lea    rax, [rip + 0xecd]
   0x401154 <main+30>    mov    rdi, rax
 ► 0x401157 <main+33>    call   puts@plt                      <puts@plt>
        s: 0x402021 ◂— 'Get address from GOT'
...
pwndbg> si
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x401040       <puts@plt>      endbr64
   0x401044       <puts@plt+4>    bnd jmp qword ptr [rip + 0x2fcd]     <puts>
    ↓
   0x7ffff7e02ed0 <puts>          endbr64
   0x7ffff7e02ed4 <puts+4>        push   r14
   0x7ffff7e02ed6 <puts+6>        push   r13
   0x7ffff7e02ed8 <puts+8>        push   r12
   0x7ffff7e02eda <puts+10>       mov    r12, rdi
   0x7ffff7e02edd <puts+13>       push   rbp
   0x7ffff7e02ede <puts+14>       push   rbx
   0x7ffff7e02edf <puts+15>       sub    rsp, 0x10
   0x7ffff7e02ee3 <puts+19>       call   *ABS*+0xa8720@plt                <*ABS*+0xa8720@plt>
...

 

 

si

 

** 시스템 해킹의 관점에서 본 PLT와 GOT

PLT와 GOT는 동적 링크된 바이너리에서 라이브러리 함수의 주소를 찾고, 기록할 때 사용되는 중요한 테이블이다. 하지만 시스템 해커의 관점에서 보면 PLT에서 GOT를 참조하여 실행 흐름을 옮길 때, GOT의 값을 검증하지 않는다는 보안 상의 약점이 있다.

따라서 만약 앞의 예에서 puts의 GOT 엔트리에 저장된 값을 공격자가 임의로 변경할 수 있으면, puts가 호출될 때 공격자가 원하는 코드가 실행되게 할 수 있다.

GOT 엔트리에 저장된 값을 임의로 변조할 수 있는 수단이 있음을 가정하고, 이 공격 기법이 가능한지 gdb를 이용하여 간단하게 실험을 해볼 수 있다. got 바이너리에서 main() 내 두 번째 puts() 호출 직전에 puts의 GOT의 엔트리를 "AAAAAAAA"로 변경한 후 실행시키면, 실제로 "AAAAAAAA"로 실행 흐름이 옮겨지는 것을 확인할 수 있다.

$ gdb -q ./got
pwndbg> b *main+33
pwndbg> r
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x401157 <main+33>    call   puts@plt                      <puts@plt>
        s: 0x402021 ◂— 'Get address from GOT'

   0x40115c <main+38>    mov    eax, 0
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 1
[0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e02ed0 (puts) ◂— endbr64

pwndbg> set *(unsigned long long *)0x404018 = 0x4141414141414141

pwndbg> got
GOT protection: Partial RELRO | GOT functions: 1
[0x404018] puts@GLIBC_2.2.5 -> 0x4141414141414141 ('AAAAAAAA')
pwndbg> c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401044 in puts@plt ()
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x401044 <puts@plt+4>    bnd jmp qword ptr [rip + 0x2fcd]     <0x4141414141414141>

 

set *(unsigned long long *)0x404018 = 0x4141414141414141
got
c

이와 같이 GOT 엔트리에 임의의 값을 오버라이트(Overwrite)하여 실행 흐름을 변조하는 공격 기법을 GOT Overwrite라고 부른다. 일반적으로 임의 주소에 임의의 값을 오버라이트 하는 수단을 가지고 있을 때 수행하는 공격 기법이다. 이후 Exploit Tech: Return Oriented Programming 강의에서 이 공격기법과 연계된 실습을 진행할 것이다.

 

마치며

마치며🚪

이번 강의에서는 라이브러리, 동적 링크정적 링크, 그리고 PLT&GOT에 대해 배웠습니다. 다음 강의로 넘어가기 전에 배운 내용을 정리해보고, 직접 코드도 분석해보시기 바랍니다.

만약 강의 중간에 다뤘던 runtime resolve가 어떻게 이뤄지고, 구현됐는지 더 자세히 알아보고 싶다면 구글에서 관련 자료를 쉽게 찾아볼 수 있습니다. 드림핵에서도 조만간 심화 로드맵을 통해 이를 분석한 강의를 제공하겠습니다.

다음 강의에서는 라이브러리를 이용하여 NX를 우회하는 공격 기법으로 Return to Libc를 소개하겠습니다. 🚩

 

Q1. 정적 링크된 바이너리에서 처음 함수 func를 호출하면, runtime resolve를 거쳐야 해서 시간이 소모된다

X

 

Q2. GOT에는 runtime resolve를 하기 위한 코드가 들어있다

X

 

Q3. 동적 링크된 바이너리는 정적 링크된 바이너리에 비해 바이너리의 크기가 작다는 장점이 있다

O

 

Q4. PLT에서 GOT를 참조하여 실행 흐름을 옮길 때, GOT의 값은 반드시 라이브러리 함수의 주소를 가리켜야 한다

X

댓글