1
2
3
4
5
|
void evilfunction(char *input)
{
char buffer[512];
strcpy(buffer, input);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
0x08049166 <+0>: push ebp
0x08049167 <+1>: mov ebp,esp
0x08049169 <+3>: push ebx
0x0804916a <+4>: sub esp,0x204
0x08049170 <+10>: call 0x80491d5 <__x86.get_pc_thunk.ax>
0x08049175 <+15>: add eax,0x2e7f
0x0804917a <+20>: sub esp,0x8
0x0804917d <+23>: push DWORD PTR [ebp+0x8]
0x08049180 <+26>: lea edx,[ebp-0x208]
0x08049186 <+32>: push edx
0x08049187 <+33>: mov ebx,eax
0x08049189 <+35>: call 0x8049040 <strcpy@plt>
0x0804918e <+40>: add esp,0x10
0x08049191 <+43>: nop
0x08049192 <+44>: mov ebx,DWORD PTR [ebp-0x4]
0x08049195 <+47>: leave
0x08049196 <+48>: ret
|
函数序言
1
2
3
4
|
push ebp ; 保存上一个函数的 ebp
mov ebp, esp ; 设置当前函数的 ebp
push ebx ; 保存 ebx(被调用函数需要保存的寄存器)
sub esp, 0x204 ; 为局部变量分配栈空间(512 字节 buffer + 4 字节对齐)
|
push ebp
1
2
|
执行前: esp -> [返回地址]
执行后: esp -> [上一个函数的 ebp] -> [返回地址]
|
保存调用者的 ebp,为了稍后恢复栈帧。
- mov ebp, esp
现在 ebp = esp 当前值
1
2
3
4
5
6
|
栈布局:
[参数2] (ebp+12)
[参数1] (ebp+8) <- 函数的第1个参数
[返回地址] (ebp+4)
ebp -> [旧 ebp] (ebp+0)
...
|
- push ebx
ebx 是调用者需要保存的寄存器
1
2
3
4
5
6
7
|
执行后: [参数...]
[返回地址]
[旧 ebp]
esp -> [旧 ebx]
现在:ebp -> [旧 ebp] (ebp+0)
ebp-4 -> [旧 ebx] <- 这就是为什么最后要 mov ebx, [ebp-0x4]
|
- sub esp, 0x204
0x204 = 516 字节(十进制)
执行后:esp 下移 516 字节,为局部变量分配空间
1
2
3
4
5
6
7
8
|
栈布局现在是:
ebp+8 -> [参数1]
ebp+4 -> [返回地址]
ebp -> [旧 ebp]
ebp-4 -> [旧 ebx]
ebp-520 -> [局部变量区域开始] <- 这就是缓冲区!
...
esp -> [局部变量区域结束] <- 当前栈指针
|
序言转换为 c 语言为:
1
2
3
4
5
6
|
void evilfunction(char *src) {
// push ebp; mov ebp, esp; push ebx; sub esp, 0x204
// 相当于在栈上分配了 520 字节的本地缓冲区
char buffer[520]; // [ebp-0x208] 是缓冲区的开始
// ...
}
|
PIC(位置无关代码) 计算
1
2
3
|
call 0x80491d5 <__x86.get_pc_thunk.ax>
add eax,0x2e7f
sub esp,0x8
|
- call 0x80491d5 <__x86.get_pc_thunk.ax>
__x86.get_pc_thunk.ax 是一个特殊的函数,用于获取当前指令的地址(程序计数器 PC)。
objdump 反汇编如下:
1
2
3
|
080491d5 <__x86.get_pc_thunk.ax>:
80491d5: 8b 04 24 mov eax,DWORD PTR [esp]
80491d8: c3 ret
|
在 x86 调用约定中,函数调用时会用 call 指令将返回地址(即 call 指令下一条指令的地址)压入栈顶。此时 esp 指针正好指向这个返回地址,因为这是最近一次被压入栈的数据。这样函数执行完毕后,通过 ret 指令可以从 esp 指向的位置弹出返回地址,实现返回到调用点。
执行过程:
1
2
3
4
5
6
7
8
|
1. call 将下一条指令的地址 push 到栈上
push (0x08049175) <- 下一条 add 指令的地址
2. jmp 到 0x80491d5
在那里执行 mov eax, [esp]
eax = 0x08049175 (当前指令位置)
3. ret 弹出栈上的返回地址,返回到 0x08049175
|
执行后 eax 保存了当前指令地址 0x08049175。
- add eax,0x2e7f
目的:计算 [email protected] 的地址。
1
2
|
eax = 0x08049175 + 0x2e7f
= 0x0804aff4 (这就是 strcpy 在 GOT 表中的地址!)
|
- 因为这是 PIC 代码,基地址会变化(ASLR)
- 不能写死 GOT 地址要动态计算
- 0x2e7f 就是 GOT 地址相对于当前代码位置的偏移
执行后 eax = [email protected] 的地址
- sub esp, 0x8
为 strcpy 的两个参数预留栈空间
1
2
3
4
5
6
7
8
|
栈布局:
ebp+8 -> [参数1 (src)]
ebp+4 -> [返回地址]
ebp -> [旧 ebp]
ebp-4 -> [旧 ebx]
ebp-0x208 -> [缓冲区开始]
...
esp -> [预留的参数空间开始]
|
调用 strcpy
1
2
3
4
5
|
push DWORD PTR [ebp+0x8] ; 推送 src
lea edx,[ebp-0x208] ; 计算 dest
push edx ; 推送 dest
mov ebx,eax ; 保存 [email protected]
call 0x8049040 <strcpy@plt> ; 调用 strcpy
|
strcpy 声明:
1
|
char *stpcpy(char *restrict dest, const char *restrict src);
|
- push DWORD PTR [ebp+0x8]
[ebp+0x8] 是函数的第一个参数 input,即 src。
- 通常是一个字符串指针。
- 推送这个值作为 strcpy 的第二个参数。
1
2
3
4
|
栈布局:
...
[预留空间]
esp -> [src 地址] <- strcpy 的第 2 个参数
|
- lea edx,[ebp-0x208]
lea = Load Effective Address(加载有效地址)
- [ebp-0x208] 是局部缓冲区
buffer 的地址。(0x208 = 520 字节)
- 不是读取该地址的内容,而是计算地址本身
1
2
3
4
|
ebp-0x208 -> [缓冲区开始]
|
v
edx = ebp - 0x208 (缓冲区的地址)
|
执行后 edx = 局部缓冲区地址
- push edx
推送缓冲区地址作为 strcpy 的第一个参数 dest。
1
2
3
4
5
|
栈布局:
...
[预留空间]
[src 地址] <- strcpy 的第 2 个参数(源)
esp -> [dest 地址(缓冲区)] <- strcpy 的第 1 个参数(目标)
|
- mov ebx,eax
保存 [email protected] 的地址到 ebx。
PIC 代码需要保存地址,可能用于后续操作或恢复。
- call 0x8049040 strcpy@plt
1
2
3
4
5
|
// C 代码:
strcpy(dest, src);
// 其中:
// dest = [ebp-0x208] (缓冲区地址)
// src = [ebp+0x8] (函数参数)
|
- 当前指令地址 push 到栈上(返回地址)
- jmp 到 0x8049040 (strcpy@plt)
- strcpy 执行:
- 从 src 读取字符,写入 dest
- 直到遇到 ‘\0’ 为止
⚠️ 缓冲区溢出风险:
如果 src 指向的字符串长度 > 520 字节:
- 缓冲区[0x208]只能放 520 字节
- 多余的字节会溢出到栈上的其他数据
- 可能覆盖保存的 ebp、返回地址等
函数收尾
1
2
3
4
5
|
add esp,0x10 ; 清理栈
nop ; 无操作(填充)
mov ebx,DWORD PTR [ebp-0x4] ; 恢复 ebx
leave ; 恢复栈帧
ret ; 返回
|
- add esp,0x10
栈指针上移 16 字节,清理 strcpy 的两个参数。
x86 32 位 cdecl 调用约定中,调用者负责清理参数栈空间。
1
2
3
4
5
6
7
|
...
[返回地址]
[src]
栈前:esp -> [dest]
...
栈后:esp -> [返回地址]
|
- nop
无操作指令,通常用于:
- mov ebx,DWORD PTR [ebp-0x4]
恢复保存的 ebx 值。
ebp-0x4 存储的是函数开始时保存的旧 ebx现在读出来,恢复 ebx 的值
ebx 是被调用函数需要保存的寄存器,必须恢复原值以保证调用者的状态不被破坏。
- leave
等价于:
1
2
|
mov esp, ebp ; 栈指针恢复到 ebp
pop ebp ; 恢复上一个函数的 ebp
|
执行后:
1
2
|
esp -> [返回地址]
ebp = 调用者的 ebp
|
- ret
含义:
1
|
pop eip ; 从栈上弹出返回地址到 eip
|
返回到调用者。
完整的栈帧变化过程
函数开始前
1
2
3
|
[调用者的旧数据...]
[参数 src]
esp -> [返回地址]
|
函数序言后
1
2
3
4
5
6
|
ebp -> [旧 ebp]
ebp-4 -> [旧 ebx]
...
ebp-0x208 -> [缓冲区 buffer[520]]
...
esp -> [栈顶(局部变量区)]
|
调用 strcpy 前
1
2
3
4
|
...
[缓冲区...]
[dest 地址(参数1)]
esp -> [src 地址(参数2)]
|
函数结束后
1
2
3
|
esp -> [返回地址] <- 准备执行 ret
ebp = 原调用者的 ebp
ebx = 原 ebx 值
|