解剖SQLSERVER 第二篇 对数据页面头进行逆向(译)

时间:2024-08-22 08:35:32

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

http://improve.dk/reverse-engineering-sql-server-page-headers/

在开发OrcaMDF 的时候第一个挑战就是解析数据页面头部,我们知道数据页面分两部分,96字节的页面头部和8096字节的数据行

大神 Paul Randal 写了一篇文章很好的描述了页头结构,然而,即使文章描述得很详细,但是我还是找不出任何关于页头存储的格式

每一个字段的数据类型和他们的顺序

我们可以使用DBCC PAGE命令,我填充一些随机数据进去数据页面,然后把页面dump出来

页面号是(1:101):

DBCC TRACEON (3604)
DBCC PAGE (TextTest, 1, 101, 2)

结果分两部分,首先,我们获得DBCC PAGE已经格式化好的页面内容,dump出来的内容的第二部分是96字节的页面头

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

开始动手了,我们需要找出页面头部的这些数据值对应的数据类型是什么

为了简单,我们需要关注一些唯一值以便我们不会获取到某些存在含糊的数值

我们从m_freeCnt这个字段开始,我们看到m_freeCnt的值是4066,而数据行大小是8060,所以很明显,m_freeCnt的数据类型不可能是tinyint

m_freeCnt不太可能使用int类型,一种有依据的猜测是m_freeCnt有可能使用smallint类型,这让数据行足够容纳 0-8060 字节空间的数据

smallint:从-2^15(-32,768)到2^15-1(32,767)的整数数据 存储大小为 2 个字节,本人也觉得m_freeCnt这个字段的值不可能太大

现在,4066这个十进制数换成十六进制是0x0FE2. 字节交换,变成0xE20F,现在我们知道,我们已经匹配到m_freeCnt的数据类型了

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

另外,我们已经知道页头里面第一个字段的数据类型和位置

/*
Bytes Content
----- -------
00-27 ?
28-29 FreeCnt (smallint)
30-95 ?
*/

继续我们的查找,我们看到m_freeData =3895,换算成十六进制是0x0F37 字节交换后0x370F

我们发现m_freeCnt这个字段存储在m_freeCnt的后面

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

使用这个技巧,我们能匹配存储在页头的并且没有含糊的唯一数据值

不过 ,对于m_level这个字段,他跟m_xactReserved字段,m_reservedCnt字段,m_ghostRecCnt字段的值是一样的

我们怎麽知道0这个值哪个才属于m_level字段? 并且我们怎麽找出他的数据类型呢?这有可能是tinyint 到bigint类型

我们请出Visual Studio,然后shutdown SQLSERVER

把mdf文件拖入VS,VS会打开hex编辑器,我们根据页面偏移算出页面位置

101 * 8192 = 827,392

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

看着红色框给我们标出的字节内容,他已经标识出我们的页面头内容,并且确定了我们已经跳转到正确的位置

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

现在我们会填一些数值进去mdf文件里面然后保存文件,请不要胡乱在生产数据库上进行测试

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

现在我们启动SQLSERVER,然后再次运行DBCC PAGE命令

DBCC TRACEON (3604)
DBCC PAGE (TextTest, 1, 101, 2)

可以注意到,现在页面头变成了这样

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

有几个数值变了,m_xactReserved 字段先前的数值是0,现在变成了30806,将这个数字转换成十六进制并进行字节交换得到0x5678

看一下页面头,现在我们已经识别出另外一个字段的值和数据类型(smallint)

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

我们更新一下我们页头表格

/*
Bytes Content
----- -------
00-27 ?
28-29 FreeCnt (smallint)
30-49 ?
50-51 XactReserved (smallint)
30-95 ?
*/

沿着这种方法继续,把页头进行混乱修改,将修改后的页头和DBCC PAGE的输出进行关联,有可能找出这些字段的数据类型

如果你看到下面的消息,你就知道已经把页面头部搞混乱了

解剖SQLSERVER 第二篇  对数据页面头进行逆向(译)

你应该觉得自豪的,没有人能修好你胡乱修改出来的错误

我已经编好了一个页头结构表

/*
Bytes Content
----- -------
00 HeaderVersion (tinyint)
01 Type (tinyint)
02 TypeFlagBits (tinyint)
03 Level (tinyint)
04-05 FlagBits (smallint)
06-07 IndexID (smallint)
08-11 PreviousPageID (int)
12-13 PreviousFileID (smallint)
14-15 Pminlen (smallint)
16-19 NextPageID (int)
20-21 NextPageFileID (smallint)
22-23 SlotCnt (smallint)
24-27 ObjectID (int)
28-29 FreeCnt (smallint)
30-31 FreeData (smallint)
32-35 PageID (int)
36-37 FileID (smallint)
38-39 ReservedCnt (smallint)
40-43 Lsn1 (int)
44-47 Lsn2 (int)
48-49 Lsn3 (smallint)
50-51 XactReserved (smallint)
52-55 XdesIDPart2 (int)
56-57 XdesIDPart1 (smallint)
58-59 GhostRecCnt (smallint)
60-95 ?
*/

我不确定页头的其他字节跟DBCC PAGE输出的字段对应关系,我测试过的所有页面这些字节似乎都存储为0

我认为这些应该都是为将来某种用途使用的保留字节。好了, 我们已经获得页头格式,读取每个字段就很简单了

HeaderVersion = header[];
Type = (PageType)header[];
TypeFlagBits = header[];
Level = header[];
FlagBits = BitConverter.ToInt16(header, );
IndexID = BitConverter.ToInt16(header, );
PreviousPage = new PagePointer(BitConverter.ToInt16(header, ), BitConverter.ToInt32(header, ));
Pminlen = BitConverter.ToInt16(header, );
NextPage = new PagePointer(BitConverter.ToInt16(header, ), BitConverter.ToInt32(header, ));
SlotCnt = BitConverter.ToInt16(header, );
ObjectID = BitConverter.ToInt32(header, );
FreeCnt = BitConverter.ToInt16(header, );
FreeData = BitConverter.ToInt16(header, );
Pointer = new PagePointer(BitConverter.ToInt16(header, ), BitConverter.ToInt32(header, ));
ReservedCnt = BitConverter.ToInt16(header, );
Lsn = "(" + BitConverter.ToInt32(header, ) + ":" + BitConverter.ToInt32(header, ) + ":" + BitConverter.ToInt16(header, ) + ")";
XactReserved = BitConverter.ToInt16(header, );
XdesID = "(" + BitConverter.ToInt16(header, ) + ":" + BitConverter.ToInt32(header, ) + ")";
GhostRecCnt = BitConverter.ToInt16(header, );

大家可以看一下我写的pageheader类

using System;
using System.Text;
namespace OrcaMDF.Core.Engine.Pages
{
public class PageHeader
{
public short FreeCnt { get; private set; }
public short FreeData { get; private set; }
public short FlagBits { get; private set; }
public string Lsn { get; private set; }
public int ObjectID { get; private set; }
public PageType Type { get; private set; }
public short Pminlen { get; private set; }
public short IndexID { get; private set; }
public byte TypeFlagBits { get; private set; }
public short SlotCnt { get; private set; }
public string XdesID { get; private set; }
public short XactReserved { get; private set; }
public short ReservedCnt { get; private set; }
public byte Level { get; private set; }
public byte HeaderVersion { get; private set; }
public short GhostRecCnt { get; private set; }
public PagePointer NextPage { get; private set; }
public PagePointer PreviousPage { get; private set; }
public PagePointer Pointer { get; private set; }
public PageHeader(byte[] header)
{
if (header.Length != )
throw new ArgumentException("Header length must be 96.");
/*
Bytes Content
----- -------
00 HeaderVersion (tinyint)
01 Type (tinyint)
02 TypeFlagBits (tinyint)
03 Level (tinyint)
04-05 FlagBits (smallint)
06-07 IndexID (smallint)
08-11 PreviousPageID (int)
12-13 PreviousFileID (smallint)
14-15 Pminlen (smallint)
16-19 NextPageID (int)
20-21 NextPageFileID (smallint)
22-23 SlotCnt (smallint)
24-27 ObjectID (int)
28-29 FreeCnt (smallint)
30-31 FreeData (smallint)
32-35 PageID (int)
36-37 FileID (smallint)
38-39 ReservedCnt (smallint)
40-43 Lsn1 (int)
44-47 Lsn2 (int)
48-49 Lsn3 (smallint)
50-51 XactReserved (smallint)
52-55 XdesIDPart2 (int)
56-57 XdesIDPart1 (smallint)
58-59 GhostRecCnt (smallint)
60-63 Checksum/Tornbits (int)
64-95 ?
*/
HeaderVersion = header[];
Type = (PageType)header[];
TypeFlagBits = header[];
Level = header[];
FlagBits = BitConverter.ToInt16(header, );
IndexID = BitConverter.ToInt16(header, );
PreviousPage = new PagePointer(BitConverter.ToInt16(header, ), BitConverter.ToInt32(header, ));
Pminlen = BitConverter.ToInt16(header, );
NextPage = new PagePointer(BitConverter.ToInt16(header, ), BitConverter.ToInt32(header, ));
SlotCnt = BitConverter.ToInt16(header, );
ObjectID = BitConverter.ToInt32(header, );
FreeCnt = BitConverter.ToInt16(header, );
FreeData = BitConverter.ToInt16(header, );
Pointer = new PagePointer(BitConverter.ToInt16(header, ), BitConverter.ToInt32(header, ));
ReservedCnt = BitConverter.ToInt16(header, );
Lsn = "(" + BitConverter.ToInt32(header, ) + ":" + BitConverter.ToInt32(header, ) + ":" + BitConverter.ToInt16(header, ) + ")";
XactReserved = BitConverter.ToInt16(header, );
XdesID = "(" + BitConverter.ToInt16(header, ) + ":" + BitConverter.ToInt32(header, ) + ")";
GhostRecCnt = BitConverter.ToInt16(header, );
}
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine("m_freeCnt:\t" + FreeCnt);
sb.AppendLine("m_freeData:\t" + FreeData);
sb.AppendLine("m_flagBits:\t0x" + FlagBits.ToString("x"));
sb.AppendLine("m_lsn:\t\t" + Lsn);
sb.AppendLine("m_objId:\t" + ObjectID);
sb.AppendLine("m_pageId:\t(" + Pointer.FileID + ":" + Pointer.PageID + ")");
sb.AppendLine("m_type:\t\t" + Type);
sb.AppendLine("m_typeFlagBits:\t" + "0x" + TypeFlagBits.ToString("x"));
sb.AppendLine("pminlen:\t" + Pminlen);
sb.AppendLine("m_indexId:\t" + IndexID);
sb.AppendLine("m_slotCnt:\t" + SlotCnt);
sb.AppendLine("m_nextPage:\t" + NextPage);
sb.AppendLine("m_prevPage:\t" + PreviousPage);
sb.AppendLine("m_xactReserved:\t" + XactReserved);
sb.AppendLine("m_xdesId:\t" + XdesID);
sb.AppendLine("m_reservedCnt:\t" + ReservedCnt);
sb.AppendLine("m_ghostRecCnt:\t" + GhostRecCnt);
return sb.ToString();
}
}
}

第二篇完