Hello,
I am relatively new to C programming, and have encountered a strange issue.
Background:
While playing around with linux, I got an idea to make a C program to encapsulate a shell script to provide a more secure approach to executing scripts through SUID. While this approach is not exactly completely secure, it was more of a short (or so I thought) learning experience. I created a small program which would have a shell script appended to it. Upon execution it would access the script through a file descriptor and redirect it to stdin. From there it would use an exec call to run the shell and thus allow a script to execute with Set User ID.
Problem:
Everything was working fine until I realized that under a very specific set of circumstances the file descriptor redirection would fail (although it never threw an error) and the contents of the file would not get to stdin. After trying to debug this issue for a good couple of hours I thought I would reach out to the community and see if anyone had any insight on this issue (or if maybe I missed something through lack of experience)
I am running Linux Mint 17. Out-of-the-box bash shell.
The simple explanation of what I did is:
main.c is compiled. bash2bin.c is compiled and used to append a small shell script onto main. main is executed and it reads the shell script from its own file. It reads the shebang when it is present (that's when the program fails to redirect stdin) and tries to launch the script through the correct shell interpreter. When no shebang is present, it defaults to /bin/bash and everything works fine.
So to phrase it as a question; Why does the code run just fine when the first line of test.txt is removed? Is it a bug that I haven't found (after hours of looking)? Some obscure bug somewhere deep inside linux (highly unlikely, I know)? Is something corrupt in my installation?
I know it's a fair amount of code, but it's well commented and any help would be appreciated.
The procedure I used for compiling and running this somewhat convoluted project:
Code:
#!/bin/bash
gcc -Wextra -ggdb main.c -o main
gcc -Wextra -ggdb bash2bin.c -o bash2bin
./bash2bin < test.txt
./main
typeset -i error=$?
if ((${error}!=0)); then
echo "Error: ${error}";
fi
bash2bin.c:
Used for attaching the script to main
Code:
#include <stdio.h>#include <stdlib.h>
#include <unistd.h>
char magic[] = {0x23, 0x22, 0xF0, 0x48, 0x00, 0x52, 0xA1, 0x44}; //The same magic constant as in the other file
/*
* Simple helper error method. Used to terminate the program with error code "1" and print a String to stderr.
*/
void error(char* errMsg) {
fprintf(stderr, "%s\n", errMsg);
exit(1);
}
int main() {
FILE *my_file = fopen("main", "a"); //Append to the main binary (compiled from main.c)
if(!my_file) //Error check
error("Failed to open the file for reading!");
int fd = fileno(my_file); //Get the fd.
lseek(fd, 0, SEEK_END); //Seek to end (I didn't get along with append for some reason)
write(fd, magic, 8); //Write the magic constant to the end of the file (or the begginging of the shell script)
char buffer[1024];
int total_read = 0;
/*
* Copy the data from the shell script (read from stdin, in reality it is test.txt) to the end of the main binary
*/
while(1) {
int dataRead = read(STDIN_FILENO, buffer, 1024);
total_read += dataRead;
if(dataRead < 1)
break;
write(fd, buffer, dataRead);
}
fclose(my_file); //Close the main binary
return 0;
}
main.c
The main code, which has the bash script appended to it. This is where the problem is:
Code:
#include <stdio.h>#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
char magic[] = {0x23, 0x22, 0xF0, 0x48, 0x00, 0x52, 0xA1, 0x44}; //The constant the program looks for in the file to begin reading
/*
* Stadard error method. Exits the program with "1" error code.
* errMsg - String which will be printed to stderr before terminating
*/
void error(char* errMsg) {
fprintf(stderr, "%s\n", errMsg);
exit(1);
}
/*
* Checks the rest of the magic constant, once the first character has been found.
*/
int checkRestOfSequance(int fd) {
int currentMagic = 1;
char buffer[1];
while(1) {
if(!read(fd,&buffer,1))
error("Reached EOF without finding magic number.");
if(buffer[0] != magic[currentMagic++])
return 0;
if(currentMagic==8)
break;
}
return 1;
}
/*
* Goes through the file until the magic constant is located.
* In reallity it is looking for the second magic constant as though the first will be the definition of the above field.
*/
void seekFdToValue(int fd) {
char buffer[1];
int magicCount=0;
while(1) {
if(!read(fd,&buffer,1))
error("Reached EOF without finding magic number.");
if(buffer[0]==magic[0]) {
if(checkRestOfSequance(fd)) {
if(++magicCount==2)
break;
}
}
}
}
void tryParseShebang(FILE* file, char* shell, unsigned int shell_len) {
char shebang[3] = {0}; //Three bytes allocated to store possible shebang. e.g. #!\0
char ret_shell[100] = {0}; //This will store the shell behind the shebang or the default /bin/bash
fscanf(file, "%2s", shebang); //read two characers, which may or may not be #!
/*
* When the first result of the "if" executes, the script works fine,
* When the else executes, redirection from file to stdin fails (although no error code is ever returned).
*/
if(strcmp(shebang, "#!") != 0) { //If the script has no shebang.
fseek(file, -2, SEEK_CUR); //Go back 2 bytes, so as to leave the two characters in the "stream"
strcpy(ret_shell, "/bin/bash"); //Default to /bin/bash
} else {
//fseek(file, -2, SEEK_CUR); //Different attempts to get this working
//char c;
//int idx = 0;
//while((c=fgetc(file))!='\n')
// ret_shell[idx++]=c;
//fgets(ret_shell, 100, file);
fscanf(file, "%100[^\n]\n", ret_shell); //This line, as well as the ones above it, cause the problem.
}
if(strlen(ret_shell) > shell_len) //If there is not enough space to safely copy the value
error("The shell variable is too small to hold the shell"); //Exit with error
strcpy(shell, ret_shell); //Copy the value from local variable to the "out" argument - shell
}
/*
* Debugger method. Prints a line from stdin.
*/
void print_line_from_stdin() {
char toPrint[50] = {0};
scanf("%50[^\n]\n", toPrint);
fprintf(stderr, "%s\n", toPrint);
}
/*
* Debugger method. Prints a line from the file provided.
*/
void print_line_from_file(FILE* file) {
char var[100] = {0};
fscanf(file, "%100[^\n]\n", var);
fprintf(stderr, "%s\n", var);
}
int main(int argc, char* args[]) {
FILE *my_file = fopen(args[0], "r"); //Opens itself. This is where the bash script is stored.
if(!my_file) //Obvious error check.
error("Failed to open the file for reading!");
int fd = fileno(my_file); //Get the file descriptor for this file "stream".
seekFdToValue(fd); //Seek to the proper location. The beggining of the bash script which lives at the end of the file. (marked by magic constant)
char shell[100] = {0}; //The shell which will be used to launch the script e.g. "/bin/bash"
tryParseShebang(my_file, shell, 100); //This is where the trouble starts. When no shebang is present, it works like a charm. with shebang the shell is returned properly, but problems start happening.
//fd = fileno(my_file); //Tried this for sanity after around 70 debug runs.
/*
* This may have to be dup3 to account for the close-on-exec, but I do not believe that is the problem, as though without shebang the program runs fine.
*/
int dup_res = dup2(fd, 0); //Map the file descriptor (now pointing to the begginging of the bash script) onto stdin, in preperation for rediraction into the shell.
if(dup_res == -1) //Check if the dup2() method failed. Never does.
error("Failed to redirect file into stdin");
fprintf(stderr, "Shell: %s\n", shell); //Check to make sure the shell got read correctly (debugging)
/*
* This is the part that drives me crazy. The dup2 was sucessful, and when no shebang is present, stdin contains the lines of the bash script.
* However, when a shebang is present, nothing is read from stdin, but data can be normaly read from the file stream.
* This is insane as though the dup2 system call should have guaranteed that reading fd 0 would be synonymous to reading the file fd.
*/
//These are just debug lines, idealy stdin is in place for the exec call
print_line_from_stdin();
//print_line_from_file(my_file);
print_line_from_stdin();
print_line_from_stdin();
args[0] = shell; //Change the first argument to the shell.
execv(shell, args); //Execute the shell, with the arguments, and with stdin redirected this way, it should run. (and it does when no shebang is present)
return 0;
}
test.txt
My little shell script which is meant to be ran through/inside the C program. It should be noted that everything works just fine without the first line:
Code:
#!/bin/bash
echo "Line 1"
echo "Line Two"
echo "Line III"