Forensic Tea Party
- Category: Forensics, Reverse Engineering
- 400 points
- Solved by JCTF Team
Solution
The attached archive Windows10x64_AliceInWonderland-b4365e16.vmem.zip
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 windows.info
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()
{
///...sniped...///
Application.Run(new global::TeaParty.TeaParty());
}
// TeaParty.TeaParty
public TeaParty()
{
InitializeComponent();
}
private void InitializeComponent()
{
///...sniped...///
base.Load += new System.EventHandler(Form1_Load);
///...sniped...///
}
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++)
{
buffers.Add("");
}
}
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:
- Indeed the program installs a kernel module which we will investigate later.
- It has some anti-debug measures.
- It sets a Win32 Hook number 13 which is called
WH_KEYBOARD_LL
it is a Low-Level keyboard events hook. - 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())
{
UnhookWindowsHookEx(hookId);
MessageBox.Show("You Got the FLAG!");
GetFlag(buffers.Last().ToLower());
Application.Exit();
}
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);
Marshal.FreeHGlobal(intPtr2);
CloseHandle(intPtr);
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);
Marshal.FreeHGlobal(intPtr2);
CloseHandle(intPtr);
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);
MessageBox.Show(Encoding.ASCII.GetString(array));
Marshal.FreeHGlobal(intPtr2);
Marshal.FreeHGlobal(intPtr3);
CloseHandle(intPtr);
return Encoding.ASCII.GetString(array);
}
The common structure of these is:
- Open a handle to the installed driver.
- Allocate memory for return values.
- Issue IOCTL(s) to the driver.
- Return the results.
Also note that from
GetFlag
we see that the passcode is indeed of length17
.
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
:
Result |
---|
file.0xa08ce38c74b0.0xa08ce2ebde50.DataSectionObject.TeaParty.sys.dat |
file.0xa08ce38c74b0.0xa08ce3537de0.ImageSectionObject.TeaParty.sys.img |
We open the .img
file in IDA Pro.
Reversing the driver
Imports
First look for interesting imports used by the driver and indeed we find some:
BCrypt*
functions fromksecdd
- indicats that the flag is probably encrypted and the passcode is needed to decrypt it.Io*
functions fromntoskrnl
- driver bread and butter, the IOCTL handler(s) will be set on a DRIVER_OBJECT stucture probably passed to DriverEntry.ZwClose
,ZwOpenKey
andZwQueryValueKey
- registry access, might be important.
Strings
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:
- The flag is encrypted with AES in CBC mode.
- The correct passcode will be needed to decrypt the flag.
- The registry will probably play some role in the challenge. That part can be handled by using the relevant volatility plugin(s).
- Probably the MD5 hash of the passcode will be needed to recover it.
- Possible strategies to recover the hash of the passcode:
- Staticly reversing the implementation(s) of the IOCTLs called by
GetHash1
andGetHash2
. 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:- 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. - 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
. - 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 is0x48b0000
. Dump the heap usingvol -f vm.vmem windows.vadinfo --address 0x48b0000 --pid 4484 --dump
and thanstrings -n 32 pid.4484.vad.0x48b0000-0x68affff.dmp | grep -P '^[a-f0-9]{32}' | sort -u
.
- Brute-Force approach, what we actually did during the CTF, run
- Staticly reversing the implementation(s) of the IOCTLs called by
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;
init_stack_canary();
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;
do
{
v12 = v10++;
*((_BYTE *)hash2 + idx) = hash2_enc_off_FFFFF80944E02140[idx] ^ *((_BYTE *)&from_registry + (v12 & 3));
++idx;
}
while ( v10 < 32 );
out_hash_lo = hash2[0];
out_hash_hi = hash2[1];
}
else
{
// JCTF-NOTE: GetHash1 IOCTL=0x222008
out_hash = pIRP->AssociatedIrp.SystemBuffer;
v15 = 0;
io_sl->Parameters.Read.Length = 32;
idx_ = 0i64;
do
{
v17 = v15++;
*((_BYTE *)hash1 + idx_) = hash1_enc_off_FFFFF80944E02120[idx_] ^ *((_BYTE *)&from_registry + (v17 & 3));
++idx_;
}
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;
DbgPrint((PCH)pIRP->AssociatedIrp.SystemBuffer);
passcode_user_strlen = -1i64;
passcode = 0i64;
xmmword_FFFFF80944E03068 = 0i64;
do
++passcode_user_strlen;
while ( passcode_user[passcode_user_strlen] );
memmove(&passcode, passcode_user, passcode_user_strlen);
decrypted_flag = decrypt_flag(encrypted_flag, 0x30u);
DbgPrint((PCH)decrypted_flag);
LABEL_17:
pIRP->IoStatus.Status = 0;
pIRP->IoStatus.Information = 48i64;
IofCompleteRequest(pIRP, 0);
return ret_code;
}
Things to note:
- The IOCTLs called by
GetHash1
andGetHash2
which each calculate a hash. Some reversing can reveal that:- A DWORD long xor-key is stored under the registry key
AliceInWonderlandTeaParty
namedCareForTea
. - The xor-key is used to decrypt a global of length 32, a different global for each IOCTL. The encrypted globals are:
39D32A983EDF7AC36EDA25C03F8F79993DDE2B99328D78C73CDE7FC26ADE79C0
32892DC73AD37EC2328E29943DD27AC33ADD2AC039D97FC03D8E7E9238DC7D95
- A DWORD long xor-key is stored under the registry key
- The two
GetFlag
IOCTLs:- The first:
- Gets the passcode and stores it in a global.
- 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.
- The first:
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 )
BCryptDestroyKey();
if ( pKey )
ExFreePoolWithTag(pKey, 0xAABBCCDD);
if ( pIV2 )
ExFreePoolWithTag(pIV2, 0xAABBCCDD);
return plaintext;
}
- By the call to
BCryptSetProperty
key length is 32 it's value is probably the zero-padded passcode:- KEY is
77686f7468656675636b6973616c696365000000000000000000000000000000
.
- KEY is
- 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
.
- CT is
- By the last argument to BCryptDecrypt the plaintext includes padding.
- Putting all of this into CyberChef we get the flag
INTENT{0ff_w1th_7h31r_H34ds}
.