C语言
学C
混淆编译
g++ -O2 -s -fno-stack-protector -fomit-frame-pointer -DNDEBUG -static-libgcc -static-libstdc++ "-Wl,--strip-all,--disable-stdcall-fixup" .\Encryption.c -o .\Encryption.exe
函数流程
Windows 操作系统架构基础
完整编译过程及参与的“器”
预处理器(Preprocessor)
输入:源代码(如
.c
,.cpp
)。功能:
展开宏(
#define
)处理条件编译(
#ifdef
,#endif
)包含头文件(
#include
)
输出:预处理后的纯代码(无注释、宏已展开)。
编译器(Compiler)
输入:预处理后的代码。
功能:
语法/语义分析(生成抽象语法树)
优化(如常量折叠、死代码消除)
生成汇编代码(与CPU架构相关)。
输出:汇编语言文件(如
.s
)。
汇编器(Assembler)
输入:汇编代码文件(
.s
)。功能:
将汇编指令逐行翻译为机器码(二进制)。
生成可重定位目标文件(
.o
或.obj
)。
输出:目标文件(含机器码、未解析符号表)。
链接器(Linker)
输入:多个目标文件(
.o
) + 库文件(.a
或.so
)。功能:
符号解析:匹配函数/变量的引用与定义(如
printf
来自库)。重定位:合并所有目标文件,分配最终内存地址。
生成可执行文件(如
a.out
或.exe
)。
输出:完整的可执行程序。
细节补充:- 链接器分为静态和动态链接,静态地址可以进行相对地址寻址
- 静态链接很少,动态链接应用较多
- 由于windows的ALSR机制存在,库地址每次启动程序都会变化,故只能绝对寻址
- 如果要自定义函数,除了直接写到源码里,进行静态编译,还可以把这些内容编译为dll,在需要时按需加载,进行动态寻址
源代码 (.c)
→ [预处理器] → 展开后的代码
→ [编译器] → 汇编代码 (.s)
→ [汇编器] → 目标文件 (.o)
→ [链接器] → 可执行文件 (a.out)
关于动态的,操作系统生成的,用于描述进程的数据结构--PEB和TEB
PEB/TEB在内存中的位置
高地址 (0x7FFFFFFF) // 32位进程示例
+---------------------------------------+ ----
| | ^
| 内核空间 | | // 禁止访问 (Inaccessible)
| | |
+=======================================+ <--- 用户/内核空间分界线 (e.g., 0x80000000)
| | |
| 用户空间共享数据页 | | // 所有进程共享的只读数据,如系统时间、性能计数器
| (KUSER_SHARED_DATA, 固定地址) | |
| | |
+---------------------------------------+ |
| | |
| 栈 (Stack) | | // 用于函数调用,向下增长 (↓)
| (↓ grows down) | |
| | | +-----------------------+
| | | | TEB |
+---------------------------------------+ | +-----------------------+
| TEB 数组 | | | - Stack Base (栈底) | // 线程的栈信息在这里
| (TEB for additional threads, if any)| | | - Stack Limit (栈顶) |
| | | | - Ptr to PEB | ---+
+---------------------------------------+ | | - Thread ID | |
| 环境块 | | | - LastError Value | |
| (Process Environment Block, PEB) | | +-----------------------+ |
| | | |
| +---------------------------------+ | | +-----------------------+ |
| | PEB Contents: | | | | PEB | |
| | - ImageBaseAddress (exe addr) | | | +-----------------------+ |
| | - Ldr (Ptr to ---+--> PEB_LDR_DATA)| | | - ImageBaseAddress | |
| | - ProcessParameters (CmdLine, EnvVars)| | | - Ldr (Module Lists) | |
| | - ... | | | | - ProcessParameters | |
| +---------------------------------+ | | | - ... | |
| | | +-----------------------+ |
+---------------------------------------+ | |
| 堆 (Heap) | | |
| (默认进程堆 + 可选私有堆, ↑ grows up) | | |
| | | |
+---------------------------------------+ | |
| 其他内存映射文件/区域 | | // 例如:额外的DLL、内存映射文件 |
| (Memory Mapped Files, e.g., DLLs) | | |
| | | |
+---------------------------------------+ | |
| PE 映像 (exe) | | // 您的主程序.exe文件被映射到这里 |
| (Mapped Executable Image, .exe) | | +----------------------------+ |
| | | | .text (Code, RX) | |
| +---------------------------------+ | | | .data (Init Data, RW) | |
| | .text (Code, RX) | | | | .rdata (Const Data, R) | |
| | .data (Init Data, RW) | | | | .idata (Import Table, R) | | // **关键:导入表在这里**
| | .rdata (Const Data, R) | | | | .reloc (Relocations, R) | | // **关键:重定位表在这里**
| | .idata (Import Table, R) | | | | ... | |
| | .reloc (Relocations, R) | | | +----------------------------+ |
| | ... | | | |
| +---------------------------------+ | | |
| | | |
+---------------------------------------+ | |
| PE 映像 (DLLs) | | // 所有依赖的DLL(如kernel32.dll) |
| (Mapped DLL Images, .dlls) | | 也被映射到类似的区域,地址随机化(ASLR)|
| | | |
| +---------------------------------+ | | |
| | .text (Code, RX) | | | |
| | .data (Init Data, RW) | | | |
| | .rdata (Const Data, R) | | | |
| | .edata (Export Table, R) | | | // **关键:导出表在这里** |
| | ... | | | |
| +---------------------------------+ | | |
| | v
+---------------------------------------+ ----
低地址 (0x00010000) // 通常从0x10000开始以避免空指针访问
TEB内容描述
PEB 和 TEB 不是静态的、只读的数据结构。它们是进程和线程运行时状态的动态反映,其内容在整个生命周期内会不断被操作系统和程序本身修改。
PEB 的动态变化 (Dynamic Changes to the PEB)
PEB 中许多字段的值在进程运行时会发生改变:
Ldr
(加载器数据 - Loader Data):- 这是最活跃的部分之一。每当进程动态加载一个新的 DLL(例如通过
LoadLibrary()
API)或卸载一个 DLL(FreeLibrary()
)时,操作系统加载器都会立即更新PEB->Ldr
所指向的模块链表。 - 新模块的
LDR_DATA_TABLE_ENTRY
结构会被动态创建并插入到三个链表(InLoadOrderModuleList
,InMemoryOrderModuleList
,InInitializationOrderModuleList
)中。反之,卸载时会被移除。 - 反调试和恶意软件分析:许多软件(包括恶意软件)会遍历这个链表来检测是否有调试器相关的DLL(如
*.dll
)被加载,或者试图隐藏自身模块。
- 这是最活跃的部分之一。每当进程动态加载一个新的 DLL(例如通过
BeingDebugged
:- 这个标志位直接由调试器操作。当一个调试器(如 OllyDbg, x64dbg, WinDbg)附加到进程时,操作系统内核会将此标志设置为 1。
- 程序可以通过检查
PEB->BeingDebugged
来执行反调试操作(例如,如果发现被调试就主动崩溃或改变行为)。著名的IsDebuggerPresent()
API 函数内部就是直接读取的这个字段。
ProcessHeap
:- 进程可以创建多个私有堆。虽然默认堆的句柄通常不变,但如果堆管理器因内存分配/释放而扩展或收缩堆的大小,其内部结构会变化,这些信息也可能间接反映出来。
ProcessParameters
中的某些字段:- 虽然命令行和环境变量通常在创建后不变,但进程完全可以调用 API 来动态修改当前目录等信息,这些更改会同步回
PEB->ProcessParameters
。
- 虽然命令行和环境变量通常在创建后不变,但进程完全可以调用 API 来动态修改当前目录等信息,这些更改会同步回
TEB 的动态变化 (Dynamic Changes to the TEB)
TEB 的变化更加频繁,因为它与线程的执行流直接相关:
LastErrorValue
:- 这是变化最频繁的字段之一。几乎每次调用 Windows API 函数后,该值都可能被设置。
GetLastError()
这个 API 的本质就是直接返回TEB->LastErrorValue
的值。SetLastError()
则是直接写入它。
- 这是变化最频繁的字段之一。几乎每次调用 Windows API 函数后,该值都可能被设置。
栈信息 (
StackBase
,StackLimit
):- 虽然栈的范围通常不会变,但随着函数调用和返回,栈指针 (
ESP/RSP
) 在不断变化。而TEB
本身也位于栈内存的某个区域。
- 虽然栈的范围通常不会变,但随着函数调用和返回,栈指针 (
异常处理链 (
ExceptionList
):- 每当线程进入一个
__try
块或类似的异常处理结构时,编译器生成的代码会将一个异常处理记录注册(压入)到TEB->ExceptionList
链表中。离开__try
块时再解除注册(弹出)。这个链表在异常发生时被内核遍历,以决定由哪个处理程序来接管。
- 每当线程进入一个
线程本地存储 (TLS) 数组:
- 线程可以通过
TlsSetValue()
等 API 在预分配的 TLS 槽中存储数据。这些数据就存放在 TEB 的 TLS 数组里,随时可以被读写。
- 线程可以通过
ArbitraryUserPointer
:- 这是一个可供程序自由使用的指针,任何线程都可以随时读写它,用于存储线程上下文的任意信息。
谁在修改它们?
操作系统内核 (Operating System Kernel):
- 这是最主要的修改者。加载DLL、附加调试器、处理异常等核心操作都是由内核发起的,内核会代表用户模式代码更新这些结构。
系统运行时库 (System Runtime Libraries, 如 NTDLL.dll):
- 用户模式的系统函数(如
LoadLibrary
,SetLastError
)在内部最终会执行修改 PEB/TEB 的代码。
- 用户模式的系统函数(如
程序自身 (The Program Itself):
- 程序可以直接或间接地修改这些结构。
- 间接:通过调用官方API(如
SetLastError
,TlsSetValue
)。 - 直接:通过硬编码方式直接访问和修改。例如,在汇编中:
关于PE 映像----所有的DLL和exe都在此区域
可执行文件格式 (The Key to DLL Loading)
- PE 文件格式 (Portable Executable): 这是理解DLL加载机制的最关键知识。DLL和EXE都是PE文件。
基地址(ImageBase): PE文件首选的加载内存地址。
导入表(Import Address Table, IAT): 存储这个文件需要调用的其他DLL中的函数地址。Windows加载器在加载过程中会填充这个表,这就是“动态链接”的核心。
导出表(Export Table): 存储这个DLL向外提供的函数列表和地址。
重定位表(Relocation Table): 如果DLL无法加载到首选基地址,需要通过这个表中的信息对代码和数据进行修正。
注:地址随机化(ASLR)在此区域内进行
Windows PE 特有的关键段
这些段是 PE 格式的核心组成部分,承载了 Windows 特有的加载、链接、重定位、资源管理等功能:
.idata
(Import Directory)功能:导入表。这是 PE 文件最重要的特有段之一。
内容:描述了该模块(EXE 或 DLL)依赖哪些其他 DLL,以及需要从这些 DLL 中导入哪些函数。包含一个
IMAGE_IMPORT_DESCRIPTOR
结构数组,每个结构对应一个依赖的 DLL,并指向该 DLL 的导入名称表 (INT) 和导入地址表 (IAT)。属性:通常为
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ
(只读)。重要性:加载器根据此段加载依赖的 DLL 并填充 IAT,实现动态链接。逆向分析和破解经常需要修改此段。
.edata
(Export Directory)功能:导出表。主要用于 DLL。
内容:描述了该 DLL 向其他模块提供了哪些函数或变量。包含一个
IMAGE_EXPORT_DIRECTORY
结构,以及导出函数名数组、函数序号数组、函数地址数组 (EAT)。属性:通常为
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ
(只读)。重要性:其他模块通过此表找到并使用该 DLL 的功能。
.reloc
(Base Relocations)功能:重定位表。对于支持 ASLR 或无法加载到首选基地址 (
ImageBase
) 的模块至关重要。内容:包含一个
IMAGE_BASE_RELOCATION
结构数组。每个结构描述了一页内存(4KB)内所有需要重定位的地址偏移量及其类型。作用:如果模块无法加载到其
ImageBase
,加载器会根据实际加载地址和此表中的信息,修正代码和数据中对绝对地址的引用。属性:通常为
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_DISCARDABLE
(可读,加载后通常可丢弃)。
.rsrc
(Resource Directory)功能:资源段。存储程序的各类资源。
内容:图标、光标、位图、对话框模板、菜单、字符串表、版本信息、清单文件 (Manifest) 等。采用树形目录结构 (
IMAGE_RESOURCE_DIRECTORY
) 组织。属性:通常为
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ
(只读)。访问:程序通过
FindResource
,LoadResource
等 API 访问这些资源。
.tls
(Thread Local Storage)功能:线程局部存储初始化数据。
内容:定义了使用
__declspec(thread)
声明的线程局部变量的初始值和大小。作用:当新线程创建时,系统会为每个线程分配一块 TLS 内存区域,并根据此段的内容初始化该线程的 TLS 变量。
属性:通常为
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE
(可读、可写?实际运行时每个线程有自己的副本)。
这是C程序编译后的内存的结构
高地址 (0x7fffffffffff)
+------------------------------+
| | 内核空间 (Kernel Space)
| | (用户程序不可访问)
+------------------------------+ <-- 0x7fffffffffff (1<<47 -1)
| ... |
| 环境变量 (Environment vars) |
| 命令行参数 (Command-line args)| 栈顶 (初始时)
| |
+------------------------------+ <-- 栈顶 (Stack Pointer, RSP) 向下增长
| Stack |
| (向下增长 ↓) |
| |
| ... |
| |
+------------------------------+
| ... |
| ... | 未映射区域 (Hole / Guard Page)
+------------------------------+
| |
| Heap |
| (向上增长 ↑) |
| |
+------------------------------+ <-- 堆底 (Break, brk/sbrk 管理)
| .bss | (未初始化/零初始化全局/静态变量)
+------------------------------+
| .data | (初始化非零全局/静态变量)
+------------------------------+
| .rodata | (只读数据, 如字符串常量) [可能合并到 .text]
+------------------------------+
| .text | (程序代码, 只读)
+------------------------------+
|程序头部等 (ELF Header)LINUX系统| 低地址 (0x400000 附近)
+------------------------------+
.text 是可执行代码存放的位置,
.rodata 是常量部分,如字符串的值
.data 初始化的非零,存储了键值对的内存段
.bss 值为0的变量区域,存储了键
Heap 和 stack不必多说
从程序调用角度理解内存块的协调配合
程序调用过程中的内存图解
高地址 (0x7fffffffffff)
+---------------------------------------+
| 内核空间 |
+---------------------------------------+ <-- 0x7fffffffffff
| |
| 调用者栈帧 (Caller) |
| (Caller's Frame) |
| |
| +---------------------------------+ |
| | 调用者局部变量 (Caller's Locals) | | <-- 调用者EBP (Caller's EBP)
| +---------------------------------+ |
| | 参数区域 (Parameters Area) | |
| +---------------------------------+ |
| |
+=======================================+ <-- 调用者栈顶 (Caller's ESP before call)
| 函数调用过程 |
| (Function Call) |
+---------------------------------------+
| |
| 被调用函数栈帧 |
| (Callee Frame) |
| |
| +---------------------------------+ |
| | 参数2 (Param2) | | <-- [EBP + 12]
| +---------------------------------+ |
| | 参数1 (Param1) | | <-- [EBP + 8]
| +---------------------------------+ |
| | 返回地址 (Return Address) | | <-- [EBP + 4]
| +---------------------------------+ |
| | 保存的EBP (Saved EBP) | | <-- EBP 当前指向这里 ★
| +---------------------------------+ |
| | 局部变量1 (Local Var1) | | <-- [EBP - 4]
| +---------------------------------+ |
| | 局部变量2 (Local Var2) | | <-- [EBP - 8]
| +---------------------------------+ |
| | ... (其他局部变量) | | <-- ESP 当前指向这里
| +---------------------------------+ |
| |
+=======================================+ <-- 栈当前顶部 (Current ESP)
| 未使用栈空间 |
| (Free Stack Space) |
| |
+---------------------------------------+
| 堆 (Heap) |
| (向上增长 ↑) |
+---------------------------------------+
| .bss / .data / .rodata |
+---------------------------------------+
| .text (代码段) |
+---------------------------------------+
低地址 (0x400000)
📌 函数调用流程(linux):
- 调用 (
call func
):
→ 压入返回地址,跳转至func
代码。 - 建立新栈帧 (序言):
→push ebp
(保存调用者栈底)
→mov ebp, esp
(设置当前栈底)
→sub esp, N
(为局部变量腾空间)。 - 执行函数体:
→ 用[ebp + offset]
访问参数
→ 用[ebp - offset]
访问局部变量
→ 执行业务逻辑(.text 指令)。 - 拆除栈帧 (尾声):
→mov esp, ebp
(丢弃局部变量)
→pop ebp
(恢复调用者栈底)
→ret
(弹出返回地址,跳回调用处)。
注意
- 参数和局部变量中间夹着返回地址,意味着当返回地址被弹出后,参数还留在栈上,需要手动清理
栈溢出
32位系统示例:
char buffer[32]
→ 32字节- EBP指针 → 4字节
- 总偏移 = 32 + 4 = 36字节
64位系统示例: char buffer[32]
→ 32字节- RBP指针 → 8字节
- 总偏移 = 32 + 8 = 40字节