
本片虽然是讲格式化字符串漏洞但是涉及到了栈更高地址的讲解相信你看完本片会发现‘哇原来栈上面是这么的神奇’除此之外还会了解更多关于二进制文件启动进程的知识话不多说我们开始格式化字符串漏洞的成因printf(x) printf(%d,x)printf不检查前面的这些格式串和后面给的这些参数是否相等printf只知道遇到了%d%s%p这种就要去相应的位置去取值那么相应位置是什么呢这里我们要说说函数参数的储存位置了这个知识点可以去看我的另一篇博客32位与64位传参的不同https://blog.csdn.net/2403_87702597/article/details/166638258?spm1001.2014.3001.5501这里我们值得提醒的是系统调用和函数调用用的不是一套逻辑好了我们接着开始printf(“%d”,x,y,z,c)我们知道%d%s取的是格式串后面的参数,x,y,z,c那么我们如果没有后面的这些参数呢,本质是取的是固定位置的数x,y,z,c只是因为这些参数把值赋给了指定的位置printf(fmt, a1, a2, a3, a4, a5, a6, a7,...)rdi rsi rdx rcx r8 r9 │ │ │ │ │ │ │ │ │1$2$3$4$5$ 栈[0]栈[8]6$7$ 格式串自身不占编号//格式串说的是rdi这样就好说了本质是取这些寄存器的值参数的作用只是赋值给这些寄存器%N$p,%N$s这种都不陌生吧那么当我们突破了r9之后再忘下就是栈的数据了这样就能泄漏栈上面的数据此上就是格式化字符串漏洞的原理还有很多高级利用我们在后面的格式化字符串讲解这样大家一篇博客看着也轻松一点点┌───────────┬──────────────────────────────┬───────────────────────────┬───────────────────────────┐ │ 目标 │ 手段 │ 特征 │ 拿到后能干什么 │ ├───────────┼──────────────────────────────┼───────────────────────────┼───────────────────────────┤ │ canary │ %N$p│ 末尾 00如 0xa3ef...5600 │ 绕过栈保护回头打栈溢出 │ ├───────────┼──────────────────────────────┼───────────────────────────┼───────────────────────────┤ │ PIE 基址 │ %N$p读返回地址 │ 0x55... / 0x56... │ 算出程序内任意符号的地址 │ ├───────────┼──────────────────────────────┼───────────────────────────┼───────────────────────────┤ │ libc 基址 │ %N$p读 __libc_start_mainxx │ 0x7f... │ 算出 system / one_gadget │ ├───────────┼──────────────────────────────┼───────────────────────────┼───────────────────────────┤ │ 栈地址 │ %N$p读 saved rbp │ 0x7ffd... │ 定位自己的 payload 在哪 │ ├───────────┼──────────────────────────────┼───────────────────────────┼───────────────────────────┤ │ 堆地址 │ %N$p读堆指针 │ — │ 堆利用 │ ├───────────┼──────────────────────────────┼───────────────────────────┼───────────────────────────┤ │ 任意内存 │ %N$s 把地址放到栈上 │ — │ 读 flag / 密钥 / 环境变量 │ └───────────┴──────────────────────────────┴───────────────────────────┴───────────────────────────┘这些都是后面可以遇到的题型后面在栈题目集里面都会更新的重头戏–更深的格式化我们泄漏出来的都是什么我们先放图╔════════╦════════════════════════╦═══════════════════════════════════════════════╗ ║ 槽号 ║ 值 ║ 这一格是什么 ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ ║ ║ ▓▓▓ 第一区 · 寄存器printf 最先从这里取5个 ▓▓▓ ║ ║ 走 rsi rdx rcx r8 r9。是上一次函数调用的残值我们控制不了 ║ ║ ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ %1$ ║ 0x7ffd2c7c5150 ║ 残留 rsi ── 一个栈地址 ║ ║ %2$ ║ 0x30 ║ 残留 rdx ── 小整数 ║ ║ %3$ ║ 0x7fc4cbaee932 ║ 残留 rcx ── libc 里的地址 ║ ║ %4$ ║ 0x6 ║ 残留 r8 ── 小整数 ║ ║ %5$ ║ 0x7fd909678040 ║ 残留 r9 ── libc 数据段 ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ ║ ║ ▓▓▓ 第二区 · main 的栈帧 ▓▓▓ ║ ║printf从[rsp]开始走栈。★buf 恰好printf调用时的 rsp ★ ║ ║ 所以 buf 的字节本身就是 %6$ %7$ %8$ …… 就是参数槽║ ║ ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ %6$ ║ 0x7f0a70243625 ║ ███ 我们的输入%6$p\n的原始字节 ███ ║ ║ %7$ ║ 0x10101000000 ║ main 栈帧残留 ║ ║ %8$ ║ 0x2 ║ main 栈帧残留 ║ ║ %9$ ║ 0x78bfbff ║ main 栈帧残留 ║ ║ %10$ ║ 0x7fff0362a159 ║ 栈地址指向字符串区某处 ║ ║ %11$ ║ 0x64 ║100║ ║ %12$ ║ 0x1000 ║4096║ ║ %13$ ║ 0xdcb396301d527200 ║ ★ main 的 canary末尾 00 ║ ║ %14$ ║ 0x1 ║ ║ ║ %15$ ║ 0x7f3f80d7ed90 ║ libc 里的地址 ║ ║ %16$ ║(nil)║ ║ ║ %17$ ║ 0x5572a5e7f1c9 ║ ★ main 的返回地址 → PIE 基址 ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ ║ ║ ▓▓▓ 第三区 · __libc_start_main / ld.so 的栈帧 ▓▓▓ ║ ║printf踩过别人的栈帧。所以这里全是代码指针和残渣 ║ ║ ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ %18$ ║ 0x19c09a3f0 ║ 残渣 ║ ║ %19$ ║ 0x7ffdf4e7c998 ║ 栈地址 ║ ║ %20$ ║(nil)║ ║ ║ %21$ ║ 0x587ded15da13e811 ║ 随机值 ║ ║ %22$ ║ 0x7ffee1a74628 ║ 栈地址 ║ ║ %23$ ║ 0x55fca6a081c9 ║ PIE 里的代码地址 ║ ║ %24$ ║ 0x56338413ada0 ║ PIE 里的数据地址 ║ ║ %25$ ║ 0x7f6e6ffe9040 ║ libc 里的地址 ║ ║ %26$ ║ 0x7555d74e0c4853b2 ║ 随机值 ║ ║ %27$ ║ 0x9211542207d63f16 ║ 随机值 ║ ║ %28$ ║ 0x7f0300000000 ║ 残渣 ║ ║ %29$ ║(nil)║ ║ ║ %30$ ║(nil)║ ║ ║ %31$ ║(nil)║ ║ ║ %32$ ║(nil)║ ║ ║ %33$ ║ 0x412fca4774980c00 ║ canary另一层栈帧的末尾 00 ║ ║ %34$ ║(nil)║ ║ ║ %35$ ║ 0x7f8a51bc0e40 ║ libc 里的地址 ║ ║ %36$ ║ 0x7ffec1cd9808 ║ 栈地址 ║ ║ %37$ ║ 0x55c511c9dda0 ║ PIE 里的数据地址 ║ ║ %38$ ║ 0x7f55072132e0 ║ libc 里的地址 ║ ║ %39$ ║(nil)║ ║ ║ %40$ ║(nil)║ ║ ║ %41$ ║ 0x558260c780e0 ║ PIE 里的数据地址 ║ ║ %42$ ║ 0x7fff7a36dae0 ║ 栈地址 ║ ║ %43$ ║(nil)║ ║ ║ %44$ ║(nil)║ ║ ║ %45$ ║ 0x55ba1a7d7105 ║ PIE 里的代码地址 ║ ║ %46$ ║ 0x7ffdd3668b18 ║ 栈地址 ║ ║ %47$ ║ 0x1c(28)║ 栈上遗留值 ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ ║ ║ ▓▓▓ 第四区 · 内核铺的启动数据区 ▓▓▓ ║ ║ 距 buf 只有344字节。顺序严格是: ║ ║ argc → argv[]NULL → envp[]NULL → auxv →(字符串区)║ ║ ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ %48$ ║ 0x1 ║ ★★★ argc1【整片启动区的坐标原点】 ║ ║ %49$ ║ 0x7ffd7b733fa1 ║ ★★ argv[0]──►./pwn2║ ║ %50$ ║(nil)║ ★ argv 的 NULL 终止符argc1所以只有1项 ║ ║ %51$ ║ 0x7fff45baefa8 ║ ★★★ envp[0]──►FLAGPKWCTF{543fe185-…}║ ║ %52$ ║ 0x7fffe4c80fda ║ ★★ envp[1]──►REMOTE_HOST10.10.0.19║ ║ %53$ ║(nil)║ ★ envp 的 NULL 终止符 → 只有2个环境变量 ║ ╟────────╫────────────────────────╫───────────────────────────────────────────────╢ ║ %54$ ║ 0x21(33)║ auxv[0]类型 AT_SYSINFO_EHDR ║ ║ %55$ ║ 0x7ffe1e1e8000 ║ auxv[0]值 → vDSO 基址 ║ ║ %56$ ║ 0x33(51)║ auxv[1]类型 AT_MINSIGSTKSZ ║ ║ %57$ ║ 0x6f0 ║ auxv[1]值 ║ ║ %58$ ║ 0x10(16)║ auxv[2]类型 AT_HWCAP ║ ║ %59$ ║ 0x78bfbff ║ auxv[2]值 ║ ║ %60$ ║ 0x6 ║ auxv[3]类型 AT_PAGESZ ║ ║ %61$ ║ 0x1000 ║ auxv[3]值4096║ ║ %62$ ║ 0x11(17)║ auxv[4]类型 AT_CLKTCK ║ ║ %63$ ║ 0x64 ║ auxv[4]值100║ ║ %64$ ║ 0x3 ║ auxv[5]类型 AT_PHDR ║ ║ %65$ ║ 0x55e853789040 ║ auxv[5]值 → 程序头表 → ★ PIE 基址 ║ ║ %66$ ║ 0x4 ║ auxv[6]类型 AT_PHENT ║ ║ %67$ ║ 0x38 ║ auxv[6]值56║ ║ %68$ ║ 0x5 ║ auxv[7]类型 AT_PHNUM ║ ║ %69$ ║ 0xd ║ auxv[7]值13║ ║ %70$ ║ 0x7 ║ auxv[8]类型 AT_BASE ║ ║ %71$ ║ 0x7f18e60b3000 ║ auxv[8]值 → ★ ld.so 基址 ║ ║ %72$ ║ 0x8 ║ auxv[9]类型 AT_FLAGS ║ ║ %73$ ║(nil)║ auxv[9]值0║ ║ %74$ ║ 0x9 ║ auxv[10]类型 AT_ENTRY ║ ║ %75$ ║ 0x5593680330e0 ║ auxv[10]值 → 程序入口点 ║ ║ %76$ ║ 0xb(11)║ auxv[11]类型 AT_UID ║ ║ %77$ ║ 0x3e8 ║ auxv[11]值1000║ ║ %78$ ║ 0xc(12)║ auxv[12]类型 AT_EUID ║ ║ %79$ ║ 0x3e8 ║ auxv[12]值1000║ ║ %80$ ║ 0xd(13)║ auxv[13]类型 AT_GID ║ ║ %81$ ║ 0x3e8 ║ auxv[13]值1000║ ║ %82$ ║ 0xe(14)║ auxv[14]类型 AT_EGID ║ ║ %83$ ║ 0x3e8 ║ auxv[14]值1000║ ║ %84$ ║ 0x17(23)║ auxv[15]类型 AT_SECURE ║ ║ %85$ ║(nil)║ auxv[15]值0║ ║ %86$ ║ 0x19(25)║ auxv[16]类型 AT_RANDOM ║ ║ %87$ ║ 0x7ffe4a1bb229 ║ auxv[16]值 → ★ canary 种子 ║ ║ %88$ ║ 0x1a(26)║ auxv[17]类型 AT_HWCAP2 ║ ║ %89$ ║ 0x2 ║ auxv[17]值 ║ ║ %90$ ║ 0x1f(31)║ auxv[18]类型 AT_EXECFN ║ ║ %91$ ║ 0x7ffcff758ff1 ║ auxv[18]值 → ★ 真实可执行文件路径 ║ ║ %92$ ║ 0xf(15)║ auxv[19]类型 AT_PLATFORM ║ ║ %93$ ║ 0x7ffe8f0ee6c9 ║ auxv[19]值 → 指向x86_64║ ║ %94$ ║(nil)║ auxv[20]类型 AT_NULL —— auxv 数组到此结束 ║ ╟────────╫────────────────────────╫───────────────────────────────────────────────╢ ║ %95$ ║(nil)║ AT_NULL 的值0║ ║ %96$ ║(nil)║ 对齐填充 ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ ║ ║ ▓▓▓ 第五区 · 字符串区正文 ▓▓▓ ║ ║ 上面所有指针的【指向目标】。flag 的真正字节就在这一区的某处 ║ ║(注意: 槽号到 %100$ 只是扫到的范围, 字符串区其实还长得多)║ ║ ║ ╠════════╬════════════════════════╬═══════════════════════════════════════════════╣ ║ %97$ ║ 0x540f116cf0cb7200 ║ 字符串区的字节 ║ ║ %98$ ║ 0xce302182ae3e99bb ║ 字符串区的字节 ║ ║ %99$ ║ 0x34365f3638786f ║ 字符串区的字节 → ASCIIox86_64║ ║ %100$ ║(nil)║ 字符串区 / 对齐 ║ ╚════════╩════════════════════════╩═══════════════════════════════════════════════╝第一区我们的寄存器这就不必说了第二区这里其实是栈溢出的常覆盖的地方对吧是bufcanaryrbpreturn┌──────────────────────────────────────┐ 高地址 │ %17$ main 的返回地址 → PIE 基址 │ ├──────────────────────────────────────┤ │ %14$~%16$ main 的其他局部变量 │ ├──────────────────────────────────────┤ │ %13$ ★ canary │ ├──────────────────────────────────────┤ │ %7$~%12$ buf 的后续 相邻局部 │ ├──────────────────────────────────────┤ │ %6$ ███ buf[0..7]我们的输入 ███ │ ← rsp 就在这 └──────────────────────────────────────┘ 低地址第三区这个区块不太重要里面全是“代码指针和残渣”里面有栈地址pie地址canarylibc地址但是不好利用因为我们并不知道偏移第四块argcargvenvpauxv这几个是有联系的我们先分开讲在说联系argc----参数计数器定义 - 一个 int4 字节数的是 argv 里有几项用途 - 告诉 main “你有几个参数”。for (i 0; i argc; i) 就靠它至于这个main的参数我们后面的联系里面会讲到价值 - 坐标原点——用它才能算出 envp 从哪开始它是唯一可靠的参数个数来源argv[0] 可以是假的, argv 数组本身也可能被玩坏,但 argc 是内核【当场数】出来的argv----命令行参数定义 - 指针数组每项指向字符串区里一个字符串NULL 收尾就是多一个 NULL 元素用途 - 把命令行的词传给程序。./pwn2 -f file.txt里的./pwn2-ffile.txt就在这价值 - argv[0] 是情报有些题把 flag 当参数传;我们就能用%S获取里面存的 这些都在字符串区 都是地址 ┌──────────┬────────────────────┐ │ argv[0]│ ──►./pwn2│ 程序名可伪造 │ argv[1]│ ──►-f│ 真正的参数 │ argv[2]│ ──►file.txt│ │ NULL │ │ ← 遍历到此结束 └──────────┴────────────────────┘ 项数argc 槽数argc 1多一个 NULLenv----环境变量定义 - 指针数组每项指向 “KEYVALUE” 格式的字符串NULL 收尾用途 - 把环境变量传给程序。getenv(“HOME”) 的内部实现就是遍历这个数组价值 - flag 常在这里。也是平台注入东西的唯一通道┌──────────┬──────────────────────────────────┐ │ envp[0]│ ──►FLAGPKWCTF{543fe185-…}│ ★★★ │ envp[1]│ ──►REMOTE_HOST10.10.0.19│ │ NULL │ │ ← 遍历到此结束 └──────────┴──────────────────────────────────┘ ★ 关键: envp 是【调用者100% 控制】的 平台启动容器时: execve(/pwn2, argv,[FLAGPKWCTF{...}, NULL])└────────┬────────┘ 平台自己写的 容器里跑的任何程序, 环境里都带着这个 FLAGauxv----全都是程序的一些必备项定义 - (类型, 值) 成对的整数数组AT_NULL 收尾用途 - ld.so 自举时问内核我脚下这台机器什么样,通俗一点就是程序启动时的初始化价值 - 送 PIE 基址、ld.so 基址、canary 种子、真实路径┌────────────┬──────────────┬─────────────────────────┐ │ 类型 │ 值 │ 回答了什么问题 │ ├────────────┼──────────────┼─────────────────────────┤ │3AT_PHDR │ PIE 里的地址 │程序头表在哪?→ PIE基址 │ │6PAGESZ │ 0x1000 │页多大?│ │7AT_BASE │ 0x7f... │ld.so 装在哪?│ │9ENTRY │ 0x55... │从哪开始跑?│ │25RANDOM│ 栈地址 │随机数种子在哪?│ │31EXECFN │ 栈地址 │我真名叫什么?真话 │ │15PLATFORM│ 栈地址 │什么架构?→x86_64│ │0AT_NULL │0│问完了│ └────────────┴──────────────┴─────────────────────────┘现在来说说他们的联系物理上物理上连续内存只是排了队 低地址 ┌──────────┐ │ argc │ %48$ ├──────────┤ │ argv[0]│ %49$ ┐ │ NULL │ %50$ ┘ ├──────────┤ │ envp[0]│ %51$ ┐ │ envp[1]│ %52$ │ │ NULL │ %53$ ┘ ├──────────┤ │ auxv[0]类型│ %54$ ┐ │ auxv[0]值 │ %55$ │ │... │ │ │ AT_NULL │ %94$ ┘ ├──────────┤ │ 填充 │ %95$ %96$ └──────────┘ 高地址 ★ 四块【紧挨着】, 中间没有任何空隙。这就是槽号连续递增的原因。逻辑· 逻辑上一条链一环扣一环 argc ──决定──► argv 有几项 │ │ envp[0]槽号argv[0]槽号 argc 1▼ argv[]──遍历到 NULL──► 结束 │ │ 紧接着地址上 ▼ envp[]──遍历到 NULL──► 结束 │ │ 紧接着地址上 ▼ auxv ──遍历到 AT_NULL──► 结束语义上┌────────────────────────────────────────────────────┐ │ argc / argv我是谁, 谁派我来的│ │ → 这次任务的具体指令 │ │ → 最贴近【这次调用】 │ ├────────────────────────────────────────────────────┤ │ envp我生活在什么环境里│ │ → 长期配置, 平台注入的东西在这 │ │ → 最贴近【这台机器/这个平台】 │ ├────────────────────────────────────────────────────┤ │ auxv我脚下的硬件和内核什么样│ │ → 与具体任务、具体环境都无关 │ │ → 最贴近【这台机器本身】 │ └────────────────────────────────────────────────────┘总结总结表 ┌────────┬──────────────┬──────────┬────────────┬──────────────────┬────────────────┬─────────────────────────┐ │ 块 │ 元素形态 │ 结束标志 │ 谁填的 │ 谁读它 │ 设计用途 │ 攻击价值 │ ├────────┼──────────────┼──────────┼────────────┼──────────────────┼────────────────┼─────────────────────────┤ │ argc │ 一个 int │ — │ 内核数出来 │ main │ 循环遍历 argv │ ★★ 坐标原点 │ ├────────┼──────────────┼──────────┼────────────┼──────────────────┼────────────────┼─────────────────────────┤ │ argv[]│ 指针数组 │ NULL │ 调用者 │ main / getopt │ 传命令行参数 │ ★★ 情报 / 参数里的 flag │ ├────────┼──────────────┼──────────┼────────────┼──────────────────┼────────────────┼─────────────────────────┤ │ envp[]│ 指针数组 │ NULL │ 调用者 │ getenv / environ │KEYVALUE 环境│ ★★★ flag │ ├────────┼──────────────┼──────────┼────────────┼──────────────────┼────────────────┼─────────────────────────┤ │ auxv │(类型,值)对 │ AT_NULL │ 内核 │ ld.so / libc │ 机器说明书 │ ★★ 基址 / 种子 / 真话 │ └────────┴──────────────┴──────────┴────────────┴──────────────────┴────────────────┴─────────────────────────┘自测argv 有计数器 argc为什么 envp 没有 envc 内核为什么不多给一个公式 envp[0] argv[0] argc 1。那个 1 是加什么 如果 argc 0理论上会怎样argv 用 NULL 收尾auxv 用 AT_NULL 收尾。为什么 auxv 不能用 NULL提示auxv 的元素是什么形态三层语义里顺序是 argv → envp → auxv。如果我把顺序倒过来auxv 在最下%N$ 的定位会怎么变对我们打 flag 有好处还是坏处envp 和 auxv 里都有指向字符串区的指针。argv 里的指针指向哪儿 三者的字符串是不是混在同一片区域里┌─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ 答案一句话 │ ├─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │1│ argc 是1979年的历史包袱 argv 需要遍历envp 后补的且只做按名查找用 NULL 收尾就够 │ ├─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │2│ 1 是 argv 的 NULL 终止符argc0做不到——内核会补一个空串静态链接实测同理 │ ├─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │3│ auxv 装的是整数对0 是合法数据AT_FLAGS0不能用 NULL 收尾必须用专门的 AT_NULL │ ├─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │4│ 顺序内核写死改不了假设倒过来41 格的 auxv 会把 envp 从 %51$ 顶到 %90$对我们不利 │ ├─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │5│ 是同一片区域但分两撮大撮argvenvpexecfn 连续小撮AT_PLATFORM/AT_RANDOM 贴 auxv字符串区是连续的读到一个附近就是别的 │ └─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘第五区----字符串区定义 - 内核在 execve 时把所有文本摞到栈的最顶上形成的一片连续区域。程序还没执行第一条指令它就已经在那儿了。用途 - 用 env -i PATH/bin FLAG… USERctf ./strdump hello world 跑出来的全部内容字符串区内容按地址从小到大0x7fff8c4cd859 AT_PLATFORMx86_640x7fff8c4cdfae argv[0]./strdump0x7fff8c4cdfb8 argv[1]hello0x7fff8c4cdfbe argv[2]world0x7fff8c4cdfc4 envp[0]PATH/bin0x7fff8c4cdfce envp[1]FLAGPKWCTF{demo_flag}0x7fff8c4cdfe5 envp[2]USERctf0x7fff8c4cdfee AT_EXECFN./strdump┌─────────────┬──────────────────────────────────────────────────┬──────────────────────┐ │ 来源 │ 字符串 │ 谁指向它 │ ├─────────────┼──────────────────────────────────────────────────┼──────────────────────┤ │ argv │./strdumphelloworld│ %49$ %50$... 那些槽 │ ├─────────────┼──────────────────────────────────────────────────┼──────────────────────┤ │ envp │PATH/binFLAG...USERctf│ envp[]的槽 │ ├─────────────┼──────────────────────────────────────────────────┼──────────────────────┤ │ AT_EXECFN │./strdump和 argv[0]内容相同但是两份拷贝 │ %91$ │ ├─────────────┼──────────────────────────────────────────────────┼──────────────────────┤ │ AT_PLATFORM │x86_64│ %93$ │ ├─────────────┼──────────────────────────────────────────────────┼──────────────────────┤ │ AT_RANDOM │16字节随机数不是文本 │ %87$ │ └─────────────┴──────────────────────────────────────────────────┴──────────────────────┘特征 - 一大片绝大部分是空的为什么会有这一段空白内核是【分两批】放的:批一: 调用者传进来的 (argv 字符串 envp 字符串 execfn)批二: 内核自己生成的 (“x86_64” 16 字节随机数)两批各自占据栈顶的一段, 中间就留出了这片 NUL 填充。所以字符串区不是一个整块是两个簇高地址 ┌────────────────────────────────────────────┐ │ ██ 主字符串块 ██ │ │ argv 字符串 → envp 字符串 → execfn 字符串 │ ← 紧密相连只有\0分隔 ├────────────────────────────────────────────┤ │1870字节 NUL 填充 │ ├────────────────────────────────────────────┤ │ ██ 内核字符串簇 ██ │ │x86_6416字节随机数 │ ├────────────────────────────────────────────┤ │ 对齐填充 │ ├────────────────────────────────────────────┤ │ auxv[]│ │ envp[] NULL │ │ argv[] NULL │ │ argc │ └────────────────────────────────────────────┘ 低地址和前面四块的关系┌────────────────────────────────────────────────────────────┐ │ argc → 数字, 谁都不指 │ │ argv[]→ 指针 ──┐ │ │ envp[]→ 指针 ──┼──► 全部指向【字符串区】 │ │ auxv → 少数几项是指针 ──┘ │ └────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────┐ │ 字 符 串 区 │ │ argv 字符串 / envp 字符串 /x86_64/ 真实路径 / 随机数 │ └────────────────────────────────────────────────────────────┘概述┌──────────────────────────────────────────────────────────────┐ │ 字符串区内核在 execve 时, 把所有文本摞到栈顶形成的一片 │ │ │ │ 前四块是【目录】(指针/数字), 只有它是【正文】(字节)│ │ │ │ 里面只有: argv 字符串、envp 字符串、真实路径、x86_64、随机数 │ │ 排列: argv → envp → execfn 连续;platform/random 单独在低处 │ │ 特征:96% 是 NUL 填充, 有效内容紧密相连 │ │ │ │ 对攻击者: ★★★ 唯一能直接读到文本的地方 —— flag 就在这里 │ └──────────────────────────────────────────────────────────────┘单独部分说完了我们来结合程序说一下我们启动这个二进制程序到开启进程 你想想我们是什么命令呢./pwn2 hello world arg2接下来回进行什么呢① shell 分词(word splitting)← argc 在这里定下来, 不在程序里 ▼[./pwn2][hello world][arg2]│ ② shell 组装 ▼ argv[./pwn2,hello world,arg2,NULL]envp[PATH...,FLAG...,NULL]│ ③ execve(./pwn2, argv, envp)← C 函数, 固定3个参数 ▼ ④ 内核: 按 path 加载文件 把字符串抄到栈顶 在下面摆指针数组 argcargv 中非 NULL 元素个数 ▼ ⑤ main(argc, argv)execve 的两层最易混淆intexecve(constchar*path,char*constargv[],char*constenvp[]);// 第1个 第2个 第3个问题答案execve 有几个参数永远 3 个argv 是 execve 的什么它的第 2 个参数一个数组argc 数的是谁argv 数组里非 NULL 元素的个数与 execve 参数个数无关./pwn2算 execve 参数吗算 —— 它是path./pwn2算 argv 里的吗也算 —— 它是argv[0]这两处必须一样吗不必可以完全不同实测反例execve 恒 3 个参数argc 随词数变./echoargs - argc 1 ./echoargs a - argc 2 ./echoargs a b c - argc 4 ./echoargs a b c d e - argc 6path 与 argv[0] 解耦实测execve(/tmp/echoargs,{我是假名字,参数1,NULL},envp);// 程序看到: argv[0] 我是假名字// 但 AT_EXECFN /tmp/echoargs ← 内核记的真话shell 等价写法(exec -a 假名字 /tmp/echoargs 参数1)2. shell 分词规则规则说明空格 / Tab分隔符连续多个只算一个引号分组引号本身不进入参数...保留空格$变量仍展开...一切原封不动$变量也不展开\转义下一个字符* ? []由 shell 展开成多个词argc 会变加引号可禁止实测对照命令程序收到的 argvargc./args[./args]1./args a b c[./args,a,b,c]4./args hello world[./args,hello world]2./args hello world[./args,hello world]2./args hello\ world[./args,hello world]2./args [./args,]2./args ab cd[./args,ab cd]2./args $HOME[./args,/home/lpzn]2./args $HOME[./args,$HOME]2./args a b[./args,a,b]3./args a b[./args,a b]2./args *.txt[./args,a.txt,b.txt,c.txt]4./args *.txt[./args,*.txt]2要点引号是shell 的语法不是数据。程序永远看不到引号。空参数 ≠ 没有参数产生一个长度 0 的真实参数能骗过argc N的检查。绕过 shell 直接execve时没有任何东西拦截; | $等字符。execve(path, NULL, envp)实测得到argc1, argv[0]argc 拿不到 0。好了到这我们就对整个程序的内存有一定的认识了接下来的高级字符串漏洞用法见栈题目专栏-----点个小赞谢谢啦你的支持是我最大的动力