GunP4ng 2024. 10. 13. 23:37

PIE (Position Independent Executable)


1. PIE

PIE (Position Independent Executable) 란?

전체가 위치 독립 코드(PIC) 로 이뤄진 실행 가능한 바이너리이다.

 

무작위 주소에 매핑되어도 실행 가능한 실행 파일이다.

ASLR 이 코드 영역에도 적용되게 해주는 기술이다.

→ ASLR 을 적용해도 코드 영역의 주소는 변하지 않는다.

 

PIE 는 재배치가 가능하기 때문에,

ASLR 이 적용된 시스템에서는 실행 파일도 무작위 주소에 적재된다.

 

반대로, ASLR 이 적용되지 않은 시스템에서는 PIE 가 적용된 바이너리라도 무작위 주소에 적재되지 않는다.

 

리눅스는 기본적으로 ASLR 이 적용된 상태이기 때문에

PIE 가 적용되면 바이너리가 실행될 때마다 바이너리의 주소가 랜덤화된다.

 

// Name : address.c

#include <stdio.h>

char *buf = "Hello";

void func() {
    printf("Hello\n");
}

void main(){
    printf("[.data] : %p\n",buf);
    printf("[Function] : %p\n",func);
}

buf 의 위치와 func 함수의 위치를 출력하는 코드이다.

위 코드를 이용하여 PIE 가 적용됐을 때와 그렇지 않을 때를 비교해보자

 

 

2. PIE 비교하기

1. PIE 적용

$ gcc -o pieaddress address.c

gcc 는 기본적으로 PIE 를 적용하여 컴파일 한다.

  • -fPIE (컴파일 옵션)
  • -pie (링커 옵션)

위의 옵션을 사용하여도 PIE 를 적용하여 컴파일 할 수 있다.

-fPIE 옵션은 컴파일 단계에서 수행되고, -pie 옵션은 링킹 단계에서 수행된다.

-pie 옵션을 주지 않고 -fPIE 옵션만 준다면 바이너리에 PIE 가 적용되지 않는다.

 

checksec 명령어로 확인해보자

[*] '/home/gunp4ng/pwnable/pie/pieaddress'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

PIE enabled 로 PIE 가 적용된 것을 볼 수 있다.

 

file 명령어로 확인해보자

pie executable

PIE 가 적용된 파일은 "pie executable"로 뜨는 것을 알 수 있다.

 

파일을 실행해보자

pieaddress

파일을 실행할 때마다 전역변수와 사용자 정의 함수의 주소가 변경된다.

 

gdb 로 어셈블리를 확인해보자

Dump of assembler code for function main:
   0x0000000000001183 <+0>:     endbr64
   0x0000000000001187 <+4>:     push   rbp
   0x0000000000001188 <+5>:     mov    rbp,rsp
   0x000000000000118b <+8>:     mov    rax,QWORD PTR [rip+0x2e7e]        # 0x4010 <buf>
   0x0000000000001192 <+15>:    mov    rsi,rax
   0x0000000000001195 <+18>:    lea    rax,[rip+0xe6e]        # 0x200a
   0x000000000000119c <+25>:    mov    rdi,rax
   0x000000000000119f <+28>:    mov    eax,0x0
   0x00000000000011a4 <+33>:    call   0x1070 <printf@plt>
   0x00000000000011a9 <+38>:    lea    rax,[rip+0xffffffffffffffb9]        # 0x1169 <func>
   0x00000000000011b0 <+45>:    mov    rsi,rax
   0x00000000000011b3 <+48>:    lea    rax,[rip+0xe5e]        # 0x2018
   0x00000000000011ba <+55>:    mov    rdi,rax
   0x00000000000011bd <+58>:    mov    eax,0x0
   0x00000000000011c2 <+63>:    call   0x1070 <printf@plt>
   0x00000000000011c7 <+68>:    nop
   0x00000000000011c8 <+69>:    pop    rbp
   0x00000000000011c9 <+70>:    ret
End of assembler dump.

코드가 매우 작은 주소에 위치하는 것을 볼 수 있다.

→ 메모리의 어느 위치에 매핑되어도 작동하도록 상대주소로 되어있기 때문이다.

 

2. PIE 적용 X

$ gcc -no-pie -o address address.c

gcc 에서 -no-pie 옵션을 사용하면 PIE 를 적용하지 않고 컴파일 할 수 있다.

 

checksec 명령어로 확인해보자

[*] '/home/gunp4ng/pwnable/pie/address'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

No PIE 로 PIE 가 적용되지 않은 것을 알 수 있다.

 

file 명령어로 확인해보자

No PIE

PIE 가 적용되지 않은 파일은 "executable" 인 것을 알 수 있다.

 

파일을 실행해보자

address

파일을 여러번 실행해도 전역변수와 사용자 정의 함수의 주소가 변하지 않는다.

 

gdb 로 어셈블리를 확인해보자

Dump of assembler code for function main:
   0x0000000000401170 <+0>:     endbr64
   0x0000000000401174 <+4>:     push   rbp
   0x0000000000401175 <+5>:     mov    rbp,rsp
   0x0000000000401178 <+8>:     mov    rax,QWORD PTR [rip+0x2eb9]        # 0x404038 <buf>
   0x000000000040117f <+15>:    mov    rsi,rax
   0x0000000000401182 <+18>:    lea    rax,[rip+0xe81]        # 0x40200a
   0x0000000000401189 <+25>:    mov    rdi,rax
   0x000000000040118c <+28>:    mov    eax,0x0
   0x0000000000401191 <+33>:    call   0x401060 <printf@plt>
   0x0000000000401196 <+38>:    lea    rax,[rip+0xffffffffffffffb9]        # 0x401156 <func>
   0x000000000040119d <+45>:    mov    rsi,rax
   0x00000000004011a0 <+48>:    lea    rax,[rip+0xe71]        # 0x402018
   0x00000000004011a7 <+55>:    mov    rdi,rax
   0x00000000004011aa <+58>:    mov    eax,0x0
   0x00000000004011af <+63>:    call   0x401060 <printf@plt>
   0x00000000004011b4 <+68>:    nop
   0x00000000004011b5 <+69>:    pop    rbp
   0x00000000004011b6 <+70>:    ret
End of assembler dump.

정해진 주소에 코드가 위치하는 것을 볼 수 있다.

전역변수 buf 와 사용자 정의 함수 func 도 절대주소로 고정되어 있다.

 

 

3. PIE 우회

1. code base

데이터 영역에 접근하기 위해서는 바이너리가 적재된 주소를 알아야 한다.

이 주소를 PIE base 또는 Code base 라고 한다.

 

코드 영역의 임의 주소를 읽고 오프셋을 빼면 구할 수 있다.

 

2. Partial Overwrite

코드 베이스를 구하지 못할 때 반환 주소의 일부 바이트만 덮는 공격을 할 수 있다.

이런 공격 기법을 Partial Overwrite 라고 한다.

 

ASLR 은 하위 12비트 값이 항상 같다는 특성이 있다.

→ 코드 영역의 주소도 하위 12비트 값은 항상 같다.

 

코드 가젯의 주소가 반환 주소와 한 바이트만 다르면, 이 값을 덮어서 원하는 코드를 실행할 수 있다.

만약 두 바이트 이상이 다르면 브루트 포싱을 통해 공격해야 한다.