DawgCTF 2022 - Crypto & Reversing

DawgCTF is an introductory Capture The Flag hosted by the UMBC Cyberdawgs, University of Maryland, Baltimore County. I played solo and managed to solve the highest point challenges in cryptography and reversing categories, which are Narrowbranch and Little Hidden.

Narrowbranch (300 points, 10 solves)

Challenge

I implemented this Flag Decryption Module (FDM) to protect our valuable intellectual property. However, I’m not a crypto expert, so maybe there’s some way you can bypass my license restrictions? nc chals.umbccd.net 2256

package main

// Tested with go version go1.18.1 linux/amd64

import (
    "bufio"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "strconv"
    "strings"
)

type FDM struct {
    Key  []byte
    Flag string
}

func NewFDM() *FDM {
    key := make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, key); err != nil {
        fmt.Println("bad crypto, bad life")
        os.Exit(1)
    }
    flag, err := ioutil.ReadFile("flag")
    if err != nil {
        fmt.Println("Could not load flag.")
        fmt.Println("If you see this on the server, contact admins")
        fmt.Println("If you see this on your machine, make a file called 'flag' containing a fake flag for testing.")
        os.Exit(1)
    }
    return &FDM{
        key,
        string(flag),
    }
}

func (fdm *FDM) pad(data []byte) []byte {
    bs := aes.BlockSize
    padding := bs - (len(data) % bs)
    padded_len := len(data) + padding
    padded_data := make([]byte, padded_len)
    copy(padded_data[:len(data)], data)
    for i := len(data); i < len(padded_data); i++ {
        padded_data[i] = byte(padding)
    }
    return padded_data
}

func (fdm *FDM) unpad(data []byte) ([]byte, error) {
    padding_byte := data[len(data)-1]
    padding := int(padding_byte)
    if len(data) < padding {
        return nil, fmt.Errorf("invalid padding")
    }
    for i := len(data) - 1; i > len(data)-padding; i-- {
        if data[i] != padding_byte {
            return nil, fmt.Errorf("invalid padding")
        }
    }
    return data[:len(data)-padding], nil
}

func (fdm *FDM) Encrypt(data []byte) ([]byte, error) {
    padded := fdm.pad(data)

    block, err := aes.NewCipher(fdm.Key)
    if err != nil {
        return nil, err
    }

    ciphertext := make([]byte, aes.BlockSize+len(padded))
    iv := ciphertext[:aes.BlockSize]

    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        return nil, err
    }

    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext[aes.BlockSize:], padded)
    return ciphertext, nil
}

func (fdm *FDM) Decrypt(ciphertext []byte) ([]byte, error) {
    block, err := aes.NewCipher(fdm.Key)
    if err != nil {
        return nil, err
    }

    if len(ciphertext) < aes.BlockSize {
        return nil, fmt.Errorf("Too short ciphertext")
    }
    iv := ciphertext[:aes.BlockSize]
    ciphertext = ciphertext[aes.BlockSize:]

    if len(ciphertext)%aes.BlockSize != 0 {
        return nil, fmt.Errorf("invalid ciphertext len")
    }

    mode := cipher.NewCBCDecrypter(block, iv)
    mode.CryptBlocks(ciphertext, ciphertext)
    return fdm.unpad(ciphertext)
}

func remote_attestation(fdm *FDM) {
    data, err := fdm.Encrypt([]byte(fdm.Flag))
    if err != nil {
        fmt.Println("Error encrypting. This shouldn't happen.")
        return
    }
    fmt.Println("Proof:", hex.EncodeToString(data))
}

func encrypt_flag(fdm *FDM, scanner *bufio.Scanner) {
    fmt.Printf("Input string to encrypt: ")
    if !scanner.Scan() {
        fmt.Println("You didn't enter anything")
        return
    }
    user_data := strings.TrimSpace(scanner.Text())
    data, err := fdm.Encrypt([]byte(user_data))
    if err != nil {
        fmt.Println("Error encrypting. This shouldn't happen.")
        return
    }
    fmt.Println("Encrypted:", hex.EncodeToString(data))
}

func decrypt_flug(fdm *FDM, scanner *bufio.Scanner) {
    fmt.Printf("Input flug to decrypt: ")
    if !scanner.Scan() {
        fmt.Println("You didn't enter anything")
        return
    }
    user_data, err := hex.DecodeString(strings.TrimSpace(scanner.Text()))
    if err != nil {
        fmt.Println("invalid hex")
        return
    }
    data, err := fdm.Decrypt([]byte(user_data))
    if err != nil {
        fmt.Println("could not decrypt")
        return
    }
    flug := string(data)
    if strings.Contains(flug, "CTF") {
        fmt.Println("Please purchase the full version to decrypt flags!")
        fmt.Println("Please allow 4-6 weeks for your order to arrive")
        return
    }
    fmt.Println("Decrypted:", flug)
}

func main() {
    fdm := NewFDM()
    fmt.Println("Narrowbranch Flag Decryption Module")
    fmt.Println("Trial license activated.")

    fmt.Println("Loading...")

    uses := 0
    scanner := bufio.NewScanner(os.Stdin)
    for {
        fmt.Println("1. Remote attestation of flag knowledge")
        fmt.Println("2. Encrypt potential flag (for testing)")
        fmt.Println("3. Decrypt flug (full version supports both flags and flugs)!")
        fmt.Println("4. Exit")
        if !scanner.Scan() {
            break
        }
        choice, err := strconv.Atoi(scanner.Text())
        if err != nil {
            fmt.Println("bad choice")
            continue
        }
        switch choice {
        case 1:
            remote_attestation(fdm)
        case 2:
            encrypt_flag(fdm, scanner)
        case 3:
            decrypt_flug(fdm, scanner)
        case 4:
            return
        default:
            fmt.Println("bad choice")
            continue
        }
        uses += 1
        if uses >= 5 {
            fmt.Println("Trial FDM only allows 5 operations before rebooting.")
            fmt.Println("Thanks for testing our product. Contact your account manager to purchase.")
            return
        }
    }
}

Solution

The main function gives us 4 options:

  • Get encrypted flag
  • Encrypt message
  • Decrypt message
  • Exit

But in option 3, we can’t directly decrypt the encrypted flag we got from option 1 because it checks if there’s a CTF in the plaintext. This happens because the flag has the format DawgCTF{...}.

if strings.Contains(flug, "CTF") {
    fmt.Println("Please purchase the full version to decrypt flags!")
    fmt.Println("Please allow 4-6 weeks for your order to arrive")
    return
}

Since the service uses AES in CBC mode, we can do a byte/bit flipping attack. The idea is to modify the first block of the encrypted flag by flipping the bytes in the CTF position to some other 3 bytes to bypass the check in option 3.

Implementation

#!/usr/bin/env python3
from pwn import *

io = remote('chals.umbccd.net', 2256)

def get_encrypted_flag():
    io.sendlineafter(b'4. Exit\n', b'1')
    io.recvuntil(b'Proof: ')
    enc = bytes.fromhex(io.recvline(0).decode())
    return enc

def decrypt_flug(enc):
    io.sendlineafter(b'4. Exit\n', b'3')
    io.sendlineafter(b'Input flug to decrypt: ', enc.hex().encode())
    try:
        io.recvuntil(b'Decrypted: ')
        dec = io.recvline(0).decode()    
        return dec
    except:
        pass

enc = get_encrypted_flag()
print(enc.hex())

ks = b'\x00'*4 + b'\x01'*3 + b'\x00'*9
changed_iv = xor(enc[:16], ks)
pload = changed_iv + enc[16:]
print(pload.hex())

dec = decrypt_flug(pload)
assert dec[:4] == 'Dawg'
dec = 'DawgCTF' + dec[7:]
print(dec)
$ python3 solve.py 
[+] Opening connection to chals.umbccd.net on port 2256: Done
981f798e0e3b4479e2bf4bd54005db493d2e5e4c507cdba40e49257f4b9f9f9010b207e2fa6bedcec41d4870b8d635fce5acd22077dbda87c1af565bf38ebf44
981f798e0f3a4579e2bf4bd54005db493d2e5e4c507cdba40e49257f4b9f9f9010b207e2fa6bedcec41d4870b8d635fce5acd22077dbda87c1af565bf38ebf44
DawgCTF{maybe_i_sh0uldnt_r011_My_0wN_CrYpT0}
[*] Closed connection to chals.umbccd.net port 2256

Little Hidden (250 points, 15 solves)

Challenge

I guess the flag is a little hidden…

$ file littlehidden.exe
littlehidden.exe: PE32 executable (console) Intel 80386, for MS Windows

Solution

I opened it in IDA and analyzed the pseudocode.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  if ( !sub_4015B8() )
  {
    atexit(sub_4015A0);
    exit(0);
  }
  atexit(sub_401580);
  return 0;
}
int sub_4015B8()
{
  return *(_DWORD *)&NtCurrentPeb()->BeingDebugged & 1;
}

void __cdecl sub_4015A0()
{
  MessageBoxW(0, L"Did you catch that?", L"whoosh!", 0);
}

void __cdecl sub_401580()
{
  MessageBoxW(0, L"You probably shouldn't run code that you're not sure what it does :)", L"Gotcha", 0);
}

Since the challenge name is Little Hidden, I figured the flag isn’t executed or called by the program (unlike typical crackme challenges). After that, I found some suspicious functions.

int __usercall sub_401410@<eax>(int a1@<ebp>)
{
  char *v1; // eax
  char *v2; // esi
  _BYTE *v3; // eax
  int v4; // ecx
  __int128 v6; // [esp-30h] [ebp-3Ch] BYREF
  __int64 v7; // [esp-20h] [ebp-2Ch]
  int v8; // [esp-18h] [ebp-24h]
  char v9; // [esp-14h] [ebp-20h]
  int v10; // [esp-4h] [ebp-10h]
  _DWORD v11[3]; // [esp+0h] [ebp-Ch] BYREF
  void *retaddr; // [esp+Ch] [ebp+0h]

  v11[0] = a1;
  v11[1] = retaddr;
  v6 = 0i64;
  v7 = 0i64;
  v8 = 0;
  v9 = 0;
  sub_4013C0(&v6);
  v1 = (char *)VirtualAlloc(0, 0x100u, 0x3000u, 0x40u);
  v2 = v1;
  if ( v1 )
  {
    memset(v1 + 2, 195, 0xFEu);
    *(_WORD *)v2 = 32747;
    v3 = v2 + 2;
    *(_OWORD *)(v2 + 2) = v6;
    *(_QWORD *)(v2 + 18) = v7;
    *(_DWORD *)(v2 + 26) = v8;
    v2[30] = v9;
    v4 = 29;
    do
    {
      *v3++ ^= 0xABu;
      --v4;
    }
    while ( v4 );
    ((void (*)(void))v2)();
  }
  return sub_4015CC((unsigned int)v11 ^ v10);
}

When I clicked jump to xref on sub_401410, I got thrown into the main function tab, which is interesting. In the sub_401410 function above, it calls sub_4013C0(&v6) that seems to be where the flag is stored.

_BYTE *__cdecl sub_4013C0(_BYTE *a1)
{
  sub_401000(a1);
  sub_4010F0(a1);
  sub_401190(a1);
  sub_401240(a1);
  return sub_401340(a1);
}
_BYTE *__cdecl sub_401000(_BYTE *a1)
{
  _BYTE *result; // eax

  a1[21] = -26;
  a1[23] = -25;
  a1[4] = -24;
  a1[3] = -20;
  a1[1] = -22;
  a1[22] = a1[1] + 1;
  a1[6] = -19;
  *a1 = -16;
  result = a1;
  --*a1;
  return result;
}

int __cdecl sub_4010F0(_BYTE *a1)
{
  int result; // eax

  a1[20] = -12;
  a1[15] = -12;
  a1[13] = -7;
  a1[5] = -1;
  a1[9] = -12;
  result = 2;
  a1[2] = -4;
  return result;
}

_BYTE *__cdecl sub_401190(_BYTE *a1)
{
  _BYTE *result; // eax

  a1[7] = -48;
  a1[24] = -36;
  a1[26] = -39;
  a1[28] = -40;
  result = a1 + 28;
  a1[28] -= 2;
  return result;
}

_BYTE *__cdecl sub_401240(_BYTE *a1)
{
  _BYTE *result; // eax

  a1[8] = -62;
  a1[10] = -57;
  a1[12] = -54;
  a1[14] = a1[12] - 5;
  a1[16] = -51;
  a1[19] = -58;
  result = a1;
  a1[25] = -54;
  return result;
}

_BYTE *__cdecl sub_401340(_BYTE *a1)
{
  _BYTE *result; // eax

  a1[11] = -104;
  a1[27] = -104;
  a1[17] = -96;
  --a1[17];
  a1[18] = -103;
  result = a1 + 17;
  a1[18] += 2;
  return result;
}

From all these functions, we can see that the length of BYTE *a1 is 29 bytes. I got the original value of BYTE *a1 before sub_4013C0 is called. I also tried different numbers to XOR with 0xAB which affects BYTE *a1.

Implementation

#!/usr/bin/env python3

a1 = [0 for _ in range(29)]

a1[0] = -16
a1[0] -= 1
a1[1] = -22
a1[2] = -4
a1[3] = -20
a1[4] = -24
a1[5] = -1
a1[6] = -19
a1[7] = -48
a1[8] = -62
a1[9] = -12
a1[10] = -57
a1[11] = -104
a1[12] = -54
a1[13] = -7
a1[14] = a1[12] - 5
a1[15] = -12
a1[16] = -51
a1[17] = -96
a1[17] -= 1
a1[18] = -103
a1[18] += 2
a1[19] = -58
a1[20] = -12
a1[21] = -26
a1[22] = a1[1] + 1
a1[23] = -25
a1[24] = -36
a1[25] = -54
a1[26] = -39
a1[27] = -104
a1[28] = -40
a1[28] -= 2
print(a1)

a2 = [(x+104) for x in a1]
print(a2)

for i in range(256):
    a3 = [(x+i^0xAB) % 256 for x in a2]
    print(i, bytes(a3))
$ python3 solve.py 
[-17, -22, -4, -20, -24, -1, -19, -48, -62, -12, -57, -104, -54, -7, -59, -12, -51, -97, -101, -58, -12, -26, -21, -25, -36, -54, -39, -104, -42]
[87, 82, 100, 84, 80, 103, 85, 56, 42, 92, 47, 0, 50, 97, 45, 92, 53, 7, 3, 46, 92, 78, 83, 79, 68, 50, 65, 0, 62]
0 b'\xfc\xf9\xcf\xff\xfb\xcc\xfe\x93\x81\xf7\x84\xab\x99\xca\x86\xf7\x9e\xac\xa8\x85\xf7\xe5\xf8\xe4\xef\x99\xea\xab\x95'
1 b'\xf3\xf8\xce\xfe\xfa\xc3\xfd\x92\x80\xf6\x9b\xaa\x98\xc9\x85\xf6\x9d\xa3\xaf\x84\xf6\xe4\xff\xfb\xee\x98\xe9\xaa\x94'
2 b'\xf2\xff\xcd\xfd\xf9\xc2\xfc\x91\x87\xf5\x9a\xa9\x9f\xc8\x84\xf5\x9c\xa2\xae\x9b\xf5\xfb\xfe\xfa\xed\x9f\xe8\xa9\xeb'
...
152 b'DAWGCTF{i_l3aRn_f40m_M@Lwar3}'
...