Search code examples
c++segmentation-faultglobal-variablesshared-librariesclang++

Unable to assign value to global variable through function (shared library injection on Linux)


I am messing with shared library injection and managed to make it work. But it didn't take long until the first issue showed up. If you take a look at the following code blocks, you'll see that the only difference between them is that on the first one, the value returned from the "test" function is assigned to a local variable, while in the second code block, it is assigned to a global variable.

//main.cpp
#include <iostream>
#include "mem/mem.hpp"
#define PROCESS_MOD_NAME MEM_STR("/target\n")
#define LIBC_MOD_NAME    MEM_STR("/libc.so")

mem::moduleinfo_t test()
{
    mem::moduleinfo_t _modinfo;
    _modinfo.name = "testing";
    _modinfo.path = "testing";
    _modinfo.base = (mem::voidptr_t)10;
    _modinfo.end  = (mem::voidptr_t)20;
    _modinfo.size = 10;

    return _modinfo;
}

__attribute__((constructor))
void  libmain()
{
    mem::moduleinfo_t modinfo = test(); //local variable
    std::cout << "name: " << modinfo.name << std::endl;
    std::cout << "path: " << modinfo.path << std::endl;
    std::cout << "base: " << modinfo.base << std::endl;
    std::cout << "size: " << modinfo.size << std::endl;
    std::cout << "end:  " << modinfo.end  << std::endl;
}

This one works, no crash.

//main.cpp
#include <iostream>
#include "mem/mem.hpp"
#define PROCESS_MOD_NAME MEM_STR("/target\n")
#define LIBC_MOD_NAME    MEM_STR("/libc.so")

mem::moduleinfo_t test()
{
    mem::moduleinfo_t _modinfo;
    _modinfo.name = "testing";
    _modinfo.path = "testing";
    _modinfo.base = (mem::voidptr_t)10;
    _modinfo.end  = (mem::voidptr_t)20;
    _modinfo.size = 10;

    return _modinfo;
}

mem::moduleinfo_t modinfo; //global variable

__attribute__((constructor))
void  libmain()
{
    modinfo = test();
    std::cout << "name: " << modinfo.name << std::endl;
    std::cout << "path: " << modinfo.path << std::endl;
    std::cout << "base: " << modinfo.base << std::endl;
    std::cout << "size: " << modinfo.size << std::endl;
    std::cout << "end:  " << modinfo.end  << std::endl;
}

On this block of code, the target program simply crashes. When calling "dlopen" on GDB, the output is:

Program received signal SIGSEGV, Segmentation fault.
0xf7c19404 in __memcpy_sse2_unaligned () from /usr/lib32/libc.so.6
The program being debugged was signaled while in a function called from GDB.
GDB remains in the frame where the signal was received.
To change this behavior use "set unwindonsignal on".
Evaluation of the expression containing the function
(dlopen) will be abandoned.

The target program code is:

//target.cpp
#include <iostream>
#include <dlfcn.h>

int main()
{
    while(true);
    return 0;
}

Command line to compile this code:

clang++ -m32 target.cpp -o build/target -ldl
clang++ -m32 -shared -fPIC main.cpp mem/mem.cpp -o build/libinject.so -ldl

Not being able to use global variables is a big deal, so what could be causing this crash and how do I fix it?


Solution

  • As pointed by yugr, "C++ global constructors run after C constructors (i.e. functions marked with attribute((constructor))". With that in mind, I thought about a way around this: creating a C thread that would run on the background AFTER the C++ stuff loaded using PThread. Then I did this to test out:

    //main.cpp (shared library)
    #include <iostream>
    #include <pthread.h>
    
    typedef struct
    {
        std::string name;
        std::string path;
        void*       base;
        void*       end;
    }example_t;
    
    example_t test()
    {
        example_t example;
        example.name = "hello";
        example.path = "test";
        example.base = (void*)0;
        example.end = (void*)0;
        return example;
    }
    
    example_t global_var;
    
    void* thread_init(void* _args)
    {
        global_var = test();
        std::cout << "global_var name: " << global_var.name << std::endl;
        std::cout << "global_var path: " << global_var.path << std::endl;
        std::cout << "global_var base: " << global_var.base << std::endl;
        std::cout << "global_var end:  " << global_var.end  << std::endl;
        return (void*)NULL;
    }
    
    __attribute__((constructor))
    void libmain()
    {
        pthread_t thread;
        pthread_create(&thread, NULL, (void*(*)(void*))thread_init, (void*)NULL);
    }
    
    //target.cpp (target program)
    #include <iostream>
    #include <dlfcn.h>
    
    int main()
    {
        while(true);
        return 0;
    }
    

    ... using the following script to compile:

    if [ ! -d build ]; then
        mkdir build
    fi
    
    clang++ -g -m32 -shared -fPIC main.cpp -o build/libinject.so -pthread
    clang++ -g -m32 target.cpp -o build/target -ldl
    

    ... and this one to inject the shared library:

    #/bin/bash
    
    if [ "$EUID" -ne 0 ]; then
        echo "[!] Run as root"
        exit 0
    fi
    
    if [ ! command -v gdb &> /dev/null ]; then
        echo "[!] Unable to find GDB, make sure you have it installed"
        exit 0
    fi
    
    proc_name="target"
    proc_id=""
    libpath="$(pwd)/build/libinject.so"
    
    if ! proc_id=$(pgrep $proc_name) > /dev/null 2>&1; then
        echo "[!] The target process is not running"
        exit 0
    fi
    
    if [ ! -f $libpath ]; then
        echo "[!] Invalid shared library file"
    fi
    
    gdb -n -q -batch \
      -ex "attach $proc_id" \
      -ex "set \$dlopen = (void*(*)(char*, int)) dlopen" \
      -ex "call \$dlopen(\"$libpath\", 1)" \
      -ex "detach" \
      -ex "quit" > /dev/null 2>&1
    
    echo "[*] done"
    
    exit 1
    

    guess what? worked just fine