pwn知识点——字节流
字节流(Byte Stream)在 Pwn 中至关重要,它直接涉及数据的原始字节处理,是理解漏洞和构造利用载荷(Payload)的基础。
一、字节流的基本概念
字节流是字节的序列,不包含任何固有格式或解释。在 C 语言中,字节流通常用 char
数组(或 unsigned char
数组)表示,每个元素占 1 字节。
在 Pwn 中,许多输入输出操作(例如通过 read
, write
系统调用,或 C 库函数 fgets
, printf
)本质上都是在处理字节流。漏洞常源于程序如何处理这些字节流,例如未对长度进行正确检查,或错误地解释了流中的内容。
二、Pwn 中字节流的重要性
数据传输与输入向量:程序通常从外部(如网络、文件、用户输入)获取字节流。攻击面通常就在这里,例如,向程序发送精心构造的字节流可以触发缓冲区溢出、格式化字符串等漏洞。
漏洞利用的载体:成功利用漏洞通常需要向目标程序发送特定的字节流序列(即 Payload)。Payload 是精确计算的字节序列,可能包含:
填充数据 (Junk data):用于填充缓冲区直至覆盖目标地址(如返回地址、函数指针)。
地址数据:用于覆盖目标地址,指向 Shellcode、Gadgets 或特定函数(如
system
)。Shellcode:一段实现特定功能(如获取 shell)的机器指令字节序列。
格式化字符串:利用
%n
等格式化符向任意地址写入数据的特殊字节流。
内存操作的基石:理解程序如何将字节流写入内存至关重要,这涉及字节序(Endianness)和内存布局。
字节序:指多字节数据(如地址)在内存中的存储顺序。
小端序 (Little-endian):低位字节存储在低地址。x86/x86-64 架构常用此序。
大端序 (Big-endian):高位字节存储在低地址。
在构造 Payload 时,写入的地址值必须符合目标架构的字节序。Pwntools 等工具中的
p32()
,p64()
函数能方便地将整数打包为小端序格式的字节流。
三、常见漏洞中的字节流操作
下面是一个表格,对比了常见漏洞中的字节流操作:
漏洞类型 | 危险的字节流操作函数 | 关键的字节流操作 | 利用目标 |
---|---|---|---|
栈溢出 |
| 输入超长字节流,覆盖返回地址或函数指针 | 跳转至 Shellcode 或 ROP Chain |
堆溢出 |
| 覆盖相邻堆块的元数据或用户数据,可能引发任意地址写 | 改写 |
格式化字符串 |
| 使用 | 泄露关键地址,修改返回地址或 GOT 表项 |
四、相关工具与函数
分析漏洞和构造字节流 Payload 时,常用以下工具和函数:
PWNTools:Python库,提供
p32()
,p64()
,u32()
,u64()
等函数处理字节序转换,方便远程交互和本地调试。GDB:调试器,用于动态分析程序如何处理输入的字节流,查看内存中的字节状态。
C语言中常见的危险函数(在处理字节流时易引发漏洞)包括:
gets(char *s)
: 极度危险,从不检查输入长度。strcpy(char *dest, const char *src)
: 不检查目标缓冲区大小。strcat(char *dest, const char *src)
: 同上。sprintf(char *str, const char *format, ...)
: 可能造成目标数组溢出。scanf
,printf
系列: 使用不当会导致漏洞。
相对安全函数(鼓励使用):
fgets(char *s, int size, FILE *stream)
: 指定读取最大长度。strncpy(char *dest, const char *src, size_t n)
: 指定最大拷贝字符数。snprintf(char *str, size_t size, const char *format, ...)
: 指定目标缓冲区大小。
五、应用——u64,p64,u32,p32
在 Pwn 漏洞利用中,经常需要将整数(如内存地址)和特定格式的字节流进行转换。Pwntools 库提供的 p32
, p64
, u32
, u64
这些函数就是为了方便地处理这种转换,它们会自动处理字节序(默认小端序)。
1. 基本功能与区别
p32
和p64
(Pack)功能:将整数打包为特定长度的字节流(字符串形式)。
p32(num)
:将 32 位整数(如0xdeadbeef
)打包为 4 字节的小端序字节流(如b'\xef\xbe\xad\xde'
)。p64(num)
:将 64 位整数打包为 8 字节的小端序字节流(如p64(0xdeadbeef)
可能产生b'\xef\xbe\xad\xde\x00\x00\x00\x00'
,高位不足会补零)。主要用于构造 Payload,例如将函数地址(整数形式)转换为内存中存储的字节形式,用于覆盖返回地址或函数指针。
u32
和u64
(Unpack)功能:将特定长度的字节流解包为整数。
u32(bytes)
:将 4 字节的字节流(如b'\xef\xbe\xad\xde'
)解包为一个 32 位整数(如0xdeadbeef
)。u64(bytes)
:将 8 字节的字节流解包为一个 64 位整数。主要用于解析程序泄漏的信息,例如从程序输出中提取函数的内存地址(这些地址通常以原始字节流形式出现)。
【u32
和 u64
主要使用场景及特征提示:
解析程序泄漏的内存地址
特征提示:当你利用漏洞(如格式化字符串或栈溢出)促使程序输出某些内存区域的内容(例如 GOT 表项、栈上的返回地址、libc 中的函数地址),你收到的会是原始的字节数据(例如
b'\xef\xbe\xad\xde'
)。这些数据就是内存地址在特定架构下的字节表示。为何使用:为了计算 libc 基地址或其他关键函数的真实地址,你需要将这些字节数据转换为整数进行计算。例如,泄漏出
puts
函数在 GOT 表中的地址后,用u64(leaked_data)
(64位)或u32(leaked_data)
(32位)将其转换为整数,然后减去 libc 中puts
的偏移量来得到 libc 的基地址。
处理网络数据或文件中的结构化数据
特征提示:如果程序从网络接收或从文件读取包含了打包整数(如长度字段、魔数、关键偏移量)的结构化数据,并且你需要将这些字段作为整数进行逻辑判断或计算时。
为何使用:这些整数在传输或存储时通常会被打包成字节流(例如小端序)。收到后,你需要用
u32
或u64
将其解包回整数以便使用。
验证或利用特定数值
特征提示:在漏洞利用过程中,有时需要比较或验证从程序接收到的某个值(例如一个 Canary 值或特定的标记值)。
为何使用:将这些字节流解包为整数后,可以更方便地进行数值比较和条件判断。】
2.关于字节序(Endianness)
计算机存储多字节数据(如地址)时有字节序之分。常见架构如 x86 和 x86-64 采用小端序,即数据的低位字节存储在低内存地址,高位字节存储在高内存地址。
p32
,p64
,u32
,u64
这些函数默认使用小端序,因为它们的设计初衷就是为了处理这些架构的程序
3.示例代码
下面是一个简单的例子,展示如何使用这些函数:
from pwn import * # 导入pwntools# p32 示例:整数 -> 字节流
addr_32 = 0x8048000
packed_data_32 = p32(addr_32)
print(f"p32(0x{addr_32:x}) = {packed_data_32}")
# 输出类似:p32(0x8048000) = b'\x00\x80\x04\x08'# p64 示例:整数 -> 字节流
addr_64 = 0x7ffff7dcfd00
packed_data_64 = p64(addr_64)
print(f"p64(0x{addr_64:x}) = {packed_data_64}")
# 输出类似:p64(0x7ffff7dcfd00) = b'\x00\xfd\xdc\xf7\xff\x7f\x00\x00'# u32 示例:字节流 -> 整数
# 假设从程序输出中接收到4字节:puts函数的真实地址
received_bytes_32 = b'\x50\xfd\xdc\xf7'
unpacked_addr_32 = u32(received_bytes_32)
print(f"u32(b'\\x50\\xfd\\xdc\\xf7') = 0x{unpacked_addr_32:x}")
# 输出:u32(b'\x50\xfd\xdc\xf7') = 0xf7dcfd50 (具体值取决于接收到的数据)# u64 示例:字节流 -> 整数
# 假设从程序输出中接收到8字节:某个64位地址
received_bytes_64 = b'\x00\xfd\xdc\xf7\xff\x7f\x00\x00'
unpacked_addr_64 = u64(received_bytes_64)
print(f"u64(8_bytes_data) = 0x{unpacked_addr_64:x}")
# 输出:u64(8_bytes_data) = 0x7ffff7dcfd00
4.在漏洞利用中的作用
这些函数在编写漏洞利用脚本(Exploit)时至关重要:
构造Payload:
当你需要覆盖栈上的返回地址、函数指针或修改特定内存值时,你需要将整数值(如目标函数的地址)转换为符合程序期望格式的字节序列。例如,在 64 位栈溢出中,用
p64(system_addr)
将system
函数的地址打包成 8 字节 payload 的一部分。解析泄漏信息:
许多漏洞利用技术(如 Ret2libc)需要先泄漏来自程序的内存地址(如来自 GOT 表的 Libc 函数地址)。程序输出的这些地址通常是原始的字节流。使用
u64
或u32
可以将这些字节流转换回整数,以便计算 Libc 基地址和其他关键函数的真实地址。
5.注意事项
字节长度:使用
u32
时,输入必须是 4 字节;使用u64
时,输入必须是 8 字节,否则会引发异常。字节序控制:虽然这些函数默认使用小端序,但你也可以通过
context
设置或函数的endianness
参数来指定字节序(如 'big' 表示大端序)。符号处理:这些函数还可以处理有符号和无符号整数,通常通过
context
设置或函数的sign
参数来控制。