Intro
Part of the Cyberforce competition I covered last week was the anomaly section, with ~60 CTF-style challenges to be solved. Admittedly, some of them were a lot more work than they were worth, but this year, many of them were weighted towards reverse engineering and some forensics. I didn’t get a lot of time to really work on these during the event as I was bogged down in incident response, but here are some writeups, mostly for me, if we’re being honest.
What’s Up Bro? (formerly brah)
Anomaly 13
Description
`We have noticed some suspicious activity leaving a particular machine in the network. We have isolated the machine and recorded its behavior over the course of 20 minutes or so. Either way it is not a full day so hopefully it was enough to get what we need for analysis.
We are worried it is exfiltrating some data over a C2 channel but we have not been able to pinpoint the channel. Our only indicator is some shady website online that keeps showing data that has been leaked from our network and specifically data on that machine!
Author: @pascal_0x90 (LLNL - Nate)
Challenge
We’re only given the README.md with the description and the packet capture, so we can pop the packet capture right into Wireshark. Since the packet capture is ~14 MB, we can use the statistics tab to get a gist of what’s at play here, and we find a lot of TLS traffic.
In order to decrypt TLS, we would somehow need to get access to the session keys, which, unless they were transmitted in cleartext at some point over HTTP, we’re not getting them. The next most frequent protocol is DNS, which is actually quite interesting given the description.
Solution
Unit 42 has a very solid blog explaining exactly how this technique works, and you can see examples of it documented with Sliver or Cobalt Strike. The core idea is that if I have a DNS server and keep track of the DNS queries made to the server, in this case for A records, I can parse data transmitted in the subdomain as arbitrary data, as opposed to actual DNS information. This is not the only way DNS can be leveraged for covert operations. For instance, a TXT record could be used to store payloads that could then be used in PowerShell payloads (source: Alh4zr3d, John Hammond).
If we filter the packet capture for DNS, we see this in action.
The goal now is to recover the data sent through here. Rather than copy this out by hand, we can use pyshark
like we did with corCTF 2022: whack-a-frog to automate this process.
import pyshark
def get_domains():
pcap_path = "./out.pcap"
domains = []
packets = pyshark.FileCapture(pcap_path, display_filter="dns")
print("[*] Parsing packets...")
for pkt in packets:
if pkt.dns.qry_name and "cybrforce.io" in pkt.dns.qry_name and pkt.dns.qry_name not in domains:
domains.append(pkt.dns.qry_name)
packets.close()
print("[+] Parsing complete.")
return domains
domains = get_domains()
payload = ""
for d in domains:
payload += d.split(".")[0]
print(bytes.fromhex(payload))
One thing to note is the pkt.dns.qry_name not in domains
in the if statement; DNS uses UDP, which doesn’t exactly prioritize the continuity of packets, and instead focuses on speed. As a result, UDP packets can get transmitted multiple times. This is not a perfect solution, as the same domain may have been used in two different places with how the plaintext might have been split up, but it ends up working fine here.
If we run the script, we see some base64:
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 13 - What’s up Bro (formerly brah)/challenge_dist$ python3 parse.py
[*] Parsing packets...
[+] Parsing complete.
b'SW4gYSB3b3JsZCB3aGVyZSBieXRlcyBhbmQgcGFja2V0cyBwbGF5LApUaHJvdWdoIHRoZSBkaWdpdGFsIG1pc3QsIHRoZXkgZmluZCB0aGVpciB3YXkuCkFtb25nIHRoZSBzdHJlYW1zIG9mIGRhdGEsIHZhc3QgYW5kIGRlZXAsCkxpZXMgYSBzZWNyZXQgdGhhdCB0aGUgc2hhZG93cyBrZWVwLgoKVGhyb3VnaCBzdWJkb21haW5zLCBhIGpvdXJuZXkgc3B1biwKQSB0YWxlIG9mIGV4ZmlsdHJhdGlvbiwgc3VidGx5IGRvbmUuCkVhY2ggRE5TIHF1ZXJ5LCBhIHNpbGVudCB3aGlzcGVyLApSZXZlYWxzIGEgc3RvcnksIGJvdGggY2xlYXIgYW5kIGNyaXNwZXIuCgpHYXplIHVwb24gdGhlIGZyYWdtZW50cywgc2NhdHRlcmVkIHdpZGUsCldoZXJlIHNlY3JldHMgaW4gdGhlIG9wZW4sIGNob29zZSB0byBoaWRlLgpEYXRhIHRyYXZlbHMgaW4gZGlzZ3Vpc2UsIHNvIHNsZWVrLApNYXNraW5nIHRydXRocyB0aGF0IHRoZSBjdXJpb3VzIHNlZWsuCgpUd2lzdHMgYW5kIHR1cm5zIGluIGV2ZXJ5IGJ5dGUsCkNoYWxsZW5nZSB0aGUgbWluZCwgYm90aCBkYXkgYW5kIG5pZ2h0LgpTZWVrZXJzIHNpZnQgdGhyb3VnaCByZWNvcmRzLCB2YXN0IGFuZCB0YWxsLApEZWNvZGluZyBtZXNzYWdlcyB0aGF0IHNpbGVudGx5IGNhbGwuCgpJbiBhIHN5bXBob255IG9mIGRpZ2l0YWwgZmxvd3MsCkxpZXMgYSBwYXR0ZXJuIG9ubHkgdGhlIHZpZ2lsYW50IGtub3dzLgpTdWJ0bGUgY2x1ZXMgaW4gdGhlIHZhc3QgZGF0YSBmb2csCkxlYWQgdG8gdGhlIHJldmVsYXRpb24sIG5vdCBqdXN0IGFueSBsb2cuCgpCdXQgaW4gdGhpcyBjeWJlciBxdWVzdCBzbyBncmFuZCwKQmUgd2FyeSBvZiB3aGF0IHRoZSBudW1iZXJzIGRlbWFuZC4KTm8gcGVyc29uYWwgc2VjcmV0cywgbm8gbnVtYmVycyB0byB0cmFjaywKSnVzdCBhIHB1enpsZSB0byBzb2x2ZSwgbm8gZXRoaWNhbCBjcmFjay4KCkFuZCBzbyB0aGUgam91cm5leSBjb21lcyB0byBhbiBlbmQsCkEgdGFsZSBvZiBpbnRyaWd1ZSwgYXJvdW5kIGV2ZXJ5IGJlbmQuCkJ1dCBhbGFzLCB0aGVyZSB3YXMganVzdCBhIGZsYWcsCkhpZGRlbiBub3QgaW4gcmljaGVzLCBub3IgaW4gYSByYWc6CgoiZmxhZ3t3aDR0NV91cF9icjBfdzNyM195MHVfY2gwcHAxbl9sMGc1P30iCgpJbiB0aGVzZSBjaGFyYWN0ZXJzLCB2aWN0b3J5IGlzIGNsZWFyLApGb3IgdGhvc2Ugd2hvIHNvdWdodCwgd2l0aCBtaW5kcyBzbyBzaGVlci4KVGhlIGNoYWxsZW5nZSBjb21wbGV0ZSwgdGhlIGpvdXJuZXksIGEgc29uZywKSW4gdGhlIHdvcmxkIG9mIGN5YmVyc3BhY2UsIHdoZXJlIG1pbmRzIGJlbG9uZy4='
We can add an extra line to then decode the base64, and find the flag:
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 13 - What’s up Bro (formerly brah)/challenge_dist$ python3 parse.py
[*] Parsing packets...
[+] Parsing complete.
b'In a world where bytes and packets play,\nThrough the digital mist, they find their way.\nAmong the streams of data, vast and deep,\nLies a secret that the shadows keep.\n\nThrough subdomains, a journey spun,\nA tale of exfiltration, subtly done.\nEach DNS query, a silent whisper,\nReveals a story, both clear and crisper.\n\nGaze upon the fragments, scattered wide,\nWhere secrets in the open, choose to hide.\nData travels in disguise, so sleek,\nMasking truths that the curious seek.\n\nTwists and turns in every byte,\nChallenge the mind, both day and night.\nSeekers sift through records, vast and tall,\nDecoding messages that silently call.\n\nIn a symphony of digital flows,\nLies a pattern only the vigilant knows.\nSubtle clues in the vast data fog,\nLead to the revelation, not just any log.\n\nBut in this cyber quest so grand,\nBe wary of what the numbers demand.\nNo personal secrets, no numbers to track,\nJust a puzzle to solve, no ethical crack.\n\nAnd so the journey comes to an end,\nA tale of intrigue, around every bend.\nBut alas, there was just a flag,\nHidden not in riches, nor in a rag:\n\n"flag{wh4t5_up_br0_w3r3_y0u_ch0pp1n_l0g5?}"\n\nIn these characters, victory is clear,\nFor those who sought, with minds so sheer.\nThe challenge complete, the journey, a song,\nIn the world of cyberspace, where minds belong.'
flag: flag{wh4t5_up_br0_w3r3_y0u_ch0pp1n_l0g5?}
EmojiWare
Anomaly 14
Description
This is an entire program written in emojis... Yes. You heard right. Emojis! The program that is in this challenge is an emulator that can interpret the emojis and provide emulation support. The code created acts like all the files on the computer have been encrypted. The goal of this challenge? Get the program to print out the encrypted flag.
Author: @pascal_0x90 (LLNL - Nate)
Challenge
We’re given a few files to work with here.
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 14 - EmojiWare/dist$ ls -la
total 6500
drwxr-xr-x 2 kali kali 4096 Nov 2 15:22 .
drwx------ 4 kali kali 4096 Nov 9 11:21 ..
-rw-r--r-- 1 kali kali 161 Oct 29 07:52 Dockerfile
-rw-r--r-- 1 kali kali 658415 Oct 29 07:52 emojis.out
-rw-r--r-- 1 kali kali 5979208 Oct 29 07:52 emulator
-rw-r--r-- 1 kali kali 378 Nov 2 15:23 README.txt
The README.txt
contained the description, so that’s unimportant. The emojis.out
is exactly what it sounds like, it’s a lot of emojis.
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 14 - EmojiWare/dist$ head emojis.out
👻😡🤖👻😓🤖👻😷🤖😖😡🙈😖😡🙈😖😡🙈😖😡👾😖😷🤖😭😡😷👻😡🤖👻😓🤖👻😷🤖😖😡🙈😖😡🙈😖😡🙈😖😡👾😖😷😍😭😡😷👻😡🤖👻😓🤖👻😷🤖😖😡[trim...]
The emulator
is an ELF executable that seems like the point of interest here, but we’ll come back to that shortly.
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 14 - EmojiWare/dist$ file emulator
emulator: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b3a59e7c076b3b5dce5196ca64d323f0e0d84424, for GNU/Linux 2.6.32, stripped
The Dockerfile
is an interesting addition, since emulator
isn’t built on some esoteric architecture. Reading it, we get an interesting clue.
FROM ubuntu:18.04
RUN apt update && apt install -y gcc python3.8-dev
ADD ./emulator /emulator
ADD ./emojis.out /emojis.out
WORKDIR /
ENTRYPOINT ["/emulator"]
It doesn’t make sense to explicitly include python3.8-dev
without actual python unless (a) this is a distraction or (b) there’s some Python-magic going on here. If we start running through our basic reversing checks, something immediately jumps out at us:
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 14 - EmojiWare/dist$ strings -n 8 emulator
[trim...]
blib-dynload/resource.cpython-38-x86_64-linux-gnu.so
blib-dynload/termios.cpython-38-x86_64-linux-gnu.so
blibbz2.so.1.0
blibcrypto.so.1.1
blibexpat.so.1
blibffi.so.6
bliblzma.so.5
blibmpdec.so.2
blibpython3.8.so.1.0
blibssl.so.1.1
blibz.so.1
opyi-contents-directory _internal
xbase_library.zip
zPYZ-00.pyz
4libpython3.8.so.1.0
.shstrtab
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got.plt
.comment
Solution
Notice the references to Python? Unless the author was trying to make it seem as if this was written with Python, there is very real likelihood that this binary was produced by using some library to compile Python to executable code. We can test this by pointing pyinstxtractor at it.
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 14 - EmojiWare/dist$ python3 pyinstxtractor.py emulator
[+] Processing emulator
[+] Pyinstaller version: 2.1+
[+] Python version: 3.8
[+] Length of package: 5923152 bytes
[+] Found 42 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: emulator.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.8 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: emulator
You can now use a python decompiler on the pyc files within the extracted directory
Our hypothesis was right, and we can now dig into the emulator_extracted/
directory to find emulator.pyc
which has the Python byte code in it (i.e. a compiled Python file, which is what the interpreter actually parses).
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 14 - EmojiWare/dist$ ls -la emulator_extracted/
total 11480
drwxr-xr-x 4 kali kali 4096 Nov 9 11:46 .
drwxr-xr-x 3 kali kali 4096 Nov 9 11:46 ..
-rw-r--r-- 1 kali kali 841682 Nov 9 11:46 base_library.zip
-rw-r--r-- 1 kali kali 11358 Nov 9 11:46 emulator.pyc
-rw-r--r-- 1 kali kali 66728 Nov 9 11:46 libbz2.so.1.0
-rw-r--r-- 1 kali kali 2917216 Nov 9 11:46 libcrypto.so.1.1
drwxr-xr-x 2 kali kali 4096 Nov 9 11:46 lib-dynload
-rw-r--r-- 1 kali kali 202880 Nov 9 11:46 libexpat.so.1
-rw-r--r-- 1 kali kali 31032 Nov 9 11:46 libffi.so.6
-rw-r--r-- 1 kali kali 153912 Nov 9 11:46 liblzma.so.5
-rw-r--r-- 1 kali kali 227944 Nov 9 11:46 libmpdec.so.2
-rw-r--r-- 1 kali kali 5477560 Nov 9 11:46 libpython3.8.so.1.0
-rw-r--r-- 1 kali kali 577312 Nov 9 11:46 libssl.so.1.1
-rw-r--r-- 1 kali kali 116960 Nov 9 11:46 libz.so.1
-rw-r--r-- 1 kali kali 875 Nov 9 11:46 pyiboot01_bootstrap.pyc
-rw-r--r-- 1 kali kali 3678 Nov 9 11:46 pyimod01_archive.pyc
-rw-r--r-- 1 kali kali 16926 Nov 9 11:46 pyimod02_importers.pyc
-rw-r--r-- 1 kali kali 4019 Nov 9 11:46 pyimod03_ctypes.pyc
-rw-r--r-- 1 kali kali 851 Nov 9 11:46 pyi_rth_inspect.pyc
-rw-r--r-- 1 kali kali 2425 Nov 9 11:46 pyi_rth_multiprocessing.pyc
-rw-r--r-- 1 kali kali 1158 Nov 9 11:46 pyi_rth_pkgutil.pyc
-rw-r--r-- 1 kali kali 1043281 Nov 9 11:46 PYZ-00.pyz
drwxr-xr-x 2 kali kali 4096 Nov 9 11:46 PYZ-00.pyz_extracted
-rw-r--r-- 1 kali kali 311 Nov 9 11:46 struct.pyc
I’m in the process of writing something on reversing Python malware, so I’ll save an in-depth discussion of the topic for then, but pycdc
is nice in that we can clone the repo, build the project, and just point the decompiler at the .pyc
file. The build instructions aren’t entirely clear from the repo, but you can just set up the Makefile using cmake
and go from there.
kali@transistor:~/Documents/cyberforce-23/anomalies$ cd pycdc/
kali@transistor:~/Documents/cyberforce-23/anomalies/pycdc$ cmake -S .
-- The C compiler identification is GNU 13.1.0
-- The CXX compiler identification is GNU 13.1.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found PythonInterp: /usr/bin/python (found version "3.11.4")
-- Configuring done (0.4s)
-- Generating done (0.0s)
-- Build files have been written to: /home/kali/Documents/cyberforce-23/anomalies/pycdc
kali@transistor:~/Documents/cyberforce-23/anomalies/pycdc$ make
[trim...]
I’ll move the emulator.pyc
out to a different directory, and then point pycdc
at it to obtain the original source code.
kali@transistor:~/Documents/cyberforce-23/anomalies/Z-2023 CFC Dependency Files/Anomaly 14 - EmojiWare/dist$ ../../../pycdc/pycdc emulator.pyc
# Source Generated with Decompyle++
# File: emulator.pyc (Python 3.8)
Unsupported opcode: BEGIN_FINALLY
from io import TextIOWrapper
from sys import stdout, stdin, exit
from math import ceil
from time import sleep
from copy import deepcopy
from enum import Enum, auto
from dataclasses import dataclass
from multiprocessing.dummy import Process
from typing import Dict, Tuple
class ISA(Enum):
IMM = auto()
ADD = auto()
[trim...]
We get a couple of warnings, but it seems like we get most of the source code, which we can put in a separate .py
file for easier viewing. The file itself comes out to be ~400 lines, so I might put it in a gist or on GitHub later, so we’ll only be looking at the most relevant segments.
We’ve solved a challenge similar in concept to this before here, HTB’s Alien Saboteur challenge was a nice introduction to VM reversing challenges, and I highly recommend you check that out if you’re unfamiliar with the idea. Luckily for us here, the emulator is written in Python, so more of the work is on parsing the emojis.out
than actually reversing the VM. Lines 36 - 72 give us what each of the emojis mean.
VMCODE = {
'🈳': ISA.NOP,
'➕': ISA.ADD,
'😖': ISA.ADDI,
'➖': ISA.SUB,
'✨': ISA.SUBI,
'❌': ISA.MULT,
'⏬': ISA.PUSH,
'🔝': ISA.POP,
'😄': ISA.LDM,
'😭': ISA.STM,
'💯': ISA.CMP,
'🚀': ISA.JMP,
'🌮': ISA.JMPN,
'💀': ISA.SYS,
'👻': ISA.IMM,
'🥑': ISA.XOR }
VMDATA = {
'🤖': 0,
'😍': 1,
'💢': 2,
'🤙': 3,
'😩': 4,
'👾': 5,
'🤢': 6,
'😿': 7,
'💙': 8,
'🙉': 9,
'🙈': 10 }
REGS = {
'🤡': 'SP',
'🦷': 'IP',
'😡': 'A',
'😓': 'B',
'😷': 'C',
'🤥': 'D',
'😿': 'F' }
The emulator, as expected, tells us exactly how to interpret these opcodes in the interp_instr()
function.
def parse_instr(self = None, instr = None):
opcode = instr[self.order[0]]
arg1 = instr[self.order[1]]
arg2 = instr[self.order[2]]
return (opcode, arg1, arg2)
# ...trim...
def interp_instr(self, instr):
(opcode, arg1, arg2) = self.parse_instr(instr)
op = VMCODE[opcode]
if op == ISA.NOP:
temp = 3
temp2 = 2 + temp
temp3 = temp + temp2
del temp3
del temp2
return None
if None == ISA.ADD:
(r1name, reg1val) = self.get_register_value(arg1)
(_, reg2val) = self.get_register_value(arg2)
val = reg1val + reg2val
calculated = val
self.set_register_value(r1name, calculated)
return None
if None == ISA.ADDI:
(r1name, reg1val) = self.get_register_value(arg1)
imm = VMDATA[arg2]
val = reg1val + imm
calculated = val
self.set_register_value(r1name, calculated)
return None
# trim...
An interesting thing to note is the Processor()
class that this is all coming from seems to have code to debug the registers.
def print_regs(self):
regs = f'''\n======\nSP: {self._REGISTERS.SP}\nIP: {(self._REGISTERS.IP - 2048) / 3}\n======\nA: {self._REGISTERS.A}\nB: {self._REGISTERS.B}\nC: {self._REGISTERS.C}\nD: {self._REGISTERS.D}\n======\nF: {self._REGISTERS.F}\n======\nSTACK (first 10 values from SP):\n{self._MEM[max(0, (self._REGISTERS.SP - 10) + 1):self._REGISTERS.SP + 1]}\n======\n '''
print(regs)
def load_code(self, exec_code):
'''
MEM:
code
stack
data_stored
'''
code = deepcopy(exec_code)
MAX_SIZE = roundup(len(code) + 2048)
self._MEM = [
''] * MAX_SIZE
for i in range(len(code)):
self._MEM[i + 2048] = code[i]
stats = f'''\n CODE LENGTH: {len(code)}\n MEM LENGTH: {len(self._MEM)}\n '''
print(stats)
def init_registers(self = None):
if self._REGISTERS is not None:
print('Overriding a current state!')
s = input('Continue? [yY/nN]')
if 'n' in s.lower():
return -1
self._REGISTERS = None(0, 2048, 0, 0, 0, 0, 0)
return 0
However, it seems like these functions don’t get used in the actual execution of the program when we try to run the emulator (which I totally forgot to check until now).
$ ./emulator
CODE LENGTH: 164622
MEM LENGTH: 166912
################################################
# YOUR COMPUTER HAS BEEN FULLY ENCRYPTED!!!! #
# IN ORDER TO GET YOUR FILES BACK #
# YOU MUST ENTER IN THE SECRET KEY #
# THAT WE SEND YOU IN EXCHANGE FOR #
# DOGECOIN. #
################################################
ENTER IN THE SECRET KEY:PASSWORD
YOU HAVE ENTERED AN INCORRECT KEY! KILLING DECRYPTOR!
From here, there’s a couple of different ways to go about this:
- Write a disassembler like we did for Alien Saboteur
- Clean up the
emulator.py
we got frompycdc
and use that to debug the registers - Debug
emulator
with GDB and find the right things to breakpoint on
The third is definitely the worst way to go about this, since it’s still using Python bytecode in the ELF, so we’re not only debugging the emoji VM, but also the underlying Python VM used to make the emulator. As for the other two options, I actually originally tried to do option 1, but when I did, I got 54873 instructions. For reference, the other VM challenge I did only had < 1000. For sanity’s sake (although the brain worms want me to look at the assembly), we’re reconstructing the emulator.
As good as pycdc
is, it did not give us a perfect decompile. For one, there’s various continue
statements strewn across the program in weird spots, and I also have ambiguous things happening:
REGISTERS = dataclass(<NODE:12>)
FILE = dataclass(<NODE:12>)
Part of the reason Python bytecode reversing is so funky is that with every new release of Python, the way control flow works is slightly different. pycdc
’s merits come from the fact it’s written in C++ and generally does not care for the version up until the more recent ones. However, it seems as though we might need to use a Python tool instead, and I used decompyle3, because pycdc
told us the version was 3.8, and that’s what decompyle3
was made for.
In order to use decompyle3
, you’ll need an install of Python 3.8. The easiest solution would probably be using pyenv to manage the versions you have installed, but I used Docker. I pulled down the Python 3.8.5 Docker container, mounted my current directory using a volume, and then entered the container.
Note, when using volumes, make sure your path has no weird characters in it or spaces, you’ll get an error like this and be confused until you remember why!
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist/better$ docker run --rm -it -v $(pwd):/data -d python:3.8.5
Unable to find image 'python:3.8.5' locally
3.8.5: Pulling from library/python
57df1a1f1ad8: Pull complete
71e126169501: Pull complete
1af28a55c3f3: Pull complete
03f1c9932170: Pull complete
65b3db15f518: Pull complete
3e3b8947ed83: Pull complete
a4850b8bdbb7: Pull complete
416533994968: Pull complete
1b580f9ce4ce: Pull complete
Digest: sha256:e9b7e3b4e9569808066c5901b8a9ad315a9f14ae8d3949ece22ae339fff2cad0
Status: Downloaded newer image for python:3.8.5
eecf03d12646e7f2835d8fc05277ca368767deeddcebfe2b1446576881ef350f
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist/better$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
eecf03d12646 python:3.8.5 "python3" About a minute ago Up About a minute fervent_kirch
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist/better$ docker exec -it eecf03d12646 /bin/bash
root@eecf03d12646:/# cd /data
root@eecf03d12646:/data# ls -la
total 20
drwxr-xr-x 2 1000 1000 4096 Nov 12 03:18 .
drwxr-xr-x 1 root root 4096 Nov 12 03:21 ..
-rw-r--r-- 1 1000 1000 11358 Nov 12 00:36 emulator.pyc
root@eecf03d12646:/data# pip install decompyle3
root@eecf03d12646:/data# decompyle3 emulator.pyc > emulator.py
root@eecf03d12646:/data# exit
exit
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist/better$ docker stop eecf03d12646
eecf03d12646
This emulator.py
is already way better if you look at the source code. Let’s try running it and see what happens to make sure it works.
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3 emulator.py
CODE LENGTH: 164622
MEM LENGTH: 166912
Traceback (most recent call last):
File "/home/kali/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist/emulator.py", line 496, in <module>
p.execvm()
File "/home/kali/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist/emulator.py", line 140, in execvm
self.interp_instr(instr=instr)
File "/home/kali/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist/emulator.py", line 224, in interp_instr
r1name, reg1val = self.get_register_value(arg1)
^^^^^^^^^^^^^^^
TypeError: cannot unpack non-iterable NoneType object
Ah. Very cool! This part took a minute to figure out, but the problem function is here:
REGS_INV = {k: v for k, v in REGS.items()}
# ...trim...
def get_register_value(self, reg) -> Tuple[(str, int)]:
try:
emoji = REGS_INV[reg]
except:
emoji = reg
else:
if emoji == '🤡':
return ('SP', self._REGISTERS.SP)
if emoji == '🦷':
return ('IP', self._REGISTERS.IP)
if emoji == '😡':
return ('A', self._REGISTERS.A)
if emoji == '😓':
return ('B', self._REGISTERS.B)
if emoji == '😷':
return ('C', self._REGISTERS.C)
if emoji == '🤥':
return ('D', self._REGISTERS.D)
if emoji == '😿':
return ('F', self._REGISTERS.F)
The problem is that the REGS_INV
dictionary doesn’t actually invert the keys and values, and the else
statement here isn’t really necessary. We can fix that pretty quickly though:
REGS_INV = {v: k for k, v in REGS.items()}
# ...trim...
def get_register_value(self, reg) -> Tuple[(str, int)]:
try:
emoji = REGS_INV[reg]
except:
emoji = reg
if emoji == '🤡':
return ('SP', self._REGISTERS.SP)
if emoji == '🦷':
return ('IP', self._REGISTERS.IP)
if emoji == '😡':
return ('A', self._REGISTERS.A)
if emoji == '😓':
return ('B', self._REGISTERS.B)
if emoji == '😷':
return ('C', self._REGISTERS.C)
if emoji == '🤥':
return ('D', self._REGISTERS.D)
if emoji == '😿':
return ('F', self._REGISTERS.F)
If we try it again, we can safely say we’ve restored the emulator!
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3 emulator.py
CODE LENGTH: 164622
MEM LENGTH: 166912
################################################
# YOUR COMPUTER HAS BEEN FULLY ENCRYPTED!!!! #
# IN ORDER TO GET YOUR FILES BACK #
# YOU MUST ENTER IN THE SECRET KEY #
# THAT WE SEND YOU IN EXCHANGE FOR #
# DOGECOIN. #
################################################
ENTER IN THE SECRET KEY:password
YOU HAVE ENTERED AN INCORRECT KEY! KILLING DECRYPTOR!
So what’s changed? Now that we have the Python source code, we can change things how ever we want! In particular, we can change the code that interprets instructions to print out exactly what’s happening. Since this is a crackme, we can start by checking any uses of the CMP
instruction.
# ...trim
if op == ISA.CMP:
_, reg1val = self.get_register_value(arg1)
_, reg2val = self.get_register_value(arg2)
# NEW
print(f"[DEBUG] {reg1val} == {reg2val}")
self._REGISTERS.F = 0
if reg1val == reg2val:
self._REGISTERS.F |= 1
if reg1val > reg2val:
self._REGISTERS.F |= 2
if reg1val < reg2val:
self._REGISTERS.F |= 4
if reg1val != reg2val:
self._REGISTERS.F |= 8
return
# trim...
If we try running the program now, we get a debug statement.
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3 emulator.py
CODE LENGTH: 164622
MEM LENGTH: 166912
################################################
# YOUR COMPUTER HAS BEEN FULLY ENCRYPTED!!!! #
# IN ORDER TO GET YOUR FILES BACK #
# YOU MUST ENTER IN THE SECRET KEY #
# THAT WE SEND YOU IN EXCHANGE FOR #
# DOGECOIN. #
################################################
ENTER IN THE SECRET KEY:password
[DEBUG] 69 == 119
YOU HAVE ENTERED AN INCORRECT KEY! KILLING DECRYPTOR!
There are two things to be gleaned from this:
- Despite submitting an 8 character entry, we only had one comparison. This could either be because of a length check, or it is checking one byte at a time and exiting if it’s wrong.
- 69 (nice) and 119 are both decimal values, neither of which correspond to the first character. 119 could be the “w” in the middle of the password but that’s just weird.
If we make a hypothesis and assume that there’s some encryption going on, we could try to hook the XOR opcode as well and see what happens.
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3 emulator.py
CODE LENGTH: 164622
MEM LENGTH: 166912
################################################
# YOUR COMPUTER HAS BEEN FULLY ENCRYPTED!!!! #
# IN ORDER TO GET YOUR FILES BACK #
# YOU MUST ENTER IN THE SECRET KEY #
# THAT WE SEND YOU IN EXCHANGE FOR #
# DOGECOIN. #
################################################
ENTER IN THE SECRET KEY:password
[DEBUG]: A <-- 69 = 0 ^ 69
[DEBUG]: A <-- 12 = 0 ^ 12
[DEBUG]: A <-- 69 = 0 ^ 69
[DEBUG]: A <-- 12 = 0 ^ 12
[DEBUG]: A <-- 69 = 0 ^ 69
[DEBUG]: A <-- 12 = 0 ^ 12
[trim...]
Turns out there’s a lot of XORs, ~100 to be exact. We also see that there’s an alternating pattern of XORs with 69 then 12, which we can only assume to be the key (remember that this is decimal!). I’ll use the cyclic
tool from pwntools to generate a string of 100 characters to see if I can pass the length check.
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3 emulator.py
CODE LENGTH: 164622
MEM LENGTH: 166912
################################################
# YOUR COMPUTER HAS BEEN FULLY ENCRYPTED!!!! #
# IN ORDER TO GET YOUR FILES BACK #
# YOU MUST ENTER IN THE SECRET KEY #
# THAT WE SEND YOU IN EXCHANGE FOR #
# DOGECOIN. #
################################################
ENTER IN THE SECRET KEY:aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[DEBUG]: A <-- 36 = 97 ^ 69
[DEBUG]: A <-- 109 = 97 ^ 12
[DEBUG]: A <-- 36 = 97 ^ 69
[DEBUG]: A <-- 109 = 97 ^ 12
[DEBUG]: A <-- 39 = 98 ^ 69
[DEBUG]: A <-- 109 = 97 ^ 12
[DEBUG]: A <-- 36 = 97 ^ 69
[DEBUG]: A <-- 109 = 97 ^ 12
[DEBUG]: A <-- 38 = 99 ^ 69
[DEBUG]: A <-- 109 = 97 ^ 12
[trim...]
[DEBUG] 36 == 119
YOU HAVE ENTERED AN INCORRECT KEY! KILLING DECRYPTOR!
It still didn’t give us additional CMP checks, but at least we can confirm that the XORs are being applied to the password. At this point, you could write a pwntools script to bruteforce the password, or we could try to “dump the memory” at the password check. I can modify the execvm()
function as follows:
def execvm(self):
self.init_registers()
self._REGISTERS.IP = 2048
while self._REGISTERS.IP >= 2048:
while self._REGISTERS.IP <= len(self._MEM):
try:
instr = self._MEM[self._REGISTERS.IP:self._REGISTERS.IP + 3]
self.interp_instr(instr=instr)
except KeyboardInterrupt:
#self.print_regs()
print(self._MEM[:2048])
input()
self._REGISTERS.IP += 3
Now, when I hit CTRL+C at the password prompt, I see this.
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3 emulator.py
CODE LENGTH: 164622
MEM LENGTH: 166912
################################################
# YOUR COMPUTER HAS BEEN FULLY ENCRYPTED!!!! #
# IN ORDER TO GET YOUR FILES BACK #
# YOU MUST ENTER IN THE SECRET KEY #
# THAT WE SEND YOU IN EXCHANGE FOR #
# DOGECOIN. #
################################################
ENTER IN THE SECRET KEY:^C['', 0, '', [...trim...] '', '', '', '', '', 119, 60, 33, 60, 113, 58, 38, 57, 125, 105, 114, 61, 119, 61, 115, 56, 32, 106, 116, 109, 32, 62, 35, 53, 36, 52, 118, 56, 113, 109, 118, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
Interestingly, this is only ~32 bytes. Still, we can pull these numbers out, apply the XOR key, and see what the plaintext is.
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3
Python 3.11.4 (main, Jun 7 2023, 10:13:09) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> ct = bytearray([119, 60, 33, 60, 113, 58, 38, 57, 125, 105, 114, 61, 119, 61, 115, 56, 32, 106, 116, 109, 32, 62, 35, 53, 36, 52, 118, 56, 113, 109, 118, 105])
KeyboardInterrupt
>>> from pwn import xor # this is just easy
>>> ct = bytearray([119, 60, 33, 60, 113, 58, 38, 57, 125, 105, 114, 61, 119, 61, 115, 56, 32, 106, 116, 109, 32, 62, 35, 53, 36, 52, 118, 56, 113, 109, 118, 105])
>>> key = bytes.fromhex('450c')
>>> xor(ct, key)
b'20d046c58e712164ef1ae2f9a8344a3e'
If I copy this result three times into the password prompt 4 times (to reach the 100 characters), we get the flag.
kali@transistor:~/Documents/cyberforce-23/anomalies/2023-CFC-Dependencies/A14-Emojiware/dist$ python3 emulator.py
CODE LENGTH: 164622
MEM LENGTH: 166912
################################################
# YOUR COMPUTER HAS BEEN FULLY ENCRYPTED!!!! #
# IN ORDER TO GET YOUR FILES BACK #
# YOU MUST ENTER IN THE SECRET KEY #
# THAT WE SEND YOU IN EXCHANGE FOR #
# DOGECOIN. #
################################################
ENTER IN THE SECRET KEY:20d046c58e712164ef1ae2f9a8344a3e20d046c58e712164ef1ae2f9a8344a3e20d046c58e712164ef1ae2f9a8344a3e20d046c58e712164ef1ae2f9a8344a3e
Congrats! Here is the flag:
flag{3m0j1s_L1ght_Up_My_D4y}
flag: flag{3m0j1s_L1ght_Up_My_D4y}
WATT’s The Story Morning Glory?
Anomaly 44 - 47
Description
You are a seasoned QA Engineer at DER8.9 testing a new system named the 'SmartMeter Workstation for Administration of Telemetric Technologies (WATT) Control and Maintenance Interface' prior to deployment for business customers and residential field technicians. Before this software goes live, it's crucial to ensure that it is not only free from defects but also securely designed and implemented. Thoroughly test the application and identify all functional and security issues, spotting any vulnerabilities and insecure code practices. Retrieve a set of 4 associated flags for each insecure coding practice / vulnerability as a proof of discovery.
Author: @ANL - Jocelyn
To any Cyberforce competitor, I sincerely apologize for the existence of this challenge. I am friends with the challenge author outside of this event and I am the one who mentioned Nim when that was hot (before I realized writing C, or even better, PIC shellcode, was God’s way).
Solution 1: Hardcoded Key
We’re given a file called smartmeter_management_interface.exe
, some additional information in Question.md
that just adds some context that isn’t necessarily worth mentioning here. If I run the binary, we’re immediately hit with roadblock #1.
Ah, a crackme. Of course.
Before jumping to disassembling the binary, let’s briefly take a look at what PEStudio tells us. There are no immediately weird looking imports (e.g. CreateRemoteThread
, Nt*
), but as soon as we look at strings, we know what were up against.
If it isn’t my old enemy Nim. For the uninitiated, Nim is a language that was very hyped up last year in the red teaming space as something extremely evasive and hard to reverse engineer, while also embracing Python-like simplicity in syntax, yet supporting memory management and macros like languages like Rust or C++ do. An in-depth discussion of the merits of using Nim and other esolangs for malware development is beyond the scope of this blog, but the key factor we’ll be dealing with is the reverse engineering bit.
Nim compiles to C, and then uses a compiler for C to finish it off, meaning symbols and functions get seriously garbled. It’s still very possible to work through it, it’s just a pain. For languages like Nim, I prefer using Cutter over Ghidra. Once the binary is loaded into Cutter, we can start by looking for the main function. If we use the side bar to search for main
, we’re greeted with 6 different “mains”.
For the purposes of reverse engineering, we can usually jump right to NimMainModule
. Keeping the window open in graph view, we have a little bit of noise from what Nim does at an assembly level, but we can stay focused if we just look for calls to other symbols that look like functions. Eventually, we should find main__smartmeter95management95interface_528
, which is where we can actually see assembly that corresponds to what we saw with the execution. Let’s zoom into the first block of assembly here:
0x140038213 push rbp
0x140038214 mov rbp, rsp
0x140038217 sub rsp, 0x190
0x14003821e lea rax, str.main ; 0x14004ae07
0x140038225 mov qword [var_50h], rax
0x140038229 lea rax, str.C:_Cyberforce_November_2023_smartmeter_management_interface.nim ; 0x14004ab90
0x140038230 mov qword [var_40h], rax
0x140038234 mov qword [var_48h], 0
0x14003823c mov word [var_38h], 0
0x140038242 lea rax, [var_58h]
0x140038246 mov rcx, rax ; int64_t arg1
0x140038249 call nimFrame ; sym.nimFrame_0x14003346b
0x14003824e call getFrame ; sym.getFrame_0x140036e8e
0x140038253 mov qword [var_10h], rax
0x140038257 mov qword [var_48h], 0x125 ; 293
0x14003825f lea rax, str.C:_Cyberforce_November_2023_smartmeter_management_interface.nim ; 0x14004ab90
0x140038266 mov qword [var_40h], rax
0x14003826a call mainBanner__smartmeter95management95interface_126 ; sym.mainBanner__smartmeter95management95interface_126
0x14003826f mov qword [var_48h], 0x126 ; 294
0x140038277 lea rax, str.C:_Cyberforce_November_2023_smartmeter_management_interface.nim ; 0x14004ab90
0x14003827e mov qword [var_40h], rax
0x140038282 mov edx, 1 ; int64_t arg2
0x140038287 lea rax, data.140048e38 ; 0x140048e38
0x14003828e mov rcx, rax ; int64_t arg1
0x140038291 call echoBinSafe ; sym.echoBinSafe
0x140038296 mov qword [var_48h], 0x129 ; 297
0x14003829e lea rax, str.C:_Cyberforce_November_2023_smartmeter_management_interface.nim ; 0x14004ab90
0x1400382a5 mov qword [var_40h], rax
0x1400382a9 mov byte [var_19h], 0
0x1400382ad call accessMaintenanceInterface__smartmeter95management95interface_245 ; sym.accessMaintenanceInterface__smartmeter95management95interface_245
0x1400382b2 mov byte [var_19h], al
0x1400382b5 cmp byte [var_19h], 0
0x1400382b9 jne 0x140038317
If you’ve been following along, or you see this mess, you begin to understand why looking at Nim can be challenging- there’s just a lot of stuff that you don’t need to be looking at 90% of the time. However, at 0x14003826a
, we see a call to what looks like the function that prints the main banner. At 0x1400382ad
, we have another call, this time to the accessMaintenanceInterface()
function. Looking at that function, this bit of assembly jumps out at me.
0x14003444a mov qword [var_70h], rax
0x14003444e mov qword [var_20h], 0
0x140034456 mov qword [var_28h], 0
0x14003445e lea rax, data.140048f80 ; 0x140048f80
0x140034465 mov rcx, rax ; int64_t arg1
0x140034468 call decodeBase64__smartmeter95management95interface_89 ; sym.decodeBase64__smartmeter95management95interface_89
0x14003446d mov qword [var_28h], rax
0x140034471 mov qword [var_30h], 0
0x140034479 lea rax, data.140048fc0 ; 0x140048fc0
0x140034480 mov rcx, rax ; int64_t arg1
0x140034483 call decodeBase64__smartmeter95management95interface_89 ; sym.decodeBase64__smartmeter95management95interface_89
0x140034488 mov qword [var_30h], rax
0x14003448c cmp qword [var_28h], 0
0x140034491 je 0x14003449c
I don’t know the exact calling convention at play here, but if I had to guess, those data.1400...
addresses are being passed into the decodeBase64()
function. Following those, we get two base64 strings: Y2VhZTA5YzM5YTRiMjczOQ==
and ZDVlMDMwODBmNDI2YzkyMA==
. We can decode them to get the values ceae09c39a4b2739
and d5e03080f426c920
, respectively. Concatenating these and decoding as hex gives random bytes, so we’re still not entirely sure how this gets used. Later on in this function, however, we see the following:
0x14003457c call readLine__systemZio_364 ; sym.readLine__systemZio_364
0x140034581 mov qword [var_48h], rax
0x140034585 mov qword [var_78h], 0x86 ; 134
0x14003458d lea rax, str.C:_Cyberforce_November_2023_smartmeter_management_interface.nim ; 0x14004ab90
0x140034594 mov qword [var_70h], rax
0x140034598 mov rax, qword [var_48h]
0x14003459c mov rcx, rax ; int64_t arg1
0x14003459f call getMD5__OOZ85sersZmurraZOnimbleZpkgs50Zchecksums4548O49O48455352525551dcb50db5649cc5154fc54ac5154ab5453df48e4957525251534948Zch ; sym.getMD5__OOZ85sersZmurraZOnimbleZpkgs50Zchecksums4548O49O48455352525551dcb50db5649cc5154fc54ac5154ab5453df48e4957525251534948Zch
0x1400345a4 mov qword [var_50h], rax
0x1400345a8 mov qword [var_78h], 0x87 ; 135
0x1400345b0 lea rax, str.C:_Cyberforce_November_2023_smartmeter_management_interface.nim ; 0x14004ab90
0x1400345b7 mov qword [var_70h], rax
0x1400345bb mov rdx, qword [var_38h] ; int64_t arg2
0x1400345bf mov rax, qword [var_50h]
0x1400345c3 mov rcx, rax ; int64_t arg1
0x1400345c6 call eqStrings ; sym.eqStrings_0x14003428e
That getMD5
suggests that the user’s input is actually being compared via hashing, so one might guess that the base64 strings encode the hash. We can try both ways, and using Crackstation, we eventually find that ceae09c39a4b2739d5e03080f426c920
corresponds to neverhere
.
We can try this in the terminal and see that it works.
PS C:\Users\sreisz\Desktop\wattsup> .\smartmeter.exe
------------------------------------------------------------
DER8.9 SmartMeter Workstation for Administration of Telemetric Technologies (WATT)
------------------------------------------------------------
------------------------------------------------------------
Control and Maintenance Interface
------------------------------------------------------------
@ @
@ @
@ @/ .@
# @
@@* @ @
@ @
@@
@&
@
Welcome to DER8.9s SmartMeter WATT Maintenance Interface.
Please enter the maintenance token and select an option from the menu.
Enter maintenance token:
neverhere
Accepted Maintenance Token... Access Granted!
DER8.9 SmartMeter WATT Control and Maintenance Interface
------------------------------------------------------------
MENU OPTIONS
------------------------------------------------------------
1. Display current reading
2. Display historical readings
3. Add Configuration
4. Delete Configuration
5. Update Configuration
6. Display Configuration
7. Process Configuration
8. Access meter logs
9. Display meter firmware version
10. Export Configurations as JSON
11. Import Configurations from JSON
12. Restore Configurations
13. Exit
Enter your choice:
flag: neverhere
Solution 2: Data Deletion
Finding the other flags is a little bit of a challenge, as we don’t really have direction for what to do other than look for vulnerabilities. However, we can do a little bit of metagaming and look for interesting strings, and we find the following.
Since I am privy to the order of the flags, we’ll start with the “insecure data deletion”. First, let’s try to look at this in the program. We have a few options related to configuration management:
- (3) Add Configuration
- (4) Delete Configuration
- (5) Update Configuration
- (6) Display Configuration
- (7) Process Configuration
- (12) Restore Configurations
If we display configurations, we see what’s already loaded.
------------------------------------------------------------
CONFIGURATION DISPLAY
------------------------------------------------------------
ID: 999 Data: 5,7,0,2,24,7,14,16,5,14,16,19,2,23,6,8,19
DER8.9 SmartMeter WATT Control and Maintenance Interface
Let’s try to delete it and see what happens.
Enter your choice:
4
------------------------------------------------------------
DELETE CONFIGURATION
------------------------------------------------------------
Enter maintenance token:
neverhere
Enter configuration ID to delete:
999
DER8.9 SmartMeter WATT Control and Maintenance Interface
------------------------------------------------------------
MENU OPTIONS
------------------------------------------------------------
[...trim...]
Enter your choice:
6
------------------------------------------------------------
CONFIGURATION DISPLAY
------------------------------------------------------------
ID: 999 Data: DELETED
DER8.9 SmartMeter WATT Control and Maintenance Interface
Well that was easy. Based on the string we found, let’s try to restore it.
Enter your choice:
12
------------------------------------------------------------
RESTORE CONFIGURATION
------------------------------------------------------------
Enter maintenance token:
neverhere
Configurations restored!
DER8.9 SmartMeter WATT Control and Maintenance Interface
------------------------------------------------------------
MENU OPTIONS
------------------------------------------------------------
[...trim...]
Enter your choice:
6
------------------------------------------------------------
CONFIGURATION DISPLAY
------------------------------------------------------------
ID: 999 Data: RESTORED DELETED DATA || INSECURE DATA DELETION FLAG #2: DONT-LOOK-BACK-AT-DELETED-DATA-I-HEARD-YOU-SAY
DER8.9 SmartMeter WATT Control and Maintenance Interface
Well that was even easier. Moral of the story, when data gets deleted, make sure it actually gets deleted and wiped from memory. We could dig into why this is happening by looking at the assembly, but this post is long enough as is and we have two more to get through. I might make a follow up post later to dive even deeper, but I’ll be honest, I’m too tired to dig into this right now.
flag: DONT-LOOK-BACK-AT-DELETED-DATA-I-HEARD-YOU-SAY
Solution 3: JSON Deserialization
Another one of the strings had to do with deserialization, and we have two options that would be related to this.
- (10) Export Configurations as JSON
- (11) Import Configurations as JSON
If we try to call (10), we get this:
Enter your choice:
10
------------------------------------------------------------
EXPORT CONFIGURATION
------------------------------------------------------------
[{"id":999,"data":"5,7,0,2,24,7,14,16,5,14,16,19,2,23,6,8,19"}]
DER8.9 SmartMeter WATT Control and Maintenance Interface
It seems like we also have the option to submit our own data. I can submit a made up configuration and it seems like it goes through no problem.
Enter your choice:
11
Provide JSON data for configurations:
[{"id":123,"data":"1,1,1,1,1"}]
[...trim...]
Enter your choice:
10
------------------------------------------------------------
EXPORT CONFIGURATION
------------------------------------------------------------
[{"id":999,"data":"5,7,0,2,24,7,14,16,5,14,16,19,2,23,6,8,19"},{"id":123,"data":"1,1,1,1,1"}]
Looks like we can’t submit arbitrary keys and values though:
Enter your choice:
11
Provide JSON data for configurations:
[{"fakeKey":"fakeValue"}]
------------------------------------------------------------
IMPORT CONFIGURATION
------------------------------------------------------------
C:\Cyberforce-November-2023\smartmeter_management_interface.nim(341) smartmeter_management_interface
C:\Cyberforce-November-2023\smartmeter_management_interface.nim(331) main
C:\Cyberforce-November-2023\smartmeter_management_interface.nim(261) importConfigurations
C:\msys64\mingw64\lib\nim\pure\json.nim(517) []
C:\msys64\mingw64\lib\nim\pure\collections\tables.nim(246) []
C:\msys64\mingw64\lib\nim\pure\collections\tables.nim(234) raiseKeyError
Error: unhandled exception: key not found: id [KeyError]
If that’s the case, it seems like we need to find what the possible keys are. We have a couple of functions to look at as far as the Nim code goes: exportConfigurations()
, importConfigurations()
, processConfiguration()
, updateConfiguration()
. After a long winded journey of exploring the various functions, first updateConfiguration()
then importConfigurations()
, we find the following assembly.
0x140037332 lea rdx, data.14004a2a0 ; 0x14004a2a0 ; int64_t arg2
0x140037339 mov rcx, rax ; int64_t arg1
0x14003733c call hasKey__pureZjson_3212 ; sym.hasKey__pureZjson_3212
0x140037341 mov byte [var_29h], al
0x140037344 movzx eax, byte [var_29h]
0x140037348 xor eax, 1
0x14003734b test al, al
0x14003734d jne 0x140037384
0x14003734f mov qword [var_78h], 0
0x140037357 mov rax, qword [var_48h]
0x14003735b lea rdx, data.14004a2a0 ; 0x14004a2a0 ; int64_t arg2
0x140037362 mov rcx, rax ; int64_t arg1
0x140037365 call X5BX5D___pureZjson_3095 ; sym.X5BX5D___pureZjson_3095
0x14003736a mov qword [var_78h], rax
0x14003736e mov rax, qword [var_78h]
0x140037372 mov edx, 0 ; int64_t arg2
0x140037377 mov rcx, rax ; int64_t arg1
0x14003737a call getBool__pureZjson_189 ; sym.getBool__pureZjson_189
0x14003737f mov byte [var_29h], al
0x140037382 jmp 0x140037385
0x140037384 nop
0x140037385 movzx eax, byte [var_29h]
That hasKey__pureZjson_3212
is particularly interesting, considering there’s something going on with data.14004a2a0
before it. If I follow that variable, the nearest string in Cutter is isAdmin
, which absolutely looks like a key. We can also see a later call to getBool
, which may imply the data type to go with the isAdmin
key is a boolean. All together, we can try injecting some JSON.
Enter your choice:
11
Provide JSON data for configurations:
[{"isAdmin":true}]
------------------------------------------------------------
IMPORT CONFIGURATION
------------------------------------------------------------
ADMIN ACCESS GRANTED || INSECURE DESERIALIZATION FLAG #4: WOO-HOO-AND-IM-INSECURE-WITH-DESERIALIZATION
DER8.9 SmartMeter WATT Control and Maintenance Interface
Despite the fact that we’re not using something like ysoserial to get RCE, this is still deserialization! The JSON gets loaded into the program as some kind of dictionary structure, which is how it’s checking for keys. Since there’s no checks on what we can submit, that malicious config gets evaluated and injects the admin condition.
flag: WOO-HOO-AND-IM-INSECURE-WITH-DESERIALIZATION
Solution 4: IDOR
Out last challenge has to do with an insecure direct object reference. We can use the success string to find exactly the code block we want to get to. One thing I learned while solving this is that you can’t directly check for X-Refs from the string, you want to scroll up (at least in Cutter) for the data.XXXXXXX
reference and use that.
We’re inside the processConfiguration()
function, and the control flow graph is a little bit more complicated than we might want to look at statically. Let’s take a look at what “processing” a configuration does.
Enter your choice:
7
------------------------------------------------------------
PROCESS CONFIGURATIONS
------------------------------------------------------------
Processed Configuration (Not a flag... sadly): USZXBSLJULJGXCTRG
(Hint: do you think the developers took out any backdoors for debug access?)
DER8.9 SmartMeter WATT Control and Maintenance Interface
While attempting to solve this one, I actually ended up reverse engineering the entire algorithm to go from configuration to processed string. It didn’t help at all, but you can see from when we looked at the default configuration, the indices are all less than 26, and the resulting string is entirely alphabetic. If you look at the strings, you find the string ZYXWVUTSRQPONMLKJIHGFEDCBA-123456#
, which looks like a lookup array. For instance, the first number in the default config is 5, and U has index 5 (we’re starting from Z = 0, sorry Lua devs).
Although figuring this out was fun, the solution is actually way simpler than that. Knowing what our end goal is, we can trace stuff back through the CFG to find what conditions are necessary to get to our end goal. At one point, we find this block:
0x140035ee1 mov rax, qword [var_50h]
0x140035ee8 mov rax, qword [rax + 8]
0x140035eec lea rdx, data.140048a80 ; 0x140048a80 ; int64_t arg2
0x140035ef3 mov rcx, rax ; int64_t arg1
0x140035ef6 call eqStrings ; sym.eqStrings_0x14003428e
0x140035efb mov byte [var_11h], al
0x140035f01 jmp 0x140035f04
The data in data.140048a80
is @5,7,0,2,24,7,14,16,5,14,16,19,2,23,6,8,19
, which is what the default config was. Following this is a call to eqStrings
which probably does what it says it does, checks if two strings are equal. If we try submitting a new config like this, it doesn’t look like anything changes.
Enter your choice:
3
------------------------------------------------------------
ADD CONFIGURATION
------------------------------------------------------------
Enter maintenance token:
neverhere
Enter configuration ID:
1337
Enter configuration data:
5,7,0,2,24,7,14,16,5,14,16,19,2,23,6,8,19
DER8.9 SmartMeter WATT Control and Maintenance Interface
------------------------------------------------------------
MENU OPTIONS
------------------------------------------------------------
[...trim...]
Enter your choice:
7
------------------------------------------------------------
PROCESS CONFIGURATIONS
------------------------------------------------------------
Processed Configuration (Not a flag... sadly): USZXBSLJULJGXCTRG
(Hint: do you think the developers took out any backdoors for debug access?)
Processed Configuration (Not a flag... sadly): USZXBSLJULJGXCTRG
(Hint: do you think the developers took out any backdoors for debug access?)
DER8.9 SmartMeter WATT Control and Maintenance Interface
There might be an additional check going on. In the block immediately before the eqStrings
, we see another interesting comparison.
0x140035eaf mov qword [var_130h], rax
0x140035eb3 mov byte [var_11h], 0
0x140035eba mov rax, qword [var_50h]
0x140035ec1 mov rax, qword [rax]
0x140035ec4 cmp rax, 0x7c9 ; 1993
0x140035eca sete al
0x140035ecd mov byte [var_11h], al
0x140035ed3 movzx eax, byte [var_11h]
0x140035eda xor eax, 1
0x140035edd test al, al
0x140035edf jne 0x140035f03
Although this is Nim and there’s a bunch of random noise that is happening, the comparison at 0x140035ec4
is such an oddly specific number. We could probably go backwards to confirm that this is a desired configuration ID, but there’s no harm in trying. I’ll restart the program and try again.
Enter your choice:
3
------------------------------------------------------------
ADD CONFIGURATION
------------------------------------------------------------
Enter maintenance token:
neverhere
Enter configuration ID:
1993
Enter configuration data:
5,7,0,2,24,7,14,16,5,14,16,19,2,23,6,8,19
DER8.9 SmartMeter WATT Control and Maintenance Interface
------------------------------------------------------------
MENU OPTIONS
------------------------------------------------------------
[...trim...]
Enter your choice:
7
------------------------------------------------------------
PROCESS CONFIGURATIONS
------------------------------------------------------------
Processed Configuration (Not a flag... sadly): USZXBSLJULJGXCTRG
(Hint: do you think the developers took out any backdoors for debug access?)
DEVELOPER DEBUG MODE ACTIVATED || Processed Configuration! INSECURE DIRECT OBJECT REFERENCE FLAG #3: INSECURE-REFS-LIKE-INSECURE-PROGRAMS-DO
DER8.9 SmartMeter WATT Control and Maintenance Interface
And that’s the flag! In most examples of IDOR, it’s usually about accessing information you shouldn’t have access to by changing a parameter, usually an index. While this might not be that, I would still call it an IDOR in the sense that no normal user should just be able to call the debug mode by calling a specific ID (after all, we did find an admin attribute). It’s mostly a problem of hard coded keys, but I don’t think calling this IDOR is extremely wrong.
flag: INSECURE-REFS-LIKE-INSECURE-PROGRAMS-DO
Conclusion
This year’s Cyberforce main event had way more difficult challenges than previous years, and while some of them were extremely stupid (looking at you Ste-what-graphy), the progress the anomaly team has made over the years has been great. I still remember back in 2020 and 2021 where you could cheese half of the reversing and steg challenges by doing strings binary | grep flag
. I wish I was able to solve these during the event, but got extremely bogged down in incident response, but I appreciate the work nonetheless.
If you’re interested in doing some of these challenges yourself (and are a US collegiate student), Cyberforce has some more events coming up that might be worth checking out. I’m not allowed back after (allegedly) stealing the agenda but that’s besides the point :p
Until next time! :D