lddx: ldd extended
09.08.2020 [Software]
I often run into the situation, where I need to know the direct linking dependencies of an executable and a simple way to copy them and change their rpath to $ORIGIN. I use the following script for more than 15 years now and it has become one of the standard tools in my binary analysis and packaging toolbox.

Usage

The basic usage is lddx <executable>. This lists all direct dependencies of the specified executable.

Let's take a look at the options:
Options:
  -h, --help    show this help message and exit
  -c directory  copy the libs into directory
  -i            ignore libs in /lib* and /usr
  -r            recursive mode
  -p            set rpath of copied libs to $ORIGIN (needs patchelf)

I think, the help is pretty much self-explanatory. You can copy the direct dependencies or all dependencies into a folder by using -c and -r as arguments, optionally ignoring libraries from the system directories (-i switch) and let the script set the rpath to $ORIGIN with -p. Pretty handy, right?

The script

File: lddx
#!/usr/bin/env python3

import optparse
import sys
import os
import glob
import subprocess
import shutil
import re

class OptionParser(optparse.OptionParser):
    def __init__(self):
        super().__init__(self.usage)
        
        self.add_option('-c', type="string", metavar='directory', dest='copy', help='copy the libs into directory')
        self.add_option('-i', action="store_true", dest='ignore_system', help='ignore libs in /lib* and /usr')
        self.add_option('-r', action="store_true", dest='recursive', help='recursive mode')
        self.add_option('-p', action="store_true", dest='patchelf', help='set rpath of copied libs to $ORIGIN (needs patchelf)')
        
        
    def parse(self):
        (self.options, self.args) = self.parse_args()

    usage = ''


def neededLibs(filename):
    ret = subprocess.check_output(['readelf', '-d', filename], stderr=subprocess.STDOUT).decode('utf-8').split('\n')

    regex = re.compile('.*NEEDED.*')
    needed = list()
    for line in ret:
        if regex.match(line):
            needed.append(line.split(' ')[-1][1:-1])

    return needed
    
    
def dlls(filename):
    ret = subprocess.check_output(['ldd', filename]).decode('utf-8').split('\n')
    dlls = dict()
    for line in ret:
        spl = line.split(' => ')
        if len(spl) > 1:
            dlls[spl[0].strip()] = spl[1].split(' (0x')[0]
    
    return dlls


rec_shown = dict()
output = list()


def process(filename):
    if os.path.isfile(filename):
        libs = dlls(filename)
        for lib in neededLibs(filename):
            if lib in libs:
                if parser.options.ignore_system and (libs[lib].startswith('/usr') or libs[lib].startswith('/lib')):
                    continue
                    
                if lib in rec_shown:
                    continue
                    
                rec_shown[lib] = True
            
                output.append('\t' + lib + ' => ' + libs[lib])
                if parser.options.copy:
                    if os.path.isfile(libs[lib]):
                        if  not os.path.isfile(os.path.join(parser.options.copy, lib)):
                            os.makedirs(parser.options.copy, exist_ok=True)
                            try:
                                shutil.copy(libs[lib], parser.options.copy)

                                if parser.options.patchelf:
                                    ret = subprocess.check_output(['patchelf', '--set-rpath', '$ORIGIN', os.path.join(parser.options.copy, lib)])
        
                            except:
                                pass
                            
                            
                    else:
                        print(os.path.basename(filename) + ': ' + lib + ' => ' + libs[lib])
                    
                if parser.options.recursive:
                    process(libs[lib])

    if filename == sys.argv[-1]:
        print('\n'.join(sorted(output)))


if __name__ == '__main__':
    parser = OptionParser()
    parser.parse()
    
    if len(sys.argv) == 1:
        parser.print_help()
        sys.exit(0)
    

    if os.path.isfile(sys.argv[-1]):
        process(sys.argv[-1])
            
    else:
        parser.print_help()