Forensic Tea Party

problem description


The attached archive sized 392MB is extracted into one file sized 1GB which is probably a memory snapshot of a VMWare virtual machine.

Memory Forensics Baby steps

We will use volatility which is a well-known memory-forensic open-source tool, in case we do not have it yet it is simply installed by running python -m pip install volatility3.

As a sanity-check we run vol -f vm.vmem the results show that indeed volatility has successfully identified the memory image:

Volatility 3 Framework 2.4.0

Variable        Value
Kernel Base     0xf80378e83000
DTB     0x1ab000
Symbols file:///...
Is64Bit True
IsPAE   False
layer_name      0 WindowsIntel32e
memory_layer    1 FileLayer
KdVersionBlock  0xf803791d8720
Major/Minor     15.16299
MachineType     34404
KeNumberProcessors      2
SystemTime      2022-09-02 22:32:31
NtSystemRoot    C:\Windows
NtProductType   NtProductWinNt
NtMajorVersion  10
NtMinorVersion  0
PE MajorOperatingSystemVersion  10
PE MinorOperatingSystemVersion  0
PE Machine      34404
PE TimeDateStamp        Tue Apr  2 04:29:15 2019

First we will see which process are currently running by using vol -f vm.vmem windows.pslist, and we can see there is one interesting process named TeaParty.exe with PID 4484:

PID     PPID    ImageFileName   Offset(V)       Threads   CreateTime

4       0       System          0xa08ce048c440  123       2022-09-02 22:01:11
280     4       smss.exe        0xa08ce1772040  3         2022-09-02 22:01:11
4484    2700    TeaParty.exe    0xa08ce26aa080  9         2022-09-02 22:26:18

Dump the process executable file using vol -f vm.vmem windows.pslist --pid 4484 --dump:

PID     PPID    ImageFileName   Offset(V)       Threads   File output
4484    2700    TeaParty.exe    0xa08ce26aa080  9         pid.4484.0x420000.dmp

By using file pid.4484.0x420000.dmp we find out that this is a .net assembly:

PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

Some .Net "reversing"

Open the executable in ILSpy, the result is successful and we can see the decompiled C# clearly, starting from the main entry point:

// TeaParty.Program
private static void Main()
    Application.Run(new global::TeaParty.TeaParty());
// TeaParty.TeaParty
public TeaParty()
private void InitializeComponent()
    base.Load += new System.EventHandler(Form1_Load);
private void Form1_Load(object sender, EventArgs e)
    InstallDriver("TeaParty", "C:\\Program Files\\AliceInWonderlandTeaParty\\TeaParty.sys");
    if (CheckDebugging())
        Environment.FailFast("Suspecting Anti-Analysis Environment");
    hookId = SetHook(hookProc);
    for (int i = 0; i < passcodeLength; i++)
private static IntPtr SetHook(HookProc hookProc)
    IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
    return SetWindowsHookEx(13, hookProc, moduleHandle, 0u);
private const int WH_KEYBOARD_LL = 13;
private static int passcodeLength = 17;
private static List<string> buffers = new List<string>();
private static HookProc hookProc = HookCallback;

Some points to note:

  1. Indeed the program installs a kernel module which we will investigate later.
  2. It has some anti-debug measures.
  3. It sets a Win32 Hook number 13 which is called WH_KEYBOARD_LL it is a Low-Level keyboard events hook.
  4. It expects a passcode which is probably needed in order to get the flag and is. of length 17.

The HookCallback contents:

private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    if (nCode >= 0 && wParam == (IntPtr)256)
        int num = Marshal.ReadInt32(lParam);
        for (int i = 0; i < passcodeLength; i++)
            buffers[i] += (char)num;
        if (CalculateMd5HexDigest(buffers.Last()) == GetHash1() || CalculateMd5HexDigest(buffers.Last()) == GetHash2())
            MessageBox.Show("You Got the FLAG!");
        buffers.RemoveAt(buffers.Count - 1);
        buffers.Insert(0, "");
    return CallNextHookEx(hookId, nCode, wParam, lParam);
private const int WM_KEYDOWN = 256;

The 256 constant means WM_KEYDOWN, each keypress captured is appended to all 17 strings in the list buffers, if the MD5 of the last string is one of two hashes - the app concludes that you got the right passcode and use it to asks for the flag from the driver, otherwise it rotates the strings in buffers by removing the last and adding a new empty string at the beginning.

Now we should look what is the implementation of GetHash1(), GetHash2() and GetFlag(string):

private static string GetHash1()
    IntPtr intPtr = CreateFileA("\\\\.\\TeaParty", 1073741824u, 2u, IntPtr.Zero, 3u, 0u, IntPtr.Zero);
    IntPtr intPtr2 = Marshal.AllocHGlobal(32);
    int lpBytesReturned = 0;
    bool flag = DeviceIoControl(intPtr, 2236424u, IntPtr.Zero, 0u, intPtr2, 32u, out lpBytesReturned, IntPtr.Zero);
    byte[] array = new byte[32];
    Marshal.Copy(intPtr2, array, 0, 32);
    return Encoding.ASCII.GetString(array);
private static string GetHash2()
    if (CheckDebugging())
        Environment.FailFast("Suspecting Anti-Analysis Environment");
    IntPtr intPtr = CreateFileA("\\\\.\\TeaParty", 1073741824u, 2u, IntPtr.Zero, 3u, 0u, IntPtr.Zero);
    IntPtr intPtr2 = Marshal.AllocHGlobal(32);
    int lpBytesReturned = 0;
    bool flag = DeviceIoControl(intPtr, 2236428u, IntPtr.Zero, 0u, intPtr2, 32u, out lpBytesReturned, IntPtr.Zero);
    byte[] array = new byte[32];
    Marshal.Copy(intPtr2, array, 0, 32);
    return Encoding.ASCII.GetString(array);
private static string GetFlag(string passcode)
    IntPtr intPtr = CreateFileA("\\\\.\\TeaParty", 1073741824u, 2u, IntPtr.Zero, 3u, 0u, IntPtr.Zero);
    IntPtr intPtr2 = Marshal.AllocHGlobal(512);
    int lpBytesReturned = 0;
    IntPtr intPtr3 = Marshal.AllocHGlobal(passcode.Length + 1);
    Marshal.Copy(Encoding.ASCII.GetBytes(passcode + "\0"), 0, intPtr3, passcode.Length + 1);
    bool flag = DeviceIoControl(intPtr, 2236416u, intPtr3, (uint)(passcode.Length + 1), IntPtr.Zero, 0u, out lpBytesReturned, IntPtr.Zero);
    flag = DeviceIoControl(intPtr, 2236420u, IntPtr.Zero, 0u, intPtr2, 512u, out lpBytesReturned, IntPtr.Zero);
    byte[] array = new byte[lpBytesReturned];
    Marshal.Copy(intPtr2, array, 0, lpBytesReturned);
    return Encoding.ASCII.GetString(array);

The common structure of these is:

  1. Open a handle to the installed driver.
  2. Allocate memory for return values.
  3. Issue IOCTL(s) to the driver.
  4. Return the results. Also note that from GetFlag we see that the passcode is indeed of length 17.

When reversing the driver we should keep in mind the following:

Function IOCTL(s) called
GetHash1 0x222008
GetHash2 0x22200c
GetFlag 0x222000, 0x222004

To find the driver executable file we first run vol -f vm.vmem windows.filescan | grep TeaParty\.sys:

0xa08ce38c74b0  \Program Files\AliceInWonderlandTeaParty\TeaParty.sys   216

Now dump the driver executable with vol -f vm.vmem windows.dumpfiles --virtaddr 0xa08ce38c74b0:


We open the .img file in IDA Pro.

Reversing the driver


First look for interesting imports used by the driver and indeed we find some:

  1. BCrypt* functions from ksecdd - indicats that the flag is probably encrypted and the passcode is needed to decrypt it.
  2. Io* functions from ntoskrnl - driver bread and butter, the IOCTL handler(s) will be set on a DRIVER_OBJECT stucture probably passed to DriverEntry.
  3. ZwClose, ZwOpenKey and ZwQueryValueKey - registry access, might be important.


Next look for interesting strings in the driver, which hopefully will be related to the imports we are interested in, the low hanging fruits are:

Address Length String
01DF0 0x08 AES
01E00 0x1A ObjectLength
01E20 0x18 BlockLength
01E40 0x20 ChainingModeCBC
01E60 0x1A ChainingMode
01E80 0x16 CareForTea
01EA0 0x6A \\Registry\\Machine\\SOFTWARE\\AliceInWonderlandTeaParty

Hence some assumptions and notes:

  1. The flag is encrypted with AES in CBC mode.
  2. The correct passcode will be needed to decrypt the flag.
  3. The registry will probably play some role in the challenge. That part can be handled by using the relevant volatility plugin(s).
  4. Probably the MD5 hash of the passcode will be needed to recover it.
  5. Possible strategies to recover the hash of the passcode:
    • Staticly reversing the implementation(s) of the IOCTLs called by GetHash1 and GetHash2. Not the way we solved this part of the challenge, but this will be explored in this write-up.
    • Dynamicly running the executable and the driver - also not the way we choose to tackle this.
    • Looking at the memory of the TeaParty.exe process in hope of finding the hashes returned from the driver since they are allocated on the heap and are not cleaned properly - might work only if the driver was asked for the hashes at least once. This can be done with different degrees of finesse:
      1. Brute-Force approach, what we actually did during the CTF, run strings vm.vmem and look for possible hex-encoded md5 hashes in the output.
      2. Use the Yara based volatility plugin: windows.vadyarascan to search for hex-encoded md5 hashes in the process memory, run: vol -f /mnt/c/Users/dgootvil/Downloads/Windows10x64_AliceInWonderland-b4365e16.vmem windows.vadyarascan --yara-rules '/\W[0-9a-f]{32}\W/' --pid 4484 2>/dev/null | grep ^0x | cut -f5 | cut -b3-99 | sed 's: ::g' | sort -u | while read hex; do xxd -r -p <(echo $hex) ; echo; done.
      3. Search the heap memory for the process directly, find the VAD in the process that has the most commit-charge and is mapped as PAGE_READWRITE. Use: vol -f vm.vmem windows.vadinfo --pid 4484 | grep PAGE_READWRITE | sort -k8 -n -r | head -n1 to find out that the heap adderss is 0x48b0000. Dump the heap using vol -f vm.vmem windows.vadinfo --address 0x48b0000 --pid 4484 --dump and than strings -n 32 pid.4484.vad.0x48b0000-0x68affff.dmp | grep -P '^[a-f0-9]{32}' | sort -u.

Code dive

To confirm our assumption we choose to statically reverse the driver and looking for the IOCTL handler of the driver, to handle IOCTLs the driver sets a dispatch routine for IRP_MJ_DEVICE_CONTROL which is defined in wdm.h as 0x0e, the "enhanced" decompilation from the entry point onward is as follows:

NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
  struct _DRIVER_OBJECT *drv; // rdi

  drv = DriverObject;
  return init_driver(drv);
__int64 __fastcall init_driver(PDRIVER_OBJECT DriverObject)
  PDRIVER_OBJECT drv; // rbx
  UNICODE_STRING DestinationString; // [rsp+40h] [rbp-28h]
  UNICODE_STRING SymbolicLinkName; // [rsp+50h] [rbp-18h]

  drv = DriverObject;
  DbgPrint("Hello World!\n");
  RtlInitUnicodeString(&DestinationString, L"\\Device\\TeaParty");
  RtlInitUnicodeString(&SymbolicLinkName, L"\\DosDevices\\TeaParty");
  IoCreateDevice(drv, 0, &DestinationString, 0x22u, 0, 0, &DeviceObject);
  IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
  drv->MajorFunction[0] = (PDRIVER_DISPATCH)no_op_success;
  drv->MajorFunction[2] = (PDRIVER_DISPATCH)no_op_success;
  drv->MajorFunction[3] = (PDRIVER_DISPATCH)no_op_success;
  drv->MajorFunction[4] = (PDRIVER_DISPATCH)no_op_success;
  drv->MajorFunction[0xE] = (PDRIVER_DISPATCH)handle_ioctl;
  drv->DriverUnload = (PDRIVER_UNLOAD)unload_driver;
  return 0i64;

The code of the IOCTL handler itself is the most interesting to us:

NTSTATUS __fastcall handle_ioctl(_DEVICE_OBJECT *DeviceObject, _IRP *Irp)
  _IO_STACK_LOCATION *io_sl; // rdi
  _IRP *pIRP; // rbx
  NTSTATUS ret_code; // esi
  ULONG controlCode; // ecx
  int v6; // ecx
  int v7; // ecx
  int v8; // ecx
  _OWORD *out_hash; // rdx
  int v10; // er8
  __int64 idx; // r9
  char v12; // cl
  __int128 out_hash_lo; // xmm0
  __int128 out_hash_hi; // xmm1
  int v15; // er8
  __int64 idx_; // r9
  char v17; // cl
  _OWORD *flag_out_user; // rax
  _OWORD *flag_out; // rcx
  unsigned __int8 *passcode_user; // rdi
  size_t passcode_user_strlen; // r8
  unsigned int from_registry_; // [rsp+20h] [rbp-19h]
  _OWORD hash2[2]; // [rsp+28h] [rbp-11h]
  _OWORD hash1[2]; // [rsp+48h] [rbp+Fh]
  unsigned int from_registry; // [rsp+68h] [rbp+2Fh]

  io_sl = Irp->Tail.Overlay.CurrentStackLocation;
  pIRP = Irp;
  ret_code = 0;
  from_registry_ = 0;
  hash1[0] = 0i64;
  hash1[1] = 0i64;
  hash2[0] = 0i64;
  hash2[1] = 0i64;
  read_from_registry(L"\\Registry\\Machine\\SOFTWARE\\AliceInWonderlandTeaParty", L"CareForTea", &from_registry_);
  controlCode = io_sl->Parameters.DeviceIoControl.IoControlCode;
  from_registry = from_registry_;
  v6 = controlCode - 0x222000;
  if ( v6 )
    v7 = v6 - 4;
    if ( !v7 )
      // JCTF-NOTE: GetFlag (2nd call) IOCTL=0x222004
      flag_out_user = pIRP->AssociatedIrp.SystemBuffer;
      flag_out = decrypted_flag;
      io_sl->Parameters.Read.Length = 8;
      *flag_out_user = *flag_out;
      flag_out_user[1] = flag_out[1];
      flag_out_user[2] = flag_out[2];
      ExFreePoolWithTag(flag_out, 0xAABBCCDD);
      goto LABEL_17;
    v8 = v7 - 4;
    if ( v8 )   
      if ( v8 != 4 )
        ret_code = 0xC0000010;
        goto LABEL_17;
      // JCTF-NOTE: GetHash2 IOCTL=0x22200c
      out_hash = pIRP->AssociatedIrp.SystemBuffer; 
      v10 = 0;
      io_sl->Parameters.Read.Length = 32;
      idx = 0i64;
        v12 = v10++;
        *((_BYTE *)hash2 + idx) = hash2_enc_off_FFFFF80944E02140[idx] ^ *((_BYTE *)&from_registry + (v12 & 3));
      while ( v10 < 32 );
      out_hash_lo = hash2[0];
      out_hash_hi = hash2[1];
      // JCTF-NOTE: GetHash1 IOCTL=0x222008
      out_hash = pIRP->AssociatedIrp.SystemBuffer;
      v15 = 0;
      io_sl->Parameters.Read.Length = 32;
      idx_ = 0i64;
        v17 = v15++;
        *((_BYTE *)hash1 + idx_) = hash1_enc_off_FFFFF80944E02120[idx_] ^ *((_BYTE *)&from_registry + (v17 & 3));
      while ( v15 < 32 );
      out_hash_lo = hash1[0];
      out_hash_hi = hash1[1];
    *out_hash = out_hash_lo;
    out_hash[1] = out_hash_hi;
    goto LABEL_17;
  // JCTF-NOTE: GetFlag (1st call) IOCTL=0x222000
  passcode_user = (unsigned __int8 *)pIRP->AssociatedIrp.SystemBuffer;
  passcode_user_strlen = -1i64;
  passcode = 0i64;
  xmmword_FFFFF80944E03068 = 0i64;
  while ( passcode_user[passcode_user_strlen] );
  memmove(&passcode, passcode_user, passcode_user_strlen);
  decrypted_flag = decrypt_flag(encrypted_flag, 0x30u);
  pIRP->IoStatus.Status = 0;
  pIRP->IoStatus.Information = 48i64;
  IofCompleteRequest(pIRP, 0);
  return ret_code;

Things to note:

  1. The IOCTLs called by GetHash1 and GetHash2 which each calculate a hash. Some reversing can reveal that:
    • A DWORD long xor-key is stored under the registry key AliceInWonderlandTeaParty named CareForTea.
    • The xor-key is used to decrypt a global of length 32, a different global for each IOCTL. The encrypted globals are:
      • 39D32A983EDF7AC36EDA25C03F8F79993DDE2B99328D78C73CDE7FC26ADE79C0
      • 32892DC73AD37EC2328E29943DD27AC33ADD2AC039D97FC03D8E7E9238DC7D95
  2. The two GetFlag IOCTLs:
    • The first:
      1. Gets the passcode and stores it in a global.
      2. Passes the probably encrypted flag and the size 0x30 which is 3 blocks of AES to the decryption function, there is something fishy about this length value as a data reference at the start of the 3rd block exists.
    • The second just returns the already decrypted flag.

Decrypting the hashes

To decrypt the hashes first retrive the value from the registry, searching for the registry key AliceInWonderlandTeaParty using vol -f vm.vmem windows.registry.hivescan 2>/dev/null | sed -n -e '5,$p' | while read offset; do vol -f vm.vmem windows.registry.printkey --offset ${offset} --key AliceInWonderlandTeaParty 2>/dev/null; done the result after some editing is:

Hive Offset Type Key Name Data
0xde88d6335000 Key ?\AliceInWonderlandTeaParty - -
... Key ?\AliceInWonderlandTeaParty - -
0xde88d0ae4000 REG_DWORD SOFTWARE\AliceInWonderlandTeaParty CareForTea 2703026955
... Key ?\AliceInWonderlandTeaParty - -
0xde88d606c000 Key ?\AliceInWonderlandTeaParty - -

Hence the dword value retrieved from the registry is 0xa11ceb0b now we can find the hashes running the following python script:

k = (0xa11ceb0b).to_bytes(4, byteorder='little')
h1 = bytes.fromhex('39D32A983EDF7AC36EDA25C03F8F79993DDE2B99328D78C73CDE7FC26ADE79C0')
h2 = bytes.fromhex('32892DC73AD37EC2328E29943DD27AC33ADD2AC039D97FC03D8E7E9238DC7D95')
print('\n'.join(''.join(chr(a^b) for a,b in zip(h, k*(len(h)//len(k)))) for h in [h1,h2]))
# prints:
# 286954fbe19a4de865789fdf75cca5ea
# 9b1f18bc9e5569fb166a22ca6eb337a4

Putting these hashes into crack-station we get:

Hash Type Result
286954fbe19a4de865789fdf75cca5ea md5 whothefuckisalice
9b1f18bc9e5569fb166a22ca6eb337a4 Unknown Not found.

So the correct passcode is probably: whothefuckisalice, equipped with that we can finally tackle the decryption of the flag.

Decrypting the flag

Look at the code implementing the flag decryption:

PVOID __fastcall decrypt_flag(unsigned __int8 *ciphertext, unsigned int len)
  unsigned int len_; // er14
  unsigned __int8 *ct; // r15
  PVOID plaintext; // rsi
  PVOID pKey; // rdi
  void *pIV2; // rbx
  PVOID pIV; // rax
  unsigned int NumberOfBytes[2]; // [rsp+50h] [rbp-20h]
  __int64 hAES; // [rsp+58h] [rbp-18h]
  __int64 hKey; // [rsp+60h] [rbp-10h]
  unsigned int iv_len; // [rsp+C0h] [rbp+50h]
  unsigned int plaintext_len; // [rsp+C8h] [rbp+58h]

  len_ = len;
  ct = ciphertext;
  hAES = 0i64;
  hKey = 0i64;
  plaintext_len = 0;
  NumberOfBytes[1] = 0;
  NumberOfBytes[0] = 0;
  iv_len = 0;
  plaintext = 0i64;
  pKey = 0i64;
  pIV2 = 0i64;
  if ( (int)BCryptOpenAlgorithmProvider(&hAES, L"AES", 0i64, 0i64) >= 0
    && (int)BCryptGetProperty(hAES, L"ObjectLength", NumberOfBytes, 8i64, &NumberOfBytes[1], 0) >= 0 )
    pKey = ExAllocatePoolWithTag(PagedPool, NumberOfBytes[0], 0xAABBCCDD);
    if ( pKey )
      if ( (int)BCryptGetProperty(hAES, L"BlockLength", &iv_len, 8i64, &NumberOfBytes[1], 0) >= 0 && iv_len <= 0x10 )
        pIV = ExAllocatePoolWithTag(PagedPool, iv_len, 0xAABBCCDD);
        pIV2 = pIV;
        if ( pIV )
          memmove(pIV, IV, iv_len);
          if ( (int)BCryptSetProperty(hAES, L"ChainingMode", L"ChainingModeCBC", 0x20i64, 0) >= 0
            && (int)BCryptGenerateSymmetricKey(hAES, &hKey, pKey, NumberOfBytes[0], &passcode, 0x20, 0) >= 0
            && (int)BCryptDecrypt(hKey, ct, len_, 0i64, pIV2, iv_len, 0i64, 0, &plaintext_len, 1) >= 0 )
            plaintext = ExAllocatePoolWithTag(PagedPool, plaintext_len, 0xAABBCCDD);
            memset(plaintext, 0, plaintext_len);
            if ( plaintext )
              BCryptDecrypt(hKey, ct, len_, 0i64, pIV2, iv_len, plaintext, plaintext_len, &plaintext_len, 1);
  if ( hAES )
    BCryptCloseAlgorithmProvider(hAES, 0i64);
  if ( hKey )
  if ( pKey )
    ExFreePoolWithTag(pKey, 0xAABBCCDD);
  if ( pIV2 )
    ExFreePoolWithTag(pIV2, 0xAABBCCDD);
  return plaintext;
  1. By the call to BCryptSetProperty key length is 32 it's value is probably the zero-padded passcode:
    • KEY is 77686f7468656675636b6973616c696365000000000000000000000000000000.
  2. The data reference to the 3rd block is actually the IV, we assume this is a bug in the challenge or possibly a misdirection, anyway we assume the total ciphertext length including the IV is 3 blocks. This means the ciphertext and IV are:
    • CT is 74F814897D5AC9C05301FD9922C3AC84FDFB4312FF39AB49EE39E580C1F5160C.
    • IV is 000102030405060708090A0B0C0D0E0F.
  3. By the last argument to BCryptDecrypt the plaintext includes padding.
  4. Putting all of this into CyberChef we get the flag INTENT{0ff_w1th_7h31r_H34ds}.