Search code examples
phpcdlopenld-preloaddlsym

How to override php_network_connect_socket_to_host in PHP with LD_PRELOAD and dlsym?


I am trying to override "php_network_connect_socket_to_host" and cannot do it, although I am able to override "connect".

Here is the source code of wrapper.c:

#define _GNU_SOURCE 1
#include <stdio.h>
#include <arpa/inet.h>
#include <dlfcn.h>
#include <string.h>

#include "php.h"
#include "php_network.h"

// libc
int (*real_connect)(int, const struct sockaddr *, socklen_t);

// php-src/main/network.c
int (*real_php_network_connect_socket_to_host)(const char *, unsigned short,
int, int, struct timeval *, zend_string **,
int *, const char *, unsigned short, long);

void _init (void)
{
    const char *err;

    real_connect = dlsym (RTLD_NEXT, "connect");
    if ((err = dlerror()) != NULL) printf("dlsym (connect): %s\n", err);

    void *handle = dlopen("", RTLD_GLOBAL);
    real_php_network_connect_socket_to_host = dlsym (handle, "php_network_connect_socket_to_host");
    if ((err = dlerror()) != NULL) printf("dlsym (php_network_connect_socket_to_host): %s\n", err);
}

int connect(int fd, const struct sockaddr *sk, socklen_t sl)
{
    static struct sockaddr_in *sk_in;
    sk_in = (struct sockaddr_in *)sk;

    printf("[+] connect wrapper: %d %s:%d\n", fd, inet_ntoa(sk_in->sin_addr), ntohs(sk_in->sin_port));

    return real_connect(fd, sk, sl);
}

int php_network_connect_socket_to_host(const char *host, unsigned short port,
int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string,
int *error_code, const char *bindto, unsigned short bindport, long sockopts)
{
    printf("[+] php_network_connect_socket_to_host wrapper\n");

    return real_php_network_connect_socket_to_host(host, port, socktype, asynchronous, timeout, error_string, error_code, bindto, bindport, sockopts);
}

I compile the .so with this command:

gcc `php-config --includes` -nostartfiles -fpic -shared wrapper.c -o wrapper.so -ldl

The symbol names look normal and match with the values in wrapper.c.

Here is the output of this command: readelf -Ws /usr/bin/php8.2 | grep -E "connect|php_network_connect_socket_to_host":

Symbol table '.dynsym' contains 3015 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
   217: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND connect@GLIBC_2.2.5 (5)
  1388: 000000000029f910  1519 FUNC    GLOBAL DEFAULT   14 php_network_connect_socket_to_host
...

ldd /usr/bin/php8.2:

    linux-vdso.so.1 (0x00007ffd38b75000)
    libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007fc9f5ccb000)
    libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007fc9f5cc6000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fc9f5be7000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fc9f5be2000)
    libxml2.so.2 => /lib/x86_64-linux-gnu/libxml2.so.2 (0x00007fc9f5454000)
    libssl.so.1.1 => /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007fc9f53c1000)
    libcrypto.so.1.1 => /lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007fc9f5000000)
    libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fc9f5327000)
    libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fc9f5bc1000)
    libsodium.so.23 => /lib/x86_64-linux-gnu/libsodium.so.23 (0x00007fc9f4fa6000)
    libargon2.so.1 => /lib/x86_64-linux-gnu/libargon2.so.1 (0x00007fc9f5bb7000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc9f4dc5000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fc9f5cff000)
    libicuuc.so.72 => /lib/x86_64-linux-gnu/libicuuc.so.72 (0x00007fc9f4bc7000)
    liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007fc9f5b86000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc9f5322000)
    libicudata.so.72 => /lib/x86_64-linux-gnu/libicudata.so.72 (0x00007fc9f2c00000)
    libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fc9f2800000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fc9f5302000)`

Here is the php script itself (test.php):

<?php

$host = 'ssl://google.com:443';
$sock = stream_socket_client($host, $errNo, $errStr, 3, STREAM_CLIENT_CONNECT);

if (!$sock) {
     echo "ERROR: $errNo - $errStr \n";
}
else {
    echo "connection established\n\n";
    echo "local socket name: ". stream_socket_get_name($sock, false) . "\n";
}

and run the the PHP script this way: LD_PRELOAD=./wrapper.so /usr/bin/php8.2 test.php

And here is the output of test.php:

dlsym() returned successfully for connect
dlsym() returned successfully for php_network_connect_socket_to_host
[+] connect wrapper: 5 216.58.212.46:443
connection established
local socket name: 192.168.0.34:42078

As seen from the output, connect() is successfully overridden, no errors are given and when the PHP code is trying to execute connect() it executes the wrapper instead which is what I want.

However, that is not the case with php_network_connect_socket_to_host(). dlsym() returns successfully and no error is logged but the it looks like the function has never been overridden because after the PHP invocation of "stream_socket_client" the wrapper in C does not get called. I did check if the stream_socket_client implementation is using internally "php_network_connect_socket_to_host" and I can confirm it is because I ran the C code and followed the execution flow.

I am getting an error that the symbol cannot be found if I try the following code: real_connect = dlsym (RTLD_NEXT, "php_network_connect_socket_to_host");

This is why I am trying with dlopen() first.

Maybe the problem is related to the way I load the .so. Also I saw that the values in the readelf output for few of the fields differ a lot between connect and php_network_connect_socket_to_host (the fields are: value, size, Ndx) Any help appreciated.

Thanks!


Solution

  • This is because real_php_network_connect_socket_to_host is defined within the PHP binary itself and not in an external shared object like connect is.

    The PHP binary is almost-certainly calling it through the compile-time-known offset rather than through a runtime lookup (through dlsym for example).

    If you want to hijack the function, you might be able to do some runtime memory patching to replace the function body with an assembly jump to your replacement implementation.