スタティックリンクでもPIE(Position Independent Executable)

2023/08/07に公開

PIEとは

PIE(Position Independent Executable)はプログラムをロードするアドレスが固定ではない実行ファイルです。OSはロードアドレスをランダム化することで安全性を上げることができます。

gccに-pieというオプションがかなり昔からありましたが、単にこのオプションをつけるだけでPIEとして実行できるわけではなく、実行直前にダイナミックリンカがアドレスを解決する必要があったので、スタティックリンクした場合にはPIEにはなりませんでした。
しかし、最近gccに-static-pie というオプションがいつの間にか追加されていることに気がつきました。

hello worldでmain関数のアドレスを調べてみる

AArch64のUbuntu 22.04を使用しています。

テストプログラムはこちら。

hello.c
#include <stdio.h>

int main()
{
    printf("Hello, world! main=%p\n", main);
}

使用したgccのバージョン。

$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

まずは普通にコンパイル。最近ではPIEを生成するのがデフォルトになっています。

$ gcc -o hello hello.c
$ file hello
hello: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=e16d464604a4e7037dfa901b45b1c890ee19501c, for GNU/Linux 3.7.0, not stripped
$ ./hello 
Hello, world! main=0xaaaae1400754
$ ./hello 
Hello, world! main=0xaaaaba1c0754
$ ./hello 
Hello, world! main=0xaaaaac870754
$ ./hello 
Hello, world! main=0xaaaac9570754
$ ./hello 
Hello, world! main=0xaaaae5d30754

mainのアドレスが毎回変わっています。

次にstatic link してみます。

$ gcc -o hello_static -static hello.c
$ file hello_static 
hello_static: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=b7dcb5685275ccededf79295b7b1bbf7ec55fa7a, for GNU/Linux 3.7.0, not stripped
$ ./hello_static 
Hello, world! main=0x4006d4
$ ./hello_static 
Hello, world! main=0x4006d4
$ ./hello_static 
Hello, world! main=0x4006d4
$ ./hello_static 
Hello, world! main=0x4006d4
$ ./hello_static 
Hello, world! main=0x4006d4

mainのアドレスは毎回同じです。

今度は -static-pie オプションを試してみました。

$ gcc -o hello_static_pie -static-pie hello.c
$ file hello_static_pie 
hello_static_pie: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=13c05ddbd6a58129ee0921a8ed3c2cdea6eddc7f, for GNU/Linux 3.7.0, not stripped
$ ./hello_static_pie
Hello, world! main=0xffffbd531bd4
$ ./hello_static_pie
Hello, world! main=0xffffa45f2bd4
$ ./hello_static_pie
Hello, world! main=0xffffb48ccbd4
$ ./hello_static_pie
Hello, world! main=0xffffba07cbd4
$ ./hello_static_pie
Hello, world! main=0xffffb339cbd4

static linkでありながら、mainのアドレスが毎回変わっています。

適当にpieがつくシンボルを探してみると

$ nm hello_static_pie |grep pie
000000000002b840 t _dl_relocate_static_pie

_dl_relocate_static_pie いかにもそれっぽい関数が含まれていました。おそらくCランタイムからこれが呼び出されて、アドレスのリロケーションを行うのでしょう。

以下のページの情報によるとこの関数はglibcに含まれるもののようです。
https://stackoverflow.com/questions/62586808/how-are-relocations-supposed-to-work-in-static-pie-binaries

gccに-static-pieオプションが追加されたときのパッチはこれのようです。
https://patchwork.ozlabs.org/project/gcc/patch/20170808221841.GA16793@gmail.com/#1758721

ELFのヘッダ情報を見てみる

$ readelf -h hello_static
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           AArch64
  Version:                           0x1
  Entry point address:               0x400580
  Start of program headers:          64 (bytes into file)
  Start of section headers:          644656 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         6
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30
$ readelf -h hello_static_pie 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           AArch64
  Version:                           0x1
  Entry point address:               0x8ac0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          689840 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         8
  Size of section headers:           64 (bytes)
  Number of section headers:         36
  Section header string table index: 35

ELFのヘッダのTypeの項目でPIEかどうかを判別できるようです。OSはこれを見てロードアドレスをランダムにするかどうかを決めているのでしょう。

clangの場合

$ clang --version
clang version 16.0.0 (https://github.com/llvm/llvm-project.git 434575c026c81319b393f64047025b54e69e24c2)
Target: aarch64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
$ clang -o hello_static_pie_clang -static-pie hello.c
$ file hello_static_pie_clang 
hello_static_pie_clang: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (GNU/Linux), static-pie linked, for GNU/Linux 3.7.0, not stripped
$ ./hello_static_pie_clang 
Hello, world! main=0xffff88eeebd4
$ ./hello_static_pie_clang 
Hello, world! main=0xffff87d60bd4
$ ./hello_static_pie_clang 
Hello, world! main=0xffffa4342bd4
$ ./hello_static_pie_clang 
Hello, world! main=0xffffacdcabd4

clangでも -static-pieオプションでstatic-pieの実行ファイルを作ることができました。

参考

https://en.wikipedia.org/wiki/Position-independent_code
https://ja.wikipedia.org/wiki/位置独立コード
https://ja.wikipedia.org/wiki/アドレス空間配置のランダム化

関連

https://zenn.dev/tetsu_koba/articles/dc87d3c86c8d50
https://zenn.dev/tetsu_koba/articles/f08100a8fc4c34

Discussion