Once you understand the basic concepts of C programming, the rest is pretty easy. The bulk of the power of C comes from using other functions. In fact, if the functions were removed from any of the preceding programs, all that would remain are very basic statements.
There are two primary ways to access files in C: file descriptors and filestreams. File descriptors use a set of low-level I/O functions, and filestreams are a higher-level form of buffered I/O that is built on the lower-level functions. Some consider the filestream functions easier to program with; however, file descriptors are more direct. In this book, the focus will be on the low-level I/O functions that use file descriptors.
The bar code on the back of this book represents a number. Because this number is unique among the other books in a bookstore, the cashier can scan the number at checkout and use it to reference information about this book in the store's database. Similarly, a file descriptor is a number that is used to reference open files. Four common functions that use file descriptors are open(), close(), read()
, and write()
. All of these functions will return –1 if there is an error. The open()
function opens a file for reading and/or writing and returns a file descriptor. The returned file descriptor is just an integer value, but it is unique among open files. The file descriptor is passed as an argument to the other functions like a pointer to the opened file. For the close()
function, the file descriptor is the only argument. The read()
and write()
functions' arguments are the file descriptor, a pointer to the data to read or write, and the number of bytes to read or write from that location. The arguments to the open()
function are a pointer to the filename to open and a series of predefined flags that specify the access mode. These flags and their usage will be explained in depth later, but for now let's take a look at a simple note-taking program that uses file descriptors—simplenote.c. This program accepts a note as a command-line argument and then adds it to the end of the file /tmp/notes. This program uses several functions, including a familiar looking error-checked heap memory allocation function. Other functions are used to display a usage message and to handle fatal errors. The usage()
function is simply defined before main()
, so it doesn't need a function prototype.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> void usage(char *prog_name, char *filename) { printf("Usage: %s <data to add to %s>\n", prog_name, filename); exit(0); } void fatal(char *); // A function for fatal errors void *ec_malloc(unsigned int); // An error-checked malloc() wrapper int main(int argc, char *argv[]) { int fd; // file descriptor char *buffer, *datafile; buffer = (char *) ec_malloc(100); datafile = (char *) ec_malloc(20); strcpy(datafile, "/tmp/notes"); if(argc < 2) // If there aren't command-line arguments, usage(argv[0], datafile); // display usage message and exit. strcpy(buffer, argv[1]); // Copy into buffer. printf("[DEBUG] buffer @ %p: \'%s\'\n", buffer, buffer); printf("[DEBUG] data file @ %p: \'%s\'\n", datafile, datafile); strncat(buffer, "\n", 1); // Add a newline on the end. // Opening file fd = open(datafile, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(fd == -1) fatal("in main() while opening file"); printf("[DEBUG] file descriptor is %d\n", fd); // Writing data if(write(fd, buffer, strlen(buffer)) == -1) fatal("in main() while writing buffer to file"); // Closing file if(close(fd) == -1) fatal("in main() while closing file"); printf("Note has been saved.\n"); free(buffer); free(datafile); } // A function to display an error message and then exit void fatal(char *message) { char error_message[100]; strcpy(error_message, "[!!] Fatal Error "); strncat(error_message, message, 83); perror(error_message); exit(-1); } // An error-checked malloc() wrapper function void *ec_malloc(unsigned int size) { void *ptr; ptr = malloc(size); if(ptr == NULL) fatal("in ec_malloc() on memory allocation"); return ptr; }
Besides the strange-looking flags used in the open()
function, most of this code should be readable. There are also a few standard functions that we haven't used before. The strlen()
function accepts a string and returns its length. It's used in combination with the write()
function, since it needs to know how many bytes to write. The perror()
function is short for print error and is used in fatal()
to print an additional error message (if it exists) before exiting.
reader@hacking:~/booksrc $ gcc -o simplenote simplenote.c reader@hacking:~/booksrc $ ./simplenote Usage: ./simplenote <data to add to /tmp/notes> reader@hacking:~/booksrc $ ./simplenote "this is a test note" [DEBUG] buffer @ 0x804a008: 'this is a test note' [DEBUG] data file @ 0x804a070: '/tmp/notes' [DEBUG] file descriptor is 3 Note has been saved. reader@hacking:~/booksrc $ cat /tmp/notes this is a test note reader@hacking:~/booksrc $ ./simplenote "great, it works" [DEBUG] buffer @ 0x804a008: 'great, it works' [DEBUG] datafile @ 0x804a070: '/tmp/notes' [DEBUG] file descriptor is 3 Note has been saved. reader@hacking:~/booksrc $ cat /tmp/notes this is a test note great, it works reader@hacking:~/booksrc $
The output of the program's execution is pretty self-explanatory, but there are some things about the source code that need further explanation. The files fcntl.h and sys/stat.h had to be included, since those files define the flags used with the open()
function. The first set of flags is found in fcntl.h and is used to set the access mode. The access mode must use at least one of the following three flags:
O_RDONLY Open file for read-only access. |
O_WRONLY Open file for write-only access. |
O_RDWR Open file for both read and write access. |
These flags can be combined with several other optional flags using thebitwise OR operator. A few of the more common and useful of these flags areas follows:
O_APPEND Write data at the end of the file. |
O_TRUNC If the file already exists, truncate the file to 0 length. |
O_CREAT Create the file if it doesn't exist. |
Bitwise operations combine bits using standard logic gates such as OR and AND. When two bits enter an OR gate, the result is 1 if either the first bit or the second bit is 1. If two bits enter an AND gate, the result is 1 only if both the first bit and the second bit are 1. Full 32-bit values can use these bitwise operators to perform logic operations on each corresponding bit. The source code of bitwise.c and the program output demonstrate these bitwise operations.
#include <stdio.h> int main() { int i, bit_a, bit_b; printf("bitwise OR operator |\n"); for(i=0; i < 4; i++) { bit_a = (i & 2) / 2; // Get the second bit. bit_b = (i & 1); // Get the first bit. printf("%d | %d = %d\n", bit_a, bit_b, bit_a | bit_b); } printf("\nbitwise AND operator &\n"); for(i=0; i < 4; i++) { bit_a = (i & 2) / 2; // Get the second bit. bit_b = (i & 1); // Get the first bit. printf("%d & %d = %d\n", bit_a, bit_b, bit_a & bit_b); } }
The results of compiling and executing bitwise.c are as follows.
reader@hacking:~/booksrc $ gcc bitwise.c reader@hacking:~/booksrc $ ./a.out bitwise OR operator | 0 | 0 = 0 0 | 1 = 1 1 | 0 = 1 1 | 1 = 1 bitwise AND operator & 0 & 0 = 0 0 & 1 = 0 1 & 0 = 0 1 & 1 = 1 reader@hacking:~/booksrc $
The flags used for the open()
function have values that correspond to single bits. This way, flags can be combined using OR logic without destroying any information. The fcntl_flags.c program and its output explore some of the flag values defined by fcntl.h and how they combine with each other.
#include <stdio.h> #include <fcntl.h> void display_flags(char *, unsigned int); void binary_print(unsigned int); int main(int argc, char *argv[]) { display_flags("O_RDONLY\t\t", O_RDONLY); display_flags("O_WRONLY\t\t", O_WRONLY); display_flags("O_RDWR\t\t\t", O_RDWR); printf("\n"); display_flags("O_APPEND\t\t", O_APPEND); display_flags("O_TRUNC\t\t\t", O_TRUNC); display_flags("O_CREAT\t\t\t", O_CREAT); printf("\n"); display_flags("O_WRONLY|O_APPEND|O_CREAT", O_WRONLY|O_APPEND|O_CREAT); } void display_flags(char *label, unsigned int value) { printf("%s\t: %d\t:", label, value); binary_print(value); printf("\n"); } void binary_print(unsigned int value) { unsigned int mask = 0xff000000; // Start with a mask for the highest byte. unsigned int shift = 256*256*256; // Start with a shift for the highest byte. unsigned int byte, byte_iterator, bit_iterator; for(byte_iterator=0; byte_iterator < 4; byte_iterator++) { byte = (value & mask) / shift; // Isolate each byte. printf(" "); for(bit_iterator=0; bit_iterator < 8; bit_iterator++) { // Print the byte's bits. if(byte & 0x80) // If the highest bit in the byte isn't 0, printf("1"); // print a 1. else printf("0"); // Otherwise, print a 0. byte *= 2; // Move all the bits to the left by 1. } mask /= 256; // Move the bits in mask right by 8. shift /= 256; // Move the bits in shift right by 8. } }
The results of compiling and executing fcntl_flags.c are as follows.
reader@hacking:~/booksrc $ gcc fcntl_flags.c reader@hacking:~/booksrc $ ./a.out O_RDONLY : 0 : 00000000 00000000 00000000 00000000 O_WRONLY : 1 : 00000000 00000000 00000000 00000001 O_RDWR : 2 : 00000000 00000000 00000000 00000010 O_APPEND : 1024 : 00000000 00000000 00000100 00000000 O_TRUNC : 512 : 00000000 00000000 00000010 00000000 O_CREAT : 64 : 00000000 00000000 00000000 01000000 O_WRONLY|O_APPEND|O_CREAT : 1089 : 00000000 00000000 00000100 01000001 $
Using bit flags in combination with bitwise logic is an efficient and commonly used technique. As long as each flag is a number that only has unique bits turned on, the effect of doing a bitwise OR on these values is the same as adding them. In fcntl_flags.c, 1 + 1024 + 64 = 1089. This technique only works when all the bits are unique, though.
If the O_CREAT
flag is used in access mode for the open()
function, an additional argument is needed to define the file permissions of the newly created file. This argument uses bit flags defined in sys/stat.h, which can be combined with each other using bitwise OR logic.
If you are already familiar with Unix file permissions, those flags should make perfect sense to you. If they don't make sense, here's a crash course in Unix file permissions.
Every file has an owner and a group. These values can be displayed using ls -l
and are shown below in the following output.
reader@hacking:~/booksrc $ ls -l /etc/passwd simplenote* -rw-r--r-- 1 root root 1424 2007-09-06 09:45 /etc/passwd -rwxr-xr-x 1 reader reader 8457 2007-09-07 02:51 simplenote -rw------- 1 reader reader 1872 2007-09-07 02:51 simplenote.c reader@hacking:~/booksrc $
For the /etc/passwd file, the owner is root and the group is also root. For the other two simplenote files, the owner is reader and the group is users.
Read, write, and execute permissions can be turned on and off for three different fields: user, group, and other. User permissions describe what the owner of the file can do (read, write, and/or execute), group permissions describe what users in that group can do, and other permissions describe what everyone else can do. These fields are also displayed in the front of the ls -l
output. First, the user read/write/execute permissions are displayed, using r
for read, w
for write, x
for execute, and - for off. The next three characters display the group permissions, and the last three characters are for the other permissions. In the output above, the simplenote program has all three user permissions turned on (shown in bold). Each permission corresponds to a bit flag; read is 4 (100 in binary), write is 2 (010 in binary), and execute is 1 (001 in binary). Since each value only contains unique bits, a bitwise OR operation achieves the same result as adding these numbers together does. These values can be added together to define permissions for user, group, and other using the chmod
command.
reader@hacking:~/booksrc $ chmod 731 simplenote.c reader@hacking:~/booksrc $ ls -l simplenote.c -rwx-wx--x 1 reader reader 1826 2007-09-07 02:51 simplenote.c reader@hacking:~/booksrc $ chmod ugo-wx simplenote.c reader@hacking:~/booksrc $ ls -l simplenote.c -r-------- 1 reader reader 1826 2007-09-07 02:51 simplenote.c reader@hacking:~/booksrc $ chmod u+w simplenote.c reader@hacking:~/booksrc $ ls -l simplenote.c -rw------- 1 reader reader 1826 2007-09-07 02:51 simplenote.c reader@hacking:~/booksrc $
The first command (chmod 721
) gives read, write, and execute permissions to the user, since the first number is 7 (4 + 2 + 1), write and execute permissions to group, since the second number is 3 (2 + 1), and only execute permission to other, since the third number is 1. Permissions can also be added or subtracted using chmod
. In the next chmod
command, the argument ugo-wx
means Subtract write and execute permissions from user, group, and other. The final chmod u+w
command gives write permission to user.
In the simplenote program, the open()
function uses S_IRUSR|S_IWUSR
for its additional permission argument, which means the /tmp/notes file should only have user read and write permission when it is created.
reader@hacking:~/booksrc $ ls -l /tmp/notes -rw------- 1 reader reader 36 2007-09-07 02:52 /tmp/notes reader@hacking:~/booksrc $
Every user on a Unix system has a unique user ID number. This user ID can be displayed using the id
command.
reader@hacking:~/booksrc $ id reader
uid=999(reader)
gid=999(reader)
groups=999(reader),4(adm),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),4
4(video),46(plugdev),104(scanner),112(netdev),113(lpadmin),115(powerdev),117(a
dmin)
reader@hacking:~/booksrc $ id matrix
uid=500(matrix) gid=500(matrix) groups=500(matrix)
reader@hacking:~/booksrc $ id root
uid=0(root) gid=0(root) groups=0(root)
reader@hacking:~/booksrc $
The root user with user ID 0 is like the administrator account, which has full access to the system. The su
command can be used to switch to a different user, and if this command is run as root, it can be done without a password. The sudo
command allows a single command to be run as the root user. On the LiveCD, sudo
has been configured so it can be executed without a password, for simplicity's sake. These commands provide a simple method to quickly switch between users.
reader@hacking:~/booksrc $ sudo su jose jose@hacking:/home/reader/booksrc $ id uid=501(jose) gid=501(jose) groups=501(jose) jose@hacking:/home/reader/booksrc $
As the user jose, the simplenote program will run as jose if it is executed, but it won't have access to the /tmp/notes file. This file is owned by the user reader, and it only allows read and write permission to its owner.
jose@hacking:/home/reader/booksrc $ ls -l /tmp/notes -rw------- 1 reader reader 36 2007-09-07 05:20 /tmp/notes jose@hacking:/home/reader/booksrc $ ./simplenote "a note for jose" [DEBUG] buffer @ 0x804a008: 'a note for jose' [DEBUG] datafile @ 0x804a070: '/tmp/notes' [!!] Fatal Error in main() while opening file: Permission denied jose@hacking:/home/reader/booksrc $ cat /tmp/notes cat: /tmp/notes: Permission denied jose@hacking:/home/reader/booksrc $ exit exit reader@hacking:~/booksrc $
This is fine if reader is the only user of the simplenote program; however, there are many times when multiple users need to be able to access certain portions of the same file. For example, the /etc/passwd file contains account information for every user on the system, including each user's default login shell. The command chsh
allows any user to change his or her own login shell. This program needs to be able to make changes to the /etc/passwd file, but only on the line that pertains to the current user's account. The solution to this problem in Unix is the set user ID (setuid)
permission. This is an additional file permission bit that can be set using chmod
. When a program with this flag is executed, it runs as the user ID of the file's owner.
reader@hacking:~/booksrc $ which chsh /usr/bin/chsh reader@hacking:~/booksrc $ ls -l /usr/bin/chsh /etc/passwd -rw-r--r-- 1 root root 1424 2007-09-06 21:05 /etc/passwd -rwsr-xr-x 1 root root 23920 2006-12-19 20:35 /usr/bin/chsh reader@hacking:~/booksrc $
The chsh
program has the setuid
flag set, which is indicated by an s
in the ls
output above. Since this file is owned by root and has the setuid
permission set, the program will run as the root user when any user runs this program. The /etc/passwd file that chsh
writes to is also owned by root and only allows the owner to write to it. The program logic in chsh
is designed to only allow writing to the line in /etc/passwd that corresponds to the user running the program, even though the program is effectively running as root. This means that a running program has both a real user ID and an effective user ID. These IDs can be retrieved using the functions getuid()
and geteuid()
, respectively, as shown in uid_demo.c.
#include <stdio.h> int main() { printf("real uid: %d\n", getuid()); printf("effective uid: %d\n", geteuid()); }
The results of compiling and executing uid_demo.c are as follows.
reader@hacking:~/booksrc $ gcc -o uid_demo uid_demo.c reader@hacking:~/booksrc $ ls -l uid_demo -rwxr-xr-x 1 reader reader 6825 2007-09-07 05:32 uid_demo reader@hacking:~/booksrc $ ./uid_demo real uid: 999 effective uid: 999 reader@hacking:~/booksrc $ sudo chown root:root ./uid_demo reader@hacking:~/booksrc $ ls -l uid_demo -rwxr-xr-x 1 root root 6825 2007-09-07 05:32 uid_demo reader@hacking:~/booksrc $ ./uid_demo real uid: 999 effective uid: 999 reader@hacking:~/booksrc $
In the output for uid_demo.c, both user IDs are shown to be 999 when uid_demo
is executed, since 999 is the user ID for reader. Next, the sudo
command is used with the chown
command to change the owner and group of uid_demo
to root. The program can still be executed, since it has execute permission for other, and it shows that both user IDs remain 999, since that's still the ID of the user.
reader@hacking:~/booksrc $ chmod u+s ./uid_demo chmod: changing permissions of `./uid_demo': Operation not permitted reader@hacking:~/booksrc $ sudo chmod u+s ./uid_demo reader@hacking:~/booksrc $ ls -l uid_demo -rwsr-xr-x 1 root root 6825 2007-09-07 05:32 uid_demo reader@hacking:~/booksrc $ ./uid_demo real uid: 999 effective uid: 0 reader@hacking:~/booksrc $
Since the program is owned by root now, sudo
must be used to change file permissions on it. The chmod u+s
command turns on the setuid
permission, which can be seen in the following ls -l
output. Now when the user reader executes uid_demo
, the effective user ID is 0 for root, which means the program can access files as root. This is how the chsh
program is able to allow any user to change his or her login shell stored in /etc/passwd.
This same technique can be used in a multiuser note-taking program. The next program will be a modification of the simplenote program; it will also record the user ID of each note's original author. In addition, a new syntax for #include
will be introduced.
The ec_malloc()
and fatal()
functions have been useful in many of our programs. Rather than copy and paste these functions into each program, they can be put in a separate include file.
// A function to display an error message and then exit void fatal(char *message) { char error_message[100]; strcpy(error_message, "[!!] Fatal Error "); strncat(error_message, message, 83); perror(error_message); exit(-1); } // An error-checked malloc() wrapper function void *ec_malloc(unsigned int size) { void *ptr; ptr = malloc(size); if(ptr == NULL) fatal("in ec_malloc() on memory allocation"); return ptr; }
In this new program, hacking.h, the functions can just be included. In C, when the filename for a #include
is surrounded by <
and >
, the compiler looks for this file in standard include paths, such as /usr/include/. If the filename is surrounded by quotes, the compiler looks in the current directory. Therefore, if hacking.h is in the same directory as a program, it can be included with that program by typing #include "hacking.h"
.
The changed lines for the new notetaker program (notetaker.c) are displayed in bold.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h>#include "hacking.h"
void usage(char *prog_name, char *filename) { printf("Usage: %s <data to add to %s>\n", prog_name, filename); exit(0); } void fatal(char *); // A function for fatal errors void *ec_malloc(unsigned int); // An error-checked malloc() wrapper int main(int argc, char *argv[]) {int userid, fd; // File descriptor
char *buffer, *datafile; buffer = (char *) ec_malloc(100); datafile = (char *) ec_malloc(20);strcpy(datafile, "/var/notes");
if(argc < 2) // If there aren't command-line arguments, usage(argv[0], datafile); // display usage message and exit. strcpy(buffer, argv[1]); // Copy into buffer. printf("[DEBUG] buffer @ %p: \'%s\'\n", buffer, buffer); printf("[DEBUG] datafile @ %p: \'%s\'\n", datafile, datafile); // Opening the file fd = open(datafile, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(fd == -1) fatal("in main() while opening file"); printf("[DEBUG] file descriptor is %d\n", fd);userid = getuid(); // Get the real user ID.
// Writing dataif(write(fd, &userid, 4) == -1) // Write user ID before note data. fatal("in main() while writing userid to file"); write(fd, "\n", 1); // Terminate line. if(write(fd, buffer, strlen(buffer)) == -1) // Write note. fatal("in main() while writing buffer to file"); write(fd, "\n", 1); // Terminate line.
// Closing file if(close(fd) == -1) fatal("in main() while closing file"); printf("Note has been saved.\n"); free(buffer); free(datafile); }
The output file has been changed from /tmp/notes to /var/notes, so the data is now stored in a more permanent place. The getuid()
function is used to get the real user ID, which is written to the datafile on the line before the note's line is written. Since the write()
function is expecting a pointer for its source, the &
operator is used on the integer value userid
to provide its address.
reader@hacking:~/booksrc $ gcc -o notetaker notetaker.c reader@hacking:~/booksrc $ sudo chown root:root ./notetaker reader@hacking:~/booksrc $ sudo chmod u+s ./notetaker reader@hacking:~/booksrc $ ls -l ./notetaker -rwsr-xr-x 1 root root 9015 2007-09-07 05:48 ./notetaker reader@hacking:~/booksrc $ ./notetaker "this is a test of multiuser notes" [DEBUG] buffer @ 0x804a008: 'this is a test of multiuser notes' [DEBUG] datafile @ 0x804a070: '/var/notes' [DEBUG] file descriptor is 3 Note has been saved. reader@hacking:~/booksrc $ ls -l /var/notes -rw------- 1 root reader 39 2007-09-07 05:49 /var/notes reader@hacking:~/booksrc $
In the preceding output, the notetaker program is compiled and changed to be owned by root, and the setuid
permission is set. Now when the program is executed, the program runs as the root user, so the file /var/notes is also owned by root when it is created.
reader@hacking:~/booksrc $ cat /var/notes
cat: /var/notes: Permission denied
reader@hacking:~/booksrc $ sudo cat /var/notes
?
this is a test of multiuser notes
reader@hacking:~/booksrc $ sudo hexdump -C /var/notes
00000000 e7 03 00 00
0a 74 68 69 73 20 69 73 20 61 20 74 |.....this is a t|
00000010 65 73 74 20 6f 66 20 6d 75 6c 74 69 75 73 65 72 |est of multiuser|
00000020 20 6e 6f 74 65 73 0a | notes.|
00000027
reader@hacking:~/booksrc $ pcalc 0x03e7
999 0x3e7 0y1111100111
reader@hacking:~/booksrc $
The /var/notes file contains the user ID of reader (999) and the note. Because of little-endian architecture, the 4 bytes of the integer 999 appear reversed in hexadecimal (shown in bold above).
In order for a normal user to be able to read the note data, a corresponding setuid
root program is needed. The notesearch.c program will read the note data and only display the notes written by that user ID. Additionally, an optional command-line argument can be supplied for a search string. When this is used, only notes matching the search string will be displayed.
#include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include "hacking.h" #define FILENAME "/var/notes" int print_notes(int, int, char *); // Note printing function. int find_user_note(int, int); // Seek in file for a note for user. int search_note(char *, char *); // Search for keyword function. void fatal(char *); // Fatal error handler int main(int argc, char *argv[]) { int userid, printing=1, fd; // File descriptor char searchstring[100]; if(argc > 1) // If there is an arg, strcpy(searchstring, argv[1]); // that is the search string; else // otherwise, searchstring[0] = 0; // search string is empty. userid = getuid(); fd = open(FILENAME, O_RDONLY); // Open the file for read-only access. if(fd == -1) fatal("in main() while opening file for reading"); while(printing) printing = print_notes(fd, userid, searchstring); printf("-------[ end of note data ]-------\n"); close(fd); } // A function to print the notes for a given uid that match // an optional search string; // returns 0 at end of file, 1 if there are still more notes. int print_notes(int fd, int uid, char *searchstring) { int note_length; char byte=0, note_buffer[100]; note_length = find_user_note(fd, uid); if(note_length == -1) // If end of file reached, return 0; // return 0. read(fd, note_buffer, note_length); // Read note data. note_buffer[note_length] = 0; // Terminate the string. if(search_note(note_buffer, searchstring)) // If searchstring found, printf(note_buffer); // print the note. return 1; } // A function to find the next note for a given userID; // returns -1 if the end of the file is reached; // otherwise, it returns the length of the found note. int find_user_note(int fd, int user_uid) { int note_uid=-1; unsigned char byte; int length; while(note_uid != user_uid) { // Loop until a note for user_uid is found. if(read(fd, ¬e_uid, 4) != 4) // Read the uid data. return -1; // If 4 bytes aren't read, return end of file code. if(read(fd, &byte, 1) != 1) // Read the newline separator. return -1; byte = length = 0; while(byte != '\n') { // Figure out how many bytes to the end of line. if(read(fd, &byte, 1) != 1) // Read a single byte. return -1; // If byte isn't read, return end of file code. length++; } } lseek(fd, length * -1, SEEK_CUR); // Rewind file reading by length bytes. printf("[DEBUG] found a %d byte note for user id %d\n", length, note_uid); return length; } // A function to search a note for a given keyword; // returns 1 if a match is found, 0 if there is no match. int search_note(char *note, char *keyword) { int i, keyword_length, match=0; keyword_length = strlen(keyword); if(keyword_length == 0) // If there is no search string, return 1; // always "match". for(i=0; i < strlen(note); i++) { // Iterate over bytes in note. if(note[i] == keyword[match]) // If byte matches keyword, match++; // get ready to check the next byte; else { // otherwise, if(note[i] == keyword[0]) // if that byte matches first keyword byte, match = 1; // start the match count at 1. else match = 0; // Otherwise it is zero. } if(match == keyword_length) // If there is a full match, return 1; // return matched. } return 0; // Return not matched. }
Most of this code should make sense, but there are some new concepts. The filename is defined at the top instead of using heap memory. Also, the function lseek()
is used to rewind the read position in the file. The function call of lseek(fd, length * -1, SEEK_CUR);
tells the program to move the read position forward from the current position in the file by length * -1
bytes. Since this turns out to be a negative number, the position is moved backward by length
bytes.
reader@hacking:~/booksrc $ gcc -o notesearch notesearch.c reader@hacking:~/booksrc $ sudo chown root:root ./notesearch reader@hacking:~/booksrc $ sudo chmod u+s ./notesearch reader@hacking:~/booksrc $ ./notesearch [DEBUG] found a 34 byte note for user id 999 this is a test of multiuser notes -------[ end of note data ]------- reader@hacking:~/booksrc $
When compiled and setuid
root, the notesearch program works as expected. But this is just a single user; what happens if a different user uses the notetaker and notesearch programs?
reader@hacking:~/booksrc $ sudo su jose jose@hacking:/home/reader/booksrc $ ./notetaker "This is a note for jose" [DEBUG] buffer @ 0x804a008: 'This is a note for jose' [DEBUG] datafile @ 0x804a070: '/var/notes' [DEBUG] file descriptor is 3 Note has been saved. jose@hacking:/home/reader/booksrc $ ./notesearch [DEBUG] found a 24 byte note for user id 501 This is a note for jose -------[ end of note data ]------- jose@hacking:/home/reader/booksrc $
When the user jose uses these programs, the real user ID is 501. This means that value is added to all notes written with notetaker, and only notes with a matching user ID will be displayed by the notesearch program.
reader@hacking:~/booksrc $ ./notetaker "This is another note for the reader user" [DEBUG] buffer @ 0x804a008: 'This is another note for the reader user' [DEBUG] datafile @ 0x804a070: '/var/notes' [DEBUG] file descriptor is 3 Note has been saved. reader@hacking:~/booksrc $ ./notesearch [DEBUG] found a 34 byte note for user id 999 this is a test of multiuser notes [DEBUG] found a 41 byte note for user id 999 This is another note for the reader user -------[ end of note data ]------- reader@hacking:~/booksrc $
Similarly, all notes for the user reader have the user ID 999 attached to them. Even though both the notetaker and notesearch programs are suid
root and have full read and write access to the /var/notes datafile, the program logic in the notesearch program prevents the current user from viewing other users' notes. This is very similar to how the /etc/passwd file stores user information for all users, yet programs like chsh
and passwd
allow any user to change his own shell or password.
Sometimes there are multiple variables that should be grouped together and treated like one. In C, structs are variables that can contain many other variables. Structs are often used by various system functions and libraries, so understanding how to use structs is a prerequisite to using these functions.
A simple example will suffice for now. When dealing with many time functions, these functions use a time struct called tm
, which is defined in /usr/include/time.h. The struct's definition is as follows.
struct tm { int tm_sec; /* seconds */ int tm_min; /* minutes */ int tm_hour; /* hours */ int tm_mday; /* day of the month */ int tm_mon; /* month */ int tm_year; /* year */ int tm_wday; /* day of the week */ int tm_yday; /* day in the year */ int tm_isdst; /* daylight saving time */ };
After this struct is defined, struct tm
becomes a usable variable type, which can be used to declare variables and pointers with the data type of the tm
struct. The time_example.c program demonstrates this. When time.h
is included, the tm
struct is defined, which is later used to declare the current_time
and time_ptr
variables.
#include <stdio.h> #include <time.h> int main() { long int seconds_since_epoch; struct tm current_time, *time_ptr; int hour, minute, second, day, month, year; seconds_since_epoch = time(0); // Pass time a null pointer as argument. printf("time() - seconds since epoch: %ld\n", seconds_since_epoch); time_ptr = ¤t_time; // Set time_ptr to the address of // the current_time struct. localtime_r(&seconds_since_epoch, time_ptr); // Three different ways to access struct elements: hour = current_time.tm_hour; // Direct access minute = time_ptr->tm_min; // Access via pointer second = *((int *) time_ptr); // Hacky pointer access printf("Current time is: %02d:%02d:%02d\n", hour, minute, second); }
The time()
function will return the number of seconds since January 1, 1970. Time on Unix systems is kept relative to this rather arbitrary point in time, which is also known as the epoch. The localtime_r()
function expects two pointers as arguments: one to the number of seconds since epoch and the other to a tm
struct. The pointer time_ptr
has already been set to the address of current_time
, an empty tm
struct. The address-of operator is used to provide a pointer to seconds_since_epoch
for the other argument to localtime_r()
, which fills the elements of the tm
struct. The elements of structs can be accessed in three different ways; the first two are the proper ways to access struct elements, and the third is a hacked solution. If a struct variable is used, its elements can be accessed by adding the elements' names to the end of the variable name with a period. Therefore, current_time.tm_hour
will access just the tm_hour
element of the tm
struct called current_time
. Pointers to structs are often used, since it is much more efficient to pass a four-byte pointer than an entire data structure. Struct pointers are so common that C has a built-in method to access struct elements from a struct pointer without needing to dereference the pointer. When using a struct pointer like time_ptr
, struct elements can be similarly accessed by the struct element's name, but using a series of characters that looks like an arrow pointing right. Therefore, time_ptr->tm_min
will access the tm_min
element of the tm
struct that is pointed to by time_ptr
. The seconds could be accessed via either of these proper methods, using the tm_sec
element or the tm
struct, but a third method is used. Can you figure out how this third method works?
reader@hacking:~/booksrc $ gcc time_example.c reader@hacking:~/booksrc $ ./a.out time() - seconds since epoch: 1189311588 Current time is: 04:19:48 reader@hacking:~/booksrc $ ./a.out time() - seconds since epoch: 1189311600 Current time is: 04:20:00 reader@hacking:~/booksrc $
The program works as expected, but how are the seconds being accessed in the tm
struct? Remember that in the end, it's all just memory. Since tm_sec
is defined at the beginning of the tm
struct, that integer value is also found at the beginning. In the line second = *((int *) time_ptr)
, the variable time_ptr
is typecast from a tm
struct pointer to an integer pointer. Then this typecast pointer is dereferenced, returning the data at the pointer's address. Since the address to the tm
struct also points to the first element of this struct, this will retrieve the integer value for tm_sec
in the struct. The following addition to the time_example.c code (time_example2.c) also dumps the bytes of the current_time
. This shows that the elements of tm
struct are right next to each other in memory. The elements further down in the struct can also be directly accessed with pointers by simply adding to the address of the pointer.
#include <stdio.h> #include <time.h> void dump_time_struct_bytes(struct tm *time_ptr, int size) { int i; unsigned char *raw_ptr; printf("bytes of struct located at 0x%08x\n", time_ptr); raw_ptr = (unsigned char *) time_ptr; for(i=0; i < size; i++) { printf("%02x ", raw_ptr[i]); if(i%16 == 15) // Print a newline every 16 bytes. printf("\n"); } printf("\n"); } int main() { long int seconds_since_epoch; struct tm current_time, *time_ptr; int hour, minute, second, i, *int_ptr; seconds_since_epoch = time(0); // Pass time a null pointer as argument. printf("time() - seconds since epoch: %ld\n", seconds_since_epoch); time_ptr = ¤t_time; // Set time_ptr to the address of // the current_time struct. localtime_r(&seconds_since_epoch, time_ptr); // Three different ways to access struct elements: hour = current_time.tm_hour; // Direct access minute = time_ptr->tm_min; // Access via pointer second = *((int *) time_ptr); // Hacky pointer access printf("Current time is: %02d:%02d:%02d\n", hour, minute, second); dump_time_struct_bytes(time_ptr, sizeof(struct tm)); minute = hour = 0; // Clear out minute and hour. int_ptr = (int *) time_ptr; for(i=0; i < 3; i++) { printf("int_ptr @ 0x%08x : %d\n", int_ptr, *int_ptr); int_ptr++; // Adding 1 to int_ptr adds 4 to the address, } // since an int is 4 bytes in size. }
The results of compiling and executing time_example2.c are as follows.
reader@hacking:~/booksrc $ gcc -g time_example2.c reader@hacking:~/booksrc $ ./a.out time() - seconds since epoch: 1189311744 Current time is: 04:22:24 bytes of struct located at 0xbffff7f0 18 00 00 00 16 00 00 00 04 00 00 00 09 00 00 00 08 00 00 00 6b 00 00 00 00 00 00 00 fb 00 00 00 00 00 00 00 00 00 00 00 28 a0 04 08 int_ptr @ 0xbffff7f0 : 24 int_ptr @ 0xbffff7f4 : 22 int_ptr @ 0xbffff7f8 : 4 reader@hacking:~/booksrc $
While struct memory can be accessed this way, assumptions are made about the type of variables in the struct and the lack of any padding between variables. Since the data types of a struct's elements are also stored in the struct, using proper methods to access struct elements is much easier.
A pointer simply contains a memory address and is given a data type that describes where it points. Usually, pointers are used for variables; however, they can also be used for functions. The funcptr_example.c program demonstrates the use of function pointers.
#include <stdio.h> int func_one() { printf("This is function one\n"); return 1; } int func_two() { printf("This is function two\n"); return 2; } int main() { int value; int (*function_ptr) (); function_ptr = func_one; printf("function_ptr is 0x%08x\n", function_ptr); value = function_ptr(); printf("value returned was %d\n", value); function_ptr = func_two; printf("function_ptr is 0x%08x\n", function_ptr); value = function_ptr(); printf("value returned was %d\n", value); }
In this program, a function pointer aptly named function_ptr
is declared in main()
. This pointer is then set to point at the function func_one()
and is called; then it is set again and used to call func_two()
. The output below shows the compilation and execution of this source code.
reader@hacking:~/booksrc $ gcc funcptr_example.c reader@hacking:~/booksrc $ ./a.out function_ptr is 0x08048374 This is function one value returned was 1 function_ptr is 0x0804838d This is function two value returned was 2 reader@hacking:~/booksrc $
Since computers are deterministic machines, it is impossible for them to produce truly random numbers. But many applications require some form of randomness. The pseudo-random number generator functions fill this need by generating a stream of numbers that is pseudo-random. These functions can produce a seemingly random sequence of numbers started from a seed number; however, the same exact sequence can be generated again with the same seed. Deterministic machines cannot produce true randomness, but if the seed value of the pseudo-random generation function isn't known, the sequence will seem random. The generator must be seeded with a value using the function srand()
, and from that point on, the function rand()
will return a pseudo-random number from 0 to RAND_MAX
. These functions and RAND_MAX
are defined in stdlib.h. While the numbers rand()
returns will appear to be random, they are dependent on the seed value provided to srand()
. To maintain pseudo-randomness between subsequent program executions, the randomizer must be seeded with a different value each time. One common practice is to use the number of seconds since epoch (returned from the time()
function) as the seed. The rand_example.c program demonstrates this technique.
#include <stdio.h> #include <stdlib.h> int main() { int i; printf("RAND_MAX is %u\n", RAND_MAX); srand(time(0)); printf("random values from 0 to RAND_MAX\n"); for(i=0; i < 8; i++) printf("%d\n", rand()); printf("random values from 1 to 20\n"); for(i=0; i < 8; i++) printf("%d\n", (rand()%20)+1); }
Notice how the modulus operator is used to obtain random values from 1 to 20.
reader@hacking:~/booksrc $ gcc rand_example.c reader@hacking:~/booksrc $ ./a.out RAND_MAX is 2147483647 random values from 0 to RAND_MAX 815015288 1315541117 2080969327 450538726 710528035 907694519 1525415338 1843056422 random values from 1 to 20 2 3 8 5 9 1 4 20 reader@hacking:~/booksrc $ ./a.out RAND_MAX is 2147483647 random values from 0 to RAND_MAX 678789658 577505284 1472754734 2134715072 1227404380 1746681907 341911720 93522744 random values from 1 to 20 6 16 12 19 8 19 2 1 reader@hacking:~/booksrc $
The program's output just displays random numbers. Pseudo-randomness can also be used for more complex programs, as you will see in this section's final script.
The final program in this section is a set of games of chance that use many of the concepts we've discussed. The program uses pseudo-random number generator functions to provide the element of chance. It has three different game functions, which are called using a single global function pointer, and it uses structs to hold data for the player, which is saved in a file. Multi-user file permissions and user IDs allow multiple users to play and maintain their own account data. The game_of_chance.c program code is heavily documented, and you should be able to understand it at this point.
#include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <time.h> #include <stdlib.h> #include "hacking.h" #define DATAFILE "/var/chance.data" // File to store user data // Custom user struct to store information about users struct user { int uid; int credits; int highscore; char name[100]; int (*current_game) (); }; // Function prototypes int get_player_data(); void register_new_player(); void update_player_data(); void show_highscore(); void jackpot(); void input_name(); void print_cards(char *, char *, int); int take_wager(int, int); void play_the_game(); int pick_a_number(); int dealer_no_match(); int find_the_ace(); void fatal(char *); // Global variables struct user player; // Player struct int main() { int choice, last_game; srand(time(0)); // Seed the randomizer with the current time. if(get_player_data() == -1) // Try to read player data from file. register_new_player(); // If there is no data, register a new player. while(choice != 7) { printf("-=[ Game of Chance Menu ]=-\n"); printf("1 - Play the Pick a Number game\n"); printf("2 - Play the No Match Dealer game\n"); printf("3 - Play the Find the Ace game\n"); printf("4 - View current high score\n"); printf("5 - Change your user name\n"); printf("6 - Reset your account at 100 credits\n"); printf("7 - Quit\n"); printf("[Name: %s]\n", player.name); printf("[You have %u credits] -> ", player.credits); scanf("%d", &choice); if((choice < 1) || (choice > 7)) printf("\n[!!] The number %d is an invalid selection.\n\n", choice); else if (choice < 4) { // Otherwise, choice was a game of some sort. if(choice != last_game) { // If the function ptr isn't set if(choice == 1) // then point it at the selected game player.current_game = pick_a_number; else if(choice == 2) player.current_game = dealer_no_match; else player.current_game = find_the_ace; last_game = choice; // and set last_game. } play_the_game(); // Play the game. } else if (choice == 4) show_highscore(); else if (choice == 5) { printf("\nChange user name\n"); printf("Enter your new name: "); input_name(); printf("Your name has been changed.\n\n"); } else if (choice == 6) { printf("\nYour account has been reset with 100 credits.\n\n"); player.credits = 100; } } update_player_data(); printf("\nThanks for playing! Bye.\n"); } // This function reads the player data for the current uid // from the file. It returns -1 if it is unable to find player // data for the current uid. int get_player_data() { int fd, uid, read_bytes; struct user entry; uid = getuid(); fd = open(DATAFILE, O_RDONLY); if(fd == -1) // Can't open the file, maybe it doesn't exist return -1; read_bytes = read(fd, &entry, sizeof(struct user)); // Read the first chunk. while(entry.uid != uid && read_bytes > 0) { // Loop until proper uid is found. read_bytes = read(fd, &entry, sizeof(struct user)); // Keep reading. } close(fd); // Close the file. if(read_bytes < sizeof(struct user)) // This means that the end of file was reached. return -1; else player = entry; // Copy the read entry into the player struct. return 1; // Return a success. } // This is the new user registration function. // It will create a new player account and append it to the file. void register_new_player() { int fd; printf("-=-={ New Player Registration }=-=-\n"); printf("Enter your name: "); input_name(); player.uid = getuid(); player.highscore = player.credits = 100; fd = open(DATAFILE, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(fd == -1) fatal("in register_new_player() while opening file"); write(fd, &player, sizeof(struct user)); close(fd); printf("\nWelcome to the Game of Chance %s.\n", player.name); printf("You have been given %u credits.\n", player.credits); } // This function writes the current player data to the file. // It is used primarily for updating the credits after games. void update_player_data() { int fd, i, read_uid; char burned_byte; fd = open(DATAFILE, O_RDWR); if(fd == -1) // If open fails here, something is really wrong. fatal("in update_player_data() while opening file"); read(fd, &read_uid, 4); // Read the uid from the first struct. while(read_uid != player.uid) { // Loop until correct uid is found. for(i=0; i < sizeof(struct user) - 4; i++) // Read through the read(fd, &burned_byte, 1); // rest of that struct. read(fd, &read_uid, 4); // Read the uid from the next struct. } write(fd, &(player.credits), 4); // Update credits. write(fd, &(player.highscore), 4); // Update highscore. write(fd, &(player.name), 100); // Update name. close(fd); } // This function will display the current high score and // the name of the person who set that high score. void show_highscore() { unsigned int top_score = 0; char top_name[100]; struct user entry; int fd; printf("\n====================| HIGH SCORE |====================\n"); fd = open(DATAFILE, O_RDONLY); if(fd == -1) fatal("in show_highscore() while opening file"); while(read(fd, &entry, sizeof(struct user)) > 0) { // Loop until end of file. if(entry.highscore > top_score) { // If there is a higher score, top_score = entry.highscore; // set top_score to that score strcpy(top_name, entry.name); // and top_name to that username. } } close(fd); if(top_score > player.highscore) printf("%s has the high score of %u\n", top_name, top_score); else printf("You currently have the high score of %u credits!\n", player.highscore); printf("======================================================\n\n"); } // This function simply awards the jackpot for the Pick a Number game. void jackpot() { printf("*+*+*+*+*+* JACKPOT *+*+*+*+*+*\n"); printf("You have won the jackpot of 100 credits!\n"); player.credits += 100; } // This function is used to input the player name, since // scanf("%s", &whatever) will stop input at the first space. void input_name() { char *name_ptr, input_char='\n'; while(input_char == '\n') // Flush any leftover scanf("%c", &input_char); // newline chars. name_ptr = (char *) &(player.name); // name_ptr = player name's address while(input_char != '\n') { // Loop until newline. *name_ptr = input_char; // Put the input char into name field. scanf("%c", &input_char); // Get the next char. name_ptr++; // Increment the name pointer. } *name_ptr = 0; // Terminate the string. } // This function prints the 3 cards for the Find the Ace game. // It expects a message to display, a pointer to the cards array, // and the card the user has picked as input. If the user_pick is // -1, then the selection numbers are displayed. void print_cards(char *message, char *cards, int user_pick) { int i; printf("\n\t*** %s ***\n", message); printf(" \t._.\t._.\t._.\n"); printf("Cards:\t|%c|\t|%c|\t|%c|\n\t", cards[0], cards[1], cards[2]); if(user_pick == -1) printf(" 1 \t 2 \t 3\n"); else { for(i=0; i < user_pick; i++) printf("\t"); printf(" ^-- your pick\n"); } } // This function inputs wagers for both the No Match Dealer and // Find the Ace games. It expects the available credits and the // previous wager as arguments. The previous_wager is only important // for the second wager in the Find the Ace game. The function // returns -1 if the wager is too big or too little, and it returns // the wager amount otherwise. int take_wager(int available_credits, int previous_wager) { int wager, total_wager; printf("How many of your %d credits would you like to wager? ", available_credits); scanf("%d", &wager); if(wager < 1) { // Make sure the wager is greater than 0. printf("Nice try, but you must wager a positive number!\n"); return -1; } total_wager = previous_wager + wager; if(total_wager > available_credits) { // Confirm available credits printf("Your total wager of %d is more than you have!\n", total_wager); printf("You only have %d available credits, try again.\n", available_credits); return -1; } return wager; } // This function contains a loop to allow the current game to be // played again. It also writes the new credit totals to file // after each game is played. void play_the_game() { int play_again = 1; int (*game) (); char selection; while(play_again) { printf("\n[DEBUG] current_game pointer @ 0x%08x\n", player.current_game); if(player.current_game() != -1) { // If the game plays without error and if(player.credits > player.highscore) // a new high score is set, player.highscore = player.credits; // update the highscore. printf("\nYou now have %u credits\n", player.credits); update_player_data(); // Write the new credit total to file. printf("Would you like to play again? (y/n) "); selection = '\n'; while(selection == '\n') // Flush any extra newlines. scanf("%c", &selection); if(selection == 'n') play_again = 0; } else // This means the game returned an error, play_again = 0; // so return to main menu. } } // This function is the Pick a Number game. // It returns -1 if the player doesn't have enough credits. int pick_a_number() { int pick, winning_number; printf("\n####### Pick a Number ######\n"); printf("This game costs 10 credits to play. Simply pick a number\n"); printf("between 1 and 20, and if you pick the winning number, you\n"); printf("will win the jackpot of 100 credits!\n\n"); winning_number = (rand() % 20) + 1; // Pick a number between 1 and 20. if(player.credits < 10) { printf("You only have %d credits. That's not enough to play!\n\n", player.credits); return -1; // Not enough credits to play } player.credits -= 10; // Deduct 10 credits. printf("10 credits have been deducted from your account.\n"); printf("Pick a number between 1 and 20: "); scanf("%d", &pick); printf("The winning number is %d\n", winning_number); if(pick == winning_number) jackpot(); else printf("Sorry, you didn't win.\n"); return 0; } // This is the No Match Dealer game. // It returns -1 if the player has 0 credits. int dealer_no_match() { int i, j, numbers[16], wager = -1, match = -1; printf("\n::::::: No Match Dealer :::::::\n"); printf("In this game, you can wager up to all of your credits.\n"); printf("The dealer will deal out 16 random numbers between 0 and 99.\n"); printf("If there are no matches among them, you double your money!\n\n"); if(player.credits == 0) { printf("You don't have any credits to wager!\n\n"); return -1; } while(wager == -1) wager = take_wager(player.credits, 0); printf("\t\t::: Dealing out 16 random numbers :::\n"); for(i=0; i < 16; i++) { numbers[i] = rand() % 100; // Pick a number between 0 and 99. printf("%2d\t", numbers[i]); if(i%8 == 7) // Print a line break every 8 numbers. printf("\n"); } for(i=0; i < 15; i++) { // Loop looking for matches. j = i + 1; while(j < 16) { if(numbers[i] == numbers[j]) match = numbers[i]; j++; } } if(match != -1) { printf("The dealer matched the number %d!\n", match); printf("You lose %d credits.\n", wager); player.credits -= wager; } else { printf("There were no matches! You win %d credits!\n", wager); player.credits += wager; } return 0; } // This is the Find the Ace game. // It returns -1 if the player has 0 credits. int find_the_ace() { int i, ace, total_wager; int invalid_choice, pick = -1, wager_one = -1, wager_two = -1; char choice_two, cards[3] = {'X', 'X', 'X'}; ace = rand()%3; // Place the ace randomly. printf("******* Find the Ace *******\n"); printf("In this game, you can wager up to all of your credits.\n"); printf("Three cards will be dealt out, two queens and one ace.\n"); printf("If you find the ace, you will win your wager.\n"); printf("After choosing a card, one of the queens will be revealed.\n"); printf("At this point, you may either select a different card or\n"); printf("increase your wager.\n\n"); if(player.credits == 0) { printf("You don't have any credits to wager!\n\n"); return -1; } while(wager_one == -1) // Loop until valid wager is made. wager_one = take_wager(player.credits, 0); print_cards("Dealing cards", cards, -1); pick = -1; while((pick < 1) || (pick > 3)) { // Loop until valid pick is made. printf("Select a card: 1, 2, or 3 "); scanf("%d", &pick); } pick--; // Adjust the pick since card numbering starts at 0. i=0; while(i == ace || i == pick) // Keep looping until i++; // we find a valid queen to reveal. cards[i] = 'Q'; print_cards("Revealing a queen", cards, pick); invalid_choice = 1; while(invalid_choice) { // Loop until valid choice is made. printf("Would you like to:\n[c]hange your pick\tor\t[i]ncrease your wager?\n"); printf("Select c or i: "); choice_two = '\n'; while(choice_two == '\n') // Flush extra newlines. scanf("%c", &choice_two); if(choice_two == 'i') { // Increase wager. invalid_choice=0; // This is a valid choice. while(wager_two == -1) // Loop until valid second wager is made. wager_two = take_wager(player.credits, wager_one); } if(choice_two == 'c') { // Change pick. i = invalid_choice = 0; // Valid choice while(i == pick || cards[i] == 'Q') // Loop until the other card i++; // is found, pick = i; // and then swap pick. printf("Your card pick has been changed to card %d\n", pick+1); } } for(i=0; i < 3; i++) { // Reveal all of the cards. if(ace == i) cards[i] = 'A'; else cards[i] = 'Q'; } print_cards("End result", cards, pick); if(pick == ace) { // Handle win. printf("You have won %d credits from your first wager\n", wager_one); player.credits += wager_one; if(wager_two != -1) { printf("and an additional %d credits from your second wager!\n", wager_two); player.credits += wager_two; } } else { // Handle loss. printf("You have lost %d credits from your first wager\n", wager_one); player.credits -= wager_one; if(wager_two != -1) { printf("and an additional %d credits from your second wager!\n", wager_two); player.credits -= wager_two; } } return 0; }
Since this is a multi-user program that writes to a file in the /var directory, it must be suid root.
reader@hacking:~/booksrc $ gcc -o game_of_chance game_of_chance.c reader@hacking:~/booksrc $ sudo chown root:root ./game_of_chance reader@hacking:~/booksrc $ sudo chmod u+s ./game_of_chance reader@hacking:~/booksrc $ ./game_of_chance -=-={ New Player Registration }=-=- Enter your name: Jon Erickson Welcome to the Game of Chance, Jon Erickson. You have been given 100 credits. -=[ Game of Chance Menu ]=- 1 - Play the Pick a Number game 2 - Play the No Match Dealer game 3 - Play the Find the Ace game 4 - View current high score 5 - Change your username 6 - Reset your account at 100 credits 7 - Quit [Name: Jon Erickson] [You have 100 credits] -> 1 [DEBUG] current_game pointer @ 0x08048e6e ####### Pick a Number ###### This game costs 10 credits to play. Simply pick a number between 1 and 20, and if you pick the winning number, you will win the jackpot of 100 credits! 10 credits have been deducted from your account. Pick a number between 1 and 20: 7 The winning number is 14. Sorry, you didn't win. You now have 90 credits. Would you like to play again? (y/n) n -=[ Game of Chance Menu ]=- 1 - Play the Pick a Number game 2 - Play the No Match Dealer game 3 - Play the Find the Ace game 4 - View current high score 5 - Change your username 6 - Reset your account at 100 credits 7 - Quit [Name: Jon Erickson] [You have 90 credits] -> 2 [DEBUG] current_game pointer @ 0x08048f61 ::::::: No Match Dealer ::::::: In this game you can wager up to all of your credits. The dealer will deal out 16 random numbers between 0 and 99. If there are no matches among them, you double your money! How many of your 90 credits would you like to wager? 30 ::: Dealing out 16 random numbers ::: 88 68 82 51 21 73 80 50 11 64 78 85 39 42 40 95 There were no matches! You win 30 credits! You now have 120 credits Would you like to play again? (y/n) n -=[ Game of Chance Menu ]=- 1 - Play the Pick a Number game 2 - Play the No Match Dealer game 3 - Play the Find the Ace game 4 - View current high score 5 - Change your username 6 - Reset your account at 100 credits 7 - Quit [Name: Jon Erickson] [You have 120 credits] -> 3 [DEBUG] current_game pointer @ 0x0804914c ******* Find the Ace ******* In this game you can wager up to all of your credits. Three cards will be dealt: two queens and one ace. If you find the ace, you will win your wager. After choosing a card, one of the queens will be revealed. At this point you may either select a different card or increase your wager. How many of your 120 credits would you like to wager? 50 *** Dealing cards *** ._. ._. ._. Cards: |X| |X| |X| 1 2 3 Select a card: 1, 2, or 3: 2 *** Revealing a queen *** ._. ._. ._. Cards: |X| |X| |Q| ^-- your pick Would you like to [c]hange your pick or [i]ncrease your wager? Select c or i: c Your card pick has been changed to card 1. *** End result *** ._. ._. ._. Cards: |A| |Q| |Q| ^-- your pick You have won 50 credits from your first wager. You now have 170 credits. Would you like to play again? (y/n) n -=[ Game of Chance Menu ]=- 1 - Play the Pick a Number game 2 - Play the No Match Dealer game 3 - Play the Find the Ace game 4 - View current high score 5 - Change your username 6 - Reset your account at 100 credits 7 - Quit [Name: Jon Erickson] [You have 170 credits] -> 4 ====================| HIGH SCORE |==================== You currently have the high score of 170 credits! ====================================================== -=[ Game of Chance Menu ]=- 1 - Play the Pick a Number game 2 - Play the No Match Dealer game 3 - Play the Find the Ace game 4 - View current high score 5 - Change your username 6 - Reset your account at 100 credits 7 - Quit [Name: Jon Erickson] [You have 170 credits] -> 7 Thanks for playing! Bye. reader@hacking:~/booksrc $ sudo su jose jose@hacking:/home/reader/booksrc $ ./game_of_chance -=-={ New Player Registration }=-=- Enter your name: Jose Ronnick Welcome to the Game of Chance Jose Ronnick. You have been given 100 credits. -=[ Game of Chance Menu ]=- 1 - Play the Pick a Number game 2 - Play the No Match Dealer game 3 - Play the Find the Ace game 4 - View current high score 5 - Change your username 6 - Reset your account at 100 credits 7 - Quit [Name: Jose Ronnick] [You have 100 credits] -> 4 ====================| HIGH SCORE |==================== Jon Erickson has the high score of 170. ====================================================== -=[ Game of Chance Menu ]=- 1 - Play the Pick a Number game 2 - Play the No Match Dealer game 3 - Play the Find the Ace game 4 - View current high score 5 - Change your username 6 - Reset your account at 100 credits 7 - Quit [Name: Jose Ronnick] [You have 100 credits] -> 7 Thanks for playing! Bye. jose@hacking:~/booksrc $ exit exit reader@hacking:~/booksrc $
Play around with this program a little bit. The Find the Ace game is a demonstration of a principle of conditional probability; although it is counterintuitive, changing your pick will increase your chances of finding the ace from 33 percent to 50 percent. Many people have difficulty understanding this truth—that's why it's counterintuitive. The secret of hacking is understanding little-known truths like this and using them to produce seemingly magical results.