Linux的系统和函数调用
32 位 Linux 程序在运行过程中遵循栈平衡的原则。ESP 和 EBP 作为栈指针和帧指针寄存器,EAX 作为返回值。
而 64 位 Linux 程序使用 fast call 的调用方式进行传参。同样源码编译的 64 位版本与 32 位的主要区别是,函数的前6个参数会依次使用 RDI、RSI、RDX、R8、R9 寄存器进行传递,如果还有多余的参数,那么与 32 位一样使用栈进行传递。
PWN 过程中也经常需要直接调用操作系统提供的 API 函数。
在 32 位 Linux 操作系统中,调用系统调用需要执行 int 0x80 软中断指令。此时,eax 中保存系统调用号,系统调用的参数依次保存在 EBX、ECX、EDX、ESI、EDI、EBP 寄存器中。调用的返回结果保存在 EAX 中。其实,系统调用可以看成一种特殊的函数调用,只是使用 int 0x80 指令代替 call 指令。call 指令中的函数地址变成了存放在 EAX 中的系统调用号,而参数改成使用寄存器进行传递。相较于 32 位系统,64 位 Linux 系统调用指令变成了 syscall,传递参数的寄存器变成了 RDI、RSI、RDX、R10、R8、R9,并且系统调用对应的调用号发生了变化。
ELF 文件结构
之前在C与链接中讲过。
Linux 下的可执行文件格式为 ELF,类似 windows 的 PE 格式。
ELF 头必须在文件开头,表示这是个 ELF 文件及其基本信息。ELF 头包括 ELF 的 magic code、程序运行的计算机架构、程序入口等内容,可以通过“readlf -h”命令读取其内容,一半用于寻找一些程序的入口。
ELF 文件由多个节组成,其中存放各种数据。描述节的各种信息的数据统一存放在节头表中。ELF 中的节用来存放各种各样不同的数据,主要包括:
- .text:已编译程序的机器代码。
- .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
- .data:存放程序可修改的数据,如已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
- .bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
- .plt 节和 .got节:程序调用动态链接库(SO文件)中函数时,需要这两个节配合,以获取被调用函数的地址。
Linux保护机制
之前提到在对抗缓冲区溢出时,编译器和操作系统有很多保护机制。这次是专门了解这些保护机制。
checksec
checksec是一个bash脚本,用来检测可执行文件的属性,可执行文件属性包括:PIE, RELRO, PaX, Canaries, ASLR, Fortify Sourc
拿到pwn题的第一步大都是运行下,再拿checksec看开启了哪些保护机制。
- 使用(peda内置)
1 | checksec filename |
Arch
程序架构信息。判断是拖进64位IDA还是32位?exp编写时p64还是p32函数?
RELRO
Relocation Read-Only (RELRO) 此项技术主要针对 GOT 改写的攻击方式。它分为两种,Partial RELRO 和 Full RELRO。
部分RELRO 易受到攻击,例如攻击者可以atoi.got为system.plt,进而输入/bin/sh\x00获得shell。
完全RELRO 使整个 GOT 只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。
1 | gcc -o hello test.c // 默认情况下,是Partial RELRO |
Stack-canary
栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入类似cookie的信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。
1 | gcc -fno-stack-protector -o hello test.c //禁用栈保护 |
NX
NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,如此一来, 当攻击者在堆栈上部署自己的 shellcode 并触发时, 只会直接造成程序的崩溃,但是可以利用rop这种方法绕过。
1 | gcc -o hello test.c // 默认情况下,开启NX保护 |
PIE
PIE(Position-Independent Executable, 位置无关可执行文件)技术与 ASLR 技术类似,ASLR 将程序运行时的堆栈以及共享库的加载地址随机化,而 PIE 技术则在编译时将程序编译为位置无关, 即程序运行时各个段(如代码段等)加载的虚拟地址也是在装载时才确定。这就意味着,在 PIE 和 ASLR 同时开启的情况下,攻击者将对程序的内存布局一无所知,传统的改写。
GOT 表项的方法也难以进行,因为攻击者不能获得程序的.got 段的虚地址。
若开启一般需在攻击时泄露地址信息。
1 | gcc -o hello test.c // 默认情况下,不开启PIE |
GOT 和 PLT
ELF 文件中通常存在 .GOT.PLT 和 .PLT 这两个特殊的节,ELF 编译时无法知道 libc 等动态链接库的加载地址。如果一个程序想调用动态链接库中的函数,就必须使用 .GOT.PLT 和 .PLT 配合完成调用。
.PLT 表还是一段代码,作用是从内存中取出一个地址然后跳转。
.GOT.PLT 表其实是一个函数指针数组,数组中保存着 ELF 中所有用到的外部函数的地址。 .GOT.PLT 表的初始化工作则由操作系统来完成。
由于 Linux 非常特殊的 Lazy Binding 机制。在没有开启 Full Rello 的 ELF 中, .GOT.PLT 表的初始化是在第一次调用该函数的过程中完成的。也就是说,某个函数必须被调用过, .GOT.PLT 表中才会存放函数的真实地址。
.GOT.PLT 和 .PLT 对于 PWN 的作用:
.PLT 可以直接调用某个外部函数,这在后续介绍的栈溢出中会有很大的帮助。
由于 .GOT.PLT 中通常会存放 libc 中函数的地址,在漏洞利用中可以通过读取 .GOT.PLT 来获得 libc 的地址,或者通过写 .GOT.PLT 来控制程序的执行流。通过 .GOT.PLT 进行漏洞利用在 CTF 中非常常见。