I am trying to write a little program, which is resistant against buffer overflow and similar vulnerabilities.
Since we cannot trust the user input, I thought it would be a good idea to concatenate all the strings that are inputted into one string and then pass it to a static path, which is a bash script in the same folder and then add all the parameters/flags/arguments (e.g. ./script.sh test1 test2 test3 test4).
My logic on paper is the following:
- Check whether the number of argc is 5 exactly (so program name + the 4 arguments), if not - exit immediately
- The specific char array for 5 strings (so array elements) with a max length of 4096 bytes length is initialized.
- Since
argv[0]is equal to the program name, we need to skip it. Hence the loop starts at 1 (first argument) and ends at argc-1. So good so far - We append the argv[1] string the ending
.keyin-memory. - We
memcpyall the strings to make it safe to use - For each iteration we don't forget to add the null terminator at the end of the string.
- After we have all the arguments safe to use we concat it in the function parse_output and call the bash script called script.sh and add all the required arguments
- We return the output from the
script.sh 4argshereto the user
I tried freeing the memory like in the comments but it seems to not work or I have errors somewhere else.
My segfaulting Proof of Concept:
#include <stdlib.h> #include <string.h> #include <stdio.h> #define BUFSIZE 1000 char *concatenate(size_t size, char *array[size], const char *joint); char *concatenate(size_t size, char *array[size], const char *joint){ size_t jlen, lens[size]; size_t i, total_size = (size-1) * (jlen=strlen(joint)) + 1; char *result, *p; for(i=0;i<size;++i){ total_size += (lens[i]=strlen(array[i])); } p = result = malloc(total_size); for(i=0;i<size;++i){ memcpy(p, array[i], lens[i]); p += lens[i]; if(i<size-1){ memcpy(p, joint, jlen); p += jlen; } } *p = '\0'; return result; } int parse_output(char *safeargv[]) { char safeargs = *concatenate(5, safeargv, " "); char cmd[BUFSIZE]; snprintf(cmd, BUFSIZE, "./script.sh %s", safeargs); char buf[BUFSIZE] = {0}; FILE *fp; if ((fp = popen(cmd, "r")) == NULL) { printf("Error opening pipe!\n"); //free(safeargs); return -1; } while (fgets(buf, BUFSIZE, fp) != NULL) { printf("OUTPUT: %s", buf); } if (pclose(fp)) { printf("Command not found or exited with error status\n"); //free(safeargs); return -1; } //free(safeargs); return 0; } int main(int argc, char *argv[]) { if(argc != 5) { exit(1); } char *safeargv[5][4096]; for (int i = 1; i < argc - 1; i++) { if (i == 1) strcat(argv[1], ".key"); for (int x = 0; x < strlen(argv[i]); x++) { char *unsafe_string = argv[i]; size_t max_len = 4096; size_t len = strnlen(unsafe_string, max_len) + 1; char *x = malloc(len * sizeof(char)); if (x != NULL) { strncpy(x, unsafe_string, len); x[len-1] = '\0'; // Ensure null-termination strcpy(safeargv[i], x); free(x); } } } parse_output(safeargv); return 0; } The warnings I get while compiling:
chal.c: In function 'main': chal.c:78:32: warning: passing argument 1 of 'strcpy' from incompatible pointer type [-Wincompatible-pointer-types] 78 | strcpy(safeargv[i], x); | ~~~~~~~~^~~ | | | char ** In file included from chal.c:2: /usr/include/string.h:141:39: note: expected 'char * restrict' but argument is of type 'char **' 141 | extern char *strcpy (char *__restrict __dest, const char *__restrict __src) | ~~~~~~~~~~~~~~~~~^~~~~~ chal.c:83:18: warning: passing argument 1 of 'parse_output' from incompatible pointer type [-Wincompatible-pointer-types] 83 | parse_output(safeargv); | ^~~~~~~~ | | | char * (*)[4096] chal.c:32:24: note: expected 'char **' but argument is of type 'char * (*)[4096]' 32 | int parse_output(char *safeargv[]) { | ~~~~~~^~~~~~~~~~ It seems only my argc check works, because if I call ./programname abc abc abc abc it segfaults. Also what's the proper way to detect an error, if for case there's a typo in the argument for the script?
What mistake(s) did I make?
My last ltrace lines:
strlen(" ") = 1 strlen(nil <no return ...> It looks as if it would try to detect the length of a null character or similar.
argvfrom anexec*call is passed into the kernel. The kernel checks these when creatingargv(viacopy_from_userfor parent andcopy_to_userfor child) in the target program address space. So, no need to copy outargvfrom a programmainas it is already "safe". As long as you stay within the bounds of theargvarray, you're fine. In other words, theargvsent viaexec*is not blindly used as is. The kernel probes each element and copies it to a new area (checking along the way).memcpyand etc. is only security theater in this case? A simplewhile(*argv != NULL)is enough then?argvthat is passed tomain(and better as the strings can be > 4096 (IIRC)?). There is a similarchar*array for the environment variables. When doingexec*, the parent can put theargvanywhere in its memory. The kernel when copying into the target, starts at the highest [virtual] address, growing the area downward. Ditto forenviron. The initial stack pointer is set just below those. Try printing (via%p) the addresses ofargvin both caller/callee (as well as stack pointer). sp < argv < environ for callee (e.g.)git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.gitLook in the filefs/exec.c. Look at thecopy_stringsfunction. For the limits, look ininclude/uapi/linux/binfmts.h(forMAX_ARG_STRLEN). It is 32 pages (not 1). And, up to 0xFFFFFFFF separateargventries. The max number of pages devoted to args is 32char safeargs = *concatenate(5, safeargv, " ");should bechar *safeargs = concatenate(5, safeargv, " ");. Perhaps other issues too.