First, we prepare project patch by
examining the differences in assembler files generated during the original
and the patched source code build. Finally, users invoke the libcare-ctl that
applies the patches. This is a lot like loading a shared object (library)
into other process memory and then changing original code to unconditionally
jump to the new version of the code.
Binary patches are built from augmented assembly files. Augmented files are
made via kpatch_gensrc which notes the difference in assembly files
produced from the original and the patched source code.
This is done in two steps, both are described detailed in Manual Patch Creation.
First, the original code is built as is either by invoking make directly or
by the packaging system. The build is done with compiler substituted to
libcare-cc wrapper. Wrapper's behaviour is configured via environment
variables.
When libcare-cc is invoked with KPATCH_STAGE=original it simply builds
the project while keeping intermediate assembly files under the name
.kpatch_${filename}.original.s invoking the real compiler twice: first with the
-S flag to produce the assembly files from the original code and then with
the -c flag to produce object files out of these intermediate assembly
files.
Project binaries built during the original stage are stashed and later used in
the patch preparation. When building patches for a package from distribution the
objects built during original stage must be compatible with those from the
distro's binary package.
Assembly files resulting from the correct original build can be stored to speed
up patch builds later on.
Next, source code patches are applied and the build is redone.
This time the libcare-cc wrapper is instructed by environment variable
KPATCH_STAGE=patched to build a special patch-containing object.
Wrapper first calls real compiler with -S flag to produce an assembly file
for the patched version, which is stored under file name
.kpatch_${filename}.patched.s. It then calls kpatch_gensrc that
compares original and patched files and produces a patch-containing assembly
where all the changes in the code are put in the .kpatch-prefixed sections
while original code is left as is. This assembly is finally compiled to a
patch-containing object file by calling compiler with the -c flag.
Linking done by the project build system carries these sections to the target
binary and shared object files. During the link stage libcare-cc adds ld
argument -q that instructs linker to keep information about all the
relocations. This is required for the Patching to (dynamically)
link patch into running binary.
Then the sanity check is done, checking that the symbols originating from the
non-kpatch sections in the patched binary are equal to those from the
original binary or its debuginfo.
The last part is postprocessing the patch-containing binaries: stripping off the original binary sections, fixing relocations and prepending the resulting ELF content with a common kpatch header. Look at Manual Patch Creation for details.
The above algorithm is implemented in two various helper scripts. The first is
libcare-patch-make that can build patches for any project buildable via
make and the second aims at building patches for applications and libraries
coming from distribution packages scripts/pkgbuild.
Both are using libcare-cc wrapper described below. It is recommended to go through Manual Patch Creation at least once.
The scripts/pkgbuild is responsible for the building of the patch
and pre-building the original package and assembly files. At the moment
it only supports the building of the RPM-based packages.
Each package has its own directory packages/$distro/$package with
different package versions as subdirectories. For instance, the directory
packages/rhel7/glibc/ contains subdirectory glibc-2.17-55.el7 that has the
configuration and scripts for building and testing of the sample security
patches for that version of glibc package for RHEL7.
The project directory contains three main files:
Shell-sourceable
infothat has the necessary environment variables specified along with the hooks that can alter package just before the build and test patch before it is packed. For instance,packages/rhel7/glibc/glibc-2.17-55.el7/infocontains both hooks and akp_patch_testfunction that runs glibc test suite with each invocation being patched with the built patch.The list
plistof the patches to be applied. File names are relative to the top-level directorypatches.YAML file
properties.yamlcontaining version-specific configuration, such as URLs for pre-build storage, original source packages URL, and Docker container images with toolchain (GCC/binutils) version is required to properly build the package.This is not used at the moment and left as an information source for the users.
Most of the binaries in the system are in the elf(5)
format. From the producer
point of view, the file of this format consists of a set of blocks called
sections. Sections can contain data (.rodata, .data),
executable code (usually called .text) and auxiliary data. The text
references its parts and necessary data (such as variables) by means
of symbols. For instance, C's main is a special symbol where the
control is transferred by the C runtime after the required initialization
is done. The symbols are listed in the section .symtab whenever it
is required.
There are three main types of ELF format files: the DYNamic shared
object, used primarily to store common code in libraries; the binary
EXECutable, used to contain the application; and the
RELocatable object file resulting from compiling an assembler file
(GNU C compiler actually generates assembler which is fed to the GNU
assembler).
These differ mostly by the types of relocations they may and do contain.
Relocations are the technique used to allow address changes in the binary
object files. For instance, when linking a set of .o files into executable,
the linker merges sections from them into a single section such as .text,
.data or .rodata. The linker then adjusts relocation info such as the
place where it should be applied (called r_offset), target symbol and its
address and/or addend relative to the symbol value (called r_addend). Some
types of relocations are also allowed in the final binary object and are
resolved upon load by the dynamic linker.
The DYNamic object contains all the data necessary to load the
library on a random base address. This randomization of the base leads
to randomization of the library functions addresses, making it harder
for an intruder to exploit a vulnerability, and allowing multiple
libraries to be loaded without interfering each other. Because it
is impossible to know the address of a variable at the compile time the
DYNamic code refers to its data objects using so-called Global
Offset Table (GOT). This table contains addresses of the variables, so
accessing a variable takes two steps: first loading the GOT entry, then
unreferencing it. GOT entry is usually referenced in the
instruction pointer relative manner. The GOT is filled by the dynamic
linker such as ld-linux while resolving relocations from the
.rela.dyn. Only a few types of relocations are allowed there, they
are (for x86-64): R_X86_64_RELATIVE, R_X86_64_64 and
R_X86_64_GLOB_DATA. The symbols provided by the DYNamic object
are listed in the .dynsym section with the names stored in the
.dynstr section. Special section .dynamic contains all the data
required to load an object, such as a list of required libraries,
pointers to the relocation entries and so on.
The EXECutable objects are usually linked to a fixed address
and contain no relocation information. The kernel only needs to know
how to load this type of objects along with the interpreter if
specified. Most of the binaries have the dynamic linker ld-linux
specified as the interpreter. It is loaded by the kernel and the control
is transferred here. The dynamic loader duty is to load all the necessary
libraries, resolve symbols and transfer the control to the application code.
The RELocatable object file can contain any type of relocation.
The static linker, such as ld, links these into an EXECutable file
or a DYNamic one. The REL object file is merely an assembler
file turned into a binary file, with the symbol references noted as
appropriately. That is, for every symbol reference in the assembler file
there is a corresponding symbol added to the RELocatable ELF file
and the relocation referencing this symbol. For every symbol defined the
corresponding symbol is added to the .symtab section. ASCII
zero-ended string names are stored into the .strtab section. The
static linker then resolves symbol referenced in one object file with
the symbols defined in another object file or DYNamic shared
object file.
Here we are going to describe how the patching is performed.
This is the act that looks like a mix of static and dynamic linking in
the process address space expecting that we are doing it using ptrace.
There is infant task to reuse rtld's _dl_open calls to do the
job for us.
The following is the verbose description of the
kpatch_process_patches function flow.
When a user asks libcare-ctl to patch a process with a given patch
(or a directory with patches), the patcher (let's call it doctor) first
attaches to the threads to be patched (let's call it patient) thus
stopping their execution.
Now, if we are about to patch a freshly executed binary, we have to
continue its execution until all the libraries are loaded. That is, if
the binary has a non-zero interpreter, such as ld-linux, the
kernel first executes the interpreter and it is the interpreter task to
transfer control to the application text after all the initialization is
successful. So, to ensure that all the libraries are loaded so we can
use symbols provided by them in our patches, we have to wait until the
initialization is done. We do this by inserting a TRAP instruction
at the entry point of the application, so when the interpreter is done
loading the libraries, we have to parse auxiliary
vector
information to find the entry point. This is done in the
kpatch_load_libraries function.
The next step is to find out what ELF objects are loaded and where. This way
we know offsets for reach dynamically-loaded library and can actually
resolve symbols from there. This is done by the function
kpatch_create_object_files. For the correct mapping of the object
symbol addresses to the virtual address space we have to parse the
instructions on how to load the object stored in the
program headers part of the ELF, and are used by the dynamic
loader or the kernel. This part is done by the function
kpatch_create_object_files.
Next, if we are given a patch file we check that there are indeed
patches for the objects of the application (kpatch_verify_objects).
If we are given a directory, we lookup for patch files named
$BuildID.kpatch inside it and load what we have found
(kpatch_load_patches). If there are no patches we just let the
application continue its execution, free our resources and we are done.
Otherwise, we call the kpatch_apply_patches function that goes
through the list of objects that do have patches and applies patches.
Regular executable objects (both EXEC and DYN) reference global
functions via Procedure Linkage Table and global object symbols by
copying them into local data using R_X86_64_COPY relocation (for
EXEC) or looking for the address in the application or library using
R_X86_64_GLOB_DATA relocation (for DYN). We had to implement a
jump table for the function references which is reused as GOT for the
symbol reference. It is also used as the Thread Pointer Offset table for
the TLS data.
So, the first we need to count if there is a need for the jump table at all. For that, we do count undefined and TLS symbols and allocate the jump table if there are any of them.
The next we need to find a region in the patient address space suitable to
mmap the patch here. We start to look for the hole after the object and
check if there is enough space to fit the patch, looking farther upon
failure. This is done by the kpatch_find_patch_region function.
We allocate an anonymous region to hold the patch on the patient's
behalf using the code injected by ptrace. This is done by the
kpatch_mmap_remote function that executes a mmap syscall
remotely.
Once we got the address of the region and allocated memory there, we are all prepared to resolve the relocations from the kpatch.
Since relocations are made against symbols we have first to resolve
symbols. This is done by the function kpatch_resolve present in the
kpatch_elf.c file.
We resolve sections addresses first. We know the address of the
region we allocated for the kpatch, so we can calculate the
kpatch's sections addresses. Other sections' addresses are resolved
from the original object file we are about to patch.
After the section addresses are resolved we resolve addresses for the
symbols present in the kpatch. The functions and data objects symbols of
type STT_FUNC and STT_OBJECT have the containing section offset
added to the st_value.
The thread-local storage objects of type STT_TLS may be referenced
by two different relocations, one that gets offset from a GOT
(GOTTPOFF), another that asks offset to be put inline
(TPOFF{32,64}). We use symbol field st_size to store the
original offset and st_value to store the offset in the jump table.
Objects of unknown type STT_NOTYPE are resolved via the jump table. If it
is later discovered that they are referenced by a relocation as a
Global Offset Table entry such as GOTPCREL then only the address
value from the jump table is used.
Rest of the symbol types are unsupported. The appearence of the unsupported
symbol type will cause the doctor to fail.
Now that we are all set, we resolve the relocations. This is done by the
function kpatch_relocate that calls kpatch_apply_relocate_add
for all the sections of type SHT_RELA.
The code is pretty straightforward except for two relocations. The first one is
the TPOFF{32,64} relocations that do restore offset saved in st_size.
Another one is Global Offset Table-related relocations such as GOTTPOFF,
GOTPCREL, and Ubuntu Xenial specific REX_GOTPCRELX. If the referenced
symbol has type STT_NOTYPE or STT_TLS, then the jump table entry is
reused as the Global Offset Table entry. If the relocation aims for either
original object or patch section, then we convert the mov
instruction present to the lea instruction as there is no appropriate jump
table entry which is not required in that case since the target section is
closer than 2GiB (we allocate the memory for the patch that way).
Now that the patch is fully prepared it is written into the previously allocated region of patient's memory.
But we are not yet done with the patching of the patient. We now have to reroute the execution paths from the old buggy functions into our just loaded new shiny ones. But it is dangerous to patch functions that are being executed at the moment, since this can change the way the data is structured and corrupt everything. So, we have to wait until the patient leaves functions we are about to patch.
This is done by the function kpatch_ensure_safety which checks that
there is no patched symbols on the stack and, if there is any, waits for
the patient to hit breakpoints placed at their returns. The function
uses libunwind function with pluggable unwinder interfaces.
If we ensured the patching safety, we start the patching
itself. For that the entry point of the original functions are rewritten
with the unconditional jumps to the patched functions. This is done by
the function kpatch_apply_hunk called for each of the original
functions that do have patched one.
At this point doctor done with his job, it frees resources and leaves. If anything wrong happens during any of the actions the appropriate error MUST be printed.
Throughout this section the availability of the kpatch tools is assumed. To build them and add them into PATH, do:
$ make -C src
$ export PATH=$PWD/src:$PATHSo, the main working horse for the whole project, including kernel patches, is
the kpatch_gensrc utility. It compares two assembler files and whenever
there are differences in the code of a particular function, it emits a new code
after the original one but with a name suffixed with .kpatch and in the
.kpatch.text section. Keeping the original code maintains all the data and
references in the original order. All the new variables are being put into
.kpatch.data section.
So, imagine that you have two source code versions available, let's name them
foo for the original and bar for the patched version:
// foo.c
#include <stdio.h>
#include <time.h>
void i_m_being_patched(void)
{
printf("i'm unpatched!\n");
}
int main(void)
{
while (1) {
i_m_being_patched();
sleep(1);
}
}// bar.c
#include <stdio.h>
#include <time.h>
void i_m_being_patched(void)
{
printf("you patched my %s\n", "tralala");
}
int main(void)
{
while (1) {
i_m_being_patched();
sleep(1);
}
}Now we need to get assembler code for both of the files:
$ gcc -S foo.c $ gcc -S bar.c
Take a look at what is different in the files:
$ diff -u foo.s bar.s
--- foo.s 2016-07-16 16:09:16.635239145 +0300
+++ bar.s 2016-07-16 16:10:43.035575542 +0300
@@ -1,7 +1,9 @@
- .file "foo.c"
+ .file "bar.c"
.section .rodata
.LC0:
- .string "i'm unpatched!"
+ .string "tralala"
+.LC1:
+ .string "you patched my %s\n"
.text
.globl i_m_being_patched
.type i_m_being_patched, @function
@@ -13,8 +15,10 @@
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
- movl $.LC0, %edi
- call puts
+ movl $.LC0, %esi
+ movl $.LC1, %edi
+ movl $0, %eax
+ call printf
popq %rbp
.cfi_def_cfa 7, 8
retYou can see that the GCC optimized a call to a printf without
arguments to a simple puts call, and our patch brings the
printf call back.
Now it's time to produce a patch result. Execute kpatch_gensrc:
$ $KPATCH_PATH/kpatch_gensrc --os=rhel6 -i foo.s -i bar.s -o foobar.s FATAL! Blocks of type other mismatch 1-1 vs. 1-1
Oops, the difference in .file is fatal. Let's trick that and try
again:
$ sed -i 's/bar.c/foo.c/' bar.s $ $KPATCH_PATH/kpatch_gensrc --os=rhel6 -i foo.s -i bar.s -o foobar.s
The result is:
.file "foo.c"
#---------- var ---------
.section .rodata
.LC0:
.string "i'm unpatched!"
#---------- func ---------
.text
.globl i_m_being_patched
.type i_m_being_patched, @function
i_m_being_patched:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size i_m_being_patched, .-i_m_being_patched
i_m_being_patched.Lfe:
#---------- kpatch begin ---------
.pushsection .kpatch.text,"ax",@progbits
.globl i_m_being_patched.kpatch
.type i_m_being_patched.kpatch, @function
i_m_being_patched.kpatch:
.LFB0.kpatch:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0.kpatch, %esi
movl $.LC1.kpatch, %edi
movl $0, %eax
call printf
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0.kpatch:
.size i_m_being_patched.kpatch, .-i_m_being_patched.kpatch
i_m_being_patched.kpatch_end:
.popsection
.pushsection .kpatch.strtab,"a",@progbits
kpatch_strtab1:
.string "i_m_being_patched.kpatch"
.popsection
.pushsection .kpatch.info,"a",@progbits
i_m_being_patched.Lpi:
.quad i_m_being_patched
.quad i_m_being_patched.Lfe - i_m_being_patched
.quad i_m_being_patched.kpatch
.quad i_m_being_patched.kpatch_end - i_m_being_patched.kpatch
.quad kpatch_strtab1
.quad 0
.popsection
#---------- kpatch end -----------
#---------- func ---------
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
.L3:
call i_m_being_patched
movl $1, %edi
movl $0, %eax
call sleep
jmp .L3
.cfi_endproc
.LFE1:
.size main, .-main
.pushsection .kpatch.data,"aw",@progbits
.LC0.kpatch:
.string "tralala"
.popsection
.pushsection .kpatch.data,"aw",@progbits
.LC1.kpatch:
.string "you patched my %s\n"
.popsection
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbitsA watchful reader have spotted two new sections: .kpatch.info and
.kpatch.strtab. The former contains information about the function being
patched and the patch itself, such as sizes of the functions. The
compiler generates a relocation section .rela.kpatch.info against it
that references symbols from both the original binary as patch targets
and the patch as the patched function.
We should now compile both original and patched assembler files into
binaries, keeping the relocation information with linker's -q
switch:
$ gcc -o foo foo.s $ gcc -o foobar foobar.s -Wl,-q
and proceed to the building a kpatch file out of these.
The binary containing patch (foobar in the example above) has extra
sections:
$ readelf -S foobar | grep -A 1 kpatch
[16] .kpatch.text PROGBITS 0000000000400662 00000662
000000000000001a 0000000000000000 AX 0 0 1
[17] .rela.kpatch.text RELA 0000000000000000 00001ef0
0000000000000048 0000000000000018 40 16 8
--
[20] .kpatch.strtab PROGBITS 000000000040069b 0000069b
0000000000000019 0000000000000000 A 0 0 1
[21] .kpatch.info PROGBITS 00000000004006b4 000006b4
0000000000000030 0000000000000000 A 0 0 1
[22] .rela.kpatch.info RELA 0000000000000000 00001f38
0000000000000048 0000000000000018 40 21 8
--
[36] .kpatch.data PROGBITS 0000000000601050 00001050
000000000000001b 0000000000000000 WA 0 0 1
This is where the patch actually hides and we had to extract it from here. First, we need to strip all the unnecessary data from the patched binary:
$ kpatch_strip --strip foobar foobar.stripped $ stat -c '%n: %s' foobar foobar.stripped foobar: 10900 foobar.stripped: 6584
The --strip mode of the kpatch_strip operation removes all the
kpatch-unrelated sections, setting their type to PROG_NOBITS and
modifying sections offsets.
Patch code, packed into .kpatch.text section, references its part and parts
of the original binary via relocations.
These relocations are fixed by invoking kpatch_strip --rel-fixup as follows:
All relocations of type
PLT32are changed toPC32since they are resolved via the jump table.All the relocations internal to the patch are left as is -- that is, if newly introduced code references newly introduced function or data. The
doctorwill have enough information to resolve these.Some of these relocations are referencing original local symbols introduced by compiler named like
.LC0. Each relocation referencing such a symbols is replaced to relocation referencing section that contains them with an updatedr_addend.Relocations referencing Thread Local Storage symbols are harder to handle, mostly because of the variety of TLS models in use.
Relocations of type
TPOFF32are generated inEXECutable binaries for TLS symbols defined in application. We ensure that (negative) offset values into TLS block coincide between original and patched binaries.Relocations of type
GOTTPOFFare generated when code references TLS variable from another object. These are tricky: code looks for appropriate originalGOTentry which is filled viaTPOFF64relocation and writes the offset of this entry into ther_addendfield ofGOTTPOFFrelocation.All the other TLS relocation types are not supported since there is no full TLS support yet.
Another important part is the interaction between kpatch_gensrc generation
of GOTPCREL entries and linker optimization for it.
Whenever assembly code of the patch references variable not coming from patch there are two options.
First, the referenced variable can be defined in the original code that can be referenced as is since we allocate patches close to the original code and the 32-bit PC-relative relocation should be enough.
Second, the referenced non-TLS variable can be imported by the original code,
e.g. from glibc library. In that case, the variable can be further than 2GiB
away from the patch code and it ought to have a way to address it in all the
64-bit address space.
There is no reliable way to distinguish these at the compile time, so we replace
EVERY reference to a non-patch variable with an indirect reference using
a Global Offset Table entry. This is what --force-gotpcrel option of
kpatch_gensrc does.
Linker knows what symbols are defined in original binary and what symbols are
coming from imported shared libraries. Linker resolves symbols coming from the
original binary by setting a correct original section number to the symbol.
Symbols defined in the patch are assigned section number of either
.kpatch.text or .kpatch.data at this stage.
Some linker versions optimize our two-stage references to original symbols via
GOTPCREL:
mov foobar@GOTPCREL(%rip), %rax
mov (%rax), %raxinto one-stage
lea foobar(%rip), %rax
mov (%rax), %raxchanging relocation type from GOTPCREL to a simple PC32. The
kpatch_strip code ensures that this is always done for known symbols so
there is no dependency on particular linker behavior.
All the references to the variables imported by the original code are left with
the GOTPCREL relocation and these are correctly resolved during the
patching, except for the variables COPY ed by the original binary.
Now that we have fixed kpatch relocations we can finally strip all
the unnecessary symbols with strip:
$ strip --strip-unneeded foobar.strippedThis will remove the symbols that have no relocations targeted at
them, so, most of the symbols, except for the sections, patched
functions with .kpatch suffix and symbols referenced from
the patch.
Since the doctor does not care for the program section and loads patch
as a single bulk region without caring for the program header and
sections virtual addresses and offsets in the patch must be prepared
accordingly. That means we have to undo all the offsets and convert
base-address relative values into section-relative values for the
relocations offsets (r_offset), symbols (st_value) and finally
reset the sections addresses to zeroes (sh_addr). This all is done
by the --undo-link mode of kpatch_strip:
$ readelf -rs foobar.stripped
Relocation section '.rela.kpatch.text' at offset 0x11f0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000400667 00030000000a R_X86_64_32 0000000000601050 .kpatch.data + 0
00000040066c 00030000000a R_X86_64_32 0000000000601050 .kpatch.data + 8
000000400676 002500000002 R_X86_64_PC32 0000000000000000 printf@@GLIBC_2.2.5 - 4
Relocation section '.rela.kpatch.info' at offset 0x1238 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
0000004006b4 000100000001 R_X86_64_64 00000000004004d0 .text + ed
0000004006c4 002600000001 R_X86_64_64 0000000000400662 i_m_being_patched.kpat + 0
0000004006d4 000200000001 R_X86_64_64 000000000040069b .kpatch.strtab + 0
Symbol table '.symtab' contains 39 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004004d0 0 SECTION LOCAL DEFAULT 14
2: 000000000040069b 0 SECTION LOCAL DEFAULT 20
3: 0000000000601050 0 SECTION LOCAL DEFAULT 36
...
37: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
38: 0000000000400662 26 FUNC GLOBAL DEFAULT 16 i_m_being_patched.kpatchNow let's undo the link:
$ kpatch_strip --undo-link foobar.strippedTake a look at the patch afterwards to ensure that offsets have been indeed reset:
$ readelf -rs foobar.stripped
Relocation section '.rela.kpatch.text' at offset 0x11f0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000005 00030000000a R_X86_64_32 0000000000000000 .kpatch.data + 0
00000000000a 00030000000a R_X86_64_32 0000000000000000 .kpatch.data + 8
000000000014 002500000002 R_X86_64_PC32 0000000000000000 printf@@GLIBC_2.2.5 - 4
Relocation section '.rela.kpatch.info' at offset 0x1238 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000000 000100000001 R_X86_64_64 0000000000000000 .text + ed
000000000010 002600000001 R_X86_64_64 0000000000000000 i_m_being_patched.kpat + 0
000000000020 000200000001 R_X86_64_64 0000000000000000 .kpatch.strtab + 0
Symbol table '.symtab' contains 39 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 14
2: 0000000000000000 0 SECTION LOCAL DEFAULT 20
3: 0000000000000000 0 SECTION LOCAL DEFAULT 36
...
37: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
38: 0000000000000000 26 FUNC GLOBAL DEFAULT 16 i_m_being_patched.kpatchFinally, we need to prepend the kpatch ELF object with
meta-information doctor uses to check that the patch target is correct.
We do this using kpatch_make, but first we need to know what is the
name of the target object (foo in our case) and what is its
BuildID, stored in .note.build-id section:
$ readelf -n foo | grep 'Build ID'
Build ID: 9e898b990912e176275b1da24c30803288095cd1Now we are all set to convert foobar.stripped into a kpatch:
$ kpatch_make -b "9e898b990912e176275b1da24c30803288095cd1" \
foobar.stripped -o foo.kpatchNow let's apply that:
(terminal1) $ ./foo
i'm unpatched!
i'm unpatched!
...
(terminal2) $ kpatch_ctl -v patch -p $(pidof foo) ./foo.kpatch
...
(terminal1)
you patched my tralala
you patched my tralalaCongratulations, we are done with the simple patch! It was pretty complicated, wasn't it?
Building any real project following the recipe above is a nightmare since it
requires interfering with the project's build system: changing all the
compilation to go through intermediate assembly and kpatch_gensrc.
Luckily, this can be done in a gcc wrapper like libcare-cc.
It allows for the transparent compilation of the patches and hides away
the details into an additional abstraction layer that will eventually
break, be sure.