드림핵 - 시스템 해킹

Static Link vs. Dynamic Link

김가윤 2023. 5. 8. 15:50

1. 라이브러리

 

라이브러리는 컴퓨터 시스템에서,

프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 한다.

 

자주 사용되는 함수들의 정의를 묶어서 하나의 라이브러리 파일로 만들고,

이를 여러 프로그램이 공유해서 사용할 수 있도록 지원하고 있다.


2. 링크

 

많은 프로그래밍 언어에서

컴파일의 마지막 단계로 알려져 있다.

 

프로그램에서 어떤 라이브러리의 함수를 사용한다면,

호출된 함수와  실제 라이브러리의 함수가 링크 과정에서 연결된다.

 

리눅스에서 C 소스 코드는 전처리, 컴파일, 어셈블 과정을 거쳐

ELF 형식을 갖춘 오브젝트 파일(Object file)로 번역된다.

$ 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

 

완전히 컴파일

$ 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")

 

2- 1  라이브러리와 링크의 종류

라이브러리는 크게 동적 라이브러리와 정적 라이브러리로 구분되며,

동적 라이브러리를 링크하는 것을 동적 링크 (Dynamic Link),

정적 라이브러리를 링크하는 것을 정적 링크 (Static Link)라고 부른다.

 

동적 링크

도서관 사용 방법과 동일하다.

 

동적 링크된 바이너리를 실행하면

동적 라이브러리가 프로세스의 메모리에 매핑된다.

 

그리고 실행 중에 라이브러리의 함수를 호출하면

패핑된 라이브러리에서 호출할 함수의 주소를 찾고,

그 함수를 실행한다.

 

이 과정에서 사람이 도서관에 방문해서 원하는 책의 위치를 찾고,

그 책에서 정보를 습득하는 과정과 유사하다.

 

정적 링크

도서관에서 필요한 모든 책을 암기하는 것과 같다.

 

정적 링크를 하면 바이너리에

정적 라이브러리의 필요한 모든 함수가 포함된다.

 

따라서, 해당 함수를 호출할 때,

라이브러리를 참조하는 것이 아니라,

자신의 함수를 호출하는 것처럼 호출할 수 있다.

 

라이브러리에서 호출할 함수를 찾지 않아도 되니

탐색의 비용을 절감되는 듯하지만,

여러 바이너리에서 라이브러리를 사용하면

그 라이브러리의 복제가 여러 번 이루어지게 되므로

용량을 낭비하게 된다.

 

2-2 동적 링크 vs 정적 링크

hello-world.c를 정적 컴파일하며 static,

동적 컴파일하여 dynamic을 생성

// Name: hello-world.c
// Compile: gcc -o hello-world hello-world.c
#include <stdio.h>
int main() {
  puts("Hello, world!");
  return 0;
}

 

용량

ls로 비교해보면, static이 dynamic보다

100배 가까이 더 많은 용량을 차지

$ ls -lh ./static ./dynamic
-rwxr-xr-x 1 dreamhack dreamhack 8.2K Nov  9 10:24 dynamic
-rwxr-xr-x 1 dreamhack dreamhack 825K Nov  9 10:23 static

 

호출 방법

static에서는 puts가 있는 0x410230을 직접 호출

dynamic에서는 puts의 plt 주소인 0x4003f0을 호출

static

 main:
  push   rbp
  mov    rbp,rsp
  lea    rdi,[rip+0x915dc] # 0x492144
  call   0x410230 <puts>
  mov    eax,0x0
  pop    rbp
  ret
  
  dynamic
  
  main: 
 push   rbp
 mov    rbp,rsp
 lea    rdi,[rip+0x92] # 0x400584
 call   0x4003f0 <puts@plt>
 mov    eax,0x0
 pop    rbp
 ret

4. PLT & GOT

PLT (Procedure Linkage Table)와 GOT (Global Offset Table)

라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블이다.

 

바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑된다.

이 상태에서 라이브러리 함수를 호추하면, 함수의 이름을 바탕으로

라이브러리에서 심볼을 탐색하고, 해당 함수의 정의를 발견하면

그 주소로 실행의 흐름을 옮기게 된다.

이 전 과정을 통틀어 runtime resolve라고 한다.

 

그런데 만약 반복적으로 호출되는 함수의 정의를

매번 탐색해야 한다면 비효율적일 것이다.

 

그래서 ELF는 GOT라는 테이블을 두고,

resolve된 함수의 주소를 해당 테이블에 저장한다.

나중에 해당 함수를 호출하면 저장된 주소를 꺼내서 사용한다.

 

got 예제 코드

// 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되기 전

got.c를 컴파일하고, 실행한 직후에 GOT를 확인해보면

아직 puts의 주소를 찾기 전이므로, 함수의 주소가 아닌

put@plt+6라는 PLT의 내부의 주소가 적혀있다.

$ gdb ./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' */

 

put@plt를 호출하는 지점에 중단점을 설정하고, 내부로 따라간다.

PTL에서는 먼저 puts의 GOT인 0x601018에 쓰인 값으로 실행 흐름을 옮긴다.

현재 GOT에는 puts@plt+6의 주소가 쓰여있으므로, 바로 다음 주르이 코드를 실행

pwndbg> b *main+11
pwndbg> c
pwndbg> si
=> 0x4003f0       <puts@plt>                         jmp    qword ptr [rip + 0x200c22] <0x601018>
   0x4003f6       <puts@plt+6>                       push   0
   0x4003fb       <puts@plt+11>                      jmp    0x4003e0 <0x4003e0>
pwndbg> ni
   0x4003f0       <puts@plt>                         jmp    qword ptr [rip + 0x200c22] <0x601018>
=> 0x4003f6       <puts@plt+6>                       push   0
   0x4003fb       <puts@plt+11>                      jmp    0x4003e0 <0x4003e0>

 

여기서 코드를 조금 더 실행시키면

dl_runtime_resolve_xsavec라는 함수가 실행되는데,

이 함수에서 puts의 주소가 구해지고, GOT에 주소가 써진다.

...
pwndbg> ni
   0x4003e6                                          jmp    qword ptr [rip + 0x200c24] <_dl_runtime_resolve_xsavec>
=> 0x7ffff7dea910 <_dl_runtime_resolve_xsavec>       push   rbx
pwndbg> finish
   0x4004f2 <main+11>            call   puts@plt <puts@plt>
=> 0x4004f7 <main+16>            lea    rdi, qword ptr [rip + 0xb3]
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 1
[0x601018] puts@GLIBC_2.2.5 -> 0x7ffff7a62aa0 (puts) ◂— push   r13

 

resolve된 후

두 번째로 puts@plt를 호출할 때는

GOT에 puts의 주소가 쓰여있어서 바로 puts가 실행

 pwndbg> si
 => 0x4003f0       <puts@plt>    jmp    qword ptr [rip + 0x200c22] <puts>
    0x7ffff7a62aa0 <puts>        push   r13

 

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

PLT에서 GOT를 참조하여 실행 흘므을 옮길 때,

GOT의 값을 검증하지 않는다는 보안상의 약점이 있다.

 

따라서 GOT에 저장된 puts의 주소를 공격자가 임의로 변경할 수 있으면,

두 번째로 puts가 호출될 때 공격자가 원하는 코드가 실행되게할 수 있다.

 

gdb를 통해 간단한 실험을 해볼 수 있다.

got 바이너리의 두 번째 puts 호출 직전에

puts의 GOT 값을 "AAAAAAAA"로 변경하고 계속 실행시키면,

실제로 "AAAAAAAA"로 실행 흐름이 옮겨지는 것을 확인할 수 있다.

$ gdb ./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
pwndbg> set *(unsigned long long*)0x601018 = 0x4141414141414141
pwndbg> c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00000000004003f0 in puts@plt ()
0x4003f0 <puts@plt>    jmp    qword ptr [rip + 0x200c22] <0x4141414141414141>

 

이런 공격 기법을 GOT Overwrite라고 부르며,

임의 주소에 값을 쓸 수 있을 때,

RCE를 하기 위한 방법으로도 사용될 수 있다.

 

이후의 Exploit Tech: Return Oriented Programming에서

이 공격 기법과 연계된 실습을 진행

'드림핵 - 시스템 해킹' 카테고리의 다른 글

Exploit Tech: Return to Library  (0) 2023.05.15
NX & ASLR  (0) 2023.05.09
Stack Canary  (0) 2023.05.02
Exploit Tech: Return Address Overwrite  (0) 2023.03.29
Memory Corruption: Stack Buffer Overflow  (0) 2023.03.27