Question
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!