From f04087abbe3381ee3a546750a217b980143441d9 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Thu, 8 Aug 2024 11:12:05 -0700 Subject: [PATCH] WIP: try to fuzz RADIUS implementation - extract validate_radius_response() - add LLVM fuzzer implementation - suppress logging by default, let --log_min_messages override it - support const response buffer (and improve fuzzability) by computing Message-Authenticator piecewise --- meson-clang.ini | 4 + src/backend/libpq/auth.c | 336 +++++++++++++------------ src/backend/meson.build | 18 ++ src/common/meson.build | 16 ++ src/include/libpq/auth.h | 21 ++ src/test/modules/fuzzers/fuzz_radius.c | 74 ++++++ src/test/modules/fuzzers/meson.build | 24 ++ src/test/modules/meson.build | 1 + 8 files changed, 330 insertions(+), 164 deletions(-) create mode 100644 meson-clang.ini create mode 100644 src/test/modules/fuzzers/fuzz_radius.c create mode 100644 src/test/modules/fuzzers/meson.build diff --git a/meson-clang.ini b/meson-clang.ini new file mode 100644 index 0000000000..560cffe8e7 --- /dev/null +++ b/meson-clang.ini @@ -0,0 +1,4 @@ +[binaries] +c = 'clang-15' +cpp = 'clang++-15' +llvm-config = 'llvm-config-15' diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 40f0cabf3a..d97fd6e3f2 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2769,13 +2769,9 @@ CheckCertAuth(Port *port) * RADIUS authentication is described in RFC2865 (and several others). */ -#define RADIUS_VECTOR_LENGTH 16 #define RADIUS_HEADER_LENGTH 20 #define RADIUS_MAX_PASSWORD_LENGTH 128 -/* Maximum size of a RADIUS packet we will create or accept */ -#define RADIUS_BUFFER_SIZE 1024 - typedef struct { uint8 attribute; @@ -2783,16 +2779,6 @@ typedef struct uint8 data[FLEXIBLE_ARRAY_MEMBER]; } radius_attribute; -typedef struct -{ - uint8 code; - uint8 id; - uint16 length; - uint8 vector[RADIUS_VECTOR_LENGTH]; - /* this is a bit longer than strictly necessary: */ - char pad[RADIUS_BUFFER_SIZE - RADIUS_VECTOR_LENGTH]; -} radius_packet; - /* RADIUS packet types */ #define RADIUS_ACCESS_REQUEST 1 #define RADIUS_ACCESS_ACCEPT 2 @@ -2993,11 +2979,9 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH]; uint8 message_authenticator[RADIUS_VECTOR_LENGTH]; uint8 *message_authenticator_location; - uint8 message_authenticator_size; radius_packet radius_send_pack; radius_packet radius_recv_pack; radius_packet *packet = &radius_send_pack; - radius_packet *receivepacket = &radius_recv_pack; char *radius_buffer = (char *) &radius_send_pack; char *receive_buffer = (char *) &radius_recv_pack; int32 service = pg_hton32(RADIUS_AUTHENTICATE_ONLY); @@ -3216,7 +3200,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por struct timeval timeout; struct timeval now; int64 timeoutval; - const char *errstr = NULL; + int status; gettimeofday(&now, NULL); timeoutval = (endtime.tv_sec * 1000000 + endtime.tv_usec) - (now.tv_sec * 1000000 + now.tv_usec); @@ -3285,167 +3269,191 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por continue; } - if (packetlength < RADIUS_HEADER_LENGTH) + if (validate_radius_response(&status, receive_buffer, packet, packetlength, secret, server, user_name, requirema)) { - ereport(LOG, - (errmsg("RADIUS response from %s too short: %d", server, packetlength))); - continue; + closesocket(sock); + return status; } - if (packetlength != pg_ntoh16(receivepacket->length)) - { - ereport(LOG, - (errmsg("RADIUS response from %s has corrupt length: %d (actual length %d)", - server, pg_ntoh16(receivepacket->length), packetlength))); - continue; - } + /* otherwise, keep trying */ + } /* while (true) */ +} - if (packet->id != receivepacket->id) - { - ereport(LOG, - (errmsg("RADIUS response from %s is to a different request: %d (should be %d)", - server, receivepacket->id, packet->id))); - continue; - } +bool +validate_radius_response(int *status, + const char *receive_buffer, radius_packet *packet, + size_t packetlength, const char *secret, + const char *server, const char *user_name, bool requirema) +{ + radius_packet *receivepacket = (radius_packet *) receive_buffer; + uint8 *cryptvector; + uint8 encryptedpassword[RADIUS_MAX_PASSWORD_LENGTH]; + uint8 *message_authenticator_location; + uint8 message_authenticator_size; + uint8 message_authenticator[RADIUS_VECTOR_LENGTH]; + uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH] = {0}; + pg_hmac_ctx *hmac_context; + const char *errstr = NULL; - /* - * Verify the response authenticator, which is calculated as - * MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret) - */ - cryptvector = palloc(packetlength + strlen(secret)); - - memcpy(cryptvector, receivepacket, 4); /* code+id+length */ - memcpy(cryptvector + 4, packet->vector, RADIUS_VECTOR_LENGTH); /* request - * authenticator, from - * original packet */ - if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no - * attributes at all */ - memcpy(cryptvector + RADIUS_HEADER_LENGTH, receive_buffer + RADIUS_HEADER_LENGTH, packetlength - RADIUS_HEADER_LENGTH); - memcpy(cryptvector + packetlength, secret, strlen(secret)); - - if (!pg_md5_binary(cryptvector, - packetlength + strlen(secret), - encryptedpassword, &errstr)) - { - ereport(LOG, - (errmsg("could not perform MD5 encryption of received packet: %s", - errstr))); - pfree(cryptvector); - continue; - } - pfree(cryptvector); + if (packetlength < RADIUS_HEADER_LENGTH) + { + ereport(LOG, + (errmsg("RADIUS response from %s too short: %d", server, (int) packetlength))); + return false; + } - if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0) - { - ereport(LOG, - (errmsg("RADIUS response from %s has incorrect MD5 signature", - server))); - continue; - } + if (packetlength != pg_ntoh16(receivepacket->length)) + { + ereport(LOG, + (errmsg("RADIUS response from %s has corrupt length: %d (actual length %d)", + server, pg_ntoh16(receivepacket->length), (int) packetlength))); + return false; + } - /* Search for the Message-Authenticator attribute. */ - if (!radius_find_attribute((uint8 *) receive_buffer, - packetlength, - RADIUS_MESSAGE_AUTHENTICATOR, - &message_authenticator_location, - &message_authenticator_size)) - { - ereport(LOG, - (errmsg("RADIUS response from %s has malformed attributes", - server))); - continue; - } - else if (message_authenticator_location == NULL) - { - ereport(LOG, - (errmsg("RADIUS response from %s has no Message-Authenticator", - server))); + if (packet->id != receivepacket->id) + { + ereport(LOG, + (errmsg("RADIUS response from %s is to a different request: %d (should be %d)", + server, receivepacket->id, packet->id))); + return false; + } - /* We'll ignore this message, unless pg_hba.conf told us not to. */ - if (requirema) - continue; - } - else if (message_authenticator_size != RADIUS_VECTOR_LENGTH) - { - ereport(LOG, - (errmsg("RADIUS response from %s has unexpected Message-Authenticator size", - server))); - continue; - } - else - { - uint8 message_authenticator_copy[RADIUS_VECTOR_LENGTH]; + /* + * Verify the response authenticator, which is calculated as + * MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret) + */ + cryptvector = palloc(packetlength + strlen(secret)); - /* - * Save a copy of the received HMAC, and zero out the one in the - * message so that we have input required to recompute it. - */ - memcpy(message_authenticator_copy, - message_authenticator_location, - RADIUS_VECTOR_LENGTH); - memset(message_authenticator_location, - 0, - RADIUS_VECTOR_LENGTH); + memcpy(cryptvector, receivepacket, 4); /* code+id+length */ + memcpy(cryptvector + 4, packet->vector, RADIUS_VECTOR_LENGTH); /* request + * authenticator, from + * original packet */ + if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no + * attributes at all */ + memcpy(cryptvector + RADIUS_HEADER_LENGTH, receive_buffer + RADIUS_HEADER_LENGTH, packetlength - RADIUS_HEADER_LENGTH); + memcpy(cryptvector + packetlength, secret, strlen(secret)); - /* - * Compute the expected value. Note that the HMAC for - * Access-Accept and Access-Reject message uses the authenticator - * from the original Access-Request message, so we have to do a - * bit of splicing. - */ - hmac_context = pg_hmac_create(PG_MD5); - if (hmac_context == NULL || - pg_hmac_init(hmac_context, - message_authenticator_key, - lengthof(message_authenticator_key)) < 0 || - pg_hmac_update(hmac_context, - (uint8 *) receive_buffer, - offsetof(radius_packet, vector)) < 0 || - pg_hmac_update(hmac_context, - packet->vector, - RADIUS_VECTOR_LENGTH) < 0 || - pg_hmac_update(hmac_context, - (uint8 *) receive_buffer + RADIUS_HEADER_LENGTH, - packetlength - RADIUS_HEADER_LENGTH) < 0 || - pg_hmac_final(hmac_context, - message_authenticator, - lengthof(message_authenticator)) < 0) - { - ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s", - pg_hmac_error(hmac_context)))); - pg_hmac_free(hmac_context); - closesocket(sock); - return STATUS_ERROR; - } - pg_hmac_free(hmac_context); + if (!pg_md5_binary(cryptvector, + packetlength + strlen(secret), + encryptedpassword, &errstr)) + { + ereport(LOG, + (errmsg("could not perform MD5 encryption of received packet: %s", + errstr))); + pfree(cryptvector); + return false; + } + pfree(cryptvector); - /* Verify. */ - if (memcmp(message_authenticator_copy, - message_authenticator, - RADIUS_VECTOR_LENGTH) != 0) - { - ereport(LOG, (errmsg("RADIUS response from %s has invalid Message-Authenticator", - server))); - continue; - } - } + if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0) + { + ereport(LOG, + (errmsg("RADIUS response from %s has incorrect MD5 signature", + server))); + return false; + } - if (receivepacket->code == RADIUS_ACCESS_ACCEPT) - { - closesocket(sock); - return STATUS_OK; - } - else if (receivepacket->code == RADIUS_ACCESS_REJECT) + /* Search for the Message-Authenticator attribute. */ + if (!radius_find_attribute((uint8 *) receive_buffer, + packetlength, + RADIUS_MESSAGE_AUTHENTICATOR, + &message_authenticator_location, + &message_authenticator_size)) + { + ereport(LOG, + (errmsg("RADIUS response from %s has malformed attributes", + server))); + return false; + } + else if (message_authenticator_location == NULL) + { + ereport(LOG, + (errmsg("RADIUS response from %s has no Message-Authenticator", + server))); + + /* We'll ignore this message, unless pg_hba.conf told us not to. */ + if (requirema) + return false; + } + else if (message_authenticator_size != RADIUS_VECTOR_LENGTH) + { + ereport(LOG, + (errmsg("RADIUS response from %s has unexpected Message-Authenticator size", + server))); + return false; + } + else + { + uint8 message_authenticator_zero[RADIUS_VECTOR_LENGTH] = {0}; + const uint8 *attrs = (uint8 *) receive_buffer + RADIUS_HEADER_LENGTH; + const uint8 *end = (uint8 *) receive_buffer + packetlength; + + strncpy((char *) message_authenticator_key, secret, sizeof(message_authenticator_key)); + + /* + * Compute the expected value. Note that the HMAC for + * Access-Accept and Access-Reject message uses the authenticator + * from the original Access-Request message, so we have to do a + * bit of splicing. + */ + hmac_context = pg_hmac_create(PG_MD5); + if (hmac_context == NULL || + pg_hmac_init(hmac_context, + message_authenticator_key, + lengthof(message_authenticator_key)) < 0 || + pg_hmac_update(hmac_context, + (uint8 *) receive_buffer, + offsetof(radius_packet, vector)) < 0 || + pg_hmac_update(hmac_context, + packet->vector, + RADIUS_VECTOR_LENGTH) < 0 || + pg_hmac_update(hmac_context, + attrs, + message_authenticator_location - attrs) < 0 || + pg_hmac_update(hmac_context, + message_authenticator_zero, + RADIUS_VECTOR_LENGTH) < 0 || + pg_hmac_update(hmac_context, + message_authenticator_location + RADIUS_VECTOR_LENGTH, + end - (message_authenticator_location + RADIUS_VECTOR_LENGTH)) < 0 || + pg_hmac_final(hmac_context, + message_authenticator, + lengthof(message_authenticator)) < 0) { - closesocket(sock); - return STATUS_EOF; + ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s", + pg_hmac_error(hmac_context)))); + pg_hmac_free(hmac_context); + *status = STATUS_ERROR; + return true; } - else + pg_hmac_free(hmac_context); + + /* Verify. */ + if (memcmp(message_authenticator_location, + message_authenticator, + RADIUS_VECTOR_LENGTH) != 0) { - ereport(LOG, - (errmsg("RADIUS response from %s has invalid code (%d) for user \"%s\"", - server, receivepacket->code, user_name))); - continue; + ereport(LOG, (errmsg("RADIUS response from %s has invalid Message-Authenticator", + server))); + return false; } - } /* while (true) */ + } + + if (receivepacket->code == RADIUS_ACCESS_ACCEPT) + { + *status = STATUS_OK; + return true; + } + else if (receivepacket->code == RADIUS_ACCESS_REJECT) + { + *status = STATUS_EOF; + return true; + } + else + { + ereport(LOG, + (errmsg("RADIUS response from %s has invalid code (%d) for user \"%s\"", + server, receivepacket->code, user_name))); + return false; + } } diff --git a/src/backend/meson.build b/src/backend/meson.build index 78c5726814..3db6a64baf 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -70,6 +70,24 @@ postgres_lib = static_library('postgres_lib', kwargs: internal_lib_args, ) +fuzzer_backend_sources = [] +foreach item : backend_sources + if item not in main_file + fuzzer_backend_sources += item + endif +endforeach + +postgres_lib_fuzzer = static_library('postgres_lib_fuzzer', + fuzzer_backend_sources + timezone_sources + generated_backend_sources, + link_whole: backend_link_with, + dependencies: backend_build_deps, + c_pch: pch_postgres_h, + kwargs: internal_lib_args + { + 'c_args': ['-fsanitize=address,undefined,fuzzer-no-link'], + 'link_args': ['-fsanitize=address,undefined,fuzzer-no-link'], + }, +) + if cc.get_id() == 'msvc' postgres_def = custom_target('postgres.def', command: [perl, files('../tools/msvc_gendef.pl'), diff --git a/src/common/meson.build b/src/common/meson.build index de68e408fa..dee9beff1f 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -145,6 +145,17 @@ pgcommon_variants = { }, } +pgcommon_variants += { + '_fuzzer': pgcommon_variants[''] + { + 'c_args': pgcommon_variants['']['c_args'] + [ + '-fsanitize=address,undefined,fuzzer-no-link', + ], + 'link_args': [ + '-fsanitize=address,undefined,fuzzer-no-link', + ], + }, +} + foreach name, opts : pgcommon_variants # Build internal static libraries for sets of files that need to be built @@ -182,4 +193,9 @@ common_srv = pgcommon['_srv'] common_shlib = pgcommon['_shlib'] common_static = pgcommon[''] +common_fuzzer = declare_dependency( + link_with: pgcommon['_fuzzer'], + link_args: ['-fsanitize=address,undefined,fuzzer-no-link'] +) + subdir('unicode') diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index 227b41daf6..580f8422b3 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -34,4 +34,25 @@ typedef char *(*auth_password_hook_typ) (char *input); /* Default LDAP password mutator hook, can be overridden by a shared library */ extern PGDLLIMPORT auth_password_hook_typ ldap_password_hook; +#define RADIUS_VECTOR_LENGTH 16 + +/* Maximum size of a RADIUS packet we will create or accept */ +#define RADIUS_BUFFER_SIZE 1024 + +typedef struct +{ + uint8 code; + uint8 id; + uint16 length; + uint8 vector[RADIUS_VECTOR_LENGTH]; + /* this is a bit longer than strictly necessary: */ + char pad[RADIUS_BUFFER_SIZE - RADIUS_VECTOR_LENGTH]; +} radius_packet; + +extern bool +validate_radius_response(int *status, + const char *receive_buffer, radius_packet *packet, + size_t packetlength, const char *secret, + const char *server, const char *user_name, bool requirema); + #endif /* AUTH_H */ diff --git a/src/test/modules/fuzzers/fuzz_radius.c b/src/test/modules/fuzzers/fuzz_radius.c new file mode 100644 index 0000000000..7955f76e42 --- /dev/null +++ b/src/test/modules/fuzzers/fuzz_radius.c @@ -0,0 +1,74 @@ +#include "postgres.h" + +#include +#include + +#include "libpq/auth.h" +#include "postmaster/postmaster.h" +#include "utils/guc.h" +#include "utils/memutils.h" +#include "utils/resowner.h" + +const char *progname; /* to replace the export from main.c */ + +int +LLVMFuzzerInitialize(int *pargc, char ***pargv) +{ + static const struct option opts[] = { + { "log_min_messages", required_argument, NULL, 1000 }, + { 0 }, + }; + + int c; + char *log_min_messages = "fatal"; + + opterr = 0; + while ((c = getopt_long(*pargc, *pargv, "", opts, NULL)) != -1) + { + char *arg; + + if (c == 1000) /* log_min_messages */ + { + log_min_messages = optarg; + continue; + } + else if (c != '?' || optopt != 0) + { + /* Ignore other valid options and unrecognized short options. */ + continue; + } + + arg = (*pargv)[optind - 1]; + fprintf(stderr, "unrecognized argument: %s\n", arg); + exit(1); + } + + /* Fake server startup. */ + progname = "postgres"; + MemoryContextInit(); + CreateAuxProcessResourceOwner(); + InitializeGUCOptions(); + SetConfigOption("log_min_messages", log_min_messages, + PGC_POSTMASTER, PGC_S_ARGV); + + return 0; +} + +int +LLVMFuzzerTestOneInput(const uint8_t *data, size_t len) +{ + int status; + radius_packet packet = {0}; + + validate_radius_response(&status, + (const char *) data, /* input response */ + &packet, /* request packet already sent */ + len, /* length of input response */ + "secret", /* shared secret */ + NULL, /* server name */ + NULL, /* user name */ + false /* require Message-Authenticator? */ + ); + + return 0; +} diff --git a/src/test/modules/fuzzers/meson.build b/src/test/modules/fuzzers/meson.build new file mode 100644 index 0000000000..3dfc257fc1 --- /dev/null +++ b/src/test/modules/fuzzers/meson.build @@ -0,0 +1,24 @@ +# Copyright (c) 2024, PostgreSQL Global Development Group + +can_fuzz = cc.has_argument('-fsanitize=fuzzer') +if can_fuzz + fuzz_radius_sources = files( + 'fuzz_radius.c', + ) + + fuzz_radius = executable('fuzz_radius', + fuzz_radius_sources + generated_headers, + c_args: [ + '-fsanitize=address,fuzzer', + '-Wno-missing-prototypes', + '-fprofile-instr-generate', '-fcoverage-mapping' + ], + link_args: ['-fsanitize=address,fuzzer'], + include_directories: [postgres_inc], + link_with: [postgres_lib_fuzzer, pgport_static], + dependencies: [os_deps, libintl], + kwargs: default_bin_args + { + 'install': false, + }, + ) +endif diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index d8fe059d23..9765e0522a 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -5,6 +5,7 @@ subdir('commit_ts') subdir('delay_execution') subdir('dummy_index_am') subdir('dummy_seclabel') +subdir('fuzzers') subdir('gin') subdir('injection_points') subdir('ldap_password_func') -- 2.34.1