How do I get the path of the directory opened from opendir()?

91 Views Asked by At

I'm attempting to do function hooking of readdir(). I want to be able to intercept a readdir() call only if a specific path is opened. Essentially, I want to reverse opendir() in that I want to be able to recover the directory path from a DIR struct.

I am aware one way of doing this is just opening the directory I want to intercept and comparing both structs, but the directory I am looking to intercept have exactly the same files I don't want to intercept in other directories. Is there a way to do what I am looking for?

1

There are 1 best solutions below

0
Craig Estey On

If we had control of the actual target program source, we could just add calls surrounding the desired calls.

However, I'm going to assume that you want to be able to "spy" on a given binary program.

We can do this by creating a shared library (e.g. spy.so) that intercepts opendir/readddir/closedir using dlsym et. al.

When we want to spy on a given program, we do:

env LD_PRELOAD=/path_to_spy_library/spy.so /path_to_target_program/test

This "spy" library will:

  1. Upon initialization save a list of directories that are "of interest". Here, for simplicity, we use the environment variable SPYDIR, a colon separated list of directories (similar to PATH).
  2. When the library intercepts the opendir call, it will check the argument against this "desired/passive" list.
  3. If there's a match, it will add an entry to an "active" list, saving the DIR * return value from [the real] opendir
  4. When readdir is intercepted, the DIR * argument is matched within the active list. If a match, we "intercept" the readdir
  5. When closedir is called, it removes the corresponding entry from the active list [if any].

Here is the shared "spy" library source (spy.c). It is annotated:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dlfcn.h>
#include <dirent.h>

// directory entry
typedef struct spydir {
    struct spydir *spy_next;
    DIR *spy_dir;                           // stream
    union {
        struct spydir *spy_par;             // parent/containing directory
        char *spy_name;                     // directory name
    };
} spydir_t;

// list of directories
typedef struct {
    spydir_t *spy_head;
} spylist_t;

enum {
    LIST_PASSIVE,                           // directories of interest
    LIST_ACTIVE                             // directory streams being spied on
};

static spylist_t spylist[2];                // directories of interest

// spylistof -- locate directory control
static spylist_t *
spylistof(int listidx)
{

    return &spylist[listidx];
}

// spydef -- load up real function
static void *
spydef(const char *sym)
{
    void *realfunc = dlsym(RTLD_NEXT,sym);

    if (realfunc == NULL) {
        perror(sym);
        exit(1);
    }

    return realfunc;
}

#define SPYDEF(_ret,_fnc,_arg) \
    static _ret (*realfunc)(_arg) = NULL; \
    if (realfunc == NULL) \
        realfunc = spydef(#_fnc)

// spyadd_byname -- add directory to list
static spydir_t *
spyadd_byname(int listidx,const char *name)
{
    spylist_t *list = spylistof(listidx);
    spydir_t *spy;

    // NOTE: unlikely but, we allow multiple open streams for the same directory
    for (spy = list->spy_head;  spy != NULL;  spy = spy->spy_next) {
        if (strcmp(spy->spy_name,name) == 0)
            break;
    }

    do {
        if (spy != NULL)
            break;

        spy = calloc(1,sizeof(*spy));

        spy->spy_name = strdup(name);

        spy->spy_next = list->spy_head;
        list->spy_head = spy;
    } while (0);

    return spy;
}

// spyadd_bydir -- add directory to list
static spydir_t *
spyadd_bydir(int listidx,spydir_t *par,DIR *dir)
{
    spylist_t *list = spylistof(listidx);

    spydir_t *spy = calloc(1,sizeof(*spy));
    spy->spy_par = par;
    spy->spy_dir = dir;

    spy->spy_next = list->spy_head;
    list->spy_head = spy;

    return spy;
}

// spyfind_byname -- locate directory by name
static spydir_t *
spyfind_byname(int listidx,const char *name)
{
    const spylist_t *list = spylistof(listidx);
    spydir_t *spy;

    for (spy = list->spy_head;  spy != NULL;  spy = spy->spy_next) {
        if (strcmp(spy->spy_name,name) == 0)
            break;
    }

    return spy;
}

// spyfind_bydir -- locate directory by DIR entry
static spydir_t *
spyfind_bydir(int listidx,DIR *dir)
{
    const spylist_t *list = spylistof(listidx);
    spydir_t *spy;

    for (spy = list->spy_head;  spy != NULL;  spy = spy->spy_next) {
        if (spy->spy_dir == dir)
            break;
    }

    return spy;
}

// spydel_bydir -- delete directory by DIR entry
static void
spydel_bydir(int listidx,DIR *dir)
{
    spylist_t *list = spylistof(listidx);
    spydir_t *spy;
    spydir_t *next = NULL;
    spydir_t *prev = NULL;

    for (spy = list->spy_head;  spy != NULL;  spy = next) {
        next = spy->spy_next;
        if (spy->spy_dir == dir)
            break;
        prev = spy;
    }

    do {
        if (spy == NULL)
            break;

        if (prev != NULL)
            prev->spy_next = next;
        else
            list->spy_head = next;

        spy->spy_par = NULL;

        free(spy);
    } while (0);
}

// opendir -- emulate opendir
DIR *
opendir(const char *name)
{
    SPYDEF(DIR *,opendir,const char *);

    DIR *dir = realfunc(name);
    int sverr = errno;

    do {
        // handle opendir failure
        if (dir == NULL)
            break;

        // is directory one we're interested in?
        spydir_t *spy = spyfind_byname(LIST_PASSIVE,name);

        // no, bug out
        if (spy == NULL)
            break;

        // yes, add stream to list of actively spied on directory streams
        spy = spyadd_bydir(LIST_ACTIVE,spy,dir);
    } while (0);

    errno = sverr;

    return dir;
}

// readdir -- emulate readdir
struct dirent *
readdir(DIR *dir)
{
    SPYDEF(struct dirent *,readdir,DIR *);

    struct dirent *ent = realfunc(dir);

    do {
        if (ent == NULL)
            break;

        // is this a stream we're spying on?
        spydir_t *spy = spyfind_bydir(LIST_ACTIVE,dir);

        // no, bug out
        if (spy == NULL)
            break;

        // yes, just for demo -- prove we intercepted the call
        // replace this with whatever code is desired ...
        ent->d_name[0] = '%';

        // if we want to know the directory name ...
#if 0
        spydir_t *owner = spy->spy_par;
        printf("readdir: directory owner='%s'\n",owner->spy_name);
#endif
    } while (0);

    return ent;
}

// closedir -- emulate closedir
int
closedir(DIR *dir)
{
    SPYDEF(int,closedir,DIR *);

    spydel_bydir(LIST_ACTIVE,dir);

    int err = realfunc(dir);

    return err;
}

// init -- library constructor
static void __attribute__((constructor))
init(void)
{
    char *dirs;

    do {
        // get a list of directories we want to intercept
        char *cp = getenv("SPYDIR");
        if (cp == NULL)
            break;

        dirs = strdupa(cp);
        cp = strtok(dirs,":");

        while (cp != NULL) {
            spyadd_byname(LIST_PASSIVE,cp);
            printf("init: SPYDIR '%s'\n",cp);
            cp = strtok(NULL,":");
        }

        fflush(stdout);
    } while (0);
}

// fini -- library destructor
static void __attribute__((destructor))
fini(void)
{
    spylist_t *list;
    spydir_t *next;

    // this should _always_ be empty if target program calls closedir properly
    list = spylistof(LIST_ACTIVE);
    for (spydir_t *spy = list->spy_head;  spy != NULL;  spy = spy->spy_next) {
        spydir_t *par = spy->spy_par;
        printf("fini: closedir not called -- spy_dir=%p spy_name='%s'\n",
            spy->spy_dir,par->spy_name);
    }

    list = spylistof(LIST_PASSIVE);
    for (spydir_t *spy = list->spy_head;  spy != NULL;  spy = next) {
        next = spy->spy_next;
        printf("fini: spy_name='%s'\n",spy->spy_name);

        free(spy->spy_name);
        spy->spy_name = NULL;

        free(spy);
    }

    list->spy_head = NULL;
}

Here is a test program (test.c):

#include <stdio.h>
#include <dirent.h>

void
dodir(const char *name)
{

    printf("\n");
    printf("dodir: DIR '%s'\n",name);

    DIR *dir = opendir(name);

    do {
        if (dir == NULL)
            break;

        int count = 0;
        while (1) {
            struct dirent *ent = readdir(dir);
            if (ent == NULL)
                break;

            // limit output for demo
            if (++count < 10)
                printf("dodir: '%s'\n",ent->d_name);
        }

        closedir(dir);
    } while (0);
}

int
main(int argc,char **argv)
{

    --argc;
    ++argv;

    dodir("/usr/bin");
    dodir("/etc");
    dodir("/usr/lib");
    dodir("/usr/lib64");
    dodir("/usr/local");

    return 0;
}

Build and run with this Makefile:

TARGETS += spy.so
TARGETS += test

export SPYDIR=/usr/bin:/etc

all: $(TARGETS)

run: $(TARGETS)
    env LD_PRELOAD=./spy.so ./test

clean:
    rm -f $(TARGETS)

spy.so:
    cc -fpic -shared -o spy.so spy.c -ldl -Wall -Werror

test:
    cc -o test test.c -Wall -Werror

Here is the sample output. In the demo, note that the "intercepted" calls replace the first character of the filename with %:

init: SPYDIR '/usr/bin'
init: SPYDIR '/etc'

dodir: DIR '/usr/bin'
dodir: '%'
dodir: '%.'
dodir: '%lvm-xray'
dodir: '%brt-action-analyze-xorg'
dodir: '%olphin'
dodir: '%webengine_convert_dict'
dodir: '%view'
dodir: '%cgrep'
dodir: '%owernow-k8-decode'

dodir: DIR '/etc'
dodir: '%'
dodir: '%.'
dodir: '%hadow'
dodir: '%ostname'
dodir: '%cmtk'
dodir: '%oomatic'
dodir: '%eport.d'
dodir: '%links.conf'
dodir: '%ron.monthly'

dodir: DIR '/usr/lib'
dodir: '.'
dodir: '..'
dodir: 'python3.7'
dodir: 'fipscheck'
dodir: 'libcrack.so.2.9.0'
dodir: 'crtn.o'
dodir: 'libnvidia-allocator.so'
dodir: 'libgd.so.3.0.5'
dodir: 'engines-1.1'

dodir: DIR '/usr/lib64'
dodir: '.'
dodir: '..'
dodir: 'libOpenNI.jni.so'
dodir: 'libkldap.so.4.14.38'
dodir: 'libQt5Location.so.5.11.3'
dodir: 'libtss2-tcti-mssim.so.0'
dodir: 'libknewstuff3.so.4'
dodir: 'libxapian.so.30'
dodir: 'libgoffice-0.8.so.8'

dodir: DIR '/usr/local'
dodir: 'thunderbird'
dodir: 'go'
dodir: 'cuda-10.2'
dodir: 'redacted'
dodir: 'firefox'
dodir: 'lib'
dodir: '.'
dodir: '..'
dodir: 'firefox.v95'