slimm609 / checksec.sh

Checksec.sh
https://slimm609.github.io/checksec.sh/
Other
1.99k stars 299 forks source link

checksec 2.7.0 FORTIFY detection #235

Closed teoberi closed 4 months ago

teoberi commented 4 months ago

checksec 2.6.0 ./checksec.old --file=/usr/bin/ssh

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols        Yes   10              21              /usr/bin/ssh

checksec 2.7.0 ./checksec --file=/usr/bin/ssh

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      No      10              21              /usr/bin/ssh

In checksec 2.7.0 FORTIFY=No appears for all tests in which version 2.6.0 has FORTIFY=Yes. For example /usr/bin/ssh

Clarified changes in the code!

teoberi commented 4 months ago

It's a bit confusing, let me elaborate: Case 1: Fortifiable=0 (also valid if Fortified=0) -> FORTIFY=N/A but https://github.com/slimm609/checksec.sh/blob/1507d4a5cbb23a5a693567446b72918671b54b4c/src/functions/filecheck.sh#L141-L142 set FORTIFY=Yes Case 2: Fortified greater than zero but less than Fortifiable so partial (analogous to Partial/Full RELRO) -> FORTIFY=Partial Changed src/functions/filecheck.sh to:

  if [[ "${FS_cnt_total}" == "0" ]]; then
    echo_message "\033[32mN/A\033[m" "N/A," ' fortify_source="n/a" ' '"fortify_source":"n/a",'
  else
    if [[ $FS_cnt_checked -eq $FS_cnt_total ]]; then
      echo_message '\033[32mYes\033[m' 'Yes,' ' fortify_source="yes" ' '"fortify_source":"yes",'
    else
      if [[ "${FS_cnt_checked}" == "0" ]]; then
        echo_message "\033[31mNo\033[m" "No," ' fortify_source="no" ' '"fortify_source":"no",'
      else
        echo_message "\033[33mPartial\033[m" "Partial," ' fortify_source="partial" ' '"fortify_source":"partial",'
      fi
    fi
  fi

Result:

checksec --file=/tmp/test/hello

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   38 Symbols        N/A   0               0               /tmp/test/hello

checksec --file=/tmp/test/hello1f

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   38 Symbols        Yes   2               2               /tmp/test/hello1f

checksec --file=/tmp/test/hello1

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   38 Symbols        No    0               2               /tmp/test/hello1

checksec --file=/usr/bin/ssh

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Partial 10              21              /usr/bin/ssh

I also used pull request #230

petervas commented 4 months ago

Case 2: Fortified greater than zero but less than Fortifiable so partial (analogous to Partial/Full RELRO) -> FORTIFY=Partial

I am not sure if the logic of Partial/Full RELRO applies to FORTIFY_SOURCE.

Partial/Full RELRO is clearly defined. Partial RELRO is using the compiler flag "-z,relro" only. Full RELRO additionally uses the compiler flag "-z,now". (See: https://www.redhat.com/de/blog/hardening-elf-binaries-using-relocation-read-only-relro )

FORTIFY_SOURCE will protect suitable function calls only when the context allows fortification. There is no way to force all functions to be fortified by a compiler flag. So the "partial" output suggests the fortification is not complete. I think this will be misleading. The number in the "Fortifiable" column should be understood as "potentially fortifiable" and not that fortifiability is doable with the right flags.

Even FORTIFY_SOURCE=3 will only increase the coverage of fortified functions but not fortify everything in real world binaries (see: https://developers.redhat.com/articles/2022/09/17/gccs-new-fortification-level )

I think instead developers need to pay attention to the Fortified/Fortifiable numbers to get an idea how well the coverage is. The distinction in FORTIFY between "partial" or "yes" does not seem helpful to me. There is no action one can take to turn partial into a yes. Personally I would just say "yes" if anything has been fortified, because it means FORTIFY_SOURCE has been used. Optionally maybe add a new column "Fortify Coverage" and print the percentage (Fortified/Fortifiable)*100.

@teoberi @slimm609 what do you guys think?

teoberi commented 4 months ago

I thought of the "Partial" case for situations where, for example, the compilation takes place with the -D_FORTIFY_SOURCE=1 flag, although there are also the =2 and =3 variants as a way of warning that things can be improved. To say that Fortify=Yes even though the mode of operation is =1 seems to me to be misleading through false indication. How do you differentiate the Fortify source status if Fortified=Fortifiable versus if they are different. "Partial" may mean that the way Fortify works can be improved or it may not be possible but still not 100%. As an example, the case from here where I try to detail the status for Fortify source in the case of processes. I see "Yes" where there is equality, so Fortify is possible and complete and "Partial" where the two numbers differ due to the operating modes for Fortify or through the design and implementation of the application. I thought of this way when I was applying the recommendations from here in the case of my Slackware Linux distribution and I was using checksec to verify the results. The problem appeared after the checksec update to version 2.7.0, see the post here.

https://www.redhat.com/en/blog/enhance-application-security-fortifysource https://developers.redhat.com/blog/2021/04/16/broadening-compiler-checks-for-buffer-overflows-in-_fortify_source#

petervas commented 3 months ago

I thought of the "Partial" case for situations where, for example, the compilation takes place with the -D_FORTIFY_SOURCE=1 flag, although there are also the =2 and =3 variants as a way of warning that things can be improved.

But the thing with Fortify Source is that you can not really tell apart if 1, 2 or 3 was used by just looking at how many functions were hardened. It is impossible to tell the fortify level apart. The examples where all the possible libc functions are hardened in a binary are mostly true for small toy/example binaries or binaries that only have very few libc calls. The "Yes" and "Partial" results only measure the complexity of the binary in the fortify source case in the end now.

To illustrate that here are the checksec results for Ubuntu 22.04 (compiled with FORTIFY_SOURCE=2) and Ubuntu 24.04 (compiled with FORTIFY_SOURCE=3). checksec_u22.04.csv checksec_u24.04.csv

Ubuntu 22.04

Ubuntu 24.04

As you can see there is "some" improvement by using level 3, but "Partial" remains the top result by far. So as a developer you are now told that you only partially hardened your binary, but you can not do any better than using fortify source 3.

To say that Fortify=Yes even though the mode of operation is =1 seems to me to be misleading through false indication.

I think the "Yes" was always meant as "Fortify Source has been used at all" and not about the fortify level since it can not be measured. To measure the quality of the hardening you have the listing of how many of all possible libc calls were hardened. So i think it is equally misleading to say partial even when fortify 3 was used, if there is nothing the developer can do about it to change it to a 'Yes'. That is not the case for partial RELRO. In that case a compilation flag missing and adding it will change the checksec result to Full RELRO.

@slimm609: Any thoughts on this?

teoberi commented 3 months ago

A good study for _FORTIFY_SOURCE=3 https://developers.redhat.com/articles/2022/09/17/gccs-new-fortification-level#

In this short study, we found that _FORTIFY_SOURCE=3 improved fortification by nearly 4 times. For example, the Bash shell went from roughly 3.4% coverage with _FORTIFY_SOURCE=2 to nearly 47% with _FORTIFY_SOURCE=3. This is an improvement of nearly 14 times. Also, fortification of programs in sudo went from a measly 1.3% to 49.57% — a jump of almost 38 times!

An analysis of the problem exposed in the previous post here: https://github.com/slimm609/checksec.sh/issues/246#issuecomment-2134638135

petervas commented 3 months ago

Just to clarify: I am just trying to understand the need for "Partial" and want to point out the confusion it will cause. It is not about the issue listed here.

A good study for _FORTIFY_SOURCE=3 https://developers.redhat.com/articles/2022/09/17/gccs-new-fortification-level#

Thank you for the link. It describes what i meant to get across quite well. As you can see in the example even with _FORTIFY_SOURCE=3 checksec would currently rate the Bash shell as "Partial" because the coverage is not 100% that would be needed for a "Yes". But please look at the real world numbers between Ubuntu 22.04 (_FORTIFY_SOURCE=2) and Ubuntu 24.04 (_FORTIFY_SOURCE=3) i posted earlier.

An analysis of the problem exposed in the previous post here: #246 (comment)

The linked comment describes exactly the confusion the new "Partial" result will cause. People see 'Partial' and wonder what they should do about it, but there is no solution because they already use _FORTIFY_SOURCE=3.

slimm609 commented 3 months ago

If you are only comparing 2 and 3, then its not the case. Partial is really for _FORTIFY_SOURCE=1. You also can't determine if it was 2 or 3 specifically that was used.

slimm609 commented 3 months ago

the difference between 1 and 2 or 1 and 3 can be determined, the difference between 2 and 3 can't be determined. (at least not that I have been able to find).

petervas commented 3 months ago

Partial is really for _FORTIFY_SOURCE=1. You also can't determine if it was 2 or 3 specifically that was used.

But look at the Ubuntu results i posted. You get "Partial" for most binaries. None of these binaries were compiled with _FORTIFY_SOURCE=1 but with _FORTIFY_SOURCE=2 and _FORTIFY_SOURCE=3 in the newer Ubuntu release.

the difference between 1 and 2 or 1 and 3 can be determined, the difference between 2 and 3 can't be determined. (at least not that I have been able to find).

Ok, then I understand what you are trying to do with "Partial". But i do not think that the current implementation actually does that. To my knowledge _FORTIFY_SOURCE=1, 2 or 3 effectively just increase the coverage on higher levels and you will just see an increase in the "Fortified" Number. I am not aware of any property you can check for that makes it possible to see the difference between _FORTIFY_SOURCE 1 and 2 or 1 and 3.

checksec currently says "Yes" only if coverage is 100%, meaning that everything that is in theory fortifiable has been actually fortified (Fortified==Fortifiable and Fortifiable>0). For any coverage below 100% you get "Partial" (and N/A for 0% coverage). Maybe I am missing something, but do you have a source that describes what property one would need to check for to tell the difference between _FORTIFY_SOURCE 1 and 2?

See https://man7.org/linux/man-pages/man7/feature_test_macros.7.html the section on _FORTIFY_SOURCE:

If _FORTIFY_SOURCE is set to 1, with compiler optimization level 1 (gcc -O1) and above, checks that shouldn't change the behavior of conforming programs are performed. With _FORTIFY_SOURCE set to 2, some more checking is added, but some conforming programs might fail.

Just the amount of checks (the number of detected *_chk functions) increases between 1 and 2. I see no special property that makes them differentiable.

petervas commented 3 months ago

Here is a simple counter example that will always produce "Partial" for _FORTIFY_SOURCE=1, 2 or 3: fortify_partial.zip

Compile with gcc -D_FORTIFY_SOURCE=1 -Wall -g -O2 fortify_partial.c -o fortify_partial and try the different values for _FORTIFY_SOURCE.

When you check with the latest checksec ./checksec --file=fortify_partial you will always get "Partial" for FORTIFY. This is not a problem or bug in checksec though. It just shows that it is not possible to tell the difference between _FORTIFY_SOURCE=1, 2 or 3.

teoberi commented 3 months ago

Partial means everything that is different from Yes or No regardless of the fortification level chosen during compilation. I chose the smallest evil, that is, Partial, even if nothing changes if you change the level of fortification, except to display Yes even if the level of fortification is inadequate.

I am open to any proposal that could clarify this situation, I now see no other solution. Feedback from developers from major Linux distributions or those most familiar with this topic would be welcome. Until the problem with the difference in results between versions 2.6.0 and 2.7.0 appeared, I was using checksec without thinking if what I was getting when running the script was OK or not. My Linux distribution (Slackware) does not use the hardening flags recommended here and even by GCC, so I have to build the packages from sources and checksec is extremely useful for me to verify the result.

slimm609 commented 3 months ago

looking at it in some more depth, there is some issue with the check. I think this is a case where the test files are very basic so they all work properly but the case comes out when we have more complex binaries.

I agree with the goal of the partial but want to figure out the proper way to detect it more accurately if possible

slimm609 commented 3 months ago

https://github.com/gcc-mirror/gcc/blob/master/gcc/builtins.def#L1112

It looks like the list of functions the output from readelf of glibc may not be an accurate way to check. Trying to understand the details more but the list of "fortiable" functions may be much lower than I originally thought

slimm609 commented 3 months ago

From investigating a bunch of programs and recompiling each one switch FORTIFY_SOURCE 1, 2, and 3.

2 and 3 differences can't be easily detected but 1 can. looking for printf_chk was the one outlier that I found that always seemed to be accurate between 1 and 2. If there are any fortify functions and printf_chk is included, it was done with either 2 or 3, if printf_chk is not included, then it was done with 1. This was also consistent with the test files as well from everything I could find.

This also resolves the ubuntu scan issues pointed out by @petervas

see https://github.com/slimm609/checksec.sh/pull/248

petervas commented 3 months ago

But that means that if a program does not use printf you can't tell the difference again.

I'll see if there is a way to get a fortified printf with fortify 1 as a demo. Would be interesting if it is impossible and it were actually possible to tell the difference for sure.

With the Ubuntu scans I just wanted to point out that even with fortify source 3 you can't guarantee a 'yes' in the current checksec state that needs 100% coverage for a 'yes'.

slimm609 commented 3 months ago

If there is a better way, for sure. This is the most accurate way so far but we can continue to investigate better ways for sure.

slimm609 commented 3 months ago

trying to go through the GCC code a bit to determine which flags don't get triggered between 1 and 2 but not having luck so far. If I slim it down, 1,2 and 3 all produce the same binary.

6e89ac4fc94ae1100c1fd79eaa9230b430d2e68aac796746a3e6d9fec34dac49  SOURCE_1
6e89ac4fc94ae1100c1fd79eaa9230b430d2e68aac796746a3e6d9fec34dac49  SOURCE_2
6e89ac4fc94ae1100c1fd79eaa9230b430d2e68aac796746a3e6d9fec34dac49  SOURCE_3
petervas commented 3 months ago

Yeah that is what i expected too. The higher FORTIFY_SOURCE levels just increase the coverage by being able to address some more special cases in the code, but there should be no single function that only gets fortified at higher levels. So telling the difference between the levels is not really possible.

Personally I would just revert back to the behaviour that a 'Yes' is returned if ANY fortified function is found - regardless of the coverage - to just simply mean "FORITFY was used" without trying to evaluate the quality of the fortification. The current implementation just has no real interpretable "meaning" behind it for me and more potential for confusion.

For a deeper analysis one has to look at the fortifiable vs fortified numbers on a case by case basis. To make it easier to analyze on first glance a new column "Fortify Coverage" could be added to display a percentage (Fortified/Fortifiable)*100 maybe?

teoberi commented 3 months ago

But that means that if a program does not use printf you can't tell the difference again.

I'll see if there is a way to get a fortified printf with fortify 1 as a demo. Would be interesting if it is impossible and it were actually possible to tell the difference for sure.

With the Ubuntu scans I just wanted to point out that even with fortify source 3 you can't guarantee a 'yes' in the current checksec state that needs 100% coverage for a 'yes'.

An example 7zr, 7-Zip compiled from sources with -D_FORTIFY_SOURCE=3 checksec --file=/usr/bin/7zr

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Partial 4               13              /usr/bin/7zr

checksec --fortify-file=/usr/bin/7zr

* FORTIFY_SOURCE support available (libc)    : Yes
* Binary compiled with FORTIFY_SOURCE support: Yes
 ------ EXECUTABLE-FILE ------- . -------- LIBC --------
 Fortifiable library functions | Checked function names
 -------------------------------------------------------
 getcwd                         | __getcwd_chk
 mbstowcs                       | __mbstowcs_chk
 memcpy                         | __memcpy_chk
 memcpy_chk                     | __memcpy_chk
 memmove                        | __memmove_chk
 memset                         | __memset_chk
 memset_chk                     | __memset_chk
 read                           | __read_chk
 read_chk                       | __read_chk
 readlink                       | __readlink_chk
 wcstombs                       | __wcstombs_chk
 wmemcpy                        | __wmemcpy_chk
 wmemcpy_chk                    | __wmemcpy_chk
SUMMARY:
* Number of checked functions in libc                : 83
* Total number of library functions in the executable: 132
* Number of Fortifiable functions in the executable : 13
* Number of checked functions in the executable      : 4
* Number of unchecked functions in the executable    : 9

Missing "printf"? 7zr.zip

slimm609 commented 3 months ago

If printf isn't used, then it won't be there. I do think switching back to yes/no/na and leaving out partial may be the proper solution. There doesn't seem to be a reliable way to be able to identify which level.

teoberi commented 3 months ago

After all the attempts, 2 options remain:

  1. Partially meaning that not all functions have been fortified regardless of whether the programmer did everything possible to ensure this; https://developers.redhat.com/articles/2023/02/06/how-improve-application-security-using-fortifysource3#how_to_improve_application_fortification
  2. Yes, regardless of fortification level, so there must be at least one fortified function. In this case, Yes does not mean that everything is OK, but that fortified functions were found. This should at least be specified in the documentation, the values ​​for Fortified and Fortifiable remain relevant.
petervas commented 3 months ago

If printf isn't used, then it won't be there. I do think switching back to yes/no/na and leaving out partial may be the proper solution. There doesn't seem to be a reliable way to be able to identify which level.

Fully agreed. This is the way to go.

  1. Partially meaning that not all functions have been fortified regardless of whether the programmer did everything possible to ensure this;

Partial in this case is "wrong" so many times that it loses its meaning. See my Ubuntu 22.04 example above. Partial was the strong majority although everything was compiled with FORTIFY_SOURCE=3. Having every function in a binary fortified is an outlier. That happens mostly if very few fortifiable functions are used in non complex contexts.

  1. Yes, regardless of fortification level, so there must be at least one fortified function. In this case, Yes does not mean that everything is OK, but that fortified functions were found. This should at least be specified in the documentation, the values ​​for Fortified and Fortifiable remain relevant.

That has always been my understanding, but it is a good suggestion to make explicit in the documentation.

The following research results are obsolete now :)

I will post them anyways, since they underline that reverting is the right way.

I was checking for more functions like printf to tell the difference between FORTIFY_SOURCE=1 and FORTIFY_SOURCE=2/3. I had this example:

#include <stdio.h>
#include <stdarg.h>
#include <wchar.h>
#include <stdlib.h>

void use_snprintf() {
    char buffer[50];
    snprintf(buffer, 50, "Hello, World! (using snprintf)\n");
    printf("%s", buffer);
}

void use_vsnprintf(const char *format, ...) {
    char buffer[50];
    va_list args;
    va_start(args, format);
    vsnprintf(buffer, 50, format, args);
    va_end(args);
    printf("%s", buffer);
}

void use_fprintf() {
    fprintf(stdout, "Hello, World! (using fprintf)\n");
}

void use_vfprintf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stdout, format, args);
    va_end(args);
}

void use_dprintf() {
    dprintf(1, "Hello, World! (using dprintf)\n");
}

void use_vdprintf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vdprintf(1, format, args);
    va_end(args);
}

void use_vprintf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args);
}

void use_wprintf() {
    wprintf(L"Hello, World! (using wprintf)\n");
}

void use_fwprintf() {
    fwprintf(stdout, L"Hello, World! (using fwprintf)\n");
}

void use_vwprintf(const wchar_t *format, ...) {
    va_list args;
    va_start(args, format);
    vwprintf(format, args);
    va_end(args);
}

void use_vfwprintf(const wchar_t *format, ...) {
    va_list args;
    va_start(args, format);
    vfwprintf(stdout, format, args);
    va_end(args);
}

int main() {
    use_snprintf();
    use_vsnprintf("Hello, World! (using vsnprintf)\n");
    use_fprintf();
    use_vfprintf("Hello, World! (using vfprintf)\n");
    use_dprintf();
    use_vdprintf("Hello, World! (using vdprintf)\n");
    use_vprintf("Hello, World! (using vprintf)\n");
    use_wprintf();
    use_fwprintf();
    use_vwprintf(L"Hello, World! (using vwprintf)\n");
    use_vfwprintf(L"Hello, World! (using vfwprintf)\n");
    return 0;
}

But then @teoberi made a good point earlier. What if a binary simply does not use printf (or any of the above)? You can not tell apart FORTIFY_SOURCE=1 or FORTIFY_SOURCE=2/3 in that case.

And then there is another case. What if you do use FORTIFY_SOURCE=2/3 but printf is not fortified? A plain "hello world" will do that:

#include <stdio.h>

int main() {
    printf("hello world.");
    return 0;
}

In this simple case printf will not be fortified even if FORTIFY_SOURCE=2/3 is used. Impossible to tell apart FORTIFY_SOURCE=1 or FORTIFY_SOURCE=2/3 if such a simple printf call is part of your binary.