라이브러리, 링크, Plt와 Got

라이브러리
라이브러리는 컴퓨터 시스템에서 프로그램들이 함수, 변수를 공유해서 쓸 수 있게 한다. 그 중 범용적으로 많이 사용되는 함수들은 표준 라이브러리가 제작되어 있다. 백준에서 사용 가능한 그것들
링크
컴파일의 마지막 단계라고 할 수 있다.

사진은 컴파일의 과정인데 링킹은 오브젝트 파일을 실행 파일로 바꾸는 것을 말한다.
오브젝트 파일은 실행 가능한 형식이기는 하지만 라이브러리 함수들의 정의가 어딨는지 알지 못한다.
따라서
//hello-world.c
#include <stdio.h>
int main() {
puts("Hello World!");
return 0;
}
와 같은 소스 코드를
gcc -o hello-world.c -o hello-world.o
로 오브젝트 파일로까지만 바꾸면 소스코드 자체는 라이브러리를 include했음에도 puts의 선언이 있는 stdio.h 심볼의 자세한 정보가 기록되어있지 않다.
❯ readelf -s hello-world.o | grep puts
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
완전한 컴파일을 진행해주면
❯ gcc hello-world.c -o hello-world
❯ ldd hello-world
linux-vdso.so.1 (0x00007101e1c26000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007101e1a05000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007101e1c28000)
puts의 정의를 찾아 연결한 것을 확인할 수 있다. 링크를 거치고 나면 프로그램에서 puts를 호출할 때 puts의 정의가 있는 라이브러리에서 puts의 코드를 찾고 해당 코드를 실행하게 된다.
라이브러리와 링크의 종류
라이브러리는 동적 라이브러리와 정적 라이브러리로 구분되고, 동적 라이브러리를 링크하는 것을 동적 링크, 정적 라이브러리를 링크하는 것을 정적 링크라고 한다.
동적 링크
동적 링크된 바이너리를 실행하면 동적 라이브러리가 프로세스의 메모리에 매핑된다. 실행 중에 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고 함수를 실행한다.
정적 링크
정적 링크를 하면 바이너리에 정적 라이브러리의 필요한 '모든' 함수가 포함된다. 탐색 자체의 비용은 절감될 수 있지만 여러 바이너리에서 라이브러리를 사용하면 복제가 여러 번 이루어지므로 용량의 낭비가 있을 수 있다. 컴파일 옵션에 따라 include 한 헤더의 함수가 모두 포함될 수도 있고 그렇지 않을 수도 있다.
동적 링크와 정적 링크 비교하기
컴파일 옵션을 붙여서
~/hacking ───────────────────────────── max-env at 20:53:25
❯ gcc -o static hello-world.c -static
~/hacking ───────────────────────────── max-env at 21:55:37
❯ gcc -o dynamic hello-world.c -no-pie
와 같이 각각 정적 컴파일, 동적 컴파일을 수행할 수 있다.
각각의 실행파일을 디스어셈블하면
static

정적 컴파일에서는 puts를 직접적으로 호출하지만
dynamic

동적 컴파일에서는 puts@plt을 호출하고 있다. 여기서 plt이란 함수의 주소를 라이브러리에서 찾을 수 있게 하는 테이블이다. 자세한 내용은 아래 설명한다.
PLT & GOT
PLT와 GOT는 아까 간략히 소개한대로 라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블이다.
PLT(Procedure Linkage Table): 외부 라이브러리 함수를 사용할 수 있도록 그 함수가 있는 주소를 프로그램에 연결시켜주는 테이블
GOT(Global Offset Table): PLT가 참조하는 테이블로 '실제 함수'들의 주소는 여기에 들어있음.
그러한 테이블들을 바탕으로 프로그램이 실행되는 동안(런타임)에 특정 코드 요소들을 찾아서 동적으로 연결하는 과정을 runtime resolve라고 한다.
runtime resolve:
바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑
여기서 라이브러리 함수를 호출하면 함수의 이름을 바탕으로 라이브러리에서 심볼 탐색
탐색에서 해당 함수의 정의를 발견하면 함수 주소로 실행 흐름 이동
함수의 주소를 찾아가는 과정에서, 동일한 함수를 여러 번 호출하는 경우 첫 번째는 PLT를 참조하고 PLT가 GOT를 참조해서 실제 함수의 절대 주소를 알아가는데, 두 번째부터는 바로 GOT를 참조하게 된다.

위 과정을 GDB로 추적해 볼 수 있다.
//got.c
#include <stdio.h>
int main() {
puts("Resolving address of 'puts'.");
puts("Get address from GOT");
}
의 코드를
gcc -o got got.c -no-pie
로 컴파일하고 gdb로 열어보자.

main함수에 puts를 두 번 호출하게 되어있는데,


아직은 plt의 어딘가에 대한 정보밖에 없다. 그런데 진행하다보면 <_dl_runtime_resolve_xsavec> 를 발견할 수 있는데 이러한 resolve가 끝나면 실제 함수의 주소가 담기게 된다. ni를 통해 resolve 함수 내부로 진입하고 finish를 통해 빠져나온 후 got를 입력해주면

실제 함수의 주소가 담기는 것을 볼 수 있다.
그러나 PLT가 GOT 엔트리의 값을 참조하여 실행 흐름을 옮길 때, GOT의 값을 검증하지 않는다는 문제가 있다.

set 명령어로 GOT 엔트리에 저장된 값을 임의로 변조해버릴 수 있다.
그 상태로 계속 실행하면

segmentation fault가 발생한다.
이처럼 GOT 엔트리의 값을 오버라이트해서 실행 흐름을 변조하는 공격 기법을 GOT Overwrite이라고 한다.

