UTCTF 2025
https://ctftime.org/event/2641
CHALL’S SOLVED
Category | Challenge |
---|---|
Cryptography | RSA |
Cryptography | DCΔ |
Forensics | Streamified |
Forensics | Forgotten Footprints |
Misc | Trapped in Plain Sight 1 |
Reverse Engineering | Ostrich Algorithm |
Cryptography
RSA
Overview
Author: by jocelyn (@jocelyn3270 on discord)
Description: Idk why people make e so large for rsa… it’s so much easier to just use 3. Why use big number when small number do trick?
Common RSA challenge, but this one is classic “small exponent” (e=3)
, which can sometimes be exploited with a cube root attack,
n: 21507386633439519550169998646896627263990342978145866337442653437291500212804540039826669967421406761783804525632864075787433199834243745244830254423626433057121784913173342863755047712719972310827106310978325541157116399004997956022957497614561358547338887866829687642469922480325337783646738698964794799137629074290136943475809453339879850896418933264952741717996251598299033247598332283374311388548417533241578128405412876297518744631221434811566527970724653020096586968674253730535704100196440896139791213814925799933321426996992353761056678153980682453131865332141631387947508055668987573690117314953760510812159
e: 3
c: 6723702102195566573155033480869753489283107574855029844328060266358539778148984297827300182772738267875181687326892460074882512254133616280539109646843128644207390959955541800567609034853
n
: The moduluse
: Public exponent, which is 3 (very small)c
: Ciphertext
Encryption works as: c ≡ m^e (mod n)
, where m
is the original message.
Since e=3
, we have c ≡ m^3 (mod n)
. If m^3 < n
, then c = m^3
exactly (without modular reduction). In that case, we can simply take the cube root of c
to find m
.
1from gmpy2 import mpz, iroot
2import binascii
3
4n = 21507386633439519550169998646896627263990342978145866337442653437291500212804540039826669967421406761783804525632864075787433199834243745244830254423626433057121784913173342863755047712719972310827106310978325541157116399004997956022957497614561358547338887866829687642469922480325337783646738698964794799137629074290136943475809453339879850896418933264952741717996251598299033247598332283374311388548417533241578128405412876297518744631221434811566527970724653020096586968674253730535704100196440896139791213814925799933321426996992353761056678153980682453131865332141631387947508055668987573690117314953760510812159
5e = 3
6c = 6723702102195566573155033480869753489283107574855029844328060266358539778148984297827300182772738267875181687326892460074882512254133616280539109646843128644207390959955541800567609034853
7
8c_mpz = mpz(c)
9root, is_exact = iroot(c_mpz, 3)
10
11pt = binascii.unhexlify(hex(root)[2:]).decode('ascii')
12print(pt)
Taking the cube root of c
directly gives us the original plaintext.
FLAG
utflag{hmm_maybe_bad_idea}
DCA
Overview
Author: By Sasha (@kyrili on discord)
Description: Due to a national shortage of primes, the US Department of Agriculture is rationing all citizens to a limit of one generated prime number per CTF challenge.
Next one is..
n = 399956368360808862373914258335185223080849636197711424060797090309268643429064461492550414549161330948819635837600839124910339139212025975705016633767495247163243281423582407941339197895052969960664399531226116807938480610953640675838340969642399505783577667601230289640157854573282615113017817753471366212008719316238931155299741896658264134636523008018510523774126757209492757800553768281613227711738371473830681563493341816035127889532515105148615575695347672918819305383651095344758737833444302556494599778991752161562622963652164008980839152347260377969421014616624263631920322958235478733540894255954351848359580013695597870908080170511403061620632540407634608773118202473287854599776791229532885611074739079107324575619148211269673210431496846247978541032947073060592123529635361112170678347924377962162254827262375685704046691718585952854410058401794022674628779309507437739620598639589987596443373586284136126401843497367142210715014480599609277532331148988390798073713743339823218981940779096432112651466716648010370850152213399051968069102663753404120592506704133217642671853086570223710424683386625314802805217882906873879240914022607713870946351691046929143491841506422542038315876506588525639983398522454145866029283449
e = 65537
c = 22644125297186385803212285721101686380290089858624593588464228942417644877688212364383835956263619653769244324906844180248816686517049952319431524113838480708352331162026595736354019259708442449783760846242702532176456117138374450898213788623580234048867117546091028843127595147910526821835855070663317466469650577618010308109119812464711010326075908158768138773973732088207030977470605554056485614676156104134673446546446752627654287202815354367643042773923258958887865030737447323798382020847653880886311162447594373201951226217556835030816588457674298560260109378271244834215832992407457137601161490484862135147963942227371690835380497920998286827898323068399708168699403459009009580152834747843780155917438758224782364193716322974594031272100820264364860227674838730962348140555980411714722361909800417953974064469599278274083750031569853934963716467881656073359393449142980936480726005445774158733389270553554093627622406166942859792490275434896108377393648278975530519769034633686070931694499857110956537102727286491854314244036392929790997824274724196292688659782806587688964714529943288954314300861531138101192901942534064757877725334672680909389193357725470116673323012331269218651347104807494994267835408427908717684178629
We have:
n
: The moduluse
: Public exponent,65537
which a common RSA exponentc
: Ciphertext
To decrypt, we need to compute m = c^d mod n
, where d
is the private exponent. The private exponent is calculated as d = e^(-1) mod φ(n)
, where φ(n)
is Euler’s totient function calculated as p*(p-1)
instead of the usual (p-1)*(q-1)
.
1from sympy import isprime, mod_inverse
2from math import isqrt
3
4n = 399956368360808862373914258335185223080849636197711424060797090309268643429064461492550414549161330948819635837600839124910339139212025975705016633767495247163243281423582407941339197895052969960664399531226116807938480610953640675838340969642399505783577667601230289640157854573282615113017817753471366212008719316238931155299741896658264134636523008018510523774126757209492757800553768281613227711738371473830681563493341816035127889532515105148615575695347672918819305383651095344758737833444302556494599778991752161562622963652164008980839152347260377969421014616624263631920322958235478733540894255954351848359580013695597870908080170511403061620632540407634608773118202473287854599776791229532885611074739079107324575619148211269673210431496846247978541032947073060592123529635361112170678347924377962162254827262375685704046691718585952854410058401794022674628779309507437739620598639589987596443373586284136126401843497367142210715014480599609277532331148988390798073713743339823218981940779096432112651466716648010370850152213399051968069102663753404120592506704133217642671853086570223710424683386625314802805217882906873879240914022607713870946351691046929143491841506422542038315876506588525639983398522454145866029283449
5e = 65537
6c = 22644125297186385803212285721101686380290089858624593588464228942417644877688212364383835956263619653769244324906844180248816686517049952319431524113838480708352331162026595736354019259708442449783760846242702532176456117138374450898213788623580234048867117546091028843127595147910526821835855070663317466469650577618010308109119812464711010326075908158768138773973732088207030977470605554056485614676156104134673446546446752627654287202815354367643042773923258958887865030737447323798382020847653880886311162447594373201951226217556835030816588457674298560260109378271244834215832992407457137601161490484862135147963942227371690835380497920998286827898323068399708168699403459009009580152834747843780155917438758224782364193716322974594031272100820264364860227674838730962348140555980411714722361909800417953974064469599278274083750031569853934963716467881656073359393449142980936480726005445774158733389270553554093627622406166942859792490275434896108377393648278975530519769034633686070931694499857110956537102727286491854314244036392929790997824274724196292688659782806587688964714529943288954314300861531138101192901942534064757877725334672680909389193357725470116673323012331269218651347104807494994267835408427908717684178629
7
8p = isqrt(n)
9assert p * p == n and isprime(p), "n is not a perfect square of a prime."
10
11phi_n = p * (p - 1)
12d = mod_inverse(e, phi_n)
13m = pow(c, d, n)
14
15m_bytes = int.to_bytes(m, (m.bit_length() + 7) // 8, 'big')
16pt = m_bytes.decode(errors='ignore')
17
18print(pt)
The only difference is how φ(n)
is calculated:
- In standard RSA where
n = p*q
(product of two distinct primes),φ(n) = (p-1)*(q-1)
- In this special case where
n = p²
(square of a prime),φ(n) = p*(p-1)
- Using
phi_n = p * (p - 1)
which is the correct totient function for this case
FLAG
utflag{th3_t0t13nt_funct10n_uns1mpl1f13d}
Forensics
Streamified
Overview
Author: by Caleb (@eden.caleb.a on discord)
Description: Apparently I’m supposed to scan this or something… but I don’t really get it.
So we are given an attachment in .txt
file, and there’s binary inside.
1111111000011110101111111100000100110101100100000110111010110110111010111011011101010101001101011101101110101001010010101110110000010100101111010000011111111010101010101111111000000001011110100000000010111110001110110011111000111010101100000010100000100011110111100101110111100000100001010100010000011000001000000001011011111100010001010111011100011010100010101001111100110111011100001001100110000011100001100110101011111111100000000110000001000110101111111001111001101010011100000101101001010001000010111010111100011111111011011101011001110011010011101110101010011110010010110000010011011001011100011111111010101010000010111
It wasn’t just a bunch of binary-encoded text, as I had assumed.
In actuality, we must see the bitstring as a black-and-white picture, where 0
represent black and 1
represents white. This might be any binary picture, including a QR code. Thus, I attempt to convert it to a PNG picture.
1import matplotlib.pyplot as plt
2import numpy as np
3
4bitstring = open("bitstring.txt", "r").read().strip()
5
6length = len(bitstring)
7size = int(length ** 0.5)
8grid = 1 - np.array([int(b) for b in bitstring[:size*size]]).reshape((size, size))
9
10plt.imshow(grid, cmap="gray", interpolation="nearest")
11plt.axis("off")
12plt.savefig("output.png", dpi=300, bbox_inches="tight")
Result:

$ ls
bitstring.txt output.png sol.py
$ zbarimg output.png
QR-Code:utflag{b!t_by_b!t}
scanned 1 barcode symbols from 1 images in 0.08 seconds
FLAG
utflag{b!t_by_b!t}
Forgotten Footprints
Overview
Author: by Caleb (@eden.caleb.a on discord)
Description: I didn’t want anyone to find the flag, so I hid it away. Unfortunately, I seem to have misplaced it.
$ file disk.img
disk.img: BTRFS Filesystem sectorsize 4096, nodesize 16384, leafsize 16384, UUID=19796fde-a3e0-4003-a5c6-607e2f34b80f, 2375680/131072000 bytes used, 1 devices
It looks like a Btrfs filesystem image.
What can we do next?.. Yeah unintended way babeyyyy :3 !!
○ Unintended Way
# create loop device
$ sudo losetup -Pf disk.img
# mount & verify
$ losetup -a
$ sudo mkdir -p /mnt/btrfs
$ sudo mount -o loop /dev/loop0 /mnt/btrfs
$ df -hT | grep btrfs
$ ls /mnt/btrfs
# unmount
$ sudo umount /mnt/btrfs
$ sudo losetup -d /dev/loop0
$ cat *.txt > out
In text editor, search for 7574666c6167...
which is the utflag
in hex format,
> Shortcut
Using strings and grep directly on disk.img
. Notice the suspicious long hex, unhex it.
strings disk.img \
| grep -E '.{100,}' \
| xxd -r -p \
| grep -a -oE 'utflag\{[^}]+\}'
○ Intended Way ?

The data should be recoverable with btrfs-restore, right? Alternatively, if the program supports the btrfs format (which autopsy doesn’t by default), it may be used like Photorec, Autopsy, or FTK Imager.
FLAG
utflag{d3l3t3d_bu7_n0t_g0n3_4ever}
Misc
Trapped in Plain Sight 1
Overview
Author: by Caleb (@eden.caleb.a on discord)
Description: Just try to read my flag. 0x0
The password ispassword
.
ssh -p 4301 trapped@challenge.utctf.live
First privilege escalation challenge,
$ ssh -p 4301 trapped@challenge.utctf.live
...
trapped@47ca6c33ca55:~$ ls
flag.txt
trapped@47ca6c33ca55:~$ cat flag.txt
cat: flag.txt: Permission denied
trapped@47ca6c33ca55:~$ tac flag.txt
tac: failed to open 'flag.txt' for reading: Permission denied
trapped@47ca6c33ca55:~$ nl flag.txt
nl: flag.txt: Permission denied
trapped@47ca6c33ca55:~$ $(echo Y2F0IGZsYWcudHh0 | base64 -d)
cat: flag.txt: Permission denied
trapped@47ca6c33ca55:~$
trapped@47ca6c33ca55:~$
trapped@47ca6c33ca55:~$ ls -lah
total 32K
dr-xr-xr-x 1 trapped trapped 4.0K Mar 14 19:23 .
dr-xr-xr-x 1 root root 4.0K Mar 14 19:23 ..
-r--r--r-- 1 trapped trapped 220 Feb 25 2020 .bash_logout
-r--r--r-- 1 trapped trapped 3.7K Feb 25 2020 .bashrc
-r--r--r-- 1 trapped trapped 807 Feb 25 2020 .profile
-r-x------ 1 noaccess noaccess 28 Mar 14 19:23 flag.txt
Hmm… it’s unreadable, so let’s considered looking up the SUID.
ⓘ Useful command for privesc by viewing SUID
Why is it useful?
$ find / -perm -4000 -type f 2>/dev/null
- Finding SUID binaries is a common privilege escalation technique in penetration testing and CTF challenges.
- Some misconfigured SUID programs can be exploited to gain root access.
$ find / -perm -4000 -type f 2>/dev/null
/usr/bin/passwd
/usr/bin/chsh
/usr/bin/su
/usr/bin/chfn
/usr/bin/mount
/usr/bin/umount
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/xxd
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
Notice the xxd
, we can use it to read the flags as a hexdump, and the contents of flag.txt can also be displayed.
trapped@47ca6c33ca55:~$ xxd flag.txt
00000000: 7574 666c 6167 7b53 7065 6369 614c 5f50 utflag{SpeciaL_P
00000010: 6572 6d69 7373 696f 6e7a 7d0a ermissionz}.
FLAG
utflag{SpeciaL_Permissionz}
Reverse Engineering
Ostrich Algorithm
Overview
Author: By Anthony (@stuckin414141 on discord)
Description: The worst algorithm, except for all the rest.
After a lot of digging and decompiling, I discovered this as a main function.
__int64 __fastcall sub_401774()
{
__int64 v0; // rbp
int v1; // edx
int v2; // ecx
int v3; // er8
int v4; // er9
int v5; // edx
int v6; // ecx
int v7; // er8
int v8; // er9
__int64 result; // rax
int i; // [rsp-A0h] [rbp-A8h]
int j; // [rsp-9Ch] [rbp-A4h]
_BYTE v12[96]; // [rsp-98h] [rbp-A0h] BYREF
_BYTE v13[16]; // [rsp-38h] [rbp-40h] BYREF
char v14[24]; // [rsp-28h] [rbp-30h] BYREF
unsigned __int64 v15; // [rsp-10h] [rbp-18h]
__int64 v16; // [rsp-8h] [rbp-10h]
v16 = v0;
v15 = __readfsqword(0x28u);
strcpy(v14, "welcome to UTCTF!");
for ( i = 0; i <= 16; ++i )
{
if ( v14[i] != aOiiaoiiaoiiaoi[i] )
sub_40C090(0LL);
}
sub_401C20(v12);
sub_4018C0((__int64)v12, (__int64 (__fastcall *)())((char *)sub_401774 + 1), 0x20uLL);
sub_401AC0(v13, v12);
sub_40CBC0((unsigned int)"utflag{", (unsigned int)v12, v1, v2, v3, v4);
for ( j = 0; j <= 15; ++j )
sub_40CBC0((unsigned int)"%02x", (unsigned __int8)v13[j], v5, v6, v7, v8);
sub_413790(125LL);
result = 0LL;
if ( v15 != __readfsqword(0x28u) )
sub_4534C0();
return result;
}
First, it copies “welcome to UTCTF!” into v14. It then loops through this string and compares each character against a hardcoded string aOiiaoiiaoiiaoi
. If any character doesn’t match, it calls sub_40C090(0LL)
which is likely an exit function.
Next, it calls some functions on v12 which is a 96-byte array:
- Initializes an MD5 context with
sub_401C20
- Hashes 32 bytes from the code of the function itself with
sub_4018C0
- Finalizes the hash with
sub_401AC0
It then starts building the flag.
All we need to do is find the address of sub_401774
> add 1 to get the starting address > then dumping 32 bytes from that location.
$ gdb -q chal
...
gef➤ start
[+] Breaking at entry-point: 0x401650
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x0
$rcx : 0x0
$rdx : 0x0
$rsp : 0x00007fffffffda60 → 0x0000000000000001
$rbp : 0x0
$rsi : 0x0
$rdi : 0x0
$rip : 0x0000000000401650 → endbr64
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0
$r11 : 0x0
$r12 : 0x0
$r13 : 0x0
$r14 : 0x0
$r15 : 0x0
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
───────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffda60│+0x0000: 0x0000000000000001 ← $rsp
0x00007fffffffda68│+0x0008: 0x00007fffffffdd44 → "/home/nopedawn/CCUG/UTCTF25/Ostrich_Algorithm/chal"
0x00007fffffffda70│+0x0010: 0x0000000000000000
0x00007fffffffda78│+0x0018: 0x00007fffffffdd77 → "SHELL=/bin/bash"
0x00007fffffffda80│+0x0020: 0x00007fffffffdd87 → "PYENV_SHELL=bash"
0x00007fffffffda88│+0x0028: 0x00007fffffffdd98 → "NVM_INC=/home/nopedawn/.nvm/versions/node/v20.13.1[...]"
0x00007fffffffda90│+0x0030: 0x00007fffffffddd8 → "WSL2_GUI_APPS_ENABLED=1"
0x00007fffffffda98│+0x0038: 0x00007fffffffddf0 → "rvm_prefix=/home/nopedawn"
─────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401647 mov rdi, rbp
0x40164a call 0x494b80
0x40164f nop
●→ 0x401650 endbr64
0x401654 xor ebp, ebp
0x401656 mov r9, rdx
0x401659 pop rsi
0x40165a mov rdx, rsp
0x40165d and rsp, 0xfffffffffffffff0
─────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "chal", stopped 0x401650 in ?? (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401650 → endbr64
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ continue
Continuing.
[Inferior 1 (process 20889) exited normally]
gef➤ break *0x401774
Breakpoint 1 at 0x401774
gef➤ x/32xb 0x401774+1
0x401775: 0xf3 0x0f 0x1e 0xfa 0x55 0x48 0x89 0xe5
0x40177d: 0x48 0x81 0xec 0xa0 0x00 0x00 0x00 0x64
0x401785: 0x48 0x8b 0x04 0x25 0x28 0x00 0x00 0x00
0x40178d: 0x48 0x89 0x45 0xf8 0x31 0xc0 0x48 0xb8
gef➤
All clear! Now we have the bytes needed for the MD5 hash, let’s convert these to a bytearray and calculate the MD5 hash.
1import hashlib
2
3byte2hash = bytearray([
4 0xf3, 0x0f, 0x1e, 0xfa, 0x55, 0x48, 0x89, 0xe5,
5 0x48, 0x81, 0xec, 0xa0, 0x00, 0x00, 0x00, 0x64,
6 0x48, 0x8b, 0x04, 0x25, 0x28, 0x00, 0x00, 0x00,
7 0x48, 0x89, 0x45, 0xf8, 0x31, 0xc0, 0x48, 0xb8
8])
9
10md5hash = hashlib.md5(byte2hash).hexdigest()
11print(f"utflag{{{md5hash}}}")
FLAG
utflag{d686e9b8f13bef2a3078c324ceafd25d}