Shared library injection into a Linux process (part 2)

Shared library injection into a Linux process (part 2)

Introduction

In the last article, we saw how to retrieve a function pointer into the memory of a process as well as the theory about shared library injection and . As a reminder, here are the steps to inject a library:

  1. Compute the address of __libc_dlopen_mode into the victim process.
  2. Attach the victim process with ptrace.
  3. Modify the victim process to make it execute the __libc_dlopen_mode function.

So, in this article we will see how to interfere with the execution of a process in order to make it load our shared library.

Change the control flow of a victim process

In order to change the normal execution of a process, we can exploit several techniques. Among those, we will use this method:

  • Allocate a new readable and writable mapping in the victim.
  • Write into this new mapping the path of the lib we want to inject.
  • Set the victim to make it call __libc_dlopen_mode(our_new_mapping, RTLD_NO).
  • Wake up the victim and let the magic happen.

Let’s go!

Allocate a mapping

To allocate a mapping, we can use the syscall mmap (man mmap). As explained in its man page:

mmap() creates a new mapping in the virtual address space of the calling process.

Now, how do we make it call this syscall? We saw in the previous article that ptrace is a good option because it allows to arbitrarily modify the memory of a process. Thus we can modify the next instruction of the victim with an asm syscall instruction (opcode: OfO5).

The commented code below shows how to call mmap:

NR_MMAP = 9 

PROT_READ  = 1
PROT_WRITE = 2

MAP_PRIVATE   = 0x02
MAP_ANONYMOUS = 0x20

SYSCALL = b'\x0f\x05\x00\x00\x00\x00\x00\x00'

# [...]

# step 1: make a backup of the current victim registers
with victim.get_regs_and_restore() as regs:

    # step 2: write an asm syscall instruction into the address pointed by rip
    with victim.write_mem_words_and_restore(regs.rip, SYSCALL):

        # step 3: set all the registers according to the mmap parameters
        # mmap(0, 8192, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0)
        regs.rax = NR_MMAP
        regs.rdi = 0
        regs.rsi = 8192 # size
        regs.rdx = PROT_WRITE | PROT_READ
        regs.r10 = MAP_ANONYMOUS | MAP_PRIVATE
        regs.r8  = 0
        regs.r9  = 0
        victim.set_regs(regs)

        # step 4: let the victim executes the syscall instruction (step 2)
        victim.step()

        # step 5: get the syscall result
        victim.get_regs(regs)
        mapping = regs.rax

    # step 6: restore the original instruction (automatically done)

# step 7: restore all the registers (automatically done)

The write_mem_words_and_restore method uses ptrace to write into the process memory. It works even if the mapping has no write permission (as in this case).

Because calling a syscall is a frequent task, I made a syscall method into my proc library. All the above code can be replaced by:

prot    = PROT_WRITE | PROT_READ | PROT_EXEC
flags   = MAP_ANONYMOUS | MAP_PRIVATE
mapping = victim.syscall(NR_MMAP, 0, 8192, prot, flags, 0, 0)

Writing into the new mapping

We saw in the last section that ptrace can be used to write into a process memory. Another method relies on the function process_vm_writev (man page). It is more practical than ptrace because it’s able to write a buffer of any size. However, it can only write into a writable mapping.

Into the excerpt below, the path of the library to inject is written at the beginning of our new mapping (write_mem_array uses process_vm_writev):

victim.write_mem_array(mapping, path)

Call __libc_dlopen_mode

To call a function, we can use the previous method. However, rather than a syscall instruction, we have to put a call instruction. The easier method is to put the function address into rax and do a call rax. However, once the call finished, we have to restore some data: the registers and the original instruction overridden by our call. Without this, the victim process could crash (with a high probability). To do that, we can add an int3 instruction just after the call. It will make the victim emit a SIGTRAP signal then pause. During this pause, we are free to restore all the data before allowing the victim to continue.

Here is the code:

# call rax, int3
CALL_INT = b'\xff\xd0\xcc\x00\x00\x00\x00\x00'

# prepare the dlopen call
dlopen_addr = victim.get_sym_addr(libc_path, '__libc_dlopen_mode')
# make a backup of original registers values
with victim.get_regs_and_restore() as regs:
    # set the registers to call __libc_dlopen_mode(our_new_mapping, RTLD_NO)
    regs.rax = dlopen_addr
    regs.rdi = mapping
    regs.rsi = RTLD_NOW
    # set the stack frame to point on the middle of the new mapping
    regs.rsp = mapping + 4096
    regs.rbp = regs.rsp
    victim.set_regs(regs)
    # write the call/int3 instructions
    with victim.write_mem_words_and_restore(regs.rip, CALL_INT):
        # during the continue, the victim will execute the call and the int3
        victim.continue_()

Again, because calling a function is a redundant task, I made a simple method:

dlopen_addr = victim.get_sym_addr(libc_path, '__libc_dlopen_mode')
victim.call(dlopen_addr, mapping, RTLD_NOW, stack_frame_addr=mapping+4096)

Final result

Below is the full code of our dummy injector:

from deedee.proc import process


NR_MMAP = 9

PROT_READ  = 1
PROT_WRITE = 2

MAP_PRIVATE   = 0x02
MAP_ANONYMOUS = 0x20

RTLD_NOW = 0x02


def run(pid, libc_path, lib_path):
    # attach the victim
    victim = process.Process(pid)
    victim.attach()
    # allocate a new mapping
    prot    = PROT_WRITE | PROT_READ
    flags   = MAP_ANONYMOUS | MAP_PRIVATE
    mapping = victim.syscall(NR_MMAP, 0, 8192, prot, flags, 0, 0)
    # write the path lib into the beginning of the mapping
    path = lib_path.encode() + b'\x00'
    victim.write_mem_array(mapping, path)
    # call dlopen
    dlopen_addr = victim.get_sym_addr(libc_path, '__libc_dlopen_mode')
    handler     = victim.call(dlopen_addr, mapping, RTLD_NOW, stack_frame_addr=mapping+4096)


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser(description='Dummy Shared Library Injector')
    parser.add_argument('pid',  type=int, help='the pid in which the lib must be injected')
    parser.add_argument('libc', type=str, help='the path of the libc')
    parser.add_argument('lib',  type=str, help='the path of the lib to inject')
    args = parser.parse_args()

    run(args.pid, args.libc, args.lib)

Demonstration

For this demonstration, we need a little shared library to inject into a process. We can make one quickly:

#include <stdio.h>

__attribute__((constructor))
static void init()
{
    puts("#######################\n");
    puts("I hate the bearded man.\n");
    puts("#######################\n");
}

The syntax __attribute__((constructor)) is used to execute a function when the library is loaded. To compile it:

$ gcc -Wall -shared -fPIC -o libdummy.so libdummy.c

Now, we have to run a program and start our injector. In the examples below, I tried to inject our dummy lib into vlc (paths may vary):

$ $ python dummy.py $(pidof vlc) /usr/lib64/libc-2.30.so $(pwd)/libdummy/libdummy.so

Voilà! Our library is injected into the vlc process:

We can also check the maps file of vlc:

As a reminder, if you want some details about Python functions I exposed in this article, you can find the source code of the proc library on Github:

https://github.com/d33d33l4bs/proc

Improvements

This dummy injector can be greatly improved:

  • Free the allocated mapping.
  • Propose to dlclose the injected library.
  • Allowing to call some functions of the injected library.
  • etc.

The end

That’s all folks. If you have any questions, do not hesitate.

P.S: I hate Kami.

Related articles

One thought on “Shared library injection into a Linux process (part 2)

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.