dreamhack의 [Background: Library - Static Link vs. Dynamic Link]를 토대로 정리하였으며, 추가적으로 덧붙인 내용입니다.

 

Dynamic Link에서는 PLTGOT이 필요함. Static Link 방식으로 컴파일하면 라이브러리가 프로그램 내부에 있어 별도로 함수의 주소를 알아내는 과정이 필요하지 않음. 그러나 Dynamic Link 방식으로 컴파일하면 라이브러리가 외부에 위치해 함수 주소를 알아오는 과정이 필요하게 됨.

 

PLT(Procedure Linkage Table)이란?

외부 library 함수를 사용할 수 있도록 연결해주는 table

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

 

GOT(Global Offset Table)이란?

PLT가 참조하는 table

PLT에서 호출하는 resolve 함수를 통해 구한 library 함수들의 절대 주소가 저장되어 있음

 


PLT와 GOT의 호출 관계

출처: https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/

Dynamic Link 방식에서 프로그램이 함수를 호출하게 되면 PLT를 가장 먼저 참조하게 됨

PLT에서 GOT로 jump하게 되며, GOT에 기록된 실제 함수 주소(library에 기록되어 있음)를 호출하여 함수가 동작하게 됨

 

이는 1번째 호출인지, 아닌지에 따라 동작 과정에 차이가 있음

바이너리가 실행되면 ASLR에 의해 library가 임의의 주소에 mapping 됨
해당 상태에서 library 함수를 호출하면, 함수의 이름을 바탕으로 library에서 symbol들을 탐색하고 해당 함수의 정의를 발견하였을 때 그 주소로 실행 흐름을 옮김
→ 이 과정을 runtime resolve라고 부름

 

만약, 반복적으로 호출되는 함수 정의를 매번 탐색하게 된다면 이는 매우 비효율적임

이에 GOT이라는 table에 resolve된 함수의 주소를 저장하여 사용하고 있음 → 저장된 주소를 꺼내쓰기만 하면 되기 때문!

 

결론적으로 함수에 대해 1번째 호출하게 된다면, 다음과 같이 호출흐름을 그려볼 수 있음

✨함수 호출 → PLT 이동 → GOT 참조 → PLT로 재이동 → _dl_runtime_resolve 수행 → GOT에 실제 주소 저장 후 실제 함수 주소로 분기

출처: https://bob3rdnewbie.tistory.com/190

 

반대로, 함수의 최초 호출이 진행된 후 재호출하게 된다면 GOT에 저장된 실제 함수 주소를 참조하기만 하면 됨

✨ 함수 호출 → PLT 이동 → GOT 참조 → 실제 함수 주소로 jump

출처: https://bob3rdnewbie.tistory.com/190

 

위의 상황을 모두 조합하여 흐름을 figure로 확인해보면 아래와 같음

출처: https://bnzn2426.tistory.com/27


실습으로 알아보는 resolve 전후 과정

실습 코드

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

#include <stdio.h>

int main() {
  puts("Resolving address of 'puts'.");
  puts("Get address from GOT");
}
  • 코드는 단순히 puts 함수를 2번 호출하고 있음
    • 1번째 puts 함수2번째 puts 함수의 동작 과정은 상이할 것임! (1번째 puts 함수에서는 resolve 과정이 수행되기 이고, 2번째 puts 함수에서는 resolve 과정이 끝난 상태일 것)

 

gdb 동적 분석

main 함수

pwndbg> disass main
Dump of assembler code for function main:
   0x0000000000401136 <+0>:	endbr64 
   0x000000000040113a <+4>:	push   rbp
   0x000000000040113b <+5>:	mov    rbp,rsp
   0x000000000040113e <+8>:	lea    rdi,[rip+0xebf]        # 0x402004
   0x0000000000401145 <+15>:	call   0x401040 <puts@plt>
   0x000000000040114a <+20>:	lea    rdi,[rip+0xed0]        # 0x402021
   0x0000000000401151 <+27>:	call   0x401040 <puts@plt>
   0x0000000000401156 <+32>:	mov    eax,0x0
   0x000000000040115b <+37>:	pop    rbp
   0x000000000040115c <+38>:	ret    
End of assembler dump.
  • main+15, main+27에서 puts 함수를 호출하고 있음

 

puts 함수

pwndbg> disass puts
Dump of assembler code for function puts@plt:
   0x0000000000401040 <+0>:	endbr64 
   0x0000000000401044 <+4>:	bnd jmp QWORD PTR [rip+0x2fcd]        # 0x404018 <puts@got.plt>
   0x000000000040104b <+11>:	nop    DWORD PTR [rax+rax*1+0x0]
End of assembler dump.

resolve되기 전

pwndbg> start
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 1
[0x404018] puts@GLIBC_2.2.5 -> 0x401030 ◂— endbr64

binary를 실행한 직후의 GOT 주소를 확인해보면 0x401030을 가리키고 있음(실제 puts 함수 주소가 아님)

 

1번째 puts 함수를 호출하는 부분(main+15)에 bp를 걸어 puts 함수을 step in 해보면 

pwndbg> b *main+15
pwndbg> c
pwndbg> si
   0x401040       <puts@plt>                        endbr64 
 ► 0x401044       <puts@plt+4>                      bnd jmp qword ptr [rip + 0x2fcd]     <0x401030>
    ↓
   0x401030                                         endbr64 
   0x401034                                         push   0
   0x401039                                         bnd jmp 0x401020                     <0x401020>
    ↓
   0x401020                                         push   qword ptr [rip + 0x2fe2]      <_GLOBAL_OFFSET_TABLE_+8>
   0x401026                                         bnd jmp qword ptr [rip + 0x2fe3]     <_dl_runtime_resolve_xsavec>
    ↓
   0x7ffff7fe7bc0 <_dl_runtime_resolve_xsavec>      endbr64 
   0x7ffff7fe7bc4 <_dl_runtime_resolve_xsavec+4>    push   rbx
   0x7ffff7fe7bc5 <_dl_runtime_resolve_xsavec+5>    mov    rbx, rsp
   0x7ffff7fe7bc8 <_dl_runtime_resolve_xsavec+8>    and    rsp, 0xffffffffffffffc0

puts@plt+4에서 앞서 확인한 0x401030으로 jump하는 것을 볼 수 있음

이후에는 _dl_runtime_resolve_xsavec 함수가 실행되는데, 해당 함수가 실행되면서 puts 함수의 실제 주소가 구해짐

_dl_runtime_resolve_xsavec 함수를 통해 구한 주소는 GOT table에 저장됨

 

pwndbg> finish
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 1
[0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e46420 (puts) ◂— endbr64

GOTputs 함수의 실제 주소가 저장된 것을 확인할 수 있음


resolve된 후

1번째 puts 함수를 호출할 때 _dl_runtime_resolve_xsavec 함수를 통해 실제 puts 함수 주소를 구해 GOT에 저장했음

2번째 puts 함수를 호출할 때에는 1번째 puts 함수를 호출하며 구한 주소가 GOT에 저장되어 있으므로 바로 puts 함수를 실행할 수 있음

pwndbg> b *main+27
pwndbg> c
pwndbg> si
►  0x401040       <puts@plt>      endbr64 
   0x401044       <puts@plt+4>    bnd jmp qword ptr [rip + 0x2fcd]     <puts>
    ↓
   0x7ffff7e46420 <puts>          endbr64 
   0x7ffff7e46424 <puts+4>        push   r14
   0x7ffff7e46426 <puts+6>        push   r13
   0x7ffff7e46428 <puts+8>        push   r12
   0x7ffff7e4642a <puts+10>       mov    r12, rdi
   0x7ffff7e4642d <puts+13>       push   rbp
   0x7ffff7e4642e <puts+14>       push   rbx
   0x7ffff7e4642f <puts+15>       call   *ABS*+0x9f630@plt                <*ABS*+0x9f630@plt>
 
   0x7ffff7e46434 <puts+20>       mov    r13, qword ptr [rip + 0x167b0d]

PLT & GOT의 보안 취약점

PLT에서 GOT을 참조하여 실행 흐름을 옮길 때, GOT의 값을 검증하지 않음

앞선 실습에서 1번째 puts 함수를 호출하며 GOT에 저장한 puts 함수의 실제 주소를 변조할 수 있다면, 2번째 puts 함수를 호출할 때 puts 함수 대신 다른 코드가 실행되도록 할 수 있음(GOT Overwrite)

 

실습 코드를 약간 수정하여 /bin/sh를 호출할 수 있는 get_shell 함수를 삽입함

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

#include <stdio.h>

void get_shell(){
        system("/bin/sh");
}

int main() {
  puts("Resolving address of 'puts'.");
  puts("Get address from GOT");
}
pwndbg> info func
All defined functions:

Non-debugging symbols:
0x0000000000401000  _init
0x0000000000401050  puts@plt
0x0000000000401060  system@plt
0x0000000000401070  _start
0x00000000004010a0  _dl_relocate_static_pie
0x00000000004010b0  deregister_tm_clones
0x00000000004010e0  register_tm_clones
0x0000000000401120  __do_global_dtors_aux
0x0000000000401150  frame_dummy
0x0000000000401156  get_shell
0x0000000000401172  main
0x00000000004011a0  __libc_csu_init
0x0000000000401210  __libc_csu_fini
0x0000000000401218  _fini


pwndbg> disass puts
Dump of assembler code for function puts@plt:
   0x0000000000401050 <+0>:	endbr64 
   0x0000000000401054 <+4>:	bnd jmp QWORD PTR [rip+0x2fbd]        # 0x404018 <puts@got.plt>
   0x000000000040105b <+11>:	nop    DWORD PTR [rax+rax*1+0x0]
End of assembler dump.
pwndbg> b *main+27
Breakpoint 1 at 0x40118d
pwndbg> r
pwndbg> set *(unsigned long long*) 0x404018=0x401156
pwndbg> c
Continuing.
$ ls
got  got.c  Return_Address_Overwrite  ReturnToShellcode  ssp_000  ssp_001
$
  • gdb에서 값을 변경하고 싶을 때에는 set 명령을 이용할 수 있음
    • puts@got(0x404018)get_shell 함수 주소(0x401156)으로 변조하면 2번째 puts 함수를 호출하였을 때 /bin/sh 쉘이 출력되는 것을 확인할 수 있음

새롭게 알게 된 것

libc의 모든 함수들은 정해진 순서가 있으며, 외부 바이너리에서 함수를 호출하면 공유 라이브러리 내부의 순서를 우선시하여 index를 부여함

'Hacking Study > Pwnable' 카테고리의 다른 글

Static Link vs Dynamic Link  (0) 2023.03.02
Stack Canary  (2) 2023.03.01

+ Recent posts