goelfobjcopygpg-signature

Objcopy --remove-section does not leave an ELF binary the same as the original before --add-section


I am trying to use objcopy to embed a GPG signature in my binary so I can verify it during updates on linux systems. The problem is, objcopy appears to be leaving modifications of the ELF binary in place after removing the added signature section.

Here is what I am doing to sign it

gpg --yes --output sig_before --detach-sign --sign binary_before
objcopy --add-section sigdata=sig_before binary_before binary_signed

And what am I doing to verify it

objcopy --remove-section=sigdata binary_signed binary_after
objcopy --dump-section sigdata=sig_after binary_signed
gpg --verify sig_after binary_after

The problem is the verification always fails because binary_after is different from binary_before as indicated by the file size.

-rwxr-xr-x 1 root root 4608224 Oct 14 15:46 binary_after
-rwxr-xr-x 1 root root 4608135 Oct 14 14:48 binary_before
-rwxr-xr-x 1 root root 4608416 Oct 14 15:46 binary_signed
-rw-r--r-- 1 root root     119 Oct 14 15:46 sig_after
-rw-r--r-- 1 root root     119 Oct 14 15:46 sig_before

I can do gpg --verify sig_after binary_before and it verifies.

I was looking at the section headers, but this has left me a little lost:

# readelf -S binary_before 
There are 13 section headers, starting at offset 0x158:
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000
       00000000002130ec  0000000000000000  AX       0     0     32
  [ 2] .rodata           PROGBITS         0000000000615000  00215000
       00000000000e4ea1  0000000000000000   A       0     0     32
  [ 3] .typelink         PROGBITS         00000000006f9ec0  002f9ec0
       000000000000171c  0000000000000000   A       0     0     32
  [ 4] .itablink         PROGBITS         00000000006fb5e0  002fb5e0
       00000000000006f0  0000000000000000   A       0     0     32
  [ 5] .gosymtab         PROGBITS         00000000006fbcd0  002fbcd0
       0000000000000000  0000000000000000   A       0     0     1
  [ 6] .gopclntab        PROGBITS         00000000006fbce0  002fbce0
       0000000000146ef8  0000000000000000   A       0     0     32
  [ 7] .go.buildinfo     PROGBITS         0000000000843000  00443000
       0000000000000250  0000000000000000  WA       0     0     16
  [ 8] .noptrdata        PROGBITS         0000000000843260  00443260
       0000000000015680  0000000000000000  WA       0     0     32
  [ 9] .data             PROGBITS         00000000008588e0  004588e0
       000000000000c450  0000000000000000  WA       0     0     32
  [10] .bss              NOBITS           0000000000864d40  00464d40
       00000000000213c0  0000000000000000  WA       0     0     32
  [11] .noptrbss         NOBITS           0000000000886100  00486100
       000000000000dc50  0000000000000000  WA       0     0     32
  [12] .shstrtab         STRTAB           0000000000000000  00465000
       0000000000000087  0000000000000000           0     0     1

# readelf -S binary_after
There are 13 section headers, starting at offset 0x464da0:
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000
       00000000002130ec  0000000000000000  AX       0     0     32
  [ 2] .rodata           PROGBITS         0000000000615000  00215000
       00000000000e4ea1  0000000000000000   A       0     0     32
  [ 3] .typelink         PROGBITS         00000000006f9ec0  002f9ec0
       000000000000171c  0000000000000000   A       0     0     32
  [ 4] .itablink         PROGBITS         00000000006fb5e0  002fb5e0
       00000000000006f0  0000000000000000   A       0     0     32
  [ 5] .gosymtab         PROGBITS         00000000006fbcd0  002fbcd0
       0000000000000000  0000000000000000   A       0     0     1
  [ 6] .gopclntab        PROGBITS         00000000006fbce0  002fbce0
       0000000000146ef8  0000000000000000   A       0     0     32
  [ 7] .go.buildinfo     PROGBITS         0000000000843000  00443000
       0000000000000250  0000000000000000  WA       0     0     16
  [ 8] .noptrdata        PROGBITS         0000000000843260  00443260
       0000000000015680  0000000000000000  WA       0     0     32
  [ 9] .data             PROGBITS         00000000008588e0  004588e0
       000000000000c450  0000000000000000  WA       0     0     32
  [10] .bss              NOBITS           0000000000864d40  00464d30
       00000000000213c0  0000000000000000  WA       0     0     32
  [11] .noptrbss         NOBITS           0000000000886100  00464d30
       000000000000dc50  0000000000000000  WA       0     0     32
  [12] .shstrtab         STRTAB           0000000000000000  00464d30
       0000000000000070  0000000000000000           0     0     1

What is objcopy leaving behind in the binary? Is there any way I can embed the signature without altering the output binary?


Solution

  • From the section names it is apparent that your binary is a Go program built with Google's golang toolchain.

    What's the problem?

    Look at this:

    $ cat binary_before.go 
    package main
    import "fmt"
    func main() {
        fmt.Println("hello world")
    }
    
    $ go build binary_before.go
    
    $ ./binary_before 
    hello world
    

    The size of binary_before is:

    $ stat -c "%s" binary_before
    1893833
    

    Here is its ELF header:

    $ readelf -h binary_before
    ELF Header:
      Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF64
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              EXEC (Executable file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x463900
      Start of program headers:          64 (bytes into file)
      Start of section headers:          400 (bytes into file)
      Flags:                             0x0
      Size of this header:               64 (bytes)
      Size of program headers:           56 (bytes)
      Number of program headers:         6
      Size of section headers:           64 (bytes)
      Number of section headers:         23
      Section header string table index: 20
    

    Note that the section headers start at byte 400 (= 0x190). That's right after the 6 56-byte program headers, which start at byte 64, right after the ELF Header itself. The sections themselves are wherever the section headers say they are, but they've all got to after the end of the 23 64-byte section headers, i.e. after byte 400 + (23*64) = 1872. readelf -S binary_before will show you that's true.

    Let's just copy the program with objcopy:

    $ objcopy binary_before binary_copy
    $ ./binary_copy 
    hello world
        
    

    The size of the copy is:

    $ stat -c "%s" binary_copy
    1893232
    

    which is 1893833 - 1893232 = 601 bytes smaller than binary_before. And here's the ELF header of binary_copy

    $ readelf -h binary_copy
    ELF Header:
      Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF64
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              EXEC (Executable file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x463900
      Start of program headers:          64 (bytes into file)
      Start of section headers:          1891760 (bytes into file)
      Flags:                             0x0
      Size of this header:               64 (bytes)
      Size of program headers:           56 (bytes)
      Number of program headers:         6
      Size of section headers:           64 (bytes)
      Number of section headers:         23
      Section header string table index: 22
      
    

    It's different. Notably, the section headers now start at byte 1891760 = 0x1cddb0, not byte 400. That's not right after the program headers. Where is it? Here are the section details:

    $ readelf -S binary_copy
    There are 23 section headers, starting at offset 0x1cddb0:
    
    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
           0000000000000000  0000000000000000           0     0     0
      [ 1] .text             PROGBITS         0000000000401000  00001000
           000000000007f93a  0000000000000000  AX       0     0     32
      [ 2] .rodata           PROGBITS         0000000000481000  00081000
           000000000003d2da  0000000000000000   A       0     0     32
      [ 3] .typelink         PROGBITS         00000000004be2e0  000be2e0
           0000000000000590  0000000000000000   A       0     0     32
      [ 4] .itablink         PROGBITS         00000000004be880  000be880
           0000000000000058  0000000000000000   A       0     0     32
      [ 5] .gosymtab         PROGBITS         00000000004be8d8  000be8d8
           0000000000000000  0000000000000000   A       0     0     1
      [ 6] .gopclntab        PROGBITS         00000000004be8e0  000be8e0
           00000000000645d8  0000000000000000   A       0     0     32
      [ 7] .go.buildinfo     PROGBITS         0000000000523000  00123000
           0000000000000130  0000000000000000  WA       0     0     16
      [ 8] .noptrdata        PROGBITS         0000000000523140  00123140
           00000000000054a0  0000000000000000  WA       0     0     32
      [ 9] .data             PROGBITS         00000000005285e0  001285e0
           0000000000004250  0000000000000000  WA       0     0     32
      [10] .bss              NOBITS           000000000052c840  0012c830
           000000000005fb30  0000000000000000  WA       0     0     32
      [11] .noptrbss         NOBITS           000000000058c380  0012c830
           0000000000003a40  0000000000000000  WA       0     0     32
      [12] .debug_abbrev     PROGBITS         0000000000000000  0012c830
           0000000000000135  0000000000000000   C       0     0     1
      [13] .debug_line       PROGBITS         0000000000000000  0012c965
           000000000001f60b  0000000000000000   C       0     0     1
      [14] .debug_frame      PROGBITS         0000000000000000  0014bf70
           0000000000006232  0000000000000000   C       0     0     1
      [15] .debug_gdb_s[...] PROGBITS         0000000000000000  001521a2
           000000000000002d  0000000000000000           0     0     1
      [16] .debug_info       PROGBITS         0000000000000000  001521cf
           000000000003d87e  0000000000000000   C       0     0     1
      [17] .debug_loc        PROGBITS         0000000000000000  0018fa4d
           000000000001c466  0000000000000000   C       0     0     1
      [18] .debug_ranges     PROGBITS         0000000000000000  001abeb3
           000000000000b10f  0000000000000000   C       0     0     1
      [19] .note.go.buildid  NOTE             0000000000400f9c  00000f9c
           0000000000000064  0000000000000000   A       0     0     4
      [20] .symtab           SYMTAB           0000000000000000  001b6fc8
           000000000000baa8  0000000000000018          21    95     8
      [21] .strtab           STRTAB           0000000000000000  001c2a70
           000000000000b24a  0000000000000000           0     0     1
      [22] .shstrtab         STRTAB           0000000000000000  001cdcba
           00000000000000f0  0000000000000000           0     0     1
    

    The last section, .shstrtab (section headers string table) starts at 0x1cdcba, is 0xf0 bytes long, so ends at 0x1cddaa = byte 1891754. That means the section headers at byte 1891760 = 0x1cddb0 start on the next 16-byte boundary right after the sections themselves.

    By merely "copying" binary_before to binary_copy - no sections added or removed - obcopy has rearranged the output ELF data layout from Program Headers, Section Headers, Sections to Program Headers, Sections, Section Headers. And in doing so it has saved 601 bytes on section/segment alignment padding. The digital signature extracted from binary_before is already bound to be different from that of binary_copy.

    objcopy is allowed to do this. The ELF format doesn't specify the order of the program headers, sections headers and sections. The program headers and sections seaders are where the ELF header says they are, and the sections are where the section headers say they are.

    But you wouldn't see this rearrangement if you objcopy-ed a program that had been compiled and linked with a GNU toolchain. That's because the ELF layout Program Headers, Sections, Section Headers, although not mandatory, is the default layout adhered to by GNU ELF tools, including the GNU linker in its stock configurations and GNU objcopy.

    For reasons I don't know, the golang toolchain prefers the Program Headers, Section Headers, Sections ELF layout, while objcopy restores the conventional GNU layout, and that defeats your attempt to verify a digital signature using objcopy

    Solution 1

    Instead of using the Google Go toolchain to build your program, use GCC's if it is packaged for your distro1 or if you're cool with building it from source. If you can't do one of those things use Solution 2.

    GCC Go sticks with the conventional GNU ELF layout. See:

    $ gccgo -o binary_before_gcc binary_before.go
    $ ./binary_before_gcc 
    hello world
    
    $ objcopy binary_before_gcc binary_before_gcc_copy
    $ ./binary_before_gcc_copy 
    hello world
    

    The objcopyed GCC binary is identical with the original:

    $ cmp binary_before_gcc binary_before_gcc_copy ; echo Done
    Done
    

    So it will yield the same digital signature.

    Solution 2

    In your build, use obcopy to "copy" binary_before to (say) binary_copy, as we've already done. That gives you a functionally equivalent program (like binary_before_gcc) that has the conventional GNU ELF layout. Regard binary_before as a disposable intermediate file; regard binary_copy as your program.

    Then use objcopy again as you've already done to digitally sign binary_copy and verify the digital signature.

    Like this.

    Make signed program:

    $ gpg --yes --output sig_before --detach-sign --sign binary_copy
    $ objcopy --add-section sigdata=sig_before binary_copy binary_copy_signed
    

    Verify signature:

    $ objcopy --remove-section=sigdata binary_copy_signed binary_copy_after
    $ objcopy --dump-section sigdata=sig_after binary_copy_signed
    $ gpg --verify sig_after binary_copy_after
    gpg: Signature made Tue 15 Oct 2024 18:04:09 BST
    gpg:                using EDDSA key 64F0C44FABCDA322C7977E6BE602681EEE59BA4E
    gpg: Good signature from "Mike Kinghan (Throwaway key) <redacted@redacted.com>" [ultimate]
    

    1. It is packaged for recent Debian & Ubuntu releases: sudo apt install gccgo