我们用代码说话
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
n为u32arr[0], unk_4014为u32arr[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; __int64 v5; char s[280]; unsigned __int64 v7;
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; const void *src; int v5; _QWORD v7[8];
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]
所以在实际的解题代码编写过程中, 我们最好使用语言自带的类型转换而不是用算术转换, 字节序的问题可能导致解题失败.
并且一般来说, 解题都使用小端序.