有时候程序并没有直接提供返回
/bin/sh的函数地址可供覆盖,这时候我们就需要去libc.so(C 语言的标准动态链接库)来找我们需要的函数。libc.so(C 语言的标准动态链接库)
- 概念:它是 C 语言的标准动态链接库。里面包含了成千上万个编译好的函数实体代码,比如
printf、read、puts,以及后门函数system。 - 物理特性:它是一个极其庞大的独立文件
ASLR 机制:程序运行时,操作系统为了防攻击,会把
libc.so加载到内存中的一个完全随机的基地址,导致我们不能简单的去泄漏想要的函数。GOT 表
- 概念:全局偏移表。它存在于程序(ELF文件)内部,本质上是一个指针数组(一排内存格子)。
- 作用:专门用来存放
libc.so中各个函数的真实、绝对内存地址。 - 物理特性:它被分配在程序的数据段。这意味着它是可读、可写的。
结论:只要能查到 GOT 表里的值,就能破解 ASLR,泄露地址。
PLT 表
- 概念:过程链接表。它也存在于你的程序内部,是一排微小的代码片段(汇编指令)。
- 作用:因为你的代码不知道
libc.so的地址,所以当代码想调用printf时,它不能直接call printf,而是call printf@plt。PLT 表的作用就是去读取 GOT 表,然后跳过去。 - 物理特性:它被分配在程序的代码段。这意味着它是只读、可执行的,而且在没有开启 PIE 保护的程序中,PLT 表的地址是固定死不变的。
Lazy Binding机制
阶段一:第一次调用 printf(
- 代码跳转:主程序执行了一句
call printf@plt,跳到了 PLT 表里属于printf的那个小蹦床。 - 查看通讯录:PLT 蹦床的汇编指令会去读
printf@got这个格子,想拿到真实地址。 - 发现没写:因为是第一次运行,GOT 表里还没有填入真实的
libc地址,里面填的是一个指回 PLT 表的“默认假地址”。 - 呼叫系统解析:顺着这个假地址,程序跳到了系统的动态解析器(
ld.so)。解析器去茫茫内存中找到了libc.so里printf的真实物理地址。 - 写入 GOT 表(关键):解析器不仅把程序引导到了
printf去执行,还顺手把刚才找到的真实物理地址,死死地刻在了printf@got这个格子里。
阶段二:第二次及以后调用 printf(光速直达)
- 代码跳转:主程序再次
call printf@plt。 - 查看通讯录:PLT 蹦床再次去读
printf@got这个格子。 - 直接起飞:这一次,GOT 表里已经赫然写着真实的绝对地址了!PLT 拿到地址,直接跳转到
libc.so内部执行,再也不用找解析器了。
这里贴一道pwn的栈结构作为备忘
==== 低地址 (Low Address) | 栈顶方向 ====
|
| <-- 你的输入 (buf) 从这里开始写入,像倒水一样往下流
V
+---------------------+ <-- ebp - 0x70
| |
| buf[100] | (这里足足有 100 个字节的空间)
| | (Payload 第一部分:b'A' * 100,刚好填满这个大坑)
| |
+---------------------+ <-- ebp - 0x0C
| Canary (v3) | (金丝雀,占 4 个字节)
| | (Payload 第二部分:p32(canary),悄悄把真密码放回去)
+---------------------+ <-- ebp - 0x08
| |
| 对齐缝隙 / 杂物区 | (编译器为了对齐留下的空白,占 8 个字节)
| |
+---------------------+ <-- ebp + 0x00
| 旧 EBP (Saved EBP) | (保存着上一个函数的抽屉底,占 4 个字节)
| | (Payload 第三部分:b'B' * 12,无情碾压过缝隙和旧EBP)
+---------------------+ <-- ebp + 0x04
| 返回地址 | (也就是 Return Address,占 4 个字节)
| | (Payload 第四部分:p32(system_addr))
+---------------------+ <-- ebp + 0x08
| 假的返回地址 | (system 执行完去哪?我们随便填 b'AAAA')
+---------------------+ <-- ebp + 0x0C
| 传给 system 的参数 | (也就是 p32(binsh_addr),指引它去执行 "/bin/sh")
+---------------------+
|
==== 高地址 (High Address) | 栈底方向 ====
评论已关闭