深入理解Pwn_Base_knowledge

韩乔落

Linux进程布局及ELF文件结构

这里的讲解并不是很深入,只是浅浅的做了些提示,本文内容是原本准备讲课的提示词,现在在做课程升级,就把之前想要讲课用的内容放在这里。

虚拟地址

操作系统加载可执行文件后,创建了一个进程,这个进程就有了自己的虚拟地址空间,每个进程的虚拟地址空间都一样。

Linux 进程布局

32位布局

x32

  • Kernel space0xFFFFFFFF->0xC00000001GB 大小的空间被分为内核空间,用户进程无法直接访问内核的虚拟内存空间,仅能通过系统调用来进入内核态,从而来访问指定的内核空间地址。
  • Stack:在用户空间的最顶部的部分被叫做栈空间,它一般用于存放函数参数或局部变量,进程中的每一个线程都有属于自己的栈栈由高地址向低地址增长。
  • Mmap:内核将文件内容映射在此这里,例如加载动态链接库。另外,在 Linux 中,如果你通过 malloc 函数申请一块大于MMAP_THRESHOLD (默认为128KB)大小的堆空间时,glibc 会返回一块匿名的 mmap 内存块而非一块堆内存,也就是匿名映射。
  • Heap:堆同栈一样,都是为进程运行提供动态的内存分配,但其和栈的的一个很大区别在于堆上内存的生命期和执行分配的函数的生命期不一致,堆上分配的内存只有在对应进程通过系统调用主动释放或进程结束后才会释放。
  • BSS:用来存放未初始化的全局或静态变量,程序加载时初始化为 0
  • Data:用来存放初始化的全局或者静态变量。
  • Text:这段中存有程序的指令代码。Text 段是通过只读的方式加载到内存中的,他在多个进程中是可以被安全共享的。
  • 0x00000000~0x80480000Reserve(保留区),用户不可访问。

x64进程布局

x64

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>

char *data = "hello data";

char bss[0x10];

int a;

int main()
{
int b = 1;
printf("a==>%d, b==>%d", a, b);
malloc(1);

return 0;
}

二进制简介

计算机使用二进制系统执行计算,系统执行的机器码被称为二进制代码,二进制文件包含每个程序的所有代码和数据。

C语言编译过程

image-20231109145303530

  • 预处理阶段:处理#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
2
3
4
5
6
7
8
9
10
#include <stdio.h>

#define FORMAT "%s"
#define MESSAGE "Hello, World\n"

int main(int argc, char const *argv[])
{
printf(FORMAT, MESSAGE);
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ELF header (Ehdr)
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; /* 幻数以及其他信息*/
uint16_t e_type; /* 对象文件类型*/
uint16_t e_machine; /* 架构*/
uint32_t e_version; /* 对象文件版本*/
ElfN_Addr e_entry; /* 程序入口的虚拟地址*/
ElfN_Off e_phoff; /* 程序头表的偏移量(按字节计算)*/
ElfN_Off e_shoff; /* 节头表的偏移量(按字节计算)*/
uint32_t e_flags; /* 保存与文件相关的、特定于处理器的标 志。标志名称采用EF_machine_flag的格式*/
uint16_t e_ehsize; /* ELF头部的大小(按字节计算) */
uint16_t e_phentsize; /* 程序头表的条目大小(按字节计算) */
uint16_t e_phnum; /* 程序头表的条目数,可以为0 */
uint16_t e_shentsize; /* 节头表的条目大小(按字节计算) */
uint16_t e_shnum; /* 节头表的条目数,可以为0 */
uint16_t e_shstrndx;/* 节头表中与节名称字符串表相关的条目的 索引。如果文件没有节名称字符串表,此参数可以为SHN_UNDEF */
} ElfN_Ehdr;

ELF程序头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Program header (Phdr)typedef struct {  
uint32_t p_type; /*segment type*/
Elf32_Off p_offset; /*segment offset*/
Elf32_Addr p_vaddr; /*segment virtual addr*/
Elf32_Addr p_paddr; /*segment physical addr*/
uint32_t p_filesz; /*size of segment in the file*/
uint32_t p_memsz; /*size of segment in the memory*/
uint32_t p_flags; /*segment flag : R|W|X*/
uint32_t p_align; /*segment alignment in memory*/
} Elf32_Phdr;
/*FOR 64Bit, p_flags is in total struct*/
typedef struct {
uint32_t p_type;
uint32_t p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
uint64_t p_filesz;
uint64_t p_memsz;
uint64_t p_align;
} Elf64_Phdr;

ELF节头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Section header (Shdr)    
typedef struct {
uint32_t sh_name; // offset into shdr string table for shdr name
uint32_t sh_type; // shdr type I.E SHT_PROGBITS
uint32_t sh_flags; // shdr flags I.E SHT_WRITE|SHT_ALLOC
Elf32_Addr sh_addr; // address of where section begins
Elf32_Off sh_offset; // offset of shdr from beginning of file
uint32_t sh_size; // size that section takes up on disk
uint32_t sh_link; // points to another section
uint32_t sh_info; // interpretation depends on section type
uint32_t sh_addralign; // alignment for address of section
uint32_t sh_entsize; // size of each certain entries that may be in
} Elf32_Shdr;
typedef struct {
uint32_t sh_name; / * Section name (string tbl index) */
uint32_t sh_type; / * Section type */
uint64_t sh_flags; / * Section flags */
Elf64_Addr sh_addr; / * Section virtual addr at execution */
Elf64_Off sh_offset; / * Section file offset */
uint64_t sh_size; / * Section size in bytes */
uint32_t sh_link; / * Link to another section */
uint32_t sh_info; / * Additional section information */
uint64_t sh_addralign; / * Section alignment */
uint64_t sh_entsize; / * Entry size if section holds table */
} Elf64_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

二进制文件的加载

image-20231109145853764

  • 创建一个进程,包含虚拟地址空间。
  • 将解释器映射到进程的虚拟内存中,它用于加载二进制文件并执行必要的重定位操作,一般名字为 ld-linux.so。解释器加载后内核将控制权交给解释器,解释器会在用户空间工作。
  • 解释器解析并找出二进制文件使用的动态库,并将其映射到虚拟地址空间,然后在代码节执行所有必要的重定位。

动态链接

二进制文件加载到进程中执行的时候动态链接器执行了最后的重定位。例如在编译时由于不知道加载地址,因此它会解析共享库中函数的引用。这里需要简单介绍一下,实际上在加载二进制文件的时候许多重定位一般都不会立即完成,而是延迟到对未解析位置进行首次引用之前,这就是延迟绑定。

plt.got

推荐书籍

《二进制分析实战》

《Linux二进制分析》

汇编语言基础

寄存器

x32 寄存器:

x32reg

x64 寄存器:

x64reg

常见汇编指令

mov 操作数,源操作数

mov 指令将第二个操作数(可以是寄存器的内容、内存中的内容或值)复制到第一个操作数(寄存器或内存)。mov不能用于直接从内存复制到内存

1
2
3
4
5
mov <reg>,<reg>  	; mov rax, rbx
mov <reg>,<mem> ; mov rax, [rbc] ; mov edx, [esi+4*ebx + 0x8]
mov <mem>,<reg> ; mov [rax], rbx
mov <reg>,<const> ; mov rax, 0x8
mov <mem>,<const> ; mov [rax], 0xff

push 操作数

push 指令将操作数压入内存的栈中,并且rsp - 8 / esp - 4

1
2
3
push <reg>			; push rax
push <mem> ; push [rdx]
push <con> ; push 0xff

pop 操作数

pop 指令与 push 指令相反,它执行的是出栈的工作,并且 rsp + 8 / esp + 4

1
2
pop <reg>			; pop rax
pop <mem> ; pop [rdi]

lea 操作数,源操作数

lea 实际上是一个载入有效地址指令,将第二个操作数表示的地址载入到第一个操作数(寄存器)中。

1
lea <reg>,<mem>		; lea edi, [ebx+4*esi] 

add 操作数,源操作数

add 指令将两个操作数相加,且将相加后的结果保存到第一个操作数中。

1
2
3
4
5
add <reg>,<reg>		;
add <reg>,<mem> ;
add <mem>,<reg> ;
add <reg>,<con> ;
add <mem>,<con> ;

sub 操作数,源操作数

sub指令指示第一个操作数减去第二个操作数,并将相减后的值保存在第一个操作数,

1
2
3
4
5
sub <reg>,<reg>		;
sub <reg>,<mem> ;
sub <mem>,<reg> ;
sub <reg>,<con> ;
sub <mem>,<con> ;

inc 操作数 && dec 操作数

incdec 分别表示将操作数自加1,自减1,

1
2
3
4
inc <reg>			;
inc <mem> ;
dec <reg> ;
dec <mem> ;

imul

整数相乘指令,它有两种指令格式,一种为两个操作数,将两个操作数的值相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器;第二种格式为三个操作数,其语义为:将第二个和第三个操作数相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器。

1
2
3
4
imul <reg>,<reg>		; imul rax, rbx ==> rax = rax * rbx
imul <reg>,<mem> ;
imul <reg>,<reg>,<con> ; imul rax, rbx, 0x10 ==> rax = rbx * 0x10
imul <reg>,<mem>,<con> ;

and 操作数,源操作数, or 操作数,源操作数, xor 操作数,源操作数

逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
and <reg>,<reg>
and <reg>,<mem>
and <mem>,<reg>
and <reg>,<con>
and <mem>,<con>

or <reg>,<reg>
or <reg>,<mem>
or <mem>,<reg>
or <reg>,<con>
or <mem>,<con>

xor <reg>,<reg>
xor <reg>,<mem>
xor <mem>,<reg>
xor <reg>,<con>
xor <mem>,<con>

not

位翻转指令,将操作数中的每一位翻转,即0->1, 1->0。

1
2
not <reg>
not <mem>

neg

取负指令。

1
2
neg <reg>
neg <mem>

SHL、SHR、SAL、SAR: 移位指令

算数移位考虑符号,逻辑移位不考虑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;SHL(Shift Left):             ;逻辑左移
;SHR(Shift Right): ;逻辑右移
;SAL(Shift Arithmetic Left): ;算术左移
;SAR(Shift Arithmetic Right): ;算术右移

;其中的 SHL 和 SAL 相同, 但 SHR 和 SAR 不同.

;SHL, SAL: 每位左移, 低位补 0, 高位进 CF
;SHR : 每位右移, 低位进 CF, 高位补 0
;SAR : 每位右移, 低位进 CF, 高位不变
;它们的结果影响 OF、SF、ZF、PF、CF
;它们的指令格式相同:
SHL/SHR/SAL/SAR <reg>, <con8>
SHL/SHR/SAL/SAR <mem>, <con8>
SHL/SHR/SAL/SAR <reg>, CL
SHL/SHR/SAL/SAR <mem>, CL

ROL、ROR、RCL、RCR: 循环移位指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;ROL(Rotate Left):                循环左移
;ROR(Rotate Right): 循环右移
;RCL(Rotate through Carry Left): 带进位循环左移
;RCR(Rotate through Carry Right): 带进位循环右移

;ROL: 循环左移, 高位到低位并送 CF
;ROR: 循环右移, 低位到高位并送 CF
;RCL: 循环左移, 进位值(原CF)到低位, 高位进 CF
;RCR: 循环右移, 进位值(原CF)到高位, 低位进 CF

;它们的结果影响 OF、CF
;它们的指令格式相同:
ROL/ROR/RCL/RCR <reg>, <con8>
ROL/ROR/RCL/RCR <mem>, <con8>
ROL/ROR/RCL/RCR <reg>, CL
ROL/ROR/RCL/RCR <mem>, CL

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;

标志寄存器

状态标志

状态标志用于指示算术运算(例如使用ADDSUBMULDIV等指令)后的结果,它们包括包括有以下几个标志:

  • CF:进位标志(Carry flag)是标志寄存器的第0位,又被称之为CY,当其被设置时表示运算结果的最高有效位发生进位或借位的情况,并在无符号整数的运算中表示运算的溢出状态。
  • PF:奇偶校验标志(Parity flag)是标志寄存器的第2位,当其被设置表示结果中包含偶数个值为1的位,否则表示结果中包含奇数个值为1的位。
  • AF:辅助进位标志(Auxiliary carry flag)是标志寄存器的第4位,当其被设置表示在算术运算中低三位发生进位或借位(例如ALAH进位或借位)或BCD码算术运算中发生进位或借位的情况。
  • ZF:零标志(Zero flag)是标志寄存器的第6位,当其被设置时运算的结果是否等于0,否则不等于0。
  • SF:符号标志(Sign flag)是标志寄存器的第7位,当其被设置时表示结果为负数,否则为正数。
  • OF:溢出标志(Overflow flag)是标志寄存器的第11位,当其被设置时代表运算结果溢出,即结果超出了能表达的最大范围。

状态标志中,只有CF标志能被直接通过STCCLC以及CMC指令修改。

控制标志

  • DF:方向标志(Direction flag)是标志寄存器的第10位,用于指示串操作指令地址的变化方向。当其被设置时,存储器由自高向低方向变化,否则相反。STDCLD指令分别用于设置、清除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寄存器中VMEPVI位被设置且IOPL小于3时,处理器才将识别该标志。
  • VIP:虚拟中断挂起标志(Virtual interrupt pending flag)是标志寄存器的第20位,其被设置表示有一个中断被挂起(等待处理),否则表示没有等待处理的中断。该标志通常与VIF标志搭配一起使用。
  • ID:ID标志(Identification flag)是标志寄存器的第21位,通过修改该位的值可以测试是否支持CPUID指令。

函数调用栈及ELF相关安全机制

函数调用栈

x32 函数调用栈:

函数通过栈传参,参数从右到左依次入栈。

x32stack

x64 函数调用栈:

参数前六个参数从左到右分别为rdi, rsi, rdx, rcx, r8, r9。超过六个参数,多余参数通过栈传参。

x64stack

1
2
3
4
5
6
# 64bit 系统调用编号
# ra`保存调用号,系统调用传参顺序为rdi, rsi, rdx, r10, r8, r9。
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep __NR_
# 32bit 系统调用编号
# eax保存调用号,系统调用传参顺序为ebx,ecx,edx
cat /usr/include/asm/unistd_32.h | grep __NR_

ELF相关安全机制

RELRO

  • 简介

设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对 GOT(Global Offset Table)攻击。RELRO 为”Partial RELRO,说明我们对 GOT 表具有写权限。

  • 开启方式:
1
2
3
4
gcc -o test test.c						// 默认情况下,是Partial RELRO
gcc -z norelro -o test test.c // 关闭,即No RELRO
gcc -z lazy -o test test.c // 部分开启,即Partial RELRO 重定位表格只读,重定位项可读写
gcc -z now -o test test.c // 全部开启,重定位表格和重定位项均为只读

Canary

  • 简介

函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证 cookie 信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行。在 Linux 中我们将cookie 信息称为 canary

  • 开启方式:
1
2
3
4
gcc -o test test.c						// 默认情况下,不开启Canary保护
gcc -fno-stack-protector -o test test.c //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码

PIE & ASLR

  • 简介

ASLR保护分为三个层级:

1
2
3
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈和堆的随机化。

PIE 保护即位置无关的可执行文件,程序开启地址随机化选、意味着程序每次运行的时候地址都会变化,包含 .bss.text, .data 等段都会被随机化。

  • 开启方式

PIE

1
2
3
4
5
gcc -o test test.c				// 默认情况下,不开启PIE
gcc -fpie -pie -o test test.c // 开启PIE,此时强度为1
gcc -fPIE -pie -o test test.c // 开启PIE,此时为最高强度2
gcc -fpic -o test test.c // 开启PIC,此时强度为1,不会开启PIE
gcc -fPIC -o test test.c // 开启PIC,此时为最高强度2,不会开启PIE

ASLE

1
2
3
sudo echo 0 > /proc/sys/kernel/randomize_va_space # 0 - 表示关闭进程地址空间随机化。
sudo echo 1 > /proc/sys/kernel/randomize_va_space # 1 - 表示将mmap的基址,stack和vdso页面随机化。
sudo echo 2 > /proc/sys/kernel/randomize_va_space # 2 - 表示在1的基础上增加栈和堆的随机化。

NX

  • 简介

NXNo-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入 shellcode时,程序会尝试在数据页面上执行指令,此时 CPU 就会抛出异常,而不是去执行恶意指令。

  • 开启方式
1
2
3
gcc -o test test.c					// 默认情况下,开启NX保护
gcc -z execstack -o test test.c // 禁用NX保护
gcc -z noexecstack -o test test.c // 开启NX保护

FORTIFY

  • 简介

fority 其实非常轻微的检查,用于检查是否存在缓冲区溢出的错误。适用情形是程序采用大量的字符串或者内存操作函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets以及宽字符的变体。

例如 :

  • 开启方式
1
2
3
gcc -o test test.c							// 默认情况下,不会开这个检查
gcc -D_FORTIFY_SOURCE=1 -o test test.c // 较弱的检查
gcc -D_FORTIFY_SOURCE=2 -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.
Comments