LACTF 2023 Writeups

Reading time ~6 minutes

This weekend I played my first CTF of 2023, LACTF. Organized by ACM Cyber at UCLA. It featured plenty of challenges to keep me busy for the time I was able to play across pwn, reversing, crypto, misc, and web categories. I played with a team of friends and colleagues this time and focused mostly on Crypto, Pwn and Reversing categories.

These are my solutions for the challenges rev/ctfd_plus and crypto/guess_the_bit.

ctfd-plus - reversing - 397 points

This challenge comes with 1 file, the binary itself. The challenge reads:

CTFd is too insufferably slow. You know why? Because they use an 
SQL database that's bogged down by JOINs instead of a web scale 
database like MongoDB. MongoDB is web scale. You turn it on and 
it scales right up. You know what's more web scale though? 
Nothing. That's right, the throughput of /dev/null is off the 
charts. Behold, CTFd+, the first databaseless CTF platform. 
Can you get the flag for the only challenge?

When executing the binary :

$ ./ctfd_plus

Welcome to CTFd+!
So far, we only have one challenge, which is one more than the number of databases we have.

Very Doable Pwn - 500 points, 0 solves
Can you help me pwn this program?
#include <stdio.h>
int main(void) {
    puts("Bye!");
    return 0;
}

Enter the flag:
lactf{flag}
Incorrect flag.

The binary itself was stripped but we’re quickly able to find the main() function and look at the logic in Ghidra:

undefined8 main(void)

{
  char cVar1;
  size_t sVar2;
  long index;
  undefined4 *puVar3;
  char userinput [256];
  
  puts("Welcome to CTFd+!");
  puts(
      "So far, we only have one challenge, which is one more than the number of databases we have.\n "
      );
  puts("Very Doable Pwn - 500 points, 0 solves");
  puts("Can you help me pwn this program?");
  puts("#include <stdio.h>\nint main(void) {\n    puts(\"Bye!\");\n    return 0;\n}\n");
  puts("Enter the flag:");
  fgets(userinput,0x100,stdin);
  sVar2 = strcspn(userinput,"\n");
  index = 0;
  puVar3 = &ciphertext;
  userinput[sVar2] = '\0';
  do {
    cVar1 = encryptbyte(puVar3[index]);
    if (cVar1 != userinput[index]) {
      puts("Incorrect flag.");
      return 0;
    }
    index = index + 1;
  } while (index != 0x2f);
  puts("You got the flag! Unfortunately we don\'t exactly have a database to store the solve in...")
  ;
  return 0;
}

So it seems like the program does:

  • Asks the user for the flag
  • Iterate over bytes at a constant location in memory encoding them byte by byte
  • Comparing the resulting byte to the user input to see if its correct.

Due to the way this happens, we should be able to inspect memory at each comparison to see what the expected byte value is. We already know the first few bytes will be lactf{since that is the flag format. We can see in the disassembly that this comparison happens at address 0x10b.

Ghidra

We can use GDB to check our assumption is true next:

$ gdb ./ctfd_plus
gdb-peda$ br *0x55555555510b
Breakpoint 1 at 0x55555555510b
gdb-peda$ r
Starting program: /root/ctf/lactf/rev/ctfd/ctfd_plus 
Welcome to CTFd+!
So far, we only have one challenge, which is one more than the number of databases we have.

Very Doable Pwn - 500 points, 0 solves
Can you help me pwn this program?
#include <stdio.h>
int main(void) {
    puts("Bye!");
    return 0;
}

Enter the flag:
lactf{

Breakpoint 1, 0x000055555555510b in ?? ()
gdb-peda$ p $al
$1 = 0x6c

At 0x55555555510b we break and print the value of the register al and it is 0x6c which is what we expect, the ASCII code for lowercase l.

We continue through, breaking at each letter and inspecting $al until we error out:

Breakpoint 1, 0x000055555555510b in ?? ()
gdb-peda$ p $al
$7 = 0x6d
gdb-peda$ c
Continuing.
Incorrect flag

But we already learned something, right before we errored out we learned 1 new byte of the flag, 0x6d which is ASCII code for lowercase m. Now we just need to add that to the input we know and repeat this process to leak each flag byte. I wrote some code to automate this:

from pwn import *

# start with just the known bytes of the flag.
attempt = 'lactf{'

# turn peda color off
open('x.gdb','w').write('peda set option ansicolor off')

while True:
    pos = 0
    log.warn(f"progress: {attempt}")
    with context.local(log_level = 'warn'):
        p = process('gdb --command=x.gdb ./ctfd_plus' , shell=True)
        p.sendlineafter(b'peda$ ', b'br *0x55555555510b')       # 0x55555555510b = cmp
        p.sendlineafter(b'peda$ ', b'r')
        p.sendlineafter(b'Enter the flag:\n', attempt.encode())

        while True:
            # AL register holds what the letter should be in this pos.
            p.sendlineafter(b'peda$ ', b'p $al')            
            res = p.recvline().decode().split(' = ')[1]
            res = chr(int(res,16))
            if pos >= len(attempt):
                attempt += res
                if res == "}":
                    log.warn(f"Flag: {attempt}")
                    quit()
                break            

            pos += 1
            p.sendlineafter(b'peda$ ', b'c')

Which runs and spits out the flag for us:

$ ./solve.py
[!] progress: lactf{
[!] progress: lactf{m
[!] progress: lactf{m4
[!] progress: lactf{m4y
[!] progress: lactf{m4yb
...
[!] progress: lactf{m4yb3_th3r3_1s_s0m3_m3r1t_t0_us1ng_4_db
[!] Flag: lactf{m4yb3_th3r3_1s_s0m3_m3r1t_t0_us1ng_4_db}

Nice. I always prefer to automate GDB rather than reverse an algorithm when its not necessary.

guess-the-bit - crypto - 369 points

This challenge comes with 1 file and a network service which holds the flag. It reads:

I'm trying out for this new game show, but it doesn't seem that hard
since there are only two choices? Regardless, I heard someone name 
Pollard could help me out with it?

chall.py

The script chall.py has the source of the service running online:

import random
from Crypto.Util.number import getPrime

n = 43799663339063312211273714468571591746940179019655418145595314556164983756585900662541462573429625012257141409310387298658375836921310691578072985664621716240663221443527506757539532339372290041884633435626429390371850645743643273836882575180662344402698999778971350763364891217650903860191529913028504029597794358613653479290767790778510701279503128925407744958108039428298936189375732992781717888915493080336718221632665984609704015735266455668556495869437668868103607888809570667555794011994982530936046877122373871458757189204379101886886020141036227219889443327932080080504040633414853351599120601270071913534530651

a = 6

print("n = ", n)
print("a = ", 6)

for i in range(150):
    bit = random.randrange(0,2)
    c = random.randrange(0, n)
    print(f"orig c = {c}")
    c = c**2
    if bit == 1:
        c *= a
    print("c = ", c)
    guess = int(input("What is your guess? "))
    if guess != bit:
        print("Better luck next time!")
        exit()


print("Congrats! Here's your flag: ")
flag = open("flag.txt", "r").readline().strip()
print(flag)
exit(0)

The service does the following:

  • Chooses a random bit 0, or 1
  • Chooses a large random integer c < n
  • Squares c
  • Conditionally, if the bit it chose was 1 then it multiplies c by 6
  • Prints c to the user and asks the user to guess the value of the bit correctly.
  • The user must guess the bit 150 times to get the flag.

My approach to solving this relies on the tell tale sign that, if the bit is 0 then the server will NOT mutiply c * 6 and therefore the c should be a perfect square. Solving this involved some simple client code:

from pwn import *
from math import isqrt

def is_perfect_square(number):
    return pow(isqrt(number),2) == number

p = remote("lac.tf", 31190)
p.recvlines(2)      # Skip 2 lines
res = p.recvline()

l = log.progress('iteration')
m = log.progress('current guess')
count = 0
while True:
    try:
        c = int(res.split(b" = ")[1].decode())
    except IndexError:
        # We're probably at the end.
        p.interactive()

    l.status(f"{count}")

    bit = "1"
    if is_perfect_square(c):
        bit = "0"
    
    m.status(f"{bit}")
    p.sendlineafter(b"What is your guess? ", bit.encode())
    res = p.recvline()

    if b"Better luck next time!" in res:
        p.close()
        quit()
    
    count += 1

Which when we run it, gave us the flag on the first try!

$ ./solve.py 
[+] Opening connection to lac.tf on port 31190: Done
[p] iteration: 150
[o] current guess: 0
[*] Switching to interactive mode
lactf{sm4ll_pla1nt3xt_sp4ac3s_ar3n't_al4ways_e4sy}
[*] Got EOF while reading in interactive

Overall I had fun dipping my toes back into CTFing and solved some good challenges. Great job UCLA team!

Interviewing in Tech: Security Engineer & Security Analyst

Landing a job as a security engineer or analyst at a tech company is a significant feat. It requires not only technical acumen but also s...… Continue reading

BSides Sydney 2023 Writeups

Published on November 24, 2023

DUCTF 2023 Writeups

Published on August 31, 2023