iTranslated by AI
picoCTF 2026 Writeup
This time, I entered as a solo competitor alongside Claude-senpai, finishing 52nd with 3950 points. (I threw the Blockchain problems to the AI and solved them completely, so I'll only write about the others...)
General Skills
SUDO MAKE ME A SANDWICH
Connecting gives access to Linux, but flag.txt requires root privileges.
ctf-player@challenge:~$ ls -al
total 20
drwxr-xr-x 1 ctf-player ctf-player 50 Mar 12 10:49 .
drwxr-xr-x 1 root root 24 Mar 9 21:32 ..
-rw-r--r-- 1 ctf-player ctf-player 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 ctf-player ctf-player 3771 Feb 25 2020 .bashrc
drwx------ 2 ctf-player ctf-player 34 Mar 12 10:46 .cache
drwxrwxr-x 3 ctf-player ctf-player 19 Mar 12 10:49 .local
-rw-r--r-- 1 ctf-player ctf-player 807 Feb 25 2020 .profile
-rw------- 1 ctf-player ctf-player 561 Mar 12 10:49 .viminfo
-r--r----- 1 root root 31 Mar 9 21:32 flag.txt
According to this article, you can check which commands you can execute with sudo using sudo -l.
ctf-player@challenge:~$ sudo -l
Matching Defaults entries for ctf-player on challenge:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User ctf-player may run the following commands on challenge:
(ALL) NOPASSWD: /bin/emacs
Since emacs can be started with sudo without a password, you can open a shell within emacs using M-x shell (press Alt+x, then type 'shell') and open it from there.
picoCTF{ju57_5ud0_17_9a782247}
ABSOLUTE NANO
Since you can connect to Linux, first check your privileges.
$ sudo -l
Matching Defaults entries for ctf-player on challenge:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User ctf-player may run the following commands on challenge:
(ALL) NOPASSWD: /bin/nano /etc/sudoers
/etc/sudoers describes which users/groups can execute which commands with root privileges. Normally, this cannot be modified, but since /bin/nano can be run with sudo without a password, you can escalate yourself to root by going through nano.
By writing the following in nano /etc/sudoers, you can open it.
ctf-player ALL=(ALL:ALL) ALL
picoCTF{n4n0_411_7h3_w4y_f559c068}
bytemancy 0
A program like this is provided. If you input the byte sequence corresponding to ASCII 101 (eee), the flag is handed over.
while(True):
try:
print('⊹──────[ BYTEMANCY-0 ]──────⊹')
print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
print()
print('Send me ASCII DECIMAL 101, 101, 101, side-by-side, no space.')
print()
print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
print('⊹─────────────⟡─────────────⊹')
user_input = input('==> ')
if user_input == "\x65\x65\x65":
print(open("./flag.txt", "r").read())
break
else:
print("That wasn't it. I got: " + str(user_input))
print()
print()
print()
except Exception as e:
print(e)
break
picoCTF{pr1n74813_ch4r5_62006ed0}
bytemancy 1
Similar to 0, if you send the byte sequence, the flag is handed over. This time, sending 'e' 1751 times is sufficient.
while(True):
try:
print('⊹──────[ BYTEMANCY-1 ]──────⊹')
print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
print()
print('Send me ASCII DECIMAL 101 1751 times, side-by-side, no space.')
print()
print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
print('⊹─────────────⟡─────────────⊹')
user_input = input('==> ')
if user_input == "\x65"*1751:
print(open("./flag.txt", "r").read())
break
else:
print("That wasn't it. I got: " + str(user_input))
print()
print()
print()
except Exception as e:
print(e)
break
picoCTF{h0w_m4ny_e's???_6e0cc4c6}
bytemancy 2
Just like 0 and 1, you just need to send a specific byte sequence... but since you cannot input characters corresponding to 0xFF normally, use pwntools.
import sys
while(True):
try:
print('⊹──────[ BYTEMANCY-2 ]──────⊹')
print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
print()
print('Send me the HEX BYTE 0xFF 3 times, side-by-side, no space.')
print()
print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
print('⊹─────────────⟡─────────────⊹')
print('==> ', end='', flush=True)
user_input = sys.stdin.buffer.readline().rstrip(b"\n")
if user_input == b"\xff\xff\xff":
print(open("./flag.txt", "r").read())
break
else:
print("That wasn't it. I got: " + str(user_input))
print()
print()
print()
except Exception as e:
print(e)
break
from pwn import *
io = remote("lonely-island.picoctf.net", 57398)
io.recvuntil(b"==>")
io.sendline(b'\xff\xff\xff')
print(io.recvline())
picoCTF{3ff5_4_d4yz_40517c29}
bytemancy 3
Just like the others, you just need to send a byte sequence... but you need to enter the function address of spellbook 3 times.
Checking spellbook with Ghidra reveals the following functions:
int main(void)
{
uint32_t uVar1;
uint32_t uVar2;
uint uVar3;
uint32_t focus;
uVar1 = ember_sigil(0x41414141);
uVar2 = glyph_conflux(uVar1 ^ 0x41414141);
uVar3 = uVar2 ^ uVar1 ^ 0x41414141;
uVar1 = astral_spark(uVar3);
uVar3 = uVar1 ^ uVar3;
uVar1 = binding_word(uVar3);
return (uVar1 ^ uVar3) & 0xff;
}
uint32_t ember_sigil(uint32_t soulstones)
{
return leyline_seed + (soulstones ^ 0x13371337);
}
uint32_t glyph_conflux(uint32_t index)
{
return leyline_seed >> 3 ^ index + 0x4444;
}
uint32_t astral_spark(uint32_t salt)
{
return salt * 7 + 0x55aa10;
}
uint32_t binding_word(uint32_t step)
{
uint32_t twist;
return (step >> 4 | step << 4) ^ 0xfeedc0de;
}
The functions in spellbook are just calculations and don't need to be analyzed. Since pwntools can launch an ELF and enumerate function symbols, we use that capability to create a solver.
from pwn import *
io=remote("green-hill.picoctf.net", 57349)
elf = ELF('./spellbook')
for i in range(3):
# Read until just before "for procedure 'function name'."
io.recvuntil(b"for procedure '")
# Extract the function name
func_name = io.recvuntil(b"'").decode('utf-8').strip("'")
io.recvuntil(b"==> ")
# Identify the address from the extracted function name and convert to a 4-byte little-endian binary using p32()
addr = elf.symbols[func_name]
payload = p32(addr)
print(f"[+] Q: {func_name} -> Address: {hex(addr)}")
# Send binary data
io.send(payload)
print("[+] Clear!")
io.interactive()
picoCTF{0bjdump_m4g1c_9ee35d3a}
Password Profiler
-
userinfo.txt: User information -
hash.txt: SHA1 hash of the password -
check_password.py: Script for password checking
These files are provided. Inside check_password.py, there is a mention of a "wordlist that was generated using CUPP," suggesting that this library was used to create the dictionary.
The information required for input is described in userinfo.txt, so I used it to fill in the fields, generated a wordlist, and ran the script.
picoCTF{Aj_15901990}
Piece by Piece
The Linux environment is structured as follows. Looking into instructions.txt reveals that you need to restore a zip file (password: supersecret).
ctf-player@pico-chall$ ls
instructions.txt part_aa part_ab part_ac part_ad part_ae
ctf-player@pico-chall$ cat instructions.txt
Hint:
- The flag is split into multiple parts as a zipped file.
- Use Linux commands to combine the parts into one file.
- The zip file is password protected. Use this "supersecret" password to extract the zip file.
- After unzipping, check the extracted text file for the flag.
You can restore flag.txt by combining the parts in order and using unzip.
ctf-player@pico-chall$ cat part_* > conbined.zip
ctf-player@pico-chall$ unzip ./conbined.zip
Archive: ./conbined.zip
[./conbined.zip] flag.txt password:
extracting: flag.txt
picoCTF{z1p_and_spl1t_f1l3s_4r3_fun_2d6c5d3f}
MultiCode
Opening the provided file reveals what is clearly a base64-like string.
NjM3NjcwNjI1MDQ3NTMyNTM3NDI2MTcyNjY2NzcyNzE1ZjcyNjE3MDMwNzE3NjYxNzQ1ZjczNzM2ZjM2NzA2ZTZlMzIyNTM3NDQ=
Using CyberChef in the following order gives the flag:
- From Base64
- From Hex
- URL Decode
- ROT13
picoCTF{nested_enc0ding_ffb6caa2}
Crypto
cryptomaze
(I asked Claude-senpai about this one, so I will write down the interpretation...)
Looking at the attached file, the following information is provided, and it is clear that an LFSR (Linear Feedback Shift Register) is used for random number generation for the key.
LFSR Initial State:
[0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1]
LFSR Taps:
[63, 61, 60, 58]
Encrypted Flag:
8f0e6d0f5b0dc1db201948b9e0cebd8f4e0f7cb6a86d4243f62f1438e07a632c38338e7e04fbddef0c6260a4eb758417
The encryption follows this flow:
- XOR the positions in Taps.
- Output the most significant bit of the State as the key.
- Shift the entire State to the left and add the XORed value to the end.
The Encrypted Flag is 96 characters long, and given that AES is 16 bytes/block, it seems to be AES-128 in ECB mode without an IV.
from Crypto.Cipher import AES
from itertools import product
initial_state = [0,0,1,0,0,1,0,1,1,1,1,0,1,1,0,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,0,1,
0,1,0,0,1,1,0,1,1,0,0,0,1,0,1,1,1,1,0,0,0,1,0,0,0,1,0,1,1,0,1,1]
taps = [63, 61, 60, 58]
encrypted_flag = "8f0e6d0f5b0dc1db201948b9e0cebd8f4e0f7cb6a86d4243f62f1438e07a632c38338e7e04fbddef0c6260a4eb758417"
KEY=0
def lfsr():
key=0
state=initial_state
print(len(state))
for i in range(128):
key=(key << 1) + state[0]
xor = state[63] ^ state[61] ^ state[60] ^ state[58]
state = state[1:]
state.append(xor)
return key
if __name__ == "__main__":
KEY = lfsr()
cipher = AES.new(KEY.to_bytes(16, byteorder='big'), AES.MODE_ECB)
decrypted_flag = cipher.decrypt(bytes.fromhex(encrypted_flag))
print(decrypted_flag.decode())
picoCTF{scr8mbledt_flvg_35821959}
StegoRSA
Looking at the EXIF data of images.jpg, there is a mysterious comment. Converting it from Hex using CyberChef reveals an OpenSSL private key.
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDq3mThsuMFoG3/
wmlyt4fUZ92sI8fMLIMFUVWvxX6WMPHA1VJlo8kfx5skiHzWWl5XYIalGr7KW7X0
kzNnnVgF3kUegRZMS8HzTQt5d1HRWBQTCEktfUaeO+rL5bJLpP6uRRgb2egyN9cq
K2jozFQX8dIlMtjATG4yWDCjHGFycXvK5r6fOxA7DPX8ji8GHpexYqj+rN9HEDoY
G7BWLiWL0zdfZBWzJHxcXsGtuDj+XHnhq0v3R/mJUoPeNDigjQdDKQkF44JlMzO/
vF566aM6WWT3+6Bk6n2xaiZMqbODGe4IaLzHTmR2e2sV6qMZiRsIxL6E6WQxWOwM
L7qZO+gxAgMBAAECggEAJQUFyKSzb4JweXTRaHFbhFQUwW+9q8XDpwsWuEb8ThG2
i+rz0FfHDpkd2oh1iNcjR0l3H5SoyD45XqyfAxtusYoYbJed20vA7Sen1y7tTf5r
N5lsPgfLduBsgKlFTPKw6T4JxUYM+RwKcDf5vWE2ILS50f7YNu3fxqlWN+IezUn0
NOLB0tmEzAp1tJecJJ22M8O5kKXnK7uFqBafkBVjjfaI8eE1FI1hzuFA4WDE/DlI
0BfmW7gb4KDc3vrhNv12MZDeCc5pXkXPnqmBqrjsDOvlhd1mkrX9x49nvK/5OUUN
CegycseSNPiDPie9KlLBM9qhyWwghmfqJaLDXiQTwwKBgQDtue1AU45VMeM0OnSf
osCYOoGPOjaUCkjwsGK4AjtPGIQKOijK6BkmCybIQp/qyu3cK7KKiPAhOPHwCObL
1tUaY+KksMpIRpkFMOJxehjYhbPEzsgpWu9JanmSsTHN0r6vulOULWSLOIgXP8AN
TmSPEzPT542WWIt+D9ugReZLjwKBgQD87Dw8ZWP8ZwurOKXWC8S56rCs8jQJusOk
btlOvE0OJyiPITAy5XN5Eo3p8lV9IponbkINIXCAUldtt6TUDD9AC8VFpbN2Fa8y
L2kP1nOhph8emGCrg2shgMYK1HRvGjEhT3gA3wX7NriO/QvqwDYkR1HWT/Cf9Pxj
HxJiKhOwPwKBgQCo3yntRy3V0VF/+YJ9ICVGPlFoyEabFU9JQ1NtOZCeGGE7zqLJ
uOSchNFw8vsc1Djx7UywYqSKRSSAiiJBbQQG8iu/KCMaAqSS7m39hGl/7kKMrQO9
dO6ErZFdJmiluG5i8K6MlU5WI7txIIUyLpz6kf6AKn4G7jFxRJyUlvMIewKBgB9T
Tv6X/DNFu/8/6+I/4OS5+ZniAan21MZn6EhFMDIBjZd0n9id7JhhQOxp1EbEY11g
3ZNswddS23s+VI7i2W6gRpWiuUB13RYVIykQAZBS1+XdL5PumfUzUtQCjk04bD9Y
7V8GQGJl26PyGWjA17PUlYE6s23MVPod3hQEbB3XAoGAGmQWjQ2MOlpAjlZfyfyT
h4otp2T2KxZim/NcxfL1NUKC+VxSLCQtxdnVIWcg0vmNLSB+9eF1dawts3BF3uRx
QeswBhJCl0kyiIxDFXZPz8u7sRROrRK11YyUznG0OTfnb9Vjl8TbhpZVHOyp754M
UwkkM+srAQK+sVVR0Qbl0yU=
-----END PRIVATE KEY-----
Save this key as privkey.pem and use openssl to restore it.
openssl rsautl -decrypt -inkey privkey.pem -in flag.enc
picoCTF{rs4_k3y_1n_1mg_d8526dc3}
ClusterRSA
The following files are provided:
n = 8749002899132047699790752490331099938058737706735201354674975134719667510377522805717156720453193651
e = 65537
ct = 3021569373773402689513257373362764131880473249842187164838297943840513930619586623604677697191914325
When you find yourself without the primes p and q necessary for RSA, and n is large, your first stop should be factordb.com.
Checking it reveals that n is the product of four primes.
n = 9671406556917033397931773
× 9671406556917033398314601
× 9671406556917033398439721
× 9671406556917033398454847
Let's perform the RSA decryption with p=9671406556917033397931773, q=9671406556917033398314601, r=9671406556917033398439721, and s=9671406556917033398454847.
p=9671406556917033397931773
q=9671406556917033398314601
r=9671406556917033398439721
s=9671406556917033398454847
e=65537
c=3021569373773402689513257373362764131880473249842187164838297943840513930619586623604677697191914325
#----------------------
n = p * q * r * s
phi = (p - 1) * (q - 1) * (r - 1) * (s - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
flag = m.to_bytes((m.bit_length() + 7) // 8, byteorder='big').decode()
print(flag)
picoCTF{mul71_rsa_787c01b3}
Forensics
Binary Digits
A massive amount of 0s and 1s is provided. Using Cyberchef to perform From Bin followed by To Hex results in a hex string starting with ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 00 01 00 00 ff db 00 43 00 08 06....
After asking Claude, it turns out this is a JPEG magic number. Using my preferred Hex Editor, I saved it as a JPEG file, and opening it revealed the flag.
(I used HxD)

Asked (+Link 1: https://filesig.search.org/)
picoCTF{h1dd3n_1n_th3_b1n4ry_cc2099d3}
Reversing
Gatekeeper
An ELF is provided, so I examined the main() function as usual.
undefined8 main(void)
{
int iVar1;
size_t sVar2;
long lVar3;
undefined8 uVar4;
long in_FS_OFFSET;
int local_40;
char local_38 [40];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Enter a numeric code (must be > 999 ): ");
fflush(stdout);
__isoc99_scanf(&DAT_00102070,local_38);
sVar2 = strlen(local_38);
iVar1 = is_valid_decimal(local_38);
if (iVar1 == 0) {
iVar1 = is_valid_hex(local_38);
if (iVar1 == 0) {
puts("Invalid input.");
uVar4 = 1;
goto LAB_00101698;
}
lVar3 = strtol(local_38,(char **)0x0,0x10) ;
local_40 = (int)lVar3;
}
else {
local_40 = atoi(local_38);
}
if (local_40 < 1000) {
puts("Too small.");
}
else if (local_40 < 10000) {
if ((int)sVar2 == 3) {
reveal_flag();
}
else {
puts("Access Denied.");
}
}
else {
puts("Too high.");
}
uVar4 = 0;
LAB_00101698:
if (local_10 != *(long *)(in_FS_OFFSET + 0x28 )) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar4;
}
The key point in this function is here:
iVar1 = is_valid_decimal(local_38);
if (iVar1 == 0) {
iVar1 = is_valid_hex(local_38);
if (iVar1 == 0) {
puts("Invalid input.");
uVar4 = 1;
goto LAB_00101698;
}
lVar3 = strtol(local_38,(char **)0x0,0x10) ;
local_40 = (int)lVar3;
}
is_valid_decimal() is a function that returns 1 if the input is a valid integer, and strtol() is used to generate a long from a string. The code checks for a decimal integer, and if that fails, it checks if the input is a valid hex. If valid as hex, it uses that value. This means if I input something like FFF, I satisfy all the conditions needed for the flag:
- Input is 3 characters long.
- Input is >= 1000 and < 10000.
When I entered it, instead of the flag string, a mysterious string appeared.
JUCK-picoctf@webshell:~$ nc green-hill.picoctf.net 53104
Enter a numeric code (must be > 999 ): FFF
Access granted: }e78ftc_oc_ipde8cftc_oc_ipc_99ftc_oc_ip9_TGftc_oc_ip_xehftc_oc_ip_tigftc_oc_ipid_3ftc_oc_ip{FTCftc_oc_ipocipftc_oc_ip
Looking at reveal_flag(), it prints in reverse and adds ftc_oc_ip every 3 characters, so I removed that part.
void reveal_flag(void)
{
FILE *__stream;
size_t __n;
void *__ptr;
uint local_24;
__stream = fopen("/flag.txt","r");
if (__stream == (FILE *)0x0) {
puts("Flag file not found.");
}
//...
while (local_24 = local_24 - 1, -1 < (int)local_24) {
putchar((int)*(char *)((long)__ptr + (long)(int)local_24));
if ((local_24 & 3) == 0) { //local_24 is used as index
printf("ftc_oc_ip"); //here
}
}
//...
}
picoCTF{3_digit_hex_GT_999_cc8ed87e}
hiddencipher
Since an ELF file is provided, using strings reveals it is packed with upx.
After unpacking with upx -d hiddencipher, I analyzed the code and found the following function:
undefined8 main(void)
{
FILE *__stream;
undefined8 uVar1;
size_t __n;
void *__ptr;
long lVar2;
int local_2c;
__stream = fopen("flag.txt","rb");
if (__stream == (FILE *)0x0) {
perror("[!] Failed to open flag.txt");
uVar1 = 1;
}
else {
fseek(__stream,0,2);
__n = ftell(__stream);
rewind(__stream);
__ptr = malloc(__n + 1);
if (__ptr == (void *)0x0) {
puts("[!] Memory allocation error.");
fclose(__stream);
uVar1 = 1;
}
else {
fread(__ptr,1,__n,__stream);
fclose(__stream);
*(undefined1 *)((long)__ptr + __n) = 0;
lVar2 = get_secret();
puts("Here your encrypted flag:");
for (local_2c = 0; (long)local_2c < (long)__n; local_2c = local_2c + 1) {
printf("%02x",(ulong)(*(byte *)(lVar2 + local_2c % 6) ^
*(byte *)((long)__ptr + (long)local_2c)));
}
putchar(10);
free(__ptr);
uVar1 = 0;
}
}
return uVar1;
}
undefined7 * get_secret(void)
{
s.0._0_1_ = 0x53;
s.0._1_1_ = 0x33;
s.0._2_1_ = 0x43;
s.0._3_1_ = 0x72;
s.0._4_1_ = 0x33;
s.0._5_1_ = 0x74;
s.0._6_1_ = 0;
return &s.0;
}
The line (ulong)(*(byte *)(lVar2 + local_2c % 6) ^ *(byte *)((long)__ptr + (long)local_2c)); performs an XOR operation using values from lVar2. lVar2 is retrieved from get_secret() and constructs the string {0x53, 0x33, 0x43, 0x72, 0x33, 0x74} which corresponds to "S3Cr3t".
To restore it, I simply split the HEX string into two-character chunks and apply the same XOR operation.
encrypted_hex = "235a201d702015483b1d412b265d3313501f0c072d135f0d2002302d07466656764b06422e"
key = b"S3Cr3t"
enc = bytes.fromhex(encrypted_hex)
flag = bytes([enc[i] ^ key[i % 6] for i in range(len(enc))])
print(flag.decode())
picoCTF{xor_unpack_4nalys1s_425e5956}
hiddencipher 2
First, you are asked to solve an arithmetic problem. If correct, you are given an encrypted flag.
Focusing on encode_flag(), I noticed that the flag string is passed as the first argument and the math result as the second, and it outputs flag_char * math_result.
undefined8 main(void)
{
int iVar1;
time_t tVar2;
undefined8 uVar3;
long in_FS_OFFSET;
char local_29;
uint local_28;
uint local_24;
int local_20;
int local_1c;
void *local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
tVar2 = time((time_t *)0x0);
srand((uint)tVar2);
local_1c = generate_math_question(&local_29 ,&local_28,&local_24);
printf("What is %d %c %d? ",(ulong)local_28, (ulong)(uint)(int)local_29,(ulong)local_24 );
fflush(stdout);
iVar1 = __isoc23_scanf(&DAT_0010201d,&local_20);
if (iVar1 == 1) {
if (local_1c == local_20) {
local_18 = (void *)read_flag_file("flag.txt");
if (local_18 == (void *)0x0) {
uVar3 = 1;
}
else {
encode_flag(local_18,local_1c);
free(local_18);
uVar3 = 0;
}
}
else {
puts("Wrong answer! No flag for you.");
uVar3 = 1;
}
}
else {
puts("Invalid input. Exiting.");
uVar3 = 1;
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28 )) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar3;
}
void encode_flag(long param_1,int param_2)
{
int local_c;
puts("Encoded flag values:");
for (local_c = 0; *(char *)(param_1 + local_c) != '\0'; local_c = local_c + 1) {
printf("%d",(ulong)(uint)(*(char *)(param_1 + local_c) * param_2));
// (flag character * math result) is printed here
if (*(char *)(param_1 + (long)local_c + 1) != '\0') {
printf(", ");
}
}
putchar(10);
return;
}
picoCTF{m4th_b3h1nd_c1ph3r_f8ce7ad6}
Autorev1
When connecting, a raw ELF binary is sent, and it asks for a "secret".
The secret and the ELF are provided together, so you just need to input it, but if more than 1 second passes, it complains that it is too slow.
I used pwntools to create a program that receives the secret and immediately sends it back 20 times.
from pwn import *
io = remote("mysterious-sea.picoctf.net", 55766)
io.recvuntil(b"Welcome! I think I'm pretty good at reverse enginnering. There's NO WAY anyone's better than me. Wanna try? I have 20 binaries I'm going to send you and you have 1 second EACH to get the secret in each one. Good luck >:)\n")
for i in range(20):
data=io.recvline().strip().decode()
io.recvuntil(b"What's the secret?:")
io.sendline(data)
io.recvuntil(b"Correct!\n")
while True:
try:
print(io.recvline())
except EOFError:
break
picoCTF{4u7o_r3v_g0_brrr_78c345aa}
Silent Stream
A Python script that handles the encryption and the actual captured pcap are provided.
import socket
def encode_byte(b, key):
return (b + key) % 256
def simulate_flag_transfer(filename, key=42):
print(f"[!] flag transfer for '{filename}' using encoding key = {key}")
with open(filename, "rb") as f:
data = f.read()
print(f"[+] Encoding and sending {len(data)} bytes...")
for b in data:
encoded = encode_byte(b, key)
pass
print("Transfer complete")
if __name__ == "__main__":
simulate_flag_transfer("flag.txt")
Since it is encrypted with (char + 42) % 256, I concatenated all payloads and restored the file.
Instead of a flag, a JPEG magic number appeared, so I saved it as .jpeg and opened it to obtain the flag.
import struct
KEY = 42
with open("packets.pcap", "rb") as f:
f.read(24)
payloads = []
while True:
hdr = f.read(16)
if len(hdr) < 16:
break
_, _, incl_len, _ = struct.unpack('<IIII', hdr)
data = f.read(incl_len)
payload = data[54:]
if payload:
payloads.append(payload)
encrypted = b''.join(payloads)
decrypted = bytes([(b - KEY) % 256 for b in encrypted])
with open("flag.jpg", "wb") as out:
out.write(decrypted)
print("Complete")
picoCTF{tr4ck_th3_tr4ff1c_0c09bb9e}
Conclusion
While there is a trend toward pay2win due to AI, I only used that approach for Blockchain by throwing the distribution files at it.
Generally, I learned a lot by utilizing AI in a collaborative way (e.g., "Teach me about this attack method," or "I have this problem and it looks like I can do XXXX, so could you implement this?") [1].
I personally feel that not just CTF, but the security world also has elements of puzzle-solving...
I couldn't focus on it during the period due to university commitments, but I got that rush of solving a problem again after a long time, so I am satisfied with solving it on my own and learning from it, rather than focusing on the ranking.
-
In return, I also felt the consequence of running away from things until now... Don't run from pwntools and Boot2Root. ↩︎
Discussion