说在前面:
感觉自己的博客有点空空,索性把整理的笔记发上来罢(
为了和网上所能搜到的知识做出区别性,我决定在里面加点自己的东西和延申。
想了想还是从内存开始吧,再推及栈到堆,之所以叫做补全计划也是为了看看自己还有哪方面不了解hh
示例:

这是我用c写的一个列表
这个程序的内存结构大致如下,用ai跑了一下图
从上往下来看,先是内核空间,这个内存空间用户态程序不可访问,任何对这片区域的读写都会触发段错误。用户空间和内核空间的分界线在 0x00007FFFFFFFFFFF。
往下是栈(stack),由 [stack] 标记,地址通常在 0x7FFFFFFFxxxx 附近。栈从高地址向低地址增长,存放局部变量、函数返回地址、saved rbp、以及 argv 和环境变量指针。主线程栈默认约 8MB。紧邻栈的下方还可能有线程栈,以匿名映射形式存在。
再往下是动态链接器 ld.so,地址在 0x7FFFF7FFxxxx 左右。它和普通共享库一样分四个权限段:r–p(只读数据)、r-xp(代码)、r–p(更多只读数据)、rw-p(可写段,其中包含 _rtld_global 结构体)。
紧接着是内核注入的两个特殊映射:[vdso] 和 [vvar],地址在 0x7FFFF7FCxxxx 附近。vdso 是 r-xp,存放 gettimeofday 等快速系统调用的代码段,由内核直接注入进程地址空间,避免陷入内核上下文切换。vvar 是 r–p,存放内核暴露给用户态的只读数据,比如时钟信息。
继续往下是共享库区域,核心是 libc.so.6,地址在 0x7FFFF7xx0000。libc 同样分成多个权限段:r–p 段存放只读数据,"/bin/sh" 字符串就在这里;r-xp 段存放代码,system、execve、malloc 等函数都在这里,约 1.5MB;最底下是 rw-p 可写数据段,存放 environ、stdout、stdin 等指针,旧版 glibc(<2.34)的 __free_hook 和 __malloc_hook 也在这。libc 上下可能夹着其他共享库和匿名映射。
在 libc 区域和堆之间有一片松散的mmap 匿名映射区。大块的 malloc(超过 128KB 阈值)直接从这分配,不经过 brk。libc 内部的 arena 管理结构也常出现在这片区域。
继续往下是堆(heap),由 [heap] 标记,地址在 0x5555555xxxxx 附近。堆通过 brk 系统调用从低地址向高地址增长。小块 malloc(默认小于 128KB)的 chunk 链表就在这。glibc 在此之上建立了 tcache、fastbin、smallbin、largebin、unsorted bin 的完整管理体系。
再往下是程序本体(ELF),PIE 开启时地址从 0x555555554000 左右开始,PIE 关闭时固定在 0x400000。同样是多段权限布局:最上面是 r–p(ELF 头、.interp、.note 等元信息),然后是 r-xp(.text、.init、.plt 代码段),接着是 r–p(.rodata 字符串常量、.eh_frame 异常处理表),再下面是 r–p 或 rw-p(取决于 Full/Partial RELRO),最底下是 rw-p(.data 全局变量、.bss 未初始化数据、.got.plt 函数指针表)。
程序本体和地址零点之间,有保留区域和 NULL 页。NULL 页由 mmap_min_addr 保护,任何对地址零附近的访问都会直接报错——这是为了防止空指针解引用被利用,也是为什么用户程序的第一个有效地址从 0x400000 或 0x555... 开始而不是从 0x0 开始。
还是先从ELF本体开始好了。
ELF会分成好几段,每段都有不同的作用,我在文后再罗列好了。在此就讲几个比较重要的
.text(代码段),存放编译后的机器指令,只读但可执行。main函数入口地址存放在这。CPU 的指令寄存器(RIP/EIP)指向这里取指执行。若能改写这里,可实现代码重用攻击(ROP)。
.rodata(只读数据段),存放字符串常量、switch 跳转表、const 修饰的全局变量。尝试写入会触发 segfault(段错误)。格式化字符串漏洞中,%s 可以用来读这里的内容,从而泄露程序中的固定字符串(如 “/bin/sh” 的地址)。
.plt(过程链接表),存放一小段跳转指令,用于调用动态链接库中的外部函数。每个外部函数对应一个 PLT 条目(如 printf@plt)。第一次调用时跳转到动态链接器解析真实地址,后续调用直接跳转到 .got.plt 中已填充的地址。常用于 ret2plt 技术,绕过 ASLR 泄露库函数地址。
.got(全局偏移表),存放全局变量和外部函数的运行时实际地址。可读写。编译时无法确定的外部符号(如 extern 变量、其他共享库的函数),在动态链接器加载或第一次引用时,会将真实地址填充到这里。若能够改写 .got 表项,可以劫持程序控制流(GOT 劫持)。
.got.plt,GOT 的一个子集,专门服务于 .plt,用于存放外部函数的实际地址。可读写。延迟绑定的关键位置:第一次调用外部函数前,.got.plt 中存放的是指向 .plt 中解析逻辑的地址;动态链接器解析完成后,直接覆盖为真实函数地址。格式化字符串漏洞中,%n 常被用来向这里写入恶意地址,实现 GOT 劫持。
.data(数据段),存放已初始化且值非零的全局变量和静态变量(如 int g = 42;)。可读写。这些变量在编译时就确定了初始值,占用磁盘空间。生命周期为整个程序运行期间。
.bss(Block Started by Symbol),存放未初始化或初始化为 0 的全局变量和静态变量(如 int g; 或 static int s = 0;)。可读写。不占用磁盘文件空间(只记录大小和位置),程序加载时由操作系统分配内存并清零。BSS 段较大的程序,文件体积却很小,就是这个原因。
像是在pwn里面我们经常(可能?)会碰到触发了溢出但是空间不够的问题,或者是存在canary,这时候我们可能就要做个栈迁移之类的操作。
具体操作:
第一次 leave; ret:设置 RBP 为目标地址,RSP 跟随到目标位置
第二次 leave; ret:将目标地址的 RBP 值弹出,RSP 指向布置好的 ROP 链,然后 ret 开始执行
这里的leave相当于mov rsp, rbp; pop rbp
而ret则是pop rip
这里要迁移到的目标地址则可以选择上述的.bss段,或者是堆,或者是其他缓冲区。
预告一下栈吧
栈是操作系统为每个线程分配的一块连续内存区域,先进后出。
数据 压栈(push) 时,RSP 减小(向低地址移动)
数据 弹栈(pop) 时,RSP 增大(向高地址移动)
“栈顶”实际上是地址更小的位置,在下面一篇再详细讲栈和寄存器,再到堆吧OVO
附:其他段
.init(初始化段),存放程序启动前需要执行的初始化代码(_init 函数)。可执行。C++ 中全局对象的构造函数、GCC 的 attribute((constructor)) 函数会在这里被调用。在 main 函数之前执行。
.fini(终结段),存放程序正常结束前需要执行的清理代码(_fini 函数)。可执行。C++ 中全局对象的析构函数、GCC 的 attribute((destructor)) 函数会在这里被调用。在 main 函数返回后或 exit() 调用时执行。
.eh_frame(异常处理帧),存放用于异常处理(C++ try/catch)和栈回溯(backtrace)的帧信息。只读。包含了函数栈帧的解码信息,用于在抛出异常时展开栈并找到对应的异常处理器。攻击者在构造 ROP 链时有时会利用这里的 gadget。
.dynamic(动态链接信息),存放供动态链接器使用的元数据。包含了依赖的共享库列表(DT_NEEDED)、.got / .plt 的位置、重定位表地址等。动态链接器在加载程序时会读取这个段来解析符号。
.symtab(符号表),存放函数名、全局变量名等符号信息。用于链接和调试。如果可执行文件没有被 strip,攻击者可以通过这里泄露函数地址,绕过 ASLR。通常发布版本会移除这个段以减少体积和暴露信息。
.strtab(字符串表),存放 .symtab 中符号名字对应的实际字符串,以 null 结尾的字符串连续排列。用于辅助符号表解析。
.shstrtab(节头部字符串表),存放每个段(section)名字的字符串(如 .text、.data 这个名字本身)。节头部表中的 sh_name 字段是这个表的索引。
.debug_(调试信息段),包括 .debug_info、.debug_line、.debug_abbrev 等。存放 DWARF 格式的调试信息,用于 GDB 等调试器进行源码级调试。只在开发版本中存在,发布版本通常会被 strip 掉。*
.comment(注释段),存放编译器版本信息(如 GCC: (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0)。不影响程序执行,仅用于标识工具链信息。
.interp(解释器段),存放动态链接器的路径(如 /lib64/ld-linux-x86-64.so.2)。只读。内核在加载 ELF 文件时会读取这个段,找到动态链接器并先执行它,再由动态链接器加载共享库并跳转到程序入口点。没有这个段的 ELF 是静态链接的可执行文件。
.note.ABI-tag(ABI 标记段),存放内核版本和 ABI(应用程序二进制接口)兼容性信息。用于告知操作系统需要的最低内核版本。Linux 内核会检查这个信息来决定是否允许运行该程序。
.hash / .gnu.hash(哈希表),存放符号的哈希表,用于动态链接器快速查找符号。.gnu.hash 是 GNU 扩展,比传统 .hash 效率更高。攻击者可以通过分析哈希表来泄露符号信息。