컴퓨터 시스템에서, 프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 합니다.
예를 들어 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은 puts
의 plt
주소인 0x4003f0를 호출합니다.
이렇게 호출 방법이 차이 나는 이유는 동적 링크된 바이너리는 함수의 주소를 라이브러리에서 찾아야 하기 때문입니다. plt
는 이 과정에서 사용되는 테이블입니다.
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를 참조하여 실행 흐름을 옮길 때, 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라고 합니다.