Writeup: FlareOn 2021: 006 - PetTheKitty

1. TLDR

PetTheKitty graph

2. Input data

The challenge file is here. Hasło: flare.

The available files are IR_PURRMACHINE.pcapng and README.txt

'IR_PURRMACHINE.pcapng'
 README.txt

The txt file contained a message:

Hello,
Recently we experienced an attack against our super secure MEOW-5000 network.
Forensic analysis discovered evidence of the files PurrMachine.exe and PetTheKitty.jpg; however, these files were ultimately unrecoverable.
We suspect PurrMachine.exe to be a downloader and do not know what role PetTheKitty.jpg plays (likely a second-stage payload).
Our incident responders were able to recover malicious traffic from the infected machine.
Please analyze the PCAP file and extract additional artifacts.
Looking forward to your analysis,
~Meow

3. Analysis of the pcapng file

I first listed the protocols used:

>tshark -r IR_PURRMACHINE.pcapng -T fields -e frame.protocols | sort | uniq
eth:ethertype:ip:tcp
eth:ethertype:ip:tcp:data
eth:ethertype:ip:udp:dns
eth:ethertype:ip:udp:nbdgm:smb:browser

I then listed the communications using the DNS protocol:

>tshark -r IR_PURRMACHINE.pcapng -Y "dns"
    1   0.000000 172.16.111.139 → 172.16.111.144 DNS 96 Standard query 0xa997 A xn--zn8hscq4eeafedhjjkl.flare-on.com
    2   0.002683 172.16.111.144 → 172.16.111.139 DNS 112 Standard query response 0xa997 A xn--zn8hscq4eeafedhjjkl.flare-on.com A 172.16.111.144
   58   0.336143 172.16.111.139 → 172.16.111.144 DNS 95 Standard query 0xfdac A xn--zn8hrcq4eeadihijjk.flare-on.com
   59   0.336253 172.16.111.144 → 172.16.111.139 DNS 111 Standard query response 0xfdac A xn--zn8hrcq4eeadihijjk.flare-on.com A 172.16.111.144

I then listed communications using the BROWSER protocol:

>tshark -r IR_PURRMACHINE.pcapng -Y "browser"
  109  73.311139 172.16.111.139 → 172.16.111.255 BROWSER 243 Host Announcement USER-PC, Workstation, Server, NT Workstation

I then listed the data streams transmitted using TCP protocols:

>tshark -r IR_PURRMACHINE.pcapng -T fields -e tcp.stream | sort | uniq

0
1

Using the Wireshark tool, I saved the identified data streams:

 Directory of .\exported

2022-02-11  20:40    <DIR>          .
2022-02-11  20:40    <DIR>          ..
2022-02-11  20:38               130 tcp_stream_0_request_139_144.raw
2022-02-11  20:38           213 160 tcp_stream_0_response_144_139.raw
2022-02-11  20:40            11 368 tcp_stream_1_request_139_144.raw
2022-02-11  20:40             2 095 tcp_stream_1_response_144_139.raw
               4 File(s)        226 753 bytes
               2 Dir(s)  40 046 911 488 bytes free

In the next step, I made an attempt to recover the files:

 foremost -t all -i ./tcp_stream_0_response_144_139.raw
Processing: ./tcp_stream_0_response_144_139.raw
|*|

4. Analysis of data streams

In the above-described spoób, I extracted a png file and a file that has a format compatible with the Windows update (PA30). I decided to use the PA30 file as a patch for the png file. I used the tool available here.

.\exported\patch>python delta_patch.py -i C.png -o C-out.png pa30.raw
Applied 1 patch successfully
Final hash: OsdAMg6SJ4EFnx69R5NJFq2ToD4utovfwrzBaVxmssk=

Such an action had no effect. Analyzing the data streams, I noticed several times the string meoow`` being sent. So I tried modifying the delta_patchscript to read the flag usingmeow```:

import binascii
from pprint import pprint
from ctypes import windll, wintypes, c_uint64, cast, POINTER, Union,\
                    c_ubyte, LittleEndianStructure, byref, c_size_t
# types and flags
DELTA_FLAG_TYPE             = c_uint64
DELTA_FLAG_NONE             = 0x00000000
DELTA_APPLY_FLAG_ALLOW_PA19 = 0x00000001

# structures
class DELTA_INPUT(LittleEndianStructure):
    class U1(Union):
        _fields_ = [('lpcStart', wintypes.LPVOID),\
                    ('lpStart', wintypes.LPVOID)]
    _anonymous_ = ('u1',)
    _fields_ = [('u1', U1),\
                ('uSize', c_size_t),\
                ('Editable', wintypes.BOOL)]

class DELTA_OUTPUT(LittleEndianStructure):
    _fields_ = [('lpStart', wintypes.LPVOID),\
                ('uSize', c_size_t)]

# functions
ApplyDeltaB = windll.msdelta.ApplyDeltaB
ApplyDeltaB.argtypes = [DELTA_FLAG_TYPE, DELTA_INPUT, DELTA_INPUT,\
                        POINTER(DELTA_OUTPUT)]
ApplyDeltaB.rettype = wintypes.BOOL
DeltaFree = windll.msdelta.DeltaFree
DeltaFree.argtypes = [wintypes.LPVOID]
DeltaFree.rettype = wintypes.BOOL
gle = windll.kernel32.GetLastError


# this function is modified apply_patchfile_to_buffer from delta_patch.py
def apply_patchfile_to_buffer(buf, buflen, patch_contents, legacy):
    # some patches (Windows Update MSU) come with a CRC32 prepended to the file
    # if the file doesn't start with the signature (PA) then check it
    if patch_contents[:2] != b"PA":
        paoff = patch_contents.find(b"PA")
        if paoff != 4:
            raise Exception("Patch is invalid")
        crc = int.from_bytes(patch_contents[:4], 'little')
        patch_contents = patch_contents[4:]
        if zlib.crc32(patch_contents) != crc:
            raise Exception("CRC32 check failed. Patch corrupted or invalid")
    
    applyflags = DELTA_APPLY_FLAG_ALLOW_PA19 if legacy else DELTA_FLAG_NONE
    
    dd = DELTA_INPUT()
    ds = DELTA_INPUT()
    dout = DELTA_OUTPUT()
    
    ds.lpcStart = cast(buf, wintypes.LPVOID)
    ds.uSize = buflen
    ds.Editable = False
    
    dd.lpcStart = cast(patch_contents, wintypes.LPVOID)
    dd.uSize = len(patch_contents)
    dd.Editable = False
    
    status = ApplyDeltaB(applyflags, ds, dd, byref(dout))
    #if status == 0:
      #raise Exception("Patch {} failed with error {}".format(patch_contents, gle()))
    if status == 0:
      result = bytes()
    else:
      result = bytes((c_ubyte*dout.uSize).from_address(dout.lpStart))
    DeltaFree(dout.lpStart)
    return result


def parse_file(file_name, key):
  parts=[]
  with open(file_name,'rb') as meow_file:
    content = meow_file.read()
    parts = binascii.hexlify(content).split(b'4d453057')
  for part in parts:
    if part:
      pa30 = part[16:]
      pa30 = binascii.unhexlify(pa30)
      pa30_len = len(pa30)
      null_buf = b'\0' * pa30_len
      out = apply_patchfile_to_buffer(null_buf, pa30_len, pa30, False)[:pa30_len]
      plain = ''.join([chr(c ^ k) for (c,k) in zip(out, key)])
      print(plain)


def main():
  key = b'meoow'*1024
  parse_file('tcp_stream_1_request_139_144.raw', key)
  parse_file('tcp_stream_1_response_144_139.raw', key)

if __name__ == "__main__":
  main()

5. Reading the flag

After launching, a text file was read:

type Gotcha.txt
We're no strangers to love
You know the rules and so do I
A full commitment's what I'm thinking of
You wouldn't get this from any other guy
I just wanna tell you how I'm feeling
Gotta make you understand
Never gonna give you up, never gonna let you down
Never gonna run around and desert you
Never gonna make you cry, never gonna say goodbye
Never gonna tell a lie and hurt you
We've known each other for so long
Your heart's been aching but you're too shy to say it
Inside we both know what's been going on
We know the game and we're gonna play it
And if you ask me how I'm feeling
Don't tell me you're too blind to see
1m_H3rE_Liv3_1m_n0t_a_C4t@flare-on.com
Never gonna give you up, never gonna let you down
Never gonna run around and desert you
Never gonna make

… containing the flag:

1m_H3rE_Liv3_1m_n0t_a_C4t@flare-on.com