二进制基础
二进制基础
Cristrik010源代码的编译和链接
- 编译:由C语言代码生成汇编语言
- 汇编:由汇编代码生成机器码
- 链接:将多个机器码的目标链接成一个可执行文件
可执行文件
什么是可执行文件
可执行文件(Executable file)是一种计算机文件,它包含了一组计算机指令和数据,可以直接在特定的操作系统中运行。可执行文件通常用于执行特定任务或应用程序。
Windows:PE
- 可执行文件:.exe
- 动态链接库:.dll
- 静态链接库:.lib
Linux:ELF
- 可执行文件:.out
- 动态链接库:.so
- 静态链接库:.a
名称 | 内容 |
---|---|
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 | 此节用于存储动态链接的全局偏移表和过程链接表相关的信息。 |
… | ………… |
程序的装载与运行
静态链接过程
- 加载可执行文件:操作系统负责加载可执行文件到内存中,并创建进程。加载过程中,操作系统会为程序分配内存空间,并将可执行文件的代码段、数据段等内容加载到相应的地址空间。
- 初始化:在加载完成后,操作系统会执行一些初始化操作,包括设置栈帧、初始化全局变量和静态变量等。
- 程序执行:操作系统会将控制权交给程序的入口点(通常是
main()
函数),程序开始执行。程序按照顺序执行代码,调用不同的函数和执行各种指令。 - 符号解析和重定位:在程序执行过程中,如果遇到对函数或变量的引用,需要进行符号解析和重定位。符号解析是通过查找符号表来确定引用的符号地址,而重定位是将该地址修正为正确的值。
- 调用函数和跳转:当程序执行到函数调用或跳转指令时,需要进行相关处理。对于函数调用,会保存当前函数的状态,包括返回地址和局部变量等;然后跳转到被调用函数的入口点,并传递参数。函数执行完毕后,返回到调用点继续执行。
- 数据访问:程序可能需要读取或修改数据,包括全局变量、静态变量和常量等。对于全局变量和静态变量,可以直接通过相应的地址进行访问。对于常量,通常会将其存储在只读的数据段中。
- 程序结束:当程序执行到结束点或遇到退出指令时,会执行相应的清理操作,并将控制权交还给操作系统。操作系统回收程序所占用的内存,并终止进程的执行。
动态链接执行过程
- 加载可执行文件:操作系统负责加载可执行文件到内存中,并创建进程。加载过程中,操作系统会为程序分配内存空间,并将可执行文件的代码段、数据段等内容加载到相应的地址空间。
- 初始化:在加载完成后,操作系统会执行一些初始化操作,包括设置栈帧、初始化全局变量和静态变量等。
- 程序执行:操作系统会将控制权交给程序的入口点(通常是
main()
函数),程序开始执行。程序按照顺序执行代码,调用不同的函数和执行各种指令。 - 符号解析和重定位:在程序执行过程中,如果遇到对函数或变量的引用,需要进行符号解析和重定位。与静态链接不同的是,动态链接过程中符号解析是在运行时进行的,通过动态链接器(如动态链接库)来完成。动态链接器会根据需要加载相应的共享库文件,并解析其中的符号表,确定引用的符号地址,然后进行重定位。
- 函数调用和跳转:当程序执行到函数调用或跳转指令时,会进行相关处理。对于动态链接库中的函数,程序会通过跳转到库文件中的入口点来执行相应的代码。参数传递和返回值处理等操作也会参考约定和调用规则进行。
- 数据访问:程序可能需要读取或修改数据,包括全局变量、静态变量和常量等。对于全局变量和静态变量,可以直接通过相应的地址进行访问。对于常量,通常会将其存储在只读的数据段中。
- 程序结束:当程序执行到结束点或遇到退出指令时,会执行相应的清理操作,并将控制权交还给操作系统。操作系统回收程序所占用的内存,并终止进程的执行。
动态链接流程(以 puts
为例)
首次调用
puts@plt
:1
2
3jmp [puts@got.plt] ; 首次指向 .got.plt[2](_dl_runtime_resolve)
push index ; 压入重定位表索引
jmp .plt[0] ; 跳转到动态链接器动态链接器解析:
- 根据索引找到
puts
的符号定义。 - 将真实地址写入
puts@got.plt
。
- 根据索引找到
后续调用
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先被压入栈中。
- 先创建一个栈帧,然后依次存放父函数的基地址(EBP)、函数的参数。
- 当有子函数时,再开辟一个栈帧,先将该子函数下一指令压入栈中,然后将该子函数的父函数的基地址压入栈中,并且使寄存器EBP指向ESP。
- 随后将子函数的变量压入栈中。
- 调用完成之后,子函数参数弹出。
- 弹出父函数的EBP并将其赋值给寄存器EBP
- 弹出返回地址(子函数EIP)并赋值给寄存器EIP
ret leave call
ret
ret命令有两个操作:
- pop rip
- 跳转
leave
leave有三个操作:
- mov rsp,rbp:恢复栈指针
- pop rbp:恢复为调用者的 RBP
call
- push EIP
- 修改EIP
- 跳转
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
被覆盖(如果存在)。
- 这是默认设置(通常由 GCC 的
Full RELRO
:整个重定位节是只读的,防止了某些攻击,如GOT覆盖攻击。- (通常由 GCC 的
-Wl,-z,relro -Wl,-z,now
启用): - 在程序加载后,立即解析所有外部函数地址,并将整个
.got.plt
段(惰性和非惰性绑定)标记为只读。 - 也保护
.fini_array
等构造函数/析构函数数组。 - 消除了篡改 GOT 进行攻击(如 GOT overwrite)的可能性。
- 轻微增加程序启动时间(因为所有符号在启动时解析)。
- (通常由 GCC 的
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
:程序不是位置无关的。它有一个固定的加载基地址(通常是0x400000
或0x8048000
)。即使系统 ASLR 开启,该程序的.text
/.data
等段的地址也是固定的、可预测的。 栈和堆的地址可能仍然被 ASLR 随机化,库地址也一定被随机化。
- 注意:
checksec
通常只报告程序自身的PIE
状态。系统级ASLR
状态需要单独检查(cat /proc/sys/kernel/randomize_va_space
)。
RPATH/RUNPATH
- 目的: 指定程序在运行时搜索共享库(
.so
文件)的额外目录路径。 - 安全问题:
- 不安全路径: 如果
RPATH
或RUNPATH
包含当前目录(.
)、空目录或用户可写的目录,攻击者可以将恶意共享库放在这些目录下。程序在加载时可能会优先加载攻击者的恶意库而非系统标准库,导致任意代码执行(LD_PRELOAD
攻击的一种变体)。 RPATH
vsRUNPATH
:RPATH
优先级很高(在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的版本
获取内存中两个函数的绝对地址的偏移差,与已知的libc进行对比
在线网站搜索libc-database
与已知的libc对比低12位地址
why:Linux 系统中,动态库(如
libc
)加载到内存时,其基地址(libc_base
)会按 内存页大小(通常为 4KB,即0x1000
字节)对齐。这意味着基地址的最后 12 位(0x000
)始终为0
。