红队安全研发系列之字节序研究

阅读量    76861 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

前言

网络上很多说字节序的文章,那么为什么还要写这篇文章呢?我认为这是很关键且很基础的问题,但经过查阅网上资料,许多博客仅仅画个图描述下存储,但是却没有告诉你问题的关键,既什么时候需要考虑字节的顺序。那么我们在这篇文章中将学习在Windows平台上发送数据到Java平台上产生的字节序问题,且学习如何处理这个问题。而这也是信息安全中的基础知识,尤其是二进制的基础知识点。例如网上能搜索到的和序列化相关的知识都和字节序相关,并且还能找到IOT是高字节序对于strcpy发生的栈溢出需要注意的问题,又或者最基础的栈溢出都会涉及到最基础的字节序知识。

 

字节序

字节序是大于一个字节对象存储到机器,每个字节的在内存中要按照什么方式排列。而它又和CPU架构有关,CPU根据架构设计有两种存储方式,Big Endian(大端字节序)和Little Endian(小端字节序)。

大端字节序:高位字节在前,低位字节在后。(课本上二进制表示法也是高->低,默认左为高位,通俗点说就是人类通用的二进制表示法是大端)
小端字节序:低位字节在前,高位字节在后。

 

什么时候会涉及到字节序

当你要把一个大于一个字节的对象(基础数据类型Int,Long,结构体等等都是)要存储到机器中时就会涉及到这个操作,由CPU来将数据按字节存放到内存中。同样当还原数据时,按照其指定顺序还原。

 

大端存储(Java)和小端存储(Windows X86)传输实验

下列我们将使用一个大于一个字节的对象,整数int(32位)来作为实验传输的数据。实验平台则使用Big Endian方式存储数据的Java平台和Little Endian存储数据的Windows(X86 CPU)平台来实验。这样能够直观学习到两种表示法在不同平台上的应用。那么我们先看0x44332211在不同字节序是如何存储的。

下表为0x44332211的二进制表示:

0x44332211的二进制表示(按照字节分组)
0x44 0x33 0x22 0x11
01000100 00110011 00100010 00010001

下表为0x44332211在不同字节序的存储方式:

字节序
Big Endian的二进制表示 01000100 00110011 00100010 00010001
Big Endian的十六进制表示 0x44 0x33 0x22 0x11
Little Endian的二进制表示 00010001 00100010 00110011 01000100
Little Endian的十六进制表示 0x11 0x22 0x33 0x44

可以很明显的看出,当需要写入内存的数据大于一个字节时,分别按照字节的顺序进行排列,既[字节,字节,字节,字节]按照这样的存储方式来进行从大到小或者从小到大排列。

Demo

下列为Java的实验Demo代码,其功能为建立Tcp Server端,循环读取发送的数据,将其解析为一个Int整数。

package com.company;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;

public class Main {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    byte[] IntToByteArray( int data ) {
        byte[] result = new byte[4];
        result[0] = (byte) ((data & 0xFF000000) >> 24);
        result[1] = (byte) ((data & 0x00FF0000) >> 16);
        result[2] = (byte) ((data & 0x0000FF00) >> 8);
        result[3] = (byte) ((data & 0x000000FF) >> 0);
        return result;
    }

    public void start(int port) throws IOException {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        DataInputStream dis = new DataInputStream(new BufferedInputStream(clientSocket.getInputStream()));

        int number = dis.available();
        while(number >= 0){
            if(number > 0){
                int littleEndian = dis.readInt();

                //交换字节序
                byte[] littleEndArray = IntToByteArray(littleEndian);
                byte[] bigEndianArray = new byte[4];
                bigEndianArray[0] = littleEndArray[3];
                bigEndianArray[1] = littleEndArray[2];
                bigEndianArray[2] = littleEndArray[1];
                bigEndianArray[3] = littleEndArray[0];

                int BigEndian = ByteBuffer.wrap(bigEndianArray).getInt();
                BigEndian++;
            }
            number = dis.available();
        };
    }

    public void stop() throws IOException {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        Main server=new Main();
        try {
            server.start(6666);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

下列代码为Windows平台上Demo,其功能为建立一个Tcp Client,循环发送一个Int,既0x44332211到Tcp Server。

#define WIN32_LEAN_AND_MEAN
#include "stdafx.h"
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")


#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "6666"

int __cdecl main(int argc, char **argv) 
{
    WSADATA wsaData;
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo *result = NULL,
        *ptr = NULL,
        hints;

    int sendBuffer = 0x44332211;

    char recvbuf[DEFAULT_BUFLEN];
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;

    // Validate the parameters
    if (argc != 2) {
        printf("usage: %s server-name\n", argv[0]);
        return 1;
    }

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // Resolve the server address and port
    iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Attempt to connect to an address until one succeeds
    for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            return 1;
        }

        // Connect to server.
        iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 1;
    }

    while(true){
        iResult = send( ConnectSocket, (char *)&sendBuffer, sizeof(int), 0 );
        if (iResult == SOCKET_ERROR) {
            printf("send failed with error: %d\n", WSAGetLastError());
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }
        Sleep(1000);
    }

    return 0;
}

调试

首先我们将上述的Java Demo通过Idea编译器进行调试运行,再将Windows上的Demo进行调试运行。先后顺序是Java作为Tcp Server要首先接受请求,而Windows上的则是Client发送请求。当我们在Java Demo Server完成监听后,运行Windows Demo Client我们可以得到下图所示的内存布局。

Java的内存布局:

Windows的内存布局:

很清晰的看到在Windows(Little Endian)上发送的原始数据为0x44332211而内存表示为0x11223344。但是我们的Java平台是Big Endian,BigEnd上的对0x44332211数据的内存内存表示0x44332211。

所以要正确还原为原来的值0x44332211,则需要在内存中保存为0x44332211才能正确还原为原来的整数值。也就是我们需要把0x11,0x22,0x33,0x44改为0x44,0x33,0x22,0x11才能还原为原来的值。下列为转换代码,将字节顺序调换即可。

byte[] bigEndArray = new byte[4];
bigEndArray[0] = littleEndArray[3];
bigEndArray[1] = littleEndArray[2];
bigEndArray[2] = littleEndArray[1];
bigEndArray[3] = littleEndArray[0];

既把高位和低位的字节顺序交换。当我们通过调换后,我们可以看到我们的int值为1144201745,在java上也能正确表示为相同的值。

 

字节序在日常安全研究中的应用

经过上面两个平台的示例演示,我们知道了Big Endian和Little Endian。那么在日常的安全研究中,什么时候又会应用到这些知识呢?答案是时时刻刻,就如我们上述说的,只要涉及到大于一个字节的对象存储与还原,都会涉及。下面我们来看一个简单的栈溢出漏洞。

Demo

#include <stdio.h>
#include <string.h>
#include <windows.h>

void overflow(char* buf)
{
    char des[5]="";
    MessageBox(NULL,NULL,NULL,NULL);//方便使用OD调试增加的定位特征码
    strcpy(des,buf);
    return;
}
void main(int argc,char *argv[])
{
    char longbuf[]={0x61,0x61,0x61,0x61,0x61,0x61,0x61,0x61,0x61,0x61,0x61,0x61,
        //以上是填充栈的字符数,des长度为5,从des位置开始填充,填充至ret时在栈的位置根据计算需要
        //填充12个字节
        0x8d,0xf4,0x31,0x77//以上是填充和跳回esp
    };
    overflow(longbuf);
    return;
}

将上述代码进行编译,在c/c++->code generation关闭DEP和在linker->advanced中关闭buffer security check,randomized base address。

在进行调试前,我们先看看overflow函数的汇编指令。

void overflow(char* buf)
{
00411260  push        ebp  
00411261  mov         ebp,esp  
00411263  sub         esp,48h  
00411266  push        ebx  
00411267  push        esi  
00411268  push        edi  
    char des[5]="";
00411269  mov         al,byte ptr [string "" (41473Ch)]  
0041126E  mov         byte ptr [ebp-8],al  
00411271  xor         eax,eax  
00411273  mov         dword ptr [ebp-7],eax  
    MessageBox(NULL,NULL,NULL,NULL);//方便使用OD调试增加的定位特征码
00411276  push        0  
00411278  push        0  
0041127A  push        0  
0041127C  push        0  
0041127E  call        dword ptr [__imp__MessageBoxW@16 (4172C0h)]  
    strcpy(des,buf);
00411284  mov         eax,dword ptr [buf]  
00411287  push        eax  
00411288  lea         ecx,[des]  
0041128B  push        ecx  
0041128C  call        @ILT+100(_strcpy) (411069h)  
00411291  add         esp,8  
    }
    return;
}

我们可以从汇编代码得知,des指针对应的地址在栈的-8位置(EBP-8),而且EBP+4则是上一个函数的返回地址,也就是需要劫持的值。下图为函数栈的构成。

那么我们的overflow函数实际的栈为下表。

EBP – 8 des指针的值
EBP – 4
EBP main函数EBP
EBP + 4 返回地址

从上可以得知,我们从EBP-8的地址覆盖到EBP+4,一共需要十六个字节。十二个0x61作为填充数据,0x8d,0xf4,0x31,0x77则是真正的返回地址。当我们了解了如何通过栈溢出覆盖EIP后,我们将在x64dbg中调试我们的程序,看看实际情况是怎么样的。

 

X64 Dbg调试

将编译好的程序通过x64dbg载入,在x64dbg中通过bp MessageBoxW来让函数暂停到Messagebox调用中。

经过step over,返回到overflow函数。

可以从上图看到传递给strcpy的参数,des(0019FEA4)和buf(0019FF04)指针。而这时的栈没有被改写,如下图所示,指针指向的是des指针。

接下来我们在overflow函数返回前(0041129A)打上断点,并且运行到该处,可以看到EIP被改写为7731F48D。

我们单步运行后,直接 跳转到了地址7731F48D

那么为什么不是地址0x8d,0xf4,0x31,0x77呢?这就引入我们这一篇所说的知识了—字节序,我们的机器是Little Endian,而且小端字节序对应的地址正是7731F48D。

 

总结

字节序是计算机中的基础知识,只要涉及到大于一个字节对象的存与取,都会与字节序息息相关。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多