2023年4月10日 星期一

Extract module certificate from Linux kernel binary

In some cases, for debugging or auditing, we need to verify kernel module or kexec kernel binary manually. But kernel doesn't allow keyctl tool to export keys from .builtin_trusted_keys keyring. And we can NOT google out any URL for downloading the module certificate of the kernel. In those cases, we can direct extract module certificate from kernel binary. 

This approach is base on openSUSE. But I believe it can be applied on other distros. 

Where is the module certificate in kernel binary?

Looking at the source code of kernel, the module certificate be included in certs/system_certificates.S

/* SPDX-License-Identifier: GPL-2.0 */
#include 
#include 

        __INITRODATA

        .align 8
        .globl system_certificate_list
system_certificate_list:
__cert_list_start:
__module_cert_start:
        .incbin "certs/signing_key.x509"
__module_cert_end:
        .incbin "certs/x509_certificate_list"
__cert_list_end:

As the code, it's in __INITRODATA. section, and the __module_cert_start is in the same address with system_certificate_list. In System.map, we can find the virtual address of system_certificate_list:


vi /boot/System.map-5.14.21-150400.12-default

ffffffff81000000 T _stext
...
ffffffff831fe000 D early_top_pgt
...
ffffffff8338afc8 d __cert_list_start
ffffffff8338afc8 d __module_cert_start
ffffffff8338afc8 D system_certificate_list
ffffffff8338b4d0 d __cert_list_end
ffffffff8338b4d0 d __module_cert_end
ffffffff8338b4d0 D system_certificate_list_size
ffffffff8338b4d8 D module_cert_size
...
As the above example, the virtual address of system_certificate_list is 0xffffffff8338afc8. The __module_cert_start and __cert_list_start are in the same address. Using objdump tool can also print the address of system_certificate_list:
objdump --all-headers vmlinux-5.14.21-150400.12-default | less
...
ffffffff8338afc8 g       .init.data     0000000000000000 system_certificate_list
...

Transfer virtual address to the offset in kernel binary file

OK! Now we got the address of __module_cert_start (which equals to system_certificate_list). But it's a virtual address, not the offset in kernel binary file. If we want to use "dd" tool to extract the certificate in kernel binary, we need the offset. Because __module_cert_start is in __INITRODATA section, we can use readelf tool to get the virtual address and offset of .init.data section:
readelf --sections vmlinux-5.14.21-150400.12-default  | less
There are 73 section headers, starting at offset 0x50b83a0:

Section Headers:
  [Nr] Name              Type             Address           Offset
...
  [45] .init.data        PROGBITS         ffffffff831fe000  025fe000
       00000000001b7130  0000000000000000  WA       0     0     8192
...
As the above example, the address of .init.data is 0xffffffff831fe000, and the offset in file is 0x25fe000. Then we can calculate the offset of __module_cert_start:
Address offset:
__module_cert_start - .init.data = 0xffffffff8338afc8 - 0xffffffff831fe000 = 0x18cfc8

File offset for __module_cert_start:
0x25fe000 + 0x18cfc8 = 0x278afc8 = 41,463,752

The length of module certificate 

OK! Now we got the offset of __module_cert_start in kernel binary file. But we still need the length. Just like __module_cert_start, we can find the __module_cert_end in System.map
vi /boot/System.map-5.14.21-150400.12-default

ffffffff81000000 T _stext
...
ffffffff831fe000 D early_top_pgt
...
ffffffff8338afc8 d __cert_list_start
ffffffff8338afc8 d __module_cert_start
ffffffff8338afc8 D system_certificate_list
ffffffff8338b4d0 d __cert_list_end
ffffffff8338b4d0 d __module_cert_end
ffffffff8338b4d0 D system_certificate_list_size
ffffffff8338b4d8 D module_cert_size
...
Now we can calculate the length of module certificate:


length = __module_cert_end - __module_cert_start = 0xffffffff8338b4d0 - 0xffffffff8338afc8 = 0x508 = 1,288

Extract module certificate by dd

After we got the start offset in file and length. Then we can use dd to extract blob to a file. The file will be the certificate. 

Before using dd, we need use vmlinux binary file as the source. If you only have vmlinuz file, you can use the scripts/extract-vmlinux tool in linux kernel source to extract vmlinux from vmlinuz:

./scripts/extract-vmlinux ~/vmlinuz-5.14.21-150400.12-default > ~/vmlinux-5.14.21-150400.12-default
Then running dd to extract module certificate. Remember to use decimal:
dd bs=1 skip=41463752 count=1288 if=./vmlinux-5.14.21-150400.12-default of=~/signkey.crt
1288+0 records in
1288+0 records out
1288 bytes (1.3 kB, 1.3 KiB) copied, 0.00334262 s, 385 kB/s
As the above dd command, we can extract certificate to a signkey.crt file. Then we can use openssl to print out the certificate file:
openssl x509 -in ./signkey.crt -inform DER -noout -text | less
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            ed:87:85:b7:8f:fc:12:7e
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = SUSE Linux Enterprise Secure Boot CA, C = DE, L = Nuremberg, O = SUSE Linux Products GmbH, OU = Build Team, emailAddress = build@suse.de
        Validity
            Not Before: Mar  8 10:15:08 2021 GMT
            Not After : Dec 31 10:15:08 2030 GMT
        Subject: CN = SUSE Linux Enterprise Secure Boot Signkey, C = DE, L = Nuremberg, O = SUSE Linux Products GmbH, OU = Build Team, emailAddress = build@suse.de
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:b6:f0:29:43:8a:a0:c4:38:9b:3a:96:2e:7c:5c:
                    90:1b:71:c3:b1:0b:f0:8c:f7:eb:40:77:58:aa:47:
...
If you can print the certificate by openssl. Then it confirmed that we extracted right blob of module certificate from kernel binary.