Hacking/Pwnable

공유 라이브러리(PLT, GOT)

GunP4ng 2024. 6. 17. 23:28

공유 라이브러리( PLT, GOT)


1. 라이브러리란?

1. 라이브러리(Library)

프로그램은 공통으로 사용하는 함수들이 있다

printf, scanf, strlen, memcpy, malloc 등 많은 함수들이 있다

이러한 함수들의 정의를 묶어 만든 파일(오브젝트 파일)을 라이브러리라고 한다

 

C의 표준 라이브러리인 libc 는 우분투에 기본으로 탑재된 라이브러리다

 

2. Lazy Binding & Now Binding

ELF(Executable and Linkable Format)

리눅스 기반 시스템의 기본 바이너리 형식

 

Lazy Binding

Dynamic Linking 방식으로 컴파일 된  바이너리에서

함수를 처음 호출할 때 해당 함수의 주소를 공유 라이브러리에서 가져오는 것

 

Now Binding

프로그램이 실행될 때 해당 프로그램에서 사용하는 모든 함수들의 주소를 읽어와 got영역에 저장하는 것이다

 

2. 링크

1. 링크(Link)

리눅스에서 C 소스 코드는 전처리, 컴파일, 어셈블 과정을 거쳐서 ELF 형식을 갖춘 오브젝트 파일(Object file)로 번역된다

C 컴파일 과정

오브젝트 파일은 라이브러리 함수들의 정의의 위치를 모르므로 실행이 불가능하다

링크 과정

라이브러리 등 필요한 오브젝트 파일을 연결시키는 작업을 링킹(Linking)이라고 한다

링크 과정까지 마치면 최종 실행파일이 생긴다

 

2. Static (정적) 링크

Static Link 방식은 파일 생성시 라이브러리 내용을 포함한 실행 파일을 만든다

#include <stdio.h>

void main() {
    puts("pwd");
}

pwd 를 출력하는 간단한 C 코드이다

 

리눅스에서 정적 링크로 파일을 만드려면 -static 옵션을 사용하면 된다

gcc 는 기본적으로 동적 (dynamic)으로 컴파일 한다

gcc -o pg_static pg.c -static
gcc -o pg_dynamic pg.c

파일을 확인해보면 statically linked 로 정적 링크된 것을 확인할 수 있다

 

ls 명령으로 확인해보면 

정적으로 링크된 파일은 실행 파일 안에 모든 코드가 포함되기 때문에 용량이 큰 것을 확인할 수 있다

 

   0x0000000000401775 <+0>:     endbr64
   0x0000000000401779 <+4>:     push   rbp
   0x000000000040177a <+5>:     mov    rbp,rsp
   0x000000000040177d <+8>:     lea    rax,[rip+0x96880]        # 0x498004
   0x0000000000401784 <+15>:    mov    rdi,rax
   0x0000000000401787 <+18>:    call   0x40c170 <puts>
   0x000000000040178c <+23>:    nop
   0x000000000040178d <+24>:    pop    rbp
   0x000000000040178e <+25>:    ret

pg_static 파일의 어셈블리를 확인해보자

static 링크 방식에서는 puts 가 있는 0x40c170  을 직접 호출한다

 

함수 호출 전과 후의 주소가 같은 것을 확인할 수 있다

처음부터 puts 함수의 주소를 가리키고 있고, 주소는 프로그램의 text 영역에 있는 것으로 확인할 수 있다

 

3. Dynamic (동적) 링크

Dynamic link 방식은 공유 라이브러리를 사용한다

라이브러리를 하나의 메모리 공간에 매핑하고 여러 프로그램에서 공유하여 사용한다

 

gcc 는 기본적으로 Dynamic link 방식으로 컴파일한다

 

실행 파일 안에 라이브러리 코드를 포함하지 않기 때문에 static 방식보다 파일 크기가 훨씬 작다

덕분에 실행시에도 적은 메모리를 사용하게 된다

하지만 라이브러리가 없으면 실행할 수 없다는 단점이 있다

 

   0x0000000000001149 <+0>:     endbr64
   0x000000000000114d <+4>:     push   rbp
   0x000000000000114e <+5>:     mov    rbp,rsp
   0x0000000000001151 <+8>:     lea    rax,[rip+0xeac]        # 0x2004
   0x0000000000001158 <+15>:    mov    rdi,rax
   0x000000000000115b <+18>:    call   0x1050 <puts@plt>
   0x0000000000001160 <+23>:    nop
   0x0000000000001161 <+24>:    pop    rbp
   0x0000000000001162 <+25>:    ret

pg_dynamic 파일의 어셈블리를 확인해보자

dynamic 에서는 puts 의 plt 주소0x1050을 호출한다

dynamic link 방식은 함수의 주소를 라이브러리에서 찾아야 하는데,

이 과정에서 사용되는 것이 plt 이다

 

파일을 실행한 후 주소를 확인하면

함수 호출 전과 후의 주소가 다르고, 두 번째 호출부터 puts 의 주소를 가리키고 있다

 

 

3. PLT, GOT

1. PLT & GOT

PLT(Procedure Linkage Table)

외부 라이브러리 함수를 사용할 수 있도록 주소를 연결해주는 테이블이다

PLT를 통해 다른 라이브러리에 있는 함수를 호출해 사용할 수 있다

 

GOT(Global Offset Table)

PLT 에서 호출하는 resolve 함수를 통해 구한 라이브러리 함수의 절대 주소가 저장되어 있는 테이블이다

 

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

라이브러리 함수를 호출하면, 함수의 이름을 바탕으로 라이브러리에서 심볼을 탐색하고,

해당 함수의 정의를 발견하면 그 주소로 실행 흐름을 옮기게 된다

이 과정을 runtime resolve 라고 한다

 

2. dl_resolve

Dynamic Link 방식으로 컴파일 하면 함수를 호출할 때 PLT 를 참조하게 된다

PLT 에서는 GOT로 점프하는데, GOT 에는 라이브러리에 존재하는 실제 함수의 주소가 쓰여있다

_dl_resolve

두 번째 호출이면 GOT 에 실제 함수의 주소가 쓰여있지만, 첫 번째 호출이면 주소가 쓰여있지 않다

그래서 첫 호출 시에는 Linkerdl_resolve 라는 함수를 사용해 필요한 함수의 주소를 알아오고,

GOT 에 써준 후 해당 함수를 호출한다.

 

// Name : got.c
//Compile : gcc -o got got.c -no-pie -fno-stack-protector -m32

#include <stdio.h>

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

위의 코드를 컴파일 하고 실행해보자

 

Dump of assembler code for function main:
   0x08049176 <+0>:     lea    ecx,[esp+0x4]
   0x0804917a <+4>:     and    esp,0xfffffff0
   0x0804917d <+7>:     push   DWORD PTR [ecx-0x4]
   0x08049180 <+10>:    push   ebp
   0x08049181 <+11>:    mov    ebp,esp
   0x08049183 <+13>:    push   ebx
   0x08049184 <+14>:    push   ecx
   0x08049185 <+15>:    call   0x80490b0 <__x86.get_pc_thunk.bx>
   0x0804918a <+20>:    add    ebx,0x2e76
   0x08049190 <+26>:    sub    esp,0xc
   0x08049193 <+29>:    lea    eax,[ebx-0x1ff8]
   0x08049199 <+35>:    push   eax
   0x0804919a <+36>:    call   0x8049050 <puts@plt>
   0x0804919f <+41>:    add    esp,0x10
   0x080491a2 <+44>:    sub    esp,0xc
   0x080491a5 <+47>:    lea    eax,[ebx-0x1fdb]
   0x080491ab <+53>:    push   eax
   0x080491ac <+54>:    call   0x8049050 <puts@plt>
   0x080491b1 <+59>:    add    esp,0x10
   0x080491b4 <+62>:    mov    eax,0x0
   0x080491b9 <+67>:    lea    esp,[ebp-0x8]
   0x080491bc <+70>:    pop    ecx
   0x080491bd <+71>:    pop    ebx
   0x080491be <+72>:    pop    ebp
   0x080491bf <+73>:    lea    esp,[ecx-0x4]
   0x080491c2 <+76>:    ret
End of assembler dump.

main 함수의 어셈블리이다

main() 에서 puts@plt 를 호출하는 지점에 break를 건다

 

3. resolve 되기 전

call 하는 주소로 이동하면 PLT 가 있다

PLT 는 GOT 를 참조하기 때문에 0x804c010 은 GOT 일 것이다

 

GOT 가 맞는 것을 확인할 수 있다

 

_dl_runtime_resolve

si 명령어로 실행흐름을 따라가보면

_dl_runtime_resolve 함수가 호출되는 것을 확인할 수 있다

_dl_rumtime_resolve 함수는 puts 의 실제 주소를 구하고, GOT 에 주소를 저장한다

 

si 명령어로 _dl_runtime_resolve 안으로 진입한 후, finish 명령어로 빠져나오면

puts 의 GOT 엔트리에 라이브러리 영역의 실제 puts 주소인 0xf7df52a0 이 쓰여있는 것을 확인할 수 있다

 

4. resolve 된 후

puts@plt 를 두 번째 호출 할 때 break 를 걸고 확인하면

puts 에 GOT 에 puts 의 실제 주소인 0xf7df52a0 이 쓰여있는 것을 확인할 수 있다

 

 

4. GOT Overwrite

1. GOT Overwrite

PLT 에서 GOT 를 참조할 때, GOT 의 값을 검증하지 않는다

따라서 puts 의 GOT 에 저장된 값을 공격자가 임의로 변경하면

puts 가 호출될 때 원하는 함수가 실행되게 할 수 있다

 

위의 그림과 같이 GOT 에 임의의 값을 Overwrite 하여 실행흐름을 변조하는 공격기법을 

GOT Overwrite 라고 한다

 

 

5. Reference


https://learn.dreamhack.io/66#1

https://blackperl-security.gitlab.io/blog/2016/03/07/2016-03-07-pltgot-01/

https://blackperl-security.gitlab.io/blog/2016/03/08/2016-03-08-pltgot-02/