前言
网络上很多说字节序的文章,那么为什么还要写这篇文章呢?我认为这是很关键且很基础的问题,但经过查阅网上资料,许多博客仅仅画个图描述下存储,但是却没有告诉你问题的关键,既什么时候需要考虑字节的顺序。那么我们在这篇文章中将学习在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。
总结
字节序是计算机中的基础知识,只要涉及到大于一个字节对象的存与取,都会与字节序息息相关。
发表评论
您还未登录,请先登录。
登录