[Dreamhack] Background: Library - Static Link vs. Dynamic Link

Sisyphus·2022년 7월 18일
0

Dreamhack - System Hacking

목록 보기
18/49

라이브러리

컴퓨터 시스템에서, 프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 합니다.

예를 들어 C 프로그래밍을 할 때 공통적으로 printf, scanf, streln, memcpy, malloc 등의 함수들을 많이 사용하는데, C언어를 포함한 많은 언어들은 자주 사용되는 함수들의 정의를 묶어서 하나의 라이브러리 파일로 만들고, 이를 여러 프로그램이 공유해서 사용할 수 있도록 지원하고 있습니다.

그래서 우리는 같은 함수를 반복적으로 정의할 필요 없이 그냥 라이브러리를 가져다 사용하면 됩니다.

라이브러리에는 대표적으로 C 표준 라이브러리인 /lib/x86_64-linux-gnu/libc-2.27.so가 있습니다.




링크

컴파일의 마지막 단계로 오브젝트 파일에서 호출된 라이브러리 함수들을 실제 라이브러리의 함수와 연결시켜줍니다.

예시 코드로 자세히 알아보면

#include <stdio.h>

int main() {
  puts("Hello, world!");
  return 0;
}
$ gcc -c hello-world.c -o hello-world.o
$ readelf -s hello-world.o | grep puts
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

오브젝트 파일은 라이브러리 함수들의 정의가 어디 있는지 알지 못하기 때문에, 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 (0x00007fffbe5dd000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1e5b6c0000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f1e5bcb3000)

libc에서 puts의 정의를 찾아 연결하였습니다.




라이브러리와 링크의 종류

동적 링크 : 바이너리를 실행하면 동적 라이브러리가 프로세스에 매핑됩니다. 그리고 실행 중에 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행합니다.

정적 링크 : 바이너리에 정적 라이브러리의 모든 함수가 포함되어 있어서 함수를 호출할 때 자신의 함수를 호출하는 것처럼 호출할 수 있습니다. 이렇게 하면 탐색 비용은 절감되는 거 같지만, 라이브러리를 통째로 포함하고 있어서 용량이 낭비됩니다.


차이점

$ gcc -o static hello-world.c -static	// 정적 컴파일
$ gcc -o dynamic hello-world.c -no-pie	// 동적 컴파일

용량

$ ls -lh ./static ./dynamic
-rwxr-xr-x 1 ion ion 8.1K May  8 23:21 ./dynamic
-rwxr-xr-x 1 ion ion 826K May  8 23:20 ./static

동적 컴파일한 static이 정적 컴파일 한 dynamic 보다 100배 정도 더 크기가 큽니다.


호출 방법

// static
Dump of assembler code for function main:
   0x0000000000400b6d <+0>:     push   rbp
   0x0000000000400b6e <+1>:     mov    rbp,rsp
   0x0000000000400b71 <+4>:     lea    rdi,[rip+0x915cc]        # 0x492144
   0x0000000000400b78 <+11>:    call   0x410120 <puts>
   0x0000000000400b7d <+16>:    mov    eax,0x0
   0x0000000000400b82 <+21>:    pop    rbp
   0x0000000000400b83 <+22>:    ret
End of assembler dump.
// dynamic
Dump of assembler code for function main:
   0x00000000004004e7 <+0>:     push   rbp
   0x00000000004004e8 <+1>:     mov    rbp,rsp
   0x00000000004004eb <+4>:     lea    rdi,[rip+0x92]        # 0x400584
   0x00000000004004f2 <+11>:    call   0x4003f0 <puts@plt>
   0x00000000004004f7 <+16>:    mov    eax,0x0
   0x00000000004004fc <+21>:    pop    rbp
   0x00000000004004fd <+22>:    ret
End of assembler dump.

static에서는 puts가 있는 0x410120을 직접 호출합니다. 하지만 dynamic은 putsplt 주소인 0x4003f0를 호출합니다.

이렇게 호출 방법이 차이 나는 이유는 동적 링크된 바이너리는 함수의 주소를 라이브러리에서 찾아야 하기 때문입니다. plt는 이 과정에서 사용되는 테이블입니다.




PLT & GOT

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

ELF는 GOT라는 테이블을 두고, reslove 된 함수의 주소를 해당 테이블에 저장합니다. 그리고 나중에 다시 해당 함수를 호출하면 저장된 주소를 꺼내서 사용합니다.


runtime resolve

라이브러리 함수를 호출하면, 함수의 이름을 바탕으로 라이브러리에서 심볼을 탐색하고, 해당 함수의 정의를 발견하면 그 주소로 실행 흐름을 옮기는 과정입니다.


예제 코드로 실제 바이너리 동작을 살펴보면

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

#include <stdio.h>

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

resolve 되기 전

$ gdb-pwndbg got
pwndbg> start
pwndbg> got

GOT protection: Partial RELRO | GOT functions: 1

[0x601018] puts@GLIBC_2.2.5 -> 0x4003f6 (puts@plt+6) ◂— push   0 /* 'h' */

실행 직후 GOT를 확인해보면 아직 puts의 주소를 찾기 전이라 puts@plt+6의 주소가 적혀있습니다.

puts@plt를 호출하는 지점에 중단점을 설정하고, 내부로 들어가서 살펴보면

pwndbg> b * main+11
pwndbg> c
pwndbg> si
0x00000000004003f0 in puts@plt ()
pwndbg> ni
pwndbg> ni
pwndbg> ni
pwndbg> ni
pwndbg> ni
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
   0x4003f0       <puts@plt>                         jmp    qword ptr [rip + 0x200c22]    <_GLOBAL_OFFSET_TABLE_+24>

   0x4003f6       <puts@plt+6>                       push   0
   0x4003fb       <puts@plt+11>                      jmp    0x4003e0                      <0x4003e0>
    ↓
   0x4003e0                                          push   qword ptr [rip + 0x200c22]    <_GLOBAL_OFFSET_TABLE_+8>
   0x4003e6                                          jmp    qword ptr [rip + 0x200c24]    <_dl_runtime_resolve_xsavec>
    ↓
 ► 0x7ffff7dea8f0 <_dl_runtime_resolve_xsavec>       push   rbx
   0x7ffff7dea8f1 <_dl_runtime_resolve_xsavec+1>     mov    rbx, rsp
   0x7ffff7dea8f4 <_dl_runtime_resolve_xsavec+4>     and    rsp, 0xffffffffffffffc0
   0x7ffff7dea8f8 <_dl_runtime_resolve_xsavec+8>     sub    rsp, qword ptr [rip + 0x211f09] <_rtld_global_ro+168>
   0x7ffff7dea8ff <_dl_runtime_resolve_xsavec+15>    mov    qword ptr [rsp], rax
   0x7ffff7dea903 <_dl_runtime_resolve_xsavec+19>    mov    qword ptr [rsp + 8], rcx
   pwndbg> ni
   .
   .
   .
   pwndbg> ni
   ──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
   0x7ffff7dea997 <_dl_runtime_resolve_xsavec+167>    mov    rax, qword ptr [rsp]
   0x7ffff7dea99b <_dl_runtime_resolve_xsavec+171>    mov    rsp, rbx
   0x7ffff7dea99e <_dl_runtime_resolve_xsavec+174>    mov    rbx, qword ptr [rsp]
   0x7ffff7dea9a2 <_dl_runtime_resolve_xsavec+178>    add    rsp, 0x18
   0x7ffff7dea9a6 <_dl_runtime_resolve_xsavec+182>    bnd jmp r11
    ↓
 ► 0x7ffff7a62970 <puts>                              push   r13
   0x7ffff7a62972 <puts+2>                            push   r12
   0x7ffff7a62974 <puts+4>                            mov    r12, rdi
   0x7ffff7a62977 <puts+7>                            push   rbp
   0x7ffff7a62978 <puts+8>                            push   rbx
   0x7ffff7a62979 <puts+9>                            sub    rsp, 8

_dl_runtime_resolve_xsavec라는 함수가 실행됩니다. 이 함수에 의해서 puts의 주소가 구해지고, GOT에 주소가 써집니다.

got 명령어로 확인을 해보면

pwndbg> finish
pwndbg> got

GOT protection: Partial RELRO | GOT functions: 1

[0x601018] puts@GLIBC_2.2.5 -> 0x7ffff7a62970 (puts) ◂— push   r13

0x4003f6 (puts@plt+6)0x7ffff7a62970 (puts)로 변경되었습니다.


resolve 된 후

pwndbg> ni
pwndbg> si
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
 ► 0x4003f0       <puts@plt>    jmp    qword ptr [rip + 0x200c22]    <puts>
    ↓
   0x7ffff7a62970 <puts>        push   r13
   0x7ffff7a62972 <puts+2>      push   r12
   0x7ffff7a62974 <puts+4>      mov    r12, rdi
   0x7ffff7a62977 <puts+7>      push   rbp
   0x7ffff7a62978 <puts+8>      push   rbx
   0x7ffff7a62979 <puts+9>      sub    rsp, 8
   0x7ffff7a6297d <puts+13>     call   *ABS*+0x9dc90@plt                <*ABS*+0x9dc90@plt>

   0x7ffff7a62982 <puts+18>     mov    rbp, qword ptr [rip + 0x36bebf] <stdout>
   0x7ffff7a62989 <puts+25>     mov    rbx, rax
   0x7ffff7a6298c <puts+28>     mov    eax, dword ptr [rbp]

두 번째로 puts@plt를 호출할 때는 GOT에 puts 주소가 쓰여있어서 바로 puts가 실행됩니다.




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

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

따라서 앞의 예에서 GOT에 저장된 puts의 주소를 공격자가 임의로 변경할 수 있으면, 두 번째로 puts가 호출될 때 공격자가 원하는 코드가 실행되게 할 수 있습니다.

한번 puts의 GOT 값을 "AAAAAAAA"로 변경해보면

$ gdb-pwndbg got
pwndbg> b * main+23
pwndbg> r
 ► 0x4004fe <main+23>               call   puts@plt                      <puts@plt>
        s: 0x4005b1 ◂— 'Get address from GOT'

   0x400503 <main+28>               mov    eax, 0
   0x400508 <main+33>               pop    rbp
   0x400509 <main+34>               ret

   0x40050a                         nop    word ptr [rax + rax]
   0x400510 <__libc_csu_init>       push   r15
   0x400512 <__libc_csu_init+2>     push   r14
   0x400514 <__libc_csu_init+4>     mov    r15, rdx
   0x400517 <__libc_csu_init+7>     push   r13
   0x400519 <__libc_csu_init+9>     push   r12
   0x40051b <__libc_csu_init+11>    lea    r12, [rip + 0x2008ee]         <__init_array_start>
   pwndbg> set *(unsigned long long*)0x601018 = 0x4141414141414141
   pwndbg> c
   ──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
 ► 0x4003f0 <puts@plt>    jmp    qword ptr [rip + 0x200c22]    <0x4141414141414141>

실행 흐름이 "AAAAAAAA"로 옮겨졌습니다.

이러한 공격 기법을 GOT Overwrite라고 합니다.



Background: Library - Static Link vs. Dynamic Link

0개의 댓글