overflow_var

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <string>
#define PASSWORD "1234567"

int verify_password (char *password)
{
int authenticated;
char buffer[8];// add local buff
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}

main()
{
int valid_flag=0;
char password[1024];
while(1)
{
printf("please input password:       ");
scanf("%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n\n");
}
        else
{
printf("Congratulation! You have passed the verification!\n");
break;
}
}
system("pause");
}

验证缓冲区溢出

运行程序

通过IDA找到调用 strcmp 和 strcpy 的位置

在ollydbg里定位到这里,并添加注释。调用strcmp和strcpy处下断点。

先尝试输入一个不会溢出的错误的密码,如“123”。
F9执行至断点处,再F8单步步过。看到eax变为FFFFFF(即-1),这是输入小于密码的返回值。

再F8运行至call strcpy的下一条命令
观察到堆栈区保存了存放“123”的地址。在数据区追踪,可以看到内存里“123/0”以空字符串结束。

再输入正确密码“1234567”,eax的返回值变为0,表示密码正确。

再构造会产生溢出的密码,如“AAAAAAAAAAAAAAAAAAAAAA”。
strcpy返回结果是1,表示输入大于密码。

在数据区追踪,看到原本存放EAX的位置已经被溢出的“A”覆盖。

由此可验证栈溢出的发生。

栈帧内存分布图与攻击逻辑


淹没相邻变量改变程序流

构造8位的输入,如“12345678”。可以看到strcmp比较完返回的是1。

由于输入为8位,正好等于buffer大小,因此最后的终结符“/0”覆盖掉下一位,使authenticated变为0,正好满足密码正确时的输出。

淹没返回地址改变程序流

计算偏移量
总填充长度=buffer[8](8)+authenticated(4)+Saved EBP(4)=16字节

找到目标地址(congratulation)
直接查找字符串

记下401116。

构造输入:16位填充+0x401116
地址以小端序写入。可以构造“A”×16+“\x16\x11\x40\x00”
打开010 Editor,输入16个A,再ctrl+H切换到16进制视图,写下16114000,再ctrl+H切换回文本试图。全选复制。


overflow_ret

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <string>
#define PASSWORD "1234567"

int verify_password (char *password)
{
int authenticated;
char buffer[8];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}

main()
{
int valid_flag=0;
char password[1024];
FILE * fp;
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}

验证缓冲区溢出

用IDA打开exe,发现这个程序是读取txt文件中的密码进行验证的。

先准备一个密码文件。

找到strcmp和strcpy的位置。

下断点并添加注释

跟overflow_var非常类似,只要输入字符串大于等于8位,就可以覆盖到authenticated

例如构造输入“111111111111111111”

栈帧内存分布图与攻击逻辑


淹没相邻变量改变程序流

还是构造输入“12345678”,让“/0”去覆盖authenticated的最低位

淹没返回地址改变程序流

偏移量:总填充长度=buffer[8](8)+authenticated(4)+Saved EBP(4)=16字节
目标地址:40112F

使用010 Editor构造输入

StackOverrun.exe

用IDA查看,发现需要输入命令行参数。

使用Ollydbg打开,调试→参数,输入参数后重载开始调试。

运行

IP地址分析

分析PE文件格式 (IDA)
先打开view→Open subviews→segments

双击进入.text代码段

从IDA底部状态栏可以看到:代码段的文件偏移为0x1000。同时,IDA显示.text区段的起始地址为0x00401000

分析内存加载 (Ollydbg)
为了从动态分析的角度验证 IDA 的静态分析结果,使用 Ollydbg 加载 StackOverrun.exe 并查看其内存映射。

此动态分析结果与 IDA 的静态分析结果(文件偏移 0x1000,虚拟地址 0x00401000) 完全一致。
Ollydbg 确认了程序的基地址为 0x00400000,而代码段的实际加载地址为 0x00401000。这清晰地展示了 PE 文件从磁盘(文件偏移 0x1000)加载到内存(虚拟地址 0x00401000)的地址变化过程。

详细分析函数调用

定位 call foo:查找字符串,双击“Address of foo = %p”,进入到main函数中调用foo函数的地方。

在call 00401000(调用foo函数)处下断点。

状态 1:即将执行 call foo

  • EIP (ip): 0040109B,指向 call 00401000 这条指令,即将执行。
  • ESP (sp): 0019FF24,是 main 函数当前的栈顶。
  • EBP (bp): 0019FF74,是 main 函数的栈帧基址。

状态 2:刚进入 foo 函数(call 执行后)

  • EIP:00401000,指向 foo 函数第一条指令 sub esp,0ch。表明 call 指令已成功执行,程序流已从 main 函数跳转到了 foo 函数的开头。
  • EBP:仍然是0019FF74。因为 foo 函数建立自己栈帧的指令(push ebp, mov ebp, esp)还没有被执行。EBP 目前仍然指向 main 函数的栈帧基址。
  • ESP:0019FF20。ESP 的值减少了 4 字节(0x24 -> 0x20)。这是因为 call 指令自动将返回地址压入了栈顶。
  • 栈 (Stack) 分析:右下角的栈窗口,ESP 指向的地址 0019FF20 处,存放的值是 0040109B,正是 “状态 1” 中 call 指令的下一条指令的地址(即 add esp, 14),证明了 call 指令已将正确的返回地址0040109B)压入栈顶,ESP 现在正指向它。

状态 3:foo 函数栈帧建立

  • EIP:00401005,指向 push 00406070 指令(即 printf 的 “My stack looks like…” 字符串参数),程序即将开始执行函数的主体
  • EBP:0019FF74,这个函数被编译器优化了,没有使用 EBP 作为栈帧指针,所以 EBP 的值在 foo 函数内部保持不变。
  • ESP:0019FF0C,ESP 的值总共减少了 0x14 字节(0x20 - 0x0C = 20
  • 栈 (Stack) 变化分析:ESP0019FF20 变为 0019FF0C,这是三条指令共同作用的结果:
    1. sub esp, 0ChESP 向下移动 12 字节(0xC),为局部变量 buf[10] 腾出空间。ESP 变为 0019FF14
    2. push esiESI 寄存器的值 (004010E1) 被压入栈。ESP 减 4,变为 0019FF10
    3. push ediEDI 寄存器的值 (004010E1) 被压入栈。ESP 再减 4,变为 0019FF0C

查看栈窗口

  • 0019FF0C (栈顶):存放着 004010E1 (刚压入的 EDI)。
  • 0019FF10:存放着 004010E1 (压入的 ESI)。
  • 0019FF20返回地址 (0040109B) 仍然安全地保存在栈的上方。

程序功能推测

通过对 StackOverrun.exe 的静态与动态分析,推测其功能如下:

  1. 暴露漏洞:程序通过 foo 函数接收一个命令行参数,并使用不安全的 strcpy 函数将其复制到一个 10 字节的局部缓冲区 buf[10]。此操作未进行边界检查,故意暴露了一个栈缓冲区溢出漏洞
  2. 提供目标:程序包含一个 bar 函数(用于打印 “Augh! I’ve been hacked!”),该函数在正常执行流程中永远不会被调用。
  3. 辅助利用:程序在启动时会主动打印 foo 函数和 bar 函数的内存地址,这为漏洞利用提供了关键信息。

结论: StackOverrun.exe 是一个专门为本实验设计的 “靶子”程序 。其全部功能就是为了让实验者利用其栈溢出漏洞,劫持程序流程,转而去执行在正常情况下无法访问的 bar 函数。

修改StackOverrun程序的流程

返回地址覆盖为bar函数起始地址

  • 填充 (Padding):使用 12 个任意字符,例如 AAAAAAAAAAAA
  • 覆盖地址 (Overwrite Address):使用 bar 函数的绝对地址 0x00401060。由于 x86 架构使用小端序 (Little-Endian) 存储,该地址在内存中表示为 \x60\x10\x40\x00
  • 最终 PayloadABCDEFGHABCD + \x60\x10\x40\x00 (共 16 字节)。

攻击逻辑:

  1. 输入与溢出:将构造好的 Payload 作为命令行参数传递给 StackOverrun.exefoo 函数内部的 strcpy 将此 Payload 复制到栈上的 buf 缓冲区。
  2. 覆盖返回地址strcpy 首先复制 12 字节的填充 (ABCDEFGHABCD),刚好填满 buf 的空间。随后,它继续复制 bar 函数的地址 (\x60\x10\x40\x00),这 4 个字节精确地覆盖了栈上原来存放 foo 函数返回地址的位置。strcpy 在复制完最后一个字节 \x00 后停止。
  3. 劫持控制流:当 foo 函数执行完毕,准备返回时,执行 retn 指令。CPU 从栈顶弹出地址并准备跳转。此时,栈顶的地址不再是原始的返回地址,而是被覆盖上去的 bar 函数地址 0x00401060
  4. 执行目标函数:CPU 将 EIP 指针设置为 0x00401060,直接跳转到 bar 函数的入口点并开始执行。
  5. 成功调用bar 函数执行其内部逻辑,调用 printf 打印出 “Augh! I’ve been hacked!”。

“坏字符”问题:虽然 bar 函数地址 0x00401060 包含空字节 \x00,但它恰好位于地址的最高位(小端序表示的最后一个字节)。strcpy 在复制完包含 \x00 的完整地址、成功覆盖返回地址之后才遇到 \x00 并停止,因此未影响攻击效果。

原始栈布局与攻击逻辑