Leveraging LD_PRELOAD for Network Behavior Analysis: A Guide for Penetration Testers and Application Developers

Demystify the network behaviors of applications by leveraging the power of LD_PRELOAD! This blog post takes you on a journey to explore the inner workings of Linux applications as they communicate with the outside world.

Leveraging LD_PRELOAD for Network Behavior Analysis: A Guide for Penetration Testers and Application Developers

Introduction

In this blog post, we will discuss how to use the LD_PRELOAD environment variable to observe and analyze the network behaviors of an application in Linux. This technique allows us to intercept and monitor crucial system calls such as getaddrinfo, socket, connect, read, and write. By understanding the network interactions of an application, penetration testers and developers can identify potential vulnerabilities and understand communications protocols.

Table of Contents

  1. Overview of LD_PRELOAD
  2. Interception of getaddrinfo
  3. Interception of socket and connect
  4. Interception of read and write
  5. Bringing it all together
  6. Conclusion

Section 1

Overview: LD_PRELOAD is an environment variable in Linux that allows us to load custom shared libraries before loading the standard system libraries. This technique enables us to override specific system calls with our custom implementations, providing an opportunity to intercept, analyze, or modify the behavior of an application without changing its source code.

Section 2

Interception of getaddrinfo To intercept calls to getaddrinfo, we will create a shared library that wraps around the getaddrinfo system call. We will then use LD_PRELOAD to load our library before the application's standard libraries.

First we create the code for out shared library intercept_getaddrinfo.c that will intercept the getaddrinfo system call which is used to resolve DNS.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) {
    static int (*original_getaddrinfo)(const char *, const char *, const struct addrinfo *, struct addrinfo **) = NULL;

    if (!original_getaddrinfo) {
        original_getaddrinfo = dlsym(RTLD_NEXT, "getaddrinfo");
    }

    printf("Intercepted getaddrinfo() call for hostname: %s\n", node);

    return original_getaddrinfo(node, service, hints, res);
}
intercept_getaddrinfo.c

Once that is finished we compile the code into a shared library.

gcc -shared -fPIC -o intercept_getaddrinfo.so intercept_getaddrinfo.c -ldl
gcc compilation command

Section 3

Interception of socket and connect Similarly, we can intercept calls to socket and connect by creating a shared library that wraps around these system calls.

Again we will create our source file using the code example below.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int socket(int domain, int type, int protocol) {
    static int (*original_socket)(int, int, int) = NULL;

    if (!original_socket) {
        original_socket = dlsym(RTLD_NEXT, "socket");
    }

    int sockfd = original_socket(domain, type, protocol);

    printf("Intercepted socket() call, created socket with file descriptor: %d\n", sockfd);

    return sockfd;
}

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    static int (*original_connect)(int, const struct sockaddr *, socklen_t) = NULL;

    if (!original_connect) {
        original_connect = dlsym(RTLD_NEXT, "connect");
    }

    if (addr->sa_family == AF_INET) {
        struct sockaddr_in *addr_in = (struct sockaddr_in *)addr;
        printf("Intercepted connect() call to IP: %s, Port: %d\n", inet_ntoa(addr_in->sin_addr), ntohs(addr_in->sin_port));
    }

    return original_connect(sockfd, addr, addrlen);
}
intercept_socket_connect.c

And compile it using the same command as above, only substituting the names of our new source file.

gcc -shared -fPIC -o intercept_socket_connect.so intercept_socket_connect.c -ldl
gcc compilation command

Section 4

Interception of read and write To intercept calls to read and write, we will create another shared library that wraps around these system calls. By monitoring read and write calls, we can gain insights into the data being transmitted between the application and the remote server.

Intercepting the read and write calls is very similar however we are now dealing with buffers also which may contain all kinds of data like json, ascii or unicode strings or just raw binary data. Because of this our example will simply print the number of bytes that were read or written and where they were read or written from/to.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count) {
    static ssize_t (*original_read)(int, void *, size_t) = NULL;

    if (!original_read) {
        original_read = dlsym(RTLD_NEXT, "read");
    }

    ssize_t bytes_read = original_read(fd, buf, count);

    printf("Intercepted read() call, read %zd bytes from file descriptor: %d\n", bytes_read, fd);

    return bytes_read;
}

ssize_t write(int fd, const void *buf, size_t count) {
    static ssize_t (*original_write)(int, const void *, size_t) = NULL;

    if (!original_write) {
        original_write = dlsym(RTLD_NEXT, "write");
    }

    ssize_t bytes_written = original_write(fd, buf, count);

    printf("Intercepted write() call, wrote %zd bytes to file descriptor: %d\n", bytes_written, fd);

    return bytes_written;
}
intercept_read_write.c

This file is compiled with the same command as the previous two.

gcc -shared -fPIC -o intercept_read_write.so intercept_read_write.c -ldl
gcc compilation command

If you knew that you were dealing with ascii output, say from an application that reads and writes information to a web socket you could modify the above source file to also print the output of the read/write operations. By replacing the printf lines above with the lines shown below.

# Replace read printf with these lines
if (bytes_read > 0) {
	printf("Intercepted read() call, read %zd bytes from file descriptor: %d\n", bytes_read, fd);
	printf("Data read: %.*s\n", (int)bytes_read, (char *)buf);
}

# Replace write printf with these lines
if (bytes_written > 0) {
	printf("Intercepted write() call, wrote %zd bytes to file descriptor: %d\n", bytes_written, fd);
	printf("Data written: %.*s\n", (int)bytes_written, (char *)buf);
}

Keep in mind this intercepts all read and write calls so if the application you are intercepting works with files also you would want to filter to only print when read from or written to network socket file descriptors which could be done by saving a list of the file descriptors returned by the connect call.

Section 5

Bringing it all together Once we have created shared libraries for intercepting the desired system calls, we can use the LD_PRELOAD environment variable to load these libraries and run the target application. This will allow us to observe the application's network behavior in real-time or log the data for further analysis.

For this example we will combine all of the examples above into a single file so we can load just one library to catch all of the calls.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) {
    static int (*original_getaddrinfo)(const char *, const char *, const struct addrinfo *, struct addrinfo **) = NULL;

    if (!original_getaddrinfo) {
        original_getaddrinfo = dlsym(RTLD_NEXT, "getaddrinfo");
    }

    printf("Intercepted getaddrinfo() call for hostname: %s\n", node);

    return original_getaddrinfo(node, service, hints, res);
}

int socket(int domain, int type, int protocol) {
    static int (*original_socket)(int, int, int) = NULL;

    if (!original_socket) {
        original_socket = dlsym(RTLD_NEXT, "socket");
    }

    int sockfd = original_socket(domain, type, protocol);

    printf("Intercepted socket() call, created socket with file descriptor: %d\n", sockfd);

    return sockfd;
}

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    static int (*original_connect)(int, const struct sockaddr *, socklen_t) = NULL;

    if (!original_connect) {
        original_connect = dlsym(RTLD_NEXT, "connect");
    }

    if (addr->sa_family == AF_INET) {
        struct sockaddr_in *addr_in = (struct sockaddr_in *)addr;
        printf("Intercepted connect() call to IP: %s, Port: %d\n", inet_ntoa(addr_in->sin_addr), ntohs(addr_in->sin_port));
    }

    return original_connect(sockfd, addr, addrlen);
}

ssize_t read(int fd, void *buf, size_t count) {
    static ssize_t (*original_read)(int, void *, size_t) = NULL;

    if (!original_read) {
        original_read = dlsym(RTLD_NEXT, "read");
    }

    ssize_t bytes_read = original_read(fd, buf, count);

    if (bytes_read > 0) {
        printf("Intercepted read() call, read %zd bytes from file descriptor: %d\n", bytes_read, fd);
        printf("Data read: %.*s\n", (int)bytes_read, (char *)buf);
    }

    return bytes_read;
}

ssize_t write(int fd, const void *buf, size_t count) {
    static ssize_t (*original_write)(int, const void *, size_t) = NULL;

    if (!original_write) {
        original_write = dlsym(RTLD_NEXT, "write");
    }

    ssize_t bytes_written = original_write(fd, buf, count);

    if (bytes_written > 0) {
		printf("Intercepted write() call, wrote %zd bytes to file descriptor: %d\n", bytes_written, fd);
		printf("Data written: %.*s\n", (int)bytes_written, (char *)buf);
	}

    return bytes_written;
}
intercept_combined.c

Compile into a shared library

gcc -shared -fPIC -o intercept_combined.so intercept_combined.c -ldl
gcc compilation command

Then run your application using the LD_PRELOAD environment variable to load your intercept library.

LD_PRELOAD=./intercept_combined.so ./your_application

Section 6

Conclusion: Understanding an application's network behavior is crucial for both penetration testing and application development. By leveraging LD_PRELOAD to intercept and monitor critical system calls, we can gain valuable insights into how an application interacts with the network and identify potential vulnerabilities or areas for optimization.