Skip to content

Latest commit

 

History

History

Lesson_Password_2

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

In the last lesson we've investigated the VFR password element and developed a code that was saving the password value as a plain text in the varstore storage. But in reality you shouldn't store passwords as it is in your storage! This is a security vulnerability. Instead you should at least store a hash value of the password. So even if the database is compromised, the attacker can't reverse the original password value.

Here is a simple example. If our form has only the password element it wouldn't be present in the HIIConfig dump output. But when I've added a checkbox element to the form, the HIIConfig dump printed all the values inside the varstorage including the current value for the password field:

GUID=3cd7f2e79a69064692b6a35e4927c4d4 (E7F2D73C-699A-4606-92B6-A35E4927C4D4)
NAME=0046006f0072006d0044006100740061 (FormData)
PATH=010414009fcf89b211f9fd419bad2c92576886647fff0400 (VenHw(B289CF9F-F911-41FD-9BAD-2C9257688664))
OFFSET=0  WIDTH=0000000000000013  VALUE=0100000074006500720063006500730079006d
6D 00 79 00 73 00 65 00 63 00 72 00 65 00 74 00  | m.y.s.e.c.r.e.t.
00 00 01                                         | ...

GUID=3cd7f2e79a69064692b6a35e4927c4d4 (E7F2D73C-699A-4606-92B6-A35E4927C4D4)
NAME=0046006f0072006d0044006100740061 (FormData)
PATH=010414009fcf89b211f9fd419bad2c92576886647fff0400 (VenHw(B289CF9F-F911-41FD-9BAD-2C9257688664))
ALTCFG=0000
OFFSET=0012  WIDTH=0001  VALUE=01
01                                               | .

GUID=3cd7f2e79a69064692b6a35e4927c4d4 (E7F2D73C-699A-4606-92B6-A35E4927C4D4)
NAME=0046006f0072006d0044006100740061 (FormData)
PATH=010414009fcf89b211f9fd419bad2c92576886647fff0400 (VenHw(B289CF9F-F911-41FD-9BAD-2C9257688664))
ALTCFG=0001
OFFSET=0012  WIDTH=0001  VALUE=01
01                                               | .

This is clearly no good! This is a potential loophole for the attaker to get our password. Not sure if this is a vulnerability in the edk2 code, but at least we can protect the initial password by storing it not as a simple string, but as something like SHA512 hash value of the password string.

Let's investigate how we can do this.

For the hash calculation UEFI specification defines the EFI_HASH2_PROTOCOL protocol. Before diving into its specification, let's understand how to get it.

This protocol is not supposed to be acquired via the usual LocateProtocol routine. It is supposed to be acquired via the standard EFI service binding scheme. To get the EFI_HASH2_PROTOCOL you need to call CreateChild() on the EFI_HASH2_SERVICE_BINDING_PROTOCOL and perform HandleProtocol() on the returned handle. To clear the resources on driver unload we need to call the DestroyChild respectively.

With the service binding scheme multiple drivers can use the hashing services. Or even one driver can have multiple simultaneous non-overlapping hash operations.

So let's try to get the EFI_HASH2_PROTOCOL protocol:

<...>

#include <Protocol/Hash2.h>
#include <Protocol/ServiceBinding.h>

<...>

EFI_SERVICE_BINDING_PROTOCOL* hash2ServiceBinding;
EFI_HASH2_PROTOCOL* hash2Protocol;
EFI_HANDLE hash2ChildHandle = NULL;

<...>

EFI_STATUS
EFIAPI
PasswordFormWithHashUnload (
  EFI_HANDLE ImageHandle
  )
{
  hash2ServiceBinding->DestroyChild(hash2ServiceBinding, hash2ChildHandle);

  <...>
}

EFI_STATUS
EFIAPI
PasswordFormWithHashEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS Status;
  Status = gBS->LocateProtocol(&gEfiHash2ServiceBindingProtocolGuid,
                               NULL,
                               (VOID **)&hash2ServiceBinding);
  if (EFI_ERROR(Status)) {
    Print(L"Error! Can't locate gEfiHash2ServiceBindingProtocolGuid: %r\n", Status);
    return Status;
  }

  Status = hash2ServiceBinding->CreateChild(hash2ServiceBinding,
                                            &hash2ChildHandle);
  if (EFI_ERROR(Status)) {
    Print(L"Error! Can't create child on gEfiHash2ServiceBindingProtocolGuid: %r\n", Status);
    return Status;
  }

  Status = gBS->OpenProtocol(hash2ChildHandle,
                             &gEfiHash2ProtocolGuid,
                             (VOID **)&hash2Protocol,
                             NULL,
                             hash2ChildHandle,
                             EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
  if (EFI_ERROR(Status)) {
    Print(L"Error! Can't open gEfiHashProtocolGuid: %r\n", Status);
    return Status;
  }

  <...>
}

Don't forget to add the necessary protocols to the INF file:

[Protocols]
  ...
  gEfiHash2ServiceBindingProtocolGuid
  gEfiHash2ProtocolGuid

Now to the actual EFI_HASH2_PROTOCOL itself:

EFI_HASH2_PROTOCOL

Summary:
This protocol describes hashing functions for which the algorithm-required message padding and finalization are performed by the supporting driver. In previous versions of the specification, the algorithms supported by EFI_HASH2_PROTOCOL were also available for use with EFI_HASH_PROTOCOL but this usage has been deprecated.

GUID:
#define EFI_HASH2_PROTOCOL_GUID { 0x55b1d734, 0xc5e1, 0x49db, 0x96, 0x47, 0xb1, 0x6a, 0xfb, 0xe, 0x30, 0x5b}

typedef _EFI_HASH2_PROTOCOL {
 EFI_HASH2_GET_HASH_SIZE GetHashSize;
 EFI_HASH2_HASH Hash;
 EFI_HASH2_HASH_INIT HashInit;
 EFI_HASH2_HASH_UPDATE HashUpdate;
 EFI_HASH2_HASH_FINAL HashFinal;
} EFI_HASH2_PROTOCOL;

Parameters:
GetHashSize  Return the result size of a specific type of resulting hash.
Hash         Create a final non-extendable hash for a single message block in a single call.
HashInit     Initializes an extendable multi-part hash calculation
HashUpdate   Continues a hash in progress by supplying the first or next sequential portion of the message text
HashFinal    Finalizes a hash in progress by padding as required by algorithm and returning the hash output.

In the edk2 this protocol is defined in the header [MdePkg/Include/Protocol/Hash2.h](file https://github.com/tianocore/edk2/blob/master/MdePkg/Include/Protocol/Hash2.h).

In our simple case we would use the EFI_HASH2_HASH Hash function.

If you want to know about the HashInit/HashUpdate/HashFinal functions, I think it is better to illustrate their work on a simple example. The 3 cases below would give you identical hash results:

Hash("ABCDEF") | HashInit()           | HashInit ()
               | HashUpdate("ABCDEF") | HashUpdate ("ABC")
               | HashFinal()          | HashUpdate ("DE")
               |                      | HashUpdate ("F")
               |                      | HashFinal ()

I hope this is pretty self-explanatory.

Now let's see the specification for the EFI_HASH2_HASH Hash function that we would use:

EFI_HASH2_PROTOCOL.Hash()

Summary:
Creates a hash for a single message text. The hash is not extendable. The output is final with any algorithm-required
padding added by the function.

Prototype:

EFI_STATUS
EFIAPI
Hash(
  IN CONST EFI_HASH2_PROTOCOL *This,
  IN CONST EFI_GUID *HashAlgorithm,
  IN CONST UINT8 *Message,
  IN UINTN MessageSize,
  IN OUT EFI_HASH2_OUTPUT *Hash
);

Parameters:
This           Points to this instance of EFI_HASH2_PROTOCOL
HashAlgorithm  Points to the EFI_GUID which identifies the algorithm to use
Message        Points to the start of the message
MessageSize    The size of Message, in bytes
Hash           On input, points to a caller-allocated buffer of the size returned by GetHashSize() for the specified HashAlgorithm
               On output, the buffer holds the resulting hash computed from the message

Description:
This function creates the hash of specified single block message text based on the specified algorithm HashAlgorithm and copies the result to the caller-provided buffer Hash. The resulting hash cannot be extended. All padding required by HashAlgorithm is added by the implementation.

The possible hash algorithms and their respected GUIDs are present below:

SHA-1:  EFI_HASH_ALGORITHM_SHA1_GUID
SHA224: EFI_HASH_ALGORITHM_SHA224_GUID
SHA256: EFI_HASH_ALGORITHM_SHA256_GUID
SHA384: EFI_HASH_ALGORITHM_SHA384_GUID
SHA512: EFI_HASH_ALGORITHM_SHA512_GUID
MD5:    EFI_HASH_ALGORTIHM_MD5_GUID

We would use the EFI_HASH_ALGORITHM_SHA512_GUID as the strongest of the above in our hash calculation.

The EFI_HASH2_PROTOCOL.Hash() returns its output in the EFI_HASH2_OUTPUT *Hash argument. The EFI_HASH2_OUTPUT type is a enum defined like this:

typedef UINT8 EFI_MD5_HASH2[16];
typedef UINT8 EFI_SHA1_HASH2[20];
typedef UINT8 EFI_SHA224_HASH2[28];
typedef UINT8 EFI_SHA256_HASH2[32];
typedef UINT8 EFI_SHA384_HASH2[48];
typedef UINT8 EFI_SHA512_HASH2[64];

typedef union {
  EFI_MD5_HASH2       Md5Hash;
  EFI_SHA1_HASH2      Sha1Hash;
  EFI_SHA224_HASH2    Sha224Hash;
  EFI_SHA256_HASH2    Sha256Hash;
  EFI_SHA384_HASH2    Sha384Hash;
  EFI_SHA512_HASH2    Sha512Hash;
} EFI_HASH2_OUTPUT;

In our form storage we need to create appropriate field to store the password hash. Since we need to know the size of the storage at compile time and our hash algorithm is fixed, instead of using the protocol EFI_HASH2_GET_HASH_SIZE GetHashSize we would just define it as a static number. From the description above you can see that the size of the SHA512 hash is 64:

#define HASHED_PASSWORD_SIZE   64

#pragma pack(1)
typedef struct {
  UINT8 Password[HASHED_PASSWORD_SIZE];
} VARIABLE_STRUCTURE;
#pragma pack()

Here you can also observe that now Password field is not even a CHAR16 string, but a UINT8 [].

Now we are ready to create a ComputeStringHash function that would calculate a hash from the EFI_STRING:

EFI_STATUS ComputeStringHash(EFI_STRING Password, UINT8* HashedPassword)
{
  EFI_GUID HashGuid = EFI_HASH_ALGORITHM_SHA512_GUID;
  EFI_HASH2_OUTPUT Hash;
  EFI_STATUS Status = hash2Protocol->Hash(hash2Protocol,
                                          &HashGuid,
                                          (UINT8*)Password,
                                          StrLen(Password)*sizeof(CHAR16),
                                          &Hash);
  if (EFI_ERROR(Status)) {
    return Status;
  }
  CopyMem(HashedPassword, Hash.Sha512Hash, HASHED_PASSWORD_SIZE);
  return EFI_SUCCESS;
}

With it we can update our HandlePasswordInput function to store hashes instead of plain passwords:

EFI_STATUS HandlePasswordInput(EFI_STRING Password)
{
  EFI_STATUS Status;

  if (Password[0] == 0) {
    // Form Browser checks if password exists
    if (FormStorage.Password[0] != 0) {
      return EFI_ALREADY_STARTED;
    } else {
      return EFI_SUCCESS;
    }
  } else {
    // Form Browser sends password value
    // It can be old password to check or initial/updated password to set

    if (FormStorage.Password[0] == 0) {
      // Set initial password
      ComputeStringHash(Password, FormStorage.Password);
      if (EFI_ERROR(Status)) {
        return Status;
      }
      return EFI_SUCCESS;
    }

    if (!OldPasswordVerified) {
      // Check old password
      UINT8 TempHash[HASHED_PASSWORD_SIZE];
      ComputeStringHash(Password, TempHash);
      if (CompareMem(TempHash, FormStorage.Password, HASHED_PASSWORD_SIZE))
        return EFI_NOT_READY;

      OldPasswordVerified = TRUE;
      return EFI_SUCCESS;
    }

    // Update password
    Status = ComputeStringHash(Password, FormStorage.Password);
    if (EFI_ERROR(Status)) {
      return Status;
    }
    OldPasswordVerified = FALSE;
    return EFI_SUCCESS;
  }
}

Now we are ready to build and test our driver.

Unfortunately the driver would fail on load:

FS0:\> load PasswordFormWithHash.efi
Error! Can't locate gEfiHash2ServiceBindingProtocolGuid: Not Found
Image 'FS0:\PasswordFormWithHash.efi' error in StartImage: Not Found

As you can see there is no EFI_HASH2_SERVICE_BINDING_PROTOCOL in the default OVMF build. So let's try to fix it.

The EFI_HASH2_SERVICE_BINDING_PROTOCOL is produced by the Hash2DxeCrypto.

Since its module type UEFI_DRIVER we can just build it:

build --platform=SecurityPkg/SecurityPkg.dsc --module=SecurityPkg/Hash2DxeCrypto/Hash2DxeCrypto.inf --arch=X64 --buildtarget=RELEASE --tagname=GCC5
cp Build/SecurityPkg/RELEASE_GCC5/X64/SecurityPkg/Hash2DxeCrypto/Hash2DxeCrypto/OUTPUT/Hash2DxeCrypto.efi ~/disk

and load in shell:

FS0:\> load Hash2DxeCrypto.efi
Image 'FS0:\Hash2DxeCrypto.efi' loaded at 640D000 - Success

Now our driver PasswordFormWithHash can load without any problems

FS0:\> load PasswordFormWithHash.efi
Image 'FS0:\PasswordFormWithHash.efi' loaded at 63D8000 - Success

You can test that the form works with the password element the same as before.

The difference is not visible to the user, but now if we repeat the exploit presented at the start of this lesson, this is what we would get:

GUID=3cd7f2e79a69064692b6a35e4927c4d4 (E7F2D73C-699A-4606-92B6-A35E4927C4D4)
NAME=0046006f0072006d0044006100740061 (FormData)
PATH=010414009fcf89b211f9fd419bad2c92576886647fff0400 (VenHw(B289CF9F-F911-41FD-9BAD-2C9257688664))
OFFSET=0  WIDTH=0000000000000041  VALUE=01a71c52e40993cb53ae8fc16c3ad3c0596cc03b67a225bef0effa0f43e9f362921ce17b26f9cf7ad6ed487fa955438206d0f211bffea4e9d216eb2b0d2133882f
2F 88 33 21 0D 2B EB 16 D2 E9 A4 FE BF 11 F2 D0  | /.3!.+..........
06 82 43 55 A9 7F 48 ED D6 7A CF F9 26 7B E1 1C  | ..CU..H..z..&{..
92 62 F3 E9 43 0F FA EF F0 BE 25 A2 67 3B C0 6C  | .b..C.....%.g;.l
59 C0 D3 3A 6C C1 8F AE 53 CB 93 09 E4 52 1C A7  | Y..:l...S....R..
01                                               | .

GUID=3cd7f2e79a69064692b6a35e4927c4d4 (E7F2D73C-699A-4606-92B6-A35E4927C4D4)
NAME=0046006f0072006d0044006100740061 (FormData)
PATH=010414009fcf89b211f9fd419bad2c92576886647fff0400 (VenHw(B289CF9F-F911-41FD-9BAD-2C9257688664))
ALTCFG=0000
OFFSET=0040  WIDTH=0001  VALUE=01
01                                               | .

GUID=3cd7f2e79a69064692b6a35e4927c4d4 (E7F2D73C-699A-4606-92B6-A35E4927C4D4)
NAME=0046006f0072006d0044006100740061 (FormData)
PATH=010414009fcf89b211f9fd419bad2c92576886647fff0400 (VenHw(B289CF9F-F911-41FD-9BAD-2C9257688664))
ALTCFG=0001
OFFSET=0040  WIDTH=0001  VALUE=01
01                                               | .

As you can see now the form storage contains not a password itself, but a hash value of the password. This way even if the storage is compromised the potential attaker still wouldn't have the original password value.

Compiling Hash2DxeCrypto to the OVMF

As you saw earlier by default the OVMF doesn't populate the EFI_HASH2_SERVICE_BINDING_PROTOCOL. To get it we needed to manually load the Hash2DxeCrypto.efi driver.

Alternatively we can include the Hash2DxeCrypto to the OVMF build, so it would execute in the DXE stage every time on the machine load.

For that we need to add it to the DSC (OvmfPkg/OvmfPkgX64.dsc):

[Components]

<...>

SecurityPkg/Hash2DxeCrypto/Hash2DxeCrypto.inf

And to the FDF (OvmfPkg/OvmfPkgX64.fdf):

[FV.DXEFV]

<...>

INF SecurityPkg/Hash2DxeCrypto/Hash2DxeCrypto.inf

And recompile the OVMF image after the changes:

build --platform=OvmfPkg/OvmfPkgX64.dsc --arch=X64 --buildtarget=RELEASE --tagname=GCC5

Now you should be able to load our PasswordFormWithHash application right after the shell load.

Alternatives to the EFI_HASH2_PROTOCOL

Instead of using the EFI_HASH2_PROTOCOL you can just use the hash functions from the BaseCryptLib library https://github.com/tianocore/edk2/blob/master/CryptoPkg/Include/Library/BaseCryptLib.h.

For example here is a description for the Sha512HashAll function:

/**
  Computes the SHA-512 message digest of a input data buffer.

  This function performs the SHA-512 message digest of a given data buffer, and places
  the digest value into the specified memory.

  If this interface is not supported, then return FALSE.

  @param[in]   Data        Pointer to the buffer containing the data to be hashed.
  @param[in]   DataSize    Size of Data buffer in bytes.
  @param[out]  HashValue   Pointer to a buffer that receives the SHA-512 digest
                           value (64 bytes).

  @retval TRUE   SHA-512 digest computation succeeded.
  @retval FALSE  SHA-512 digest computation failed.
  @retval FALSE  This interface is not supported.

**/
BOOLEAN
EFIAPI
Sha512HashAll (
  IN   CONST VOID  *Data,
  IN   UINTN       DataSize,
  OUT  UINT8       *HashValue
  );

We can just use it in our calculations simple as this:

#include <Library/BaseCryptLib.h>

...

UINT8 Hash[SHA512_DIGEST_SIZE];
BOOLEAN Status = Sha512HashAll(Password, StrLen(Password) * sizeof(CHAR16), Hash);
if (!Status) {
  return EFI_DEVICE_ERROR;
}
return EFI_SUCCESS;

The downside of this approach is that this will definitely blow the driver image size. When we use the function from the protocol, in the nutshell we just find some memory address and cast it to a target function. So no actual impementation code of the function is present in our driver.

On the other case when you use the library approach, all the function implementation code would be encoded in our driver at compile-time. This would bloat the image, but will make the driver self-sufficient.

Right now edk2 is in the process of refactoring code to the usage of EFI_HASH2_PROTOCOL. Most of the code that you would find now is based on the library approach. But nevertheless a more correct approach is to move towards the protocol solution.