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}'
...