在日常的逆向过程中,我常常会因为各种数值形式上的转换而相当纠结,比如大端,小端,字节转字符串等等,而且也十分的浪费时间去纠结这些细枝末节上的事情,尤其是每次还得 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,返回解析出来的 tuplestruct.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 |
一些需要注意的点
-
q 和 Q 只在机器支持 64 位操作时有意思
-
每个格式前可以有一个数字,表示个数
-
s 格式表示一定长度的字符串,4s 表示长度为 4 的字符串,但是 p 表示的是
pascal
字符串 -
P 用来转换一个指针,其长度和机器字长相关
-
最后一个可以用来表示指针类型的,占 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_t 或 Py_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 |