安全编程技术实验三

socket安全编程

一、实验目的

该实验为验证性实验,实验目的如下:
1.了解本地计算机的网络配置。
2.熟悉面向对象编程/C编程环境,能够编写简单程序。
3.熟悉网络查阅源代码资源并会调试、修改和测试。

二、实验内容及步骤

1.实现简单的基于TCP或UDP的通信程序(控制台和视窗都可以),要求使用VC/JAVA/C#编程。
2.对完成的socket通信进行安全性设置,4项任选2项实现(1.做用户验证(用户未注册或者密码不正确,就断开连接)2.加密信息内容3.超时机制4.黑名单机制)

三、实验要求

1.本实验一人一组,编程语言为C/C++/C#/JAVA。
2.要求学生熟练掌握C/C++/C#/JAVA语言编程,多线程编程。
3.实验报告要求:
①包含实验指导书所要求的全部内容,重点在于实验内容与分析。
②写出实验的心得体会,回答思考题。
③符合实验报告撰写规范要求。

四、实验涉及的知识点

本实验涉及的知识点包括socket编程、网络编程安全的基本原理和实现方式等。

五、实验环境

计算机:Linux、C

六、实验运行截图


七、实验代码

//client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "aes_options.h"           //add

int main()
{
    int len;
    int client_sockfd;  
    struct sockaddr_in server_addr;
    char buffer[BUFSIZ];
    char *encrypt_string = NULL;
    memset(&server_addr, 0, sizeof(server_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(9000);

    if((client_sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket create failed");
        return 1;

    }
    int username;
    int password;
    printf("enter the username:");
    scanf("%d", &username);
    printf("enter the password:");
    scanf("%d", &password);
    if (username !=2020 || password != 1209)
    {
        printf("uncorrect name!\n");
        return 1;
    }
    
    

    if(connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
    {
        perror("connect failed");
        return 1;
    }

    printf("connect to server\n");
    len = recv(client_sockfd, buffer, BUFSIZ, 0);

    buffer[len] = '\0';
    printf("%s", buffer);

    while(1)
    {
        printf("enter a data:");
        scanf("%s", buffer);
        if(!strcmp(buffer,"quit"))
            break;
        int encrypt_length = encrypt(buffer, &encrypt_string);     //add
        len = send(client_sockfd, encrypt_string, encrypt_length, 0); //add
        len = recv(client_sockfd, buffer, BUFSIZ, 0);
        buffer[len] = '\0';
        printf("recived:%s \n", buffer);
    }

    close(client_sockfd);
    printf("bye");


    return 0;
}
//server.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "aes_options.h" //add

int main()
{
	int server_fd;
	int client_fd;
	int len;
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	int sin_size;
	char buffer[BUFSIZ];
	// printf("%d",BUFSIZ);
	memset(&server_addr, 0, sizeof(server_addr)); // initialize struct
	memset(&server_addr, 0, sizeof(client_addr));

	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = INADDR_ANY;
	server_addr.sin_port = htons(9000);

	if ((server_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) // create server socket
	{
		perror("socket create failed");
		return 1;
	}

	if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0) // bind info on server socket
	{
		perror("bind failed");
		return 1;
	}

	listen(server_fd, 5); // listen port 9000

	sin_size = sizeof(struct sockaddr_in);

	if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &sin_size)) < 0)
	{
		perror("accept failed");
		return 1;
	}

	printf("accept client %s\n", inet_ntoa(client_addr.sin_addr));
	len = send(client_fd, "Welcome to my server\n", 21, 0);

	while ((len = recv(client_fd, buffer, BUFSIZ, 0)) > 0)
	{

		char *decryto_string = NULL;		   // add
		decrypt(buffer, &decryto_string, len); // add
		printf("%s \n", decryto_string);
		if (send(client_fd, decryto_string, len, 0) < 0) // modified
		{
			perror("send failed");
			return 1;
		}
	}

	close(client_fd);
	close(server_fd);

	return 0;
}
//aes_options.c
#include <stdio.h>
#include <openssl/aes.h>
#include <stdlib.h>
#include <string.h>

int encrypt(char *input_string, char **encrypt_string)
{
    AES_KEY aes;
    unsigned char key[AES_BLOCK_SIZE]; // AES_BLOCK_SIZE = 16
    unsigned char iv[AES_BLOCK_SIZE];  // init vector
    unsigned int len;                  // encrypt length (in multiple of AES_BLOCK_SIZE)
    unsigned int i;
    // set the encryption length
    len = 0;
    if ((strlen(input_string) + 1) % AES_BLOCK_SIZE == 0)
    {
        len = strlen(input_string) + 1;
    }
    else
    {
        len = ((strlen(input_string) + 1) / AES_BLOCK_SIZE + 1) * AES_BLOCK_SIZE;
    }
    // Generate AES 128-bit key
    for (i = 0; i < 16; ++i)
    {
        key[i] = 32 + i;
    }
    // Set encryption key
    for (i = 0; i < AES_BLOCK_SIZE; ++i)
    {
        iv[i] = 0;
    }
    if (AES_set_encrypt_key(key, 128, &aes) < 0)
    {
        fprintf(stderr, "Unable to set encryption key in AES\n");
        exit(0);
    }
    // alloc encrypt_string
    *encrypt_string = (unsigned char *)calloc(len, sizeof(unsigned char));
    if (*encrypt_string == NULL)
    {
        fprintf(stderr, "Unable to allocate memory for encrypt_string\n");
        exit(-1);
    }
    // encrypt (iv will change)
    AES_cbc_encrypt(input_string, *encrypt_string, len, &aes, iv, AES_ENCRYPT);
    return len;
}
void decrypt(char *encrypt_string, char **decrypt_string, int len)
{
    unsigned char key[AES_BLOCK_SIZE]; // AES_BLOCK_SIZE = 16
    unsigned char iv[AES_BLOCK_SIZE];  // init vector
    AES_KEY aes;
    int i;
    // Generate AES 128-bit key
    for (i = 0; i < 16; ++i)
    {
        key[i] = 32 + i;
    }
    // alloc decrypt_string
    *decrypt_string = (unsigned char *)calloc(len, sizeof(unsigned char));
    if (*decrypt_string == NULL)
    {
        fprintf(stderr, "Unable to allocate memory for decrypt_string\n");
        exit(-1);
    }
    // Set decryption key
    for (i = 0; i < AES_BLOCK_SIZE; ++i)
    {
        iv[i] = 0;
    }
    if (AES_set_decrypt_key(key, 128, &aes) < 0)
    {
        fprintf(stderr, "Unable to set decryption key in AES\n");
        exit(-1);
    }
    // decrypt
    AES_cbc_encrypt(encrypt_string, *decrypt_string, len, &aes, iv,
                    AES_DECRYPT);
}
//aes_option.h
#ifndef _ASE_H_
#define _ASE_H_
	int encrypt(char *input_string, char **encrypt_string);
	void decrypt(char *encrypt_string, char **decrypt_string, int len);
#endif

八、实验总结

在本次实验中我对于缓冲区溢出的知识有了一定的认识,跟着PPT尝试了栈溢出和堆溢出的实践操作,按照整数溢出的方式,学习自行编码尝试了用溢出攻击来修改密码。同时也通过学习,了解了一些关于避免缓冲区溢出的知识如使用封装的安全函数,并限定函数中内容大小等方式。

九、思考题

Socket编程在多线程编程时应注意什么问题?

1.协议的选择,是TCP还是UDP。
众所周知,TCP提供可靠连接,UDP提供不可靠传输。对于“不重要”的数据通信建议采用UDP,简单没有那么多的报文确认,而且即便丢几帧也是可以的。对于“重要”的数据通信那就要用TCP,能保证可靠的通信
2.套接字阻塞/非阻塞模式
阻塞模式,就是recv /recvfrom只有等到有数据来了才返回,否则就一直在那死等。
非阻塞模式,意思很明显,就是创建这样的套接字之后,winsock API调用会立即返回,通常,我们需要重复调用同一个函数,直到成功返回,因此,对于非阻塞模式下的socket函数调用,我们要时刻检查返回值的情况,以确保革命成功
3.套接字I/O模型选择,共有5种模型:select模型,异步窗口模型,异步事件模型,overlapped模型,以及完成端口模型。
这里,有必要先帮大家理清下套接字模式和套接字I/O模型的区别。套接字模式是用于决定在随一个套接字调用时,那些winsock函数的行为。而套接字模型则定义了一个应用程序如何对套接字上进行的I/O进行管理及处理。(windows网络编程技术中的解释)
大概的意思就是,套接字模式好比两种不同性格的人,一种是决定了一直要死等天上掉钱,否则饭也不吃觉也不睡;或者,我只是来打酱油的,没钱就立马走人!而套接字I/O模型好比是个热心肠,他说,大哥,你别着急,坐下来歇会,我先帮你盯着,一有钱下来我就告诉你!
①select模型
核心是围绕select函数做文章。
int select{
int nfds,//忽略
fd_set FAR * readfds,//可读性检查
fd_set FAR * writefds,//可写性
fd_set FAR * exceptfds,//例外数据
const struct timeval FAR *timeout //等待I/O时间,超过则返回
}
FD_SET是一个集合,使用时,假设我们想接收socket1的数据,但又不想死等,这是可以调用 FD_SET(SOCKET s)将socket1添加进readfds集合中,这样,我们先调用ret=select(…),在timeout时间内,若有数据可接收,则立即返回,下面我们就可以调用recv来接收数据,若ret表示超时,则当前没有数据可读,还有可能返回socket_error。总之,select相当于是个侦察兵,他可以让心急的我们(阻塞模式的socket)在恰当的时候出击,以保证万无一失
注:关于FD_SET集合的操作(FD_ZERO,FD_ISSET,FD_CLR)《indows网络编程技术》有详细说明。
②WSAAsyncSelect模型
异步I/O模型。利用该模型,应用程序可以在一个套接字上,接收以Windows消息为基础的网络事件通知。创建时,要调用WSAAsyncSelect函数,看到函数参数,就明白了,这就相当于给指定窗口定义了一个自定义消息,注册了触发消息的事件。这个消息是由socket发出的,窗口接收到消息后,根据消息类型,判断到底发生了什么socket事件。
注意的是,响应函数是有格式要求的:
LRESULT CALLBACK WindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
其中,wParam指定了“肇事”的是哪个socket,lParam包含两方面信息,低位字指定发生了什么网络事件,高位字包含了可能出现的任何错误代码。使用时,应该先判断错误代码,若没有错误,再分析事件类型进行处理。
③WSAEventSelect模型
异步事件通知模型,要求我们为套接字创建一个事件对象(WSACreateEvent()),这个事件对象中注册了你感兴趣的网络事件(FD_ACCEPT,FD_READ…),然后,将该事件对象与套接字绑定(WSAEventSelect(…)),这样,如果该套接字上有收到数据,那么我们就会收到FD_READ信号,提示我们接收数据。我们可以调用WSAWaitForMultipleObject、WSAEnumNetWorkEvents函数来确定是哪个socket发生的什么网络事件。
④重叠模型
⑤完成端口模型
4.对于TCP流式传输和UDP的包传输的理解。这个直接影响到编程思想,即如何接收数据的问题。即你是按帧接收还是按数据流的思想接收。
5.setsockopt/getsockopt设置socket的系统选项
setsocketopt中你可以配置socket的系统参数,比如系统分配的收发缓冲区等。
6.系统预定义的socket收发缓冲区大小是否要改变
当我们调用send/sendto发送数据时,返回成功,这时,数据只是被暂存在了系统预分配的发送缓冲区中,实际发送成功是要等TCP的ack报文才算数。增加这个缓冲,一定程度上能降低我们的重传次数,但只是缓兵之计。
7. FD_ACCEPT,FD_CONNECT,FD_READ,FD_WRITE,FD_CLOSE信号,何时触发
这里重点讲何时发出FD_WRITE信号:
1)使用connect/WSAConnect,一个套接字首次建立了连接
2)使用accept/WSAAccept,套接字被接受以后
3)若send/WSASend/sendto/WSASendTo操作失败,并且返回WSAEWOULDBLOCK错误(该错误表明发送缓冲区溢出),而且缓冲区又变得可用了
总之,FD_WRITE信号貌似不怎么好用,有经验的可以分享下经验。。。
8.Send/recv、sendto/recvfrom返回值的含义。对返回值的处理是保证正确的通信的基础
9.UDP丢包后的重传机制
10.TCP粘包现象处理
一般是在接收端,设立一个大缓冲区,收到数据时,先统统放到该缓冲区中,再进行数据的处理。
11.信号量的使用,包括Cevent,WSAEvent,Cmutex等
使用事件,能帮助我们更好的控制通信,具体用法因人而异,Cmutex用于对某个缓冲区操作时进行锁定,以防止多方同时操作一个对象,但是,在使用时,我们应该尽量避免使用互斥对象。
12.监听线程如何高效的循环等待网络事件,不要用while(1),用WaitForSingleObject/WaitForMultipleObject等,这些是高效的等待函数,当没有事件时,线程是挂起的,处于阻塞状态,不占用CPU时间。
13.Socket发送数据较简单,如何接收数据是需要注意的,特别是循环发送大量数据时,接收程序更应该考虑。
这里,有两种策略:
1)每次发送之间,增加一个延时,以协调通信双方的速度,但这不能根本解决问题。
2)如上面所说,收到的数据都先放到一个大缓冲区中,然后再按帧取数据,这里,如何按帧取数据,可以在发送帧的时候,在帧头写明这一帧的长度。

posted @ 2022-11-26 17:06  戴骏  阅读(61)  评论(0编辑  收藏  举报