Performing a MacGyver to Call Anyplace Home

by Rafael Santiago

In this tiny article, I will show you a workaround that can be helpful in situations when you need to run some types of applications in machines that cannot be updated and also are not able to build any kind of software natively.  Maybe the software is too new, maybe the system is too old, and vice versa.  With this approach, you are be able in most cases to execute a self-contained binary package in several systems having different versions of libraries, without any updating necessity.

Introduction

Sometimes it is necessary to run an application in several different versions/distributions of an operating system, especially Linux.  The problem with this issue is that the compiled program cannot be successfully executed in all machines because the libraries will vary from one OS version to another.

Updating in practice tends to be a tedious bureaucratic task for some large production environments.  You can't always just run an updating task.  In most cases, you need to plan it in advance, ask/inform a couple of departments, convince the customer, and so on...

What About Carrying Your Home on Your Shoulders?

This is not always the better solution, but in some cases it can be a nice workaround.  The idea is basically to compile the software once and scan the related dependency libraries in order to collect all of them.  Afterwards, what you should do is distribute the binary and the libraries together.

You need to recompile and some care about this recompilation must be observed.

How Can I Find All Dependencies of Software?

You should use the environment variable called LD_TRACE_LOADED_OBJECTS when calling the application:

$ LD_TRACE_LOADED_OBJECTS=1 ./app

When executed, a list of all libraries will be shown on the console.  These libraries should be copied and distributed together with your software.

Example:

$ LD_TRACE_LOADED_OBJECTS=1 /bin/sh
	linux-vdso.so.1 (0x00007ffde177c000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f221747d000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f2217668000)

Recompiling the Software

When recompiling the software, you need to pass to the compiler two important options: -rpath and -dynamic-linker.

The -rpath option specifies the directory where the libraries should be searched during the executable loading.

The -dynamic-linker is related to the dynamic linker loader and is actually a linker flag.  In GCC the basic command line would be:

The -Wl,<options> is to pass comma-separated <options> on to the linker.

gcc -rpath <directory> -Wl, -dynamic-linker=<directory>

The better choice is let -rpath and -dynamic-linker point to the same directory, which should be a directory where you will create and copy all dependencies and binaries and execute the application from there.

Notice that the compilation command line shown is for software written in C or C++.  In other languages that generate ELF, this technique will also work, but the method of setting up the -rpath and -dynamic-linker may be different.  Try to Google for more information on this.

Automating the Generation of the Package

I have written a shell script (Bash-based) that can scan executables and also libraries.  This script collects and compresses the dependencies, making it ready for distribution.  It also uses some other minor techniques that I will abstract for the sake of brevity.

The usage of this script is as follows:

$ ./snail.sh --directory <path containing the binaries to be scanned> --output <zip out>

Once the -rpath and -dynamic-linker are configured after a recompilation, all that you should do is deploy the zipped dependencies along with the executables and libraries.  The important thing here is to extract the dependencies to the directory where the -rpath is pointing.  Again: the better choice is to let -rpath and -dynamic-linker point to the same directory.

In the code listing below, you can see the entire shell script.  Some users will need to adjust the shebang path according to their systems.  You can download the script at: github.com/rafael-santiago/snail.

#!/bin/bash
#
#                              Copyright (C) 2015 by Rafael Santiago
#
# This is free software. You can redistribute it and/or modify under
# the terms of the GNU General Public License version 2.
#
# "snail.sh"
#       by Rafael Santiago
#
# Description: a simple script which scans ELF dependencies.
#

SNAIL_TEMP_DIR=".snail"

SNAIL_LD_32=""

SNAIL_LD_64=""

SHOULD_REMOVE_INTERP=0

INTERP_PATH=""

function snail_find_app_deps() {
    temp_interp=$(setup_interp $1)
    printf "\t\t@@@ - Inspecting %s's dependencies...\n" $1
    for libpath in $(LD_TRACE_LOADED_OBJECTS=1 $1 | grep ".*/" | sed s/.*=\>// | sed s/\(.*//)
    do
    filename=$(basename ${libpath})
    file_exists=$(ls -1 ${SNAIL_TEMP_DIR}/${filename} 2>/dev/null | wc -l)
    if [ ${file_exists} -eq 0 ] ; then
        printf "\t\t\t@@@ - copying: %s... " ${filename}
            cp ${libpath} ${SNAIL_TEMP_DIR}/ &>/dev/null
            if [ $? -eq 0 ] ; then
            printf "copied.\n"
            else
            printf "copy error... aborting.\n"
            fini_snail
            exit 1
            fi
        else
            printf "\t\t\t@@@ - already copied: %s.\n" ${filename}
    fi
    done
    if [ ! -z "$temp_interp" ] ; then
        remove_interp $temp_interp
    fi
    printf "\t\t@@@ - done.\n"
}

function snail_find_so_deps() {
    temp_interp=$(setup_interp $1)
    ld_so=${SNAIL_LD_32}
    if [ $(get_platform_arch) -eq 64 ] ; then
        ld_so=${SNAIL_LD_64}
    fi
    printf "\t\t@@@ - Inspecting %s's dependencies...\n" $1
    for libpath in $(LD_TRACE_LOADED_OBJECTS=1 ${ld_so} ./$1 | grep ".*/" | sed s/.*=\>// | sed s/\(.*//)
    do
        filename=$(basename ${libpath})
        file_exists=$(ls -1 ${SNAIL_TEMP_DIR}/${filename} 2>/dev/null | wc -l)
        if [ ${file_exists} -eq 0 ] ; then
            printf "\t\t\t@@@ - copying: %s... " ${filename}
            cp ${libpath} ${SNAIL_TEMP_DIR}/ &>/dev/null
            if [ $? -eq 0 ] ; then
                printf "copied.\n"
            else
                printf "copy error... aborting.\n"
                fini_snail
                exit 1
            fi
        else
            printf "\t\t\t@@@ - already copied: %s.\n" ${filename}
        fi
    done
    printf "\t\t@@@ - done.\n"
    filename=$(basename ${ld_so})
    file_exists=$(ls -1 ${SNAIL_TEMP_DIR}/${filename} 2>/dev/null | wc -l)
    if [ ${file_exists} -eq 0 ] ; then
        printf "\t\t@@@ - copying: %s... " ${filename}
        if [ $? -eq 0 ] ; then
            printf "copied.\n"
        else
            printf "copy error... aborting.\n"
            fini_snail
            exit 1
        fi
    fi
    if [ ! -z "$temp_interp" ] ; then
        remove_interp $temp_interp
    fi
}

function is_a_so() {
    retval=0
    if [ $(file $1 | grep ".*: ELF.*shared object," | wc -l) -eq 1 ] ; then
        retval=1
    fi
    echo ${retval}
}

function get_elf_arch() {
    retval=32
    if [ $(file $1 | grep ".*: ELF 64-bit" | wc -l) -eq 1 ] ; then
        retval=64
    fi
    echo ${retval}
}

LIB_SEARCH_LOCATIONS="/lib /lib32 /lib64"
function find_ld_linux32() {
    SNAIL_LD_32=$(find ${LIB_SEARCH_LOCATIONS} -name "ld-linux.so.2" -executable 2>/dev/null | tail -1)
}

function find_ld_linux64() {
    SNAIL_LD_64=$(find ${LIB_SEARCH_LOCATIONS} -name "ld-linux-x86-64.so.2" -executable 2>/dev/null | tail -1)
}

function get_platform_arch() {
    retval=32
    if [ $(uname -a | grep ".*x86_64" | wc -l) -eq 1 ] ; then
        retval=64
    fi
    echo ${retval}
}

function init_snail() {
    rm -rf ${SNAIL_TEMP_DIR}
    mkdir ${SNAIL_TEMP_DIR}
    find_ld_linux32
    if [ $(get_platform_arch) -eq 64 ] ; then
        find_ld_linux64
    fi
}

function setup_interp() {
    interpreter=""
    filename=$1
    if [ $(file ${filename} | grep ".*: ELF" | wc -l) -eq 1 ] ; then
        if [ $(is_a_so ${filename}) -ne 1 ] ; then
            interpreter=$(readelf -l ${filename} | grep "\\[.*:.*\\]" | sed s/.*\\[// | sed s/.*:// | sed s/\\].*//)
        fi
    fi

    if [ ! -z ${interpreter} ] ; then
        if [ ! -f ${interpreter} ] ; then
            mkdir -p $(dirname ${interpreter})
            if [ $(get_platform_arch) -eq 32 ] ; then
                cp ${SNAIL_LD_32} ${interpreter} &>/dev/null
            else
                cp ${SNAIL_LD_64} ${interpreter} &>/dev/null
            fi
            echo ${interpreter}
        fi
    fi
}

function remove_interp() {
    if [ -f $1 ]; then
        printf "\t\t\t@@@ - removing temporary interpreter $1\n"
        rm $1 &>/dev/null
        rmdir $(dirname $1) &>/dev/null
    fi
}

function fini_snail() {
    rm -rf ${SNAIL_TEMP_DIR}
}

function zip_deps() {
    printf "@@@ - Zipping all collected dependencies into %s... " $1
    rm $1 &>/dev/null
    zip -j $1 ${SNAIL_TEMP_DIR}/* &>/dev/null
    if [ $? -eq 0 ] ; then
        printf "ok.\n"
    else
        printf "zip error... aborting.\n"
        fini_snail
        exit 1
    fi
    printf "@@@ - done.\n"
}

function snail() {
    printf "@@@@@@@@@@@@@@@@@@@@@\n"
    printf "@@@ - S n a i l - @@@\n"
    printf "@@@@@@@@@@@@@@@@@@@@@\n\n"
    printf "@@@ - Initialising...\n"
    init_snail $1
    printf "@@@ - done.\n\n"
    printf "@@@ - Now, looking for ELFs in directory %s...\n" $1
    for filename in $(find $1 -executable -type f -print)
    do
        if [ $(file ${filename} | grep ".*: ELF" | wc -l) -eq 1 ] ; then
            if [ $(is_a_so ${filename}) -eq 1 ] ; then
                printf "\t@@@ - Shared object: %s\n" ${filename}
                snail_find_so_deps ${filename}
            else
                printf "\t@@@ - Executable found: %s\n" ${filename}
                snail_find_app_deps ${filename}
            fi
        fi
    done
    printf "@@@ - done.\n"
    zip_deps $2
    fini_snail

}

# main() {

directory=""
output=""

while test -n "$1"
do
    case "$1" in
        -d | --directory)
            shift
            directory="$1"
            ;;

        -o | --output)
            shift
            output="$1"
            ;;

        -h | --help)
            printf "use: $0 --directory <directory containing your binaries> --output <output file path>\n"
            exit 1
            ;;
    esac
    shift
done

if [ -z ${directory} ] ; then
    printf "error: --directory option is missing.\n"
    exit 1
fi

if [ -z ${output} ] ; then
    printf "error: --output option is missing.\n"
    exit 1
fi

snail ${directory} ${output}

# }

A sample of the script's output is as follows:

$ ./snail.sh --directory test --output test.zip
@@@@@@@@@@@@@@@@@@@@@
@@@ - S n a i l - @@@
@@@@@@@@@@@@@@@@@@@@@

@@@ - Initialising...
@@@ - done.

@@@ - Now, looking for ELFs in directory test...
	@@@ - Executable found: test/lex
		@@@ - Inspecting test/lex's dependencies...
			@@@ - copying: libc.so.7... copied.
		@@@ - done.
	@@@ - Executable found: test/more
		@@@ - Inspecting test/morse's dependencies...
			@@@ - already copied: libc.so.7.
		@@@ - done.
	@@@ - Executable found: test/vi
		@@@ - Inspecting test/vi's dependencies...
			@@@ - copying: libutil.so.9... copied.
			@@@ - copying: libncursesw.so.8... copied.
			@@@ - already copied: libc.so.7.
		@@@ - done.
@@@ - done.
@@@ - Zipping all collected dependencies into test.zip... ok.
@@@ - done.

As you can see, this output indicates that the subdirectory test was scanned and three executables were found: lex, morse, and vi.  The libraries libc.so.morse, and vi.  The libraries libc.so.77, libutil.so.9, and libncursesw.so.8 were copied and zipped into test.zip.  The ZIP file and the executables (already recompiled) can be successfully executed in other systems with different library versions without any update necessary over there.

Conclusion

This approach is a good way of solving little problems with software deployment, especially in operating systems that come with several distributions - and with each distribution having little changes in some shared libraries (a.k.a. Linux XYZ*).  These little changes tend to make the software incompatible from one version to another.

Originally, I wrote this script facing deployment problems in Linux, but it could be extended to other UNIX environments.

I think that the above technique should not be applied in software related to information security, since it can open possibilities of library hooking.

You always should think of this technique as a simple MacGyver workaround, a last resort, "people stop calling me," and so on.

Code snail.sh

Return to $2600 Index