[gcc] gcc 및 ld의 위치 독립적 실행 파일에 대한 -fPIE 옵션은 무엇입니까?

함수 호출과 같이 코드를 어떻게 변경합니까?



답변

PIE는 실행 파일에서 ASLR (Address Space Layout Randomization) 을 지원 합니다.

PIE 모드가 생성되기 전에는 프로그램의 실행 파일을 메모리의 임의 주소에 배치 할 수 없었으며 위치 독립적 코드 (PIC) 동적 라이브러리 만 임의 오프셋으로 재배치 할 수있었습니다. PIC가 동적 라이브러리에 대해 수행하는 작업과 매우 유사하게 작동합니다. 차이점은 PLT (프로 시저 연결 테이블)가 생성되지 않고 대신 PC 기준 재배치가 사용된다는 것입니다.

gcc / linkers에서 PIE 지원을 활성화하면 프로그램 본문이 위치 독립적 코드로 컴파일되고 링크됩니다. 동적 링커는 동적 라이브러리와 마찬가지로 프로그램 모듈에서 전체 재배치 처리를 수행합니다. 글로벌 데이터의 모든 사용은 GOT (Global Offsets Table)를 통한 액세스로 변환되고 GOT 재배치가 추가됩니다.

PIE는 이 OpenBSD PIE 프레젠테이션 에서 잘 설명되어 있습니다.

이 슬라이드 에는 기능 변경 사항이 나와 있습니다 (PIE 대 PIC).

x86 그림 대 파이

로컬 전역 변수 및 함수는 파이에서 최적화됩니다.

외부 전역 변수 및 함수는 pic과 동일합니다.

과에서 이 슬라이드 (이전 스타일의 연결 대 PIE)

x86 파이 대 플래그 없음 (고정)

지역 전역 변수 및 함수는 고정과 유사합니다.

외부 전역 변수 및 함수는 pic과 동일합니다.

PIE는 다음과 호환되지 않을 수 있습니다. -static


답변

최소 실행 가능 예 : GDB 실행 파일 두 번

몇 가지 작업을보고 싶은 사람들을 위해 ASLR이 PIE 실행 파일에서 작동하고 실행간에 주소를 변경하는 것을 살펴 보겠습니다.

main.c

#include <stdio.h>

int main(void) {
    puts("hello");
}

main.sh

#!/usr/bin/env bash
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
for pie in no-pie pie; do
  exe="${pie}.out"
  gcc -O0 -std=c99 "-${pie}" "-f${pie}" -ggdb3 -o "$exe" main.c
  gdb -batch -nh \
    -ex 'set disable-randomization off' \
    -ex 'break main' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    "./$exe" \
  ;
  echo
  echo
done

이있는 사람에게는 -no-pie모든 것이 지루합니다.

Breakpoint 1 at 0x401126: file main.c, line 4.

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126

실행을 시작하기 전에 break main에서 중단 점을 설정합니다 0x401126.

그런 다음 두 실행 중에 runaddress 에서 중지합니다 0x401126.

함께 일 -pie하지만 훨씬 더 재미있다 :

Breakpoint 1 at 0x1139: file main.c, line 4.

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x5630df2d6139

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x55763ab2e139

실행을 시작하기 전에 GDB는 실행 파일에있는 “더미”주소를 사용합니다 : 0x1139.

그러나 시작 후 GDB는 동적 로더가 프로그램을 다른 위치에 배치하고 첫 번째 중단이 0x5630df2d6139.

그런 다음 두 번째 실행에서도 실행 파일이 다시 이동하여 0x55763ab2e139.

echo 2 | sudo tee /proc/sys/kernel/randomize_va_spaceASLR이 켜져 있는지 확인합니다 (Ubuntu 17.10의 기본값) : ASLR (주소 공간 레이아웃 무작위 화)을 일시적으로 비활성화하려면 어떻게해야합니까? | Ubuntu에 문의하십시오 .

set disable-randomization off그렇지 않으면 GDB는 이름에서 알 수 있듯이 기본적으로 프로세스에 대해 ASLR을 해제하여 디버깅 환경을 개선하기 위해 실행간에 고정 주소를 제공합니다. gdb 주소와 “실제”주소의 차이? | 스택 오버플로 .

readelf 분석

또한 다음 사항도 관찰 할 수 있습니다.

readelf -s ./no-pie.out | grep main

실제 런타임로드 주소를 제공합니다 (pc는 4 바이트 뒤에 다음 명령어를 가리킴).

64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main

동안:

readelf -s ./pie.out | grep main

오프셋 만 제공합니다.

65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main

ASLR을 끄면 ( randomize_va_space또는 사용 set disable-randomization off) GDB는 항상 main주소를 제공 0x5555555547a9하므로 -pie주소가 다음에서 구성 되었다고 추론합니다 .

0x555555554000 + random offset + symbol offset (79a)

TODO Linux 커널 / glibc 로더 / 어디에서 0x555555554000 하드 코딩 된 위치는 어디입니까? Linux에서 PIE 실행 파일의 텍스트 섹션 주소는 어떻게 결정됩니까?

최소 조립 예

우리가 할 수있는 또 다른 멋진 일은 PIE의 의미를보다 구체적으로 이해하기 위해 어셈블리 코드를 가지고 노는 것입니다.

Linux x86_64 독립 어셈블리 hello world로이를 수행 할 수 있습니다.

main.S

.text
.global _start
_start:
asm_main_after_prologue:
    /* write */
    mov $1, %rax   /* syscall number */
    mov $1, %rdi   /* stdout */
    mov $msg, %rsi  /* buffer */
    mov $len, %rdx /* len */
    syscall

    /* exit */
    mov $60, %rax   /* syscall number */
    mov $0, %rdi    /* exit status */
    syscall
msg:
    .ascii "hello\n"
len = . - msg

GitHub 업스트림

그리고 다음과 같이 잘 조립되고 실행됩니다.

as -o main.o main.S
ld -o main.out main.o
./main.out

그러나 PIE로 연결하려고하면 ( --no-dynamic-linker에 설명 된대로 필요합니다 : Linux에서 정적으로 연결된 위치 독립적 실행 가능 ELF를 만드는 방법? ) :

ld --no-dynamic-linker -pie -o main.out main.o

그런 다음 링크가 실패합니다.

ld: main.o: relocation R_X86_64_32S against `.text' can not be used when making a PIE object; recompile with -fPIC
ld: final link failed: nonrepresentable section on output

라인 때문에 :

mov $msg, %rsi  /* buffer */

mov피연산자 의 메시지 주소를 하드 코딩 하므로 위치 독립적이지 않습니다.

대신 위치 독립적 인 방식으로 작성하면 :

lea msg(%rip), %rsi

그러면 PIE 링크가 제대로 작동하고 GDB는 실행 파일이 매번 메모리의 다른 위치에로드된다는 것을 보여줍니다.

여기서 차이점 은 구문 으로 인해 현재 PC lea주소에 msg상대적인 주소 를 인코딩 한다는 것 입니다. 64 비트 어셈블리 프로그램에서 RIP 상대 주소 지정을 사용하는 방법을rip 참조하십시오 .

다음을 사용하여 두 버전을 모두 분해하여 알아낼 수도 있습니다.

objdump -S main.o

각각주는 :

e:   48 c7 c6 00 00 00 00    mov    $0x0,%rsi
e:   48 8d 35 19 00 00 00    lea    0x19(%rip),%rsi        # 2e <msg>

000000000000002e <msg>:
  2e:   68 65 6c 6c 6f          pushq  $0x6f6c6c65

따라서 우리는 lea이미 msg현재 주소 + 0x19로 인코딩 된 완전한 정확한 주소를 가지고 있음을 분명히 알 수 있습니다.

그러나 mov버전은 주소를로 설정했습니다. 00 00 00 00즉, 해당 위치에서 재배치가 수행됩니다. 링커가 수행하는 작업은 무엇입니까? 오류 메시지 의 비밀 R_X86_64_32SldPIE 실행 파일에서 발생할 수없는 실제 재배치 유형입니다.

우리가 할 수있는 또 다른 재미있는 일은 with msg대신 data 섹션에 를 넣는 것입니다 .text.

.data
msg:
    .ascii "hello\n"
len = . - msg

이제 다음과 같이 .o어셈블됩니다.

e:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 15 <_start+0x15>

따라서 RIP 오프셋은 이제 0이고 어셈블러에서 재배치를 요청한 것으로 추측됩니다. 다음을 통해 확인합니다.

readelf -r main.o

다음을 제공합니다.

Relocation section '.rela.text' at offset 0x160 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000011  000200000002 R_X86_64_PC32     0000000000000000 .data - 4

PIE 실행 파일을 처리 할 수 R_X86_64_PC32있는 PC 상대 재배치가 분명 ld합니다.

이 실험을 통해 링커 자체가 프로그램이 PIE가 될 수 있는지 확인하고이를 표시합니다.

그런 다음 GCC로 컴파일 할 때 -pieGCC에 위치 독립적 어셈블리를 생성하도록 지시합니다.

그러나 어셈블리를 직접 작성하는 경우 위치 독립성을 달성했는지 수동으로 확인해야합니다.

ARMv8 aarch64에서는 ADR 명령어를 사용 하여 위치 독립적 인 hello world를 얻을 수 있습니다 .

ELF가 위치 독립적인지 확인하는 방법은 무엇입니까?

GDB를 통해 실행하는 것 외에도 몇 가지 정적 메서드가 다음에서 언급됩니다.

Ubuntu 18.10에서 테스트되었습니다.


답변