This page looks best with JavaScript enabled

Flare-On 8 2021 Challenge 6 Solution - 06_PetTheKitty

Hosted by FireEye's FLARE team from 10 September - 22 October

 ·  ☕ 6 min read  ·  🌚 drome

Thanks drome for sharing his knowledge and skills! He completed all 10 challenges and this series of writeups is done by him :)

Details Links
Official Challenge Site https://flare-on.com/
Official Challenge Announcement https://www.fireeye.com/blog/threat-research/2021/08/announcing-the-eighth-annual-flare-on-challenge.html
Official Solutions https://www.mandiant.com/resources/flare-on-8-challenge-solutions
Official Challenge Binaries http://flare-on.com/files/Flare-On8_Challenges.zip

06_PetTheKitty

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
7zip password: flare

We are given a PCAP file in this challenge. If we follow the first TCP stream, we can see some clear signs of traffic formatting, with ME0W being present in every message.

ME0W traffic header captured in TCP stream hexdump in Wireshark

From observation, the traffic header has the following format

(DWORD) 'ME0W'
(DWORD) Length of message
(DWORD) Length of message (duplicate)
(Variable length) message

In stream 0, we have 2 messages, one being a PNG file and the other being a file that has the file signature PA30.

The PNG file isn’t very useful, just a picture of a cat, so we take a look at the other file. Searching for PA30, we learn that it is the Intra Package Delta format which is also used by Microsoft for Windows update patches.

This post (which we found from this Github issue leading to another Github issue) was particularly useful, and the author wrote a challenge for another CTF based on this format, so we could use the challenge solution on Github. We tried to run it as it was initially but it gave us some error so we had to rewrite it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from ctypes import (windll, wintypes, cast, c_ubyte, c_uint64, POINTER,
                    LittleEndianStructure, byref, c_size_t)

ORIGINAL_FILE = "server0.png"
DIFF_FILE = "server1.bin"
OUT_FILE = "out.bin"

DELTA_FLAG_TYPE = c_uint64
DELTA_FLAG_NONE = 0x00000000
DELTA_APPLY_FLAG_ALLOW_PA19 = 0x00000001

class DELTA_INPUT(LittleEndianStructure):
    _fields_ = [('lpStart', wintypes.LPVOID),
                ('uSize', c_size_t),
                ('Editable', wintypes.BOOL)]

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

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

def apply_delta(original, delta):
    dd = DELTA_INPUT()
    ds = DELTA_INPUT()
    dout = DELTA_OUTPUT()

    ds.lpStart = cast(original, wintypes.LPVOID)
    ds.uSize = len(original)
    ds.Editable = False

    dd.lpStart = cast(delta, wintypes.LPVOID)
    dd.uSize = len(delta)
    dd.Editable = False

    res = windll.msdelta.ApplyDeltaB(
        DELTA_APPLY_FLAG_ALLOW_PA19,
        ds,
        dd,
        byref(dout)
    )
    if res:
        print("Success!")
    else:
        error = windll.kernel32.GetLastError()
        print(f"ApplyDeltaB failed with error {error}")
        return

    return bytes((c_ubyte * dout.uSize).from_address(dout.lpStart))

def main():
    with open(ORIGINAL_FILE, 'rb') as fp:
        original = fp.read()
    with open(DIFF_FILE, 'rb') as fp:
        delta = fp.read()
    patched_bytes = apply_delta(original, delta)
    with open(OUT_FILE, 'wb') as fp:
        fp.write(patched_bytes)

if __name__ == '__main__':
    main()

This write-up was for the same challenge and similar but it didn’t use ctypes and we ultimately didn’t use it.

Applying the diff to the PNG file, we get a PE file.

Analysis

The PE file is a 32-bit DLL with a single exported function Le_Meow.

Le_Meow connects to xn--zn8hrcq4eeadihijjk.flare-on.com:1337, then creates a thread at sub_10001CA3 which does some irrelevant UI stuff, then Le_Meow continues to call sub_100015D4.

sub_100015D4 creates a cmd.exe process, then goes into a command loop where it reads from it, then sends it to sub_100015D4, which xors the output with meoow, then uses the result to diff with Src, which is probably a buffer of null bytes, using sub_10001000 which is a wrapper for CreateDeltaB, then sends the result to the server in the following format

(DWORD) 'ME0W'
(DWORD) Length of original message
(DWORD) Length of the PA30 delta
(Variable length) PA30 Delta

Afterwards sub_100015D4 calls sub_1000128A which is the opposite and receives the delta from the server in the same format, then parses it in the same format, so we can write our script to parse all the messages and see what commands were being sent to and fro.

We see the messages in this format in stream 1, where we get back-and-forth PA30 files between the client and server.

Back-and-forth PA30 files between client and server in TCP stream hexdump in Wireshark

Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from ctypes import (windll, wintypes, cast, c_ubyte, c_uint64, POINTER,
                    LittleEndianStructure, byref, c_size_t)

XOR_KEY = b'meoow'

DELTA_FLAG_TYPE = c_uint64
DELTA_FLAG_NONE = 0x00000000
DELTA_APPLY_FLAG_ALLOW_PA19 = 0x00000001

class DELTA_INPUT(LittleEndianStructure):
    _fields_ = [('lpStart', wintypes.LPVOID),
                ('uSize', c_size_t),
                ('Editable', wintypes.BOOL)]

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

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

def apply_delta(original, delta):
    dd = DELTA_INPUT()
    ds = DELTA_INPUT()
    dout = DELTA_OUTPUT()

    ds.lpStart = cast(original, wintypes.LPVOID)
    ds.uSize = len(original)
    ds.Editable = False

    dd.lpStart = cast(delta, wintypes.LPVOID)
    dd.uSize = len(delta)
    dd.Editable = False

    res = windll.msdelta.ApplyDeltaB(
        DELTA_APPLY_FLAG_ALLOW_PA19,
        ds,
        dd,
        byref(dout)
    )
    if not res:
        error = windll.kernel32.GetLastError()
        raise f"ApplyDeltaB failed with error {error}"

    return bytes((c_ubyte * dout.uSize).from_address(dout.lpStart))

def xor_bytes(data, key):
    return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)])

def get_next_message(data):
    original_length = int.from_bytes(data[4:8], byteorder='little')
    diff_length = int.from_bytes(data[8:12], byteorder='little')
    diff_bytes = data[12:12+diff_length]
    data = data[12+diff_length:]
    return original_length, diff_bytes, data

def parse_stream(data, base_data):
    for i in range(74):
        original_length, diff_bytes, data = get_next_message(data)
        decoded_bytes = apply_delta(base_data, diff_bytes)[:original_length]
        decoded_bytes = xor_bytes(decoded_bytes, XOR_KEY)

        print("Server" if i % 2 else "Client")
        print("------")
        print(decoded_bytes.decode('ansi'))
        print("\n\n")

def main():
    with open('stream', 'rb') as fp:
        data = fp.read()
    base_data = b'\x00' * 0x100
    parse_stream(data, base_data)

if __name__ == '__main__':
    main()

The output of the script shows all the different commands sent by the server and responses from the client, and scrolling through we find this important response from the client:

Flag in one of the client responses

Flag

1m_H3rE_Liv3_1m_n0t_a_C4t@flare-on.com
Share on