Using LD_PRELOAD to overwrite Linux syscalls
#1
I wrote this to share what I was learning, I'm not a fucking expert, criticize me all you want, and since I don't have much to share here, have this.

Ugh, luakit crashed so I'm having to rewrite this entire tutorial from scratch, I'm going to try and detail this as much as possible.

What is LD_PRELOAD?

Exactly what it sounds like, LD_PRELOAD points to a shared library and loads it before any other libraries, allowing you to overwrite predefined libraries or instructions before anything is actually executed. LD_PRELOAD is present in many modern rootkits such as Azazel and Jynx2, I'll leave the github pages for Azazel and Jynx2 at the bottom of this guide.

Why LD_PRELOAD?

As I said before, LD_PRELOAD loads a target shared library before any other library, thus being somewhat secure and easy to implement.

How does this help?

I'm hoping that you have a tiny idea of how rootkits operate and how they work and whatnot. Well using LD_PRELOAD you can overwrite syscalls before they're actually defined by the kernel itself, so you can prevent a syscall like open() from opening a specific file.

How are these libraries installed?

Let's take a look at the install script I made for panther and procedurally break down each step it goes through to make this easy.

[Image: zaafri.png]

  1. script opens
  2. script identifies which uid is executing the script, if = 0 continue, else exit
  3. dynamically compile with all warnings, library inclusion allowed, don't link the output and output the bin as panther.o
  4. compile the dynamic bin as a shared library, local lib inclusion allowed, passing -soname to the linker, linking libvimcore.so, linking the dl lib, outputting as libvimcore.so, and target bin panther.o
  5. skip these 2 lines
  6. installing the shared library under read/write/execute (owner) + read/execute (other users) (755) into the /lib/ directory
  7. pointing LD_PRELOAD to the newly installed library so that /lib/libvimcore.so loads before any other library
  8. script notifies successful injection, removes compilation results and exits
My script is just a bash script replication of Azazel's Makefile, nothing else.

Let's take a look at how you overwrite a syscall.

First, you need to know what syscall you're wanting to overwrite, in this case, let's make any non-directory file starting with "__" in its name unable to be removed by rm. To do this, we need to find out what calls rm references, to do this, we'll use the tool "ltrace", similar to strace but it traces library calls and not syscalls. Syntax of ltrace is simple, "ltrace /path/to/file optional arguments", and any calls are printed to the screen. Let's find out what calls rm references by using ltrace and removing an empty file.

[Image: ssbllb.png]

Fun things start to happen at the faccessat() and unlinkat() calls, if you man unlinkat, you see that it takes 3 arguments, a file descriptor, a pathname to unlink, and flags, and it is this exact call that we need to overwrite to disallow any user from removing our file. To overwrite a syscall, you need to create C source file, defined _GNU_SOURCE, include any includes mentioned on your target call's and copy the construct on the man page too. It's simple, here's an example.

[Image: ommttb.png]

Notice how I kept the argument names the same as that from the man page.

Okay, sure, the syscall has been overwritten now, but if you compile and execute this via a shared library + LD_PRELOAD, you won't be able to remove any files, that's not what we want. If you want to overwrite the old syscall with a new one that calls the old syscall (if that makes any sense), we can use dlsym, this was linked earlier by the -l argument in the installation script. dl* are "interfaces to the dynamic linking library". Read more about it here: http://linux.die.net/man/3/dlsym
Using dlsym() you can call the original syscall even after you overwrite it.

An example as follows.

[Image: sighdg.png]

Using typeof() automatically grabs the data type of target call and then creates it using that said data type. Keep note of me passing the arguments from the new call into the old call too. The RTLD_NEXT flag just reserves the call for future use.

Now if you were to compile this and execute it via a shared library + LD_PRELOAD, the new syscall will be overwritten and it will return the old syscall - if you want to make sure that the new syscall is indeed being called, you can use printf to display a significant message when a file is removed.

Okay, so you've overwritten a syscall and you're returning the old syscall into the new one, but this isn't very beneficial as to what we want, which is to remove the ability to remove files that start with "__". To do this, we'll use the header include "stdbool", which = standard boolean, simple stuff, and strncmp, which could be your equivalent to string.startsWith() in a higher level language like Java.

[Image: sgsiot.png]

What the boolean type isHidden() is doing is checking if the supplied char a starts with MAGIC_STRING, in this case MAGIC_STRING was defined as "__", and if a starts with "__", the boolean returns true and thus the char is hidden, else the boolean returns false and the supplied char is not that of a hidden file. Now if you compile and execute this as a shared library via LD_PRELOAD, all users, root and non-root, will be unable to remove any files starting with "__".
Also make sure to keep note that I created a global boolean type to determine if a char starts with the magic string, as large projects such as rootkits often do this check many times.

You can do this with literally any syscalls, and remember to use typeof() when recreating a syscall.

Azazel github: https://github.com/chokepoint/azazel
Jynx2: https://github.com/chokepoint/Jynx2
Reply
#2
I'm a huge fan of LD_PRELOAD, the second to last edition of 2600 actually covered it. It's a great technique. But you need give a source for your article because googling different phrases from your post comes up with this exact post on several other forums.
Reply
#3
(08-31-2015, 06:21 PM)NO-OP Wrote: I'm a huge fan of LD_PRELOAD, the second to last edition of 2600 actual covered it.  It's a great technique.  But you need give a source for your article because googling different phrases from your post comes up with this exact post on several other forums.

i posted this on multiple other forums
Reply
#4
(08-31-2015, 06:22 PM)doc Wrote:
(08-31-2015, 06:21 PM)NO-OP Wrote: I'm a huge fan of LD_PRELOAD, the second to last edition of 2600 actual covered it.  It's a great technique.  But you need give a source for your article because googling different phrases from your post comes up with this exact post on several other forums.

i posted this on multiple other forums

Oh fantastic! Well it's a great little tut/paper. I'll have to dig up that copy of 2600 and post some of the source. He has a lot of interesting examples of things to do like using LD_PRELOAD to crack time trial programs and sniffing passwords entered into programs(Like SSH).

LD_PRELOAD is a great technique for fucking with C programs.

Also, welcome to GreySec
Reply
#5
You didn't necessarily have to use the <stdbool.h> header from C99 to do this. I would have just used an int:
Code:
int is_hidden(const char *s)
{
  return !strncmp(s, MAGIC_STRING, strlen(MAGIC_STRING));
}


I would have maybe even removed the call to strlen() even if it gets inlined, or optimized in any further way by the compiler. Nice informational thread though Smile
Reply
#6
Hi,

You are not overwriting system calls with LD_PRELOAD. What happens is that the shared library is loaded first and is used first in the dynamic linkers search path when it goes to resolve a symbol. When an executable (ET_EXEC) or shared library (ET_DYN) is loaded and there is a symbol which needs to be resolved the dynamic linker searches in the order in which they were loaded.

If you want to hook the printf function then your shared object needs to export a symbol named "printf". Since the dynamic linker searches in your shared object before it does libc (because your shared object was loaded first) then it will find the printf symbol in your shared objects dynamic symbol table and resolve that symbol to your function by writing it in the PLT entry in the victim executable/shared objects GOT entry for that PLT entry.

So you are not overwriting anything you are simply winning the symbol lookup race because you are the first place the dynamic linker looks in its symbol resolution routine and you are not hooking system calls. If you want to do system calling hooking then you need to write a loadable kernel module (LKM). Although there are really better ways to accomplish most of your goals using DKOM and therefore there is no need to overwrite the system call table. If you are writing a kernel mode rootkit.

Just wanted to point that out y'all understand how this technique works and don't think your overwriting system calls Smile I am new here looking forward to getting to know everyone!
Reply
#7
(07-26-2016, 03:49 PM)IRP Wrote: Hi,

You are not overwriting system calls with LD_PRELOAD. What happens is that the shared library is loaded first and is used first in the dynamic linkers search path when it goes to resolve a symbol. When an executable (ET_EXEC) or shared library (ET_DYN) is loaded and there is a symbol which needs to be resolved the dynamic linker searches in the order in which they were loaded.

If you want to hook the printf function then your shared object needs to export a symbol named "printf". Since the dynamic linker searches in your shared object before it does libc (because your shared object was loaded first) then it will find the printf symbol in your shared objects dynamic symbol table and resolve that symbol to your function by writing it in the PLT entry in the victim executable/shared objects GOT entry for that PLT entry.

So you are not overwriting anything you are simply winning the symbol lookup race because you are the first place the dynamic linker looks in its symbol resolution routine and you are not hooking system calls. If you want to do system calling hooking then you need to write a loadable kernel module (LKM). Although there are really better ways to accomplish most of your goals using DKOM and therefore there is no need to overwrite the system call table. If you are writing a kernel mode rootkit.

Just wanted to point that out y'all understand how this technique works and don't think your overwriting system calls Smile I am new here looking forward to getting to know everyone!

Thank you for your insight and feedback! Already making a great impression Smile You seem very knowledgeable and I'm very happy to have you here. So without further ado, welcome to GreySec! And please enjoy your stay. Looking forward getting to know you as well.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Introduction to Stack Based Overflows on Linux Insider 0 794 05-23-2019, 12:25 PM
Last Post: Insider