深入理解Pwn_Base_knowledge
Linux进程布局及ELF文件结构
这里的讲解并不是很深入,只是浅浅的做了些提示,本文内容是原本准备讲课的提示词,现在在做课程升级,就把之前想要讲课用的内容放在这里。
虚拟地址
操作系统加载可执行文件后,创建了一个进程,这个进程就有了自己的虚拟地址空间,每个进程的虚拟地址空间都一样。
Linux 进程布局
32位布局
Kernel space
:0xFFFFFFFF->0xC0000000
这1GB
大小的空间被分为内核空间,用户进程无法直接访问内核的虚拟内存空间,仅能通过系统调用来进入内核态,从而来访问指定的内核空间地址。Stack
:在用户空间的最顶部的部分被叫做栈空间,它一般用于存放函数参数或局部变量,进程中的每一个线程都有属于自己的栈栈由高地址向低地址增长。Mmap
:内核将文件内容映射在此这里,例如加载动态链接库。另外,在Linux
中,如果你通过malloc
函数申请一块大于MMAP_THRESHOLD
(默认为128KB)大小的堆空间时,glibc
会返回一块匿名的mmap
内存块而非一块堆内存,也就是匿名映射。Heap
:堆同栈一样,都是为进程运行提供动态的内存分配,但其和栈的的一个很大区别在于堆上内存的生命期和执行分配的函数的生命期不一致,堆上分配的内存只有在对应进程通过系统调用主动释放或进程结束后才会释放。BSS
:用来存放未初始化的全局或静态变量,程序加载时初始化为0
。Data
:用来存放初始化的全局或者静态变量。Text
:这段中存有程序的指令代码。Text
段是通过只读的方式加载到内存中的,他在多个进程中是可以被安全共享的。0x00000000~0x80480000
:Reserve
(保留区),用户不可访问。
x64进程布局
源码:
1 |
|
二进制简介
计算机使用二进制系统执行计算,系统执行的机器码被称为二进制代码,二进制文件包含每个程序的所有代码和数据。
C语言编译过程
- 预处理阶段:处理
#define
和#include
指令。
1 | gcc -E -P compilation_example.c |
- 编译阶段:将纯C代码转换为汇编语言,编译器优化。
1 | gcc -S -masm=intel compilation_example.c |
- 汇编阶段:将汇编代码转换成机器码,生成可重定位的二进制文件。
1 | gcc -c compilation_example.c |
- 链接阶段:将所有对象文件链接到一个二进制可执行文件,静态库合并到二进制可执行文件,留下符号引用,被动态链接器用来解析动态库的最终依赖关系。
1 | gcc compilation_example.c |
源码:
1 |
|
ELF文件结构
ELF分类
ET_NONE
:未知类型。这个标记表明文件类型不确定,或者还未定义。ET_REL
:重定位文件。ELF 类型标记为relocatable
意味着该文件 被标记为了一段可重定位的代码,有时也称为目标文件。可重定位 目标文件通常是还未被链接到可执行程序的一段位置独立的代码 (position independent code)。在编译完代码之后通常可以看到一 个.o 格式的文件,这种文件包含了创建可执行文件所需要的代码 和数据。ET_EXEC
:可执行文件。ELF 类型为executable
,表明这个文件被标 记为可执行文件。这种类型的文件也称为程序,是一个进程开始执 行的入口。ET_DYN
:共享目标文件。ELF 类型为dynamic
,意味着该文件被标记 为了一个动态的可链接的目标文件,也称为共享库。这类共享库会在 程序运行时被装载并链接到程序的进程镜像中。ET_CORE
:核心文件。在程序崩溃或者进程传递了一个SIGSEGV
信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以使用GDB
读取这类文件来辅助调试并查找程序崩溃的原因。
ELF头部
1 | ELF header (Ehdr) |
ELF程序头
1 | Program header (Phdr)typedef struct { |
ELF节头
1 | Section header (Shdr) |
ELF重要节
.init
节和.fini
节。.text
节。.bss
节,.data
节,.rodata
节。- 延迟绑定
.plt
,.got
,.got.plt
。 .rel.*
,.rela.*
节。.dynamic
节。.init_array
和.fini_array
。.shstrtab
,.symtab
,.strtab
,.dynsym
,.hash
及.dynstr
节。.ctors
和.dtors
节 。
符号和剥离的二进制文件
高级源代码(如C代码)均以有意义的、人类可读的函数和变量命名为中心。编译程序时,编译器会翻译符号,这些符号会跟踪其名称,并记录哪些二进制代码和数据对应哪个符号。如函数符号提供符 号从高级函数名称到第一个地址和每个函数的大小的映射。链接器在组合对象文件时通常使用此信息,例如,使用此信息来解析模块之间的函数和变量引用,并且帮助调试。
可以使用命令查看二进制文件的符合信息。
1 | readelf --syms a.out |
GCC
的默认行为是不自动剥离新编译 的二进制文件。如果你想知道带符号的二进制文件最终是如何被剥离 的,可以使用 strip
命令。
1 | strip --strip-all a.out |
二进制文件的加载
- 创建一个进程,包含虚拟地址空间。
- 将解释器映射到进程的虚拟内存中,它用于加载二进制文件并执行必要的重定位操作,一般名字为
ld-linux.so
。解释器加载后内核将控制权交给解释器,解释器会在用户空间工作。 - 解释器解析并找出二进制文件使用的动态库,并将其映射到虚拟地址空间,然后在代码节执行所有必要的重定位。
动态链接
二进制文件加载到进程中执行的时候动态链接器执行了最后的重定位。例如在编译时由于不知道加载地址,因此它会解析共享库中函数的引用。这里需要简单介绍一下,实际上在加载二进制文件的时候许多重定位一般都不会立即完成,而是延迟到对未解析位置进行首次引用之前,这就是延迟绑定。
推荐书籍
《二进制分析实战》
《Linux二进制分析》
汇编语言基础
寄存器
x32
寄存器:
x64
寄存器:
常见汇编指令
mov 操作数,源操作数
mov
指令将第二个操作数(可以是寄存器的内容、内存中的内容或值)复制到第一个操作数(寄存器或内存)。mov不能用于直接从内存复制到内存
1 | mov <reg>,<reg> ; mov rax, rbx |
push 操作数
push
指令将操作数压入内存的栈中,并且rsp - 8 / esp - 4
。
1 | push <reg> ; push rax |
pop 操作数
pop
指令与 push
指令相反,它执行的是出栈的工作,并且 rsp + 8 / esp + 4
。
1 | pop <reg> ; pop rax |
lea 操作数,源操作数
lea
实际上是一个载入有效地址指令,将第二个操作数表示的地址载入到第一个操作数(寄存器)中。
1 | lea <reg>,<mem> ; lea edi, [ebx+4*esi] |
add 操作数,源操作数
add
指令将两个操作数相加,且将相加后的结果保存到第一个操作数中。
1 | add <reg>,<reg> ; |
sub 操作数,源操作数
sub
指令指示第一个操作数减去第二个操作数,并将相减后的值保存在第一个操作数,
1 | sub <reg>,<reg> ; |
inc 操作数 && dec 操作数
inc
,dec
分别表示将操作数自加1,自减1,
1 | inc <reg> ; |
imul
整数相乘指令,它有两种指令格式,一种为两个操作数,将两个操作数的值相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器;第二种格式为三个操作数,其语义为:将第二个和第三个操作数相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器。
1 | imul <reg>,<reg> ; imul rax, rbx ==> rax = rax * rbx |
and 操作数,源操作数, or 操作数,源操作数, xor 操作数,源操作数
逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。
1 | and <reg>,<reg> |
not
位翻转指令,将操作数中的每一位翻转,即0->1, 1->0。
1 | not <reg> |
neg
取负指令。
1 | neg <reg> |
SHL、SHR、SAL、SAR: 移位指令
算数移位考虑符号,逻辑移位不考虑。
1 | ;SHL(Shift Left): ;逻辑左移 |
ROL、ROR、RCL、RCR: 循环移位指令
1 | ;ROL(Rotate Left): 循环左移 |
jmp 操作数
控制转移到label
所指示的地址。
1 | jump label |
leave
leave
指令等同于两条指令 mov rbp, rsp; pop rbp
ret
ret
指令等同于 pop rip
。
call 操作数
call
操作数 call
指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其它简单的跳转指令不同,call
指令保存调用之前的地址信息(当 call
指令结束后,返回到调用之前的地址)。
1 | call label ; call label ==> push rdi + 8; jump label; ... ; ret; |
标志寄存器
状态标志
状态标志用于指示算术运算(例如使用ADD
、SUB
、MUL
、DIV
等指令)后的结果,它们包括包括有以下几个标志:
CF
:进位标志(Carry flag)是标志寄存器的第0位,又被称之为CY
,当其被设置时表示运算结果的最高有效位发生进位或借位的情况,并在无符号整数的运算中表示运算的溢出状态。PF
:奇偶校验标志(Parity flag)是标志寄存器的第2位,当其被设置表示结果中包含偶数个值为1的位,否则表示结果中包含奇数个值为1的位。AF
:辅助进位标志(Auxiliary carry flag)是标志寄存器的第4位,当其被设置表示在算术运算中低三位发生进位或借位(例如AL
向AH
进位或借位)或BCD码算术运算中发生进位或借位的情况。ZF
:零标志(Zero flag)是标志寄存器的第6位,当其被设置时运算的结果是否等于0,否则不等于0。SF
:符号标志(Sign flag)是标志寄存器的第7位,当其被设置时表示结果为负数,否则为正数。OF
:溢出标志(Overflow flag)是标志寄存器的第11位,当其被设置时代表运算结果溢出,即结果超出了能表达的最大范围。
状态标志中,只有CF
标志能被直接通过STC
、CLC
以及CMC
指令修改。
控制标志
DF
:方向标志(Direction flag)是标志寄存器的第10位,用于指示串操作指令地址的变化方向。当其被设置时,存储器由自高向低方向变化,否则相反。STD
与CLD
指令分别用于设置、清除DF
标志的值。
系统标志
TF
:陷阱标志(Trap flag)是标志寄存器的第8位,当其被设置时将开启单步调试模式。在其被设置的情况下,每个指令被执行后都将产生一个调试异常,以便于观察指令执行后的情况。IF
:中断标志(Interrupt flag)是标志寄存器的第9位,当其被设置时表示CPU可响应可屏蔽中断(maskable interrupt)。IOPL
:I/O特权级别标志(I/O privilege level flag)是标志寄存器的第12位以及第13位,表示当其程序或任务的I/O权限级别。I/O权限级别为0~3范围之间的值,通常一般用户程序I/O特权级别为0。当前运行程序的CPL(current privilege level)必须小于等于IOPL,否则将发生异常。NT
:嵌套任务(Nested task flag)是标志寄存器的第14位,用于控制中断返回指令IRET
的执行方式。若被设置则将通过中断的方式执行返回,否则通过常规的堆栈的方式执行。在执行CALL
指令、中断或异常处理时,处理器将会设置该标志。RF
:恢复标志(Resume flag)是标志寄存器的第16位,用于控制处理器对调试异常的响应。若其被设置则会暂时禁止断点指令产生的调试异常,其复位后断点指令将会产生异常。VM
:虚拟8086模式标志(Virtual 8086 mode flag)是标志寄存器的第17位,当其被设置表示启用虚拟8086模式(在保护模式下模拟实模式),否则退回到保护模式工作。AC
:对齐检查标志(Alignment check (or access control) flag)是标志寄存器的第18位。当该标志位被设置且CR0
寄存器中的AM
位被设置时,将对用户态下对内存引用进行对齐检查,在存在未对齐的操作数时产生异常。VIF
:虚拟中断标志(Virtual interrupt flag)是标志寄存器的第19位,为IF
标志的虚拟映象。该标志与VIP
标志一起,且在CR4
寄存器中VME
或PVI
位被设置且IOPL
小于3时,处理器才将识别该标志。VIP
:虚拟中断挂起标志(Virtual interrupt pending flag)是标志寄存器的第20位,其被设置表示有一个中断被挂起(等待处理),否则表示没有等待处理的中断。该标志通常与VIF
标志搭配一起使用。ID
:ID标志(Identification flag)是标志寄存器的第21位,通过修改该位的值可以测试是否支持CPUID
指令。
函数调用栈及ELF相关安全机制
函数调用栈
x32 函数调用栈:
函数通过栈传参,参数从右到左依次入栈。
x64 函数调用栈:
参数前六个参数从左到右分别为rdi, rsi, rdx, rcx, r8, r9
。超过六个参数,多余参数通过栈传参。
1 | # 64bit 系统调用编号 |
ELF相关安全机制
RELRO
- 简介
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对 GOT(Global Offset Table)
攻击。RELRO
为”Partial RELRO
,说明我们对 GOT
表具有写权限。
- 开启方式:
1 | gcc -o test test.c // 默认情况下,是Partial RELRO |
Canary
- 简介
函数开始执行的时候会先往栈里插入cookie
信息,当函数真正返回的时候会验证 cookie
信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将 cookie
信息给覆盖掉,导致栈保护检查失败而阻止 shellcode
的执行。在 Linux
中我们将cookie
信息称为 canary
。
- 开启方式:
1 | gcc -o test test.c // 默认情况下,不开启Canary保护 |
PIE & ASLR
- 简介
ASLR
保护分为三个层级:
1 | 0 - 表示关闭进程地址空间随机化。 |
PIE
保护即位置无关的可执行文件,程序开启地址随机化选、意味着程序每次运行的时候地址都会变化,包含 .bss
,.text
, .data
等段都会被随机化。
- 开启方式
PIE
1 | gcc -o test test.c // 默认情况下,不开启PIE |
ASLE
1 | sudo echo 0 > /proc/sys/kernel/randomize_va_space # 0 - 表示关闭进程地址空间随机化。 |
NX
- 简介
NX
即 No-eXecute
(不可执行)的意思,NX(DEP)
的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入 shellcode
时,程序会尝试在数据页面上执行指令,此时 CPU
就会抛出异常,而不是去执行恶意指令。
- 开启方式
1 | gcc -o test test.c // 默认情况下,开启NX保护 |
FORTIFY
- 简介
fority
其实非常轻微的检查,用于检查是否存在缓冲区溢出的错误。适用情形是程序采用大量的字符串或者内存操作函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets
以及宽字符的变体。
例如 :
- 开启方式
1 | gcc -o test test.c // 默认情况下,不会开这个检查 |
- Title: 深入理解Pwn_Base_knowledge
- Author: 韩乔落
- Created at : 2023-12-09 08:58:33
- Updated at : 2024-11-06 14:04:57
- Link: https://jelasin.github.io/2023/12/09/深入理解Pwn_Base_knowledge/
- License: This work is licensed under CC BY-NC-SA 4.0.