Find and fix binary file vulnerabilities in Linux - with checksec utility and gcc compiler



Image: Internet Archive Book Images. Modified by Opensource.com. CC BY-SA 4.0



After compiling the same source code, we may end up with different binaries. It depends on which flags we pass into the hands of the compiler. Some of these flags allow you to enable or disable a number of security-related properties of the binary.



Some of them are enabled or disabled by the compiler by default. This is how vulnerabilities can arise in binary files that we are not aware of.



Checksec is a simple utility for determining which properties were included at compile time. In this article I will tell you:



  • how to use the checksec utility to find vulnerabilities;
  • how to use the gcc compiler to fix the vulnerabilities found.


Installing checksec



For Fedora OS and other RPM-based systems:



$ sudo dnf install checksec
      
      





For Debian based systems use apt.



Quick start with checksec



The checksec utility consists of a single script file, which, however, is quite large. Thanks to this transparency, you can find out which system commands to search for vulnerabilities in binaries are executed under the hood:



$ file /usr/bin/checksec

/usr/bin/checksec: Bourne-Again shell script, ASCII text executable, with very long lines

$ wc -l /usr/bin/checksec

2111 /usr/bin/checksec
      
      





Let's run checksec on the directory browsing utility (ls):



$ checksec --file=/usr/bin/ls

<strong>RELRO           STACK CANARY      NX            PIE             RPATH     RUNPATH      Symbols       FORTIFY Fortified       Fortifiable    FILE</strong>

Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols        Yes   5       17              /usr/bin/ls

      
      





By executing the command in the terminal, you will receive a report on what useful properties this binary has and what it does not.  



The first line is the head of the table, which lists the various security properties - RELRO, STACK CANARY, NX, and so on. The second line shows the values ​​of these properties for the ls utility binary.



Hello binary!



I will compile a binary from the simplest C code:



#include <stdio.h>

int main()

{

        printf(«Hello World\n»);

        return 0;

}

      
      





Please note that so far I have not passed a single flag to the compiler, with the exception of -o (it is beside the point, it just tells where to output the compilation result):



$ gcc hello.c -o hello

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped

$ ./hello

Hello World
      
      





Now I will run the checksec utility for my binary. Some properties are different from properties



ls (     ):

$ checksec --file=./hello

<strong>RELRO           STACK CANARY      NX            PIE             RPATH     RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE</strong>

Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   85) Symbols       No    0       0./hello

      
      





Checksec allows you to use a variety of output formats, which you can specify with the --output option. I'll choose the JSON format and make the output more descriptive with the jq utility :



$ checksec --file=./hello --output=json | jq

{

  «./hello»: {

    «relro»: «partial»,

    «canary»: «no»,

    «nx»: «yes»,

    «pie»: «no»,

    «rpath»: «no»,

    «runpath»: «no»,

    «symbols»: «yes»,

    «fortify_source»: «no»,

    «fortified»: «0»,

    «fortify-able»: «0»

  }

}

      
      





Analysis (checksec) and elimination (gcc) of vulnerabilities



The binary file created above has several properties that determine, let's say, the degree of its vulnerability. I will compare the properties of this file with the properties of the ls binary (also listed above) and explain how to do this using the checksec utility. 



For each item, I will additionally show you how to eliminate the vulnerabilities found.



1. Debug symbols



I'll start simple. Certain symbols are included in the binary at compile time. These symbols are used in software development: they are needed for debugging and bug fixing.



Debug symbols are usually removed from the version of the binary that the developers release for general use. This does not affect the operation of the program in any way. This cleanup (denoted by the word strip ) is often done to save space, as the file becomes lighter after the characters are removed. And in proprietary software, these characters are often removed also because attackers have the ability to read them in binary format and use them for their own purposes.



Checksec shows that debug symbols are present in my binary, but they are not in ls. 



$ checksec --file=/bin/ls --output=json | jq | grep symbols

    «symbols»: «no»,

$ checksec --file=./hello --output=json | jq | grep symbols

    «symbols»: «yes»,
      
      







Running the file command can show the same thing. Characters are not stripped.



$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, <strong>not stripped</strong>
      
      





How checksec works



Let's run this command with the --debug option:



$ checksec --debug --file=./hello

      
      





Since the checksec utility is one long script, you can use Bash functions to examine it. Let's display the commands that the script runs for my hello file:



$ bash -x /usr/bin/checksec --file=./hello
      
      





Pay special attention to echo_message - the output of a message about whether the binary contains debug symbols:



+ readelf -W --symbols ./hello

+ grep -q '\.symtab'

+ echo_message '\033[31m96) Symbols\t\033[m  ' Symbols, ' symbols=«yes»' '«symbols»:«yes»,'
      
      





The checksec utility uses the readelf command with the special flag --symbols to read a binary file. It prints out all the debug symbols in the binary. 



$ readelf -W --symbols ./hello
      
      





From the contents of the .symtab section, you can find out the number of symbols found:



$ readelf -W --symbols ./hello | grep -i symtab
      
      





How to remove debug symbols after compilation



The strip utility will help us with this.



$ gcc hello.c -o hello

$
 

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>not stripped</strong>

$
 

$ strip hello

$
 

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>stripped</strong>
      
      





How to remove debug symbols at compile time



When compiling, use the -s flag:



$ gcc -s hello.c -o hello

$

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=247de82a8ad84e7d8f20751ce79ea9e0cf4bd263, for GNU/Linux 3.2.0, <strong>stripped</strong>

      
      





You can also verify that the symbols have been removed using the checksec utility:



$ checksec --file=./hello --output=json | jq | grep symbols

    «symbols»: «no»,

      
      





2. Canary



Canary (informants) are "secret" values ​​that are stored on the stack between the buffer and control data. They are used to protect against buffer overflow attacks: if these values ​​are changed, then it is worth sounding the alarm. When an application is launched, its own stack is created for it. In this case, it's just a data structure with push and pop operations. An attacker could prepare malicious data and write it onto the stack. In this case, the buffer may overflow and the stack may be damaged. In the future, this will lead to a crash of the program. Analysis of the canary values ​​allows you to quickly understand that a hack has occurred and take action.



$ checksec --file=/bin/ls --output=json | jq | grep canary

    «canary»: «yes»,

$

$ checksec --file=./hello --output=json | jq | grep canary

    «canary»: «no»,

$

 ,    canary,  checksec   :

$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'
      
      





Turn on canary



To do this, when compiling, we use the -stack-protector-all flag:



$ gcc -fstack-protector-all hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep canary

    «canary»: «yes»,

      
      





Now checksec can tell us with a clear conscience that the canary mechanism is on:



$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'

     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)

    83: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2.4

$

      
      





3. PIE



The enabled PIE property allows executable code to be arbitrarily placed in memory regardless of its absolute address:



PIE (Position Independent Executable) - positionally independent executable code. The ability to predict where and what areas of memory are in the address space of a process plays into the hands of attackers. User programs are loaded and executed from a predefined process virtual memory address unless compiled with the PIE option. Using PIE allows the operating system to load sections of executable code into arbitrary chunks of memory, making it much more difficult to crack.



$ checksec --file=/bin/ls --output=json | jq | grep pie

    «pie»: «yes»,

$ checksec --file=./hello --output=json | jq | grep pie

    «pie»: «no»,

      
      





Often the PIE property is only included when compiling libraries. In the output below, hello is marked as LSB executable and the standard library libc (.so) file is marked as LSB shared object:



$ file hello

hello: ELF 64-bit <strong>LSB executable</strong>, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped

$ file /lib64/libc-2.32.so

/lib64/libc-2.32.so: ELF 64-bit <strong>LSB shared object</strong>, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4a7fb374097fb927fb93d35ef98ba89262d0c4a4, for GNU/Linux 3.2.0, not stripped

      
      





Checksec obtains this information as follows:



$ readelf -W -h ./hello | grep EXEC

  Type:                              EXEC (Executable file)

      
      





If you run the same command for the library, you will see DYN instead of EXEC:



$ readelf -W -h /lib64/libc-2.32.so | grep DYN

  Type:                              DYN (Shared object file)

      
      





Turn on PIE



When compiling the program, you need to specify the following flags:



$ gcc -pie -fpie hello.c -o hello
      
      





To make sure that the PIE property is enabled, run the following command:



$ checksec --file=./hello --output=json | jq | grep pie

    «pie»: «yes»,

$
      
      





Now our binary file (hello) will change its type from EXEC to DYN:



$ file hello

hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb039adf2530d97e02f534a94f0f668cd540f940, for GNU/Linux 3.2.0, not stripped

$ readelf -W -h ./hello | grep DYN

  Type:                              DYN (Shared object file)

      
      





4. NX



Operating system and processor tools allow you to flexibly configure access rights to virtual memory pages. By enabling the NX (No Execute) property, we can prevent data from being interpreted as processor instructions. Often, in buffer overflow attacks, attackers push code onto the stack and then try to execute it. However, by preventing the execution of code in these memory segments, such attacks can be prevented. In normal compilation using gcc, this property is enabled by default:



$ checksec --file=/bin/ls --output=json | jq | grep nx

    «nx»: «yes»,

$ checksec --file=./hello --output=json | jq | grep nx

    «nx»: «yes»,

      
      





Checksec again uses the readelf command to get information about the NX property. In this case, RW means the stack is read / write. But since this combination does not contain the E character, there is a prohibition on executing code from this stack:



$ readelf -W -l ./hello | grep GNU_STACK

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10

      
      





Disable NX 



It is not recommended to disable the NX property, but you can do it like this:



$ gcc -z execstack hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep nx

    «nx»: «no»,

      
      





After compilation, we will see that the stack permissions have changed to RWE:



$ readelf -W -l ./hello | grep GNU_STACK

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10
      
      





5. RELRO



In dynamically linked binaries, a special GOT (Global Offset Table) is used to call functions from libraries. This table is referenced by ELF (Executable Linkable Format) binaries. When RELRO (Relocation Read-Only) protection is enabled, the GOT becomes read-only. This allows you to protect against some types of attacks that modify table records:



$ checksec --file=/bin/ls --output=json | jq | grep relro

    «relro»: «full»,

$ checksec --file=./hello --output=json | jq | grep relro

    «relro»: «partial»,

      
      





In this case, only one of the RELRO properties is enabled, so checksec outputs the value "partial". Checksec uses the readelf command to display the settings. 



$ readelf -W -l ./hello | grep GNU_RELRO

  GNU_RELRO      0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R   0x1

$ readelf -W -d ./hello | grep BIND_NOW
      
      





Turn on full protection (FULL RELRO)



To do this, when compiling, you need to use the appropriate flags:



$ gcc -Wl,-z,relro,-z,now hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep relro

    «relro»: «full»,

      
      





That's it, now our binary has received the honorary title of FULL RELRO:



$ readelf -W -l ./hello | grep GNU_RELRO

  GNU_RELRO      0x002dd0 0x0000000000403dd0 0x0000000000403dd0 0x000230 0x000230 R   0x1

$ readelf -W -d ./hello | grep BIND_NOW

 0x0000000000000018 (BIND_NOW)    
      
      



      

Other checksec features



The topic of security can be studied endlessly. Even talking about the simple checksec utility in this article, I cannot cover everything. However, I will mention a few more interesting possibilities.



Checking multiple files



There is no need to run a separate command for each file. You can run one command for several binaries at once:



$ checksec --dir=/usr/bin
      
      





Checking processes



The checksec utility also allows you to analyze process security. The following command displays the properties of all running programs on your system (you need to use the --proc-all option to do this): 



$ checksec --proc-all
      
      





You can also select one process to check by specifying its name:



$ checksec --proc=bash
      
      





Kernel check



Similarly, you can analyze vulnerabilities in the kernel of your system.



$ checksec --kernel
      
      





Forewarned is forearmed



Study the security properties in detail and try to understand what exactly each of them affects and what types of attacks it can prevent. Checksec to help you!






Cloud servers from Macleod are fast and secure.



Register using the link above or by clicking on the banner and get a 10% discount for the first month of renting a server of any configuration!






All Articles