CTF | 2021 TCTF/0CTF Quals WriteUp


这是一篇放在草稿箱里已经长草的文章,清理的时候才想起来这篇由于没环境复现,后面一懒就忘记发了……

噢,喵喵也不记得有没有写完了,就将就发一下吧(摊手

引言

TCTF/0CTF 2021 Quals

比赛时间: 2021-07-03 10:00 ~ 2021-07-05 10:00

https://tctf.qq.com/

https://ctf.0ops.sjtu.cn/https://tctf.0ops.top/

前不久有个 TCTF/0CTF 2021 线上预选赛,寻思着有点意思就来打了,题目好难啊。

只不过队里的小师傅们都在准备期末考试,么得办法只有几个老人随便玩了。

这篇就随便水水吧。




Misc

welcome

Welcome to 0CTF/TCTF 2021! Join Discord for flag

flag{welcome_to_0ctf/tctf_2021_have_fun}

GutHib

What happens on GutHib, stays on GutHib.

repo: awesome-ctf/TCTF2021-Guthib

根据 commit 历史可以看到有敏感信息被移除了。

发现用了 BFG Repo-Cleaner 来清理 commit 历史里留存的敏感信息。

其官方的文档:https://rtyley.github.io/bfg-repo-cleaner/

使用说明(Usage)

对比可以发现没有执行 gc 垃圾清理操作,理论上本地的 object 里还存有原始的数据。

然而直接 git clone 下来的 object pack 中并没有冗余信息,可能传输过程中进行了精简吧……

不过利用 GitHub API 记录的 events,由于 API 是读的是 GitHub 的 event 数据库,而不是 git repository,本地最后 push 上来的并没有影响服务器上的。而 GitHub 上的也没进行 gc 清理,这就能够恢复了。

https://api.github.com/repos/awesome-ctf/TCTF2021-Guthib/events?page=4

首先从 event 里拿到之前错误的 commit hash,然后用 commit hash 再去找对应的提交和文件。

https://api.github.com/repos/awesome-ctf/TCTF2021-Guthib/commits/da883505ed6754f328296cac1ddb203593473967

https://github.com/awesome-ctf/TCTF2021-Guthib/blob/da883505ed6754f328296cac1ddb203593473967/Try.2.md

flag{ZJaNicLjnDytwqosX8ebwiMdLGcMBL}

挺有意思的 233.

See also:

使用 Roberto Tyley 的 BFG Repo-Cleaner 移除 git 库中的二进制文件

在 git 中,所有文件和文件夹的数据都只存储一次,并且每个都会有一个独一无二的 id——git-id。如果大量的 commit 都没有修改某个文件,那么这个文件只会被保存一次。如果这个文件有两个版本,并且需要在两个版本间来回切换,这个文件只会存储两次,每个版本各一次。

BFG 只对 git 库中的每个对象清理一次,然后记住它的 git-id,以后遇到它,不把它计算在内就行了。

BFG Repo-Cleaner - 从 Git 历史中真正删除文件

其实刚开始考虑 BFG 默认认为当前最新的 commit 已经删除了敏感信息,于是其进行的修改只涉及历史 commit,最新 commit 不做更改。后来发现其实当前最新的 commit 也已经删掉敏感信息了……

.git文件夹探秘,理解git运作机制

(然后把 git 底层咋存储的给摸了一遍 233

Survey

https://wj.qq.com/s2/8706427/d012/

flag{im_curious_have_u_viewed_source_before_filling_out_the_survey}

噢是这个意思啊。

Singer

Sing a song~

special format: flag{[a-z]*}

https://attachment.ctf.0ops.sjtu.cn/singer_0cc09b073ebbbffcc5720d081f42feea

给的文件是这个。

A6-D#6
G#6
G6
G6
G#6
A6-D#6

C6-G5
F#5
F#5
C6-G5

A6-F#6,D#6
A6,F#6,D#6
A6,F#6-D#6

A6,D#6
A6-D#6
A6,D#6

F#7-C7
E7-D7
F7,C#7
F#7,C7

E6,A#5
E6-A#5
E6,A#5

A6-D#6
A6-G6
F#6-E6
A6-D#6

C#7-G6
C#7,G6
C#7,A#6,G6
C#7,A#6-G6

寻思着应该是音符。

想了半天没搞懂啥意思。。

后来考虑是不是音游的谱子啊,查了查发现也不是吧。

但后来又看到某音游——

会不会是这个按键连成的啥字符啊。

然而比赛的时候就没画出来,草!

赛后发现有个 Online Sequencer,是一个在线制作音乐的工具,支持模拟多种乐器,可以导出 ogg/mp3/midi,还能分享 sequence。

然后按照文件里的分块,每个分块是一个字母,- 就是两个键之间连起来,, 就是多个键同时按下。

于是就可以得到这样的图形。

于是 flag 就是 flag{musiking}

(脑洞真的大……

TCTF Share

It is a misc quest absolutely. https://bt.hzh.moe/

Flag format: TCTF{.*}

是个 hugo 建的博客,两篇博文。

上面那篇 Wonder Egg Priority OP

两个磁力链接分别是

https://bt.hzh.moe/files/Wonder-Egg-Priority-Safe.torrent

magnet:?xt=urn:btih:CIY7BQVJYC7GXF3AF4OA6SLZTZZAQDBS&dn=%E3%83%AF%E3%83%B3%E3%83%80%E3%83%BC%E3%82%A8%E3%83%83%E3%82%B0%E3%83%BB%E3%83%97%E3%83%A9%E3%82%A4%E3%82%AA%E3%83%AA%E3%83%86%E3%82%A3%20(Wonder%20Egg%20Priority)%20OP.7z

看了下有个 txt 文件,链接就是个动漫。https://www.youtube.com/watch?v=5yN9w_yzH2w

下面那篇 Tencent CTF

https://bt.hzh.moe/files/TCTF.torrent
magnet:?xt=urn:btih:AQEQB7YBOGIMOQARSRXSHGULZUI25EY3&dn=Secret%20of%20TCTF%202021.7z

很明显就是这个了,然而上面那个下载下来是全空的,下面那个下载不下来。

参考磁力链接的结构,btih 是指 BitTorrent Info Hash。

参考大师傅的 wp,AQEQB7YBOGIMOQARSRXSHGULZUI25EY3 即被 Base32 编码过的 BTIH 散列结果,把它转换为 hex 编码的。

%04%09%00%ff%01%71%90%c7%40%11%94%6f%23%9a%8b%cd%11%ae%93%1b

再参考第一个 torrent 文件,可以得到 httpseeds

参考

对于bittorrent协议的深入研究与探讨

HTTP Seeding

HTTP-BASED SEEDING SPECIFICATION

PROTOCOL:

The client calls the URL given, in the following format:

<url>?info_hash=[hash]&piece=[piece]{&ranges=[start]-[end]{,[start]-[end]}...}

Examples:

http://www.whatever.com/seed.php?info_hash=%9C%D9i%8A%F5Uu%1A%91%86%AE%06lW%EA%21W%235%E0&piece=3
http://www.whatever.com/seed.php?info_hash=%9C%D9i%8A%F5Uu%1A%91%86%AE%06lW%EA%21W%235%E0&piece=8&ranges=49152-131071,180224-262143

于是可以构造出地址为

https://bt.hzh.moe/seeding?info_hash=%04%09%00%FF%01%71%90%C7%40%11%94%6F%23%9A%8B%CD%11%AE%93%1B&piece=0

直接浏览器里打开不行,需要改 UA 为 BT 客户端……

写个脚本分 piece 下载一下。超过范围会返回 400,判断一下退出。

import requests

fp = open('1.7z', 'wb')
header = {'user-agent': 'uTorrent'}

i = 0
while True:
    print(i)
    url = f'https://bt.hzh.moe/seeding?info_hash=%04%09%00%FF%01%71%90%C7%40%11%94%6F%23%9A%8B%CD%11%AE%93%1B&piece={i}'
    r = requests.get(url, headers=header)
    fp.write(r.content)
    if r.status_code == 400:
        break
    i += 1

print('Download OK!')

最后 piece 是 0-46,下载下来解压得到一个 TCTF.vhdx

挂载得到一个 hint.bat

最后提示了文件名进行了修改。

于是可以去查看 NTFS 硬盘历史记录。想起上次强网杯那道 EzTime 题目了……

这里试了试用 AXIOM 分析硬盘 $LogFile,发现巨大多修改文件名的记录。

找了半天确实能找到 flag 了……

TCTF{B7UmV4x8JRHNKBCWxyevXt97tnQJtHjrZ}

或者也可以导出硬盘日志,然后写个脚本整合一下。

首先用 DiskGenius 把显示元文件(Metafile)勾上,然后才能看到 $LogFile 这些信息,然后导出来。

LogFileParser 来解析硬盘日志,可以得到 csv 数据文件。

根据这个 LogFile_lfUsnJrnl.csv 可以得知文件的修改历史。

写个脚本,找一下更改为新名字且文件名长度为1的文件名,另外再根据 MFTReference 分别统计并输出。

with open('LogFile_lfUsnJrnl.csv', 'r', encoding='utf-8') as f:
    s = f.read()

l = s.split('\n')
# 404.html|304|2021-07-02 20:12:48.5212936|FILE_CREATE+DATA_EXTEND+CLOSE|40|1|39|1|archive|2|0|0x00000000|0

info = {}
data = ''
for x in l:
    d = x.split('|')
    # if d[3] == 'RENAME_NEW_NAME+CLOSE':
    if 'RENAME_NEW_NAME+CLOSE' in d:
        f = d[0]
        print(f)
        filename = f.split('.')[0]
        if len(filename) == 1:
            # extension = f.split('.')[1]
            MFTReference = d[4]
            if MFTReference not in info:
                info[MFTReference] = ''
            info[MFTReference] += filename

print(info)

for i in sorted(info, key=lambda x: x[0]):
    print(i, info[i])

# 43 wpa4b1AQVPOibxTBwz7Qn2xXcFy{Jbnca7pDyIMAR7i8Sseg5sRpPIBIW68opBiNB
# 49 t}IuwI9Chdl0O1P8hYzwku8MTfKub4li49wzetpEbWHIHfhl3WGc7gh0c0WzpMClH
# 46 iPt86USAMpWJz5fDI47Vzu6BdlqxCxX0n6viKvNoZ7AzEAEHOpny0ZfV6tuT4QZm
# 47 j}vUG4Hr}DVGRFKZcY6op5BGLT}LqLrudLZqHOrh9Hcx8LVpOIeobvkrEyaTPzpV
# 44 phYaPUyusxk5}0PJQ19HsH3jHIonhFQVBxyzvtTUL7}W61xv7t3tUBhWOKiVio}tM
# 40 a3bGkqTNUhMOUAp2DMsAlA3}5jTxtPamvcuRJugpnD{pr}Wp4FDdr2HqUqYVyQ26
# 50 5TW57LSdRtbqa404yFJd2ujXJ{lPRPUwtfGtywtJBP5eGUF{mvkZE7}wSg5deoHL}i
# 59 NCJUO0B}Olw1b6CgZTFsncv2}pYfTJ323WJHeE03g2IZaCcyhgLn3WS2BGKr7zglR6
# 54 m0ngAg1g16KUDp8Zl0K7nbwLrLhMoasREHClz2aWJY6edmGlHrOdqXFgeKUQBEko
# 53 Q2DnB4b{K{2FxleNfZ5ZDaA6tl{GIUbMFN3kLOs1p5kJgu9TzYBxGDTmYa}iet2}P
# 51 Dyxy3OTAJdsovteo3{Z1MirxF9BmJ1ZAfov7tGKi1CpiDmNF7o3IJEvhOmV9RjLDP
# 57 WOW0Is1That2Flag3TCTF{B7UmV4x8JRHNKBCWxyevXt97tnQJtHjrZ}4God5Job
# 64 Nh1W7gq{Ea7XLGNCnFpKlo3j0sXxUklmrqFWtWt04Dc80XDg90JqFgZzLiWeoJbzx
# 65 ngnv2GZ6UD9aJnfnuFBIx1Mo084lND4}Oom7x{rbT7mTHl6OtNM9CrOjZGD4Lce
# 62 L61ulkKDvC7Kh}FYXwslD0jp{auQstMN5S2hIXmSMkdZtYTVmltslyLPCTOdF1ysG
# 61 yrOyO9XJK13Pbnr2Msym{NzdA5mbyhKL5cgbO2moJC90f92B7Q0lGIucN2XAXj}MQ

最后也可以得到 flag。

WOW0Is1That2Flag3TCTF{B7UmV4x8JRHNKBCWxyevXt97tnQJtHjrZ}4God5Job




Crypto

checkin

Welcome to 0CTF/TCTF 2021!

111.186.59.11:16256

Nothing fancy, just do the math.

虽然是大数求解,但直接拿 gmpy2 来算就完事了。

from gmpy2 import mpz
from pwn import *
import re

context.log_level = 'debug'
context.timeout = 10

sh = remote('111.186.59.11', 16256)
sh.recvuntil("Show me your computation:\n")
x = sh.recvuntil(" = ?").decode()
print('====> x:', x)

d = re.findall(r'(\d+)\^\((\d+)\^(\d+)\) mod (\d+) = \?', x)[0]
print('=====> info:', d)
a, b, c, p = [int(a) for a in d]
result = pow(mpz(a), pow(mpz(b), mpz(c)), mpz(p))
print('=========> result:', result)

sh.sendlineafter("Your answer: ", str(result))
sh.recvall()

# Here is your flag: flag{h0w_m4ny_squar3s_can_u_d0_in_10_sec0nds?}

zer0lfsr-

Much easier than zer0lfsr!

nc 111.186.59.28 31337

not download

NOTICE: the download link is updated. use the link below

download

源码:

#!/usr/bin/env python3

import random
import signal
import socketserver
import string
from hashlib import sha256
from os import urandom
from secret import flag

def _prod(L):
    p = 1
    for x in L:
        p *= x
    return p

def _sum(L):
    s = 0
    for x in L:
        s ^= x
    return s

def n2l(x, l):
    return list(map(int, '{{0:0{}b}}'.format(l).format(x)))

class Generator1:
    def __init__(self, key: list):
        assert len(key) == 64
        self.NFSR = key[: 48]
        self.LFSR = key[48: ]
        self.TAP = [0, 1, 12, 15]
        self.TAP2 = [[2], [5], [9], [15], [22], [26], [39], [26, 30], [5, 9], [15, 22, 26], [15, 22, 39], [9, 22, 26, 39]]
        self.h_IN = [2, 4, 7, 15, 27]
        self.h_OUT = [[1], [3], [0, 3], [0, 1, 2], [0, 2, 3], [0, 2, 4], [0, 1, 2, 4]]

    def g(self):
        x = self.NFSR
        return _sum(_prod(x[i] for i in j) for j in self.TAP2)

    def h(self):
        x = [self.LFSR[i] for i in self.h_IN[:-1]] + [self.NFSR[self.h_IN[-1]]]
        return _sum(_prod(x[i] for i in j) for j in self.h_OUT)

    def f(self):
        return _sum([self.NFSR[0], self.h()])

    def clock(self):
        o = self.f()
        self.NFSR = self.NFSR[1: ] + [self.LFSR[0] ^ self.g()]
        self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
        return o

class Generator2:
    def __init__(self, key):
        assert len(key) == 64
        self.NFSR = key[: 16]
        self.LFSR = key[16: ]
        self.TAP = [0, 35]
        self.f_IN = [0, 10, 20, 30, 40, 47]
        self.f_OUT = [[0, 1, 2, 3], [0, 1, 2, 4, 5], [0, 1, 2, 5], [0, 1, 2], [0, 1, 3, 4, 5], [0, 1, 3, 5], [0, 1, 3], [0, 1, 4], [0, 1, 5], [0, 2, 3, 4, 5], [
            0, 2, 3], [0, 3, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4], [1, 2, 3, 5], [1, 2], [1, 3, 5], [1, 3], [1, 4], [1], [2, 4, 5], [2, 4], [2], [3, 4], [4, 5], [4], [5]]
        self.TAP2 = [[0, 3, 7], [1, 11, 13, 15], [2, 9]]
        self.h_IN = [0, 2, 4, 6, 8, 13, 14]
        self.h_OUT = [[0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6], [1, 3, 4]]

    def f(self):
        x = [self.LFSR[i] for i in self.f_IN]
        return _sum(_prod(x[i] for i in j) for j in self.f_OUT)
 
    def h(self):
        x = [self.NFSR[i] for i in self.h_IN]
        return _sum(_prod(x[i] for i in j) for j in self.h_OUT)        

    def g(self):
        x = self.NFSR
        return _sum(_prod(x[i] for i in j) for j in self.TAP2)  

    def clock(self):
        self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
        self.NFSR = self.NFSR[1: ] + [self.LFSR[1] ^ self.g()]
        return self.f() ^ self.h()

class Generator3:
    def __init__(self, key: list):
        assert len(key) == 64
        self.LFSR = key
        self.TAP = [0, 55]
        self.f_IN = [0, 8, 16, 24, 32, 40, 63]
        self.f_OUT = [[1], [6], [0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6]]

    def f(self):
        x = [self.LFSR[i] for i in self.f_IN]
        return _sum(_prod(x[i] for i in j) for j in self.f_OUT)

    def clock(self):
        self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
        return self.f()

class zer0lfsr:
    def __init__(self, msk: int, t: int):
        if t == 1:
            self.g = Generator1(n2l(msk, 64))
        elif t == 2:
            self.g = Generator2(n2l(msk, 64))
        else:
            self.g = Generator3(n2l(msk, 64))
        self.t = t

    def next(self):
        for i in range(self.t):
            o = self.g.clock()
        return o

class Task(socketserver.BaseRequestHandler):
    def __init__(self, *args, **kargs):
        super().__init__(*args, **kargs)

    def proof_of_work(self):
        random.seed(urandom(8))
        proof = ''.join([random.choice(string.ascii_letters + string.digits + '!#$%&*-?') for _ in range(20)])
        digest = sha256(proof.encode()).hexdigest()
        self.dosend('sha256(XXXX + {}) == {}'.format(proof[4: ], digest))
        self.dosend('Give me XXXX:')
        x = self.request.recv(10)
        x = (x.strip()).decode('utf-8') 
        if len(x) != 4 or sha256((x + proof[4: ]).encode()).hexdigest() != digest: 
            return False
        return True

    def dosend(self, msg):
        try:
            self.request.sendall(msg.encode('latin-1') + b'\n')
        except:
            pass

    def timeout_handler(self, signum, frame):
        raise TimeoutError

    def handle(self):
        try:
            signal.signal(signal.SIGALRM, self.timeout_handler)
            signal.alarm(30)
            if not self.proof_of_work():
                self.dosend('You must pass the PoW!')
                return
            signal.alarm(50)
            available = [1, 2, 3]
            for _ in range(2):
                self.dosend('which one: ')
                idx = int(self.request.recv(10).strip())
                assert idx in available
                available.remove(idx)
                msk = random.getrandbits(64)
                lfsr = zer0lfsr(msk, idx)
                for i in range(5):
                    keystream = ''
                    for j in range(1000):
                        b = 0
                        for k in range(8):
                            b = (b << 1) + lfsr.next()
                        keystream += chr(b)
                    self.dosend('start:::' + keystream + ':::end')
                hint = sha256(str(msk).encode()).hexdigest()
                self.dosend('hint: ' + hint)
                self.dosend('k: ')
                guess = int(self.request.recv(100).strip())
                if guess != msk:
                    self.dosend('Wrong ;(')
                    self.request.close()
                else:
                    self.dosend('Good :)')
            self.dosend(flag)
        except TimeoutError:
            self.dosend('Timeout!')
            self.request.close()
        except:
            self.dosend('Wtf?')
            self.request.close()

class ThreadedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass

if __name__ == "__main__":
    HOST, PORT = '0.0.0.0', 31337
    server = ThreadedServer((HOST, PORT), Task)
    server.allow_reuse_address = True
    server.serve_forever()

是辣个 线性反馈移位寄存器(LFSR),但又不是咱之前见过的那种。

这个貌似可以用 Fast Correlation Attacks(project)。

2019 年的 TCTF 就出过这样一题,0CTF/TCTF 2019 Quals - zer0lfsr.

他那题是静态的文件数据,没有和服务器的交互,写起来比较方便,求解也可以直接拿 Z3 solver 来实现了。

这题的话 Generator1 和 Generator3 比较简单,然而直接拿 Z3 发现整不通,会提示 unsupported format string passed to BitVecRef.__format__,也就是说 BitVec 不支持 format 操作。

比赛的时候看了一晚上这题不知道咋整,赛后看了人家大佬的 wp 才发现只需要重写一下 n2l 函数就完事了。

把这个改成位操作,用位与、移位操作来对每个 BitVec 进行操作,最后形成一个 BitVec 的 list。

另外在接收数据的处理上,只需要给前 200 位这样的结果建立方程组就能求解了,没必要全部加上。

(8000个方程太多了也没必要)

Exp 如下。

"""MiaoTony"""
from pwn import *
from itertools import product
from string import ascii_letters, digits
from hashlib import sha256
from z3 import *


def _prod(L):
    p = 1
    for x in L:
        p *= x
    return p


def _sum(L):
    s = 0
    for x in L:
        s ^= x
    return s


def n2l_(x, l):
    return list(map(int, '{{0:0{}b}}'.format(l).format(x)))


def n2l(x, l):  # need to rewrite n2l with bit computation
    ans = []
    for i in range(l):
        ans.append(x & 1)
        x = x >> 1
    return ans[::-1]


class Generator1:
    def __init__(self, key: list):
        # assert len(key) == 64
        self.NFSR = key[: 48]
        self.LFSR = key[48:]
        self.TAP = [0, 1, 12, 15]
        self.TAP2 = [[2], [5], [9], [15], [22], [26], [39], [26, 30], [
            5, 9], [15, 22, 26], [15, 22, 39], [9, 22, 26, 39]]
        self.h_IN = [2, 4, 7, 15, 27]
        self.h_OUT = [[1], [3], [0, 3], [0, 1, 2],
                      [0, 2, 3], [0, 2, 4], [0, 1, 2, 4]]

    def g(self):
        x = self.NFSR
        return _sum(_prod(x[i] for i in j) for j in self.TAP2)

    def h(self):
        x = [self.LFSR[i] for i in self.h_IN[:-1]] + [self.NFSR[self.h_IN[-1]]]
        return _sum(_prod(x[i] for i in j) for j in self.h_OUT)

    def f(self):
        return _sum([self.NFSR[0], self.h()])

    def clock(self):
        o = self.f()
        self.NFSR = self.NFSR[1:] + [self.LFSR[0] ^ self.g()]
        self.LFSR = self.LFSR[1:] + [_sum(self.LFSR[i] for i in self.TAP)]
        return o


class Generator2:
    def __init__(self, key):
        # assert len(key) == 64
        self.NFSR = key[: 16]
        self.LFSR = key[16:]
        self.TAP = [0, 35]
        self.f_IN = [0, 10, 20, 30, 40, 47]
        self.f_OUT = [[0, 1, 2, 3], [0, 1, 2, 4, 5], [0, 1, 2, 5], [0, 1, 2], [0, 1, 3, 4, 5], [0, 1, 3, 5], [0, 1, 3], [0, 1, 4], [0, 1, 5], [0, 2, 3, 4, 5], [
            0, 2, 3], [0, 3, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4], [1, 2, 3, 5], [1, 2], [1, 3, 5], [1, 3], [1, 4], [1], [2, 4, 5], [2, 4], [2], [3, 4], [4, 5], [4], [5]]
        self.TAP2 = [[0, 3, 7], [1, 11, 13, 15], [2, 9]]
        self.h_IN = [0, 2, 4, 6, 8, 13, 14]
        self.h_OUT = [[0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6], [1, 3, 4]]

    def f(self):
        x = [self.LFSR[i] for i in self.f_IN]
        return _sum(_prod(x[i] for i in j) for j in self.f_OUT)

    def h(self):
        x = [self.NFSR[i] for i in self.h_IN]
        return _sum(_prod(x[i] for i in j) for j in self.h_OUT)

    def g(self):
        x = self.NFSR
        return _sum(_prod(x[i] for i in j) for j in self.TAP2)

    def clock(self):
        self.LFSR = self.LFSR[1:] + [_sum(self.LFSR[i] for i in self.TAP)]
        self.NFSR = self.NFSR[1:] + [self.LFSR[1] ^ self.g()]
        return self.f() ^ self.h()


class Generator3:
    def __init__(self, key: list):
        # assert len(key) == 64
        self.LFSR = key
        self.TAP = [0, 55]
        self.f_IN = [0, 8, 16, 24, 32, 40, 63]
        self.f_OUT = [[1], [6], [0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6]]

    def f(self):
        x = [self.LFSR[i] for i in self.f_IN]
        return _sum(_prod(x[i] for i in j) for j in self.f_OUT)

    def clock(self):
        self.LFSR = self.LFSR[1:] + [_sum(self.LFSR[i] for i in self.TAP)]
        return self.f()


class zer0lfsr:
    def __init__(self, msk: int, t: int):
        if t == 1:
            self.g = Generator1(n2l(msk, 64))
        elif t == 2:
            self.g = Generator2(n2l(msk, 64))
        else:
            self.g = Generator3(n2l(msk, 64))
        self.t = t

    def next(self):
        for i in range(self.t):
            o = self.g.clock()
        return o


context.log_level = 'debug'
context.timeout = 10

r = remote('111.186.59.28', 31337)

rec = r.recvline().strip().decode()
suffix = rec.split("+ ")[1].split(")")[0]
digest = rec.split("== ")[1]
log.info(f"suffix: {suffix}\ndigest: {digest}")

for comb in product(ascii_letters + digits + '!#$%&*-?', repeat=4):
    prefix = ''.join(comb)
    if sha256((prefix+suffix).encode()).hexdigest() == digest:
        print(prefix)
        break
else:
    log.info("PoW failed")
r.sendlineafter(b"Give me XXXX:", prefix.encode())


for choice in [1, 3]:
    r.sendlineafter(b'which one: ', str(choice))

    d = ''
    for i in range(5):
        x = r.recvuntil(":::end\n").strip().lstrip(
            b'start:::').rstrip(b':::end').decode('latin-1')
        d += x
    # print("=====> d:", d)

    r.recvuntil("hint: ")
    hint = r.recvuntil("\n").strip().decode()
    print("=====> hint:", hint)
    r.recvuntil('k: \n')

    s = Solver()
    msk1 = BitVec('msk1', 64)

    lfsr = zer0lfsr(msk1, choice)

    # for i in range(5):
    #     keystream = ''
    #     for j in range(1000):
    #         b = 0
    #         for k in range(8):
    #             b = (b << 1) + lfsr.next()
    #         keystream += chr(b)

    digits = []
    for x in d:
        x = ord(x)
        for i in range(8):
            digit = 1 if (x & 0b10000000) > 0 else 0
            x <<= 1
            digits.append(digit)
            # print(digit, x)
            # s.add(digit == lfsr.next())
    print('[+] Receive OK!')

    for i in range(200):
        s.add(digits[i] == lfsr.next())

    s.check()
    print(s.model())
    msk = s.model()[msk1]
    assert sha256(str(msk).encode()).hexdigest() == hint

    r.sendline(str(msk))

r.interactive()

flag

flag{we_have_tried_our_best_to_prevent_the_use_of_z3}

好家伙,原来是你们故意的啊,坏坏!

另外,发现一个有意思的地方。

用 latin-1 进行编码不会改变字符的长度,而用 utf-8 编码就可能会使得长度增大。

这情况在之前也遇到过,于是处理的时候就要特别小心,正好发现 2019 TCTF zer0lfsr 一题也有这个情况。

原因是进行 utf-8 编码,当 chr 操作的数字 <= 127 时可以正常用 1byte 表示,而 python2 大于 127 会报错,python3 会使用 2bytes 进行表示。

# 在区间[128, 192)内,数字的表示形式会加上xc2前缀:
>>> chr(130).encode()
b'\xc2\x80'

# 在区间[192, 255)内,数字的表示形式会加上xc3前缀,同时数字本身减去64:
>>> chr(200).encode()
b'\xc3\x88'

解码脚本:

最简单的就是直接以 bytes 都进来,直接 decode 即可。

# python3环境下
with open('keystream','rb') as f:
    data = f.read()
data = data.decode()

# python2环境下
import codecs
with codecs.open('keystream', 'rb', 'utf8') as f:
    data = f.read()

复杂一点就手动根据前缀来解码(参考网上的)

#!/usr/bin/python3
b = b''

with open('keystream','rb') as f:
    data = f.read()
    i = 0
    while i < len(data):
        if data[i] == 194:
            b += bytes([data[i+1]])
            i += 1
        elif data[i] == 195:
            b += bytes([data[i+1] + 64])
            i += 1
        else:
            b += bytes([data[i]])
        i += 1

Extensive Reading:

深入分析CTF中的LFSR类题目(二)

LFSR原理及CTF相关(一)

CTF竞赛密码学 之 LFSR

CTF | 2020 CISCN初赛 Z3&LFSR WriteUp




Web

1linephp

http://111.186.59.2:50080
http://111.186.59.2:50081
http://111.186.59.2:50082
The three servers are the same, you can choose any one. server will be reset every 10 minutes.

源码:

<?php
($_=@$_GET['yxxx'].'.php') && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__) && include('phpinfo.html');

这题是 HITCON2018 One Line PHP Challenge 的升级版。

那题的源码是

($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);

这题其实就文件名加了个 .php

参考

hitcon2018 One Line PHP Challenge

hitcon 2018 One Line PHP Challenge

利用session.upload_progress进行文件包含

phpinfo with LFI

利用 PHP upload_progress 进行文件包含来上传文件并执行。

先看一看 session 相关的配置。

session.auto_start = Off,代码如果没有 session_start() 也是没有开启 session 的,但是如果在多部分的 POST 数据里加入 PHP_SESSION_UPLOAD_PROGRESS 字段就可以开启 session 了。

session.upload_progress.cleanup = On ,文件上传后,session 文件内容会被立即清空,于是要进行条件竞争,传一个大文件来拖时间,在清空之前包含该文件。

到这里就是之前那题就可以解了。

而这题的话,利用 zip:// 协议可以在包含的时候包含压缩包里的文件,这样就能在拼接 .php 的时候读取到需要的 php 文件了。

然而还需要一个 zip 的 trick。

由于 PHP 在进行 zip 文件读取的时候是从结尾的部分读偏移量,然后从开头+偏移量的位置开始读取,于是就构造一个 zip,跳过 upload_progress_ 来读取就完事了。

赛后发现有另一种方案,就是先在压缩包里放一个文件,然后通过命令行给压缩包附加上所需的 php 木马文件,也就是说,这样 php🐎 所存储的位置不会被破坏掉,能通过 php zip 协议正常解析。然后打过去就完事了。



下面是队友的 Writeup:

很明显是 hitcon 那个 oneline php ,复习一下 wp ,然后发现多了一个 php 后缀,对于这个点尝试了 phar/zip LFI 都不太行,最后看 zip 格式的时候,发现 zip 是从后往前读的,类似指针的构造,所以我们构造一个 zip 文件,然后修改一下,刚好upload_progress_是 16 字节,只需要把 zip 那几个段的偏移加 16 字节,挪一下就可以了。

然后用 burp 一边用zip:///tmp/sess_b#index包含这个 zip 文件,一边用 php upload progress 刷新 session ,写马进去就可以了。

worldcup

这题是队友写的 wp,后面环境关了喵喵复现不了了(

在修改昵称处用/*尝试多行注释的时候,发现得到的内容都被删除了,猜想有可能是服务端渲染,又因为是 golang ,搜了一下发现有: https://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html

尝试先用 {{.}} 试试看,发现在主页成功输出

const msg = [
      {
        name: "map[_nonce:ttJVZSLyh5Us2Gx1gLzALQ code:fadc1d lv:1 msg:\u0022\u0027*\/`s name:zedd1]",
      }
    ]

    const ParseMSG = (d) => {
      if (d['name'] != "map[_nonce:ttJVZSLyh5Us2Gx1gLzALQ code:fadc1d lv:1 msg:\u0022\u0027*\/`s name:zedd1]")
        return `${d['name']}(quota ${d['quota'] != -1 ? d['used'] + "/" + d['quota'] : 'unlimited'}): ${d['msg']}`;
      else
        return `Your quota: 1, Nickname: {"_nonce":"ttJVZSLyh5Us2Gx1gLzALQ","code":"fadc1d","lv":1,"msg":"\"'*/`s","name":"zedd1"}`;
    };

    for (let i = 0; i < 5; ++i)
      $("#msg").append('<div class="form-group row"><p>' + ParseMSG(msg[i]) + '</p></div>');

    $("#newmsg").text('(quota 1/1): \u0022\u0027*\/`s');

发现在return语句中可以尝试使用反引号闭合,所以我们只需要改一下 msg 的内容就好了。

改成

{"msg":"`+alert()+`"}

就能弹窗了,所以接下来就是 xss 了,直接window.location跳就行了。

拿到 level1 cookie 之后,访问 level2 ,但是好像可以直接 X ,不知道设置的意义在哪

$("#newmsg").text(`"`+alert()+`"`);

拿到两个 cookie

level1=NoQWeCy70QekDB5b; level2=Autx5F53FmmSFayM

访问 http://111.186.58.249:19260/casino?bet=1

{{$}}可以看到变量名,然后看了一下 text 模板文档,一开始用

{{if%20gt%20.o0ps_u_Do1nt_no_t1%20.o0ps_u_Do1nt_no_t2}}{{printf%201}}{{else}}{{printf%200}}{{end}}

前端赢麻了但是后端不加钱,然后陷入脑洞当中。

后来想到可能是先加载的模版,再更新钱,于是想个办法把错的时候搞崩就行了。

0{{if%20gt%20.o0ps_u_Do1nt_no_t1%20.o0ps_u_Do1nt_no_t2}}{{call%201}}{{end}}

然后就可以稳赢了。




小结

这比赛题目太难了啊!!!

喵喵好菜啊,喵呜呜呜!!!

哭了,队友大多在准备期末考试,都没什么师傅来一起玩。。

最后只打到了 RisingStar Scoreboard 第13,然而前 12 进决赛,太难受啦,喵呜呜呜!

总榜

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录