一、背景介绍
目前基于合规性要求,多数用户已经部署或打算部署数据库审计产品,市面审计产品通过旁路形式,对多种数据库进行综合审计(常见的oracle、mysql、sqlserver、db2等都有较好的支持),其核心原理是采集网络数据流量,通过已有的协议树进行匹配解析,最终获取请求语句信息、返回结果信息等内容。
本文基于C语言,通过对Mysql的协议深度解析,识别网络流量中的请求信息及返回结果信息,通过 Console的形式对外输出。由于环境有限,本文是解析已有的pcap数据包文件,后期可通过扩展libpcap进行旁路采集,利用已有的mysql协议解析代码实现mysql的实时审计。关于libpcap使用,本文不过多介绍。
在对mysql解析之前,如需要对mysql协议加深了解,请参考MYSQL手册及mysql协议分析,本文不过多对mysql协议进行介绍。
本文还有很多未完善之处,待后续迭代。
二、Mysql网络协议解析
在读者对第三节代码阅读时,建议读者先了解mysql的网络协议,以便更好读懂代码,如果读者非常了解mysql相关协议,可跳过该节。
mysql数据类型包括:整数型和字符串型。整数型分为定长型,如:int<1>、int<2>、int<3>、int<4>等。变长型INT<lenenc>,所存储字节数大小取决于第一个字节的数值大小。
字符串型分为固定长度的字符串、NULL结束的字符串、可变的字符串、长度编码字符串、EOF。
Mysql协议包报文格式如下:
例如COM_INIT_DB(切换数据库)整包字节序列为:07 00 00 00 02,如下图:
图中标识“74 72 73 61 70 70”十六进制转文本后为trsapp,即我们所使用的数据库名。
热身完毕后,我们来看Mysql是怎样交互的。如下图:
首先mysql是基于TCP协议,所以在建立连接与断开连接时需要三次握手和四次挥手。当客户端连接服务端时,首先TCP三次握手建立连接,随后客户端将用户信息(用户名、密码等)发送与服务端,服务端确认后返回ok_pack/error_pack包信息, OK后,客户端会发送指令至服务端,首先是use database信息,服务端返回ok信息,随后进行select、insert等指令操作。如下图:
客户端要与服务端断开时,发送COM_QUIT(0x01 )数据包,随后进行tcp四次挥手结束。
具体mysql状态码如下:
当我们发送query查询语句时,其流程如下:
客户端发起query查询后,如下图:
mysql返回内容格式为:field_cout字段信息+EOF+返回数据信息,如下图:
1.response返回-file_count字段信息
2.EOF包信息
3.返回数据信息
EOF Packet,表示结果数据完毕,若此包中的MORE_RESULT不为0 ,则说明接下来还有结果信息,又返回源头继续开始。
如果过程中读到任何一个error包后,读取将结束,并抛出错误。
以上是mysql协议核心介绍。想必你已经相对了解mysql的协议交互过程啦,如想深入了解,请参考mysql文献资料。
三、效果图及源代码
3.1 navicat请求效果图
3.2 数据包解析效果图
/*
============================================================================
Name : DatabaseAudit.c
Author : wangfeng
Version :
Copyright : Your copyright notice
Description : DatabbaseAudit in C, Ansi-style
============================================================================
*/
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include <arpa/inet.h>
#include<time.h>
#include "DatabaseAudit.h"
//字节流转换为十六进制字符串
void ByteToHexStr(const unsigned char* source, char* dest, int sourceLen)
{
short i;
unsigned char highByte, lowByte;
for (i = 0; i < sourceLen; i++)
{
highByte = source[i] >> 4;
lowByte = source[i] & 0x0f ;
highByte += 0x30;
if (highByte > 0x39)
dest[i * 2] = highByte + 0x07;
else
dest[i * 2] = highByte;
lowByte += 0x30;
if (lowByte > 0x39)
dest[i * 2 + 1] = lowByte + 0x07;
else
dest[i * 2 + 1] = lowByte;
}
return ;
}
void HexStrToByte(const char* source, unsigned char* dest, int sourceLen)
{
short i;
unsigned char highByte, lowByte;
for (i = 0; i < sourceLen; i += 2)
{
highByte = toupper(source[i]);
lowByte = toupper(source[i + 1]);
if (highByte > 0x39)
highByte -= 0x37;
else
highByte -= 0x30;
if (lowByte > 0x39)
lowByte -= 0x37;
else
lowByte -= 0x30;
dest[i / 2] = (highByte << 4) | lowByte;
}
return ;
}
/* 返回ch字符在sign数组中的序号 */
int getIndexOfSigns(char ch)
{
if(ch >= '0' && ch <= '9')
{
return ch - '0';
}
if(ch >= 'A' && ch <='F')
{
return ch - 'A' + 10;
}
if(ch >= 'a' && ch <= 'f')
{
return ch - 'a' + 10;
}
return -1;
}
/* 十六进制数转换为十进制数 */
long hexToDecNew(char *source)
{
long sum = 0;
long t = 1;
int i, len;
len = strlen(source);
for(i=len-1; i>=0; i--)
{
sum += t * getIndexOfSigns(*(source + i));
t *= 16;
}
return sum;
}
void analysisMysql(){
//define
//pcap_file_head *pcapFileHead;
pcap_head *pcapHead;
frame_head *frameHead;
ip_head *ipHead;
tcp_head *tcpHead;
mysql_request_head * mysqlHead, * mysqlCHHead;
mysql_request_eof *mysqlRequestEof;
int offset=0,dataOffset=54;
char src_ip[STRSIZE], dst_ip[STRSIZE] ;
unsigned char *mysqlTmp = (unsigned char *)malloc(sizeof(unsigned char));
int src_port, dst_port, tcp_flags;
int i;
FILE *fp; //文件
//初始化,动态内存分配
//pcapFileHead = ( pcap_file_head *)malloc(sizeof( pcap_file_head));
pcapHead = ( pcap_head *)malloc(sizeof( pcap_head));
frameHead = ( frame_head *)malloc(sizeof(frame_head));
ipHead = (ip_head *)malloc(sizeof(ip_head));
tcpHead = (tcp_head *)malloc(sizeof(tcp_head));
mysqlHead = (mysql_request_head *)malloc(sizeof(mysql_request_head));
mysqlCHHead =(mysql_request_head *)malloc(sizeof(mysql_request_head));
mysqlRequestEof=(mysql_request_eof *)malloc(sizeof(mysql_request_eof));
int mysqlEof= 0;
if((fp=fopen("/home/roo/eclipse-workspace/DatabaseAudit/src/mysql1.pcap","rb"))==NULL){
printf("error: can not open pcap file\n");
exit(0);
}
offset = 24; //pcap文件头结构 24个字节
//fseek函数是 用来设定文件的当前读写位置.
while(fseek(fp, offset, SEEK_SET) == 0) //遍历数据包(fseek 文件随机定位 文件/偏移值/从0开始)
{
dataOffset=54;
/**
* ---------pcap数据帧-------
*/
if(fread(pcapHead, sizeof(pcap_head), 1, fp) != 1){
break;
}
offset += 16 + pcapHead->caplen; //数据包长度
/**
* ---------数据帧-------
*/
if(fread(frameHead,sizeof(frame_head), 1, fp)!= 1){
break;
}
printf("destination: %02x:%02x:%02x:%02x:%02x:%02x \n",frameHead->dstMAC[0], frameHead->dstMAC[1],frameHead->dstMAC[2],frameHead->dstMAC[3],frameHead->dstMAC[4], frameHead->dstMAC[5]);
printf("src: %02x:%02x:%02x:%02x:%02x:%02x \n",frameHead->srcMAC[0], frameHead->srcMAC[1],frameHead->srcMAC[2],frameHead->srcMAC[3],frameHead->srcMAC[4], frameHead->srcMAC[5]);
printf("frame type: 0x%x---------%x\n", ntohs(frameHead->frameType),frameHead->frameType);
/**
* ---------IP帧-------
*/
if(fread(ipHead,sizeof(ip_head), 1, fp)!= 1){
break;
}
inet_ntop(AF_INET, (void *)&(ipHead->SrcIP), src_ip, 16); //将数值格式转化为点分十进制的ip地址格式
inet_ntop(AF_INET, (void *)&(ipHead->DstIP), dst_ip, 16);//将数值格式转化为点分十进制的ip地址格式
printf("src_ip:::%s dst_ip:::::%s Flag_Segment::::: %x \n",src_ip,dst_ip,ipHead->Flag_Segment);
/**
* ---------TCP帧-------
*/
if(fread(tcpHead, sizeof(tcp_head), 1, fp)!= 1){
break;
}
//网络字节序和主机字节序的转换
src_port = ntohs(tcpHead->SrcPort);
//printf("-------%d-------%d\n",tcp_header->SrcPort,src_port);
dst_port = ntohs(tcpHead->DstPort);
tcp_flags = tcpHead->Flags;
printf("src_port:::%d dst_port:::%d tcp_flags::: %x \n",src_port,dst_port,tcp_flags);
/**
* ---------MYSQL帧-------
*/
if(fread(mysqlHead, sizeof(mysql_request_head), 1, fp)!= 1){
break;
}
dataOffset+=sizeof(mysql_request_head);
printf("packetLength:%x \npacketNumber:%d \nlen:%d\n",mysqlHead->packetLength[0],mysqlHead->packetNumber,pcapHead->caplen);
//遍历column信息,应再判断上一个数据包是否为query请求[返回信息]
if(mysqlHead->packetLength[0] == 0x01 && mysqlHead->packetLength[1] == 0x00 && mysqlHead->packetLength[2] == 0x00 && mysqlHead->packetNumber == 0x1){
//读取列数
if(fread(mysqlTmp, sizeof(unsigned char), 1, fp)!= 1){
break;
}
dataOffset+=sizeof(unsigned char);
//遍历该tcp会话下的所有mysql的ColumnDefinition包
//https://blog.csdn.net/dawn_sf/article/details/80800183
//https://www.callmejiagu.com/2018/10/29/%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%B7%B1%E7%9A%84%E6%95%B0%E6%8D%AE%E5%BA%93%E9%A9%B1%E5%8A%A8%E2%80%94%E2%80%94MySQL%E5%8D%8F%E8%AE%AEResult%E5%8C%85%E8%A7%A3%E6%9E%90%EF%BC%88%E5%85%AD%EF%BC%89/
//https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnDefinition
printf("列定义信息 \n");
for(i=0;i<*mysqlTmp;i++){
if(fread(mysqlCHHead, sizeof(mysql_request_head), 1, fp)!= 1){ //读取mysql头部
break;
}
dataOffset+=sizeof(mysql_request_head);
//printf("%d--",sizeof(mysqlCHHead->packetLength[0]));
//动态创建对应mysqldata内存
unsigned char *mysqlData = (unsigned char *)malloc(mysqlCHHead->packetLength[0] * sizeof(unsigned char));
if(fread(mysqlData, (mysqlCHHead->packetLength[0] * sizeof(unsigned char)), 1, fp)!= 1){ //读取mysql数据
break;
}
printf("%s\n",mysqlData);
dataOffset+=mysqlCHHead->packetLength[0] * sizeof(unsigned char);
free(mysqlData);
// fseek(fp, (mysqlCHHead->packetLength[0]), SEEK_CUR); //下一个mysql包
}
//再读取一个eof包表示ColumnDefinition包流结束.
//fe 00 00 22 00
//读取mysql--eof
if(fread(mysqlRequestEof, sizeof(mysql_request_eof), 1, fp)!= 1){
break;
}
dataOffset+=sizeof(mysql_request_eof);
//printf("%x---%x--%x--%x--%x",mysqlRequestEof->header,mysqlRequestEof->warnings[0],mysqlRequestEof->warnings[1],mysqlRequestEof->status_flags[0],mysqlRequestEof->status_flags[1]);
//判断 eof
if(mysqlRequestEof->header == 0xfe){
mysqlEof++;
}
printf("查询返回信息\n");
while(dataOffset<=pcapHead->caplen){
//获取查询的结果信息
if(fread(mysqlCHHead, sizeof(mysql_request_head), 1, fp)!= 1){
break;
}
dataOffset+=sizeof(mysql_request_head);
//获取请求信息
char pLenArray[6];
sprintf(pLenArray,"%02x%02x%02x",mysqlCHHead->packetLength[2],mysqlCHHead->packetLength[1],mysqlCHHead->packetLength[0]);
long pLen =hexToDecNew(pLenArray);//读取对应的row内容大小
unsigned char *mysqlRowData = (unsigned char *)malloc(pLen * sizeof(unsigned char));
dataOffset+=pLen * sizeof(unsigned char);
for(i=0;i<pLen;i++){
if(fread(mysqlRowData, (sizeof(unsigned char)), 1, fp)!= 1){ //读取mysql rom数据
continue;
}
printf("%s",mysqlRowData);
}
printf("\n");
free(mysqlRowData);
}
free(mysqlCHHead);
}else{
printf("查询信息:\n");
unsigned char *mysqlData_new = (unsigned char *)malloc(mysqlHead->packetLength[0] * sizeof(unsigned char));
if(fread(mysqlData_new, (mysqlHead->packetLength[0] *sizeof(unsigned char)), 1, fp)!= 1){
break;
}
printf("%s\n",mysqlData_new);
free(mysqlData_new);
}
}
fclose(fp);
}
int main(void) {
analysisMysql();
return EXIT_SUCCESS;
}