ifunky xz
Berend De Schouwer
xz and openssh
There was a briefly successful attempt to add a backdoor to /usr/bin/sshd
on Linux.
There are a lot of discussions of how, when, what and why already on the Internet. I’ll include a few links below.
Most posts fall in one of two camps:
- brief description
- detailed description, starting with a
.m4
file.
This post instead will try to turn this into a story. What are the attacker’s goals, and how can they best be achieved?
Hopefully this different perspective will make it understandable to a different audience, which will help prevent this in the future.
goals
The attackers goal is to add a backdoor to some software that is both commonly installed, and has privileged access.
Some criteria for a good back door:
- The door is hidden
- The door can be used unoticed
- The door has a lock, so others can’t use it
- Because it’s open source, the door plans are hidden too.
location of the plans
The obvious location is OpenSSH. However, that project is very well run. That means hiding the plans becomes difficult.
Well, where else can we hide it? Let’s look:
$ ldd /usr/sbin/sshd
/lib64/ld-linux-x86-64.so.2
libcrypt.so.1 => /lib64/libcrypt.so.1
libaudit.so.1 => /lib64/libaudit.so.1
libpam.so.0 => /lib64/libpam.so.0
...
liblzma.so.5 => /lib64/glibc-hwcaps/x86-64-v3/liblzma.so.1.2.3
libzstd.so.1 => /lib64/glibc-hwcaps/x86-64-v3/libzstd.so.4.5.6
...
It turned out that liblzma, from xz utils is a good choice. On a different day, or a different project, one of the others could have worked.
chapter 1: place the door
front door
If we modify liblzma
, how does that open a door in /usr/sbin/sshd
?
For that, we need ifunc(). This behaves a little like $LD_PRELOAD
,
in that we can override functions. It has advantages for the attack:
- It’s less well known. Most setuid binaries filter
$LD_PRELOAD
- It’s better hidden.
Let’s give a quick ifunc() example:
int add_numbers_fast(int x, int y) {
return x + y; /* add */
}
int add_numbers_slow(int x, int y) {
return x + y; /* add */
}
int add_numbers(int x, int y)
__attribute__((ifunc ("resolve_add_numbers")));
static void *resolve_add_numbers(void)
{
if (1)
return add_numbers_fast;
else
return add_numbers_slow;
}
Compile with
gcc -fPIC -shared add_numbers.c -o add_numbers.so
This has two implementations of add_numbers(), and based on some criteria — like CPU — we pick one.
This is fairly common for performance-critical functions. This is also true for some encryption functions in OpenSSH.
We would then use it with
#include <stddef.h>
#include <stdio.h>
extern int add_numbers(int x, int y);
int main(int argc, char *argv[]) {
int x, y;
x = y = 3;
printf("add_numbers(%d, %d) = %d\n",
x, y,
add_numbers(x, y));
return 0;
}
Output:
add_numbers(3, 3) = 6
backdoor
Suppose the library includes another implementation, like so:
extern int add_numbers(int x, int y);
int add_numbers_fake(int x, int y) {
return x * y; /* multiply instead! */
}
int add_numbers(int x, int y)
__attribute__((ifunc ("resolve_add_numbers")));
static void *resolve_add_numbers(void)
{
return add_numbers_fake;
}
Now we can get modified the output:
add_numbers(3, 3) = 9
a note on compiling
To compile the examples1, it’s best to split compilation and linking, like
gcc -fPIC -shared add_numbers.c -o add_numbers.so
gcc -fPIC -shared add_numbers_backdoor.c -o add_numbers_backdoor.so
gcc -c ifunc_example.c -o ifunc_example.o
ld /usr/lib64/crti.o /usr/lib64/crtn.o /usr/lib64/crt1.o \
-lc ifunc_example.o \
add_numbers_backdoor.so add_numbers.so \
-dynamic-linker /lib64/ld-linux-x86-64.so.2 \
-o ifunc_example
Then you can swap implementations by swapping the order of add_numbers.so and add_numbers_backdoor.so
chapter 2: hiding the door
Now that we have something to include, we need to hide it. We cannot add random function names to xz utils: it will get noticed.
A number of systems do automatic checks, like:
$ file *
add_numbers_backdoor.c: C source, ASCII text
add_numbers_backdoor.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=6883795af4a391d0f9cb256aea233498a37ba668, with debug_info, not stripped
add_numbers.o: ELF 64-bit LSB relocatable, x86-64, version 1 (GNU/Linux), not stripped
ifunc_example.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
Adding a binary blob that matches an ELF object will be suspicious.
What we can do is add the file encrypted. In xz this is easy, because it includes binary test data.
A very simple encryption scheme, that shifts a -> b, b -> c, d -> e, …
# encrypt
echo 'hello world' | tr '[a-z]' '[b-z]a'
ifmmp xpsme
To decrypt
# decrypt
echo ifmmp xpsme | tr '[a-z]' 'z[a-y]'
hello world
Now we simply need to decrypt it before compiling.2 We can add the
following to either the .m4
file or Makefile
. Our choice.
cat myfile | tr '[a-z]' 'z[a-y]' > myfile.c
gcc myfile.c
chapter 3: hiding the use
Now that we can include some code in OpenSSH, where is the best place to put it?
- before login, so we’re still root
- before sandboxing3, so we have full access
- after encryption started, so the commands are encrypted
This is why the exploit overrides RSA_public_decrypt
.
links
Links to the actual files, and diagnosis of the actual files: