Mailing List Archive

[PATCH 1/2] fips: Use ELF header to find .rodata1 section
* src/fips.c [ENABLE_HMAC_BINARY_CHECK] (hmac256_check): Use ELF headers
to locate the binary section with the HMAC rather than information
from the loader

--

The previous method of locating the offset of the .rodata1 section in
the ELF file on disk used information obtained from the loader. This
computed the address of the section at runtime (sh_addr in the ElfN_Shdr
struct), but the offset in the file can be different (sh_offset from the
ElfN_Shdr struct).

So far, GCC did include .rodata1 close enough to the beginning of the
section table so that sh_addr and sh_offset were the same. If there were
short sections followed by sections with larger alignment requirements
in the binary, this would have caused sh_addr and sh_offset to differ,
and thus the selfcheck to fail.

To allow clang to pass the FIPS selftest, a follow-up commit will mark
the HMAC as volatile. This causes GCC to move the section closer to the
end of the section table, where sh_addr and sh_offset differ.

Switch to determining the location of the .rodata1 section from the ELF
headers in the file on disk to be robust against future section
re-ordering changes in compilers.

Signed-off-by: Clemens Lang <cllang@redhat.com>
---
README | 3 +-
src/fips.c | 153 +++++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 146 insertions(+), 10 deletions(-)

diff --git a/README b/README
index 3b465c1b..4d7697dd 100644
--- a/README
+++ b/README
@@ -157,7 +157,8 @@
--enable-hmac-binary-check
Include support to check the binary at runtime
against a HMAC checksum. This works only in FIPS
- mode and on systems providing the dladdr function.
+ mode on systems providing the dladdr function and using
+ the ELF binary format.

--with-fips-module-version=version
Specify a string used as a module version for FIPS
diff --git a/src/fips.c b/src/fips.c
index 391b94f1..193af36b 100644
--- a/src/fips.c
+++ b/src/fips.c
@@ -24,8 +24,9 @@
#include <unistd.h>
#include <string.h>
#ifdef ENABLE_HMAC_BINARY_CHECK
+# include <limits.h>
# include <dlfcn.h>
-# include <link.h>
+# include <elf.h>
#endif
#ifdef HAVE_SYSLOG
# include <syslog.h>
@@ -594,23 +595,159 @@ run_random_selftests (void)
static const unsigned char __attribute__ ((section (".rodata1")))
hmac_for_the_implementation[HMAC_LEN];

+/**
+ * Obtain the ElfN_Shdr.sh_offset value for the section with the given name in
+ * the ELF file opened as fp and return it in offset. Rewinds fp to the
+ * beginning on success.
+ */
static gpg_error_t
-hmac256_check (const char *filename, const char *key, struct link_map *lm)
+get_section_offset (FILE *fp, const char *section, unsigned long *offset)
+{
+ unsigned char e_ident[EI_NIDENT];
+#if __WORDSIZE == 64
+ Elf64_Ehdr ehdr;
+ Elf64_Shdr shdr;
+#define ELFCLASS_NATIVE ELFCLASS64
+#else
+ Elf32_Ehdr ehdr;
+ Elf32_Shdr shdr;
+#define ELFCLASS_NATIVE ELFCLASS32
+#endif
+ char *shstrtab;
+ uint16_t e_shnum;
+ uint16_t e_shstrndx;
+ uint16_t e_shidx;
+
+ // verify binary word size
+ if (1 != fread (e_ident, EI_NIDENT, 1, fp))
+ return gpg_error_from_syserror ();
+
+ if (ELFCLASS_NATIVE != e_ident[EI_CLASS])
+ return gpg_error (GPG_ERR_INV_OBJ);
+
+ // read the ELF header
+ if (0 != fseek (fp, 0, SEEK_SET))
+ return gpg_error_from_syserror ();
+ if (1 != fread (&ehdr, sizeof (ehdr), 1, fp))
+ return gpg_error_from_syserror ();
+
+ // the section header entry size should match the size of the shdr struct
+ if (ehdr.e_shentsize != sizeof (shdr))
+ return gpg_error (GPG_ERR_INV_OBJ);
+
+ /* elf(5): "If the file has no section name string table, this member holds
+ * the value SHN_UNDEF." Without a section name string table, we can not
+ * locate a named section, so error out. */
+ if (ehdr.e_shstrndx == SHN_UNDEF)
+ return gpg_error (GPG_ERR_INV_OBJ);
+
+ // jump to and read the first section header
+ if (0 != fseek (fp, ehdr.e_shoff, SEEK_SET))
+ return gpg_error_from_syserror ();
+ if (1 != fread (&shdr, sizeof (shdr), 1, fp))
+ return gpg_error_from_syserror ();
+
+ /* Number of entries in the section header table
+ *
+ * If the number of entries in the section header table is larger than or
+ * equal to SHN_LORESERVE, e_shnum holds the value zero and the real number
+ * of entries in the section header table is held in the sh_size member of
+ * the initial entry in section header table.
+ */
+ e_shnum = ehdr.e_shnum == 0 ? shdr.sh_size : ehdr.e_shnum;
+ /* Section header table index of the section name string table
+ *
+ * If the index of section name string table section is larger than or equal
+ * to SHN_LORESERVE, this member holds SHN_XINDEX and the real index of the
+ * section name string table section is held in the sh_link member of the
+ * initial entry in section header table.
+ */
+ e_shstrndx = ehdr.e_shstrndx == SHN_XINDEX ? shdr.sh_link : ehdr.e_shstrndx;
+
+ // seek to the section header for the section name string table and read it
+ if (0 != fseek (fp, ehdr.e_shoff + e_shstrndx * sizeof (shdr), SEEK_SET))
+ return gpg_error_from_syserror ();
+ if (1 != fread (&shdr, sizeof (shdr), 1, fp))
+ return gpg_error_from_syserror ();
+
+ // this is the section name string table and should have a type of SHT_STRTAB
+ if (shdr.sh_type != SHT_STRTAB)
+ return gpg_error (GPG_ERR_INV_OBJ);
+
+ // read the string table
+ shstrtab = xtrymalloc (shdr.sh_size);
+ if (!shstrtab)
+ return gpg_error_from_syserror ();
+
+ if (0 != fseek (fp, shdr.sh_offset, SEEK_SET))
+ {
+ gpg_error_t err = gpg_error_from_syserror ();
+ xfree (shstrtab);
+ return err;
+ }
+ if (1 != fread (shstrtab, shdr.sh_size, 1, fp))
+ {
+ gpg_error_t err = gpg_error_from_syserror ();
+ xfree (shstrtab);
+ return err;
+ }
+
+ // iterate over the sections, compare their names, and if the section name
+ // matches the expected name, return the offset. We already read section 0,
+ // which is always an empty section.
+ if (0 != fseek (fp, ehdr.e_shoff + 1 * sizeof (shdr), SEEK_SET))
+ {
+ gpg_error_t err = gpg_error_from_syserror ();
+ xfree (shstrtab);
+ return err;
+ }
+ for (e_shidx = 1; e_shidx < e_shnum; e_shidx++)
+ {
+ if (1 != fread (&shdr, sizeof (shdr), 1, fp))
+ {
+ gpg_error_t err = gpg_error_from_syserror ();
+ xfree (shstrtab);
+ return err;
+ }
+ if (0 == strcmp (shstrtab + shdr.sh_name, section))
+ {
+ // found section, return the offset
+ *offset = shdr.sh_offset;
+ xfree (shstrtab);
+
+ if (0 != fseek (fp, 0, SEEK_SET))
+ return gpg_error_from_syserror ();
+ return 0;
+ }
+ }
+
+ // section not found in the file
+ xfree (shstrtab);
+ return gpg_error (GPG_ERR_INV_OBJ);
+}
+
+static gpg_error_t
+hmac256_check (const char *filename, const char *key)
{
gpg_error_t err;
FILE *fp;
gcry_md_hd_t hd;
size_t buffer_size, nread;
char *buffer;
- unsigned long paddr;
+ unsigned long paddr = 0;
unsigned long off = 0;

- paddr = (unsigned long)hmac_for_the_implementation - lm->l_addr;
-
fp = fopen (filename, "rb");
if (!fp)
return gpg_error (GPG_ERR_INV_OBJ);

+ err = get_section_offset (fp, ".rodata1", &paddr);
+ if (err)
+ {
+ fclose (fp);
+ return err;
+ }
+
err = _gcry_md_open (&hd, GCRY_MD_SHA256, GCRY_MD_FLAG_HMAC);
if (err)
{
@@ -692,13 +829,11 @@ check_binary_integrity (void)
gpg_error_t err;
Dl_info info;
const char *key = KEY_FOR_BINARY_CHECK;
- void *extra_info;

- if (!dladdr1 (hmac_for_the_implementation,
- &info, &extra_info, RTLD_DL_LINKMAP))
+ if (!dladdr (hmac_for_the_implementation, &info))
err = gpg_error_from_syserror ();
else
- err = hmac256_check (info.dli_fname, key, extra_info);
+ err = hmac256_check (info.dli_fname, key);

reporter ("binary", 0, NULL, err? gpg_strerror (err):NULL);
#ifdef HAVE_SYSLOG
--
2.34.1


_______________________________________________
Gcrypt-devel mailing list
Gcrypt-devel@lists.gnupg.org
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel
Re: [PATCH 1/2] fips: Use ELF header to find .rodata1 section [ In reply to ]
* Clemens Lang via Gcrypt-devel:

> diff --git a/src/fips.c b/src/fips.c
> index 193af36b..fabc5158 100644
> --- a/src/fips.c
> +++ b/src/fips.c
> @@ -592,7 +592,10 @@ run_random_selftests (void)
> # endif
> #define HMAC_LEN 32
>
> -static const unsigned char __attribute__ ((section (".rodata1")))
> +/* Compilers can and will constant-propagate this as 0 when reading if it is
> + * not declared volatile. Since this value will be changed using objcopy(1)
> + * after compilation, this can cause the HMAC verification to fail. */
> +static const volatile unsigned char __attribute__ ((section (".rodata1")))
> hmac_for_the_implementation[HMAC_LEN];

volatile causes GCC to emit a writable section, and the link editor will
make .rodata1 (and typically .text) writable as a result. This is a
fairly significant loss of security hardening.

This bug is relevant here:

various services trigger { execmem } denials in FIPS mode
<https://bugzilla.redhat.com/show_bug.cgi?id=2034320>

> +/**
> + * Obtain the ElfN_Shdr.sh_offset value for the section with the given name in
> + * the ELF file opened as fp and return it in offset. Rewinds fp to the
> + * beginning on success.
> + */
> static gpg_error_t
> -hmac256_check (const char *filename, const char *key, struct link_map *lm)
> +get_section_offset (FILE *fp, const char *section, unsigned long *offset)
> +{
> + unsigned char e_ident[EI_NIDENT];
> +#if __WORDSIZE == 64
> + Elf64_Ehdr ehdr;
> + Elf64_Shdr shdr;
> +#define ELFCLASS_NATIVE ELFCLASS64

__WORDSIZE is an internal glibc macro, not to be used outside of glibc.
glibc's <link.h> defines ElfW as an official macro, and you could use
ElfW(Ehdr) and ElfW(Shdr) here.

The code looks at section headers. These can be stripped. Furthermore,
the .rodata1 section is not really reserved for application use.

I haven't reviewed Dmitry's OpenSSL changes (which I probably should
do), but I'd suggest to use the same approach. 8-)

Thanks,
Florian


_______________________________________________
Gcrypt-devel mailing list
Gcrypt-devel@lists.gnupg.org
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel
Re: [PATCH 1/2] fips: Use ELF header to find .rodata1 section [ In reply to ]
Dear Florian,

On Fri, Feb 11, 2022 at 5:09 PM Florian Weimer <fweimer@redhat.com> wrote:

> * Clemens Lang via Gcrypt-devel:
>
> > diff --git a/src/fips.c b/src/fips.c
> > index 193af36b..fabc5158 100644
> > --- a/src/fips.c
> > +++ b/src/fips.c
> > @@ -592,7 +592,10 @@ run_random_selftests (void)
> > # endif
> > #define HMAC_LEN 32
> >
> > -static const unsigned char __attribute__ ((section (".rodata1")))
> > +/* Compilers can and will constant-propagate this as 0 when reading if
> it is
> > + * not declared volatile. Since this value will be changed using
> objcopy(1)
> > + * after compilation, this can cause the HMAC verification to fail. */
> > +static const volatile unsigned char __attribute__ ((section
> (".rodata1")))
> > hmac_for_the_implementation[HMAC_LEN];
>
> volatile causes GCC to emit a writable section, and the link editor will
> make .rodata1 (and typically .text) writable as a result. This is a
> fairly significant loss of security hardening.
>
> This bug is relevant here:
>
> various services trigger { execmem } denials in FIPS mode
> <https://bugzilla.redhat.com/show_bug.cgi?id=2034320>
>
> > +/**
> > + * Obtain the ElfN_Shdr.sh_offset value for the section with the given
> name in
> > + * the ELF file opened as fp and return it in offset. Rewinds fp to the
> > + * beginning on success.
> > + */
> > static gpg_error_t
> > -hmac256_check (const char *filename, const char *key, struct link_map
> *lm)
> > +get_section_offset (FILE *fp, const char *section, unsigned long
> *offset)
> > +{
> > + unsigned char e_ident[EI_NIDENT];
> > +#if __WORDSIZE == 64
> > + Elf64_Ehdr ehdr;
> > + Elf64_Shdr shdr;
> > +#define ELFCLASS_NATIVE ELFCLASS64
>
> __WORDSIZE is an internal glibc macro, not to be used outside of glibc.
> glibc's <link.h> defines ElfW as an official macro, and you could use
> ElfW(Ehdr) and ElfW(Shdr) here.
>
> The code looks at section headers. These can be stripped. Furthermore,
> the .rodata1 section is not really reserved for application use.
>
> I haven't reviewed Dmitry's OpenSSL changes (which I probably should
> do), but I'd suggest to use the same approach. 8-)
>

Yes, I used the same approach. But the situation is a bit more strange.

I had to add a `volatile` modifier to the HMAC variable because the
.section attribute was ignored otherwise.
After the issue you refer to was raised, I removed this modifier - and the
section was preserved.


--
Dmitry Belyavskiy
Re: [PATCH 1/2] fips: Use ELF header to find .rodata1 section [ In reply to ]
Hi Florian,

> On 11. Feb 2022, at 17:09, Florian Weimer <fweimer@redhat.com> wrote:
>
> __WORDSIZE is an internal glibc macro, not to be used outside of glibc.
> glibc's <link.h> defines ElfW as an official macro, and you could use
> ElfW(Ehdr) and ElfW(Shdr) here.

Thanks, I’ll fix that.


> The code looks at section headers. These can be stripped. Furthermore,
> the .rodata1 section is not really reserved for application use.
>
> I haven't reviewed Dmitry's OpenSSL changes (which I probably should
> do), but I'd suggest to use the same approach. 8-)

From what I can see, it currently uses the same approach, and probably
has the same issue where the compiler could assume that the HMAC is 0
and constant-propagate that. Again, this currently works just fine with
GCC, but I don’t think it’s a good idea to rely on GCC’s unwillingness
to replace a memcmp(3) with a few assembly instructions.

Adding volatile was the simplest method I could think of to prevent that,
but you make good points that it might not be the best approach here.
I’ll try your suggestion from [1], which I guess would work because the
variable isn’t const.

The currently merged state assumes the offset in the file matches the
address at runtime. This is probably not a good assumption to make. How
would you determine the offset of a symbol in a file given its runtime
address? Find the matching program header entry that must have loaded it
and subtracting the difference between p_vaddr and p_offset?

As for stripping the section headers, GNU strip 2.37 does not seem to do
that in the default configuration, so we could just expect users that
want the FIPS selftest to not manually strip them.

[1] https://bugzilla.redhat.com/show_bug.cgi?id=2034320


Thanks,
Clemens
--
Clemens Lang
RHEL Crypto Team
Red Hat




_______________________________________________
Gcrypt-devel mailing list
Gcrypt-devel@lists.gnupg.org
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel
Re: [PATCH 1/2] fips: Use ELF header to find .rodata1 section [ In reply to ]
* Clemens Lang:

> From what I can see, it currently uses the same approach, and probably
> has the same issue where the compiler could assume that the HMAC is 0
> and constant-propagate that. Again, this currently works just fine with
> GCC, but I don’t think it’s a good idea to rely on GCC’s unwillingness
> to replace a memcmp(3) with a few assembly instructions.

Maybe GCC should provide an explicit way to treat a data object as
constant for linking purposes, while having a compiler barrier around
that.

Separate compilation does that, but the data object must not be compiled
with LTO, and that probably any whole-world assumption that makes LTO so
successful.

> The currently merged state assumes the offset in the file matches the
> address at runtime. This is probably not a good assumption to make. How
> would you determine the offset of a symbol in a file given its runtime
> address? Find the matching program header entry that must have loaded it
> and subtracting the difference between p_vaddr and p_offset?

Yes, I think that should work.

Thanks,
Florian


_______________________________________________
Gcrypt-devel mailing list
Gcrypt-devel@lists.gnupg.org
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel