C++ 黑客编程揭秘与防范(第3版)
上QQ阅读APP看书,第一时间看更新

2.1.4 字节顺序

前面介绍了关于TCP和UDP通信时使用到的一些基本的函数和数据结构,其中提到了字节序的概念,字节序的存在是由于不同架构CPU在访问数据时所采取的顺序不同,本小节来介绍一下关于字节序的内容。

在计算机内存中对数值的存储有一定的标准,而该标准随着系统架构的不同而不同。了解字节存储顺序对于逆向工程是一项基础的知识,在动态分析程序的时候,往往需要观察内存数据的变化情况,如果不了解字节存储顺序,那么可能会迷失在内存的汪洋大海中而无法继续逆向航行。这里有必要介绍字节序的相关知识。

1.字节序基础

通常情况下,数值在内存中存储的方式有两种,一种是大尾方式(大尾字节序就是网络字节序),另一种是小尾方式。

先来看一个简单的例子,比如0x01020304这样一个数值,如果用大尾方式存储,其存储方式为01 02 03 04,而用小尾方式存储则是04 03 02 01。这样表示也许不直观,用表格的形式展示其具体的区别,如表2-3所示。

表2-3 字节顺序的对比表

从表中可以得到如下结论。

大尾存储方式:内存高位地址存放数据低位字节数据,内存低位地址存放数据高位字节数据;

小尾存储方式:内存高位地址存放数据高位字节数据,内存低位地址存放数据低位字节数据。

2.主机字节序与网络字节序

主机字节序与网络字节序是相对的概念。

所谓主机字节序,是指主机在存储数据时的字节顺序,主机字节序根据系统架构的不同而不同。通常情况下,Windows操作系统兼容的CPU为小尾方式,而UNIX操作系统所兼容的CPU多为大尾方式。因此,主机字节序并非固定的字节序,需要根据不同的系统架构进行确定。

所谓网络字节序,是指网络传输相关协议所规定的字节传输顺序,TCP/IP所使用的字节序为大尾方式。

3.字节序相关函数

涉及字节序常用的相关函数有htons()、htonl()、ntohs()和ntohl()。这4个函数的定义分别如下:

        u_short htons(u_short hostshort);

        u_long htonl(u_long hostlong);

        u_short ntohs(u_short netshort);

        u_long ntohl(u_long netlong);

在Windows下,使用以上4个转换函数会改变值的大小,因为其在内存中的存放方式改变了。如果在UNIX系统下,使用以上4个转换函数是不会发生任何改变的。无论是何种系统,在进行网络开始时都需要调用这些函数进行转换,因为这样做可以有效的保证在网络中传输的确实是网络字节序。

4.编程判断主机字节序

“编程判断主机字节序”是很多杀毒软件公司或者是安全开发职位的一道面试题,因为这题比较基础。通过前面的知识,相信读者能够很容易地实现该程序。这里给出笔者自己对于该题目的实现方法。笔者认为,完成该题目有两种方法,第1种方法是“取值比较法”,第2种方法是“直接转换比较法”。(注:这两种方法是笔者自己这么称呼的。是否有第3种方法,笔者暂时没想到,难道有两种方法还不够吗?)

方法一:取值比较法

所谓取值比较法,首先定义一个4字节的十六进制数。因为使用调试器查看内存最直观的就是十六进制值,所以定义十六进制数是一个操作起来比较直观的方法。而后通过指针方式取出这个十六进制数在“内存”中的某一字节,最后和实际数值中相对应的数进行比较。由于字节序的问题,内存中的某字节与实际数值中对应的字节可能不同,这样就可以确定字节序了。

代码如下:

        int main(int argc, char* argv[])
        {
            DWORD dwSmallNum = 0x01020304;
            if ( *(BYTE *)&dwSmallNum == 0x04 )
            {
                printf("Small Sequence. \r\n");
            }
            else
            {
                printf("Big Sequence. \r\n");
            }
            return 0;
        }

以上代码中,定义了0x01020304这个十六进制数,其在小尾方式内存中的存储顺序为04 03 02 01。取*(BYTE *)&dwSmallNum内存中低地址位的值,如果是小尾方式的话,那么低地址位存储的值为0x04,如果是大尾方式则为0x01。

方法二:直接转换比较法

所谓直接转换比较法,是利用字节序转换函数将所定义的值进行转换,然后用转换后的值和原值进行比较。如果原值与转换后的值相同,说明为大尾方式,否则为小尾方式。

代码如下:

        int main(int argc, char* argv[])
        {
            DWORD dwSmallNum = 0x01020304;
            if ( dwSmallNum == htonl(dwSmallNum) )
            {
                printf("Big Sequence. \r\n");
            }
            else
            {
                printf("Small Sequence. \r\n");
            }
            return 0;
        }

这种方法比较直接,如果转换后的结果与原值相等,就说明是大尾方式,因为转换后的结果是网络字节序,网络字节序等同于大尾方式。

关于字节序的内容读者一定要自行调试体会一下,因为在网络开发中只需要进行简单的转换即可,不需要过多的关心它的细节。而如果是做逆向工程时,在内存中要进行数据的查找时,这时字节序的知识会使用到了。