课程
Answer
CSAPP P.197 计算机系统基础---3.9 越界访问和缓冲区溢出 - 知乎
- 以下是函数 getline 存在漏洞和问题的 C 语言代码实现,右边是其对应的反汇编部
char *getline()
{
char buf [8] ;
char *result ;
gets(buf) ;
result = malloc( strlen (buf) ) ;
strcpy(result, buf) ;
return result ;
}
1 0804840c <getline>:
2 804840c: 55 push %ebp
3 804840d: 89 e5 mov %esp,%ebp
4 804840f: 83 ec 28 sub $0x28,%esp
5 8048412: 89 5d f4 mov %ebx,-0xc(%ebp)
6 8048415: 89 75 f8 mov %esi,-0x8(%ebp)
7 8048418: 89 7d fc mov %edi,-0x4(%ebp)
8 804841b: 8d 75 ec lea -0x14(%ebp),%esi
9 804841e: 89 34 24 mov %esi,(%esp)
10 8048421: e8 a3 ff ff ff call 80483c9 <gets>
假定过程P调用了函数getline,其返回地址为0x80485 c8,为调用getline函数而执行call指令时,部分寄存器的内容如下: R [ebp] = 0xbffc 0800,R [esp] = 0xbffc 07f0,R [ebx] = 0x5,R[esi] = 0x10,R[edi] = 0x8。 执行程序时从标准输入读入的一行字符串为“0123456789 abcdef0123456789\n”, 此时,程序会发生段错误(segmentation fault)并终止执行,经调试确认错误是在执行getline的ret指令时发生的。 回答下列问题或完成下列任务。
(1)画出执行第7行指令后栈中的信息存放情况。要求给出存储地址和存储内容,并指出存储内容的含义(如返回地址、EBX旧值、局部变量、入口参数等)。
(2)画出执行第10行指令并调用gets函数后回到第10行的下一条指令执行时栈中的信息存放情况。
(3)当执行到getline的ret指令时,假如程序不发生段错误,则正确的返回地址是什么?发生段错误是因为执行getline的ret指令时得到了什么样的返回地址?
(4)执行完gets函数后,哪些寄存器的内容已被破坏?
(5)除了可能发生缓冲区溢出以外,getline的C代码还有哪些错误?
Answer
line 7
| 地址 | 内容 | esp | ebp | 备注 |
|---|---|---|---|---|
| bf fc 08 04 | ||||
| bf fc 08 00 | 程序开始时 | |||
| bf fc 07 fc | ||||
| bf fc 07 f8 | ||||
| bf fc 07 f4 | ||||
| bf fc 07 f0 | 08 04 85 c8 | 程序开始时 | 返回地址 | |
| bf fc 07 ec | bf fc 08 00 | line2后 | line3后 | 旧的ebp值 |
| bf fc 07 e8 | 00 00 00 08 | 旧edi | ||
| bf fc 07 e4 | 00 00 00 10 | 旧esi | ||
| bf fc 07 e0 | 00 00 00 05 | 旧ebx | ||
| bf fc 07 dc | ||||
| bf fc 07 d8 | ||||
| bf fc 07 d4 | ||||
| bf fc 07 d0 | ||||
| bf fc 07 cc | ||||
| bf fc 07 c8 | ||||
| bf fc 07 c4 | line3后 | |||
| bf fc 07 c0 |
line 10 执行后
| 地址 | 内容 | esp | ebp | 备注 |
|---|---|---|---|---|
| bf fc 08 04 | ||||
| bf fc 08 00 | 程序开始时 | |||
| bf fc 07 fc | ||||
| bf fc 07 f8 | ||||
| bf fc 07 f4 | ||||
| bf fc 07 f0 | 00 39 38 37 \0 9 8 7 | 程序开始时 | 返回地址 | |
| bf fc 07 ec | 36 35 34 33 6 5 4 3 | line2后 | line3后 | 旧的ebp值 |
| bf fc 07 e8 | 32 31 30 66 2 1 0 f | 旧edi | ||
| bf fc 07 e4 | 65 64 63 62 e d c b | 旧esi | ||
| bf fc 07 e0 | 61 20 39 38 a space 9 8 | 旧ebx | ||
| bf fc 07 dc | 37 36 35 34 7 6 5 4 | buf[7]—buf[4] | ||
| bf fc 07 d8 | 33 32 31 30 3 2 1 0 | (buf数组起始地址)line 8后buf[3]—buf[0] | ||
| bf fc 07 d4 | 注意小端方式 | |||
| bf fc 07 d0 | 注意结尾‘\0’ | |||
| bf fc 07 cc | 注意strlen不包含空字符 | |||
| bf fc 07 c8 | ||||
| bf fc 07 c4 | bf fc 07 d8 | line3后 | esi(buf作为函数gets的参数) line 9后 | |
| bf fc 07 c0 |
(3)正确的应该是 0x 08 04 85 c8; 得到了 0x 00 39 38 37 的返回地址
(4)edi esi ebx ,还有EBP的旧值
(5)1.strlen()函数不包含结尾的空字符,导致malloc所分配的空间不足,应为result = malloc(strlen(buf) + 1);
2.代码应检查返回值是否为NULL
https://www.geeksforgeeks.org/fgets-gets-c-language/#
c - What does gets() save when it reads just a newline - Stack Overflow
gets函数 gets() reads a line from stdin into the buffer pointed to by s until either a terminating newline or EOF, which it replaces with a null byte (aq\0aq). No check for buffer overrun is performed (see BUGS below).
MY
line 7
| 地址 | 内容 | esp | ebp | 备注 |
|---|---|---|---|---|
| bf fc 08 04 | ||||
| bf fc 08 00 | 程序开始时 | |||
| bf fc 07 fc | ||||
| bf fc 07 f8 | ||||
| bf fc 07 f4 | 08 04 85 c8 | 返回地址 | ||
| bf fc 07 f0 | bf fc 08 00 | 程序开始时 | 旧的ebp值 | |
| bf fc 07 ec | line2后 | line3后 | ||
| bf fc 07 e8 | 00 00 00 08 | 旧edi | ||
| bf fc 07 e4 | 00 00 00 10 | 旧esi | ||
| bf fc 07 e0 | 00 00 00 05 | 旧ebx | ||
| bf fc 07 dc | ||||
| bf fc 07 d8 | ||||
| bf fc 07 d4 | ||||
| bf fc 07 d0 | ||||
| bf fc 07 cc | ||||
| bf fc 07 c8 | ||||
| bf fc 07 c4 | line3后 | |||
| bf fc 07 c0 |
line 10 执行后
| 地址 | 内容 | esp | ebp | 备注 |
|---|---|---|---|---|
| bf fc 08 04 | ||||
| bf fc 08 00 | 程序开始时 | |||
| bf fc 07 fc | ||||
| bf fc 07 f8 | ||||
| bf fc 07 f4 | 08 04 85 00 ~ ~ ~ \0 | 返回地址 | ||
| bf fc 07 f0 | 0A 39 38 37\n 9 8 7 | 程序开始时 | 旧的ebp值 | |
| bf fc 07 ec | 36 35 34 33 6 5 4 3 | line2后 | line3后 | |
| bf fc 07 e8 | 32 31 30 66 2 1 0 f | 旧edi | ||
| bf fc 07 e4 | 65 64 63 62 e d c b | 旧esi | ||
| bf fc 07 e0 | 61 20 39 38 a space 9 8 | 旧ebx | ||
| bf fc 07 dc | 37 36 35 34 7 6 5 4 | |||
| bf fc 07 d8 | 33 32 31 30 3 2 1 0 | &~ —> esi( buf数组起始地址)line 8后 | ||
| bf fc 07 d4 | 注意小端方式 | |||
| bf fc 07 d0 | 注意结尾‘\0’ | |||
| bf fc 07 cc | 注意strlen不包含空字符 | |||
| bf fc 07 c8 | ||||
| bf fc 07 c4 | bf fc 07 d8 | line3后 | esi(buf作为函数gets的参数) line 9后 | |
| bf fc 07 c0 |
ASCII:ASCII码 - 基本ASCII码和扩展ASCII码,中文最全ASCII码对照表0~255
1 0804840c <getline>:
2 804840c: 55 push %ebp
// 保存调用者的 EBP, esp -= 4 esp = 0x bf fc 07 f0 - 4 = 0x bf fc 07 ec
3 804840d: 89 e5 mov %esp,%ebp
// 设置新的 EBP, ebp = esp = 0x bf fc 07 ec
4 804840f: 83 ec 28 sub $0x28,%esp
// 为局部变量 buf 分配空间, esp -= 0x28 = 40, esp = 0x bf fc 07 c4
5 8048412: 89 5d f4 mov %ebx,-0xc(%ebp)
// 保存调用者的 EBX, ebp - 0xc = 0x bf fc 07 e0
6 8048415: 89 75 f8 mov %esi,-0x8(%ebp)
// 保存调用者的 ESI, ebp - 0x8 = 0x bf fc 07 e4
7 8048418: 89 7d fc mov %edi,-0x4(%ebp)
// 保存调用者的 EDI, ebp - 0x4 = 0x bf fc 07 e8
8 804841b: 8d 75 ec lea -0x14(%ebp),%esi
// esi = ebp - 0x14 = (0x bf fc 07 d8)
9 804841e: 89 34 24 mov %esi,(%esp)
// 将 esi 的值保存, esp -= 4 esp = 0x bf fc 07 c0
10 8048421: e8 a3 ff ff ff call 80483c9 <gets>
| 地址 | 内容 | esp | ebp | 备注 |
|---|---|---|---|---|
| bf fc 08 04 | ||||
| bf fc 08 00 | 程序开始时 | |||
| bf fc 07 fc | ||||
| bf fc 07 f8 | ||||
| bf fc 07 f4 | 08 04 85 c8 | 返回地址? | ||
| bf fc 07 f0 | bf fc 08 00 | 程序开始时 | 旧的ebp值 | |
| bf fc 07 ec | line2后 | line3后 | ||
| bf fc 07 e8 | 00 00 00 08 | 旧edi | ||
| bf fc 07 e4 | 00 00 00 10 | 旧esi | ||
| bf fc 07 e0 | 00 00 00 05 | 旧ebx | ||
| bf fc 07 dc | ||||
| bf fc 07 d8 | ?? ?? ?? ?? | &~ —> esi (buf数组起始地址) line 8后 | ||
| bf fc 07 d4 | 注意小端方式 | |||
| bf fc 07 d0 | 注意结尾‘\0’ | |||
| bf fc 07 cc | 注意strlen不包含空字符 | |||
| bf fc 07 c8 | ||||
| bf fc 07 c4 | &? = bf fc 07 d8 | line3后 | esi (buf作为函数gets的参数) line 9后 | |
| bf fc 07 c0 |
line 7
| 地址 | 内容 | esp | ebp | 备注 |
|---|---|---|---|---|
| bf fc 08 04 | ||||
| bf fc 08 00 | 程序开始时 | |||
| bf fc 07 fc | ||||
| bf fc 07 f8 | ||||
| bf fc 07 f4 | 08 04 85 c8 | 返回地址 | ||
| bf fc 07 f0 | bf fc 08 00 | 程序开始时 | 旧的ebp值 | |
| bf fc 07 ec | line2后 | line3后 | ||
| bf fc 07 e8 | 00 00 00 08 | 旧edi | ||
| bf fc 07 e4 | 00 00 00 10 | 旧esi | ||
| bf fc 07 e0 | 00 00 00 05 | 旧ebx | ||
| bf fc 07 dc | ||||
| bf fc 07 d8 | ||||
| bf fc 07 d4 | ||||
| bf fc 07 d0 | ||||
| bf fc 07 cc | ||||
| bf fc 07 c8 | ||||
| bf fc 07 c4 | line3后 | |||
| bf fc 07 c0 |
line 10 执行后
| 地址 | 内容 | esp | ebp | 备注 |
|---|---|---|---|---|
| bf fc 08 04 | ||||
| bf fc 08 00 | 程序开始时 | |||
| bf fc 07 fc | ||||
| bf fc 07 f8 | ||||
| bf fc 07 f4 | 08 04 85 00 ~ ~ ~ \0 | 返回地址 | ||
| bf fc 07 f0 | 00 39 38 37\n 9 8 7 | 程序开始时 | 旧的ebp值 | |
| bf fc 07 ec | 36 35 34 33 6 5 4 3 | line2后 | line3后 | |
| bf fc 07 e8 | 32 31 30 66 2 1 0 f | 旧edi | ||
| bf fc 07 e4 | 65 64 63 62 e d c b | 旧esi | ||
| bf fc 07 e0 | 61 20 39 38 a space 9 8 | 旧ebx | ||
| bf fc 07 dc | 37 36 35 34 7 6 5 4 | |||
| bf fc 07 d8 | 33 32 31 30 3 2 1 0 | &~ —> esi( buf数组起始地址)line 8后 | ||
| bf fc 07 d4 | 注意小端方式 | |||
| bf fc 07 d0 | 注意结尾‘\0’ | |||
| bf fc 07 cc | 注意strlen不包含空字符 | |||
| bf fc 07 c8 | ||||
| bf fc 07 c4 | bf fc 07 d8 | line3后 | esi(buf作为函数gets的参数) line 9后 | |
| bf fc 07 c0 |
ASCII:ASCII码 - 基本ASCII码和扩展ASCII码,中文最全ASCII码对照表0~255
(3)正确的应该是 0x 08 04 85 c8; 得到了 0x 08 04 85 00 的返回地址
(4)edi esi ebx
(5)1.strlen()函数不包含结尾的空字符,导致malloc所分配的空间不足,应为result = malloc(strlen(buf) + 1);
2.代码应检查返回值是否为NULL
GPT
(1) 执行第 7 行指令后栈中的信息存放情况
第 7 行指令:mov %edi,-0x4(%ebp)
将寄存器 EDI 的值保存到栈中 -0x4(%ebp) 的位置。
栈布局:
栈在 ESP 上减少了 0x28 个字节,且按照汇编的调用规则,从低地址到高地址依次存放局部变量、保存的寄存器值和返回地址。
| 地址 | 内容 | 含义 |
|---|---|---|
0xbffc07f4 | 0x080485c8 | 返回地址 (caller 的下一指令) |
0xbffc07f0 | 0xbffc0800 | 调用者的 EBP |
0xbffc07ec | 0x5 | EBX 的旧值 |
0xbffc07e8 | 0x10 | ESI 的旧值 |
0xbffc07e4 | 0x8 | EDI 的旧值 |
0xbffc07e0 | - | 未使用的局部变量空间 (局部变量) |
(2) 执行第 10 行指令并调用 gets 函数后,回到第 10 行的下一条指令时栈中的信息存放情况
第 10 行指令:call 80483c9 <gets>
gets 函数读取输入,并存储在 buf 对应的内存中。
输入数据 “0123456789 abcdef0123456789\n” 被 gets 写入后:
| 地址 | 内容 | 含义 |
|---|---|---|
0xbffc07f4 | 0x080485c8 | 返回地址 |
0xbffc07f0 | 0xbffc0800 | 调用者的 EBP |
0xbffc07ec | 0x5 | EBX 的旧值 |
0xbffc07e8 | 0x10 | ESI 的旧值 |
0xbffc07e4 | 0x8 | EDI 的旧值 |
0xbffc07d4 | "0123456789 abcdef0123456789\0" | buf 内容,覆盖了内存 |
缓冲区溢出情况:
输入字符串超出了 buf 的大小(8 字节),导致覆盖了 EBP 和返回地址等关键数据。
(3) getline 的 ret 指令:正常返回地址与错误返回地址
正常返回地址:
在 getline 的 ret 指令执行时,返回地址应为 0x080485c8(caller 的下一指令地址)。
错误返回地址:
由于缓冲区溢出,gets 将输入覆盖了返回地址,实际返回地址被写成了字符串的一部分(如 "abcdef01" 对应的地址可能为 0x61626364)。这是导致段错误的原因。
(4) 执行完 gets 函数后,哪些寄存器的内容已被破坏?
gets 函数会:
- 改变寄存器
EAX(存储结果指针)。 - 改变寄存器
ECX和EDX(临时使用)。 - 其他寄存器可能在函数调用期间被调用者保存,但需注意缓冲区溢出会破坏栈中的内容,包括保存的
EBP。
(5) 除了缓冲区溢出,getline 的 C 代码还有哪些错误?
- malloc 的大小错误:
malloc(strlen(buf))未包含字符串末尾的\0,导致内存分配不足。应改为malloc(strlen(buf) + 1)。 - 未验证 malloc 成功:
malloc返回可能为NULL,应检查返回值是否有效。 - 未释放内存: 返回的指针需要在调用方手动释放,否则会导致内存泄漏。
- gets 函数的危险性:
gets不会检查输入大小,应改用fgets限制最大输入长度。
修正后的安全版本:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *getline_safe() {
char buf[8];
char *result;
if (!fgets(buf, sizeof(buf), stdin)) {
return NULL; // 输入失败
}
// 去除换行符
buf[strcspn(buf, "\n")] = '\0';
// 为字符串分配内存,并包含 '\0'
result = malloc(strlen(buf) + 1);
if (!result) {
perror("malloc failed");
exit(EXIT_FAILURE);
}
strcpy(result, buf);
return result;
}
Copilot
(1) 执行第7行指令后栈中的信息存放情况
执行第7行指令后,栈中的信息如下:
| 地址 | 内容 | 含义 |
|---|---|---|
| 0xbffc07f0 | 0x80485c8 | 返回地址 |
| 0xbffc07f4 | 0xbffc0800 | 调用者的 EBP |
| 0xbffc07f8 | 0x5 | 调用者的 EBX |
| 0xbffc07fc | 0x10 | 调用者的 ESI |
| 0xbffc0800 | 0x8 | 调用者的 EDI |
| 0xbffc0804 | 0x804841b | 局部变量 buf 的地址 |
(2) 执行第10行指令并调用gets函数后回到第10行的下一条指令执行时栈中的信息存放情况
执行第10行指令并调用 gets 函数后,栈中的信息如下:
| 地址 | 内容 | 含义 |
|---|---|---|
| 0xbffc07f0 | 0x80485c8 | 返回地址 |
| 0xbffc07f4 | 0xbffc0800 | 调用者的 EBP |
| 0xbffc07f8 | 0x5 | 调用者的 EBX |
| 0xbffc07fc | 0x10 | 调用者的 ESI |
| 0xbffc0800 | 0x8 | 调用者的 EDI |
| 0xbffc0804 | 0x804841b | 局部变量 buf 的地址 |
| 0xbffc0808 | ‘0’ | buf 的内容 |
| 0xbffc0809 | ‘1’ | buf 的内容 |
| 0xbffc080a | ‘2’ | buf 的内容 |
| 0xbffc080b | ‘3’ | buf 的内容 |
| 0xbffc080c | ‘4’ | buf 的内容 |
| 0xbffc080d | ‘5’ | buf 的内容 |
| 0xbffc080e | ‘6’ | buf 的内容 |
| 0xbffc080f | ‘7’ | buf 的内容 |
| 0xbffc0810 | ‘8’ | buf 的内容 |
| 0xbffc0811 | ‘9’ | buf 的内容 |
| 0xbffc0812 | ’ ’ | buf 的内容 |
| 0xbffc0813 | ‘a’ | buf 的内容 |
| 0xbffc0814 | ‘b’ | buf 的内容 |
| 0xbffc0815 | ‘c’ | buf 的内容 |
| 0xbffc0816 | ‘d’ | buf 的内容 |
| 0xbffc0817 | ‘e’ | buf 的内容 |
| 0xbffc0818 | ‘f’ | buf 的内容 |
| 0xbffc0819 | ‘0’ | buf 的内容 |
| 0xbffc081a | ‘1’ | buf 的内容 |
| 0xbffc081b | ‘2’ | buf 的内容 |
| 0xbffc081c | ‘3’ | buf 的内容 |
| 0xbffc081d | ‘4’ | buf 的内容 |
| 0xbffc081e | ‘5’ | buf 的内容 |
| 0xbffc081f | ‘6’ | buf 的内容 |
| 0xbffc0820 | ‘7’ | buf 的内容 |
| 0xbffc0821 | ‘8’ | buf 的内容 |
| 0xbffc0822 | ‘9’ | buf 的内容 |
| 0xbffc0823 | ‘\n’ | buf 的内容 |
(3) 当执行到getline的ret指令时,假如程序不发生段错误,则正确的返回地址是什么?发生段错误是因为执行getline的ret指令时得到了什么样的返回地址?
- 正确的返回地址应该是
0x80485c8。 - 发生段错误是因为执行
getline的ret指令时,返回地址被gets函数读取的超长输入覆盖,导致返回地址变成了无效地址。
(4) 执行完gets函数后,哪些寄存器的内容已被破坏?
- 执行完
gets函数后,eax、ecx和edx寄存器的内容可能已被破坏,因为这些是调用者保存寄存器。
(5) 除了可能发生缓冲区溢出以外,getline的C代码还有哪些错误?
malloc的大小应该是strlen(buf) + 1,以确保有足够的空间存储字符串和终止符\0。gets函数本身是不安全的,应该使用fgets或其他安全的输入函数来避免缓冲区溢出。













