我正在尝试使用 objcopy 在我的二进制文件中嵌入 GPG 签名,以便我可以在 Linux 系统上更新时验证它。 问题是,在删除添加的签名部分后,objcopy 似乎保留了 ELF 二进制文件的修改。
这就是我正在做的签名
gpg --yes --output sig_before --detach-sign --sign binary_before
objcopy --add-section sigdata=sig_before binary_before binary_signed
我正在做什么来验证它
objcopy --remove-section=sigdata binary_signed binary_after
objcopy --dump-section sigdata=sig_after binary_signed
gpg --verify sig_after binary_after
问题是验证总是失败,因为
binary_after
与文件大小所示的 binary_before
不同。
-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
我可以做到
gpg --verify sig_after binary_before
并且它得到验证。
我正在查看章节标题,但这让我有点迷失:
# 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
objcopy 在二进制文件中留下了什么?有什么方法可以在不改变输出二进制文件的情况下嵌入签名吗?
从部分名称可以明显看出,您的二进制文件是使用 Google 的 golang 工具链构建的 Go 程序。
有什么问题吗?
看这个:
$ cat binary_before.go
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
$ go build binary_before.go
$ ./binary_before
hello world
binary_before
的大小为:
$ stat -c "%s" binary_before
1893833
这是它的 ELF 标头:
$ 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
请注意,节头从字节 400 (= 0x190) 开始。就在6个56字节程序之后 headers,从字节 64 开始,就在 ELF 标头本身之后。这些部分本身 位于节标题所说的任何位置,但它们都必须在 23 64 字节结束之后 节标题,即字节 400 + (23*64) = 1872 之后。
readelf -S binary_before
将向您显示
确实如此。
让我们用 objcopy 复制程序:
$ objcopy binary_before binary_copy
$ ./binary_copy
hello world
副本尺寸为:
$ stat -c "%s" binary_copy
1893232
即 1893833 - 1893232 = 比
binary_before
小 601 个字节。和
这是 binary_copy
的 ELF 标头
$ 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
不一样。值得注意的是,节头现在从字节 1891760 = 0x1cddb0 开始,而不是字节 400。 那不是在程序标题之后。它在哪里?以下是该部分的详细信息:
$ 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
最后一节,
,shstrtab
(节标题字符串表)从 0x1cdcba 开始,
长度为 0xf0 字节,因此以 0x1cddaa = 字节 1891754 结束。这意味着该部分
字节 1891760 = 0x1cddb0 处的标头从紧随其后的下一个 16 字节边界开始
这些部分本身。
仅通过“复制”
binary_before
到binary_copy
- 没有添加或删除任何部分 -
obcopy
已重新排列输出 ELF 数据布局,从程序头、节头、节到程序头、节、节头。这样做节省了 601 字节
节/段对齐填充。从binary_before
中提取的数字签名是
已经注定与 binary_copy
不同。
objcopy
可以这样做。 ELF
格式不指定顺序
程序标题、节标题和节。程序标题和章节分隔符
是 ELF 标头所说的位置,而节是节标头所说的位置
他们是。
但是如果你
objcopy
编辑了一个已经被修改过的程序,你就不会看到这种重新排列。
使用 GNU 工具链进行编译和链接。那是因为
ELF 布局 Program Headers、Sections、Section Headers,虽然不是强制性的,但
GNU ELF 工具遵循的默认布局,包括 GNU 链接器
它的库存配置和 GNU objcopy
。
出于我不知道的原因,golang工具链更喜欢程序头、节头、节 ELF 布局,而
objcopy
恢复了传统的 GNU 布局,这会挫败您使用 objcopy
验证数字签名的尝试
解决方案1
不要使用 Google 的 Go 工具链来构建程序,而是使用 GCC(如果已打包) 对于您的发行版1,或者如果您愿意从源代码构建它。 如果您无法做到其中一项,请使用解决方案 2。
GCC Go 坚持传统的 GNU ELF 布局。参见:
$ 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
objcopy
ed GCC 二进制文件与原始版本相同:
$ cmp binary_before_gcc binary_before_gcc_copy ; echo Done
Done
因此它将产生相同的数字签名。
解决方案2
在您的构建中,使用
obcopy
将 binary_before
“复制”到(比如说)binary_copy
,就像我们已经完成的那样。那
为您提供一个功能等效的程序(如binary_before_gcc
),它具有传统的
GNU ELF 布局。将binary_before
视为一次性中间文件;将 binary_before_copy
视为您的
程序。
然后再次使用
objcopy
,就像您已经完成的那样,对 binary_copy
进行数字签名并验证数字
签名。
像这样。
制作签名程序:
$ gpg --yes --output sig_before --detach-sign --sign binary_copy
$ objcopy --add-section sigdata=sig_before binary_copy binary_copy_signed
验证签名:
$ 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) <[email protected]>" [ultimate]
sudo apt install gccgo