flare24-catbert-cover

Catbert Ransomware presents a disk image with four encrypted files, and UEFI bios. I’ll run the bios in an emulator, and find the binary responsible for the shell. In there, I’ll find the decrypt function and reverse it to see how it is using code at the end of the encrypted images in a small VM to check the input password. I’ll write Python VM emulator to work through the code finding the passwords. On decrypting all three, there’s some fun in the emulated bios and the flag.

Challenge

The challenge prompt reads:

A dire situation has arisen, yet again, at FLARE HQ. One of our employees has fallen victim to a ransomware attack, and the culprit is none other than the notorious Catbert, our malevolent HR manager. Catbert has encrypted the employee’s disk, leaving behind a cryptic message and a seemingly innocent decryption utility. However, this utility is not as straightforward as it seems. It’s deeply embedded within the firmware and requires a keen eye and technical prowess to extract. The employee’s most valuable files are at stake, and time is of the essence. Please, help us out one…. last… time.

The download contains a bios.bin and a disk.img file:

oxdf@hacky$ 7z l CatbertRansomware.7z 
...[snip]...
   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2024-08-16 02:16:40 ....A      4194304      1692624  bios.bin
2024-08-16 02:30:24 ....A      4194304               disk.img
------------------- ----- ------------ ------------  ------------------------
2024-08-16 02:30:24            8388608      1692624  2 files
oxdf@hacky$ file bios.bin 
bios.bin: data

disk.img

The disk.img file is a FAT disk image:

oxdf@hacky$ file disk.img 
disk.img: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "MTOO4043", sectors/cluster 2, root entries 512, sectors 8192 (volumes <=32 MB), Media descriptor 0xf8, sectors/FAT 12, sectors/track 63, heads 16, serial number 0x538247ff, unlabeled, FAT (12 bit)

I can mount it on my system, and see four files inside:

oxdf@hacky$ sudo mount disk.img /mnt
oxdf@hacky$ ls /mnt/
catmeme1.jpg.c4tb  catmeme2.jpg.c4tb  catmeme3.jpg.c4tb  DilbootApp.efi.enc
oxdf@hacky$ file /mnt/*
/mnt/catmeme1.jpg.c4tb:  data
/mnt/catmeme2.jpg.c4tb:  data
/mnt/catmeme3.jpg.c4tb:  data
/mnt/DilbootApp.efi.enc: data

Given the theme of ransomware, it seems obvious that there are three encrypted .jpg images, as well as an encrypted .efi image.

Run It

Identify UEFI

bios.bin is a UEFI bios image. One way to see this is with strings:

oxdf@hacky$ strings bios.bin | grep EFI
((UINT32) ( (((EFI_COMMON_SECTION_HEADER *) (UINTN) (FvSection))->Size[0] ) | (((EFI_COMMON_SECTION_HEADER *) (UINTN) (FvSection))->Size[1] << 8) | (((EFI_COMMON_SECTION_HEADER *) (UINTN) (FvSection))->Size[2] << 16))) == (_gPcd_FixedAtBuild_PcdOvmfPeiMemFvSize + sizeof (*FvSection))
ASSERT_EFI_ERROR (Status = %r)

binwalk will also show this:

oxdf@hacky$ binwalk bios.bin 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             UEFI PI Firmware Volume, volume size: 540672, header size: 0, revision: 0, Variable Storage, GUID: FFF12B8D-7696-4C8B-85A9-2747075B4F50
540672        0x84000         UEFI PI Firmware Volume, volume size: 3440640, header size: 96, revision: 0, EFI Firmware File System v2, GUID: 8C8CE578-8A3D-4F1C-3599-896185C32DD3
540840        0x840A8         LZMA compressed data, properties: 0x5D, dictionary size: 16777216 bytes, uncompressed size: 16122000 bytes
3981312       0x3CC000        UEFI PI Firmware Volume, volume size: 212992, header size: 96, revision: 0, EFI Firmware File System v2, GUID: 8C8CE578-8A3D-4F1C-3599-896185C32DD3
3981460       0x3CC094        Microsoft executable, portable (PE)

Emulation

Initial

qemu-system-x86_64 will emulate this bios to run it with qemu-system-x86_64 -bios bios.bin. This drops me into a window with a ransom note and a terminal:

image-20241108170318249

Mount

In the info message it mentions typing fs0: to access mounted disks. Trying to do that now just errors:

image-20241108170356623

I’ll exit, and run qemu again with -drive format=raw,file=disk.img. This mounts the disk.img file as fs0:

image-20241108170433178

Shell Commands

The help menu shows a couple pages of shell commands, but there’s one that jumps out as particularly interesting:

image-20241108170505452

I can try it on one of the three images:

image-20241108170538319

Debug

I was able to get gdb to attach to the running bios image using the instructions from this site. I’ll add some more options to my qemu invocation:

qemu-system-x86_64 -bios bios.bin -drive format=raw,file=disk.img -serial tcp::6666,server -s -debugcon file:debug.log -global isa-debugcon.iobase=0x402

This starts it with gdb listening on 1234 and serial listening on 6666. The serial makes it so that it doesn’t start until I connect, which is nice if I want to attach gdb at the start. The other options set up the debug.

Then I’ll run gdb on the file I want to debug (coming next section) and do target remote :1234 . In the debug log, it’ll have the addresses of loaded efi modules:

$ grep -i shell debug.log 
Loading driver at 0x00006A4A000 EntryPoint=0x00006A4A688 LinuxInitrdDynamicShellCommand.efi
    Boot0002: EFI Internal Shell                 0x0001
[Bds]Booting EFI Internal Shell
Loading driver at 0x00005C9C000 EntryPoint=0x00005C9C5C8 Shell.efi

I can use that to match up with what I see in Ghidra for that module as the entry:

img

To break, I’ll just do:

gdb-peda$ b *(0x00005C9C000+0x312ca)
Breakpoint 6 at 0x5ccd2ca 

I didn’t end up using this very much, as it was a bit of a pain to get started each time I restarted the emulation, and it wasn’t really necessary. But it was possible.

RE

Get Shell Binary

Extract Files

The best way I know of to extract a UEFI bios image is with 7z:

oxdf@hacky$ 7z x bios.bin -obios_extracted           
...[snip]...
oxdf@hacky$ ls bios_extracted/
9E21FD93  FFFFFFFF
oxdf@hacky$ ls bios_extracted/9E21FD93/
EE4E5898
oxdf@hacky$ ls bios_extracted/9E21FD93/EE4E5898/
0.raw  1.raw  VOLUME
oxdf@hacky$ ls bios_extracted/9E21FD93/EE4E5898/VOLUME/
1B45CC0A.raw                      EbcDxe.efi                           Mtftp4Dxe.efi                         RuntimeDxe.efi                   UiApp
AcpiTableDxe.efi                  EhciDxe.efi                          NvmExpressDxe.efi                     S3Resume2Pei                     UsbBusDxe.efi
AmdSevDxe.efi                     EmuVariableFvbRuntimeDxe.efi         PartitionDxe.efi                      S3SaveStateDxe.efi               UsbKbDxe.efi
ArpDxe.efi                        EnglishDxe.efi                       PcdDxe                                SataController.efi               UsbMassStorageDxe.efi
AtaAtapiPassThruDxe.efi           Fat.efi                              PcdPeim                               ScsiBus.efi                      VariablePolicyDynamicCommand.efi
AtaBusDxe.efi                     FaultTolerantWriteDxe.efi            PciBusDxe.efi                         ScsiDisk.efi                     VariableRuntimeDxe.efi
BdsDxe.efi                        FC510EE7.raw                         PciHostBridgeDxe.efi                  SecurityStubDxe.efi              VirtHstiDxe.efi
BootGraphicsResourceTableDxe.efi  FFFFFFFF                             PciHotPlugInitDxe.efi                 SetupBrowser.efi                 Virtio10.efi
BootScriptExecutorDxe.efi         FvbServicesRuntimeDxe.efi            PciSioSerialDxe.efi                   Shell                            VirtioBlkDxe.efi
CapsuleRuntimeDxe.efi             GraphicsConsoleDxe.efi               PcRtc.efi                             SioBusDxe.efi                    VirtioFsDxe.efi
ConPlatformDxe.efi                Hash2DxeCrypto.efi                   PeiCore                               SmbiosDxe.efi                    VirtioGpuDxe.efi
ConSplitterDxe.efi                HiiDatabase.efi                      PlatformDxe                           SmbiosPlatformDxe.efi            VirtioNetDxe.efi
CpuDxe.efi                        httpDynamicCommand.efi               PlatformPei                           SnpDxe.efi                       VirtioPciDeviceDxe.efi
CpuIo2Dxe.efi                     IncompatiblePciDeviceSupportDxe.efi  Ps2KeyboardDxe.efi                    StatusCodeHandlerPei             VirtioRngDxe.efi
CpuMpPei                          IoMmuDxe.efi                         QemuFwCfgAcpiPlatform.efi             StatusCodeHandlerRuntimeDxe.efi  VirtioScsiDxe.efi
DevicePathDxe.efi                 Ip4Dxe                               QemuKernelLoaderFsDxe.efi             TcgMor.efi                       VirtioSerialDxe.efi
Dhcp4Dxe.efi                      IScsiDxe                             QemuRamfbDxe.efi                      TcpDxe.efi                       VlanConfigDxe
DiskIoDxe.efi                     LinuxInitrdDynamicShellCommand.efi   QemuVideoDxe.efi                      TerminalDxe.efi                  WatchdogTimer.efi
DisplayEngine                     LocalApicTimerDxe.efi                RamDiskDxe                            tftpDynamicCommand.efi           XhciDxe.efi
DpcDxe.efi                        LogoDxe.efi                          ReportStatusCodeRouterPei             UdfDxe.efi
DriverHealthManagerDxe            Metronome.efi                        ReportStatusCodeRouterRuntimeDxe.efi  Udp4Dxe.efi
DxeCore.efi                       MnpDxe.efi                           ResetSystemRuntimeDxe.efi             UefiPxeBcDxe.efi
DxeIpl.efi                        MonotonicCounterRuntimeDxe.efi       RngDxe.efi                            UhciDxe.efi

That dumps out all the .efi binaries that compose different functionality of the bios. Each is actually a Windows executable:

oxdf@hacky$ file bios_extracted/9E21FD93/EE4E5898/VOLUME/Dhcp4Dxe.efi 
bios_extracted/9E21FD93/EE4E5898/VOLUME/Dhcp4Dxe.efi: PE32+ executable (DLL) (EFI boot service driver) x86-64, for MS Windows, 6 sections

Find Shell

I want to find the binary that runs when the it starts. When I try to decrypt a file and it fails, it printed a cat in ASCII art with the string “Nope.”. Unfortunately, grep -r "Nope." bios_extracted/ doesn’t return anything.

It turns out the string is there, but as a unicode string. grep doesn’t handle these well. I’ll try using find to get all the files and run strings -el on each. Then I’ll pipe the results into grep to look for that string. It works:

oxdf@hacky$ find bios_extracted/ -type f -exec strings -el {} \; | grep -F "Nope."
     Nope.

But that doesn’t tell me what name of the file is. I’ll change it to list all the files and read them into a loop, where I’ll check for the string and print the filename if it’s there:

oxdf@hacky$ find bios_extracted/ -type f | while read f; do strings -el "$f" | grep -q "Nope." && echo "$f"; done
bios_extracted/9E21FD93/EE4E5898/VOLUME/Shell/0.efi

Ghidra

Finding Decrypt Calls

I’ll open the file in Ghidra. Searching for the the “Nope.” string finds a function I’ve named decrypt_fail_nope:

image-20241108172439917

This is called from FUN_00031bc4. In this function, there are various checks. For example, if the file isn’t found:

image-20241108172535214

Or if I try to decrypt a file that isn’t one of the three images:

image-20241108172613677

A bit later, there’s a check on the length of the input key, and if it isn’t 0x10, then it calls decrypt_fail_nope:

image-20241108172954154

Just after that, there’s a bunch of data set, and then a call to a function I’ve named vm_exec:

image-20241108175118830

After the vm_exec call, if this global I’ve named success isn’t 0, it jumps back to call decrypt_fail_nope and return.

This is a very good indication that the key needs to be 0x10 bytes, and then it is checked in vm_exec, and needs to set success to 0 to decrypt.

VM Overview

The function I’ve named vm_exec initializes a stack pointer and instruction pointer (I’ve named gip), and then loads the 1 byte op code, increments gip:

image-20241108175236356

Then it drops into a huge table of if statements based on the opcode.

Images Structure

Overall Structure

The encrypted images have the following structure:

0x00: MAGIC [4 bytes]
0x04: Size of enc image [4 bytes]
0x08: Offset to VM bytecode [4 bytes]
0x0C: Size of VM Bytecode [4 bytes]
0x10: Encrypted Image
????: VM Bytecode

For example, for catmeme1.jpg.c4tb:

image-20241108174031622

“C4TB” is the magic. The size of the encrypted image is 0x00011554. Adding that to the 0x10 bytes of header, it ends at 0x1164:

image-20241108174429867

The offset to the VM code is 0x00011570, which starts just after the image with some nulls to align it:

image-20241108174550045

The length is 0x259, which lines up perfectly with the end of the file, as 0x11570 + 0x259 = 0x117c9.

Overwrites

I’ll note that the overwrites in Ghidra into this data line up nicely with where I see values like aa, bb, cc, etc in the VM code. In fact, if I pass in 16 “A” for the key, these show up when debugging:

image-20241108174911548

The key bytes are put into the VM code in the following order:

key_offsets = [5, 4, 0xc, 0xb, 0x13, 0x12, 0x1a, 0x19, 0x21, 0x20, 0x28, 0x27, 0x2f, 0x2e, 0x36, 0x35]

VM Emulation

Development Process

For me, the easiest way to approach this problem is to write a Python script to emulate the VM and then print out the commands that are run. I’ll start the script with a recognizable password, opening the encrypted file, and initializing the VM:

key = b"0123456789ABCDEF"
key_offsets = [5, 4, 0xc, 0xb, 0x13, 0x12, 0x1a, 0x19, 0x21, 0x20, 0x28, 0x27, 0x2f, 0x2e, 0x36, 0x35]

with open('catmeme1.jpg.c4tb', 'rb') as f:
    binary = f.read()

assert binary[:4] == b"C4TB"
enc_image_size = int.from_bytes(binary[4:8], 'little')
bytecode_offset = int.from_bytes(binary[8:12], 'little')
bytecode_size = int.from_bytes(binary[12:16], 'little')
bc = bytearray(binary[bytecode_offset:bytecode_offset+bytecode_size])
for i, c in zip(key_offsets, key):
    bc[i] = c
bc = bytes(bc)

i = 0
stack = []
mem = [None] * 256

Next I’ll start a loop over the code:

while i < len(bc):
    print(f"[{i:04x}] ", end="")
    match bc[i]:
        case 1:
while i < len(bc):
    print(f"[{i:04x}] ", end="")
    match bc[i]:
        case _:
            raise ValueError(f"Unknown opcode {bc[i]:x}")

By having it raise an error whenever it hits an unknown opcode, now I know what to go look at. The first failure is opcode 1, which matches what I observed at the start:

oxdf@hacky$ python runvm.py 
[0000] Traceback (most recent call last):
  File "/media/sf_CTFs/flareon-2024/10-catbertransomware/runvm.py", line 132, in <module>
    raise ValueError(f"Unknown opcode {bc[i]:x}")
ValueError: Unknown opcode 1

Now I can go into the vm_exec function and find that code:

image-20241108175841112

This code pushed two bytes from the VM code onto the stack, and increments gip to three bytes after the opcode. So I can add that to my emulator:

        case 1:
            stack.append(bc[i+2] + (bc[i+1] << 8))
            print(f"1: push 0x{bc[i+1]:x}{bc[i+2]:x}")
            i += 3

Now it gets stuck on 6:

oxdf@hacky$ python runvm.py 
[0000] 1: push 0x00
[0003] 1: push 0x3130
[0006] Traceback (most recent call last):
  File "/media/sf_CTFs/flareon-2024/10-catbertransomware/runvm.py", line 132, in <module>
    raise ValueError(f"Unknown opcode {bc[i]:x}")
ValueError: Unknown opcode 6

That code is here:

image-20241108180317029

This is popping two values from the stack. The first is the value, and the second is the index of memory to story that value. In Python:

        case 6:
            val = stack.pop()
            idx = stack.pop()
            mem[idx] = val
            i += 1
            print(f"6: mem[{idx}] = 0x{val:x}")

I’ll keep working this way until I define:

  • 0x1: Push two bytes static values
  • 0x5: Pop an index, and push the value from that index in memory to the stack
  • 0x6: Pop a value and an index, storing the value at that index in memory.
  • 0x9: Pop two values, push the sum
  • 0xd: Pop two values, push the product
  • 0xe: Jump to a offset defined in the next two values
  • 0x10: Pop a value, and jump if it’s 0
  • 0x11: Pop two values, and push 1 if they are equal or 0 otherwise
  • 0x12: Pop two values, and push 1 if the first is greater than the second, otherwise 0
  • 0x14: Pop two values, and push 1 if the first is less than the second, otherwise 0
  • 0x18: Exit
  • 0x19: Pop value into success
  • 0x1a: Pop two values, xor them, and push the result
  • 0x1b: Pop two values, or them, and push the result
  • 0x1c: Pop two values, and them, and push the result
  • 0x1d: Pop two values, and push the second mod the first
  • 0x1e: Pop two values, push the second one left shifted by the first bits
  • 0x1f: Pop two values, push the second one right shifted by the first bits
  • 0x21: Pop two values, rotate the second right by the first bits (works on four bytes)
  • 0x24: Pop two values, rotate the second left by the first bits (works on one byte)
  • 0x25: Pop two values, rotate the second right by the first bits (works on one byte)

catmeme1.jpg.c4tb

Running this produces 244 lines of output, which is long, but not too long to work through:

oxdf@hacky$ python runvm.py
[0000] 1: push 0x00
[0003] 1: push 0x3130
[0006] 6: mem[0] = 0x3130
[0007] 1: push 0x01
[000a] 1: push 0x3332
[000d] 6: mem[1] = 0x3332
[000e] 1: push 0x02
[0011] 1: push 0x3534
[0014] 6: mem[2] = 0x3534
[0015] 1: push 0x03
[0018] 1: push 0x3736
[001b] 6: mem[3] = 0x3736
[001c] 1: push 0x04
[001f] 1: push 0x3938
[0022] 6: mem[4] = 0x3938
[0023] 1: push 0x05
[0026] 1: push 0x4241
[0029] 6: mem[5] = 0x4241
[002a] 1: push 0x06
[002d] 1: push 0x4443
[0030] 6: mem[6] = 0x4443
[0031] 1: push 0x07
[0034] 1: push 0x4645
[0037] 6: mem[7] = 0x4645
[0038] 1: push 0x0a
[003b] 1: push 0x6144
[003e] 6: mem[10] = 0x6144
[003f] 1: push 0x0b
[0042] 1: push 0x7534
[0045] 6: mem[11] = 0x7534
[0046] 1: push 0x0c
[0049] 1: push 0x6962
[004c] 6: mem[12] = 0x6962
[004d] 1: push 0x0d
[0050] 1: push 0x6c63
[0053] 6: mem[13] = 0x6c63
[0054] 1: push 0x0e
[0057] 1: push 0x3165
[005a] 6: mem[14] = 0x3165
[005b] 1: push 0x0f
[005e] 1: push 0x6669
[0061] 6: mem[15] = 0x6669
[0062] 1: push 0x010
[0065] 1: push 0x6265
[0068] 6: mem[16] = 0x6265
[0069] 1: push 0x011
[006c] 1: push 0x6230
[006f] 6: mem[17] = 0x6230
[0070] 1: push 0x08
[0073] 1: push 0x03
[0076] 5: push mem[3] (0x3736)
[0077] 1: push 0x030
[007a] 0x1e: push 0x3736 << 0x30 = 0x3736000000000000
[007b] 1: push 0x02
[007e] 5: push mem[2] (0x3534)
[007f] 1: push 0x020
[0082] 0x1e: push 0x3534 << 0x20 = 0x353400000000
[0083] 0x1b: push 0x353400000000 | 0x3736000000000000 = 0x3736353400000000
[0084] 1: push 0x01
[0087] 5: push mem[1] (0x3332)
[0088] 1: push 0x010
[008b] 0x1e: push 0x3332 << 0x10 = 0x33320000
[008c] 0x1b: push 0x33320000 | 0x3736353400000000 = 0x3736353433320000
[008d] 1: push 0x00
[0090] 5: push mem[0] (0x3130)
[0091] 0x1b: push 0x3130 | 0x3736353433320000 = 0x3736353433323130
[0092] 6: mem[8] = 0x3736353433323130
[0093] 1: push 0x09
[0096] 1: push 0x07
[0099] 5: push mem[7] (0x4645)
[009a] 1: push 0x030
[009d] 0x1e: push 0x4645 << 0x30 = 0x4645000000000000
[009e] 1: push 0x06
[00a1] 5: push mem[6] (0x4443)
[00a2] 1: push 0x020
[00a5] 0x1e: push 0x4443 << 0x20 = 0x444300000000
[00a6] 0x1b: push 0x444300000000 | 0x4645000000000000 = 0x4645444300000000
[00a7] 1: push 0x05
[00aa] 5: push mem[5] (0x4241)
[00ab] 1: push 0x010
[00ae] 0x1e: push 0x4241 << 0x10 = 0x42410000
[00af] 0x1b: push 0x42410000 | 0x4645444300000000 = 0x4645444342410000
[00b0] 1: push 0x04
[00b3] 5: push mem[4] (0x3938)
[00b4] 0x1b: push 0x3938 | 0x4645444342410000 = 0x4645444342413938
[00b5] 6: mem[9] = 0x4645444342413938
[00b6] 1: push 0x012
[00b9] 1: push 0x0d
[00bc] 5: push mem[13] (0x6c63)
[00bd] 1: push 0x030
[00c0] 0x1e: push 0x6c63 << 0x30 = 0x6c63000000000000
[00c1] 1: push 0x0c
[00c4] 5: push mem[12] (0x6962)
[00c5] 1: push 0x020
[00c8] 0x1e: push 0x6962 << 0x20 = 0x696200000000
[00c9] 0x1b: push 0x696200000000 | 0x6c63000000000000 = 0x6c63696200000000
[00ca] 1: push 0x0b
[00cd] 5: push mem[11] (0x7534)
[00ce] 1: push 0x010
[00d1] 0x1e: push 0x7534 << 0x10 = 0x75340000
[00d2] 0x1b: push 0x75340000 | 0x6c63696200000000 = 0x6c63696275340000
[00d3] 1: push 0x0a
[00d6] 5: push mem[10] (0x6144)
[00d7] 0x1b: push 0x6144 | 0x6c63696275340000 = 0x6c63696275346144
[00d8] 6: mem[18] = 0x6c63696275346144
[00d9] 1: push 0x013
[00dc] 1: push 0x011
[00df] 5: push mem[17] (0x6230)
[00e0] 1: push 0x030
[00e3] 0x1e: push 0x6230 << 0x30 = 0x6230000000000000
[00e4] 1: push 0x010
[00e7] 5: push mem[16] (0x6265)
[00e8] 1: push 0x020
[00eb] 0x1e: push 0x6265 << 0x20 = 0x626500000000
[00ec] 0x1b: push 0x626500000000 | 0x6230000000000000 = 0x6230626500000000
[00ed] 1: push 0x0f
[00f0] 5: push mem[15] (0x6669)
[00f1] 1: push 0x010
[00f4] 0x1e: push 0x6669 << 0x10 = 0x66690000
[00f5] 0x1b: push 0x66690000 | 0x6230626500000000 = 0x6230626566690000
[00f6] 1: push 0x0e
[00f9] 5: push mem[14] (0x3165)
[00fa] 0x1b: push 0x3165 | 0x6230626566690000 = 0x6230626566693165
[00fb] 6: mem[19] = 0x6230626566693165
[00fc] 1: push 0x014
[00ff] 1: push 0x00
[0102] 6: mem[20] = 0x0
[0103] 1: push 0x018
[0106] 1: push 0x01
[0109] 6: mem[24] = 0x1
[010a] 1: push 0x017
[010d] 1: push 0x00
[0110] 6: mem[23] = 0x0
[0111] 1: push 0x019
[0114] 1: push 0x00
[0117] 6: mem[25] = 0x0
[0118] 1: push 0x018
[011b] 5: push mem[24] (0x1)
[011c] 1: push 0x01
[011f] 0x11: push 0x1 == 0x1
[0120] 0x10 - JNE: Did not jump to 0x241
[0123] 1: push 0x014
[0126] 5: push mem[20] (0x0)
[0127] 1: push 0x08
[012a] 0x12: push 0x0 < 0x8
[012b] 0x10 - JNE: Did not jump to 0x150
[012e] 1: push 0x015
[0131] 1: push 0x08
[0134] 5: push mem[8] (0x3736353433323130)
[0135] 1: push 0x08
[0138] 1: push 0x014
[013b] 5: push mem[20] (0x0)
[013c] 0xd: push 0x0 * 0x8 = 0x0
[013d] 0x1e: push 0x3736353433323130 >> 0x0 = 0x3736353433323130
[013e] 6: mem[21] = 0x3736353433323130
[013f] 1: push 0x016
[0142] 1: push 0x012
[0145] 5: push mem[18] (0x6c63696275346144)
[0146] 1: push 0x08
[0149] 1: push 0x014
[014c] 5: push mem[20] (0x0)
[014d] 0xd: push 0x0 * 0x8 = 0x0
[014e] 0x1e: push 0x6c63696275346144 >> 0x0 = 0x6c63696275346144
[014f] 6: mem[22] = 0x6c63696275346144
[0150] 1: push 0x014
[0153] 5: push mem[20] (0x0)
[0154] 1: push 0x07
[0157] 0x14: push 0x0 > 0x7
[0158] 0x10 - JNE: Jump to 0x17d
[017d] 1: push 0x015
[0180] 1: push 0x015
[0183] 5: push mem[21] (0x3736353433323130)
[0184] 1: push 0x0ff
[0187] 0x1b: push 0xff & 0x3736353433323130 = 0x30
[0188] 6: mem[21] = 0x30
[0189] 1: push 0x016
[018c] 1: push 0x016
[018f] 5: push mem[22] (0x6c63696275346144)
[0190] 1: push 0x0ff
[0193] 0x1b: push 0xff & 0x6c63696275346144 = 0x44
[0194] 6: mem[22] = 0x44
[0195] 1: push 0x014
[0198] 5: push mem[20] (0x0)
[0199] 1: push 0x02
[019c] 0x11: push 0x2 == 0x0
[019d] 0x10 - JNE: Jump to 0x1ac
[01ac] 1: push 0x014
[01af] 5: push mem[20] (0x0)
[01b0] 1: push 0x09
[01b3] 0x11: push 0x9 == 0x0
[01b4] 0x10 - JNE: Jump to 0x1c3
[01c3] 1: push 0x014
[01c6] 5: push mem[20] (0x0)
[01c7] 1: push 0x0d
[01ca] 0x11: push 0xd == 0x0
[01cb] 0x10 - JNE: Jump to 0x1da
[01da] 1: push 0x014
[01dd] 5: push mem[20] (0x0)
[01de] 1: push 0x0f
[01e1] 0x11: push 0xf == 0x0
[01e2] 0x10 - JNE: Jump to 0x1f1
[01f1] 1: push 0x015
[01f4] 5: push mem[21] (0x30)
[01f5] 1: push 0x016
[01f8] 5: push mem[22] (0x44)
[01f9] 0x11: push 0x44 == 0x30
[01fa] 1: push 0x00
[01fd] 0x11: push 0x0 == 0x0
[01fe] 0x10 - JNE: Did not jump to 0x208
[0201] 1: push 0x018
[0204] 1: push 0x00
[0207] 6: mem[24] = 0x0
[0208] 1: push 0x015
[020b] 5: push mem[21] (0x30)
[020c] 1: push 0x016
[020f] 5: push mem[22] (0x44)
[0210] 0x11: push 0x44 == 0x30
[0211] 0x10 - JNE: Jump to 0x220
[0220] 1: push 0x014
[0223] 1: push 0x014
[0226] 5: push mem[20] (0x0)
[0227] 1: push 0x01
[022a] 9: push 0x1 + 0x0 = 0x1
[022b] 6: mem[20] = 0x1
[022c] 1: push 0x014
[022f] 5: push mem[20] (0x1)
[0230] 1: push 0x0f
[0233] 0x14: push 0x1 > 0xf
[0234] 0x10 - JNE: Jump to 0x23e
[023e] e: jmp 0x118
[0118] 1: push 0x018
[011b] 5: push mem[24] (0x0)
[011c] 1: push 0x01
[011f] 0x11: push 0x1 == 0x0
[0120] 0x10 - JNE: Jump to 0x241
[0241] 1: push 0x017
[0244] 5: push mem[23] (0x0)
[0245] 1: push 0x010
[0248] 0x11: push 0x10 == 0x0
[0249] 0x10 - JNE: Jump to 0x253
[0253] 1: push 0x019
[0256] 5: push mem[25] (0x0)
[0257] 0x19: popped 0x0 to success
[0258] Exiting...

I’ll see at the bottom where it fails:

[0257] 0x19: popped 0x0 to success
[0258] Exiting...

This starts by pushing both the input key and a static bytes. Then it get them and does a check like this:

[0210] 0x11: push 0x44 == 0x30
[0211] 0x10 - JNE: Jump to 0x220

It’s jumping to 0x220 which is failure because my first byte of input (0x30 == “0”) doesn’t match 0x44 (“D”). If I update the first letter to be “D” and run again, now there are 341 lines of output. It does do some switches off the static value it loads at the start, but it’s pretty easy from here to get the password “DaCubicleLife101”:

image-20241108182204337

The image has a partial flag:

catmeme1

catmeme2.jpg.c4tb

This one can be solved by the same process very quickly. I’ll have to add a couple new opcodes, and each of the bytes loaded at the beginning are XORed before compared, but working through it I’ll find the password “G3tDaJ0bD0neM4te”:

image-20241108182406601

The image has more flag:

catmeme2

catmeme3.jpg.c4tb

Bytes 0-3

The same process works here, but it’s more complex to check. It’s no longer single byte checks. There are three checks in this one. The first looks at the first four bytes, and takes a djb2 hash of it and expects the results to be 0x7c8df4cb. I’ll write a Python script with z3 to generate possible solutions:

from z3 import *


def hash_djb2(key):                                                                                                                                
    hash = BitVecVal(5381, 32)
    for i in range(len(key)):
        hash = (hash *33 + ZeroExt(24, key[i])) & 0xFFFFFFFF
    return hash


s = Solver()

key = [BitVec(f'key_{i}', 8) for i in range(4)]
printable_constraints = [And(key[i] >= 33, key[i] <= 126) for i in range(4)]

target = 0x7c8df4cb

s.add(hash_djb2(key) == target)
s.add(printable_constraints)

while s.check() == sat:
    model = s.model()
    solution = [model[key[i]].as_long() for i in range(4)]
    print("Solution found: ", bytes(solution).decode())
    s.add(Or([key[i] != solution[i] for i in range(4)]))

This finds a bunch of options:

oxdf@hacky$ python img3-1.py 
Solution found:  Vg0Y
Solution found:  X$QY
Solution found:  X$R8
Solution found:  WER8
Solution found:  VfR8
Solution found:  X$Pz
Solution found:  WEPz
Solution found:  VfPz
Solution found:  X%0Y
Solution found:  WF0Y
Solution found:  WDs8
Solution found:  WEQY
Solution found:  VfQY
Solution found:  WF18
Solution found:  Vg18
Solution found:  X%18
Solution found:  WDrY
Solution found:  VerY
Solution found:  X#rY
Solution found:  X#s8
Solution found:  Ves8
Solution found:  X#qz
Solution found:  WDqz
Solution found:  Veqz
Solution found:  WF/z
Solution found:  X%/z
Solution found:  Vg/z

“VerY” jumps out as being an actual word.

Bytes 4-7

The next check makes sure that the input when rotated right 13 bits matches a target, 0x8b681d82. I’ll use Python and z3 again:

from z3 import *

target = 0x8b681d82

# Define the rotate function using Z3 expressions
def rotate(x):
    return RotateLeft(x, 32 - 13)

key = [BitVec(f'k{i}', 8) for i in range(4)]
solver = Solver()
for k in key:
    solver.add(k >= 32, k <= 126)

res = BitVecVal(0, 32)

for k in key:
    res = rotate(res) + ZeroExt(24, k)

solver.add(res == target)

while solver.check() == sat:
    model = solver.model()
    found_key = ''.join(chr(model[k].as_long()) for k in key)
    print(f"Found key: {found_key}")
    solver.add(Or([k != model[k] for k in key]))

This finds two options:

oxdf@hacky$ python img3-2.py 
Found key: Eu-B
Found key: DumB

“DumB” seem to go with “VerY” nicely.

Bytes 8-15

I actually originally just guessed at the last 8 bytes to get it, and managed to solve (someone had hinted to me that it was guessable). Still, it’s worth understanding what is happening.

These 8 bytes has two checks. First, they must have an Adler-32 checksum of 0xf910374. Then the fnv_hash of the full password must be 0x31f009d2.

This can be solved using z3, or guessing as “password”.

This image gives the third piece of the password:

catmeme3

Solve

On decrypting the third image, it shows a message:

image-20241108184100427

The .efi file is now decrypted:

image-20241108184248648

Running it generates another image, but also gives the password:

image-20241108184322752

Decrypting that file prints a message:

image-20241108184406474

The rest of the flag is actual in that message, “und3r_c0nstructi0n”. The image is just another meme:

your_mind