Posted on ::

字节序的概念

我们用代码说话

let byts: [u8; 8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
println!("0x{:x}", u64::from_le_bytes(byts))

可以得到

0x807060504030201

这就是小端序, 又称本地序.

来看看大端序, 又称网络序, 其转换则相反.

let byts: [u8; 8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
println!("0x{:x}", u64::from_be_bytes(byts))

输出为

0x102030405060708

可以看出大端序更符合我们的阅读习惯, 不过小端序的性能更好

// 大端序转换
[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] -> 0x102030405060708
// 小端序转换
[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] -> 0x807060504030201

在c语言中, 我们可以这样

uint8_t byts[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint64_t n;
memcpy(&n, byts, 8);
printf("0x%lx", n);

得到的依旧是

0x807060504030201

所以一般在c语言里, 类型转换是小端序的.

那么当我们拿到一个bytearr, 我们对这个数组进行转换时也应该是小端序的.

我们接下来看另外一个例子

uint32_t u32arr[2] = {0x01020304, 0x05060708};
uint64_t n;
memcpy(&n, u32arr, 8);
printf("0x%lx", n);

结果是

0x506070801020304

反过来, 我们试试

uint32_t u32arr[2];
uint64_t n = 0x506070801020304;
memcpy(u32arr, &n, 8);
printf("u32arr: [0x%x, 0x%x]\n", u32arr[0], u32arr[1]);

得到

u32arr: [0x1020304, 0x5060708]

在rust中也可以实现

let n: u64 = 0x506070801020304;
let u32arr = unsafe { std::mem::transmute::<u64, [u32; 2]>(n) };
println!("{:#x?}", u32arr)

可以得到

[ 0x1020304, 0x5060708, ]

储存小端序的数据

我们写一段程序来看看

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

uint64_t n = 0x506070801020304;

int main() {
  uint32_t u32arr[2];
  memcpy(u32arr, &n, 8);
  printf("u32arr: [0x%x, 0x%x]\n", u32arr[0], u32arr[1]);
  return 0;
}

看看伪代码和.data

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __printf_chk(2, "u32arr: [0x%x, 0x%x]\n", n, unk_4014);
  return 0;
}
.data:0000000000004010                 public n
.data:0000000000004010 n               db    4                 ; DATA XREF: main↑o
.data:0000000000004011                 db    3
.data:0000000000004012                 db    2
.data:0000000000004013                 db    1
.data:0000000000004014 unk_4014        db    8
.data:0000000000004015                 db    7
.data:0000000000004016                 db    6
.data:0000000000004017                 db    5
.data:0000000000004017 _data           ends

nu32arr[0], unk_4014u32arr[1], 可以看到数据是以小端序储存的, 那么就需要注意直接提取hex的uint32_t是错误的

也就是使用SHIFT+E提取04030201h实际上并不是n的值

一个示例

到这里, 我们可以看一下一个简单的示例(自己动手搓的可能某些地方不好)

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

uint8_t moon[16] = {
    0xac, 0xd7, 0xcc, 0xb2, 0x83, 0xd1, 0x8c, 0xa0,
    0xd1, 0xe1, 0x91, 0xf6, 0xbe, 0xe9, 0xfc, 0xf7,
};

void x0llia(uint8_t s[], int len);

int main() {
  int len;
  uint8_t Str[0x114];
  printf("This is a simple test! Please enter your Ciallo!~ >_<:\n");

  scanf("%s", Str);

  len = strlen((char *)Str);

  if (len != 16) {
    printf("Your key is very bbbaddd!");
    return 0;
  }

  x0llia(Str, len);

  for (int i = 0; i < len; i++) {
    if (moon[i] != Str[i]) {
      printf("Your key is bbbaddd!");
      return 0;
    }
  }
  printf("Great!");
  return 0;
}

void x0llia(uint8_t s[], int len) {
  uint32_t temp[4];
  memcpy(&temp, s, len);
  for (int i = 0; i < len / 4; i++) {
    temp[i] ^= 0xDEADBEEF;
  }
  memcpy(s, &temp, len);
}

伪代码还是很清晰

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 moon; // rdx
  __int64 v5; // rax
  char s[280]; // [rsp+0h] [rbp-130h] BYREF
  unsigned __int64 v7; // [rsp+118h] [rbp-18h]

  v7 = __readfsqword(0x28u);
  puts("This is a simple test! Please enter your Ciallo!~ >_<:");
  __isoc99_scanf("%s", s);
  if ( strlen(s) == 16 )
  {
    x0llia(s);
    v5 = 0;
    while ( moon[v5] == s[v5] )
    {
      if ( ++v5 == 16 )
      {
        __printf_chk(2, "Great!", moon);
        return 0;
      }
    }
    __printf_chk(2, "Your key is bbbaddd!", moon);
  }
  else
  {
    __printf_chk(2, "Your key is very bbbaddd!", moon);
  }
  return 0;
}

__int64 __fastcall x0llia(void *dest, int n3)
{
  _DWORD *src_1; // rax
  const void *src; // rdi
  int v5; // edx
  _QWORD v7[8]; // [rsp+0h] [rbp-40h] BYREF

  v7[3] = __readfsqword(0x28u);
  src_1 = (_DWORD *)__memcpy_chk(v7, dest, n3, 16);
  src = src_1;
  if ( n3 > 3 )
  {
    v5 = 0;
    do
    {
      ++v5;
      *src_1++ ^= 0xDEADBEEF;
    }
    while ( v5 < n3 / 4 );
  }
  memcpy(dest, src, n3);
  return 0;
}

首先, 你可以直接还原x0llia这个函数, 但是搞清楚, 字节序的问题其实出现在memcpy

memcpy(&temp, s, len);
for (int i = 0; i < len / 4; i++) {
temp[i] ^= 0xDEADBEEF;
}
memcpy(s, &temp, len);

在伪代码可以看到src_1 = (_DWORD *)__memcpy_chk(v7, dest, n3, 16); 这里是一个_DWORD, 那么我们应该每次取一个uint32_t类型的数据进行异或

那么我们看看究竟如何才能解密出原文

.data:0000000000004010 moon            db 0ACh, 0D7h, 0CCh, 0B2h, 83h, 0D1h, 8Ch, 0A0h, 0D1h
.data:0000000000004010                                         ; DATA XREF: main+86↑o
.data:0000000000004019                 db 0E1h, 91h, 0F6h, 0BEh, 0E9h, 0FCh, 0F7h

用ida的数据提取

  • hex: 直接提取实际上是大端序的, 所以实际上需要转换成 [0xB2CCD7AC, 0xA08CD183, 0xF691E1D1, 0xF7FCE9BE]
  • c arr: 变成byte arr, 后续使用类型转换 [0xAC, 0xD7, 0xCC, 0xB2, 0x83, 0xD1, 0x8C, 0xA0, 0xD1, 0xE1, 0x91, 0xF6, 0xBE, 0xE9, 0xFC, 0xF7]

exp:

fn main() {
    let enc = [
        [0xAC, 0xD7, 0xCC, 0xB2],
        [0x83, 0xD1, 0x8C, 0xA0],
        [0xD1, 0xE1, 0x91, 0xF6],
        [0xBE, 0xE9, 0xFC, 0xF7],
    ];
    for b in enc {
        print!(
            "{}",
            String::from_utf8_lossy(&(u32::from_le_bytes(b) ^ 0xDEADBEEF).to_le_bytes())
        )
    }
}

那么如果加密函数的原型是

void encrypt(uint64_t data);

并且把data变为uint32_t, 比如

0x506070801020304 -> [0x1020304, 0x50607080]

这个时候就需要注意, 在使用移位实现的时候, 不能按照人类阅读的方式去转换

比如下面的

pub fn split(data: u64) -> [u32; 2] {
    [(data >> 32) as u32, data as u32]
}

这样其实是大端序的转换

这个函数会把 0x506070801020304 直接变为 [0x50607080, 0x1020304]

所以在实际的解题代码编写过程中, 我们最好使用语言自带的类型转换而不是用算术转换, 字节序的问题可能导致解题失败.

并且一般来说, 解题都使用小端序.

Table of Contents