在日常的逆向过程中,我常常会因为各种数值形式上的转换而相当纠结,比如大端,小端,字节转字符串等等,而且也十分的浪费时间去纠结这些细枝末节上的事情,尤其是每次还得 google 去搜索相关的代码,所以我心想与其碎片化的从浏览器中得到自己想要的答案,不如将这些代码整理到这一篇博客内,供我未来遇到这方面的困扰时,可以直接在这里找到答案.

# 数据基本类型

  • 一个字节 = 8 位
  • qword 数据类型仅在 64 位程序中存在
类型 字节数 位数 ida 中的表示形式 举例
byte 1 8 db 0x12
word 2 16 dw 0x1234
dword 4 32 dd 0x12345678
qword 8 64 dq 0x1234567812345678

# 数据的存储形式

假设有如下 c 代码: int a = 0x12345678

# 大端存储 (Big-Endian)

数据的高字节存储在低地址中,数据的低字节存储在高地址中,顺序存储

栈内的数据的表示形式:

地址 数值
0x400001 12h
0x400002 34h
0x400003 56h
0x400004 78h

# 小端存储 (Little-Endian)

数据的高字节存储在高地址中,数据的低字节存储在低地址中,逆序存储

栈内的数据的表示形式:

地址 数值
0x400001 78h
0x400002 56h
0x400003 34h
0x400004 12h

# 在 IDA 中取出数据的某一位

假设有如下数据 a1=0x12345678 , 在 IDA 中可能会出现 HIBYTE , BYTE2 , BYTE1 , LOBYTE 来取出一个数据某一位的值,其含义如下

符号 取出的值
HIBYTE(a1) 0x12
BYTE2(a1) 0x34
BYTE1(a1) 0x56
LOBYTE(a1) 0x78

# 数字 && 字节

# 数字 => 字节

# to_bytes(length: int,byteorder: str,*,signed: bool = ...) -> bytes
# 默认 signed=False, 即无符号数表示
num = 0xef1312cd
bytes_big = num.to_bytes(4, 'big')  # 大端模式,顺序存储
print(bytes_big)  # b'\x12\xcd\x13\xef'
bytes_little = num.to_bytes(4, 'little')  # 小端模式,逆序存储
print(bytes_little)  # b'\xef\x13\xcd\x12'

# 字节 => 数字

# from_bytes(bytes: Iterable[int] | SupportsBytes,byteorder: str,*,signed: bool = ...) -> int
# 默认 signed=False, 即无符号数表示
bytes = b'\xef\x13\x12\xcd'
num_big = int.from_bytes(bytes, 'big')  # 大端模式,顺序存储
print(hex(num_big))  # 0xef1312cd
num_little = int.from_bytes(bytes, 'little')  # 小端模式,逆序存储
print(hex(num_little))  # 0xcd1213ef

# 字符串 && 字节

# 字符串 => 字节

str = "oacia"
byte = str.encode()
print(byte)# b'oacia'

# 字节 => 字符串

byte = b'oacia'
str = byte.decode()
print(str)# oacia

# 十六进制字符串 && 字节

# 十六进制字符串 => 字节

import binascii
# a2b_hex(__hexstr: str | bytes) -> bytes
hex_str = "6f61636961"
hex_bytes = binascii.a2b_hex(hex_str)
print(hex_bytes)  # b'oacia'

# 字节 => 十六进制字符串

import binascii
# b2a_hex(__data: bytes) -> bytes
hex_bytes = b'oacia'
hex_str = binascii.b2a_hex(hex_bytes)
print(hex_str)  # b'6f61636961'

# 列表 && 字符串

# 列表 => 字符串

my_list = [0x6f, 0x61, 0x63, 0x69, 0x61]
str = "".join(list(map(chr, my_list)))
print(str)# oacia

# 字符串 => 列表

str = "oacia"
my_list = list(map(ord, str))
print(my_list)  # [111, 97, 99, 105, 97]

# struct 处理字节流

当我们从 dump 一块内存后,肯定是想要再 python 读取字节流并转换成指定数值大小的数组 (如 Int 数组) 的形式,这就需要用到 struct 来处理 dump 下来的字节流

struct 模块中最重要的三个函数是 struct.pack() , struct.unpack() , struct.calcsize()

  • struct.pack(format, v1, v2, ...)
    按照给定的格式 (format),把数据封装成字节流 (实际上是类似于 c 结构体的字节流)
  • struct.unpack(format, byte)
    按照给定的格式 (fmt) 解析字节流 byte,返回解析出来的 tuple
  • struct.calcsize(format)
    计算给定的格式 (format) 占用多少字节的内存

# format 支持的格式

FORMAT C TYPE PYTHON TYPE STANDARD SIZE
x pad byte no value
c char string of length 1 1
b signed char integer 1
B unsigned char integer 1
? _Bool bool 1
h short integer 2
H unsigned short integer 2
i int integer 4
I unsigned int integer 4
l long integer 4
L unsigned long integer 4
q long long integer 8
Q unsigned long long integer 8
f float float 4
d double float 8
s char[] string
p char[] string
P void * integer 4

一些需要注意的点

  1. q 和 Q 只在机器支持 64 位操作时有意思

  2. 每个格式前可以有一个数字,表示个数

  3. s 格式表示一定长度的字符串,4s 表示长度为 4 的字符串,但是 p 表示的是 pascal 字符串

  4. P 用来转换一个指针,其长度和机器字长相关

  5. 最后一个可以用来表示指针类型的,占 4 个字节

# 字节对齐方式

我们可以通过添加前缀来指定字节流按照何种方式对齐

CHARACTER BYTE ORDER SIZE ALIGNMENT
@ native native native
= native standard none
< little-endian standard none
> big-endian standard none
! network (= big-endian) standard none

# 代码示例

假设我们从内存中 dump 下来了如下数据,已知该变量是以无符号 Int 类型存储到内存中的,因为在操作系统中数据都是小端存储,所以可以写出如下代码处理字节流规得到正确的变量的值

import struct
data = [0x6f,0x4e,0x5f,0x59,0x30,0x55,0x5f,0x46,0x89,0x7b,0xa4,0xc3,0xc5,0xe3,0x78,0xb3,0xc3,0x92,0x87,0xb0,0x92,0x94,0xa4,0xd3]
data = struct.unpack('<6I',bytes(data))
print(','.join(map(hex,list(data))))# 0x595f4e6f,0x465f5530,0xc3a47b89,0xb378e3c5,0xb08792c3,0xd3a49492

当我们在 IDA 中遇到浮点数时,同样可以使用 struct 库来查看十六进制浮点数对应的十进制值

import struct
hex_float = b'\x1f\x85\xebQ\xb8\x1e\t@'
float_num = struct.unpack('d',hex_float)#表示 double 类型
print(float_num)#(3.14,)

# ctypes 定义变量数值类型

我们知道在 python 中是没有所谓的 int , char 的概念的,但是在逆向的过程中,免不了要和这些 c 语言数据类型打交道,这个时候就需要我们使用 ctypes 库来作为 Python 和 C 之间的桥梁,来再 python 中对变量的数值类型进行定义,这样才不会导致移位右移的时候变量溢出,减法的时候变量的值被取到了负数等等我们不希望看到的情况发生

这是一个使用 ctypes 进行循环右移 16 位的简单示例,可以发现即使右移 16 位,变量的范围依然没有超出 uint

import ctypes
# 使用 c_uint 方法将 a 变量定义为 c 语言中的 uint (无符号整型), 并赋值 0x12345678
a = ctypes.c_uint(0x12345678)
# 使用 value 方法读取 a 变量的值 循环向右移动 16 位
a = ctypes.c_uint((a.value<<16) | (a.value>>16))
print(hex(a.value)) # 0x56781234

ctypes 中间类型,C 类型和 python 类型对应关系如下表:

ctypes 类型 C 类型 Python 类型
c_bool _Bool bool
c_char char 单字符字节串对象
c_wchar wchar_t 单字符字符串
c_byte char int
c_ubyte unsigned char int
c_short short int
c_ushort unsigned short int
c_int int int
c_uint unsigned int int
c_long long int
c_ulong unsigned long int
c_longlong __int64 或 long long int
c_ulonglong unsigned __int64 或 unsigned long long int
c_size_t size_t int
c_ssize_t ssize_tPy_ssize_t int
c_time_t time_t int
c_float float float
c_double double float
c_longdouble long double float
c_char_p char* (以 NUL 结尾) 字节串对象或 None
c_wchar_p wchar_t* (以 NUL 结尾) 字符串或 None
c_void_p void* int 或 None