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?
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 objcopy
ed 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]
sudo apt install gccgo