[毕业设计]:Linux 主机文件完整性验证工具的设计与实现 指导教师: 曹晓梅曹晓梅
摘要
自Linux首次出现,其强大的功能和众多的优点便受到了许多人的关注。相比于Windows系统,Linux更容易受到服务商们的青睐。然而,正因为如此,Linux系统也面临更多安全相关的问题,其中文件系统的完整性对Linux系统安全至关重要。
本文主要介绍了如何利用一种新型的HMAC算法对Linux的系统文件进行完整性校检。在Linux中常见的文件类型有普通文件、目录文件、链接文件与管道文件,除此之外还有比较少见的设备文件、套接字文件以及其他类型文件。该软件的核心功能就是能够识别这些文件的类型并使用新型算法将从这些文件中提取出的信息摘要进行加密生成认证码用于核验,并创建一个GLADE用户界面,再把核验后的数据结果通过该界面展现出来。这种新型的HMAC算法是一种更加简便且高效的算法。通过计算并保存系统文件的认证码至数据库,再次使用认证码时通过和当前文件计算出的认证码作比较,从而识别它们的变化来对系统文件的完整性进行保护。本次设计实现了对选定的文件夹中指定类型的系统文件进行认证码的计算,并能够进行二次核验,以此来确定系统文件是否被篡改。
关键词:Linux;系统文件;完整性; HMAC
第一章 绪论
1.1 研究背景
Linux系统是被全球广泛使用的一种操作系统,由于其开源,稳定,灵活,对硬件的需求较低,不用经常进行升级,许多企业都用它来搭建服务器,但也正是因为这些特性,针对Linux系统的安全攻击时常发生。Linux中的一切内容几乎都是以文件的形式存储的[1],一旦在Linux系统文件中植入病毒或者后门,不法分子将可能提权从而控制住系统主机,或通过篡改原有文件内容的方式来实现违法目的,这将会对Linux系统数据造成严重威胁。例如yam2-minerd挖矿程序以及watch-smartd挖矿牧马,这些挖矿程序会在成功入侵系统后写入一个文件,然后会修改文件配置进行进程隐藏,需要时还可以通过ssh远程登录遥控,对服务器造成了巨大的危害且难以被察觉。由此可见保护Linux文件完整性的重要性以及建立一个完整性保护机制的必要性。
为了保护Linux系统文件数据的安全,如何验证Linux系统文件的完整性便是其中一个重要的课题[2]。通过研究该课题,可以实现一种新型的且高效的Linux系统文件完整性校验工具,它可以被广泛运用于日常的服务器安全运维工作中,从而减少运维人员的工作量,提高运维人员的工作效率。尽管Linux系统本身拥有校验文件的函数md5sum,但是这种函数的加密时间长,一次只能加密一个文件且并不支持所有的文件类型,再加上操作繁琐且不是可视化界面,不利于使用者的操作。所以设计一个拥有可视化的界面,能够方便地操作文件完整性验证,甚至可以主动统计操作结果,用一种直观的方式呈现给使用者便成为了一个研究课题。
1.2研究目的
Linux文件系统的完整性保护至关重要,但是现有的完整性保护机制还仍然不够完善与实用,使用的便捷性也还不尽人意。因此,设计开发一个用于验证Linux系统文件完整性的软件便成为了一个十分重要的课题。这个软件不仅具有较高的安全性,能够验证Linux系统的系统文件完整性,而且还要有较高的工作效率以及良好的交互界面,能够方便使用者快速且直观地了解到Linux系统文件的完整性是否有问题以及问题出现在哪个文件上,是什么样的问题, 从而减少运维人员的工作量,提高运维人员的工作效率[3] 。
当用户选定好需要核验的系统文件夹后,只需要点击计算或者是核验按钮,就可以快速且直观地了解到文件夹中哪些文件的完整性不变,哪些文件被篡改或者是凭空出现、消失,并能够迅速找到这些文件的位置进行进一步的查看,从而保证了Linux系统的安全性。
除此之外,考虑到Linux系统的系统文件夹之多以及文件类型之广,软件还应该拥有一定的筛选功能,能够满足使用者在限定范围以及限定条件下的验证需求。为了方便使用者从宏观角度了解到所有文件夹中所有类型文件的完整性状况,软件还应该要有统计功能,这样使用者将能更加直观地从宏观角度了解到Linux系统的情况。
1.3研究内容
本课题主要针对Guest OS为Linux的环境,围绕使用HMAC算法,用C语言编程语言实现对任意指定的Linux系统文件夹中的指定类型文件进行认证码的生成与核验。针对HMAC算法要通过对比说明为什么要选用基于哈希函数的MAC算法并且要和传统HMAC算法做性能上的比对。另外还需创建一个Glade工程来实现用户的可视化界面,界面需要有多个窗口负责展示各种操作后的数据结果,最后还应该有合理布局的挂件用于实现交互。软件通过比较篡改前后生成的系统文件认证码,可以判断该文件篡改前后生成的认证码不同,从而判断该文件已经被修改;若是没有找到可比较的认证码,或者是没有能够进行比较的认证码,则可以判断该文件原本不存在与该文件夹目录下或者该文件夹目录下的文件出现缺失。第一次生成的认证码会被存放在数据库中,并赋予一个状态码用于后续操作的核验。等到核验的时候,便可以通过状态码统计一次核验成功和失败的文件数量,并和详细的错误文件路径和错误原因一同提交给可视化界面方便使用者进行查验。如果数据库出现错误,例如表缺失或者是数据错误,软件能够查询识别到并及时地进行处理从而防止软件程序的崩溃。
1.4章节组织
本文的组织结构如下:
第一章:绪论,这部分阐述了课题的研究背景以及研究目的和内容,最后论述了论文结构。
第二章:预备知识,简略介绍了Linux系统以及什么是文件完整性校检,并对哈希函数以及MD5进行了简略的介绍。核心内容是介绍新型HMAC算法以及使用方法。
第三章:系统分析与设计,这是论文的核心部分,在进行需求分析后,详细介绍了软件的设计思路以及实现流程,根据功能将软件分成了五个模块进行逐一的介绍。
第四章:系统实现,详细介绍了软件的各个关键功能的设计实现原理及过程。
第五章:运行与测试,主要介绍了软件的运行环境以及软件的运行核验成功结果以及运行核验失败结果。
第二章 预备知识
略,主要引用了文献以及前提知识
第三章 系统设计
略,主要是对系统的逻辑概念实现论述,没有涉及到设计
第四章 系统实现
4.1 认证码加密功能实现
使用的核心算法是一种新型的HMAC算法,该算法的效率以及安全性相较于传统的HMAC算法有所提高,具体的使用方法如下:
(1) 从文件中提取必要信息组成信息摘要。
(2) 将信息摘要送至HMAC算法加密生成认证码。
(3) 存储认证码以及文件绝对路径。
(4) 核验时重新加密生成认证码。
(5) 根据文件路径重新取出文件的原认证码。
(6) 新认证码和原认证码比对,判断完整性是否被破坏。
(7) 如果完整性被破坏则判断是哪种错误原因。
算法的具体实现方法如下文描述:
4.1.1 MD5加密
在获得信息摘要的部分信息后,首先要组装信息摘要,然后加密生成一个MD5散列值,核心代码如下:
void md5(char *temp1,char *temp2,char* the_md5){ int i; unsigned char decrypt[16]; char buf[16][2]; char *encrypt; encrypt = calloc(strlen(temp1)+strlen(temp2)+2,sizeof(char)); strcpy(encrypt,""); strcat(encrypt,temp1); strcat(encrypt," "); strcat(encrypt,temp2); MD5_CTX md5;//MD5格式设置 MD5Init(&md5); //初始化 MD5Update(&md5,encrypt,strlen(encrypt));//加密步骤 MD5Final(&md5,decrypt); //最终步骤 for(i=0;i<16;i++) { sprintf(buf[i], "%02x", decrypt[i]); strcat(the_md5,buf[i]); } strcat(the_md5,"\0"); free(encrypt); }
4.1.2 转换
转换分为两步,第一步是十六进制转换成二进制,核心代码如下:
void c2c(char *a,char *char_bin){//16进制字符串转二进制字符串 int i,b[32]; for(i=0;i<strlen(a);i++){ if(a[i]>47&&a[i]<58) b[i] = a[i] - 48; else b[i] = a[i] - 87; strcat(char_bin,temp[b[i]]); } }
第二步是二进制字符串转数组,核心代码如下:
void c2i(char *char_bin,int *int_bin){ //二进制字符串转数组 int i; for(i=0;i<strlen(char_bin);i++){ int_bin[i] = char_bin[i] - 48; } }
4.1.3 异或
对每一个二进制数和其前一个数进行异或,最后一个数不用,核心代码如下。
void XOR(int *int_bin){//异或 int a = int_bin[127],b ,i;//最后一个不用 for(i=0;i<128;i++){ b = int_bin[i]; int_bin[i] = int_bin[i] ^ a; a = b; } }
4.1.4 左循环
左循环的目的是制造雪崩效应,核心代码如下:
void reverse(int R[], int from, int to ) { int i ,temp ; for(i=0; i<(to-from+1)/2; i++) { temp = R[from+i]; R[from+i] = R[to-i]; R[to-i] = temp; } } void ROL(int *xor_bin){//左循环 int n = 128,p = n % 7; reverse(xor_bin, 0, p-1); reverse(xor_bin, p, n-1); reverse(xor_bin, 0, n-1); }
4.1.5转成字符
在完成了所有的操作后,需要将二进制转换回字符,核心代码如下:
void i2c(int *rol_bin,char *c_bin){//整型转字符 int i,sum; strcpy(c_bin,""); for(i=0;i<128;i+=4){ sum = rol_bin[i]*8 + rol_bin[i+1]*4 + rol_bin[i+2]*2 + rol_bin[i+3]*1; c_bin[i/4] = hex[sum] ; } }
4.2 文件完整性核验统计功能的实现
4.2.1 核验功能
在认证码生成功能的基础上,添加了单独的核验功能用于进行核验,并与后续的统计功能相连接。在每次生成了认证码后都会调用该文件在数据库中的原认证码进行核验,并把结果传递给统计功能模块。其实现核心代码如下:
cmd = calloc(strlen(tablename) + strlen(road) + 500,sizeof(char));//第一次 strcpy(cmd,"");//组装cmd命令 strcat(cmd,"select aucode from "); strcat(cmd,tablename); strcat(cmd," where road = \""); strcat(cmd,road); strcat(cmd,"\";"); count++; mysql_query(&mysql, cmd); res = *mysql_store_result(&mysql); free(cmd);//第一次 if((row = mysql_fetch_row(&res))!=NULL){ if(strcmp(row[0],the_md5) == 0){//验证成功 cmd = calloc(strlen(tablename) + strlen(road) + 500,sizeof(char));//第二次 *suc_count = *suc_count + 1; strcpy(cmd,""); strcat(cmd,"update "); strcat(cmd,tablename); strcat(cmd," set flag = 1 where road = \""); strcat(cmd,road); strcat(cmd,"\";"); mysql_query(&mysql, cmd); free(cmd);//第二次 } else{//被修改过了 cmd = calloc(strlen(tablename) + strlen(road) + 500,sizeof(char));//第三次 *fai_count = *fai_count + 1; strcpy(cmd,""); strcat(cmd,"update "); strcat(cmd,tablename); strcat(cmd," set flag = 1 where road = \""); strcat(cmd,road); strcat(cmd,"\";"); mysql_query(&mysql, cmd); free(cmd); }
4.2.2 统计功能
数据统计模块被分割成了两个部分放在两个函数中,分别负责单一文件的类型分类以及总数的统计。为了方便将所有的数据传递回给可视化界面,特意设计了数据结构res用于传递。数据结构如下:
typedef struct{ int suc_count; //用于收集成功的文件数量 int fai_count; //用于收集失败的文件数量 int lost_count; //用于收集缺失的文件数量 }res;
通过这个数据结构,就可以将所有的数据传递给可视化界面的窗口。数据统计的核心代码如下:
while((row = mysql_fetch_row(&res))!=NULL){ t = calloc(length(sum_num),sizeof(char)); cmd = calloc(strlen(t) + strlen(row[0]) + 500,sizeof(char));//第二次分配空间 *lost_count = *lost_count + 1; printf("缺失文件:%s\n",row[0]); if((row = mysql_fetch_row(&res))!=NULL){ if(strcmp(row[0],the_md5) == 0){//验证成功 cmd = calloc(strlen(tablename) + strlen(road) + 500,sizeof(char));//第二次 *suc_count = *suc_count + 1; else{//被修改过了 cmd = calloc(strlen(tablename) + strlen(road) + 500,sizeof(char));//第三次 *fai_count = *fai_count + 1; strcpy(cmd,""); strcat(cmd,"update "); strcat(cmd,tablename); strcat(cmd," set flag = 1 where road = \""); strcat(cmd,road); strcat(cmd,"\";"); mysql_query(&mysql, cmd); free(cmd); else{//没有录入的文件 *fai_count = *fai_count + 1; printf("not_exist_road:%s\n",road); t = calloc(length(sum_num) + 500,sizeof(char)); cmd = calloc(strlen(t) + strlen(road) + 500,sizeof(char));//第五次 itoa(sum_num,t);
4.3 可视化界面功能实现
4.3.1进度条功能
在介绍进度条之前,我需要简略介绍贯穿着整个可视化界面的数据结构gtk,gtk数据结构负责引入和初始化所有和界面有关的函数,并且是通过gtk来传递所有的可视化界面挂机参数,其结构如下:
t
ypedef struct{ GtkBuilder *builder;//总结构 GtkWindow *window;//窗口 GtkWidget *progress;//进度条 GtkWidget *treeview[2];//树状图 GtkTextView *textview[10]; //显示图 GtkWidget *button[6];//按钮 GtkWidget *label[3];//标签 GtkSwitch *_switch[16];//按钮 char filename[10];//文件名 }gtk;
进度条挂件功能就是通过这个数据结构进行初始化的。
进度条功能的实现目的有两个,第一个是为了美化界面;第二个是为了让使用者了解到软件还在正常运作中,由于进度条的展示时间是和标签以及计算或者核验线程所关联的,因此需要使用锁的结构。当完成了一个计算后,程序会对该线程进行上锁,对进度条进行解锁,将正在执行的进度条百分比直接调整到百分百后停止运行,代表着执行结束,使用者可以进行下一步的操作。核心代码如下:
int set_progress(gtk *p){//进度条 int i; val = val + 0.05; if(val>1) val = 0; if(st == 1){ //锁被拨到1,代表处于计算模式 gtk_label_set_text( GTK_LABEL(p->label[0]), files[flag]); gtk_label_set_text( GTK_LABEL(p->label[1]), "文件夹"); gtk_label_set_text( GTK_LABEL(p->label[2]), "计算中"); } if(st == 2){ //锁被拨到2,代表处于核验模式中 gtk_label_set_text( GTK_LABEL(p->label[0]), files[flag]); gtk_label_set_text( GTK_LABEL(p->label[1]), "文件夹"); gtk_label_set_text( GTK_LABEL(p->label[2]), "核验中"); } if(flag == -1){ //锁被拨到-1,代表功能执行结束 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(p->progress),1); gtk_label_set_text( GTK_LABEL(p->label[0]),"所有"); gtk_label_set_text( GTK_LABEL(p->label[1]), "文件夹"); gtk_label_set_text( GTK_LABEL(p->label[2]), "执行完毕"); if(check_database()) for(i=0;i<6;i++) gtk_widget_set_sensitive (p->button[i],TRUE); flag = 0; return 0; } gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(p->progress),val); return 1; }
根据文件夹的变换,进度条下方的标签也会随着一起改变,直到所要求的操作全部完成。
4.3.2文件路径实时显示功能
在设计之初,考虑使用的是只使用一个textview文本视图进行文件进度的实时显示,但是文本视图只支持一次性输入多行文件,不支持多次输入,因此我设置了10个文本视图用于显示文件进度,并设计了方案用于判断每个文件路径应该显示在哪个文本视图中。核心代码如下:
for(i=0;i<10;i++){ itoa(i+1,t); strcat(c,t); GtkTextView *textview = GTK_TEXT_VIEW(gtk_builder_get_object (p.builder, c)); p.textview[i] = textview; strcpy(c,"textview"); strcpy(t,""); } int show(gtk *p){//具体状态显示屏 char t1[10]; int i; MYSQL mysql; MYSQL_RES res ; MYSQL_ROW row ; gchar *t2; GtkTextBuffer *buffer; GtkTextIter start_find, end_find; mysql_init(&mysql); mysql_real_connect(&mysql,"localhost","root","root","AuCode",3306,NULL,0); char cmd[100]; itoa(num,t1); strcpy(cmd,"");//组装cmd命令 strcat(cmd,"select road from "); strcat(cmd,p->filename); strcat(cmd," where id = "); strcat(cmd,t1); strcat(cmd,";"); mysql_query(&mysql, cmd); res = *mysql_store_result(&mysql); if((row = mysql_fetch_row(&res))==NULL){//是空的 printf("over2\n"); num = 1; return 0; } if(num<10){ //还没有填满视图前,递归向下显示文本视图 buffer = gtk_text_view_get_buffer(p->textview[num]); //申请内存 gtk_text_buffer_set_text (buffer,row[0],-1); //设置文本内容 } else{//当所有的文本视图都被填满后,每次都会先将各个视图的内容向上移动一个再在最后显示新的文本内容。 for(i=1;i<10;i++){ buffer = gtk_text_view_get_buffer(p->textview[i]); //申请内存 gtk_text_buffer_get_start_iter(buffer, &start_find); //确定起始地址 gtk_text_buffer_get_end_iter(buffer, &end_find); //确定终止地址 t2 = (char *)gtk_text_buffer_get_text(buffer, &start_find, &end_find,FALSE); //获取内容 buffer = gtk_text_view_get_buffer(p->textview[i-1]); //重新申请 gtk_text_buffer_set_text (buffer,(char *)t2,-1); //放置到上层 } buffer = gtk_text_view_get_buffer(p->textview[9]); //新的文本内容内存申请 gtk_text_buffer_set_text (buffer,row[0],-1); //放置 } num++; mysql_close(&mysql); return 1; }
4.4 非核心功能的实现
4.4.1 数据库交互功能
数据库的交互主要涉及到了初始化、执行命令和取出数据三个功能,所有的执行函数都被封包在了函数中,只需要进行调用便可以实现和数据库的交互。在程序中由于在多个不同的地方都需要进行数据库的交互,因此数据库的交互功能零碎地分散在各处,其核心代码大体如下:
初始化:
void init_mysql(MYSQL mysql,char *tablename){//初始化数据库 char *cmd; cmd = calloc(strlen(tablename)+1000,sizeof(char)); mysql_init(&mysql); if(mysql_real_connect(&mysql,"localhost","root","root","AuCode",3306,NULL,0))//链接 printf("Database into successfully.\n"); else { mysql_real_connect(&mysql,"localhost","root","root",NULL,3306,NULL,0); if (!mysql_query(&mysql, "create database AuCode;")) { mysql_query(&mysql, "use AuCode;"); printf("creat database successfully!\n"); } else printf("ERROR: create database failed\n"); } strcpy(cmd,"");//组装cmd命令 strcat(cmd,"create table "); strcat(cmd,tablename); strcat(cmd," (id INTEGER NOT null primary key,road varchar(1000) BINARY NOT null,aucode char(33) NOT null,flag INTEGER NOT null);"); check_table(mysql,cmd,tablename); strcpy(cmd,""); strcat(cmd,"create table wrong (id INTEGER NOT null ,road varchar(1000) BINARY NOT null primary key,flag INTEGER NOT null);"); check_table(mysql,cmd,"wrong"); mysql_close(&mysql);//关闭 free(cmd); }
指令执行(部分):
strcpy(cmd,"");//组装cmd命令 strcat(cmd,"insert into "); strcat(cmd,tablename); strcat(cmd," (id,road,aucode,flag) values("); strcat(cmd,num); strcat(cmd,",\""); strcat(cmd,temp2); strcat(cmd,"\",'"); strcat(cmd,the_md5); strcat(cmd,"',0);"); id++; if (mysql_query(&mysql, cmd) !=