C#对HTTP数据还原
使用C#对HTTP数据还原
[创建时间:2016-05-12 00:19:00]
在NetAnalyzer2016中加入了一个HTTP分析功能,很过用户对此都很感兴趣,那么今天写一下具体的实现方式,对我自己也算是一个总结吧,好了,废话就不多少了,直接开始吧。
本文是专注于HTTP数据的分析,所以前期数据包采集相关的内容并不会本文不会涉及,相关内容可以见 NetAnalyzer 笔记 四
在这边默认你已经获取到了http数据报文。
一,提取TCP会话数据
通过TCP/IP协议知道,我们可以通过网络层的IP地址可以唯一的确定一台主机,通过传输层的端口确定一个主机应用,而主机应用与另外一个主机应用的一次数据通信(报文交换)我们称之为一次会话,而建立的传输层协议为TCP上面的一次交互数据我们就称之为TCP会话数据。
一次常规的TCP会话
在这里我们可以看到本地主机应用标志 IP地址: 192.168.1.102 端口 55298 和远端主机应用标志 IP地址:124.238.254.191端口 80 (80端口所对应的应用协议就是HTTP),通过这组标志,而下面每行记录都代表一TCP数据包,
每个TCP包包含了TCP状态标志,载荷数据(TCP数据包封装的数据)量,序列号等信息,大家可以留意一下每两条记录之间的序列号和载荷数据量之间的关系。
我们这里要提取的数据正是这组TCP报文中的载荷数据
这段代码用于定义并生成一个TCP标志
对于这段代码的使用方法如下:
1 // 选择一个RawCapture
2 RawCapture rawPacket = PacketList[i];
3 // 通过RawCapture 获取到一个标志
4 var tmpId = new ConnIDbyIPPort(rawPacket);
通过标志位挑选会话数据
1 /// <summary>
2 /// 通过ID和连接类型获取原始数据列表
3 /// </summary>
4 /// <param name="id"></param>
5 /// <param name="connType">之定义了三种,以后会扩展</param>
6 /// <returns></returns>
7 private RawCapture[] getConnectionList(IConnectionID id, ConnectionType connType)
8 {
9 if (id == null)
10 return null;
11 try
12 {
13 //var values = from v in PacketList
14 // where id.Equals(id.CreatID(v))
15 // select v;
16 //if (values == null)
17 // return null;
18
19 List<RawCapture> values = new List<RawCapture>();
20 foreach (var i in PacketList)
21 {
22 if (id.Equals(id.CreatID(i)))
23 {
24 values.Add(i);
25 }
26 }
27
28 return values.ToArray();
29 }
30 catch (Exception ex)
31 {
32 MessageBox.Show("正在获取网络获取相关数据,请稍后再试!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
33 return null;
34
通过这段代码,我们最终获取到了这组TCP数据,接下来就是载荷数据的提取
二,TCP载荷数据的提取与组合
我们现在获取到的数据包都是TCP,接下来我们要面对两个重要的问题:
1、对于HTTP协议中传输的文本,图片,文件等数据有可能需要多个TCP数据包才能发送或接受完整;
2、存在一些载荷数据为0的TCP数据包,这些数据包对于TCP的会话特别重要(在TCP中完成确认或重传机制,保证数据可靠性),但是对于我们分析数据并没有太多的意义。
根据以上两点,我们需要对得到的这组会话数据进行重新的组合,这里的基本思路是,建立一个单向链表结构,以第一个载荷数据不为空的数据包作进行拆包提取特征量和载荷数据,然后移到下一个载荷数据不为空的数据包,提取数据包特征量,与当前节点特征量比较,如果方向一致(端口号相同)则将载荷数据缓存到当前节点,如果不一致(端口号不相同)则生成新的节点,生成特征,提取载荷数据缓存,并把上个节点指向新节点,
新结构示意图
对于每个节点的定义如下:
1 /// <summary>
2 /// TCP单向载荷数据节点
3 /// </summary>
4 class DataNode
5 {
6 /// <summary>
7 /// 构造
8 /// </summary>
9 /// <param name="port"> 端口</param>
10 public DataNode(ushort port)
11 {
12 this.Port = port;
13 this.Buffer = new List<byte>();
14 }
15
16 public DataNode NextNode { get; set; }
17
18
19 public ushort Port { get; private set; }
20 public List<byte> Buffer { get; private set; }
21
22
23 public void InsertData(byte[] data)
24 {
25 this.Buffer.AddRange(data);
26 }
27 }
构建单向列表
/// <summary>
/// 头部节点
/// </summary>
private DataNode HeadNode; //当前比对的节点
private DataNode flagNode; public void InsertData(byte[] data, ushort port)
{
// 载荷数据判空
if (data == null || data.Length == 0)
return;
// 生成头部节点
if (HeadNode == null)
{
flagNode = new DataNode(port);
HeadNode = flagNode;
}
// 插入数据 和 生成后续节点
if (port == flagNode.Port)
{
flagNode.InsertData(data);
}
else
{
var tmpNode = new DataNode(port);
flagNode.NextNode = tmpNode;
tmpNode.InsertData(data);
flagNode = tmpNode;
}
}
TCP会话的转换
1 hp = new HttpHelper.HttpHelper();
2
3 foreach (var rawPacket in PacketList)
4 {
5 Packet packet = Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data);
6 TcpPacket tcp = TcpPacket.GetEncapsulated(packet);
7 if (tcp != null)//TCP
8 {
9 hp.InsertData(tcp.PayloadData, tcp.SourcePort);
10 }
11 }
自此,我们已经获取到了,期望的数据结构模型,那么下一节就要着手开始提取HTTP信息了。
三,http特征量提取
我们知道常规的http协议包含消息头和数据两部分:消息头是由ASCII编码的多条命令行数据组成,每行之间使用CRLF(回车和换行)进行分割,消息头结束后,需要多增加一个CRLF,
一个典型的http请求与回复会话(根据上一节内容,这段数据,红色部分为头部节点,蓝色部分在第二个节点中)
有图可知,客户端发起一个 get 的请求(红色部分),该请求并没有携带数据;服务端回复 http/1.1 200 OK 表示已经接受请求,并返回数据(蓝色部分),服务请返回的数据除了http消息头 还有自己所带的数据。
我们可以很轻松的看到http协议具体的内容,但是对于数据部分是一对乱码,那么接下来任务就是要根据http消息头获取这些乱码的信息
因为本篇以实践为主,并不想讨论具体的http命令,所以此处只对部分命令做一些详细分析,其他命令,网上内容很多请自行搜索。
通过http协议对回复数据(第二个节点)查找到以下两个字段:
1. Content-Type: application/zip 通过查询MIME 我们知道这是一个*.zip文件,那也就是消息头后面的乱码事实上是一个zip压缩文件,
2. Content-Length: 7261 由此我们判断出这个zip文件的大小为 7261个字节
对于一些其他的命令对我们后续还原数据不大,直接跳过。
至此我们就找到了http协议特征量,但是因为http传输数据格式的多样性,传输过程的复杂性,有些服务器的http协议特征更为复杂
HTTP/1.1 200 OK
Date: Wed, 11 May 2016 14:47:47 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: public, max-age=137
Expires: Wed, 11 May 2016 14:50:05 GMT
Last-Modified: Wed, 11 May 2016 14:45:05 GMT
Vary: *
X-UA-Compatible: IE=10
Content-Encoding: gzip
这段http回复中可以看到数据类型变为 Content-Type: text/html外,没有了content-Length 字段,但是增加几条新的字段,分别是:
1. charset=utf-8 这部分对于数据类型为字符串的还原非常重要,尤其是对于非英语文本的还原,
2. Transfer-Encoding: chunked 这个字段,用于标出动态加载的数据 详细信息请参见:http://www.cnblogs.com/zhaozhan/archive/2010/08/24/1807639.html
3. Content-Encoding: gzip 为了缩小数据传输量,使用该字段进行数据压缩,所以我们在还原数据的时候遇到该字段就需要解压缩
好了,扯了一大堆,那我们开始准备提取这些特征字段吧
先定义一个http数据结构
接下来对上一节提到的数据链表以此进行消息头提取
1 /// <summary>
2 /// 获取http实体列表
3 /// </summary>
4 /// <param name="encode">预指定的文本编码方式</param>
5 /// <returns></returns>
6 public List<HttpEntity> GetHttpEntityList(Encoding encode)
7 {
8 List<HttpEntity> DataList = new List<HttpEntity>();
9
10 for (DataNode tmp = HeadNode; tmp != null; tmp = tmp.NextNode)
11 {
12 for (int i = 0; i <= tmp.Buffer.Count - 4; i++)
13 {
14 if (tmp.Buffer[i] == 0x0D && tmp.Buffer[i + 1] == 0x0A && tmp.Buffer[i + 2] == 0x0D && tmp.Buffer[i + 3] == 0x0A)
15 {
16 string headStr = Encoding.ASCII.GetString(tmp.Buffer.Take(i + 1).ToArray());
17 DataList.Add(getInnerStr(tmp.Buffer.Skip(i + 4).ToArray(), headStr, encode));
18 break;
19 }
20 }
21 }
22
23 return DataList;
24 }
这里使用for循环,从链表表头开始以此查找两组CRLF(0x0D 0x0A 0x0D 0x0A)直到直到之后,截取前面的数据并使用ASCII进行解码为headStr ,而后面的数据就可以根据前面获取到的消息头进行进一步还原了,在 getInnerStr() 方法中完成
接下来我们继续看getInnerStr()方法的前半部分
1 private HttpEntity getInnerStr(byte[] data, string headFlag, Encoding defaultEncode)
2 {
3
4 //最终数据呈现
5 HttpEntity result = new HttpEntity();
6 result.HeadStr = headFlag;
7
8
9 if (data == null || data.Length == 0)
10 return result;
11
12 StringBuilder sbr = new StringBuilder();
13
14 //是否使用chunked
15 bool isChunked = headFlag.ToLower().Contains("transfer-encoding: chunked");
16 bool isGzip = headFlag.ToLower().Contains("content-encoding: gzip");
17 string contentType = "";
18 string charSet = "";
19 int ContentLength = 0;
20 Encoding enCode = defaultEncode;
21 var mtype = Regex.Match(headFlag, @"Content-Type:\s*(\w+\/[\w\.\-\+]+)", RegexOptions.IgnoreCase);
22 if (mtype.Success && mtype.Groups.Count >= 2)
23 {
24 contentType = mtype.Groups[1].Value.Trim().ToLower();
25 }
26 var Mchar = Regex.Match(headFlag, @"charset=\s*([-\w]+)", RegexOptions.IgnoreCase);
27 if (Mchar.Success && Mchar.Groups.Count >= 2)
28 {
29 charSet = Mchar.Groups[1].Value.Trim();
30 if (charSet.ToLower() == "utf8")
31 {
32 charSet = "utf-8";
33 }
34 try
35 {
36 enCode = Encoding.GetEncoding(charSet);
37 }
38 catch (Exception)
39 {
40 enCode = defaultEncode;
41 }
42 }
43
44 var MLength = Regex.Match(headFlag, @"Content-Length:\s*(\d+)", RegexOptions.IgnoreCase);
45 if (MLength.Success && MLength.Groups.Count >= 2)
46 {
47 ContentLength = int.Parse(MLength.Groups[1].Value.Trim());
48 }
49 …………
这里使用正则表达式和字符串查找功能,在消息头中分别查找和去取对应的字段(对于字符编码,有些服务器使用的utf8 在解码是需要改为utf-8 否则会引起异常)
代码相对比较简短,这里就不做过多介绍了。
四,http数据解析(包括chunked 和 gzip)
通过上一节,我们已经拿到了http消息头的特征信息和数据,这部分就开始还原数据,具体有三个步骤:
1. 数据提取,数据提取基本就是找开始位置和数据长度,根据页面加载方式的不同,数据头提取分为 通过Content-Length 提取和 通过 Chunked 提取两种方法,
2. 数据解压缩,有时候我们获得的数据可能经过gzip压缩的,所以这个时候要对数据进行解压缩处理。
3. 数据还原,对于不同的数据有着不同的还原方式,转为字符串、保存为图片,生成文件等等
那接下来就开始看代码吧,首先我们补上上一节方法的后半部分
1 /// 数据整合
2 byte[] rawData = null;
3 if (isChunked)// 通过Chunked获取数据
4 {
5 GetchunkedData(data);
6 rawData = ChunkBuffer.ToArray();
7 }
8 else if (ContentLength > 0 && ContentLength <= data.Length)//通过ContentLength 获取数
9 {
10 rawData = data.Take(ContentLength).ToArray();
11 }
12 else
13 {
14 rawData = data;
15 }
16 if (rawData == null && rawData.Length == 0)
17 return result;
18
19 /// 数据解压缩
20 if (isGzip)
21 {
22 rawData = Tools.GzipDecompress(rawData);
23 }
24 if (rawData == null && rawData.Length == 0)
25 return result;
26
27
28
29 //获取扩展名
30 string extName = "";
31 if (EimeData.EimeDic.Keys.Contains(contentType))
32 {
33 extName = EimeData.EimeDic[contentType];
34 }
35 result.ExtName = extName;
36 //获取数据或类型
37 switch (Tools.GetMimeType(contentType))
38 {
39 case MimeType.Text:
40 result.Data = Tools.ShowPopFormStr(enCode.GetString(rawData));
41 result.DataType = typeof(string);
42 break;
43 case MimeType.Image:
44 result.Data = rawData;
45 result.DataType = typeof(System.Drawing.Image);
46 break;
47 default:
48 result.Data = rawData;
49 result.DataType = typeof(byte[]);
50 break;
51 }
52 return result;
首先是整合数据,我们通过chunked、content-length等特征字段对获取的数据进行重新整合,这里涉及到Chunked数据整合,那让我们一起来看看代码吧
(具体chunked数据形式自行搜索)
1 /// <summary>
2 /// Chunked数据缓存列表
3 /// </summary>
4 List<byte> ChunkBuffer = new List<byte>();
5 private void GetchunkedData(byte[] data)
6 {
7 if (data.Length == 0)
8 return;
9 for (int i = 0; i < data.Length; i++)
10 {
11 //查找CRCL标志
12 if (data[i] == 0x0D && data[i + 1] == 0x0A)
13 {
14 int count = data.Length - 2;
15 try
16 {
17 //获取下一段数据的长度 bytes -> ASCII字符 ->加上0x前缀 (如:0x34) -> 转为数值(下一块的数量长度)
18 count = Convert.ToInt32("0x" + Encoding.ASCII.GetString(data.Take(i).ToArray()), 16);
19 }
20 catch (Exception ex)
21 {
22 //产生异常的直接返回
23 ChunkBuffer.AddRange(data.Skip(i - 2).ToArray());
24 break;
25 }
26 //如果为0表示完成Chunked数据转化跳出循环返回
27 if (count == 0)
28 break;
29
30 if (i + 2 + count <= data.Length)
31 {
32 //加入已经计算好的数据
33 ChunkBuffer.AddRange(data.Skip(i + 2).Take(count));
34 //递归 进行下一轮匹配
35 GetchunkedData(data.Skip(i + 4 + count).ToArray());
36 }
37 else
38 {
39 //对于存在数据不完整的 直接返回
40 ChunkBuffer.AddRange(data.Skip(i - 2).ToArray());
41 }
42 break;
43 }
44 }
45 }
在这里预先定义一个缓存列表 ChunkBuffer, 然后通过GetchunkedData()方法递归调用最后获取到需要的数据。
此时我们已经获取到了数据,那么就可以考虑是否解压缩的问题了,解压缩比较简单,这里依然直接贴代码了
1 /// <summary>
2 /// 通过Gzip方式解压缩数据
3 /// </summary>
4 /// <param name="data">字节数据</param>
5 /// <returns></returns>
6 public static byte[] GzipDecompress(byte[] data)
7 {
8 //解压
9 using (MemoryStream dms = new MemoryStream())
10 {
11 using (MemoryStream cms = new MemoryStream(data))
12 {
13 using (System.IO.Compression.GZipStream gzip = new System.IO.Compression.GZipStream(cms, System.IO.Compression.CompressionMode.Decompress))
14 {
15 byte[] bytes = new byte[1024];
16 int len = 0;
17 int totalLen = 0;
18 //读取压缩流,同时会被解压
19 try
20 {
21 while ((len = gzip.Read(bytes, 0, bytes.Length)) > 0)
22 {
23 dms.Write(bytes, 0, len);
24 totalLen += len;
25 }
26 }
27 catch (InvalidDataException ex)
28 {
29 //dms.Write(data.Skip(totalLen).ToArray(),0,data.Length-totalLen);
30 }
31 }
32 }
33 return dms.ToArray();
34 }
35 }
这样我们就拿到了真正需要的数据了
最后我们就可以通过 Content-Type 来判断将数据还原为那种类型,就这样我们就获得了需要的数据
接下来我们看看解析完的数据
首先是zip的数据:
导出为文件后
最后是哪个带有Chunked 和gzip 标志的数据还原
五,更加可靠的数据还原
通过上面的数据还原方法,已经基本满足我们的数据还原需求,但是因为网络、计算机、以及我们程序本身的问题,我们拿到的数据包并不会严格的按照发包的先后顺序获取到数据包,而且因为TCP协议确认重传,有可能发生在某个数据包中,及发送端发送了一个1440的数据包,接收端有可能只取700个字节,剩余的需要重现传输,对于监控端,如果没有处理这种情形的机制,就会造成数据差生误差而不能进行正确还原,更别说还有丢包的情况。
而这种机制就是TCP的数据重组,通过建立合理的模型对tcp状态迁移、序列号、数量等一系列变量进行统一的规划提取还原,最终生成完成而且正确的数据。因为本篇只是简单讨论http数据的还原,所以此处不打算深入的展开, 如果有时间,再写一篇关于TCP重做的文章吧。
感谢你的阅读,欢迎使用NetAnalyzer,欢迎关注NetAnalyzer公众平台。
NetAnalzyer交流群:39753670 (PS 只提供交流平台,群主基本不说话^_^)
C#对HTTP数据还原的更多相关文章
- NetAnalyzer笔记 之 九 使用C#对HTTP数据还原
[创建时间:2016-05-12 00:19:00] NetAnalyzer下载地址 在NetAnalyzer2016中加入了一个HTTP分析功能,很过用户对此都很感兴趣,那么今天写一下具体的实现方式 ...
- 实际使用Elasticdump工具对Elasticsearch集群进行数据备份和数据还原
文/朱季谦 目录 一.Elasticdump工具介绍 二.Elasticdump工具安装 三.Elasticdump工具使用 最近在开发当中做了一些涉及到Elasticsearch映射结构及数据导出导 ...
- linux数据误删后,灾难性数据备份与数据还原
一 准备工作 #rm –rf 误删重要数据怎么办? 1. 要冷静,通知停止该服务器一切操作 2. 查看被删除文件所在分区 #mount 3. 将该分区设置为只读 #mount -r -n -o re ...
- iOS开发小技巧--适当的清空模型中的某个数据,达到自己的需求,记得最后将数据还原(百思项目评论页面处理最热评论)
一.项目需求,显示所有贴的时候,需要显示最热评论,但是点击进入相应帖子后,最热评论的label不要显示,如图: 解决方案 -- 该暂时保存的暂时保存,该清空的清空 ...
- 帝国cms数据还原后提示数据库表不存在怎么解决?
下午,ytkah用帝国cms在wamp调试时发现了一个问题,还原备份好的数据后更新的页面提示数据库表不存在,查看了phpmyadmin分类的数据库表实际上是存在的,这个是怎么回事呢?重新搭建一个新站点 ...
- SQL Server2005的数据还原与备份具体步骤
一:备份数据库步骤 1. 第一步:在开始—>程序(P)—>MicrosoftSQLserver2005—>SQLServerManagementStudio(如下图) 2. 第二步: ...
- MySQL 数据还原
1.1还原使用mysqldump命令备份的数据库的语法如下: mysql -u root -p [dbname] < backup.sq 示例: mysql -u root -p < C: ...
- 将mnist获得的数据还原成图片形式
MNIST是一个手写数字数据集,里面所包含的数据元素是类似于一个1×784矩阵和1×10矩阵的结构,那么,如何将这些数据元素转化为更加直观的图像呢?通过以下python代码,可以实现. from PI ...
- .bak文件数据还原
.bak文件还原(见下图) 1.连接上数据库,右键数据库,选择新建数据库,输入你要还原数据库的名称 2.数据库右键-->任务-->还原-->数据库,弹出窗口选择[源设备],选择.ba ...
随机推荐
- 10-UIKit(UIDatePicker、UIPickerView、UIWebView、Storyboard)
目录: 1. UIDatePicker 2. UIPickerView 3. UIPickerView多列关联 4. UIWebView 5. Storyboard(故事板) 回到顶部 1. UIDa ...
- 前端SEO优化
结构优化 1.扁平化结构,目录层次越少越好
- Python常用模块中常用内置函数的具体介绍
Python作为计算机语言中常用的语言,它具有十分强大的功能,但是你知道Python常用模块I的内置模块中常用内置函数都包括哪些具体的函数吗?以下的文章就是对Python常用模块I的内置模块的常用内置 ...
- 架构漫谈:自己开发一个Log框架
前言 在日常开发中我们常常都会用到写日志的功能,现在网上的写Log的框架有很多,但是对于我个人而言,过于庞大:我们往往只为了使用框架中的某一个功能就不得不引用整个框架. 所以,我们今天就来自己动手开发 ...
- android模拟器 一个错误:X Error of failed request: BadRequest (invalid request code or no such operation)
最近ubuntu12.04学习python,python2.7 python3.2所不同的是还是蛮大的.学习思考的新 升级后 结果显示 输入方法不显示 update-manager 和 add- ...
- CodeForces Round #173 (282E) - Sausage Maximization 字典树
练习赛的时候这道题死活超时....想到了高位确定后..低位不能对高位产生影响..并且高位要尽可能的为1..就是想不出比较好的方法了实现... 围观大神博客..http://www.cnblogs.co ...
- 【linux kernel】 中断处理-中断上半部
欢迎转载,转载时需保留作者信息,谢谢. 邮箱:tangzhongp@163.com 博客园地址:http://www.cnblogs.com/embedded-tzp Csdn博客地址:htt ...
- uboot代码2:stage2代码,启动内核
一.uboot最终目的: 1.读出内核 do_nand read kernel { flash上存的内核:uImage = 头部 + 真正的内核; } 2.启动内核. do_bootm_linux { ...
- 创建了一个基于最短路径规划geoserver的wms服务
两点之间的文章书面请求随机最短路径sql功能,这篇文章是关于如何将上述到系统中的子功能. 1.geoserver登录 首先单击geoserver安装路径下的start Geoserver 待geose ...
- deflate——过时的网页压缩格式,最好禁用[转]
在设置GZip时,发现同时有个Deflate压缩设置,一开始并不了解Deflate压缩,于是便在启用GZip的同时,也启用了Deflate压缩.虽然同时设置GZip和Deflate压缩,并不影响网站的 ...