Pointer Overflow CTF 2023 Writeup
I found this CTF in Nov 11th, 2023. My original plan for that day (and weekend) was to play 0CTF/TCTF with b01lers, but the organizers of that CTF postponed it to some other date because they wanted to have their CTF serve as a DEFCON qualifier (I do not know the details unfortunately, so don't quote me on this because I could be wrong).
So, I instead found a CTF that I can play by myself, because, why not :)
This CTF technically ended on Sep 21, 2023 (because they got a team that solved every challenge that day), but they decided to leave it open until Jan 21, 2024. I did a quick Google search and discovered that no writeup was posted online (at least on the day I looked up) except for one OSINT problem (A Great Interior Desert).
They have nine categories, each with three challenges. They had me when I saw separate categories for steganography and password cracking. They also had a pretty good range of problems: some challenges were solvable within a few hours (or even minutes), but some took me almost a month.
The competition website is still online, but the challenges are not anymore. Here is the CTFTime event page: https://ctftime.org/event/2026/.
Crypto
Unquestioned and Unrestrained
Looks so much like base64.
Flag: poctf{uwsp_411_y0u2_8453_423_8310n9_70_u5}
A Pale, Violet Light
This time, it looks so much like RSA. The modulus is very small that it can be factorized in less than a second. No thinking needed.
I don't even know why my code is so long, I didn't notice it until now. Maybe some thinking was involved.
Flag: poctf{uwsp_533k_4nd_y3_5h411_f1nd}
Missing and Missed
And now, this looks so much like Brainfxxk (censoring the profanity part just in case) and the phrase "cerebral fornication" in the challenge description gives it away. dCode also agrees.
Flag: poctf{uwsp_219h7_w20n9_02_f0290773n}
Crack
The Gentle Rocking of the Sun
After some searches, I discovered that the value on the post-it note is a SHA-1 hash of some string "zwischen".
crack2.7z
is password protected, and typing in "zwischen" as the password unlocked the folder that consists of another folder, and so on.
The fact that it starts with p and o gives a vibe that this might be the flag. Let's try it out.
Flag: poctf{uwsp_c411f02n14_d234m1n9}
With Desperation and Need
The song that stuck in that person's head is "We Will Rock You" and this rings a bell that the infamous rockyou.txt
file might be relevant here.
There is only one password that starts with "gUn" in rockyou.txt
. Now, install VerCrypt, open crack3
with it and plug in this password.
Nice! That indeed was the password. This unlocks a folder called filesfromdecrypted
which contains flag.txt
that has the flag which is:
Flag: poctf{uwsp_qu4n717y_15_n07_4bund4nc3}
Web
We Rest Upon a Single Hope
This website takes an input and lets you submit it. Upon checking its source code, we can discover that it stores our input as key
and returns the output of a function called Zuul
, in particular Zuul(key.value)
.
So, what is this function? Apparently, it does something when the input is v
.
and whatever that is, it looks important.
And funnily enough, Zuul(v)
was the flag.
Flag: poctf{uwsp_1_4m_411_7h47_7h3r3_15_0f_7h3_m057_r341}
Vigil of the Ceaseless Eyes
Opening the link, we reach a very distracting-looking website.
So, let's just follow the hint directly. The hint says there might or might not be something in /secret/flag.pdf
directory/file.
We get something. I cannot open it, but it looks like the file definitely exists.
Let's force download it. I still cannot open it, but the file definitely has something inside.
Opening it with a text editor, however...
works like a charm and we get our flag!
Flag: poctf{uwsp_71m3_15_4n_1llu510n}
Quantity is Not Abundance
Opening the link, we again reach a very distracting-looking website.
The description for this problem has a similar hint as the previous one: there might or might not be something in /.secret/flag.txt
directory/file.
Unlike last time, we cannot open it because we do not have permission. This, however, apparently could be bypassed by forcing it to open.
Flag: poctf{uwsp_1_h4v3_70_1n5157}
Forensics
If You Don’t, Remember Me
That DF1.pdf
file seems broken. As always, we try opening it with a text editor.
Some very suspicious yet familiar string here.
Nice. Ez.
Flag: poctf{uwsp_w31c0m3_70_7h3_94m3}
A Petty Wage in Regret
Here is DF2.jpg
:
Although this picture seems normal (in the sense that it is not broken and cannot be opened), let's start by opening it with a text editor.
They even gave you the hint "ASCII" next to it. Anyway, that translates to:
::P1/2:: poctf{uwsp_7h3_w0rld_h4d
So it looks like the second half of the flag is somewhere else. I don't see any other suspicious-yet-familiar strings in the text editor.
It turns out the answer might be closer than we think/thought. The picture DF2.jpg
looks 'funny' in the sense that some parts are clearer than others. Drawing lines and dots along those parts gives us:
::P2/2:: 17_f1257
It is not very clear and hard to see at first sight (hence I am not a big fan of this chal). I found it helpful to inverse the color and play around with bit planes using StegOnline. Also, I don't know why the second part of the flag does not come with "}" but trying it with "}" worked.
Flag: poctf{uwsp_7h3_w0rld_h4d_17_f1257}
Better to Burn in the Light
This wasn't an easy chal.
Like I did for the two previous chals, I started by opening the DF3.001
file with text editor. That evidently wasn't a good move, I was immediately greeted with a frozen screen asking me if I want to force quit the text editor. How fun.
Some online searches told me that I could change the file extension from .001
to .7z
. That worked like a charm!
There are just so many files to go over. But the challenge description said "we're going to need to bring some of them back from the dead." So, we can start by focusing on the files in $RECYCLE.BIN
folder.
I (still) see so many files: some meme-y pictures and a bunch of broken files. Had no idea where to start, I simply began by opening all these files with CyberChef.
This file doesn't seem very helpful. (Btw, there were A BUNCH of files that looked like this.) But these two look mildly interesting.
That $R4K6JU8.doc
file apparently was supposed to be a jpg file or is a file that contains a jpg file. Luckily, our chef is versatile enough to extract a file from a bigger file.
Looks severely like a severely damaged flag part. Not happy, but at least we found something. Let's see if there is anything that is worth our attention.
We've seen $RLLD6JM.pdf
already, but there alledgedly is another file (named $RN367L5.jpg
) that we overlooked. Let's ask our chef for another help.
Gosh, another severely damaged flag part? On the fortunate side, this part looks like the first part (starts with "poctf{") and the other part that we found before must be the other part (ends with "}"). But they are just, too blurry to be legible, almost like some of my students' handwritings (just kidding).
If you may recall, we had a weird-looking file ($RLLD6JM.pdf
) that starts with a questionable string --POCTF{0.5c} --
. Would that mean anything?
Apparently... the $RN367L5.jpg
file has these strings in its head and tail, respectively:
CLUE 2 - 1 / 2 == 0.5a | 0.5b && 0.5a / 2 == 0.5a | 0.5c
--POCTF{NOT THE END}--
I honestly, even till this date, have no idea what that first string exactly means, but it gives some impression that something must be combined with 0.5c
which, as we just recalled, is what $RLLD6JM.pdf
starts with!
So, I copy pasted $RLLD6JM.pdf
at the end of $RN367L5.jpg
,
and removed the garbage (?) parts:
Boom!
Now for the second flag, which was from $R4K6JU8.doc
, it just says CLUE 1 - Missing header
at the top.
I wasn't sure what this meant until I took a look at another JPG file.
Yes, when it said missing header, it really meant it. I copy pasted the missing part, and my life is good now.
Flag: poctf{uwsp_5h1v3r_m3_71mb3r5}
I also tried solving this problem using TestDisk (since DF3.001
file is a disk file, it was a canonical attempt). It could only recover a half of the flag, but not the other half. I think this chal was very well-made in that sense --- it does not get trivialized by the uses of tools (well, I guess I should not deny that without CyberChef, I might have not been able to solve this problem this easily).
Exploit
Did they mean "Pwn"? Anyway...
My Friend, A Loathsome Worm
Uh, it looks like I forgot to save the challenge description. Sorry! :'(
We are given a binary executable file exploit1.bin
. I don't think I want to read this manually, so I put it into Ghidra, and this is what it returned (main
function).
It looks like, if local_1c
is equal to 0x539
, then we can get into the (root) shell. We can start writing from local_38
and it uses scanf()
function to scan and store local_38
, which is a well-known vulnerable function.
The local relative address (on memory stack) of local_38
is 0x38
and local_1c
is 0x1c
(like their Ghidra-ed names say). Here is the screenshot of Ghidra, in case you don't trust me.
So, our payload would be: 0x38 - 0x1c
many garbage bytes and 0x539
. In code (yes, I am finally using pwntools!), it'd look like this:
Flag: poctf{uwsp_5w337_c10v32_4nd_50f7_511k}
A Guilded Lily
It again looks like I again forgot to save the challenge description again. Sorry again! :'(
This chal wasn't particularly difficult (was not easy either, though), but it took me a while to actually get my code working. It also took me a while to understand the problem, especially the format of input. I also forgot exactly how I ended up using ROPgadget. I think the challenge description (which I clumsily did not save) said something about it, or that /bin/sh
does not exist (or something along those lines).
We are given a binary executable file exploit2.bin
. Let Ghidra feast on it.
It looks like a toy example of the infamous Heartbleed vulnerability. The program takes a string and its length as input, and returns the string by reading the length-amount of its memory. Therefore, by giving the length that is actually longer than the input string, we have a chance of being able to read beyond the string, which could include the memory of the machine running the program.
Let's pay attention to those two lines first (technically four, but two are more or less print statements, so you know what I mean):
The program takes the length of the input and stores it as local_41c
, and the string as local_418
of length 1032 bytes, but using scanf()
function!
Then, there is another local variable local_10
. This is actually quite interesting.
It appears that if local_10
is different from what it is supposed to be, the program returns fail. Ah, so local_10
is the stack canary!
We can bypass the stack canary by figuring out its value, then overwrite it with its value when building our attack payload. We can have it print out local_10
by filling up local_418
with 1032 characters but giving it a length longer than 1032.
I did +16 instead of +8 to see if I could also read the return address, but in the end, it was not very necessary---please keep reading if you are curious why :).
As a sanity check, I wanted to make sure that whether we actually reached and can overwrite local_10
, and whether that canary_ish_thingy
is in fact local_10
. Let's first see if changing the value beyond local_418
actually triggers the "smashing detection" system:
It very well does. Now let's see if overwriting it with our canary_ish_thingy
works.
I don't exactly know/remember what was happening, but it worked for the vast majority of the time. Anyway, our hypothesis that local_10
is indeed the stack canary and overwriting it with itself could allow us to bypass the stack smashing detection system.
So, you'd probably think that we can put shellcode inside our buffer local_418
and changing the return address to the address of the buffer would work, like this code does:
Apparently it does not. I spent all they thinking the address 0x00401ec1
was wrong, but both Ghidra and GDB gave me the same address. While being very confused, I remembered the return-oriented programming (ROP) that I taught in CS 426 that I TAed.
I turned on ROPGadget and had it generate execve
(ROP chain), set it as shellcode
, and put it into where the return address is at:
It finally worked.
Flag: poctf{uwsp_4_57udy_1n_5c42137}
For completeness and viewing pleasure, here is the full sol.py
script. It also includes the ROP chain generated by ROPgadget.
sol.py
(Click to expand)
Time is but a Window
At this point, I hope you are not surprised or mad that there is no challenge description here.
Let's start by again throwing exploit3.bin
into Ghidra.
I think this chal is simpler than the previous one, we are even given a function that executes /bin/bash
for you. But it turns out you have to pay attention to details.
get_string()
function basically reads a string character-by-character until it reaches \n
. One can infer that the size of the memory stack of greet()
is 16 + 8 = 24 based on the length of the name (stored as local_18
) which is 16.
void win()
function executes /bin/bash
and it comes right after main()
function. So the goal is to overwrite the return address of main()
to the address of win()
.
Note also that, main()
comes right after greet()
.
So we can overflow greet()
to change the address of main()
to win()
.
The address of win()
is 0x13cb
, greet()
is 0x1364
, and main()
is 0x13a8
.
Therefore, we can overflow the entire memory stack for greet()
, reach main()
, then change a8
to cb
.
Flag: poctf{uwsp_71m3_15_4_f4c702}
Stego
Absence Makes Hearts Go Yonder
At this point, you should not be surprised what we should do first. And it actually worked again.
Flag: poctf{uwsp_h342d_y0u_7h3_f1257_71m3}
An Invincible Summer
Evidently, Exploit wasn't the only chal category that I forgot to save the challenge description.
Anyway, you are given a compressed folder stego1.7z
of a bunch of pictures.
Every picture has a replica of itself except it is stored with a different file extension (png/jpg and bmp). So, we might be able to find something by subtracting/XOR-ing one file from the other. Using Steganographic Comparator, we get some positive outcomes. For example, here is the result of hand.jpg
and hand.bmp
:
This is the indicator that the message was written in the first few lines of pixels of the image. In fact, that turns out to be the case actually (using Aperi Solve.
But this doesn't give any helpful information about the flag... Also, some other pairs of pictures, like mittens.bmp
and mittens.jpg
showed a similar outcome:
But neither mittens.bmp
nor mittens.jpg
had anything interesting in it.
I decided to just put every image into Aperi Solve, and I managed to get the flag from lock.png
.
which is kind of funny because the difference between lock.bmp
and lock.png
is like, almost nothing (at least visual-wise):
So I guess this method does not always work, especially for small strings.
Flag: poctf{uwsp_h342d_y0u_7h3_f1257_71m3}
Between Secrets and Lies
It is a stego chal. The picture matters, not the challenge description. (More seriously, yes, I again forgot to save the description. Sorry!)
You have a picture of a can of beans, named bean.png
.
I plugged this picture into every online stego tool that I could find, including but not limited to, Aperi'Solve, Steganography Online, StegOnline, Steganographic Decoder, another Steganographic Decoder; pretty much, you name it, I (probably) had tried it already. Unfortunately, none of them returned anything useful.
I then thought this might be LSB steganography. I downloaded cloacked-pixel and ran it with bean.png
as input, then I got this plot:
Based on this plot, it is quite unlikely that the flag was hidden in LSB. This crosses out LSB steganography from our list.
Another possibility is that there was a hidden file embedded into some pixels of this picture. That means, it is time to zsteg it. Just running zsteg bean.png
only returned
which isn't very helpful. But with --all
flare, we get a lot of outputs. Some conspicuous ones were:
I extracted all of them, not just the ones listed above (you can automate it using stegoVeritas), and I went through every file manually, but it turns out that they are all false positive.
After staring at the picture for days with no luck, I realized that there are some red dots on the top left corner of the picture.
Let me enlarge it for you (if you are a Ubuntu user, disable "Smooth images when zoomed out" in Preferences).
This is definitely something unusual. I think it is either a Morse code or binary code. Also, at the end of this sequence of red pixels, there are some blue pixels as well.
We can start off by converting each pixel to 1 and the rest to 0. The red sequence can be represented as
01110000 01101111 01100011 01110100 01100110 01111011 01110101 01110111 01110011 01110000 01011111 01101101 00110000 01110010 00110011 01011111 01101000 01110101 01101101 00110100 01101110 01011111 00110111 01101000 00110100 01101110 01011111 01101000 01110101 01101101 00110100 01101110 01011111 00110001 00110101 01011110 00110000 00010001 00100000
And the blue sequence at the end is
11000101 01000110 11010000 01001001 01100111 11100100 01100100 01100111 11000001
So the red sequence in fact was a binary code, except some data near the end was damaged.
But unfortunately, the blue sequence is not even a valid binary string.
In fact, an 8-digit binary number should not start with 1 if it is an encoding of an ASCII symbol, according to binary-to-ASCII conversion tables. That said, the binary number for curly bracket "}" is 01111101
. So 01111101
should be somewhere. The blue sequence ends with 11000001
and 11000001 XOR 01111101 = 10111100
. So maybe, 10111100
is the key? But then this will make some numbers in the sequence start with 1, which as said should not happen. Padding the beginning and end of the sequence with some zeroes also did not help, because there always will be a binary number that starts with 1. Combining the red and blue sequence (writing both red and blue as 1, and so on) also did not work.
After wasting a week on this, I found this stego tool called Stegsolve that I decided to try as a last resort (I was going to give up after this if this does not work). After some troubleshooting (I found this StackOverflow post helpful), I managed to get it run on my machine. Then, since the message was encoded using red pixels, I selected the maximum possible "Red" on the bit planes, then...
Yes, I finally found the flag.
Flag: poctf{uwsp_m0r3_hum4n_7h4n_hum4n_15_0ur_m0770}
Even till this day, I have no idea how we can solve this problem using Stegsolve. Although I learned a lot about stego while working on this problem, I am not particularly happy (maybe I'm just salty) that a use of tool trivialized this problem. But I suppose stego chals are just mostly like that, and still, I enjoyed working on this challenge!
Misc
Sight Without Vision
Flag format is poctf{uwsp_ _ _ }
---three underscores with three blanks. So probably the answer consists of three words.
The sentence in the description "You have one, too. You carry it everywhere you go but it's not heavy" is a famous riddle with the answer "name."
So maybe the 'name' of this problem, "Sight Without Vision" but with Leet Speak which is "519h7 w17h0u7 v1510n" (that was what that "this" in the challenge description was for) is the answer. And that was indeed correct!
Flag: poctf{uwsp_519h7_w17h0u7_v1510n}
Etc.
Other than these challenges that I described in this post, I solved two more challenges: one Misc (Here You See A Passer By) and one OSINT (All is Green and Comfort).
Here You See A Passer By was a typical 2D maze problem that you likely have done in elementary school. This challenge was worth more than Sight Without Vision, and had less solves than crypto problems (Unquestioned and Unrestrained and Missing and Missed), which I found quite surprising.
All is Green and Comfort was pretty simple. The challenge description said that somebody leaked a secret on their website, so Archive (wayback machine) is the most canonical approach and that indeed was the solution.
I could not figure out one Crack challenge We Mighty, We Meek.
This definitely is a John the Ripper challenge (in particular, office2john).
But despite my best efforts, I couldn't even get anywhere near the solution. I must have done something wrong, inefficient, and/or irrelevant, but I am not sure what I could've or should've done differently. Given that this challenge is worth less than the other two Crack challenges that I managed to solve, I guess I was in a completely wrong direction.