环境搭建
虚拟机安装
- 镜像下载网站
- 为了避免环境问题建议 22.04 ,20.04,18.04,16.04 等常见版本 ubuntu 虚拟机环境各准备一份。注意定期更新快照以防意外。
- 虚拟机建议硬盘 256 G 以上,内存也尽量大一些。硬盘大小只是上界,256 G 不是真就占了 256 G,而后期如果硬盘空间不足会很麻烦。
- 更换 ubuntu 镜像源 ,建议先在
系统设置 → Software & Updates → Download from → 选择国内服务器例如阿里云
(貌似不这样后续换源会出错),然后再sudo gedit /etc/apt/sources.list
将镜像源中不高于当前系统版本的镜像复制进去(高于当前系统版本容易把apt
搞坏)。 - Ubuntu换源error:The following signatures couldn’t be verified because the public key is not available 解决方法:
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5523BAEEB01FA116
其中的5523BAEEB01FA116
是根据错误提示写的。
基础工具
net-tools
ifconfig
查看网络配置需要安装 net-tools
。
sudo apt install net-tools
vim
sudo apt install vim
gedit
不习惯 vim 的可以使用 gedit 文本编辑器。
sudo apt install gedit
git
sudo apt install git
gcc
sudo apt install gcc
sudo apt install gcc-multilib
python
ipython 提供了很好的 python 交互命令行,建议安装。
sudo apt install python2
sudo apt install python3
sudo apt install ipython
sudo apt install ipython3
另外有的版本 ubuntu 的不好安装 pip2 可以使用 get-pip.py
脚本安装。
sudo apt install python3-pip
sudo apt install curl
curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py
sudo python2 get-pip.py
ubuntu 22.04 的 ipython(python2)必须使用 pip2 安装:
sudo pip2 install ipython
docker
sudo apt install docker.io
sudo apt install docker-compose
默认情况下,Docker 命令需要使用 sudo 权限才能运行,这是因为 Docker 守护进程以 root 用户身份运行。然而,你可以通过以下步骤将当前用户添加到 Docker 用户组,从而允许在不使用 sudo 的情况下运行 Docker 命令:
-
确保当前用户属于
docker
组:运行以下命令检查当前用户是否已添加到 docker 组:groups
在输出的组列表中查找
docker
。如果没有找到docker
组,请继续下一步。 -
将当前用户添加到
docker
组:运行以下命令将当前用户添加到docker
组中(将<username>
替换为你的用户名):sudo usermod -aG docker <username>
-
更新用户组更改:运行以下命令使用户组更改生效:
newgrp docker
-
重新登录或重启系统:要使用户组更改永久生效,你需要注销当前会话并重新登录,或者重启系统。
oh-my-zsh
安装 zsh
sudo apt install zsh
安装 oh-my-zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
设置 zsh 为默认 shell(重启虚拟机后生效)
chsh -s /bin/zsh
安装 oh-my-zsh 插件 zsh-autosuggestions ,zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
编辑 ~/.zshrc
添加插件:
plugins=(
# other plugins...
zsh-autosuggestions
zsh-syntax-highlighting
extract
)
更新:
omz update
pwn 相关工具
clion
clion 是一款 C\C++ 的 IDE ,可以用来阅读 glibc 源码的工具,这款工具对宏展开,符号跳转,结构体大小以及成员偏移计算都有很好的支持。这款软件需要付费使用,不过可以某宝搞一个教育邮箱。
首先用打开 debug_glibc 解压后的 glibc 源码,这里有以下几点需要注意:
- 源码在对应版本的
source
目录下。 - 最好不要使用解压到默认
\glibc
路径下的源码,因为源码调试与行号绑定,阅读源码可能会修改到源码。 - 这里用
debug_glibc
中的源码是因为这里的源码是编译过的,clion 分析代码需要编译的配置文件。
然后这里我们看到 Makefile 没有正确导入:
在较新版本的 clion 中位于 source
根目录下的 autoreconf
的配置文件 configure.ac
配置有问题,需要改成以下内容(这个主要看版本,有时默认的就好使):
GLIBC_PROVIDES dnl See aclocal.m4 in the top level source directory.
# Local configure fragment for sysdeps/i386.
# We no longer support i386 since it lacks the atomic instructions
# required to implement NPTL threading.
if test "$config_machine" = i386; then
AC_MSG_ERROR([
*** ERROR: Support for i386 is deprecated.
*** Please use host i786, i686, i585 or i486.
*** For example: /src/glibc/configure --host=i686-pc-linux-gnu ..."])
fi
# The GNU C Library can't be built for i386. There are several reasons for
# this restriction. The primary reason is that i386 lacks the atomic
# operations required to support the current NPTL implementation. While it is
# possible that such atomic operations could be emulated in the kernel to date
# no such work has been done to enable this. Even with NPTL disabled you still
# have no atomic.h implementation. Given the declining use of i386 we disable
# support for building with `-march=i386' or `-mcpu=i386.' We don't explicitly
# check for i386, instead we make sure the compiler has support for inlining
# the builtin __sync_val_compare_and_swap. If it does then we should have no
# problem building for i386.
LIBC_COMPILER_BUILTIN_INLINED(
[__sync_val_compare_and_swap],
[int a, b, c; __sync_val_compare_and_swap (&a, b, c);],
[-O0],
[libc_cv_unsupported_i386=no],
[AC_MSG_ERROR([
*** Building with -march=i386/-mcpu=i386 is not supported.
*** Please use host i786, i686, i586, or i486.
*** For example: /source/glibc/configure CFLAGS='-O2 -march=i686' ...])])
dnl Check whether asm supports Intel MPX
AC_CACHE_CHECK(for Intel MPX support, libc_cv_asm_mpx, [dnl
cat > conftest.s <<\EOF
bndmov %bnd0,(%esp)
EOF
if AC_TRY_COMMAND(${CC-cc} -c $ASFLAGS conftest.s 1>&AS_MESSAGE_LOG_FD); then
libc_cv_asm_mpx=yes
else
libc_cv_asm_mpx=no
fi
rm -f conftest*])
if test $libc_cv_asm_mpx == yes; then
AC_DEFINE(HAVE_MPX_SUPPORT)
fi
AC_DEFINE(USE_REGPARMS)
dnl It is always possible to access static and hidden symbols in an
dnl position independent way.
AC_DEFINE(PI_STATIC_AND_HIDDEN)
另外还需要右键 Makefile 设置在命令后面添加 --disable-sanity-checks
。另外构建目标要填 all
,否则 clion 分析的源码的不全。
完整预配置命令如下:
#!/bin/sh
#
# GNU Autotools template, feel free to customize.
#
which autoreconf >/dev/null && autoreconf --install --force --verbose "${PROJECT_DIR:-..}" 2>&1; /bin/sh "${PROJECT_DIR:-..}/configure" --disable-sanity-checks
之后右键重新加载 Makefile 项目。
不勾选清理项目。
如果最后这样说明导入成功,之后耐心等待项目导入完毕即可。
gdb
sudo apt-get install gdb gdb-multiarch
pwntools
注意我这里的 pwntools 是 python2 版本的,需要指定为 4.9.0 ,因为高版本的 pwntools 已经不支持 python2 了(具体来说是高版本的 pwntools 必须依赖 unicorn 2.x.x ,而 unicorn 2.x.x 只支持 python3)。
pip install pwntools==4.10.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
如果已经装了 pwntools 需要先卸载干净再重新安装,否则更改版本无效(最好不带 sudo
也来一遍确保卸载干净)。
sudo pip2 uninstall pwntools
sudo pip2 uninstall unicorn
这样安装的 pwntools 的 plt 功可能无法正常使用,需要手动安装 Unicorn 库。
pip install unicorn==1.0.3 -i https://pypi.tuna.tsinghua.edu.cn/simple
当然这样做的代价是一些特殊架构老版本的 pwntools 不支持,这时候最好换 python3 的 pwntools 。
gdb 插件
主要有 pwndbg,peda,gef ,这里我常用的是 pwndbg 。对于一些版本过于古老导致环境装不上的可以尝试一下 peda 。
先将三个项目的代码都拉取下来。
git clone https://github.com/longld/peda.git
git clone https://github.com/pwndbg/pwndbg.git
git clone https://github.com/hugsy/gef.git
pwndbg 需要运行初始化脚本。
cd pwndbg
sudo ./setup.sh
另外还有一个 pwngdb 插件在调试多线程堆(heapinfoall
命令)的时候很有用,建议安装。
git clone https://github.com/scwuaptx/Pwngdb.git
gdb 在启动的时候会读取当前用户的主目录的 .gdbinit
文件进行 gdb 插件的初始化,这里提供一个配置方案。
source /home/sky123/tools/pwndbg/gdbinit.py
#source /home/sky123/tools/peda/peda.py
#source /home/sky123/tools/gef/gef.py
#source /home/sky123/tools/muslheap/muslheap.py
source /home/sky123/tools/Pwngdb/pwngdb.py
source /home/sky123/tools/Pwngdb/angelheap/gdbinit.py
define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end
注意,以普通用权限和管理员权限启动 gdb 时读取的 .gdbinit
文件的路径是不同的,普通权限读取的是 /home/<username>/.gdbinit
而管理员权限读取的是 /root/.gdbinit
。
pwndbg 安装 ghidra 插件可以支持代码反编译(虽然没啥用 )
- 安装
r2pipe
库pip3 install r2pipe
- 下载安装 radere2 项目
git clone https://github.com/radareorg/radare2.git cd radare2 sudo sys/install.sh
- 下载编译安装 r2ghidra 项目
git clone https://github.com/radareorg/r2ghidra.git cd r2ghidra sudo ./preconfigure sudo ./configure sudo make -j16 sudo make install
gadget 搜索工具
ROPgdbget
安装:
git clone https://github.com/JonathanSalwan/ROPgadget.git
cd ROPgadget
sudo python3 setup.py install
使用:
ROPgadget --binary ntdll.dll > rop
ropper
- 安装:
- 在 pypi 的 ropper 官网上下载 ropper
- 运行安装脚本完成 ropper 安装
sudo python3 setup.py install
- 使用:
ropper --file ./pwn --nocolor > rop
one_gadget
用于搜索 libc 中能够实现 execve("/bin/sh", (char *[2]) {"/bin/sh", NULL}, NULL);
的效果的跳转地址,由于是采用特征匹配的方法,因此只能是在 libc 中查找。
- 安装:
sudo apt install -y ruby ruby-dev sudo gem install one_gadget
- 使用:可以查找到 gadget 地址以及条件限制。
➜ ~ one_gadget /lib/x86_64-linux-gnu/libc.so.6 0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ) constraints: rsp & 0xf == 0 rcx == NULL rbp == NULL || (u16)[rbp] == NULL 0xebcf1 execve("/bin/sh", r10, [rbp-0x70]) constraints: address rbp-0x78 is writable [r10] == NULL || r10 == NULL [[rbp-0x70]] == NULL || [rbp-0x70] == NULL 0xebcf5 execve("/bin/sh", r10, rdx) constraints: address rbp-0x78 is writable [r10] == NULL || r10 == NULL [rdx] == NULL || rdx == NULL 0xebcf8 execve("/bin/sh", rsi, rdx) constraints: address rbp-0x78 is writable [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL
如果 one_gadget
在一个版本的 Ubuntu 中搜索某一版本的glibc 的 gadget 出现如下报错可以尝试换另一个版本的 Ubuntu ,具体原因未知。
seccomp-tools
用于查看和生成程序沙箱规则。
- 安装:
sudo gem install seccomp-tools
- 使用:
seccomp-tools dump ./pwn
LibcSearcher
通过泄露的 libc 中函数的地址来确定 libc 版本。
git clone https://github.com/lieanu/LibcSearcher.git
cd LibcSearcher
sudo python3 setup.py install
glibc-all-in-one
临时找 glibc 和 ld 或者编译 glibc 。
git clone https://github.com/matrix1001/glibc-all-in-one.git
更新下载列表:
➜ glibc-all-in-one ./update_list
[+] Common list has been save to "list"
[+] Old-release list has been save to "old_list"
➜ glibc-all-in-one cat list
2.23-0ubuntu10_amd64
2.23-0ubuntu10_i386
2.23-0ubuntu11_amd64
2.23-0ubuntu11_i386
2.23-0ubuntu3_amd64
2.23-0ubuntu3_i386
2.27-3ubuntu1_amd64
2.27-3ubuntu1_i386
2.28-0ubuntu1_amd64
2.28-0ubuntu1_i386
......
➜ glibc-all-in-one cat old_list
2.21-0ubuntu4.3_amd64
2.21-0ubuntu4.3_amd64
2.21-0ubuntu4_amd64
2.21-0ubuntu4_amd64
2.24-3ubuntu1_amd64
2.24-3ubuntu1_amd64
2.24-3ubuntu2.2_amd64
2.24-3ubuntu2.2_amd64
2.24-9ubuntu2.2_amd64
2.24-9ubuntu2.2_amd64
......
下载 libc ,注意要安装解压工具 zstd
,因为下载脚本中用到了。
sudo apt-get install zstd
cat list |xargs -i ./download {}
cat old_list |xargs -i ./download_old {}
编译 libc
sudo ./build [版本例如2.29] [架构例如 i686 amd64]
patchelf
安装:
sudo apt install patchelf
qemu
sudo apt install qemu-user qemu-system
如何使用题目提供的 docker 环境
netcat
在官网下载项目源码,使用如下命令进行编译。
./configure LDFLAGS=-static # 考虑到 docker 环境恶劣选择静态编译
make -j24 # 编译
编译后生成的 netcat
位于项目 src
目录下。netcat
即我们常用的 nc
命令对应的可执行程序。
在 docker 中使用如下命令将题目 io 映射到 8888 端口。
./netcat -lvp 8888 -e ./pwn
在本机可以使用如下命令连接并交互。(前提是 docker 的 8888 端口映射到本机的 8888 端口)
nc 127.0.0.1 8888
gdb
在官网下载项目源码,使用如下命令编译 gdbserver :
sudo apt-get install libgmp-dev libmpfr-dev
cd gdb-9.2/gdb/gdbserver
./configure LDFLAGS=-static
make -j $(nproc)
对于 gdb ,由于编译 gdb 时依赖的静态库需要提前编译,因此想要编译 gdb 最好直接编译整个项目:
cd gdb-9.2
mkdir build
cd build
../configure LDFLAGS=-static
make -j $(nproc)
注意以下几点:
- 编译的
gdbserver
版本一定要与本机的gdb
匹配,不同版本的gdbserver
通信协议不同。 - 有的时候在
gdbserver
中运行./configuer
命令会出现找不到Makefile
的情况,这时在根目录进行一次编译就好了。 - 连接失败之后再运行一次编译命令就可能编译成功。
gdb
位于./gdb/gdb
中。gdbserver
位于./gdbserver/gdbserver
中。
docker
- 加载镜像
docker load -i 题目附件.tar
- 查看现有镜像
docker images
- 启动容器
docker run --privileged -it -w /home/ctf -v ~/Desktop/本机目录:/home/ctf/镜像目录 -p 8888:8888 -p 9999:9999 镜像名 /bin/bash
--privileged
:加这个参数才能gdbserver
附加进程远程调试-v
:目录映射,方便传文件。-p
:端口映射,开两个端口分别给netcat
和gdbserver
用。改用--net=host
可以映射全部端口。-w
:进入 docker 后目录为/home/ctf
。
- 查看现有容器
docker ps
- 进容器 shell ,即同一个容器再开一个 shell 。
sudo docker exec -it -w /home/ctf 容器ID /bin/bash
- 停止所有容器:
docker stop $(docker ps -a -q)
- 删除所有容器:
docker rm $(docker ps -a -q)
- 删除所有镜像:
docker rmi $(docker images -q)
使用方法
exp.py
模板如下:
from pwn import *
r = remote("127.0.0.1",8888) # nc 连接远程程序
gdb.attach(target=("localhost", 9999), exe="./pwn", gdbscript="") # gdb 连接 docker 中的 gdbserver 调试 ./pwn
pause() # 阻塞脚本直到 gdb 成功连接 gdbserver防止程序跑飞
"""
r.sendlineafter("xxxx", "xxx") # 脚本远程交互
"""
r.interactive()
- 运行脚本前首先在 docker 容器中用
netcat
将题目程序 IO 映射到 8888 端口:./netcat -lvp 8888 -e ./pwn
- 运行脚本,阻塞在
gdb.attach
时脚本已经与远程的netcat
连接,此时 docker 镜像中已经有pwn
这个进程了。此时使用ps -aux | grep pwn
查看进程pid
然后运行如下命令让gdbserver
附加进程并监听 9999 端口。gdbserver :9999 --attach 进程pid
- 此时脚本执行
gdb.attach
连接 docker 中的gdbserver
并阻塞在pause()
上直到gdb
成功连接gdbserver
。 - 在脚本运行窗口按回车解除阻塞进行调试。
其中 docker 中的操作可以通过脚本自动化实现。
#!/bin/bash
IMAGE_NAME=minipy-debug
CONTAINER_HOME=/home/ctf
PROG_NAME=minipy
NC_PORT=8888
DBG_PORT=9999
# load image
if [ "$(docker images | grep ${IMAGE_NAME} | wc -l)" -lt "1" ]; then
docker load -i ${IMAGE_NAME}.tar
fi
# start continer
if [ "$(docker ps | grep ${IMAGE_NAME} | wc -l)" -lt "1" ]; then
# docker run --privileged -itd -p ${DBG_PORT}:${DBG_PORT} -p ${NC_PORT}:${NC_PORT} ${IMAGE_NAME}
docker run --privileged -itd --net=host ${IMAGE_NAME}
fi
# get continer id
CONTAINER_ID=$(docker ps -q --filter "ancestor=${IMAGE_NAME}")
# cp files
docker cp ./tools/gdbserver ${CONTAINER_ID}:${CONTAINER_HOME}
docker cp ./tools/netcat ${CONTAINER_ID}:${CONTAINER_HOME}
docker cp ./${PROG_NAME} ${CONTAINER_ID}:${CONTAINER_HOME}
# start run
docker exec -itd -w ${CONTAINER_HOME} ${CONTAINER_ID} /bin/bash -c "./netcat -lvp ${NC_PORT} -e ./${PROG_NAME}"
read
docker exec -it -w ${CONTAINER_HOME} ${CONTAINER_ID} /bin/bash -c "ps -ef | grep ${PROG_NAME} | grep -v 'grep' | grep -v '\-c' | awk '{print \$2}' | xargs ./gdbserver :${DBG_PORT} --attach"
read
#docker stop ${CONTAINER_ID}
#docker rm ${CONTAINER_ID}
docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)
ELF 文件格式
ELF(Executable and Linkable Format)是一种常见的可执行文件和可链接文件格式,主要用于Linux和类Unix系统。ELF 文件可以包含不同的类型,常见的 ELF 文件类型包括:
- 可执行文件(
ET_EXEC
):这种类型的 ELF 文件是可直接执行的程序,可以在操作系统上运行。 - 共享目标文件(
ET_DYN
):这种类型的 ELF 文件是可被动态链接的共享库,可以在运行时与其他程序动态链接。该类型文件后缀名为.so
。 - 可重定位文件(
ET_REL
):这种类型的 ELF 文件是编译器生成的目标文件,通常用于将多个目标文件链接到一个可执行文件或共享库中。该类型文件后缀名为.o
,静态链接库(.a
)也可以归为这一类。 - 核心转储文件(
ET_CORE
):这种类型的 ELF 文件是操作系统在程序崩溃或发生错误时生成的核心转储文件,用于调试和分析程序崩溃的原因。
ELF 文件结构及相关常数被定义在 /usr/include/elf.h
里,因为 ELF 文件在各种平台下都通用,ELF文件有 32 位版本和 64 位版本。32 位版本与 64 位版本的 ELF 文件的格式基本是一样的(部分结构体为了优化对齐后大小调整了成员的顺序),只不过有些成员的大小不一样。
elf.h
使用 typedef 定义了一套自己的变量体系:
自定义类型 | 描述 | 原始类型 | 长度(字节) |
---|---|---|---|
Elf32_Addr | 32 位版本程序地址 | uint32_t | 4 |
Elf32_Half | 32 位版本的无符号短整型 | uint16_t | 2 |
Elf32_Off | 32 位版本的偏移地址 | uint32_t | 4 |
Elf32_Sword | 32 位版本有符号整型 | uint32_t | 4 |
Elf32_Word | 32 位版本无符号整型 | int32_t | 4 |
Elf64_Addr | 64 位版本程序地址 | uint64_t | 8 |
Elf64_Half | 64 位版本的无符号短整型 | uint16_t | 2 |
Elf64_Off | 64 位版本的偏移地址 | uint64_t | 8 |
Elf64_Sword | 64 位版本有符号整型 | uint32_t | 4 |
Elf64_Word | 64 位版本无符号整型 | int32_t | 4 |
ELF 主要管理结构为文件头,程序头表(可重定位文件没有)和节表,其他部分有一个个节组成,多个属性相同的节构成一个段。对于节的介绍这里按照静态链接相关和动态链接相关分别介绍。
文件头
我们这里以 32 位版本的文件头结构 Elf32_Ehdr
作为例子来描述,它的定义如下:
/* The ELF file header. This appears at the start of every ELF file. */
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
e_ident
:ELF 文件的魔数和其他信息。- 前 4 字节为
ELFMAG
即\x7fELF
。 - 第 5 字节为 ELF 文件类型,值为
ELFCLASS32(1)
代表 32 位,值为ELFCLASS64(2)
代表 64 位。 - 第 6 字节为 ELF 的字节序,0 为无效格式,1 为小端格式,2 为大端格式。
- 第 7 字节为 ELF 版本,一般为 1 ,即 1.2 版本。
- 后面 9 字节没有定义一般填 0 ,有些平台会使用这 9 个字节作为扩展标志。
- 前 4 字节为
e_type
:表示ELF文件类型,如可执行文件、共享对象文件(.so
)、可重定位文件(.o
)等。e_machine
:表示目标体系结构,即程序的目标平台,如 x86、ARM 等。相关常量以EM_
开头。e_version
:ELF 文件版本号,一般为常数 1 。e_entry
:表示程序入口点虚拟地址。操作系统加载完程序后从这个地址开始执行进程的命令。可重定位文件一般没有入口地址,则这个值为 0 。e_phoff
:表示程序头表的文件偏移量。e_shoff
:表示节表的文件偏移量。e_flags
:表示处理器特定标志。e_ehsize
:表示 ELF 文件头的大小。e_phentsize
:表示程序头表中每个表项的大小。e_phnum
:表示程序头表中表项的数量。e_shentsize
:表示节表中每个表项的大小。e_shnum
:表示节表中表项的数量。e_shstrndx
:表示节表中字符串表的索引。
程序头表
ELF 可执行文件中有一个专门的数据结构叫做程序头表(Program Header Table)用来保存段(注意不是节)的信息。因为 ELF 目标文件不需要被装载,所以它没有程序头表,而 ELF 的可执行文件和共享库文件都有程序头表。
程序头表是由 Elf*_Phdr
组成的数组,用于描述 ELF 文件中每个节的属性和信息。
/* Program segment header. */
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
p_type
:段的类型,例如可执行段、数据段等。p_offset
:段在文件中的偏移量。p_vaddr
:段在虚拟内存中的起始地址。p_paddr
:段在物理内存中的起始地址。因为 ELF 还没装载不知道物理地址,所以作为保留字段。通常和p_vaddr
的值是一样的。p_filesz
:段在文件中的大小。p_memsz
:段在内存中的大小。p_flags
:段的标志,例如可读、可写、可执行等。p_align
:段在文件和内存中的对齐方式。段的的加载地址要能被 2 p_align 2^{\text{p\_align}} 2p_align 整除。
节表
ELF文件里面定义一个固定长度的 Elf*_Shdr
结构体数组用来存放节相关信息,与 PE 文件的节表相似。
在 ELF 文件中,段(Segment)和节(Section)是两个不同的概念,它们在文件结构中具有不同的作用和目的。
段(Segment)是一种逻辑上的组织单位,它定义了可执行文件或共享库在内存中的一个连续区域。每个段都有自己的虚拟地址空间,可以包含多个节。常见的段类型包括代码段(.text
),数据段(.data
、.bss
),只读数据段(.rodata
)等。段在加载和执行时被操作系统用来管理内存,设置内存保护属性以及指定虚拟地址空间的起始地址和大小。
节(Section)是一种更细粒度的组织单位,它包含了文件中的特定类型的数据或代码。每个节都有自己的名字、类型和内容。常见的节类型包括代码节(.text
),数据节(.data
、.bss
),只读数据节(.rodata
),符号表节(.symtab
),字符串表节(.strtab
)等。节不直接参与内存的加载和执行,而是用于链接器(Linker)和调试器(Debugger)等工具对文件进行处理和分析。
通俗的讲,在装载程序的时候为了节省内存会将 ELF 文件中属性相同的节(Section)合并成在一个段(Segment)加载到内存中。
段和节之间存在对应关系和映射关系:
- 一个段可以包含多个节,这些节的内容和属性都属于该段。
- 段提供了对应于虚拟内存的逻辑映射,而节则提供了对应于文件的逻辑映射。
- 段的加载和执行涉及内存管理和地址映射,而节则用于链接和调试过程中的符号解析、重定位等操作。
其中 Elf32_Shdr
定义如下:
/* Section header. */
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
sh_name
:表示节的名称在字符串表中的索引。字符串表节存储了所有节的名称,sh_name
指定了节的名称在字符串表中的位置。sh_type
:表示节的类型,指定了节的用途和属性。常见的类型包括代码段(SHT_PROGBITS(1)
)、数据段(SHT_PROGBITS(1)
)、符号表(SHT_SYMTAB(2)
)、字符串表(SHT_STRTAB(3)
)等。sh_flags
:表示节的标志,用于描述节的特性和属性。标志的具体含义取决于节的类型和上下文。sh_addr
:表示节的虚拟地址,只在可执行文件中有意义。对于可执行文件,sh_addr
指定了节在内存中的加载地址,如果该节不可被加载,则该值为 0 。sh_offset
:表示节在文件中的偏移量,指定了节在文件中的位置。对于 bss 段来说该值没有意义。sh_size
:表示节的大小,指定了节所占据的字节数。sh_link
:表示链接到的其他节的索引,用于建立节之间的关联关系,具体含义依赖于节的类型。sh_info
:附加信息,具体含义依赖于节的类型。sh_addralign
:表示节的地址对齐要求,指定了节在内存中的对齐方式。即sh_addr
需要满足 sh_addr m o d 2 sh_addralign = 0 \text{sh\_addr} \mod 2^{\text{sh\_addralign}} = 0 sh_addrmod2sh_addralign=0 。如果sh_addralign
为 0 或 1 表示该段没有对齐要求。sh_entsize
:表示节中每个项的大小,如果该字段为 0 说明节中不包含固定大小的项。
ELF 中常见的节如下:
.text
:代码段(Code Section),用于存储程序的可执行指令。.rodata
:只读数据段(Read-Only Data Section),用于存储只读的常量数据,例如字符串常量。.data
:数据段(Data Section),用于存储已初始化的全局变量和静态变量。.bss
:未初始化的数据段(Block Started by Symbol),用于存储未初始化的全局变量和静态变量。它不占用实际的文件空间,而是在运行时由系统自动初始化为零。.symtab
:符号表节(Symbol Table Section),用于存储程序的符号表信息,包括函数、变量和其他符号的名称、类型和地址等。.strtab
:字符串表节(String Table Section),用于存储字符串数据,如节名称、符号名称等。字符串表节被多个其他节引用,通过偏移量和索引来访问具体的字符串。.rel.text
或.rela.text
:代码重定位节(Relocation Section),用于存储代码段中的重定位信息,以便在链接时修正代码中的符号引用。.rel.data
或.rela.data
:数据重定位节(Relocation Section),用于存储数据段中的重定位信息,以便在链接时修正数据段中的符号引用。.dynamic
:动态节(Dynamic Section),用于存储程序的动态链接信息,包括动态链接器需要的重定位表、共享对象的名称、版本信息等。.note
:注释节(Note Section),用于存储与程序或库相关的注释或调试信息。
静态链接相关
注意:静态链接相关只在可重定位文件中存在。比如可执行文件,如果不开启 PIE 加载地址固定,不需要对自身进行重定位,而开启 PIE 后为地址无关代码,也不需要对自身进行重定位。因此不需要静态链接也就丢弃了静态链接相关的节。
符号表(.symtab)
注意:符号表除了静态链接外没有用,但是程序为了方便调试会保留符号表,我们可以通过 strip + 程序名
的方式将符号表去除,这就是为什么有的 pwn 题的附件没有函数和变量名而有的却有。
ELF 文件中的符号表往往是文件中的一个段,段名一般叫 .symtab
。符号表是一个 Elf*_Sym
结构(32 位 ELF 文件)的数组,每个 Elf*_Sym
结构对应一个符号。
/* Symbol table entry. */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
st_name
:符号名称在字符串表中的偏移量。st_value
:符号的值,即符号的地址或偏移量。- 如果该符号在目标文件中,如果是符号的定义并且该符号不是
COMMON
块类型的则st_value
表示该符号在段中的偏移。 - 在目标文件中,如果符号是
COMMON
块类型的则st_value
表示该符号的对齐属性。 - 在可执行文件中,
st_value
表示符号的虚拟地址。
- 如果该符号在目标文件中,如果是符号的定义并且该符号不是
st_size
:符号的大小,如果符号是一个函数,则表示函数的大小。如果该值为 0 表示符号的大小为 0 或未知。st_info
:该字段是一个字节,包含符号的类型和绑定信息。符号类型包括函数、数据、对象等,符号绑定包括局部符号、全局符号、弱符号等。该字段的高 4 位表示符号的类型,低 4 位表示符号的绑定信息。st_other
:保留字段,通常为 0 。st_shndx
:通常为符号所在节的索引。- 如果符号是一个常量,该字段为
SHN_ABS
(初始值不为 0 的全局变量) 或SHN_COMMON
(初始值为 0 的全局变量)。 - 如果该符号未定义但是在该文件中被引用到,说明该符号可能定义在其他目标文件中,则该字段为
SHN_UNDEF
。
- 如果符号是一个常量,该字段为
重定位表(.rel.text/.rel.data)
重定位表是一个 Elf*_Rel
结构的数组,每个数组元素对应一个重定位入口。重定位表主要有.rel.text
或 .rela.text
,即代码重定位节(Relocation Section)和 .rel.data
或 .rela.data
:数据重定位节(Relocation Section)。
/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
r_offset
:需要进行重定位的位置的偏移量或地址。这个位置通常是指令中的某个操作数或数据的地址,需要在链接时进行修正,以便正确地引用目标符号。- 对于可执行文件或共享库,
r_offset
表示需要修改的位置在内存中的位置(用于动态链接)。 - 对于可重定位文件,
r_offset
表示需要修改的位置相对于段起始位置的偏移(用于静态链接)。
- 对于可执行文件或共享库,
r_info
:低 8 位表示符号的重定位类型,重定位类型指定了进行何种类型的修正,例如绝对重定位、PC 相对重定位等。高 24 位表示该符号在符号表中的索引,用于解析重定位所引用的符号。
字符串表(.strtab)
ELF 文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。
通过这种方法,在ELF文件中引用字符串只须给出一个数字下标即可,不用考虑字符串长度的问题。一般字符串表在ELF文件中也以段的形式保存,常见的段名为“.strtab
”或“.shstrtab
”。这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。顾名思义,字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名(sh_name
)。
注意,在字符串表中的每个字符串的开头和结尾都有一个 \x00
填充。
动态链接相关
.interp 段
在动态链接的 ELF 可执行文件中,有一个专门的段叫做 .interp
段(“interp”是“interpreter”(解释器)的缩写)。
.interp
的内容很简单,里面保存的就是一个字符串 /lib64/ld-linux-x86-64.so.2
,这个字符串就是可执行文件所需要的动态链接器的路径。
通常系统通过判断一个 ELF 程序是否有 .interp
来判断该 ELF 文件是否为动态链接程序。
.dynamic 段
动态链接 ELF 中最重要的结构是 .dynamic
段,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。.dynamic
段是由Elf*_Dyn
构成的结构体数组。
/* Dynamic section entry. */
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;
Elf32_Dyn
结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。我们这里列举几个比较常见的类型值(这些值都是定义在 elf.h
里面的宏),
DT_SYMTAB
:指定了符号表的地址,d_ptr
表示.dynsym
的地址。DT_STRTAB
:指定了字符串表的地址,d_ptr
表示.synstr
的地址。DT_STRSZ
:指定了字符串表的大小,d_val
表示大小。DT_HASH
:指定了符号哈希表的地址,用于加快符号查找的速度,d_ptr
表示.hash
的地址。DT_SONAME
:指定了共享库的名称。DT_RPATH
:指定了库搜索路径(已废弃,不推荐使用)。DT_INIT
:指定了初始化函数的地址,动态链接器在加载可执行文件或共享库时会调用该函数。DT_FINI
:指定了终止函数的地址,动态链接器在程序结束时会调用该函数。DT_NEEDED
:指定了需要的共享库的名称。DT_REL/DT_RELA
:指定了重定位表的地址。
动态符号表(.dynsym)
为了完成动态链接,最关键的还是所依赖的符号和相关文件的信息。我们知道在静态链接中,有一个专门的段叫做符号表 .symtab
(Symbol Table),里面保存了所有关于该目标文件的符号的定义和引用。为了表示动态链接这些模块之间的符号导入导出关系,ELF 专门有一个叫做动态符号表(Dynamic Symbol Table)的段用来保存这些信息,这个段的段名通常叫做 .dynsym
(Dynamic Symbol),同样也是由 Elf*_Sym
构成的结构体数组。
与 .symtab
不同的是,.dynsym
只保存了与动态链接相关的符号,对于那些模块内部的符号,比如模块私有变量则不保存。很多时候动态链接的模块同时拥有 .dynsym
和 .symtab
两个表,.symtab
中往往保存了所有符号,包括 .dynsym
中的符号。
与 .symtab
类似,动态符号表也需要一些辅助的表,比如用于保存符号名的字符串表。静态链接时叫做符号字符串表 .strtab
(String Table),在这里就是动态符号字符串表 .dynstr
(Dynamic String Table);由于动态链接下,我们需要在程序运行时查找符号,为了加快符号的查找过程,往往还有辅助的符号哈希表(.hash
)。
动态链接重定位表(.rel.dyn/.rel.data)
共享对象需要重定位的主要原因是导入符号的存在。动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他共享对象,也就是说有导入的符号时,那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号的地址未知,在静态链接中,这些未知的地址引用在最终链接时被修正。但是在动态链接中,导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需要重定位。
共享对象的重定位与我们在前面“静态链接”中分析过的目标文件的重定位十分类似,唯一有区别的是目标文件的重定位是在静态链接时完成的,而共享对象的重定位是在装载时完成的。在静态链接中,目标文件里面包含有专门用于表示重定位信息的重定位表,比如 .rel.text
表示是代码段的重定位表,.rel.data
是数据段的重定位表。
动态链接的文件中,也有类似的重定位表分别叫做 .rel.dyn
和 .rel.plt
,它们分别相当于 .rel.data
和 .rel.text
。.rel.dyn
实际上是对数据引用的修正,它所修正的位置位于 .got
以及数据段;而 .rel.plt
是对函数引用的修正,它所修正的位置位于 .got.plt
。
PLT 表(.plt)
在未开启 FULL RELRO 的情况下 PLT 表的结构如下图所示, PLT 表在 .plt
(有的还包括 .plt.got
) 中。
PLT 表的形式如下所示:
PLT0:
push *(GOT+8)
jmp *(GOT+16)
⋮
bar@PLT:
jmp *(bar@GOT)
push n
jmp PLT0
\begin{align*} & \text{PLT0:} \\ & \qquad \text{push *(GOT+8)} \\ & \qquad \text{jmp *(GOT+16)} \\ & \qquad \vdots \\ & \text{bar@PLT:} \\ & \qquad \text{jmp *(bar@GOT)} \\ & \qquad \text{push n} \\ & \qquad \text{jmp PLT0} \\ \end{align*}
PLT0:push *(GOT+8)jmp *(GOT+16)⋮bar@PLT:jmp *(bar@GOT)push njmp PLT0
其中
n
n
n 为函数 bar
在 GOT 表中的值的索引,bar@GOT
中初始值为 jmp *(bar@GOT)
指令的下一条指令,也就是说第一次调用 bar
函数的时候会继续执行跳转至 PLT0
进行 bar@GOT
的重定位并调用 bar
函数;第二次调用 bar
函数的时候由于 bar@GOT
已完成重定位因此会直接跳转至 bar
函数。
在开启 FULL RELRO 的情况下 PLT 表的结构如下图所示,此时的 PLT 表在 .plt.sec
而不是 .plt
中。
由于 GOT 表在装载时已经完成重定位且不可写,因此不存在延迟绑定,PLT 直接根据 GOT 表存储的函数地址进行跳转。
GOT 表(.got/.got.plt)
ELF 将 GOT 拆分成了两个表叫做 .got
和 .got.plt
。其中 .got
用来保存全局变量引用的地址,.got.plt
用来保存函数引用的地址,也就是说,所有对于外部函数的引用全部被分离出来放到了 .got.plt
中(当然有的 ELF 文件可能吧这两个表合并为一个 .got
表,结构等同于后面提到的 .got.plt
)。另外 .got.plt
还有一个特殊的地方是它的前三项是有特殊意义的,分别含义如下:
- 第一项保存的是
.dynamic
段的偏移(也有可能是.dynamic
段的地址)。 - 第二项是一个
link_map
的结构体指针,里面保存着动态链接的一些相关信息,是重定位函数_dl_runtime_resolve
的第一个参数。 - 第三项保存的是
_dl_runtime_resolve
的地址。
.got.plt
在内存中的状态如下图所示:
静态链接程序也是有 plt 表和 got 表的,并且 plt 表也会被调用。
辅助信息数组
无论静态还是动态链接程序都有辅助信息数组,只是动态链接程序是动态链接器使用辅助信息数组。
站在动态链接器的角度看,当操作系统把控制权交给它的时候,它将开始做链接工作,那么至少它需要知道关于可执行文件和本进程的一些信息,比如可执行文件有几个段(“Segment”)、每个段的属性、程序的入口地址(因为动态链接器到时候需要把控制权交给可执行文件)等。
这些信息往往由操作系统传递给动态链接器,保存在进程的堆栈里面。我们在前面提到过,进程初始化的时候,事实上,堆栈里面还保存了动态链接器所需要的一些辅助信息数组(Auxiliary Vector)。辅助信息的格式也是一个结构数组,它的结构被定义在 elf.h
:
typedef struct
{
uint32_t a_type; /* Entry type */
union
{
uint32_t a_val; /* Integer value */
/* We use to have pointer elements added here. We cannot do that,
though, since it does not work when using 32-bit definitions
on 64-bit platforms and vice versa. */
} a_un;
} Elf32_auxv_t;
a_type
字段表示辅助信息数组的类型。下面是一些常见的a_type
值及其对应的含义:AT_NULL (0)
:辅助向量列表的结束标志。在列表的最后一个条目中使用。AT_IGNORE (1)
:忽略的辅助向量类型。在某些情况下,可以将该类型的辅助向量忽略。AT_EXECFD (2)
:可执行文件的文件描述符。表示打开可执行文件的文件描述符。AT_PHDR (3)
:程序头表的地址。指向程序头表在内存中的起始地址。AT_PHENT (4)
:程序头表中每个条目的大小(字节)。指示每个程序头表条目的字节数。AT_PHNUM (5)
:程序头表的条目数量。指示程序头表中的条目数量。AT_PAGESZ (6)
:页面大小。表示操作系统使用的页面大小。AT_BASE (7)
:共享对象的基地址。指向主共享对象的基地址。AT_FLAGS (8)
:标志位。包含一些特定于操作系统的标志。AT_ENTRY (9)
:程序入口点的地址。指向程序的入口点地址。AT_NOTELF (10)
:不是ELF文件。指示加载程序的文件不是有效的ELF文件。
a_un
:该成员是一个联合体(union),用于存储辅助向量条目的值。在这段代码中,由于指针类型的元素会在 32 位和 64 位平台上产生兼容性问题,所以注释中提到不再添加指针元素。a_val
:如果辅助向量条目的类型是一个整数值,那么该成员将存储该整数值。它也是一个 32 位的无符号整数。
程序编译过程
从源文件编译链接形成 ELF 文件的过程如下图所示:
预编译
首先是源代码文件和相关的头文件,如 stdio.h
等被预编译器 cpp 预编译成一个 .i
文件。对于 C++ 程序来说,它的源代码文件的扩展名可能是 .cpp
或 .cxx
,头文件的扩展名可能是 .hpp
,而预编译后的文件扩展名是 .ii
。
第一步预编译的过程相当于如下命令(-E
表示只进行预编译):
gcc –E hello.c –o hello.i
或者:
cpp hello.c > hello.i
预编译过程主要处理那些源代码文件中的以 #
开始的预编译指令。比如 #include
、#define
等,主要处理规则如下:
- 将所有的
#define
删除,并且展开所有的宏定义。 - 处理所有条件预编译指令,比如
#if
、#ifdef
、#elif
、#else
、#endif
。 - 处理
#include
预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。 - 删除所有的注释
//
和/* */
。 - 添加行号和文件名标识,比如
#2"hello.c"2
,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。 - 保留所有的
#pragma
编译器指令,因为编译器须要使用它们。
经过预编译后的 .i
文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i
文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。
上面的编译过程相当于如下命令:
gcc –S hello.i –o hello.s
汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。
上面的汇编过程我们可以调用汇编器 as 来完成:
as hello.s –o hello.o
或者使用 gcc 命令从 C 源代码文件开始,经过预编译、编译和汇编直接输出目标文件(Object File):
gcc –c hello.c –o hello.o
链接
静态链接
静态链接是在编译过程的最后阶段将多个目标文件(如 .o
文件)以及所需的库文件合并在一起,生成最终的可执行文件或共享库的过程。
可以使用如下命令将 a.o
和 b.o
链接为目标文件 ab 。
ld a.o b.o -o ab
合并代码和数据段(Code and Data Segment Merging)
链接器将多个目标文件中的代码段和数据段合并成一个更大的代码段和数据段。这样,所有的目标文件中的代码和数据都会被整合到最终的可执行文件或静态库中。
符号解析(Symbol Resolution)
链接器负通过重定位表解析目标文件中的符号引用。每个目标文件都包含对其他目标文件或库中定义的符号的引用,例如函数、变量等。链接器会检查这些引用并确定对应的定义位置。
对于可重定位的 ELF 文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每个要被重定位的 ELF 段都有一个对应的重定位表,而一个重定位表往往就是 ELF 文件中的一个段,所以其实重定位表也可以叫重定位段。
比如代码段 .text
如有要被重定位的地方,那么会有一个相对应叫 .rel.text
的段保存了代码段的重定位表;如果代码段 .data
有要被重定位的地方,就会有一个相对应叫 .rel.data
的段保存了数据段的重定位表。
链接器通过 Elf32_Rel
的 r_offset
加上所在段的起始位置得到重定位入口的位置;通过 r_info
的低 8 为得知重定位类型;通过 r_info
的高 24 位得到重定位符号在符号表(.symtab
)中的下标。
符号重定位(Symbol Relocation)
链接器通过符号表对应的 Elf32_Rel
的 st_value
表示该符号在段中的偏移,进而可以根据重定位类型计算出重定位入口所要修正的值。最后将对应的重定位入口 patch 成正确的值。32 位静态链接常用到的重定位类型如下:
R_386_32
:绝对地址。R_386_PC32
:相对于当前指令地址的下一条指令相对地址。
解析库依赖关系(Library Dependency Resolution)
如果目标文件依赖于外部库文件(如标准库或其他第三方库),链接器会解析这些库的依赖关系,并将所需的库文件链接到最终的可执行文件或静态库中。这样,在运行时,可执行文件或静态库就能够访问和使用这些库中提供的功能。
生成重定位表(Relocation Table)
链接器生成重定位表,记录了需要进行符号重定位的位置和相关信息。这些重定位表将在最终的可执行文件或静态库中被使用,以便在加载和执行时进行正确的符号重定位。
动态链接
动态链接(Dynamic Linking)本质是指把链接这个过程推迟到了运行时再进行,准确的说这个过程应该放在装载部分。不过动态链接的出现很大一部分原因是为了解决内存浪费问题,因此直接照搬静态链接的方式不合理,需要做一些改变。
另外我们称一个程序为动态链接程序或静态链接程序指的是该程序是否有动态链接过程。
注意动态链接不包括合并代码和数据段的过程,各个模块在内存中独立存在。
装载时重定位
由于需要将多个模块装载到内存中,因此动态链接难免会有地址冲突问题,这就需要我们在加载的时候将模块中的相关地址修改为正确的值,这就是装载时重定位。
Linux和GCC支持这种装载时重定位的方法,在产生共享对象时,使用了两个GCC参数 -shared
和 -fPIC
,如果只使用 -shared
,那么输出的共享对象就是使用装载时重定位的方法。
地址无关代码
如果采用装载时重定位的方法虽然能够做到任意地址装载,但存在弊端。比如模块装载到不同位置会导致模块的代码段内容发生改变,无法实现共享库的复用,造成内存浪费;每次装载重定位会影响性能等。
地址无关代码的出现很好的解决了装载时重定位的缺点。地址无关代码的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。这种方案就是目前被称为地址无关代码(PIC,Position-independent Code)的技术。这也就是 GCC 的 -fPIC
编译参数。
模块中各种类型的地址引用方式有以下 4 种:
- 模块内部的函数调用、跳转等。
- 模块内部的数据访问,比如模块中定义的全局变量、静态变量。
- 模块外部的函数调用、跳转等。
- 模块外部的数据访问,比如其他模块中定义的全局变量。
对于前两种引用方式由于是在模块内部,相对地址偏移固定,因此可以通过 [rip + xxx]
(注意这里的 rip 是当前指令的下一条指令的地址,下一条指令指的是地址相邻的下一条指令)的方式进行引用,从而做到地址无关。因此关键在于后两种怎么解决。
模块间的访问比模块内部稍微麻烦一点,因为模块间的数据访问目标地址要等到装载时才决定,我们前面提到要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段里面,很明显,这些其他模块的全局变量的地址是跟模块装载地址有关的。ELF 的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过 GOT 中相对应的项间接引用。
前面模块内部的解决方法实际上并不严谨,比如一些全局变量以及函数声明没有初始化会被认为是若弱符号,这些弱符号编译器并不知道是否只在本模块定义,因此不能仅使用 [rip + xxx]
的方式访问。
针对这种情况的解决办法是所有的使用这个变量的指令都指向位于可执行文件中的那个副本。ELF 共享库在编译时,默认都把定义在模块内部的全局变量当作定义在其他模块的全局变量,也就是说当作前面的类型四,通过 GOT 来实现变量的访问。当共享模块被装载时,如果某个全局变量在可执行文件中拥有副本,那么动态链接器就会把 GOT 中的相应地址指向该副本,这样该变量在运行时实际上最终就只有一个实例。如果变量在共享模块中被初始化,那么动态链接器还需要将该初始化值复制到程序主模块中的变量副本;如果该全局变量在程序主模块中没有副本,那么 GOT 中的相应地址就指向共享模块内部的该变量副本。这就是为什么 libc 的 GOT 表中会有自身函数。
地址无关代码虽然解决了模块复用的问题,但是本质还是装载时重定位因此没有解决性能问题,实际上 ELF 采用了延迟绑定的方法来解决这一问题。
地址无关代码技术除了可以用在共享对象上面,它也可以用于可执行文件,一个以地址无关方式编译的可执行文件被称作地址无关可执行文件(PIE, Position-Independent Executable)。与 GCC 的 -fPIC
和 -fpic
参数类似,产生 PIE 的参数为 -fPIE
或 -fpie
。
延迟绑定
在动态链接下,程序模块之间包含了大量的函数引用(全局变量往往比较少,因为大量的全局变量会导致模块之间耦合度变大),所以在程序开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用的符号查找以及重定位。可以想象,在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数或者是一些用户很少用到的功能模块等,如果一开始就把所有函数都链接好实际上是一种浪费。所以 ELF 采用了一种叫做延迟绑定(Lazy Binding)的做法,基本的思想就是当函数第一次被用到时才进行绑定(符号查找、重定位等),如果没有用到则不进行绑定。所以程序开始执行时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定。这样的做法可以大大加快程序的启动速度,特别有利于一些有大量函数引用和大量模块的程序。
注意,延迟绑定一般只出先在未开启 FULL RELRO 的时候,如果开启 FULL RELRO 则 got 表不可写,程序在装载时完成 got 表的重定位。当然特殊情况也有在开启 FULL RELRO 的时候进行重定位,比如 ret2dlresolve 。
我们以调用 puts
函数为例讲解一下延迟绑定的过程。
首先第一次调用 puts
时由于 puts@got
没有进行重定位,因此会调用 _dl_runtime_resolve
函数进行重定位,_dl_runtime_resolve
函数将查找到的 puts
函数地址填写到 puts@got
后会调用 puts
函数。
再次调用 puts
函数时由于 puts@got
已经完成重定位,因此会直接调用 puts
函数。
其中在第一次调用 puts
函数时调用的 _dl_runtime_resolve
函数的具体实现为:
- 用第一个参数
link_map
访问.dynamic
,取出.dynstr
,.dynsym
,.rel.plt
的指针。 .rel.plt + 第二个参数
求出当前函数的重定位表项Elf32_Rel
的指针,记作rel
。rel->r_info >> 8
作为.dynsym
的下标,求出当前函数的符号表项Elf32_Sym
的指针,记作sym
。.dynstr + sym->st_name
得出符号名字符串指针。- 在动态链接库查找这个函数的地址,并且把地址赋值给
*rel->r_offset
,即 GOT 表。 - 调用这个函数。
动态链接的步骤和实现
动态链接器自举
由于动态链接器本身的作用是重定位,因此自身的重定位也需要自身来完成,完成自身重定位的过程成为自举(Bootstrap)。
动态链接器入口地址即是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码即开始执行。自举代码首先会找到它自己的 GOT 。而 GOT 的第一个入口保存的即是 .dynamic
段的偏移地址,由此找到了动态连接器本身的“.dynamic”段。通过 .dynamic
中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们全部重定位。
从这一步开始,动态链接器代码中才可以开始使用自己的全局变量和静态变量。
装载共享对象
完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表(Global Symbol Table)。然后链接器开始寻找可执行文件所依赖的共享对象,我们前面提到过 .dynamic
段中,有一种类型的入口是 DT_NEEDED
,它所指出的是该可执行文件(或共享对象)所依赖的共享对象。由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中。然后链接器开始从集合里取一个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的 ELF 文件头和 .dynamic
段,然后将它相应的代码段和数据段映射到进程空间中。
如果这个 ELF 共享对象还依赖于其他共享对象,那么将所依赖的共享对象的名字放到装载集合中。如此循环直到所有依赖的共享对象都被装载进来为止,当然链接器可以有不同的装载顺序,如果我们把依赖关系看作一个图的话,那么这个装载过程就是一个图的遍历过程,链接器可能会使用深度优先或者广度优先或者其他的顺序来遍历整个图,这取决于链接器,比较常见的算法一般都是广度优先的。
当一个新的共享对象被装载进来的时候,它的符号表会被合并到全局符号表中,所以当所有的共享对象都被装载进来的时候,全局符号表里面将包含进程中所有的动态链接所需要的符号。
重定位和初始化
当上面的步骤完成之后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将它们的 GOT/PLT 中的每个需要重定位的位置进行修正。因为此时动态链接器已经拥有了进程的全局符号表,所以这个修正过程也显得比较容易,跟我们前面提到的地址重定位的原理基本相同。
动态链接重定位除了前面静态链接重定位类型外还有如下重定位类型:
R_386_RELATIVE
:针对下面这种代码的重定位,由于加载地址不确定,需要加载后的才能确定。static int a; static int* p = &a;
R_386_GLOB_DAT
:位于.got
的重定位入口,只需要填入正确变量地址即可。R_386_JUMP_SLOT
:位于.got.plt
的重定位入口,只需要填入正确的函数地址即可。
重定位完成之后,如果某个共享对象有 .init
段,那么动态链接器会执行 .init
段中的代码,用以实现共享对象特有的初始化过程,比如最常见的,共享对象中的 C++ 的全局/静态对象的构造就需要通过 .init
来初始化。相应地,共享对象中还可能有 .fini
段,当进程退出时会执行 .fini
段中的代码,可以用来实现类似 C++ 全局对象析构之类的操作。
如果进程的可执行文件也有 .init
段,那么动态链接器不会执行它,因为可执行文件中的 .init
段和 .fini
段由程序初始化部分代码负责执行。当完成了重定位和初始化之后,所有的准备工作就宣告完成了,所需要的共享对象也都已经装载并且链接完成了,这时候动态链接器就如释重负,将进程的控制权转交给程序的入口并且开始执行。
装载
Linux 内核装载 ELF 过程
首先在用户层面,bash 进程会调用 fork()
系统调用创建一个新的进程,然后新的进程调用 execve()
系统调用执行指定的 ELF 文件,原先的 bash 进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。
execve()
系统调用被定义在 unistd.h
,它的原型如下:
/* Replace the current process, executing PATH with arguments ARGV and
environment ENVP. ARGV and ENVP are terminated by NULL pointers. */
int execve (const char *__path, char *const __argv[], char *const __envp[]);
它的三个参数分别是被执行的程序文件名、执行参数和环境变量。
Glibc 对 execvp()
系统调用进行了包装,提供了 execl()
、execlp()
、execle()
、execv()
和 execvp()
等5个不同形式的 exec
系列 API ,它们只是在调用的参数形式上有所区别,但最终都会调用到 execve()
这个系统调用。下面是一个简单的使用 fork()
和 execlp()
实现的 minibash :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdbool.h>
#define MAX_COMMAND_LENGTH 1024
int main() {
char command[MAX_COMMAND_LENGTH];
while (true) {
printf("minibash$ ");
fgets(command, sizeof(command), stdin);
// 删除换行符
command[strcspn(command, "\n")] = '\0';
// 检查是否输入了退出命令
if (strcmp(command, "exit") == 0) {
break;
}
if (strlen(command) == 0) {
continue;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程执行命令
if (execlp(command, command, NULL) < 0) {
perror("minibash");
exit(1);
}
} else if (pid > 0) {
// 父进程等待子进程结束
int status;
waitpid(pid, &status, 0);
} else {
// fork失败
printf("fork error\n");
exit(1);
}
}
return 0;
}
在进入 execve()
系统调用之后,Linux 内核就开始进行真正的装载工作。在内核中, execve()
系统调用相应的入口是 sys_execve(), 它被定义在 arch\i386\kernel\Process.c
。sys_execve()
进行一些参数的检查复制之后,调用 do_execve()
。do_execve()
会首先查找被执行的文件,如果找到文件,则 do_execve()
读取文件的前128个字节判断文件的格式,每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头4个字节,常常被称做魔数(Magic Number),通过对魔数的判断可以确定文件的格式和类型。比如 ELF 的可执行文件格式的头 4 个字节为 \x7felf
;而 Java 的可执行文件格式的头4个字节为 cafe
;如果被执行的是 Shell 脚本或 perl 、python 等这种解释型语言的脚本,那么它的第一行往往是 #!/bin/sh
或 #!/usr/bin/perl
或 #!/usr/bin/python
,这时候前两个字节 #
和 !
就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序的路径。
当 do_execve()
读取了这 128 个字节的文件头部之后,然后调用 search_binary_handle()
去搜索和匹配合适的可执行文件装载处理过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程, search_binary_handle()
会通过判断文件头部的魔数确定文件的格式,并且调用相应的装载处理过程。比如 ELF 可执行文件的装载处理过程叫做 load_elf_binary()
; a.out 可执行文件的装载处理过程叫做 load_aout_binary()
;而装载可执行脚本程序的处理过程叫做 load_script()
。 这里我们只关心 ELF 可执行文件的装载, load_elf_binary()
被定义在 fs/Binfmt_elf.c
,这个函数的代码比较长,它的主要步骤是:
- 检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(Segment)的数量。
- 寻找动态链接的
.interp
段,设置动态链接器路径。 - 根据 ELF 可执行文件的程序头表的描述,对 ELF 文件进行映射,比如代码、数据、只读数据。
- 初始化 ELF 进程环境,比如进程启动时 EDX 寄存器的地址应该是
DT_FINI
的地址(参照动态链接)。 - 将系统调用的返回地址修改成 ELF 可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的 ELF 可执行文件,这个程序入口就是 ELF 文件的文件头中
e_entry
所指的地址;对于动态链接的 ELF 可执行文件,程序入口点是动态链接器。
当 load_elf_binary()
执行完毕,返回至 do_execve()
再返回至 sys_execve()
时,上面的第 5 步中已经把系统调用的返回地址改成了被装载的 ELF 程序的入口地址了。所以当 sys_execve()
系统调用从内核态返回到用户态时,EIP 寄存器直接跳转到了 ELF 程序的入口地址,于是新的程序开始执行,ELF 可执行文件装载完成。
进程虚拟地址空间
在现代操作系统中,每个进程都有自己的虚拟地址空间,这是一个抽象的地址空间,由连续的虚拟地址组成。每个进程在其虚拟地址空间中运行,不会直接访问物理内存地址。
操作系统将每个进程的虚拟地址空间划分为多个区域,例如代码段、数据段、堆和栈等。每个区域具有特定的用途和权限。
- 代码段:包含可执行程序的机器指令。
- 数据段:包含静态和全局变量的初始值。
- BSS 段:包含需要初始化为零的静态和全局变量。
- 动态链接段:包含动态链接所需的信息。
加载器将这些段从 ELF 文件中复制到相应的虚拟内存地址,并建立虚拟地址与物理内存地址的映射关系。
execve 系列函数之间的区别
execve
和其他 exec
系列函数(execl
, execlp
, execle
, execv
, execvp
, execvpe
)是 UNIX 和 Linux 系统编程中用于执行程序的重要工具。它们都用于在当前进程中加载并执行一个新程序,从而完全替换当前进程的内存空间、数据、堆栈等内容,但进程ID保持不变。这些函数通常用于需要替换当前执行的程序的情况,如 shell 实现中运行外部命令。
基本原理
所有这些函数最终都会调用系统的 execve
系统调用。execve
是实现其他 exec
函数的底层基础。当调用任一 exec
函数时,当前进程的地址空间将被新程序替换,但进程的 PID 保持不变。这意味着新程序将继续使用调用 exec
的进程的 PID,并从 main()
函数开始执行,而原进程的所有代码和数据则被新程序的代码和数据所替换。
函数差异
-
execve
- 原型:
int execve(const char *pathname, char *const argv[], char *const envp[]);
- 参数:
pathname
:要执行的程序路径。argv
:传递给新程序的参数数组,以 NULL 结尾。envp
:传递给新程序的环境变量数组,以 NULL 结尾。
- 特点:是唯一一个直接系统调用的
exec
函数,其他exec
函数最终都是通过调用execve
实现的。
- 原型:
-
execl, execlp, execle
- 特点:这些函数允许直接在函数调用中列出参数,而不是通过数组传递。
execl
和execle
需要提供程序的完整路径,而execlp
在 PATH 环境变量中搜索程序名。execle
允许直接指定环境变量。
-
execv, execvp, execvpe
- 特点:这些函数通过数组传递参数给新程序。
execv
需要提供程序的完整路径。execvp
和execvpe
在 PATH 环境变量中搜索程序名。execvpe
类似于execvp
,但允许指定环境变量。
使用场景
- execve:需要精确控制新程序的环境变量时使用。
- execl, execlp, execle:当参数数量已知且不需要动态构建参数数组时使用。
- execv, execvp, execvpe:当参数以数组形式提前构建好或在程序中动态生成时使用。
运行
进程栈的初始化
我们知道进程刚开始启动的时候,须知道一些进程运行的环境,最基本的就是系统环境变量和进程的运行参数。很常见的一种做法是操作系统在进程启动前将这些信息提前保存到进程的虚拟空间的栈中。
假设我们运行如下命令,即运行 ls
程序,传入的参数为 /home
。
ls /home
在程序初始状态的栈如下图所示。
栈顶寄存器 rsp 指向的位置是初始化以后堆栈的顶部,最前面的 8 个字节表示命令行参数的数量,我们的例子里面是两个,即 /usr/bin/ls
和 /home
,紧接的就是分布指向这两个参数字符串的指针;后面跟了一个0;接着是一个以 0 结尾的指向环境变量字符串的指针数组。
进程在启动以后,程序的库部分会把堆栈里的初始化信息中的参数信息传递给 main()
函数,也就是我们熟知的 main()
函数的两个 argc
和 argv
两个参数,这两个参数分别对应这里的命令行参数数量和命令行参数字符串指针数组。
main 函数之外的代码
当运行程序时,shell 或 gui 调用 execve()
,它执行 linux 系统调用 execve()
设置一个堆栈,并将 argc
、 argv
和 envp
压入其中。文件描述 0、1 和 2(stdin
、stdout
、stderr
)保留为 shell 设置的值,动态链接器完成重定位工作。当一切准备就绪后,通过调用 _start()
将控制权交给程序。
一般情况下 ELF 的入口点为 _start
函数,这个函数的主要作用是设置 ___libc_start_main
函数的所需参数。
.text:08049080 endbr32
.text:08049084 xor ebp, ebp ; 设置 ebp 为 0 表示最外层栈
.text:08049086 pop esi ; argc
.text:08049087 mov ecx, esp ; argv
.text:08049089 and esp, 0FFFFFFF0h ; 栈对齐
.text:0804908C push eax ; 静态链接程序默认为 0 ,动态链接程序默认为模块对应的 link_map
.text:0804908D push esp ; stack_end
.text:0804908E push edx ; rtld_fini
.text:0804908F call sub_80490B7
.text:0804908F
.text:08049094 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $); ebx = offset _GLOBAL_OFFSET_TABLE_
.text:0804909A lea eax, (__libc_csu_fini - 804C000h)[ebx] ; (__libc_csu_fini - _GLOBAL_OFFSET_TABLE_)[ebx]
.text:080490A0 push eax ; fini
.text:080490A1 lea eax, (__libc_csu_init - 804C000h)[ebx] ; (__libc_csu_init - _GLOBAL_OFFSET_TABLE_)[ebx]
.text:080490A7 push eax ; init
.text:080490A8 push ecx ; ubp_av
.text:080490A9 push esi ; argc
.text:080490AA mov eax, offset main
.text:080490B0 push eax ; main
.text:080490B1 call ___libc_start_main
.text:080490B1
.text:080490B6 hlt
.text:080490B7 sub_80490B7 proc near ; CODE XREF: _start+F↑p
.text:080490B7 mov ebx, [esp]
.text:080490BA retn
之后调用 __libc_start_main
函数,通过调试发现使用 glibc-2.23 的 32 位程序实际调用的是 generic_start_main
函数,该函数位于 csu/libc-start.c
中,定义如下:
# define LIBC_START_MAIN generic_start_main
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
可见和 _start
函数里的调用一致,一共有 7 个参数,其中 main
由第一个参数传入,紧接着是 argc
和 argv
(这里称为 ubp_av
,因为其中还包含了环境变量表)。除了 main
的函数指针之外,外部还要传入 3 个函数指针,分别是:
init
:main
调用前的初始化工作,默认是__libc_csu_init
函数指针。fini
:main
结束后的收尾工作,默认是__libc_csu_fini
函数指针。rtld_fini
:和动态加载有关的收尾工作,rtld
是 runtime loader 的缩写。如果是动态链接程序默认是_dl_fini
函数指针,如果是静态链接程序默认为 NULL 。
最后的 stack_end
标明了栈底的地址,即最高的栈地址。
首先初始化 __libc_multiple_libcs
为 0 之后 generic_start_main
会调用 __cxa_atexit
将 rtld_fini
注册为 main
函数结束后的回调函数。
if (__glibc_likely (rtld_fini != NULL))
__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);
当然如果是静态链接程序还会做一些额外的初始化,在动态链接程序中这些初始化是在动态连接器中完成的。
- 如果是开启 PIE 的静态程序会调用
_dl_relocate_static_pie
函数初始化link_map
并且进行重定位。 - 初始化
__environ
。 - 初始化
__libc_stack_end
。 - 调用
dl_aux_init
根据栈上的辅助信息数组做相关的初始化工作。 - 如果程序头表指针
dl_phdr
没有初始化,就通过 ELF 文件头的e_phoff
初始化dl_phdr
(程序头表地址),通过e_phnum
初始化dl_phnum
(程序头表中的项数)。不过这个一般在上一步根据栈上的辅助信息数组做相关的初始化工作时就已经初始化过了。 - 调用
_libc_init_secure
函数,如果_libc_enable_secure_decided
不为 0 则初始化_libc_enable_secure
为(__geteuid() != __getuid() || __getegid() != __getgid())
。__geteuid() != __getuid()
:比较有效用户 ID(effective user ID)和实际用户 ID(real user ID)。如果它们不相等,表示当前进程以特权用户身份运行(比如以 root 用户权限运行)。__getegid() != __getgid()
:比较有效组 ID(effective group ID)和实际组 ID(real group ID)。如果它们不相等,表示当前进程以特权用户组身份运行。- 这段代码的目的是判断当前进程是否以特权用户或特权用户组身份运行。这在某些情况下可能需要采取不同的安全措施或限制特权操作。
- 调用
__tunables_init
函数从环境变量中提取信息,并用于初始化可调节项列表,以便在程序运行时可以根据这些可调节项来进行相应的配置或调整。 - 使用
ARCH_INIT_CPU_FEATURES
宏初始化 CPU 的相关参数到cpu_features
类型的结构体_dl_x86_cpu_features
中。 - 重定位代码中的绝对地址引用 。
- 调用
__libc_setup_tls
函数初始化 tls 。 - 如果
__libc_multiple_libcs
为 0 则调用DL_SYSDEP_OSCHECK
宏来初始化dl_osversion
为内核版本号。 - 调用
__pthread_initialize_minimal
函数初始化线程库 。 - 初始化
__stack_chk_guard
。 - 初始化
pointer_chk_guard
。 - 调用
_libc_init_first
函数初始化_libc_argc
,_libc_argv
和environ
等。 - 调用
__cxa_atexit
函数将fini
注册为main
函数结束后的回调函数 。
之后判断函数指针 init
是否为空,如果不为空则调用该函数指针,也就是 __libc_csu_init
函数。
__libc_csu_init
函数定义在 csu/elf-init.c
中,内容如下:
- 如果是静态链接程序会依次调用函数指针数组
__preinit_array_start
中的所有函数。 - 调用
_init
函数。 - 依次调用函数指针数组
__init_array_start
(.init_array
)中的所有函数。
void
__libc_csu_init (int argc, char **argv, char **envp)
{
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */
#ifndef LIBC_NONSHARED
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
#endif
#ifndef NO_INITFINI
_init ();
#endif
const size_t size = __init_array_end - __init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}
其中调用的 init
函数如下:
.init:0000037C push ebx ; _init
.init:0000037D sub esp, 8
.init:00000380 call __x86_get_pc_thunk_bx
.init:00000380
.init:00000385 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.init:0000038B mov eax, ds:(__gmon_start___ptr - 1FD8h)[ebx]
.init:00000391 test eax, eax
.init:00000393 jz short loc_39A
.init:00000393
.init:00000395 call ___gmon_start__
.init:00000395
.init:0000039A
.init:0000039A loc_39A: ; CODE XREF: _init_proc+17↑j
.init:0000039A add esp, 8
.init:0000039D pop ebx
.init:0000039E retn
在静态链接程序中直接 mov eax, 0; test eax, eax;
,因此这个函数什么也不做。而动态链接程序中由于此时 __gmon_start___@got
为 NULL ,因此同样什么也不做。
从 __libc_csu_init
函数返回后会调用 main
函数和 exit
函数。
exit 中的 hook
exit
函数定义如下:
//stdlib/exit.c
void exit (int status) {
__run_exit_handlers (status, &__exit_funcs, true);
}
void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit) {
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
//首先销毁TLS
__call_tls_dtors ();
//遍历__exit_funcs,包括_dl_fini
while (*listp != NULL) {
struct exit_function_list *cur = *listp;
//...
*listp = cur->next;
if (*listp != NULL)
free (cur);
}
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
_exit (status);
/*
_exit (int status) {
status &= 0xff;
abort ();
}
*/
}
首先 __call_tls_dtors
会被 exit
调用。
void __call_tls_dtors (void) {
while (tls_dtor_list) {
struct dtor_list *cur = tls_dtor_list;
dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (func);
#endif
tls_dtor_list = tls_dtor_list->next;
func (cur->obj);
atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
free (cur);
}
}
泄露 pointer_guard
后可以劫持 tls_dtor_list
,构造 dtor_list
结构体控制 rdi(obj
域)和 rdx(next
域),进而利用 setcontext
来劫持程序执行流程 。
struct dtor_list {
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
};
泄露 pointer_guard
后(如果该 glibc 版本加密了该函数指针)可以通过劫持 __exit_funcs
数组来获取控制流。
//遍历__exit_funcs,包括_dl_fini
while (*listp != NULL) {
struct exit_function_list *cur = *listp;
while (cur->idx > 0) {
const struct exit_function *const f = &cur->fns[--cur->idx];
switch (f->flavor) {
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (onfct);
#endif
onfct (status, f->func.on.arg);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (atfct);
#endif
atfct ();
break;
case ef_cxa:
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
cxafct (f->func.cxa.arg, status);
break;
}
}
*listp = cur->next;
if (*listp != NULL)
free (cur);
}
但这种方法只能控制 rsi 。
struct exit_function {
long int flavor;
union {
void (*at) (void);
struct {
void (*fn) (int status, void *arg);
void *arg;
} on;
struct {
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};
struct exit_function_list {
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};
如果是动态链接程序 __run_exit_handlers
函数会调用 _dl_fini
函数。
_dl_fini
函数定义如下,该函数的主要作用就是依次调用 link_map->l_info[DT_FINI_ARRAY]
中描述的函数数组中的函数指针。有一种攻击方法就是通过伪造 link_map
来实现控制流劫持,这种攻击方法叫做 House Of Banana 。
//省略了有关SHARED的操作
void internal_function _dl_fini (void) {
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) {
__rtld_lock_lock_recursive (GL(dl_load_lock));
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
if (nloaded == 0)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
else {
struct link_map *maps[nloaded];
unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
//将_rtld_global.dl_ns[ns]._ns_loaded链表的结点经过check后置入maps
if (l == l->l_real) {
assert (i < nloaded);
maps[i] = l;
l->l_idx = i;
++i;
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
//_ns_loaded链表中至少有三个节点
unsigned int nmaps = i;
_dl_sort_fini (maps, nmaps, NULL, ns);
__rtld_lock_unlock_recursive (GL(dl_load_lock));
for (i = 0; i < nmaps; ++i) {
struct link_map *l = maps[i];
//遍历执行maps[i]里的函数指针
if (l->l_init_called) {
l->l_init_called = 0;
if (l->l_info[DT_FINI_ARRAY] != NULL || l->l_info[DT_FINI] != NULL) {
if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n", DSO_FILENAME (l->l_name), ns);
if (l->l_info[DT_FINI_ARRAY] != NULL) {
ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}
if (l->l_info[DT_FINI] != NULL)
DL_CALL_DT_FINI (l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
}
}
--l->l_direct_opencount;
}
}
}
}
另外 _dl_fini
中的 __rtld_lock_lock_recursive
和 __rtld_lock_unlock_recursive
宏展开后分别为 _rtld_local._dl_rtld_lock_recursive(&(_rtld_local._dl_load_lock).mutex)
和 _rtld_local._dl_rtld_unlock_recursive(&(_rtld_local._dl_load_lock).mutex)
因此我们可以劫持对应函数指针完成控制流劫持,这些函数指针就是狭义上的 exit hook 。
如果是静态链接程序 __run_exit_handlers
函数会调用 __libc_csu_fini
函数。__libc_csu_fini
函数会依次调用 .fini_array
中的函数指针,因此我们可以通过改写 .fini_array
实现控制流劫持。
void
__libc_csu_fini (void)
{
#ifndef LIBC_NONSHARED
size_t i = __fini_array_end - __fini_array_start;
while (i-- > 0)
(*__fini_array_start [i]) ();
# ifndef NO_INITFINI
_fini ();
# endif
#endif
}
之后调用 RUN_HOOK
宏:
if (run_list_atexit)
RUN_HOOK(__libc_atexit, ());
这个宏展开后的结果如下,可以看到这个宏会依次调用 __start___libc_atexit
函数指针数组直到遇到 NULL 。
do {
void *const *ptr;
for (ptr = (void *const *) ((void *const *) (&__start___libc_atexit)); !((ptr) >= (void *const *) &__stop___libc_atexit); ++ptr) (*(____libc_atexit_hook_function_t *) *ptr)();
} while (0)
函数指针所在的内存在动态链接程序中位于 libc 上。
在静态链接程序中位于程序的 __libc_atexit
段。
用于 FSOP
的 _IO_cleanup
就是在这里被调用的,另外如果我们能控制这里的函数指针也可以劫持程序执行流程。
共享库
共享库版本
共享库版本命名
Linux有一套规则来命名系统中的每一个共享库,它规定共享库的文件名规则必须如下:
libname.so.x.y.z
最前面使用前缀 lib
、中间是库的名字和后缀 .so
,最后面跟着的是三个数字组成的版本号。x
表示主版本号(Major Version Number),y
表示次版本号(Minor Version Number),z
表示发布版本号(Release Version Number)。三个版本号的含义不一样。
- 主版本号表示库的重大升级,不同主版本号的库之间是不兼容的,依赖于旧的主版本号的程序需要改动相应的部分,并且重新编译,才可以在新版的共享库中运行;或者,系统必须保留旧版的共享库,使得那些依赖于旧版共享库的程序能够正常运行。
- 次版本号表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。在主版本号相同的情况下,高的次版本号的库向后兼容低的次版本号的库。
- 发布版本号表示库的一些错误的修正、性能的改进等,并不添加任何新的接口,也不对接口进行更改。相同主版本号、次版本号的共享库,不同的发布版本号之间完全兼容,依赖于某个发布版本号的程序可以在任何一个其他发布版本号中正常运行,而无须做任何修改。
SO-NAME
系统普遍采用一种叫做 SO-NAME 的命名机制来记录共享库的依赖关系。每个共享库都有一个对应的 SO-NAME ,这个 SO-NAME 即共享库的文件名去掉次版本号和发布版本号,保留主版本号。比如一个共享库叫做 libfoo.so.2.6.1
,那么它的 SO-NAME 即 libfoo.so.2
。很明显,SO-NAME 规定了共享库的接口,SO-NAME 的两个相同共享库,次版本号大的兼容次版本号小的。在 Linux 系统中,系统会为每个共享库在它所在的目录创建一个跟 SO-NAME 相同的并且指向它的软链接(Symbol Link)。比如系统中有存在一个共享库 /lib/libfoo.so.2.6.1
,那么 Linux 中的共享库管理程序就会为它产生一个软链接 /lib/libfoo.so.2
指向它。比如 Linux 系统的 Glibc 共享库(注意稍高版本的 libc 的 libc.so.6
本身就是动态库,不是符号链接):
$ ls -l /lib/x86_64-linux-gnu/libc.so.6
lrwxrwxrwx 1 root root 12 Apr 7 2022 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 Apr 7 2022 /lib/x86_64-linux-gnu/libc-2.31.so
由于历史原因,动态链接器和 C 语言库的共享对象文件名规则不按 Linux 标准的共享库命名方法,但是 C 语言的 SO-NAME 还是按照正常的规则。
另外动态连接器的 SO-NAME 命名不按照普通的规则。
$ ls -al /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 32 Apr 7 2022 /lib64/ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-2.31.so
建立以 SO-NAME 为名字的软链接目的是,使得所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的 SO-NAME ,而不使用详细的版本号。
动态链接文件中的 .dynamic
段中的 DT_NEED
类型的字段就是 SO-NAME 而不是共享库的完整名字,这样当动态链接器进行共享库依赖文件查找时,就会根据系统中各种共享库目录中的SO-NAME软链接自动定向到最新版本的共享库。
当共享库进行升级的时候,如果只是进行增量升级,即保持主版本号不变,只改变次版本号或发布版本号,那么我们可以直接将新版的共享库替换掉旧版,并且修改 SO-NAME 的软链接指向新版本共享库,即可实现升级;当共享库的主版本号升级时,系统中就会存在多个 SO-NAME ,由于这些 SO-NAME 并不相同,所以已有的程序并不会受影响。
Linux 中提供了一个工具叫做 ldconfig
,当系统中安装或更新一个共享库时,就需要运行这个工具,它会遍历所有的默认共享库目录,比如 /lib
、/usr/lib
等,然后更新所有的软链接,使它们指向最新版的共享库;如果安装了新的共享库,那么 ldconfig
会为其创建相应的软链接。
符号版本
根据提到的可知,一个程序所依赖的共享库的次版本号如果高于系统中的共享库,那么就不保证该程序能在该系统中运行,这类问题叫做次版本号交会问题(Minor-revision Rendezvous Problem)。
这种次版本号交会问题并没有因为 SO-NAME 的存在而得到任何改善。对于这个问题,现代的系统通过一种更加精巧的方式来解决,那就是符号版本机制。这个方案的基本思路是让每个导出和导入的符号都有一个相关联的版本号,它的实际做法类似于名称修饰的方法。
.dynamic
段中的 DT_VERSYM
类型字段包含了符号版本。它的作用是维护库的版本信息,以便在运行时进行版本控制和符号解析。通过 DT_VERSYM
,动态链接器可以确定所链接的库的版本与运行时环境是否兼容,以及选择正确的版本来解析符号。
共享库系统路径
目前大多数包括 Linux 在内的开源操作系统都遵守一个叫做 FHS(File Hierarchy Standard)的标准,这个标准规定了一个系统中的系统文件应该如何存放,包括各个目录的结构、组织和作用,这有利于促进各个开源操作系统之间的兼容性。共享库作为系统中重要的文件,它们的存放方式也被 FHS 列入了规定范围。FHS 规定,一个系统中主要有两个存放共享库的位置,它们分别如下:
/lib
:该目录包含操作系统核心组件所需的共享库文件。这些库文件通常是系统引导和运行时所必需的,例如与操作系统内核相关的库文件。/usr/lib
:该目录包含操作系统提供的额外共享库文件。这些库文件用于支持系统上安装的应用程序和工具的运行,如图形界面工具包(GUI toolkit)、网络库、数据库驱动程序等。/usr/local/lib
:该目录是用于安装本地(local)软件的库文件的默认位置。当用户手动编译和安装软件到系统时,通常会将其安装到/usr/local
目录下。因此,相关的库文件也会被安装到/usr/local/lib
目录下。
共享库查找过程
动态链接器对于模块的查找有一定的规则:如果 DT_NEED
里面保存的是绝对路径,那么动态链接器就按照这个路径去查找;如果 DT_NEED
里面保存的是相对路径,那么动态链接器会在 /lib
、/usr/lib
和由 /etc/ld.so.conf
配置文件指定的目录中查找共享库。为了程序的可移植性和兼容性,共享库的路径往往是相对的。
ld.so.conf
是一个文本配置文件,它可能包含其他的配置文件,这些配置文件中存放着目录信息。
➜ ~ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf
➜ ~ cat /etc/ld.so.conf.d/*.conf
/usr/lib/x86_64-linux-gnu/libfakeroot
# Multiarch support
/usr/local/lib/i386-linux-gnu
/lib/i386-linux-gnu
/usr/lib/i386-linux-gnu
/usr/local/lib/i686-linux-gnu
/lib/i686-linux-gnu
/usr/lib/i686-linux-gnu
# libc default configuration
/usr/local/lib
# Multiarch support
/usr/local/lib/x86_64-linux-gnu
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu
# Legacy biarch compatibility support
/lib32
/usr/lib32
如果动态链接器在每次查找共享库时都去遍历这些目录,那将会非常耗费时间。所以 Linux 系统中都有一个叫做 ldconfig
的程序,这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的 SO-NAME(即相应的符号链接),这样每个共享库的 SO-NAME 就能够指向正确的共享库文件;并且这个程序还会将这些 SO-NAME 收集起来,集中存放到 /etc/ld.so.cache
文件里面,并建立一个 SO-NAME 的缓存。当动态链接器要查找共享库时,它可以直接从 /etc/ld.so.cache
里面查找。而 /etc/ld.so.cache
的结构是经过特殊设计的,非常适合查找,所以这个设计大大加快了共享库的查找过程。
如果动态链接器在 /etc/ld.so.cache
里面没有找到所需要的共享库,那么它还会遍历 /lib
和 /usr/lib
这两个目录,如果还是没找到,就宣告失败。
所以理论上讲,如果我们在系统指定的共享库目录下添加、删除或更新任何一个共享库,或者我们更改了 /etc/ld.so.conf
的配置,都应该运行 ldconfig
这个程序,以便调整 SO-NAME 和 /etc/ld.so.cache
。很多软件包的安装程序在往系统里面安装共享库以后都会调用 ldconfig
。
更改共享库
Linux 系统提供了很多方法来改变动态链接器装载共享库路径的方法,通过使用这些方法,我们可以满足一些特殊的需求,比如共享库的调试和测试、应用程序级别的虚拟等。
LD_LIBRARY_PATH
在 Linux 系统中,LD_LIBRARY_PATH
是一个由若干个路径组成的环境变量,每个路径之间由冒号隔开。默认情况下, LD_LIBRARY_PATH
为空。如果我们为某个进程设置了 LD_LIBRARY_PATH
,那么进程在启动时,动态链接器在查找共享库时,会首先查找由 LD_LIBRARY_PATH
指定的目录。这个环境变量可以很方便地让我们测试新的共享库或使用非标准的共享库。
比如更换 libdl.so.2
和 libc.so.6
的 pwntools 脚本如下:
sh = process("./lib/ld.so --preload libdl.so.2 ./pwnhub".split(), env={"LD_LIBRARY_PATH": "./lib/"})
LD_PRELOAD
系统中另外还有一个环境变量叫做 LD_PRELOAD
,这个文件中我们可以指定预先装载的一些共享库甚或是目标文件。在 LD_PRELOAD
里面指定的文件会在动态链接器按照固定规则搜索共享库之前装载,它比 LD_LIBRARY_PATH
里面所指定的目录中的共享库还要优先。无论程序是否依赖于它们,LD_PRELOAD
里面指定的共享库或目标文件都会被装载。
比如更换 libdl.so.2
和 libc.so.6
的 pwntools 脚本如下:
process("./lib/ld.so ./pwnhub".split(), env={"LD_PRELOAD": "./lib/libc.so.6 ./lib/libdl.so.2"})
LD_DEBUG
另外还有一个非常有用的环境变量 LD_DEBUG
,这个变量可以打开动态链接器的调试功能,当我们设置这个变量时,动态链接器会在运行时打印出各种有用的信息,对于我们开发和调试共享库有很大的帮助。
例如运行 LD_DEBUG=files /bin/ls
命令时动态链接器打印出了整个装载过程,显示程序依赖于哪个共享库并且按照什么步骤装载和初始化,共享库装载时的地址等。
bindings
:显示动态链接的符号绑定过程。libs
:显示共享库的查找过程。versions
:显示符号的版本依赖关系。reloc
:显示重定位过程。symbols
:显示符号表查找过程。statistics
:显示动态链接过程中的各种统计信息。
patchelf
用于对于依赖不是很复杂的程序更换 libc ,有一下几点需要注意:
- 如果在漏洞利用时用到了动态链接相关结构最好不要 patchelf,因为 patchelf 会改变动态链接相关结构的位置。
- 一个程序在一个版本的虚拟机里面 patchelf 后换到另一个版本虚拟机中可能会运行失败。
- 在 patch 完 libc 后最好把 ld 也 patch 成大版本相同的 ld ,否则会运行失败。
修改 libc:
patchelf --replace-needed libc.so.6 ./libc.so.6 ./pwn
修改 ld:
patchelf --set-interpreter ./ld-2.31.so ./pwn
多线程与 TLS
基本概念
线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,那么这就是很少见的情况),但实际运用中线程也拥有自己的私有存储空间,包括以下几方面:
- 栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)。
- 线程局部存储(Thread Local Storage, TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。
- 寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有。
实际上,线程私有的数据有:
- 局部变量
- 函数的参数
- TLS 数据
线程共享的数据有:
- 全局变量
- 堆上的数据
- 函数里的静态变量
- 程序代码,任何线程都有有权利读取并执行任何代码。
- 打开的文件,A 线程打开的文件可以由 B 线程读写。
一个全局变量如果使用 __thread
关键字修饰,那么这个变量就变成线程私有的 TLS 数据,也就是说每个线程都在自己所属 TLS 中单独保存一份这个变量的副本。例如下面的代码中,a
和 b
都是 TLS 数据,而 c
是全局变量。
// gcc test.c -o test -g -pthread
#include <pthread.h>
#include <stdio.h>
#include <stdint-gcc.h>
__thread uint32_t a = 0x114514;
__thread uint32_t b;
uint32_t c = 0x1919810;
void *thread(void *arg) {
printf("thread: a(%p) = %x, b(%p) = %x, c(%p) = %x\n", &a, a, &b, b, &c, c);
return NULL;
}
int main(void) {
a = 0x12345678;
b = 0x87654321;
c = 0xdeadbeef;
printf("thread: a(%p) = %x, b(%p) = %x, c(%p) = %x\n", &a, a, &b, b, &c, c);
pthread_t pid;
pthread_create(&pid, NULL, thread, NULL);
pthread_join(pid, NULL);
return 0;
}
/*
thread: a(0x7f1ec78f0738) = 12345678, b(0x7f1ec78f073c) = 87654321, c(0x562d7468a010) = deadbeef
thread: a(0x7f1ec70ed6f8) = 114514, b(0x7f1ec70ed6fc) = 0, c(0x562d7468a010) = deadbeef
*/
分析生成的 ELF 文件的节表,发现多出了 .tdata
和 .tbss
,这两个节分别记录已初始化和未初始化的 TLS 数据。
其中 .tbss
在 ELF 文件中不占用空间, .tdata
在 ELF 中存储了初始化的数据,比如上面的代码中的 __thread uint32_t a = 0x114514
。
ELF 加载到内存中后, .tdata
和 .tbss
这两个节合并为一个段,在程序头表中这个段的 p_type
为 PT_TLS(7)
。
TLS(Thread Local Storage)的结构与 TCB(Thread Control Block)以及 dtv(dynamic thread vector)密切相关,每一个线程中每一个使用了 TLS 功能的模块都拥有一个 TLS Block 。这几者的关系如下图所示:
注意,这里是 x86_64-ABI 要求的 TLS 结构,Glibc 实现的 TLS 结构与上图有一些差异。
根据图中显示的信息,TLS Blocks 可以分为两类:
- 一类是程序装载时就已经存在的(位于 TCB 前),这一部分 Block 被称为
_static TLS_
。 - 一类是右边的 Blocks 是动态分配的,它们被使用
dlopen
函数在程序运行时动态装载的模块所使用。
TCB 作为线程控制块,保存着 dtv
数组的入口,dtv
数组中的每一项都是 TLS Block 的入口,它们是指向 TLS Blocks 的指针。特别的,dtv
数组的第一个成员是一个计数器,每当程序使用 dlopen
函数或者 dlfree
函数加载或者卸载一个具备 TLS 变量的模块,该计数器的值都会加一,从而保证程序内版本的一致性。 特别的,ELF 文件本身对应的 TLS Block 一定在 dtv
数组中占据索引为 1 的位置,且位置上与 TCB 相邻。 还需要注意的是,图中出现了一个名为
t
p
t
tp_t
tpt 的指针,在 i386 架构上,这个指针为 gs 段寄存器;在 x86_64 架构上,该指针为 fs 段寄存器。由于该指针与 ELF 文件本身对应的 TLS Block 之间的偏移是固定的,程序在编译时就可以将 ELF 中线程变量的地址硬编码到目标文件中。
主线程 TLS 初始化
前面提到过在 main
开始前会调用 __libc_setup_tls
初始化 TLS 。
在 __libc_setup_tls
函数中,首先会遍历 ELF 的程序头表,找到 p_type
为 PT_TLS(7)
的段,这个段中就存储着 TLS 的初始化数据。
/* Look through the TLS segment if there is any. */
if (_dl_phdr != NULL)
for (phdr = _dl_phdr; phdr < &_dl_phdr[_dl_phnum]; ++phdr)
if (phdr->p_type == PT_TLS) {
/* Remember the values we need. */
memsz = phdr->p_memsz;
filesz = phdr->p_filesz;
initimage = (void *) phdr->p_vaddr + main_map->l_addr;
align = phdr->p_align;
if (phdr->p_align > max_align)
max_align = phdr->p_align;
break;
}
然后通过 brk
调用为 TLS 中的数据以及一个 pthread
结构体分配内存。其中 pthread
结构体的第一项为 tcbhead_t header;
,即前面提到的 TCB 。
/* Align the TCB offset to the maximum alignment, as
_dl_allocate_tls_storage (in elf/dl-tls.c) does using __libc_memalign
and dl_tls_static_align. */
tcb_offset = roundup (memsz + GLRO(dl_tls_static_surplus), max_align);
tlsblock = __sbrk(tcb_offset + TLS_INIT_TCB_SIZE + max_align);
tcbhead_t
结构体定义如下,也就是很多资料中提到的 TLS 。
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int vgetcpu_cache[2];
# ifndef __ASSUME_PRIVATE_FUTEX
int private_futex;
# else
int __glibc_reserved1;
# endif
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
long int __glibc_reserved2;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8];
} tcbhead_t;
之后初始化 _dl_static_dtv
,也就是前面提到的 dtv
数组,具体过程为:
- 将
tlsblock
地址关于max_align
向上对齐。 _dl_static_dtv[0].counter
初始化为dtv
的数量,由于_dl_static_dtv
前两项分别用于记录dtv
总数和使用的数量,因此这里记录的dtv
数量是要减去这两项的。_dl_static_dtv[1].counter
初始化为 0 。_dl_static_dtv[2]
也就是当前模块对应的dtv
的pointer.val
指向 TLS 。_dl_static_dtv[2].pointer.to_free
置为 NULL 。- 将 TLS 的初始数据也就是
PT_TLS
段中的数据复制到 TLS 中。
struct dtv_pointer
{
void *val; /* Pointer to data, or TLS_DTV_UNALLOCATED. */
void *to_free; /* Unaligned pointer, for deallocation. */
};
/* Type for the dtv. */
typedef union dtv
{
size_t counter;
struct dtv_pointer pointer;
} dtv_t;
/* Number of additional entries in the slotinfo array of each slotinfo
list element. A large number makes it almost certain take we never
have to iterate beyond the first element in the slotinfo list. */
#define TLS_SLOTINFO_SURPLUS (62)
dtv_t _dl_static_dtv[2 + TLS_SLOTINFO_SURPLUS];
/* Align the TLS block. */
tlsblock = (void *) (((uintptr_t) tlsblock + max_align - 1)
& ~(max_align - 1));
/* Initialize the dtv. [0] is the length, [1] the generation counter. */
_dl_static_dtv[0].counter = (sizeof(_dl_static_dtv) / sizeof(_dl_static_dtv[0])) - 2;
// _dl_static_dtv[1].counter = 0; would be needed if not already done
/* Initialize the TLS block. */
_dl_static_dtv[2].pointer.val = ((char *) tlsblock + tcb_offset
- roundup (memsz, align ?: 1));
_dl_static_dtv[2].pointer.to_free = NULL;
/* sbrk gives us zero'd memory, so we don't need to clear the remainder. */
memcpy(_dl_static_dtv[2].pointer.val, initimage, filesz);
此时 TLS 相关结构之间的关系如下图所示:
另外还会初始化 link_map
中的 TLS 相关的数据,由此我们可以知道 link_map
中这些字段的含义:
l_tls_offset
:TCB 在 TLS 中的偏移。l_tls_align
:TLS 初始数据的对齐,在 TLS 中 TLS 初始数据关于l_tls_align
向上取整。l_tls_blocksize
:TLS 初始数据的大小,也就是前面提到的 TLS Block 的大小。l_tls_initimage
:TLS 初始数据的地址。也就是PT_TLS
段的地址。l_tls_initimage_size
:PT_TLS
段在文件中的大小,也就是.tdata
的大小。l_tls_modid
:模块编号。
struct link_map *main_map = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
main_map->l_tls_offset = roundup (memsz, align ?: 1);
/* Update the executable's link map with enough information to make
the TLS routines happy. */
main_map->l_tls_align = align;
main_map->l_tls_blocksize = memsz;
main_map->l_tls_initimage = initimage;
main_map->l_tls_initimage_size = filesz;
main_map->l_tls_modid = 1;
创建线程时 TLS 初始化
创建线程的函数 pthread_create
实际调用的是 __pthread_create_2_1
函数,在该函数中调用了 allocate_stack
函数。
# define ALLOCATE_STACK(attr, pd) allocate_stack (attr, pd, &stackaddr)
struct pthread *pd = NULL;
int err = ALLOCATE_STACK (iattr, &pd);
在 allocate_stack
函数中会调用 mmap
为线程分配栈空间,然后初始化栈底为一个 pthread
结构体并将指针 pd
指向该结构体。最后调用 _dl_allocate_tls
函数为 TCB 创建 dtv
数组。
struct pthread *pd;
...
mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
...
pd = (struct pthread *) ((((uintptr_t) mem + size) - TLS_TCB_SIZE) & ~__static_tls_align_m1);
...
_dl_allocate_tls (TLS_TPADJ (pd))
_dl_allocate_tls
函数依次调用 allocate_dtv
和 _dl_allocate_tls_init
分配和初始化 dtv
数组。
void *
_dl_allocate_tls (void *mem)
{
return _dl_allocate_tls_init (mem == NULL
? _dl_allocate_tls_storage ()
: allocate_dtv (mem));
}
allocate_dtv
函数调用了 ptmalloc 堆管理器的 calloc
函数为 dtv
数组分配内存,初始化 dtv[0].counter
为数组中元素数量,并且让 pd->dtv
指向 dtv[1]
。
/* Install the dtv pointer. The pointer passed is to the element with
index -1 which contain the length. */
# define INSTALL_DTV(descr, dtvp) \
((tcbhead_t *) (descr))->dtv = (dtvp) + 1
static void *
allocate_dtv (void *result)
{
dtv_t *dtv;
size_t dtv_length;
/* We allocate a few more elements in the dtv than are needed for the
initial set of modules. This should avoid in most cases expansions
of the dtv. */
dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS;
dtv = calloc (dtv_length + 2, sizeof (dtv_t));
if (dtv != NULL)
{
/* This is the initial length of the dtv. */
dtv[0].counter = dtv_length;
/* The rest of the dtv (including the generation counter) is
Initialize with zero to indicate nothing there. */
/* Add the dtv to the thread data structures. */
INSTALL_DTV (result, dtv);
}
else
result = NULL;
return result;
}
_dl_allocate_tls_init
函数会遍历 dl_tls_dtv_slotinfo_list
中的 link_map
,初始化 dtv
数组并将初始数据复制到 TLS 变量中。从这里可以看出,如果一个模块有 TLS 变量,则该模块对应的 dtv->pointer.val
指向 TLS 变量的起始地址。
dtv[map->l_tls_modid].pointer.val = TLS_DTV_UNALLOCATED;
dtv[map->l_tls_modid].pointer.to_free = NULL;
if (map->l_tls_offset == NO_TLS_OFFSET
|| map->l_tls_offset == FORCED_DYNAMIC_TLS_OFFSET)
continue;
/* Set up the DTV entry. The simplified __tls_get_addr that
some platforms use in static programs requires it. */
dtv[map->l_tls_modid].pointer.val = dest;
/* Copy the initialization image and clear the BSS part. */
memset(__mempcpy (dest, map->l_tls_initimage,
map->l_tls_initimage_size), '\0',
map->l_tls_blocksize - map->l_tls_initimage_size);
回到 __pthread_create_2_1
函数,在完成了 pthread
的一系列初始化后调用了 THREAD_COPY_STACK_GUARD
和 THREAD_COPY_POINTER_GUARD
两个宏,这两个宏的展开如下:
((pd)->header.stack_guard = ({
__typeof(({
struct pthread *__self;
asm("mov %%fs:%c1,%0":"=r"(__self):"i"(((size_t) (&(((struct pthread *) 0)->header.self)))));
__self;
})->header.stack_guard) __value;
_Static_assert(sizeof(__value) == 1 || sizeof(__value) == 4 || sizeof(__value) == 8, "size of per-thread data");
if (sizeof(__value) == 1)asm volatile("movb %%fs:%P2,%b0":"=q"(__value):"0"(0), "i"(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); else if (sizeof(__value) == 4)asm volatile("movl %%fs:%P1,%0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); else { asm volatile("movq %%fs:%P1,%q0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.stack_guard))))); }
__value;
}))
((pd)->header.pointer_guard = ({
__typeof(({
struct pthread *__self;
asm("mov %%fs:%c1,%0":"=r"(__self):"i"(((size_t) (&(((struct pthread *) 0)->header.self)))));
__self;
})->header.pointer_guard) __value;
_Static_assert(sizeof(__value) == 1 || sizeof(__value) == 4 || sizeof(__value) == 8, "size of per-thread data");
if (sizeof(__value) == 1)asm volatile("movb %%fs:%P2,%b0":"=q"(__value):"0"(0), "i"(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); else if (sizeof(__value) == 4)asm volatile("movl %%fs:%P1,%0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); else { asm volatile("movq %%fs:%P1,%q0":"=r"(__value):"i"(((size_t) (&(((struct pthread *) 0)->header.pointer_guard))))); }
__value;
}))
不难看出这两个宏把当前线程(当前 fs 寄存器还没有指向新线程的 TCB)的 TLS 中的 stack_guard
和 pointer_guard
都复制到子线程的 TLS 的对应位置上。因此可以确定线程的 stack_guard
和 pointer_guard
与主线程相同。
最后需要确定是 fs 寄存器何时被修改,因为 fs 寄存器不能再用户态修改,因此一定是一个系统调用完成了对 fs 寄存器的修改。
通过调试发现,pthread_create->create_thread->clone
中的 clone
系统调用完成了对 fs 寄存器的修改。
子进程调试
gdb默认情况下,父进程 fork
一个子进程,gdb 只会继续调试父进程而不会管子进程的运行(pwndbg 插件设置相反)。
相关设置
- 跟踪子进程进行调试,可以使用
set follow-fork-mode mode
来设置fork
跟随模式。show follow-fork-mode
:进入 gdb 以后,我们可以使用show follow-fork-mode
来查看目前的跟踪模式。set follow-fork-mode parent
:gdb 只跟踪父进程,不跟踪子进程,这是默认的模式。set follow-fork-mode child
:gdb 在子进程产生以后只跟踪子进程,放弃对父进程的跟踪。
- 想同时调试父进程和子进程,以上的方法就不能满足了。Linux 提供了
set detach-on-fork mode
命令来供我们使用。show detach-on-fork
:show detach-on-fork
显示了目前是的detach-on-fork
模式。set detach-on-fork on
:只调试父进程或子进程的其中一个(根据follow-fork-mode
来决定),这是默认的模式。set detach-on-fork off
:父子进程都在 gdb 的控制之下,其中一个进程正常调试(根据follow-fork-mode
来决定),另一个进程会被设置为暂停状态。
调试进程切换
使用 gdb 调试多进程时,如果想要在进程间进行切换,那么就需要
- 在
fork
调用前设置:set detach-on-fork off
。 - 使用
info inferiors
来查看进程信息,得到的信息可以看到最前面有一个进程编号,使用inferior num
来进行进程切换。
常见保护
checksec 可以查看程序开启了哪些保护。
➜ ~ checksec /bin/ls
[*] '/bin/ls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
Canary
canary 是一种防止缓冲区溢出攻击的保护机制。它的基本思想是在程序的堆栈中插入一个随机生成的数值,用于检测缓冲区溢出攻击。
.text:0000000000001189 endbr64
.text:000000000000118D push rbp
.text:000000000000118E mov rbp, rsp
.text:0000000000001191 sub rsp, 30h
.text:0000000000001195 mov rax, fs:28h
.text:000000000000119E mov [rbp-8], rax
...
.text:00000000000011CE mov rdx, [rbp-8]
.text:00000000000011D2 xor rdx, fs:28h
.text:00000000000011DB jz short locret_11E2
.text:00000000000011DB
.text:00000000000011DD call ___stack_chk_fail
.text:00000000000011DD
.text:00000000000011E2 ; ---------------------------------------------------------------------------
.text:00000000000011E2
.text:00000000000011E2 locret_11E2: ; CODE XREF: f+52↑j
.text:00000000000011E2 leave
.text:00000000000011E3 retn
canary 的初始值存储在 tls 中,也就是前面提到的 stack_guard
。
在编译 c 程序时使用 -fno-stack-protector
参数可以关闭 canary 保护(注意高版本的 gcc 的 canary 保护关不掉)。
NX
NX 即 No-eXecute(不可执行),NX 的基本原理是将数据所在内存页标识为不可执行,也就是同一内存可写与可执行不共存。
gcc 编译器默认开启了 NX 选项,如果需要关闭 NX 选项,可以给 gcc 编译器添加 -zexecstack
参数。
PIE
PIE 主要随机了代码段(.text
),初始化数据段(.data
)和未初始化数据段(.bss
)的地址。另外 PIE 是否开启还会影响堆的基址。
- 开启 PIE:
- 关闭 PIE:
在编译 c 程序时使用 -no-pie
参数可以关闭 PIE 保护。
ASLR
ASLR 是系统级别的地址随机。通过修改 /proc/sys/kernel/randomize_va_space
的值可以控制 ASLR 的级别:
- 0:关闭 ASLR
- 1:栈基址,共享库,mmap 基址随机
- 2:在 1 的基础上增加堆基址的随机
RELRO
- 当 RELRO 保护为 NO RELRO 的时候,
init.array
、fini.array
、got.plt
均可读可写。 - 为 PARTIAL RELRO 的时候,
init.array
、fini.array
根据实际调试结果判断是否可写,got.plt
可读可写。 - 为 FULL RELRO 时,
init.array
、fini.array
、got.plt
均可读不可写。 -Wl,-z,norelro
编译参数可以关闭 RELRO ,使 RELRO 状态变为 NO RELRO 。-Wl,-z,lazy
会开启延迟绑定,使 RELRO 状态变为 Partial RELRO 。
调用约定
栈结构
注意 canary 不一定与 ebp 相邻,因为有些函数会先将一些寄存器保存到栈中。canary 实际位置以调试为准。
函数调用过程
32位为例:
push args
call func
{
push next_eip
jmp func
push ebp
mov ebp,esp
⋮
leave
{
mov esp,ebp
pop ebp
ret (pop eip)
\begin{align*} & \text{push args}\\ & \text{call func}\left\{\begin{matrix} \text{push next\_eip}\\ \text{jmp func} \end{matrix}\right.\\ & \text{push ebp}\\ & \text{mov ebp,esp}\\ & \vdots \\ & \text{leave}\left\{\begin{matrix} \text{mov esp,ebp}\\ \text{pop ebp} \end{matrix}\right.\\ &\text{ret}\ \text{(pop eip)} \end{align*}
push argscall func{push next_eipjmp funcpush ebpmov ebp,esp⋮leave{mov esp,ebppop ebpret (pop eip)
函数参数传递
通常 linux 都是外平栈的。
32位程序
- 普通函数传参:参数基本都压在栈上(有寄存器传参的情况,可查阅相关资料)。
int 0x80
传参:eax对应系统调用号,ebx、ecx、edx、esi、edi、ebp 分别对应前六个参数多余的参数压在栈上。
64位程序:
- 普通函数传参:先使用 rdi、rsi、rdx、rcx、r8、r9 寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上。
syscall
传参:rax 对应系统调用号,传参规则与普通函数传参一致。
系统调用号
32 位
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H 1
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
#define __NR_mknod 14
#define __NR_chmod 15
#define __NR_lchown 16
#define __NR_break 17
#define __NR_oldstat 18
#define __NR_lseek 19
#define __NR_getpid 20
#define __NR_mount 21
#define __NR_umount 22
#define __NR_setuid 23
#define __NR_getuid 24
#define __NR_stime 25
#define __NR_ptrace 26
#define __NR_alarm 27
#define __NR_oldfstat 28
#define __NR_pause 29
#define __NR_utime 30
#define __NR_stty 31
#define __NR_gtty 32
#define __NR_access 33
#define __NR_nice 34
#define __NR_ftime 35
#define __NR_sync 36
#define __NR_kill 37
#define __NR_rename 38
#define __NR_mkdir 39
#define __NR_rmdir 40
#define __NR_dup 41
#define __NR_pipe 42
#define __NR_times 43
#define __NR_prof 44
#define __NR_brk 45
#define __NR_setgid 46
#define __NR_getgid 47
#define __NR_signal 48
#define __NR_geteuid 49
#define __NR_getegid 50
#define __NR_acct 51
#define __NR_umount2 52
#define __NR_lock 53
#define __NR_ioctl 54
#define __NR_fcntl 55
#define __NR_mpx 56
#define __NR_setpgid 57
#define __NR_ulimit 58
#define __NR_oldolduname 59
#define __NR_umask 60
#define __NR_chroot 61
#define __NR_ustat 62
#define __NR_dup2 63
#define __NR_getppid 64
#define __NR_getpgrp 65
#define __NR_setsid 66
#define __NR_sigaction 67
#define __NR_sgetmask 68
#define __NR_ssetmask 69
#define __NR_setreuid 70
#define __NR_setregid 71
#define __NR_sigsuspend 72
#define __NR_sigpending 73
#define __NR_sethostname 74
#define __NR_setrlimit 75
#define __NR_getrlimit 76
#define __NR_getrusage 77
#define __NR_gettimeofday 78
#define __NR_settimeofday 79
#define __NR_getgroups 80
#define __NR_setgroups 81
#define __NR_select 82
#define __NR_symlink 83
#define __NR_oldlstat 84
#define __NR_readlink 85
#define __NR_uselib 86
#define __NR_swapon 87
#define __NR_reboot 88
#define __NR_readdir 89
#define __NR_mmap 90
#define __NR_munmap 91
#define __NR_truncate 92
#define __NR_ftruncate 93
#define __NR_fchmod 94
#define __NR_fchown 95
#define __NR_getpriority 96
#define __NR_setpriority 97
#define __NR_profil 98
#define __NR_statfs 99
#define __NR_fstatfs 100
#define __NR_ioperm 101
#define __NR_socketcall 102
#define __NR_syslog 103
#define __NR_setitimer 104
#define __NR_getitimer 105
#define __NR_stat 106
#define __NR_lstat 107
#define __NR_fstat 108
#define __NR_olduname 109
#define __NR_iopl 110
#define __NR_vhangup 111
#define __NR_idle 112
#define __NR_vm86old 113
#define __NR_wait4 114
#define __NR_swapoff 115
#define __NR_sysinfo 116
#define __NR_ipc 117
#define __NR_fsync 118
#define __NR_sigreturn 119
#define __NR_clone 120
#define __NR_setdomainname 121
#define __NR_uname 122
#define __NR_modify_ldt 123
#define __NR_adjtimex 124
#define __NR_mprotect 125
#define __NR_sigprocmask 126
#define __NR_create_module 127
#define __NR_init_module 128
#define __NR_delete_module 129
#define __NR_get_kernel_syms 130
#define __NR_quotactl 131
#define __NR_getpgid 132
#define __NR_fchdir 133
#define __NR_bdflush 134
#define __NR_sysfs 135
#define __NR_personality 136
#define __NR_afs_syscall 137
#define __NR_setfsuid 138
#define __NR_setfsgid 139
#define __NR__llseek 140
#define __NR_getdents 141
#define __NR__newselect 142
#define __NR_flock 143
#define __NR_msync 144
#define __NR_readv 145
#define __NR_writev 146
#define __NR_getsid 147
#define __NR_fdatasync 148
#define __NR__sysctl 149
#define __NR_mlock 150
#define __NR_munlock 151
#define __NR_mlockall 152
#define __NR_munlockall 153
#define __NR_sched_setparam 154
#define __NR_sched_getparam 155
#define __NR_sched_setscheduler 156
#define __NR_sched_getscheduler 157
#define __NR_sched_yield 158
#define __NR_sched_get_priority_max 159
#define __NR_sched_get_priority_min 160
#define __NR_sched_rr_get_interval 161
#define __NR_nanosleep 162
#define __NR_mremap 163
#define __NR_setresuid 164
#define __NR_getresuid 165
#define __NR_vm86 166
#define __NR_query_module 167
#define __NR_poll 168
#define __NR_nfsservctl 169
#define __NR_setresgid 170
#define __NR_getresgid 171
#define __NR_prctl 172
#define __NR_rt_sigreturn 173
#define __NR_rt_sigaction 174
#define __NR_rt_sigprocmask 175
#define __NR_rt_sigpending 176
#define __NR_rt_sigtimedwait 177
#define __NR_rt_sigqueueinfo 178
#define __NR_rt_sigsuspend 179
#define __NR_pread64 180
#define __NR_pwrite64 181
#define __NR_chown 182
#define __NR_getcwd 183
#define __NR_capget 184
#define __NR_capset 185
#define __NR_sigaltstack 186
#define __NR_sendfile 187
#define __NR_getpmsg 188
#define __NR_putpmsg 189
#define __NR_vfork 190
#define __NR_ugetrlimit 191
#define __NR_mmap2 192
#define __NR_truncate64 193
#define __NR_ftruncate64 194
#define __NR_stat64 195
#define __NR_lstat64 196
#define __NR_fstat64 197
#define __NR_lchown32 198
#define __NR_getuid32 199
#define __NR_getgid32 200
#define __NR_geteuid32 201
#define __NR_getegid32 202
#define __NR_setreuid32 203
#define __NR_setregid32 204
#define __NR_getgroups32 205
#define __NR_setgroups32 206
#define __NR_fchown32 207
#define __NR_setresuid32 208
#define __NR_getresuid32 209
#define __NR_setresgid32 210
#define __NR_getresgid32 211
#define __NR_chown32 212
#define __NR_setuid32 213
#define __NR_setgid32 214
#define __NR_setfsuid32 215
#define __NR_setfsgid32 216
#define __NR_pivot_root 217
#define __NR_mincore 218
#define __NR_madvise 219
#define __NR_getdents64 220
#define __NR_fcntl64 221
#define __NR_gettid 224
#define __NR_readahead 225
#define __NR_setxattr 226
#define __NR_lsetxattr 227
#define __NR_fsetxattr 228
#define __NR_getxattr 229
#define __NR_lgetxattr 230
#define __NR_fgetxattr 231
#define __NR_listxattr 232
#define __NR_llistxattr 233
#define __NR_flistxattr 234
#define __NR_removexattr 235
#define __NR_lremovexattr 236
#define __NR_fremovexattr 237
#define __NR_tkill 238
#define __NR_sendfile64 239
#define __NR_futex 240
#define __NR_sched_setaffinity 241
#define __NR_sched_getaffinity 242
#define __NR_set_thread_area 243
#define __NR_get_thread_area 244
#define __NR_io_setup 245
#define __NR_io_destroy 246
#define __NR_io_getevents 247
#define __NR_io_submit 248
#define __NR_io_cancel 249
#define __NR_fadvise64 250
#define __NR_exit_group 252
#define __NR_lookup_dcookie 253
#define __NR_epoll_create 254
#define __NR_epoll_ctl 255
#define __NR_epoll_wait 256
#define __NR_remap_file_pages 257
#define __NR_set_tid_address 258
#define __NR_timer_create 259
#define __NR_timer_settime 260
#define __NR_timer_gettime 261
#define __NR_timer_getoverrun 262
#define __NR_timer_delete 263
#define __NR_clock_settime 264
#define __NR_clock_gettime 265
#define __NR_clock_getres 266
#define __NR_clock_nanosleep 267
#define __NR_statfs64 268
#define __NR_fstatfs64 269
#define __NR_tgkill 270
#define __NR_utimes 271
#define __NR_fadvise64_64 272
#define __NR_vserver 273
#define __NR_mbind 274
#define __NR_get_mempolicy 275
#define __NR_set_mempolicy 276
#define __NR_mq_open 277
#define __NR_mq_unlink 278
#define __NR_mq_timedsend 279
#define __NR_mq_timedreceive 280
#define __NR_mq_notify 281
#define __NR_mq_getsetattr 282
#define __NR_kexec_load 283
#define __NR_waitid 284
#define __NR_add_key 286
#define __NR_request_key 287
#define __NR_keyctl 288
#define __NR_ioprio_set 289
#define __NR_ioprio_get 290
#define __NR_inotify_init 291
#define __NR_inotify_add_watch 292
#define __NR_inotify_rm_watch 293
#define __NR_migrate_pages 294
#define __NR_openat 295
#define __NR_mkdirat 296
#define __NR_mknodat 297
#define __NR_fchownat 298
#define __NR_futimesat 299
#define __NR_fstatat64 300
#define __NR_unlinkat 301
#define __NR_renameat 302
#define __NR_linkat 303
#define __NR_symlinkat 304
#define __NR_readlinkat 305
#define __NR_fchmodat 306
#define __NR_faccessat 307
#define __NR_pselect6 308
#define __NR_ppoll 309
#define __NR_unshare 310
#define __NR_set_robust_list 311
#define __NR_get_robust_list 312
#define __NR_splice 313
#define __NR_sync_file_range 314
#define __NR_tee 315
#define __NR_vmsplice 316
#define __NR_move_pages 317
#define __NR_getcpu 318
#define __NR_epoll_pwait 319
#define __NR_utimensat 320
#define __NR_signalfd 321
#define __NR_timerfd_create 322
#define __NR_eventfd 323
#define __NR_fallocate 324
#define __NR_timerfd_settime 325
#define __NR_timerfd_gettime 326
#define __NR_signalfd4 327
#define __NR_eventfd2 328
#define __NR_epoll_create1 329
#define __NR_dup3 330
#define __NR_pipe2 331
#define __NR_inotify_init1 332
#define __NR_preadv 333
#define __NR_pwritev 334
#define __NR_rt_tgsigqueueinfo 335
#define __NR_perf_event_open 336
#define __NR_recvmmsg 337
#define __NR_fanotify_init 338
#define __NR_fanotify_mark 339
#define __NR_prlimit64 340
#define __NR_name_to_handle_at 341
#define __NR_open_by_handle_at 342
#define __NR_clock_adjtime 343
#define __NR_syncfs 344
#define __NR_sendmmsg 345
#define __NR_setns 346
#define __NR_process_vm_readv 347
#define __NR_process_vm_writev 348
#define __NR_kcmp 349
#define __NR_finit_module 350
#define __NR_sched_setattr 351
#define __NR_sched_getattr 352
#define __NR_renameat2 353
#define __NR_seccomp 354
#define __NR_getrandom 355
#define __NR_memfd_create 356
#define __NR_bpf 357
#define __NR_execveat 358
#define __NR_socket 359
#define __NR_socketpair 360
#define __NR_bind 361
#define __NR_connect 362
#define __NR_listen 363
#define __NR_accept4 364
#define __NR_getsockopt 365
#define __NR_setsockopt 366
#define __NR_getsockname 367
#define __NR_getpeername 368
#define __NR_sendto 369
#define __NR_sendmsg 370
#define __NR_recvfrom 371
#define __NR_recvmsg 372
#define __NR_shutdown 373
#define __NR_userfaultfd 374
#define __NR_membarrier 375
#define __NR_mlock2 376
#define __NR_copy_file_range 377
#define __NR_preadv2 378
#define __NR_pwritev2 379
#endif /* _ASM_X86_UNISTD_32_H */
64 位
#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
#define __NR_mmap 9
#define __NR_mprotect 10
#define __NR_munmap 11
#define __NR_brk 12
#define __NR_rt_sigaction 13
#define __NR_rt_sigprocmask 14
#define __NR_rt_sigreturn 15
#define __NR_ioctl 16
#define __NR_pread64 17
#define __NR_pwrite64 18
#define __NR_readv 19
#define __NR_writev 20
#define __NR_access 21
#define __NR_pipe 22
#define __NR_select 23
#define __NR_sched_yield 24
#define __NR_mremap 25
#define __NR_msync 26
#define __NR_mincore 27
#define __NR_madvise 28
#define __NR_shmget 29
#define __NR_shmat 30
#define __NR_shmctl 31
#define __NR_dup 32
#define __NR_dup2 33
#define __NR_pause 34
#define __NR_nanosleep 35
#define __NR_getitimer 36
#define __NR_alarm 37
#define __NR_setitimer 38
#define __NR_getpid 39
#define __NR_sendfile 40
#define __NR_socket 41
#define __NR_connect 42
#define __NR_accept 43
#define __NR_sendto 44
#define __NR_recvfrom 45
#define __NR_sendmsg 46
#define __NR_recvmsg 47
#define __NR_shutdown 48
#define __NR_bind 49
#define __NR_listen 50
#define __NR_getsockname 51
#define __NR_getpeername 52
#define __NR_socketpair 53
#define __NR_setsockopt 54
#define __NR_getsockopt 55
#define __NR_clone 56
#define __NR_fork 57
#define __NR_vfork 58
#define __NR_execve 59
#define __NR_exit 60
#define __NR_wait4 61
#define __NR_kill 62
#define __NR_uname 63
#define __NR_semget 64
#define __NR_semop 65
#define __NR_semctl 66
#define __NR_shmdt 67
#define __NR_msgget 68
#define __NR_msgsnd 69
#define __NR_msgrcv 70
#define __NR_msgctl 71
#define __NR_fcntl 72
#define __NR_flock 73
#define __NR_fsync 74
#define __NR_fdatasync 75
#define __NR_truncate 76
#define __NR_ftruncate 77
#define __NR_getdents 78
#define __NR_getcwd 79
#define __NR_chdir 80
#define __NR_fchdir 81
#define __NR_rename 82
#define __NR_mkdir 83
#define __NR_rmdir 84
#define __NR_creat 85
#define __NR_link 86
#define __NR_unlink 87
#define __NR_symlink 88
#define __NR_readlink 89
#define __NR_chmod 90
#define __NR_fchmod 91
#define __NR_chown 92
#define __NR_fchown 93
#define __NR_lchown 94
#define __NR_umask 95
#define __NR_gettimeofday 96
#define __NR_getrlimit 97
#define __NR_getrusage 98
#define __NR_sysinfo 99
#define __NR_times 100
#define __NR_ptrace 101
#define __NR_getuid 102
#define __NR_syslog 103
#define __NR_getgid 104
#define __NR_setuid 105
#define __NR_setgid 106
#define __NR_geteuid 107
#define __NR_getegid 108
#define __NR_setpgid 109
#define __NR_getppid 110
#define __NR_getpgrp 111
#define __NR_setsid 112
#define __NR_setreuid 113
#define __NR_setregid 114
#define __NR_getgroups 115
#define __NR_setgroups 116
#define __NR_setresuid 117
#define __NR_getresuid 118
#define __NR_setresgid 119
#define __NR_getresgid 120
#define __NR_getpgid 121
#define __NR_setfsuid 122
#define __NR_setfsgid 123
#define __NR_getsid 124
#define __NR_capget 125
#define __NR_capset 126
#define __NR_rt_sigpending 127
#define __NR_rt_sigtimedwait 128
#define __NR_rt_sigqueueinfo 129
#define __NR_rt_sigsuspend 130
#define __NR_sigaltstack 131
#define __NR_utime 132
#define __NR_mknod 133
#define __NR_uselib 134
#define __NR_personality 135
#define __NR_ustat 136
#define __NR_statfs 137
#define __NR_fstatfs 138
#define __NR_sysfs 139
#define __NR_getpriority 140
#define __NR_setpriority 141
#define __NR_sched_setparam 142
#define __NR_sched_getparam 143
#define __NR_sched_setscheduler 144
#define __NR_sched_getscheduler 145
#define __NR_sched_get_priority_max 146
#define __NR_sched_get_priority_min 147
#define __NR_sched_rr_get_interval 148
#define __NR_mlock 149
#define __NR_munlock 150
#define __NR_mlockall 151
#define __NR_munlockall 152
#define __NR_vhangup 153
#define __NR_modify_ldt 154
#define __NR_pivot_root 155
#define __NR__sysctl 156
#define __NR_prctl 157
#define __NR_arch_prctl 158
#define __NR_adjtimex 159
#define __NR_setrlimit 160
#define __NR_chroot 161
#define __NR_sync 162
#define __NR_acct 163
#define __NR_settimeofday 164
#define __NR_mount 165
#define __NR_umount2 166
#define __NR_swapon 167
#define __NR_swapoff 168
#define __NR_reboot 169
#define __NR_sethostname 170
#define __NR_setdomainname 171
#define __NR_iopl 172
#define __NR_ioperm 173
#define __NR_create_module 174
#define __NR_init_module 175
#define __NR_delete_module 176
#define __NR_get_kernel_syms 177
#define __NR_query_module 178
#define __NR_quotactl 179
#define __NR_nfsservctl 180
#define __NR_getpmsg 181
#define __NR_putpmsg 182
#define __NR_afs_syscall 183
#define __NR_tuxcall 184
#define __NR_security 185
#define __NR_gettid 186
#define __NR_readahead 187
#define __NR_setxattr 188
#define __NR_lsetxattr 189
#define __NR_fsetxattr 190
#define __NR_getxattr 191
#define __NR_lgetxattr 192
#define __NR_fgetxattr 193
#define __NR_listxattr 194
#define __NR_llistxattr 195
#define __NR_flistxattr 196
#define __NR_removexattr 197
#define __NR_lremovexattr 198
#define __NR_fremovexattr 199
#define __NR_tkill 200
#define __NR_time 201
#define __NR_futex 202
#define __NR_sched_setaffinity 203
#define __NR_sched_getaffinity 204
#define __NR_set_thread_area 205
#define __NR_io_setup 206
#define __NR_io_destroy 207
#define __NR_io_getevents 208
#define __NR_io_submit 209
#define __NR_io_cancel 210
#define __NR_get_thread_area 211
#define __NR_lookup_dcookie 212
#define __NR_epoll_create 213
#define __NR_epoll_ctl_old 214
#define __NR_epoll_wait_old 215
#define __NR_remap_file_pages 216
#define __NR_getdents64 217
#define __NR_set_tid_address 218
#define __NR_restart_syscall 219
#define __NR_semtimedop 220
#define __NR_fadvise64 221
#define __NR_timer_create 222
#define __NR_timer_settime 223
#define __NR_timer_gettime 224
#define __NR_timer_getoverrun 225
#define __NR_timer_delete 226
#define __NR_clock_settime 227
#define __NR_clock_gettime 228
#define __NR_clock_getres 229
#define __NR_clock_nanosleep 230
#define __NR_exit_group 231
#define __NR_epoll_wait 232
#define __NR_epoll_ctl 233
#define __NR_tgkill 234
#define __NR_utimes 235
#define __NR_vserver 236
#define __NR_mbind 237
#define __NR_set_mempolicy 238
#define __NR_get_mempolicy 239
#define __NR_mq_open 240
#define __NR_mq_unlink 241
#define __NR_mq_timedsend 242
#define __NR_mq_timedreceive 243
#define __NR_mq_notify 244
#define __NR_mq_getsetattr 245
#define __NR_kexec_load 246
#define __NR_waitid 247
#define __NR_add_key 248
#define __NR_request_key 249
#define __NR_keyctl 250
#define __NR_ioprio_set 251
#define __NR_ioprio_get 252
#define __NR_inotify_init 253
#define __NR_inotify_add_watch 254
#define __NR_inotify_rm_watch 255
#define __NR_migrate_pages 256
#define __NR_openat 257
#define __NR_mkdirat 258
#define __NR_mknodat 259
#define __NR_fchownat 260
#define __NR_futimesat 261
#define __NR_newfstatat 262
#define __NR_unlinkat 263
#define __NR_renameat 264
#define __NR_linkat 265
#define __NR_symlinkat 266
#define __NR_readlinkat 267
#define __NR_fchmodat 268
#define __NR_faccessat 269
#define __NR_pselect6 270
#define __NR_ppoll 271
#define __NR_unshare 272
#define __NR_set_robust_list 273
#define __NR_get_robust_list 274
#define __NR_splice 275
#define __NR_tee 276
#define __NR_sync_file_range 277
#define __NR_vmsplice 278
#define __NR_move_pages 279
#define __NR_utimensat 280
#define __NR_epoll_pwait 281
#define __NR_signalfd 282
#define __NR_timerfd_create 283
#define __NR_eventfd 284
#define __NR_fallocate 285
#define __NR_timerfd_settime 286
#define __NR_timerfd_gettime 287
#define __NR_accept4 288
#define __NR_signalfd4 289
#define __NR_eventfd2 290
#define __NR_epoll_create1 291
#define __NR_dup3 292
#define __NR_pipe2 293
#define __NR_inotify_init1 294
#define __NR_preadv 295
#define __NR_pwritev 296
#define __NR_rt_tgsigqueueinfo 297
#define __NR_perf_event_open 298
#define __NR_recvmmsg 299
#define __NR_fanotify_init 300
#define __NR_fanotify_mark 301
#define __NR_prlimit64 302
#define __NR_name_to_handle_at 303
#define __NR_open_by_handle_at 304
#define __NR_clock_adjtime 305
#define __NR_syncfs 306
#define __NR_sendmmsg 307
#define __NR_setns 308
#define __NR_getcpu 309
#define __NR_process_vm_readv 310
#define __NR_process_vm_writev 311
#define __NR_kcmp 312
#define __NR_finit_module 313
#define __NR_sched_setattr 314
#define __NR_sched_getattr 315
#define __NR_renameat2 316
#define __NR_seccomp 317
#define __NR_getrandom 318
#define __NR_memfd_create 319
#define __NR_kexec_file_load 320
#define __NR_bpf 321
#define __NR_execveat 322
#define __NR_userfaultfd 323
#define __NR_membarrier 324
#define __NR_mlock2 325
#define __NR_copy_file_range 326
#define __NR_preadv2 327
#define __NR_pwritev2 328
#endif /* _ASM_X86_UNISTD_64_H */