零碎的知识
函数相关
64系统调用传参
rax:系统调用号
rdi->rsi->rdx->rcx->r8->r9
函数调用约定
__cdecl:C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡。
stdcall: windows API默认方式,参数从右向左入栈,被调函数负责栈平衡。
fastcall:快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰“fastcall”。
函数返回参数传递
32位
| 字节 | 返回参数 |
|---|---|
| 小于4字节 | eax |
| 4-8字节 | eax(低),ebx(高) |
| 大于8字节 | eax指针 |
函数使用
alarm()
unsigned int alarm(unsigned int seconds);
参数
unsigned int seconds:指定在多少秒后发送SIGALRM信号。如果参数为 0,则取消任何先前设置的闹钟。
返回值
- 成功时返回先前设置的闹钟剩余的时间(以秒为单位)。
- 如果没有先前设置的闹钟,返回 0。
关于for循环
__int64 sub_B24()
{
int i; // [rsp+0h] [rbp-4h]
for ( i = 0; i <= 19; ++i )
{
if ( !*((_QWORD *)off_202010 + i) )
return (unsigned int)i;
}
return 0xFFFFFFFFLL;
}
这里((_QWORD *)off_202010 + i)把off_202010强制转化成指向 64 位整型数组的指针,所以这里+i是按照QWORD(八个字节)来移动的。也就是说每次加1实际上是移动8字节
危险函数特点
get()在遇到'\x00'之前不会停止读取
fgets()从stdin读取输入,直到遇到换行符,EOF,或者读取指定大小的字符才会结束读取
python
语法
1. 字符串处理
p.recv() 通常返回一个字节流或者字符串数据,学会如何操作字符串是第一步。
重点内容:
-
strip():- 用于移除字符串两端的多余字符(如换行符
\n、空格、制表符等)。
data = " 0x7ffff7a05c00\n" clean_data = data.strip() # "0x7ffff7a05c00" - 用于移除字符串两端的多余字符(如换行符
-
split()和join():- 按特定分隔符分割字符串,或者将列表合并为字符串。
# 分割 data = "0x7ffff7a05c00: info" addr, info = data.split(":") # addr = "0x7ffff7a05c00", info = " info" # 合并 parts = ["0x7ffff7a0", "5c00"] joined = "".join(parts) # "0x7ffff7a05c00"
学习目标:
- 理解字符串与字节流的基本操作。
- 掌握
strip()和相关的字符串清理方法。
2. 类型转换
这部分涉及如何将接收到的字符串形式的地址或数字转换为整数。
重点内容:
-
int():- 将字符串形式的数字转换为整数,可以指定进制(如 16 表示十六进制)。
hex_str = "0x7ffff7a05c00" addr = int(hex_str, 16) # 140737348824576 -
hex()和bin():- 将整数转换为十六进制或二进制字符串。
addr = 140737348824576 hex_str = hex(addr) # '0x7ffff7a05c00' bin_str = bin(addr) # '0b111111111111111011111110100000010111001100000000' -
进制理解:
- 十六进制(
0x开头):基数为 16,常用于内存地址。 - 十进制:普通整数形式。
- 十六进制(
3. 字节流与字符串
Pwn 脚本中的 p.recv() 通常返回字节流 (bytes) 数据,你需要知道如何将它转换为字符串。
重点内容:
-
decode()和encode():- 字节流和字符串的互相转换。
data = b'0x7ffff7a05c00\n' string_data = data.decode('utf-8') # '0x7ffff7a05c00\n' byte_data = string_data.encode('utf-8') # b'0x7ffff7a05c00\n' -
strip()对字节流同样有效:data = b' 0x7ffff7a05c00\n' clean_data = data.strip() # b'0x7ffff7a05c00'
学习目标:
- 理解字节流 (
bytes) 和字符串 (str) 的区别。 - 掌握常见的编码 (
encode) 和解码 (decode) 方法。
4. 数据解析
在 Pwn 中,经常需要处理接收到的数据,将其转化为有用的信息。
重点内容:
-
提取有用部分: 使用正则表达式或字符串切片来提取地址等信息。
data = "Leak: 0x7ffff7a05c00\n" addr_str = data.split(":")[1].strip() # "0x7ffff7a05c00" addr = int(addr_str, 16) # 转换为整数 -
格式化数据:使用
f-string或
.format()格式化数据。
addr = 140737348824576 print(f"Leaked address: {addr:#x}") # 输出 'Leaked address: 0x7ffff7a05c00'
5. 调试技巧
在 Pwn 脚本中调试是必备技能,熟悉以下内容能帮助你快速定位问题:
重点内容:
-
print()和repr()- 检查接收到的数据和处理过程。
data = p.recv() print(f"Received: {data!r}") # 带调试信息显示原始数据 -
type()- 检查变量类型,确保操作方法正确。
print(type(data)) # <class 'bytes'>
数据处理
1. 返回内容的可能性
情况 1:\x00\x5c\xa0\xf7\xff\x7f(字节流,原始二进制数据)
-
表示:这是纯二进制数据,用于表示地址或其他数据结构。
-
场景:
- 目标程序直接发送内存地址的二进制表示。
- 常见于用
write系统调用直接泄露内存时。
-
示例:
leaked_data = p.recv(6) # 假设泄露了6字节地址 print(leaked_data) # 输出类似 b'\x00\x5c\xa0\xf7\xff\x7f' # 转换为整数 addr = int.from_bytes(leaked_data, 'little') # 假设小端序 print(hex(addr)) # 输出 0x7ffff7a05c00
情况 2:"0x7ffff7a05c00"(字符串,内存地址的十六进制表示)
-
表示:目标程序将内存地址以字符串形式发送,带有
0x前缀。 -
场景:
- 通常在目标程序中,使用
printf("%p")格式化输出地址。 - 常见于文本协议或 debug 信息输出。
- 通常在目标程序中,使用
-
示例:
leaked_data = p.recvline() # 假设程序输出地址并换行 print(leaked_data) # b'0x7ffff7a05c00\n' # 转换为整数 addr = int(leaked_data.strip(), 16) print(hex(addr)) # 输出 0x7ffff7a05c00
情况 3:b'0x7ffff7a05c00'(字节形式的字符串)
-
表示:类似情况 2,但仍是字节流形式。
b'...'表示 Python 中的字节类型,而不是普通字符串。
-
场景:
- 目标程序返回的内容是文本,但没有被自动转换为字符串(Python 的
p.recv()默认返回字节流)。
- 目标程序返回的内容是文本,但没有被自动转换为字符串(Python 的
-
示例:
leaked_data = p.recvline() print(leaked_data) # 输出 b'0x7ffff7a05c00\n' # 解码为字符串 string_data = leaked_data.decode('utf-8').strip() addr = int(string_data, 16) print(hex(addr)) # 输出 0x7ffff7a05c00
2. 这三者的区别
| 表示形式 | 数据类型 | 内容 | 用法 |
|---|---|---|---|
\x00\x5c\xa0\xf7\xff\x7f |
bytes |
原始二进制数据,表示内存地址 | 需要用 int.from_bytes 转换为整数。 |
"0x7ffff7a05c00" |
str |
普通字符串,表示地址的十六进制形式 | 可直接用 int(data, 16) 转换为整数。 |
b'0x7ffff7a05c00' |
bytes |
字节类型的字符串,带有 0x 前缀 |
先用 .decode('utf-8') 转换为字符串,再用 int(data, 16) 转换为整数。 |
3. 如何判断目标程序返回的数据类型?
-
直接打印输出: 调试时可以直接使用
print(repr(data))查看返回数据的真实类型和内容。data = p.recv() print(repr(data)) # b'0x7ffff7a05c00\n' 或 b'\x00\x5c\xa0\xf7\xff\x7f' -
根据协议判断:
- 如果目标程序使用
printf("%p"),返回的通常是b'0x...'的字符串形式。 - 如果目标程序直接泄露内存(如
write),返回的是 原始二进制数据。
- 如果目标程序使用
4. 实际应用中的处理方法
根据返回的数据类型,采用不同的方式进行解析和处理:
如果是二进制数据(\x00\x5c\xa0\xf7\xff\x7f):
data = p.recv(6) # 接收 6 字节
addr = int.from_bytes(data, 'little') # 小端序转换为整数
print(hex(addr)) # 输出 0x7ffff7a05c00
如果是十六进制字符串("0x7ffff7a05c00" 或 b'0x7ffff7a05c00'):
data = p.recvline() # 接收一行数据
addr = int(data.decode('utf-8').strip(), 16) # 解码并转换为整数
print(hex(addr)) # 输出 0x7ffff7a05c00
5. 总结
\x00\x5c\xa0\xf7\xff\x7f是原始二进制数据,通常需要用int.from_bytes解析。"0x7ffff7a05c00"是字符串表示的地址,直接用int(data, 16)解析。b'0x7ffff7a05c00'是字节流形式的字符串,需要先用.decode()转换,再用int()解析。
你可以通过调试工具(如 GDB)或直接打印目标程序的输出数据,来判断返回的具体格式,然后根据实际情况选择合适的处理方法。
在 Pwntools 或类似场景中,发送 b'\x00\x5c\xa0\xf7\xff\x7f', "0x7ffff7a05c00", 和 b'0x7ffff7a05c00' 会导致程序接收到完全不同的内容。这些形式的区别在于数据的内容和格式。以下是它们的详细解释:
数据发送
1. b'\x00\x5c\xa0\xf7\xff\x7f'
内容
- 这是纯粹的字节流,由 6 个字节组成:
0x000x5C0xA00xF70xFF0x7F
含义
- 通常表示一个内存地址(如
0x7ffff7a05c00的小端序表示)。 - 在程序中,机器码、内存地址或原始数据一般以这种二进制字节流的形式表示。
- 发送后: 程序会接收到原始的 6 个字节,解读时会依据上下文(比如小端序或大端序)解析为地址、数据或指令。
示例
p.send(b'\x00\x5c\xa0\xf7\xff\x7f') # 发送原始字节数据
2. "0x7ffff7a05c00"
内容
-
这是一个字符串,内容是地址的
可读文本形式
:
- 长度为 14 字符(包括 "0x")。
- 每个字符都以 ASCII 编码发送到程序。
含义
- 程序接收到的是一段文本,而不是原始二进制数据。
- 若程序设计为解析字符串(如
scanf("%lx", ...)或手动解析),它可能会将"0x7ffff7a05c00"转换为实际地址。 - 发送后: 程序接收到的内容是
b'0x7ffff7a05c00'。
示例
p.send("0x7ffff7a05c00") # 发送文本表示的地址
3. b'0x7ffff7a05c00'
内容
-
这是一个字节串,内容与
"0x7ffff7a05c00"相同,但在 Python 中直接以
bytes表示。
- 每个字符仍以 ASCII 表示。
- 长度为 14 字节。
含义
- 与
"0x7ffff7a05c00"的内容完全相同,只是数据类型不同(前者是bytes,后者是str)。 - 程序接收到的数据依然是字符串形式的地址,如果没有额外处理,程序不会直接将其作为内存地址。
示例
p.send(b'0x7ffff7a05c00') # 发送字节形式的地址字符串
区别总结
| 形式 | 内容类型 | 长度 | 程序接收的实际内容 | 适用场景 |
|---|---|---|---|---|
b'\x00\x5c\xa0\xf7\xff\x7f' |
原始字节流 | 6 字节 | 原始二进制数据 | 内存地址、机器码、原始数据传输 |
"0x7ffff7a05c00" |
文本字符串 | 14 字节 | 文本 b'0x7ffff7a05c00' |
需要传递可读字符串(程序会解析字符串成地址) |
b'0x7ffff7a05c00' |
字节串(ASCII 文本) | 14 字节 | 文本 b'0x7ffff7a05c00' |
同上,直接使用 bytes 类型 |
如何选择?
-
需要传递内存地址或机器码: 使用
b'\x00\x5c\xa0\xf7\xff\x7f',这会直接传递地址的原始字节流。比如这个python的运行结果为,可以看到二者一样
from pwn import * print(p64(0x7ffff7a05c00)) print(b'\x00\x5c\xa0\xf7\xff\x7f') #b'\x00\\\xa0\xf7\xff\x7f\x00\x00' #b'\x00\\\xa0\xf7\xff\x7f' -
需要传递地址字符串供程序解析: 使用
"0x7ffff7a05c00"或b'0x7ffff7a05c00',这会传递一个可读的地址字符串。
几个权限
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No
Rech
程序架构信息,判断是64位还是32位,exp编写的时候是p64还是p32
RELRO
Relocation Read-Onl(RELRO)此项技术主要针对GOT改写的攻击方式,它分成两种,Partial RELRO和FULL RELRO
Partial (部分)RELRO容易受到攻击,例如攻击者可以atoi.got为system.plt进而输入/bin/sh\x00获得shell,完全RELRO使整个GOT只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。
gcc -o hello test.c //默认情况下,是Partial RELRO
gcc -z norelro -o hello test.c // 关闭,即No RELRO
gcc -z lazy -o hello test.c // 部分开启,即Partial RELRO
gcc -z now -o hello test.c // 全部开启,即Full RELRO
Stack-canary
栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞是,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行,当启用栈保护后,函数开始执行的时候先会往栈里插入类似cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行,攻击者在覆盖返回地址的时候往往会将cookie信息给覆盖掉,导致栈保护检车失败而阻止shellcode的执行,在linux中我们将cookie信息称为canary。
gcc -fno-stack-protector -o hello test.c //禁用栈保护
gcc -fstack-protector -o hello test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o hello test.c //启用堆栈保护,为所有函数插入保护代码
NX
NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,如此一来,当攻击者在堆栈上部署自己的shellcode并触发时,智慧直接造成程序的崩溃,但是可以利用rop这种方法绕过
gcc -o hello test.c // 默认情况下,开启NX保护
gcc -z execstack -o hello test.c // 禁用NX保护
gcc -z noexecstack -o hello test.c // 开启NX保护
PIE
PTE(Position-Independent Executable,位置无关可执行文件)技术与ASLR技术类似,ASLR将程序运行时的堆栈以及共享库的加载地址随机化,而PIE及时则在编译时将程序编译为位置无关,即程序运行时各个段(如代码但等)加载的虚拟地址也是在装载时才确定,这就意味着。在PIE和ASLR同时开启的情况下,攻击者将对程序的内存布局一无所知,传统改写GOT表项也难以进行,因为攻击者不能获得程序的.got段的虚地址。若开始一般需在攻击时歇够地址信息
gcc -o hello test.c // 默认情况下,不开启PIE
gcc -fpie -pie -o hello test.c // 开启PIE,此时强度为1
gcc -fPIE -pie -o hello test.c // 开启PIE,此时为最高强度2
(还与运行时系统ALSR设置有关)
shellcode
shellcode进阶之手写shellcode - 先知社区
关于栈
栈的图

一.什么是堆栈平衡(比较抽象)

含义就是 当函数在一步步执行的时候 一直到 ret 执行之前,堆栈栈顶的地址 一定要是 call 指令的下一个地址。
也就是说函数执行前一直到函数执行结束,函数里面的堆栈是要保持不变的。
如果堆栈变化了,那么,要在 ret 执行前将堆栈恢复成原来的样子。
第一种情况:push 影响堆栈
比如 call ...
函数:mov ... (不影响堆栈平衡)
push..... (影响堆栈平衡)
ret.....
第二种情况:堆栈传递参数

......

堆栈如下:

因为 PUSH 1 PUSH 2 是为了函数传参而准备的 ,当函数执行完成后 ,push1,push2 就都没用了,所以要把堆栈恢复到执行前的位置
两种解决办法 :函数外部处理和内部处理
第一种 :在函数外部添加 ADD 处理

第二种:在函数内部添加

ret 8 是把 ret 和第一种情况的 add 两条指令整合成一条指令,在函数内部完成堆栈平衡

浙公网安备 33010602011771号