解剖SQLSERVER 第二篇 对数据页面头进行逆向(译)
解剖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字节的页面头
开始动手了,我们需要找出页面头部的这些数据值对应的数据类型是什么
为了简单,我们需要关注一些唯一值以便我们不会获取到某些存在含糊的数值
我们从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的数据类型了
另外,我们已经知道页头里面第一个字段的数据类型和位置
/* Bytes Content ----- ------- 00-27 ? 28-29 FreeCnt (smallint) 30-95 ? */
继续我们的查找,我们看到m_freeData =3895,换算成十六进制是0x0F37 字节交换后0x370F
我们发现m_freeCnt这个字段存储在m_freeCnt的后面
使用这个技巧,我们能匹配存储在页头的并且没有含糊的唯一数据值
不过 ,对于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
看着红色框给我们标出的字节内容,他已经标识出我们的页面头内容,并且确定了我们已经跳转到正确的位置
现在我们会填一些数值进去mdf文件里面然后保存文件,请不要胡乱在生产数据库上进行测试
前
后
现在我们启动SQLSERVER,然后再次运行DBCC PAGE命令
DBCC TRACEON (3604) DBCC PAGE (TextTest, 1, 101, 2)
可以注意到,现在页面头变成了这样
有几个数值变了,m_xactReserved 字段先前的数值是0,现在变成了30806,将这个数字转换成十六进制并进行字节交换得到0x5678
看一下页面头,现在我们已经识别出另外一个字段的值和数据类型(smallint)
我们更新一下我们页头表格
/* Bytes Content ----- ------- 00-27 ? 28-29 FreeCnt (smallint) 30-49 ? 50-51 XactReserved (smallint) 30-95 ? */
沿着这种方法继续,把页头进行混乱修改,将修改后的页头和DBCC PAGE的输出进行关联,有可能找出这些字段的数据类型
如果你看到下面的消息,你就知道已经把页面头部搞混乱了
你应该觉得自豪的,没有人能修好你胡乱修改出来的错误
我已经编好了一个页头结构表
/* 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[0]; Type = (PageType)header[1]; TypeFlagBits = header[2]; Level = header[3]; FlagBits = BitConverter.ToInt16(header, 4); IndexID = BitConverter.ToInt16(header, 6); PreviousPage = new PagePointer(BitConverter.ToInt16(header, 12), BitConverter.ToInt32(header, 8)); Pminlen = BitConverter.ToInt16(header, 14); NextPage = new PagePointer(BitConverter.ToInt16(header, 20), BitConverter.ToInt32(header, 16)); SlotCnt = BitConverter.ToInt16(header, 22); ObjectID = BitConverter.ToInt32(header, 24); FreeCnt = BitConverter.ToInt16(header, 28); FreeData = BitConverter.ToInt16(header, 30); Pointer = new PagePointer(BitConverter.ToInt16(header, 36), BitConverter.ToInt32(header, 32)); ReservedCnt = BitConverter.ToInt16(header, 38); Lsn = "(" + BitConverter.ToInt32(header, 40) + ":" + BitConverter.ToInt32(header, 44) + ":" + BitConverter.ToInt16(header, 48) + ")"; XactReserved = BitConverter.ToInt16(header, 50); XdesID = "(" + BitConverter.ToInt16(header, 56) + ":" + BitConverter.ToInt32(header, 52) + ")"; GhostRecCnt = BitConverter.ToInt16(header, 58);
大家可以看一下我写的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 != 96) 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[0]; Type = (PageType)header[1]; TypeFlagBits = header[2]; Level = header[3]; FlagBits = BitConverter.ToInt16(header, 4); IndexID = BitConverter.ToInt16(header, 6); PreviousPage = new PagePointer(BitConverter.ToInt16(header, 12), BitConverter.ToInt32(header, 8)); Pminlen = BitConverter.ToInt16(header, 14); NextPage = new PagePointer(BitConverter.ToInt16(header, 20), BitConverter.ToInt32(header, 16)); SlotCnt = BitConverter.ToInt16(header, 22); ObjectID = BitConverter.ToInt32(header, 24); FreeCnt = BitConverter.ToInt16(header, 28); FreeData = BitConverter.ToInt16(header, 30); Pointer = new PagePointer(BitConverter.ToInt16(header, 36), BitConverter.ToInt32(header, 32)); ReservedCnt = BitConverter.ToInt16(header, 38); Lsn = "(" + BitConverter.ToInt32(header, 40) + ":" + BitConverter.ToInt32(header, 44) + ":" + BitConverter.ToInt16(header, 48) + ")"; XactReserved = BitConverter.ToInt16(header, 50); XdesID = "(" + BitConverter.ToInt16(header, 56) + ":" + BitConverter.ToInt32(header, 52) + ")"; GhostRecCnt = BitConverter.ToInt16(header, 58); } 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(); } } }
第二篇完