Writeup: FlareOn 2021: 006 - PetTheKitty

1. TLDR

PetTheKitty graph

2. Dane wejściowe

Plik z zadaniem znajduje się tutaj. Hasło: flare.

Przedmiotem zadania plik był IR_PURRMACHINE.pcapng

'IR_PURRMACHINE.pcapng'
 README.txt

W pliku txt znajdowała się wiadomość:

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. Analiza pliku pcapng

W pierwszej kolejności wylistowałem wykorzystywane protokoły:

>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

Następnie wylistowałem komunikację z wykorzystaniem protokołu DNS:

>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

Następnie wylistowałem komunikację z wykorzystaniem protokołu BROWSER:

>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

Następnie wylistowałem strumienie danych przesyłanych z wykorzystaniem protokołów TCP:

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

0
1

Wykorzystując narzędzie Wireshark zapisałem zidentyfikowane strumienie danych:

 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

W następnej kolejności podjąłem próbę odzyskania plików:

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

4. Analiza strumieni danych

W wyżej opisany sposób, wyodrębniłem plik png oraz plik mający format zgodny z aktualizacją Windows (PA30). Postanowiłem wykorzystać plik PA30 jako patch dla pliku png. Wykorzystałem narzędzie dostępne tutaj.

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

Takie działanie nie przyniosło rezultatu. Analizujac strumienie danych, kilkukrotnie zauważyłem przesłanie ciągu meoow. Spróbowałem zatem zmodyfikować skrypt delta_patch w celu odczytania flagi z wykorzystaniem meow:

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. Odczytanie flagi

Po uruchomieniu odczytany został plik tekstowy:

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

… zawierający flagę:

1m_H3rE_Liv3_1m_n0t_a_C4t@flare-on.com