二进制基础

源代码的编译和链接

  • 编译:由C语言代码生成汇编语言
  • 汇编:由汇编代码生成机器码
  • 链接:将多个机器码的目标链接成一个可执行文件

image-20230710075946173

可执行文件

什么是可执行文件

可执行文件(Executable file)是一种计算机文件,它包含了一组计算机指令和数据,可以直接在特定的操作系统中运行。可执行文件通常用于执行特定任务或应用程序。

Windows:PE

  • 可执行文件:.exe
  • 动态链接库:.dll
  • 静态链接库:.lib

Linux:ELF

  • 可执行文件:.out
  • 动态链接库:.so
  • 静态链接库:.a

image-20230710083914310

名称 内容
ELF头部(ELF Header) 位于文件的开头,包含了描述整个ELF文件的基本信息。其中包括魔数、ELF文件类型、架构类型、入口地址、段表和节表的偏移等。
程序头表(Program Header Table) 包含了描述程序运行时所需的各个段(Segment)的信息。例如,代码段、数据段、动态链接段等。每个段的大小、虚拟内存地址、文件偏移等信息都存储在程序头表中。
节头表(Section Header Table) 包含了描述各个节(Section)的信息。节是ELF文件中各个具有特定功能的区域,如代码节、数据节、符号表节等。节头表记录了每个节的名称、大小、偏移等信息。
节数据(Section Data) 即各个节的实际数据,例如代码、数据、符号表等。ELF文件中的程序和数据都存储在不同的节中。
符号表(Symbol Table) 包含了程序中定义和引用的符号(Symbol)信息,如变量、函数、全局变量等。符号表可以用于进行符号解析和动态链接。
动态节(Dynamic Section) 包含了动态链接所需的信息,如共享库依赖、动态链接器的名称等。

.got和.plt节

特性 .got .got.plt .plt .plt.got
主要用途 存储非函数全局符号地址(变量、静态数据) 存储函数地址,支持延迟绑定 包含一系列 跳转代码桩,每个桩对应一个动态库函数(如 puts@plt)。 某些情况下用于 非延迟绑定 的函数调用(如 _dl_runtime_resolve)。通常较小,用途有限,不是所有二进制文件都存在。
初始化时机 程序加载时由动态链接器填充所有地址 函数第一次被调用时由动态链接器填充真实地址 首次调用函数时,桩会跳转到 .got.plt 中的默认地址(通常是绑定逻辑),后续调用直接跳转到真实函数地址。
初始内容 加载时即填充为最终正确地址 初始指向对应 PLT 条目中的第二条指令(push n;
最终内容 始终指向正确地址 函数首次调用后指向真实函数地址 后续调用直接跳转到 .got.plt 中存储的真实地址
所属段 .got .got.plt .plt .plt.got
与 RELRO 的关系 Full RELRO 将其标记为只读 Partial RELRO 时它可写(延迟绑定需要修改它);Full RELRO 时它在加载时解析完所有函数地址后被标记为只读 不受 RELRO 影响(始终只读)。 不受 RELRO 影响(始终只读)。

除了以上主要部分外,ELF文件还包含其他一些部分,如字符串表、重定位表、调试信息等。这些部分提供了更多的元数据和调试信息,以支持程序的动态链接、调试和分析等功能。

段和节

Section称为节,是指在汇编源码中经由关键字section或segment修饰、逻辑划分的指令或数据区域,汇编器会将这两个关键字修饰的区域在目标文件中编译成节,也就是说”节”最初诞生于目标文件中。

Segment称为段,是链接器根据目标文件中属性相同的多个Section合并后的Section集合,这个集合称为Segment,也就是段,链接器把目标文件链接成可执行文件,因此段最终诞生于可执行文件中。我们平时所说的可执行程序内存空间中的代码段数据段就是指的Segment。

  • 代码段

    • .text节
    • .rodata节
    • .dynsym节
    • .dynstr节
    • .plt节
    • .rel.got节
    • .line节
    • ………..
  • 数据段

    • .data节
    • .dynamic节
    • .got节
    • .got.plt节
    • .bss节
    • ………..

段视图常用于执行时的内存区域权限划分,而节视图常用于链接编译或内存存储。

名称 内容
.text 此节区包含程序的可执行指令。
.data 这些节区包含初始化了的数据,将出现在程序的内存映像中。
.bss 它存储了程序中未初始化的全局变量和静态变量的数据。在编译和链接过程中,所有位于.bss节中的变量都被初始化为零或空值。
.dynsym 此节区包含了动态链接符号表。
.dynstr 此节区包含用于动态链接的字符串
.dynamic 此节区包含动态链接信息。
.got 此节用于存储全局变量的地址。
.got.plt 此节用于存储动态链接的全局偏移表和过程链接表相关的信息。
…………

程序的装载与运行

静态链接过程

  1. 加载可执行文件:操作系统负责加载可执行文件到内存中,并创建进程。加载过程中,操作系统会为程序分配内存空间,并将可执行文件的代码段、数据段等内容加载到相应的地址空间。
  2. 初始化:在加载完成后,操作系统会执行一些初始化操作,包括设置栈帧、初始化全局变量和静态变量等。
  3. 程序执行:操作系统会将控制权交给程序的入口点(通常是main()函数),程序开始执行。程序按照顺序执行代码,调用不同的函数和执行各种指令。
  4. 符号解析和重定位:在程序执行过程中,如果遇到对函数或变量的引用,需要进行符号解析和重定位。符号解析是通过查找符号表来确定引用的符号地址,而重定位是将该地址修正为正确的值。
  5. 调用函数和跳转:当程序执行到函数调用或跳转指令时,需要进行相关处理。对于函数调用,会保存当前函数的状态,包括返回地址和局部变量等;然后跳转到被调用函数的入口点,并传递参数。函数执行完毕后,返回到调用点继续执行。
  6. 数据访问:程序可能需要读取或修改数据,包括全局变量、静态变量和常量等。对于全局变量和静态变量,可以直接通过相应的地址进行访问。对于常量,通常会将其存储在只读的数据段中。
  7. 程序结束:当程序执行到结束点或遇到退出指令时,会执行相应的清理操作,并将控制权交还给操作系统。操作系统回收程序所占用的内存,并终止进程的执行。

动态链接执行过程

  1. 加载可执行文件:操作系统负责加载可执行文件到内存中,并创建进程。加载过程中,操作系统会为程序分配内存空间,并将可执行文件的代码段、数据段等内容加载到相应的地址空间。
  2. 初始化:在加载完成后,操作系统会执行一些初始化操作,包括设置栈帧、初始化全局变量和静态变量等。
  3. 程序执行:操作系统会将控制权交给程序的入口点(通常是main()函数),程序开始执行。程序按照顺序执行代码,调用不同的函数和执行各种指令。
  4. 符号解析和重定位:在程序执行过程中,如果遇到对函数或变量的引用,需要进行符号解析和重定位。与静态链接不同的是,动态链接过程中符号解析是在运行时进行的,通过动态链接器(如动态链接库)来完成。动态链接器会根据需要加载相应的共享库文件,并解析其中的符号表,确定引用的符号地址,然后进行重定位。
  5. 函数调用和跳转:当程序执行到函数调用或跳转指令时,会进行相关处理。对于动态链接库中的函数,程序会通过跳转到库文件中的入口点来执行相应的代码。参数传递和返回值处理等操作也会参考约定和调用规则进行。
  6. 数据访问:程序可能需要读取或修改数据,包括全局变量、静态变量和常量等。对于全局变量和静态变量,可以直接通过相应的地址进行访问。对于常量,通常会将其存储在只读的数据段中。
  7. 程序结束:当程序执行到结束点或遇到退出指令时,会执行相应的清理操作,并将控制权交还给操作系统。操作系统回收程序所占用的内存,并终止进程的执行。

动态链接流程(以 puts 为例)

  1. 首次调用 puts@plt

    1
    2
    3
    jmp [puts@got.plt]  ; 首次指向 .got.plt[2](_dl_runtime_resolve)
    push index ; 压入重定位表索引
    jmp .plt[0] ; 跳转到动态链接器
  2. 动态链接器解析

    • 根据索引找到 puts 的符号定义。
    • 将真实地址写入 puts@got.plt
  3. 后续调用 puts@plt

    1
    jmp [puts@got.plt]  ; 直接跳转到 libc 中的 puts

x86架构下的寄存器

通用寄存器:

  • EAX(累加器):用于存放函数返回值或一般性的计算结果。
  • EBX(基址寄存器):一般用作指针的基地址,也可以用于存放通用数据。
  • ECX(计数器):用于循环计数或其他计数操作。
  • EDX(数据寄存器):用于存放通用数据。
  • ESI(源变址寄存器):通常用作源操作数的指针。
  • EDI(目标变址寄存器):通常用作目标操作数的指针。
  • ESP(栈指针):指向栈顶元素。
  • EBP(基址指针):在函数调用时用于保存旧的栈帧。指向栈底元素。

段寄存器:

  • CS(代码段寄存器):存放当前执行代码所在的代码段。
  • DS(数据段寄存器):存放数据段的起始地址。
  • SS(堆栈段寄存器):存放当前的堆栈段的起始地址。
  • ES(附加段寄存器):作为附加的数据段寄存器。
  • FS 和 GS(附加段寄存器):作为附加的数据段寄存器,用于扩展地址空间。

标志寄存器:

  • EFLAGS:存放各种标志位的状态,包括进位标志、零标志、符号标志、溢出标志等。

指令指针寄存器:

  • EIP:存放下一条将要执行的指令的地址。

控制寄存器和调试寄存器:

  • CR0、CR2、CR3、CR4:控制寄存器,用于控制和管理处理器的特性和行为。
  • DR0、DR1、DR2、DR3、DR6、DR7:调试寄存器,用于调试和跟踪代码的执行。

栈在程序运行时起着至关重要的作用。函数调用栈在内存中连续,用来存储函数运行时的状态信息,包括函数参数与局部变量。调用函数时,函数的状态会被保存在栈中,函数结束即从栈中弹出。函数调用栈在内存中从高地址向低地址变化,所以栈顶对应的内存地址在进栈时变小,弹出时变大。

相关寄存器

名称 作用
EBP 存储当前函数状态的基地址,即栈底元素地址。
ESP 指向栈顶元素
EIP 存放下一条将要执行的指令的地址。

C语言函数调用栈

  • 在调用一个函数之前,首先会将调用函数的下一条指令压入栈中,即EIP先被压入栈中。

image-20230711174212534

  • 先创建一个栈帧,然后依次存放父函数的基地址(EBP)、函数的参数。

image-20230711181532515

  • 当有子函数时,再开辟一个栈帧,先将该子函数下一指令压入栈中,然后将该子函数的父函数的基地址压入栈中,并且使寄存器EBP指向ESP。

image-20230711181936368

  • 随后将子函数的变量压入栈中。

image-20230711182114513

  • 调用完成之后,子函数参数弹出。

image-20230711181936368

  • 弹出父函数的EBP并将其赋值给寄存器EBP

image-20230711182648410.png

  • 弹出返回地址(子函数EIP)并赋值给寄存器EIP

image-20230711182931625

ret leave call

ret

ret命令有两个操作:

  1. pop rip
  2. 跳转

leave

leave有三个操作:

  1. mov rsp,rbp:恢复栈指针
  2. pop rbp:恢复为调用者的 RBP

call

  1. push EIP
  2. 修改EIP
  3. 跳转

32位与64位函数调用的区别

64位程序在调用函数时,System V ABI 要求 RSP 在函数调用时必须 16 字节对齐(地址为16的倍数)(如调用 libc 函数时)。

所以在进行栈溢出利用时,覆盖rip返回地址时通常是ret的地址然后是后门地址,这是为了平衡堆栈,使RSP对齐。

参数的传递

x86

  • 使用栈来传递参数
  • 使用eax存放返回值

amd64

  • 前6个参数依次存放在rdi、rsi、rdx、rcx、r8、r9
  • 7个以后的参数存放在栈中
    [[栈溢出]]
    [[格式化字符串]]

输入输出重定向

  • >: 将命令的输出重定向到指定文件,如果文件不存在则创建,如果存在则覆盖文件内容;
  • >>: 将命令的输出追加到指定文件的末尾,如果文件不存在则创建。

标准输入/输出/错误

在Unix/Linux系统中,每个进程都有三个默认打开的文件描述符:标准输入(文件描述符0)、标准输出(文件描述符1)、标准错误(文件描述符2)。通过将标准输出重定向到标准输入,可以实现将一个进程的输出传递给另一个进程。

例如:1>&0

可执行文件安全机制

RELRO (Relocation Read-Only)

  • 目的: 保护 ELF 文件的关键数据结构(如全局偏移表 .got 和过程链接表 .plt)免受篡改。
  • 检查项:
    • No RELRO:有提供RELRO保护,意味着重定位节是可写的。
    • Partial RELRO:定位节的一部分是只读的,但还有其他部分是可写的。
      • 这是默认设置(通常由 GCC 的 -Wl,-z,relro 启用)。
      • 在程序加载后,将 .got 段(包含非惰性绑定的函数地址)标记为只读。
      • .plt.got(惰性绑定的 GOT)在首次使用前仍可写,存在被篡改的风险。
      • 无法防止 .dtors(旧机制)或 .fini_array 被覆盖(如果存在)。
    • Full RELRO:整个重定位节是只读的,防止了某些攻击,如GOT覆盖攻击。
      • (通常由 GCC 的 -Wl,-z,relro -Wl,-z,now 启用):
      • 在程序加载后,立即解析所有外部函数地址,并将整个 .got.plt 段(惰性和非惰性绑定)标记为只读
      • 也保护 .fini_array 等构造函数/析构函数数组。
      • 消除了篡改 GOT 进行攻击(如 GOT overwrite)的可能性。
      • 轻微增加程序启动时间(因为所有符号在启动时解析)。

Stack Canary

  • 目的: 检测并阻止经典的栈缓冲区溢出攻击。
  • 工作原理:
    • 在函数序言中,将一个随机值(“金丝雀”)压入栈上,紧邻返回地址之前。
    • 在函数返回(尾声)之前,检查该值是否被改变。
    • 如果金丝雀值被改变(通常是由于缓冲区溢出覆盖了它),程序会立即终止(通常抛出 *** stack smashing detected *** 错误)。
  • 检查项:
    • Canary found:程序启用了栈金丝雀保护(编译选项 -fstack-protector / -fstack-protector-strong / -fstack-protector-all)。
    • No canary found:未启用。栈溢出可能直接覆盖返回地址。
  • 绕过: 攻击者需要先泄露金丝雀值,然后在溢出时精确覆盖它使其保持不变。或者找到不依赖栈溢出的漏洞。

NX (Non-eXecutable memory / DEP)

  • 目的: 阻止攻击者在内存(如栈或堆)中执行自己注入的恶意代码(shellcode)。
  • 工作原理: 利用 CPU 的 NX/XD 位,将数据区域(栈、堆、全局数据)标记为不可执行。只有代码区域(.text 段)和显式标记为可执行的库才允许执行指令。
  • 检查项:
    • NX enabled:启用了 NX/DEP 保护(默认开启,编译选项 -z noexecstack)。
    • NX disabled:未启用。栈和/或堆等数据区域可执行,允许直接运行 shellcode。

PIE (Position-Independent Executable) / ASLR (Address Space Layout Randomization)

  • 目的: 通过随机化程序代码、数据、堆栈、堆和库在内存中的加载地址,增加攻击者预测目标地址(如函数地址、gadget 地址、数据地址)的难度。
  • 关系:
    • PIE:这是一个编译选项-fPIE -pie)。它告诉链接器生成一个位置无关的可执行文件。这种文件可以被加载到内存中的任意地址运行,就像共享库一样。PIE 是程序自身启用 ASLR 的前提条件。
    • ASLR:这是一个操作系统内核特性(通过 /proc/sys/kernel/randomize_va_space 控制)。它负责在程序加载时,随机化其基地址(包括 PIE 程序的基址和所有共享库的加载基址)以及栈、堆的起始地址。
  • 检查项 (对于程序本身):
    • PIE enabled:程序编译为 PIE。如果系统 ASLR 开启(通常为 1 或 2),则该程序的基地址会在每次运行时随机化。
    • No PIE:程序不是位置无关的。它有一个固定的加载基地址(通常是 0x4000000x8048000)。即使系统 ASLR 开启,该程序的 .text/.data 等段的地址也是固定的、可预测的。 栈和堆的地址可能仍然被 ASLR 随机化,库地址也一定被随机化。
  • 注意: checksec 通常只报告程序自身的 PIE 状态。系统级 ASLR 状态需要单独检查(cat /proc/sys/kernel/randomize_va_space)。

RPATH/RUNPATH

  • 目的: 指定程序在运行时搜索共享库(.so 文件)的额外目录路径。
  • 安全问题:
    • 不安全路径: 如果 RPATHRUNPATH 包含当前目录(.)、空目录或用户可写的目录,攻击者可以将恶意共享库放在这些目录下。程序在加载时可能会优先加载攻击者的恶意库而非系统标准库,导致任意代码执行(LD_PRELOAD 攻击的一种变体)。
    • RPATH vs RUNPATHRPATH 优先级很高(在 LD_LIBRARY_PATH 和系统默认路径之前搜索),风险更大。RUNPATH 优先级较低(在 LD_LIBRARY_PATH 之后,系统默认路径之前搜索),相对安全一些,但如果路径包含用户可写目录仍然危险。
  • 检查项:
    • No RPATH / No RUNPATH:没有设置额外的库搜索路径,依赖系统默认路径和 LD_LIBRARY_PATH,通常是安全的(除非 LD_LIBRARY_PATH 本身被篡改)。
    • RPATH set:存在 RPATH 设置。检查其值! 如果包含 .、空项或用户可写目录,则危险。
    • RUNPATH set:存在 RUNPATH 设置。同样需要检查其值! 虽然比 RPATH 稍好,但包含不安全路径时仍危险。

FORTIFY_SOURCE

  • 目的: 在编译时和运行时对一些不安全的字符串/内存操作函数(如 strcpy, strcat, sprintf, gets, memcpy)提供额外的缓冲区溢出检查。
  • 工作原理:
    • 编译时: 如果编译器能确定缓冲区大小(例如目标缓冲区是固定大小的数组),并且发现操作会导致溢出,会直接报错(编译失败)。
    • 运行时: 对于大小在编译时无法确定的情况(如目标缓冲区大小由参数传入),将脆弱的函数调用替换为加强版函数(如 __strcpy_chk)。这些函数在运行时检查目标缓冲区大小是否足够容纳源数据。如果检测到溢出,程序终止。
  • 检查项:
    • Enabled:启用了 FORTIFY_SOURCE(编译选项 -D_FORTIFY_SOURCE=1-D_FORTIFY_SOURCE=2,通常与优化 -O 一起使用)。=2=1 检查更严格。
    • Disabled:未启用。不安全的函数调用没有额外保护。

Clang CFI / SafeStack (特定于 Clang)

  • 控制流完整性 (CFI):
    • 目的: 防止攻击者篡改程序的间接控制流(如函数指针、虚函数表),确保间接跳转/调用只能到达预期的、有效的目标地址。
    • 检查项: Clang CFI found (可能)
  • 安全栈 (SafeStack):
    • 目的: 将栈分为“安全栈”(存放返回地址、函数指针等敏感数据)和“不安全栈”(存放普通缓冲区等易被溢出的数据)。即使不安全栈上的缓冲区溢出,也无法覆盖安全栈上的敏感数据。
    • 检查项: SafeStack found (可能)

FORTIFY_SOURCE

FORTIFY_SOURCE本质上一种检查和替换机制,当使用一些危险函数比如strcpy、sprintf、strcat时,编译器会提示你用加强版函数。

-D_FORTIFY_SOURCE=1

仅在编译时检查,如果编译时无法确定缓冲区大小,则不会进行保护。

-D_FORTIFY_SOURCE=2

程序执行时也会进行检查,不仅检查缓冲区溢出,还会检查格式化字符串漏洞。

libc

确定libc的版本

  1. 获取内存中两个函数的绝对地址的偏移差,与已知的libc进行对比

  2. 在线网站搜索libc-database

  3. 与已知的libc对比低12位地址

    why:Linux 系统中,动态库(如 libc)加载到内存时,其基地址(libc_base)会按 内存页大小(通常为 4KB,即 0x1000 字节)对齐。这意味着基地址的最后 12 位(0x000)始终为 0